feat: initial
This commit is contained in:
commit
6ab04636a3
9 changed files with 298 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
node_modules
|
||||
out.pdf
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"editor.formatOnSave": true
|
||||
}
|
4
README.md
Normal file
4
README.md
Normal 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
166
glyphs.ts
Normal 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
14
index.ts
Normal 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
14
package.json
Normal 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
22
pdf.ts
Normal 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
66
pnpm-lock.yaml
generated
Normal 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
7
utils.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
const AbortController2 = class extends AbortController {
|
||||
constructor() {
|
||||
super();
|
||||
this.abort = this.abort.bind(this);
|
||||
}
|
||||
};
|
||||
export { AbortController2 as AbortController };
|
Loading…
Add table
Reference in a new issue