feat: initial

This commit is contained in:
m93a 2025-02-01 16:53:12 +01:00
commit 6ab04636a3
9 changed files with 298 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
node_modules
out.pdf

3
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"editor.formatOnSave": true
}

4
README.md Normal file
View file

@ -0,0 +1,4 @@
# Psací Stroj Lidu
Proof-of-concept generátor PDF pro písmenkování, s využitím
[Písma lidu](https://nicimesto.cz/font/).

166
glyphs.ts Normal file
View file

@ -0,0 +1,166 @@
import { AbortController } from "./utils.ts";
type LetterUrls = [normal: string, textured: string];
type Letter = LetterUrls | { variants: Partial<LetterUrls>[] };
const b = ([fname]: TemplateStringsArray) =>
`https://nicimesto.cz/wp-content/uploads/2024/03/${fname}.png`;
export const letterMap = {
A: [b`A`, b`A-2`],
Á: [b`A-1`, b`A-3`],
B: [b`B`, b`B-1`],
C: [b`C`, b`C-3`],
Ć: [b`C-1`, b`C-4`],
Č: [b`C-2`, b`C-5`],
CH: { variants: [[b`CH`, b`CH-1`], [b`CH2`]] },
D: [b`D`, b`D-2`],
Ď: [b`D-1`, b`D-3`],
E: [b`E`, b`E-2`],
Ě: [b`E-1`, b`E-3`],
F: {
variants: [
[b`F`, b`F-1`],
[undefined, b`F2`],
],
},
G: [b`G`, b`G-1`],
H: [b`H`, b`H-1`],
I: [b`I`, b`I-2`],
Í: [b`I-1`, b`I-3`],
J: [b`J`, b`J-1`],
K: [b`K`, b`K-1`],
L: {
variants: [
[b`L`, b`L-2`],
[b`L2`, b`L2-1`],
[b`L3`, b`L3-1`],
],
},
Ľ: [b`L-1`, b`L-3`],
M: [b`M`, b`M-1`],
N: [b`N`, b`N-2`],
Ň: [b`N-1`, b`N-2`],
O: [b`O`, b`O-3`],
Ó: {
variants: [
[b`O-1`, b`O-4`],
[undefined, b`O2`],
],
},
Ô: [b`O-2`, b`O-5`],
P: [b`P`, b`P-1`],
Q: {
variants: [
[b`Q`, b`Q-1`],
[undefined, b`Q2`],
],
},
R: [b`R`, b`R-2`],
Ř: [b`R-1`, b`R-3`],
S: [b`S`, b`S-2`],
Š: [b`S-1`, b`S-3`],
T: [b`T`, b`T`], // chybí texturka varianta
Ť: [b`T-1`, b`T-2`],
U: [b`U`, "https://nicimesto.cz/wp-content/uploads/2024/11/U-1.png"],
Ů: [b`U-1`, b`U-3`],
Ü: [b`UU`, b`UU-1`],
V: [b`V`, b`V-1`],
W: [b`W`, b`W-1`],
X: [b`X`, b`X-1`],
Y: [b`Y`, b`Y-1`],
Z: [b`Z`, b`Z-2`],
Ž: [b`Z-1`, b`Z-3`],
} satisfies Record<string, Letter>;
export const symbolMap = {
"❓️": [b`unnamed-file-1`, b`unnamed-file-5`],
"⁉️": [b`unnamed-file`, b`unnamed-file-4`],
"❗️": [b`unnamed-file-2`],
"🐾": [b`TLAPA-1`, b`Tlap`],
"#️⃣": [b`unnamed-file-3`],
":": [
"https://nicimesto.cz/wp-content/uploads/2025/01/Artboard2_12-scaled.jpg",
],
"": [
"https://nicimesto.cz/wp-content/uploads/2025/01/Artboard2_2-scaled.jpg",
],
"=": ["https://nicimesto.cz/wp-content/uploads/2025/01/Artboard2-scaled.jpg"],
"≠": ["https://nicimesto.cz/wp-content/uploads/2025/01/Artboard1-scaled.jpg"],
"1": [
"https://nicimesto.cz/wp-content/uploads/2025/01/Artboard2_3-scaled.jpg",
],
"2": [
"https://nicimesto.cz/wp-content/uploads/2025/01/Artboard2_4-scaled.jpg",
],
"3": [
"https://nicimesto.cz/wp-content/uploads/2025/01/Artboard2_5-scaled.jpg",
],
"4": [
"https://nicimesto.cz/wp-content/uploads/2025/01/Artboard2_6-scaled.jpg",
],
"5": [
"https://nicimesto.cz/wp-content/uploads/2025/01/Artboard2_7-scaled.jpg",
],
"6": [
"https://nicimesto.cz/wp-content/uploads/2025/01/Artboard2_8-scaled.jpg",
],
"7": [
"https://nicimesto.cz/wp-content/uploads/2025/01/Artboard2_9-scaled.jpg",
],
"8": [
"https://nicimesto.cz/wp-content/uploads/2025/01/Artboard2_10-scaled.jpg",
],
"9": [
"https://nicimesto.cz/wp-content/uploads/2024/03/9.png",
"https://nicimesto.cz/wp-content/uploads/2025/01/Artboard2_11-scaled.jpg",
],
"𝄢": [b`BASS`],
} satisfies Record<string, string[]>;
export const validateGlyphUrl = async (url: string) => {
const { signal, abort } = new AbortController();
const { ok, status, statusText, headers } = await fetch(url, { signal });
const mime = headers.get("Content-Type");
if (!ok) throw `HTTP ${status}: ${statusText}; URL: ${url}`;
if (mime !== "image/png")
throw `Unexpected Content-Type: ${mime}; URL: ${url}`;
abort();
};
export const validateAllGlyphs = async () => {
for (const v of Object.values(letterMap)) {
const glyphsOrUndef = "variants" in v ? v.variants.flat() : v;
const glyphs = glyphsOrUndef.filter((g) => g !== undefined);
for (const g of glyphs) await validateGlyphUrl(g);
}
};
export type Glyph = keyof typeof letterMap | keyof typeof symbolMap;
export type Font = "normal" | "textured";
export type GlyphVariant = [Glyph, number];
const variantsOfGlyph = (glyph: Glyph, font: Font): string[] => {
if (glyph in symbolMap) {
return symbolMap[glyph as keyof typeof symbolMap];
}
const fontIndex = font === "normal" ? 0 : 1;
const v = letterMap[glyph as keyof typeof letterMap];
const glyphsByFont: Partial<LetterUrls>[] =
"variants" in v ? v.variants : [v];
return glyphsByFont.map((g) => g[fontIndex]).filter((g) => g !== undefined);
};
export const countGlyphVariants = (glyph: Glyph, font: Font): number => {
return variantsOfGlyph(glyph, font).length;
};
export const glyphVariantToUrl = (
glyphVariant: GlyphVariant,
font: Font
): string => {
const [glyph, variant] = glyphVariant;
return variantsOfGlyph(glyph, font)[variant];
};

14
index.ts Normal file
View file

@ -0,0 +1,14 @@
import { glyphsToPdf } from "./pdf.ts";
import { writeFile } from "node:fs/promises";
await writeFile(
"./out.pdf",
await glyphsToPdf("normal", [
["🐾", 0],
["A", 0],
["H", 0],
["O", 0],
["J", 0],
["❗️", 0],
])
);

14
package.json Normal file
View file

@ -0,0 +1,14 @@
{
"name": "psaci-stroj-lidu",
"version": "1.0.0",
"description": "",
"main": "index.ts",
"keywords": [],
"author": "",
"license": "CC0-1.0",
"packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0",
"dependencies": {
"@types/node": "^22.13.0",
"pdf-lib": "^1.17.1"
}
}

22
pdf.ts Normal file
View file

@ -0,0 +1,22 @@
import { PDFDocument } from "pdf-lib";
import { Font, GlyphVariant, glyphVariantToUrl } from "./glyphs.ts";
export const glyphsToPdf = async (font: Font, glyphs: GlyphVariant[]) => {
const pdf = await PDFDocument.create();
for (const glyph of glyphs) {
const url = glyphVariantToUrl(glyph, font);
const res = await fetch(url);
if (!res.ok) {
throw `Failed to fetch glyph "${glyph[0]}", variant ${glyph[1]}. HTTP ${res.status}: ${res.statusText}; URL: ${url}`;
}
const bytes = await res.arrayBuffer();
const image = await pdf.embedPng(bytes);
const { width, height } = image.scaleToFit(210, 297);
const page = pdf.addPage([210, 297]);
page.drawImage(image, { width, height });
}
return await pdf.save();
};

66
pnpm-lock.yaml generated Normal file
View file

@ -0,0 +1,66 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
'@types/node':
specifier: ^22.13.0
version: 22.13.0
pdf-lib:
specifier: ^1.17.1
version: 1.17.1
packages:
'@pdf-lib/standard-fonts@1.0.0':
resolution: {integrity: sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==}
'@pdf-lib/upng@1.0.1':
resolution: {integrity: sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==}
'@types/node@22.13.0':
resolution: {integrity: sha512-ClIbNe36lawluuvq3+YYhnIN2CELi+6q8NpnM7PYp4hBn/TatfboPgVSm2rwKRfnV2M+Ty9GWDFI64KEe+kysA==}
pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
pdf-lib@1.17.1:
resolution: {integrity: sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==}
tslib@1.14.1:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
undici-types@6.20.0:
resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==}
snapshots:
'@pdf-lib/standard-fonts@1.0.0':
dependencies:
pako: 1.0.11
'@pdf-lib/upng@1.0.1':
dependencies:
pako: 1.0.11
'@types/node@22.13.0':
dependencies:
undici-types: 6.20.0
pako@1.0.11: {}
pdf-lib@1.17.1:
dependencies:
'@pdf-lib/standard-fonts': 1.0.0
'@pdf-lib/upng': 1.0.1
pako: 1.0.11
tslib: 1.14.1
tslib@1.14.1: {}
undici-types@6.20.0: {}

7
utils.ts Normal file
View file

@ -0,0 +1,7 @@
const AbortController2 = class extends AbortController {
constructor() {
super();
this.abort = this.abort.bind(this);
}
};
export { AbortController2 as AbortController };