diff --git a/assets/fonts.json b/assets/fonts.json new file mode 100644 index 00000000..2bcc81a3 --- /dev/null +++ b/assets/fonts.json @@ -0,0 +1,638 @@ +{ + "version": 1, + "baseUrl": "https://github.com/jpirnay/crosspoint-reader/blob/master/assets/sd-fonts/", + "families": [ + { + "name": "Alegreya", + "description": "Calligraphic serif/display (Latin, Greek, Cyrillic)", + "styles": [ + "regular", + "bold", + "italic", + "bolditalic" + ], + "files": [ + { + "name": "Alegreya_10.cpfont", + "size": 314326 + }, + { + "name": "Alegreya_12.cpfont", + "size": 393854 + }, + { + "name": "Alegreya_14.cpfont", + "size": 485840 + }, + { + "name": "Alegreya_16.cpfont", + "size": 595370 + } + ] + }, + { + "name": "AtkinsonHyperlegibleNext", + "description": "Accessibility font for low vision (Latin)", + "styles": [ + "regular", + "bold", + "italic", + "bolditalic" + ], + "files": [ + { + "name": "AtkinsonHyperlegibleNext_10.cpfont", + "size": 118633 + }, + { + "name": "AtkinsonHyperlegibleNext_12.cpfont", + "size": 140769 + }, + { + "name": "AtkinsonHyperlegibleNext_14.cpfont", + "size": 166585 + }, + { + "name": "AtkinsonHyperlegibleNext_16.cpfont", + "size": 194397 + }, + { + "name": "AtkinsonHyperlegibleNext_18.cpfont", + "size": 230872 + } + ] + }, + { + "name": "Bitter", + "description": "Slab serif designed for e-ink (Latin, Cyrillic)", + "styles": [ + "regular", + "bold", + "italic", + "bolditalic" + ], + "files": [ + { + "name": "Bitter_10.cpfont", + "size": 283762 + }, + { + "name": "Bitter_12.cpfont", + "size": 352284 + }, + { + "name": "Bitter_14.cpfont", + "size": 430628 + }, + { + "name": "Bitter_16.cpfont", + "size": 517782 + } + ] + }, + { + "name": "Gelasio", + "description": "Readable old-style serif (Latin, Cyrillic)", + "styles": [ + "regular", + "bold", + "italic", + "bolditalic" + ], + "files": [ + { + "name": "Gelasio_10.cpfont", + "size": 214500 + }, + { + "name": "Gelasio_12.cpfont", + "size": 280544 + }, + { + "name": "Gelasio_14.cpfont", + "size": 357582 + }, + { + "name": "Gelasio_16.cpfont", + "size": 447480 + } + ] + }, + { + "name": "GentiumBookPlus", + "description": "Scholarly serif with wide Unicode coverage (Latin, Greek, Cyrillic, IPA)", + "styles": [ + "regular", + "bold", + "italic", + "bolditalic" + ], + "files": [ + { + "name": "GentiumBookPlus_10.cpfont", + "size": 398510 + }, + { + "name": "GentiumBookPlus_12.cpfont", + "size": 519063 + }, + { + "name": "GentiumBookPlus_14.cpfont", + "size": 645720 + } + ] + }, + { + "name": "IBMPlexMono", + "description": "Monospace for code and technical reading (Latin, Greek, Cyrillic)", + "styles": [ + "regular", + "bold", + "italic", + "bolditalic" + ], + "files": [ + { + "name": "IBMPlexMono_10.cpfont", + "size": 165232 + }, + { + "name": "IBMPlexMono_12.cpfont", + "size": 210770 + }, + { + "name": "IBMPlexMono_14.cpfont", + "size": 267380 + }, + { + "name": "IBMPlexMono_16.cpfont", + "size": 332010 + }, + { + "name": "IBMPlexMono_18.cpfont", + "size": 425525 + } + ] + }, + { + "name": "IBMPlexSans", + "description": "IBM corporate sans-serif (Latin, Greek, Cyrillic)", + "styles": [ + "regular", + "bold", + "italic", + "bolditalic" + ], + "files": [ + { + "name": "IBMPlexSans_10.cpfont", + "size": 346854 + }, + { + "name": "IBMPlexSans_12.cpfont", + "size": 402760 + }, + { + "name": "IBMPlexSans_14.cpfont", + "size": 467012 + }, + { + "name": "IBMPlexSans_16.cpfont", + "size": 544046 + }, + { + "name": "IBMPlexSans_18.cpfont", + "size": 658695 + } + ] + }, + { + "name": "IBMPlexSerif", + "description": "Professional serif (Latin, Greek, Cyrillic)", + "styles": [ + "regular", + "bold", + "italic", + "bolditalic" + ], + "files": [ + { + "name": "IBMPlexSerif_10.cpfont", + "size": 261436 + }, + { + "name": "IBMPlexSerif_12.cpfont", + "size": 311135 + }, + { + "name": "IBMPlexSerif_14.cpfont", + "size": 371513 + }, + { + "name": "IBMPlexSerif_16.cpfont", + "size": 441596 + }, + { + "name": "IBMPlexSerif_18.cpfont", + "size": 542532 + } + ] + }, + { + "name": "Inter", + "description": "Modern sans-serif (Latin, Greek, Cyrillic)", + "styles": [ + "regular", + "bold", + "italic", + "bolditalic" + ], + "files": [ + { + "name": "Inter_10.cpfont", + "size": 530150 + }, + { + "name": "Inter_12.cpfont", + "size": 659788 + }, + { + "name": "Inter_14.cpfont", + "size": 807250 + } + ] + }, + { + "name": "LexicaUltralegible", + "description": "Accessibility font for low vision / dyslexia (Latin)", + "styles": [ + "regular", + "bold", + "italic", + "bolditalic" + ], + "files": [ + { + "name": "LexicaUltralegible_10.cpfont", + "size": 155718 + }, + { + "name": "LexicaUltralegible_12.cpfont", + "size": 195978 + }, + { + "name": "LexicaUltralegible_14.cpfont", + "size": 244278 + }, + { + "name": "LexicaUltralegible_16.cpfont", + "size": 297862 + }, + { + "name": "LexicaUltralegible_18.cpfont", + "size": 358357 + } + ] + }, + { + "name": "Literata", + "description": "Screen-optimized serif (Latin, Greek, Cyrillic)", + "styles": [ + "regular", + "bold", + "italic", + "bolditalic" + ], + "files": [ + { + "name": "Literata_10.cpfont", + "size": 497119 + }, + { + "name": "Literata_12.cpfont", + "size": 583306 + }, + { + "name": "Literata_14.cpfont", + "size": 691897 + } + ] + }, + { + "name": "Lora", + "description": "Calligraphic serif for literary reading (Latin, Cyrillic)", + "styles": [ + "regular", + "bold", + "italic", + "bolditalic" + ], + "files": [ + { + "name": "Lora_10.cpfont", + "size": 244674 + }, + { + "name": "Lora_12.cpfont", + "size": 302601 + }, + { + "name": "Lora_14.cpfont", + "size": 369710 + }, + { + "name": "Lora_16.cpfont", + "size": 453301 + } + ] + }, + { + "name": "Merriweather", + "description": "Warm serif for long-form reading (Latin, Cyrillic)", + "styles": [ + "regular", + "bold", + "italic", + "bolditalic" + ], + "files": [ + { + "name": "Merriweather_10.cpfont", + "size": 380502 + }, + { + "name": "Merriweather_12.cpfont", + "size": 469405 + }, + { + "name": "Merriweather_14.cpfont", + "size": 570179 + } + ] + }, + { + "name": "NotoSansArmenian", + "description": "Armenian script", + "styles": [ + "regular" + ], + "files": [ + { + "name": "NotoSansArmenian_10.cpfont", + "size": 8524 + }, + { + "name": "NotoSansArmenian_12.cpfont", + "size": 10057 + }, + { + "name": "NotoSansArmenian_14.cpfont", + "size": 12146 + }, + { + "name": "NotoSansArmenian_16.cpfont", + "size": 14418 + }, + { + "name": "NotoSansArmenian_18.cpfont", + "size": 16846 + } + ] + }, + { + "name": "NotoSansEthiopic", + "description": "Ethiopic/Ge'ez script", + "styles": [ + "regular" + ], + "files": [ + { + "name": "NotoSansEthiopic_10.cpfont", + "size": 35426 + }, + { + "name": "NotoSansEthiopic_12.cpfont", + "size": 47174 + }, + { + "name": "NotoSansEthiopic_14.cpfont", + "size": 60619 + }, + { + "name": "NotoSansEthiopic_16.cpfont", + "size": 76276 + }, + { + "name": "NotoSansEthiopic_18.cpfont", + "size": 94566 + } + ] + }, + { + "name": "NotoSansExtended", + "description": "Sans-serif (Latin, Greek, Cyrillic, Georgian, Armenian, Ethiopic)", + "styles": [ + "regular", + "bold", + "italic", + "bolditalic" + ], + "files": [ + { + "name": "NotoSansExtended_10.cpfont", + "size": 503189 + }, + { + "name": "NotoSansExtended_12.cpfont", + "size": 636573 + }, + { + "name": "NotoSansExtended_14.cpfont", + "size": 802460 + } + ] + }, + { + "name": "NotoSansGeorgian", + "description": "Georgian script", + "styles": [ + "regular" + ], + "files": [ + { + "name": "NotoSansGeorgian_10.cpfont", + "size": 12791 + }, + { + "name": "NotoSansGeorgian_12.cpfont", + "size": 15397 + }, + { + "name": "NotoSansGeorgian_14.cpfont", + "size": 18693 + }, + { + "name": "NotoSansGeorgian_16.cpfont", + "size": 22464 + }, + { + "name": "NotoSansGeorgian_18.cpfont", + "size": 26415 + } + ] + }, + { + "name": "NotoSansHangul", + "description": "Sans-serif (Korean Hangul)", + "styles": [ + "regular" + ], + "files": [ + { + "name": "NotoSansHangul_10.cpfont", + "size": 1180396 + } + ] + }, + { + "name": "NotoSerifExtended", + "description": "Serif (Latin, Greek, Cyrillic)", + "styles": [ + "regular", + "bold", + "italic", + "bolditalic" + ], + "files": [ + { + "name": "NotoSerifExtended_10.cpfont", + "size": 525124 + }, + { + "name": "NotoSerifExtended_12.cpfont", + "size": 655418 + }, + { + "name": "NotoSerifExtended_14.cpfont", + "size": 820713 + } + ] + }, + { + "name": "OpenDyslexic", + "description": "Dyslexia-friendly font (Latin, Latin-Extended)", + "styles": [ + "regular", + "bold", + "italic", + "bolditalic" + ], + "files": [ + { + "name": "OpenDyslexic_10.cpfont", + "size": 288236 + }, + { + "name": "OpenDyslexic_12.cpfont", + "size": 378678 + }, + { + "name": "OpenDyslexic_14.cpfont", + "size": 483820 + }, + { + "name": "OpenDyslexic_8.cpfont", + "size": 212648 + } + ] + }, + { + "name": "SourceCodePro", + "description": "Adobe monospace with excellent hinting (Latin)", + "styles": [ + "regular", + "bold", + "italic", + "bolditalic" + ], + "files": [ + { + "name": "SourceCodePro_10.cpfont", + "size": 143190 + }, + { + "name": "SourceCodePro_12.cpfont", + "size": 188782 + }, + { + "name": "SourceCodePro_14.cpfont", + "size": 237985 + }, + { + "name": "SourceCodePro_16.cpfont", + "size": 292971 + }, + { + "name": "SourceCodePro_18.cpfont", + "size": 359581 + } + ] + }, + { + "name": "SourceSans3", + "description": "Adobe humanist sans-serif (Latin, Greek, Cyrillic)", + "styles": [ + "regular", + "bold", + "italic", + "bolditalic" + ], + "files": [ + { + "name": "SourceSans3_10.cpfont", + "size": 380803 + }, + { + "name": "SourceSans3_12.cpfont", + "size": 470341 + }, + { + "name": "SourceSans3_14.cpfont", + "size": 561177 + }, + { + "name": "SourceSans3_16.cpfont", + "size": 669252 + } + ] + }, + { + "name": "SourceSerif4", + "description": "Adobe transitional serif (Latin, Greek, Cyrillic)", + "styles": [ + "regular", + "bold", + "italic", + "bolditalic" + ], + "files": [ + { + "name": "SourceSerif4_10.cpfont", + "size": 302084 + }, + { + "name": "SourceSerif4_12.cpfont", + "size": 359428 + }, + { + "name": "SourceSerif4_14.cpfont", + "size": 429133 + }, + { + "name": "SourceSerif4_16.cpfont", + "size": 508479 + } + ] + } + ] +} diff --git a/assets/sd-fonts/sd-fonts.yaml b/assets/sd-fonts/sd-fonts.yaml deleted file mode 100644 index 93716e4d..00000000 --- a/assets/sd-fonts/sd-fonts.yaml +++ /dev/null @@ -1,293 +0,0 @@ -# SD Card Font Families for CrossPoint++ Reader -# -# This file is the single source of truth for which fonts are generated, -# how they're sourced, and how they're described in the download manifest. -# -# Adding a new font family = adding a block here. No code changes needed. -# -# Fields: -# name: Output family name (used in filenames and on-device UI) -# description: Human-readable description (shown in download UI and manifest) -# intervals: Comma-separated Unicode interval presets for fontconvert_sdcard.py -# sizes: Point sizes to generate -# force_autohint: (optional) Force FreeType auto-hinter instead of native hinting -# styles: Map of style name -> font source -# path: relative to lib/EpdFont (for committed fonts) -# url: download URL (for fonts not in the repo) -# -# Variable fonts: -# Some fonts (Bitter, Inter, Alegreya) are distributed as variable fonts. -# freetype-py can't set variable font axis values, so the build script -# uses fonttools.instancer to extract static instances automatically. -# -# To use a variable font, add a 'variable' key to the style spec with -# axis values to pin: -# -# styles: -# regular: {url: "https://...Font[wght].ttf", variable: {wght: 400}} -# bold: {url: "https://...Font[wght].ttf", variable: {wght: 700}} -# -# Extracted static fonts are cached in instanced_fonts/ (gitignored). -# Requires fonttools: pip install -r requirements.txt - -families: - # ── Serif ────────────────────────────────────────────────────────────── - - - name: Literata - description: "Screen-optimized serif (Latin, Greek, Cyrillic)" - intervals: latin-ext,greek,cyrillic - sizes: [10, 12, 14, 16, 18] - styles: - regular: {url: "https://raw.githubusercontent.com/googlefonts/literata/main/fonts/ttf/Literata-Regular.ttf"} - bold: {url: "https://raw.githubusercontent.com/googlefonts/literata/main/fonts/ttf/Literata-Bold.ttf"} - italic: {url: "https://raw.githubusercontent.com/googlefonts/literata/main/fonts/ttf/Literata-Italic.ttf"} - bolditalic: {url: "https://raw.githubusercontent.com/googlefonts/literata/main/fonts/ttf/Literata-BoldItalic.ttf"} - - - name: SourceSerif4 - description: "Adobe transitional serif (Latin, Greek, Cyrillic)" - intervals: latin-ext,greek,cyrillic - sizes: [10, 12, 14, 16, 18] - styles: - regular: {url: "https://raw.githubusercontent.com/adobe-fonts/source-serif/release/TTF/SourceSerif4-Regular.ttf"} - bold: {url: "https://raw.githubusercontent.com/adobe-fonts/source-serif/release/TTF/SourceSerif4-Bold.ttf"} - italic: {url: "https://raw.githubusercontent.com/adobe-fonts/source-serif/release/TTF/SourceSerif4-It.ttf"} - bolditalic: {url: "https://raw.githubusercontent.com/adobe-fonts/source-serif/release/TTF/SourceSerif4-BoldIt.ttf"} - - - name: NotoSerifExtended - description: "Serif (Latin, Greek, Cyrillic)" - intervals: latin-ext,greek,cyrillic - sizes: [10, 12, 14, 16, 18] - styles: - regular: {url: "https://raw.githubusercontent.com/notofonts/NotoSerif/main/fonts/ttf/unhinted/instance_ttf/NotoSerif-Regular.ttf"} - bold: {url: "https://raw.githubusercontent.com/notofonts/NotoSerif/main/fonts/ttf/unhinted/instance_ttf/NotoSerif-Bold.ttf"} - italic: {url: "https://raw.githubusercontent.com/notofonts/NotoSerif-Italic/main/fonts/ttf/unhinted/instance_ttf/NotoSerif-Italic.ttf"} - bolditalic: {url: "https://raw.githubusercontent.com/notofonts/NotoSerif-Italic/main/fonts/ttf/unhinted/instance_ttf/NotoSerif-BoldItalic.ttf"} - - - name: Merriweather - description: "Warm serif for long-form reading (Latin, Cyrillic)" - intervals: latin-ext,cyrillic - sizes: [10, 12, 14, 16, 18] - styles: - regular: {url: "https://raw.githubusercontent.com/SorkinType/Merriweather/master/fonts/ttf/Merriweather-Regular.ttf"} - bold: {url: "https://raw.githubusercontent.com/SorkinType/Merriweather/master/fonts/ttf/Merriweather-Bold.ttf"} - italic: {url: "https://raw.githubusercontent.com/SorkinType/Merriweather/master/fonts/ttf/Merriweather-Italic.ttf"} - bolditalic: {url: "https://raw.githubusercontent.com/SorkinType/Merriweather/master/fonts/ttf/Merriweather-BoldItalic.ttf"} - - - name: Lora - description: "Calligraphic serif for literary reading (Latin, Cyrillic)" - intervals: latin-ext,cyrillic - sizes: [10, 12, 14, 16, 18] - styles: - regular: {url: "https://raw.githubusercontent.com/cyrealtype/Lora-Cyrillic/main/fonts/ttf/Lora-Regular.ttf"} - bold: {url: "https://raw.githubusercontent.com/cyrealtype/Lora-Cyrillic/main/fonts/ttf/Lora-Bold.ttf"} - italic: {url: "https://raw.githubusercontent.com/cyrealtype/Lora-Cyrillic/main/fonts/ttf/Lora-Italic.ttf"} - bolditalic: {url: "https://raw.githubusercontent.com/cyrealtype/Lora-Cyrillic/main/fonts/ttf/Lora-BoldItalic.ttf"} - - - name: Gelasio - description: "Readable old-style serif (Latin, Cyrillic)" - intervals: latin-ext,cyrillic - sizes: [10, 12, 14, 16, 18] - styles: - regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/gelasio/Gelasio%5Bwght%5D.ttf", variable: {wght: 400}} - bold: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/gelasio/Gelasio%5Bwght%5D.ttf", variable: {wght: 700}} - italic: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/gelasio/Gelasio-Italic%5Bwght%5D.ttf", variable: {wght: 400}} - bolditalic: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/gelasio/Gelasio-Italic%5Bwght%5D.ttf", variable: {wght: 700}} - - - name: GentiumBookPlus - description: "Scholarly serif with wide Unicode coverage (Latin, Greek, Cyrillic, IPA)" - intervals: latin-ext,greek,cyrillic - sizes: [10, 12, 14, 16, 18] - styles: - regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/gentiumbookplus/GentiumBookPlus-Regular.ttf"} - bold: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/gentiumbookplus/GentiumBookPlus-Bold.ttf"} - italic: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/gentiumbookplus/GentiumBookPlus-Italic.ttf"} - bolditalic: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/gentiumbookplus/GentiumBookPlus-BoldItalic.ttf"} - - - name: IBMPlexSerif - description: "Professional serif (Latin, Greek, Cyrillic)" - intervals: latin-ext,greek,cyrillic - sizes: [10, 12, 14, 16, 18] - styles: - regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/ibmplexserif/IBMPlexSerif-Regular.ttf"} - bold: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/ibmplexserif/IBMPlexSerif-Bold.ttf"} - italic: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/ibmplexserif/IBMPlexSerif-Italic.ttf"} - bolditalic: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/ibmplexserif/IBMPlexSerif-BoldItalic.ttf"} - - - name: Bitter - description: "Slab serif designed for e-ink (Latin, Cyrillic)" - intervals: latin-ext,cyrillic - sizes: [10, 12, 14, 16, 18] - styles: - regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/bitter/Bitter%5Bwght%5D.ttf", variable: {wght: 400}} - bold: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/bitter/Bitter%5Bwght%5D.ttf", variable: {wght: 700}} - italic: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/bitter/Bitter-Italic%5Bwght%5D.ttf", variable: {wght: 400}} - bolditalic: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/bitter/Bitter-Italic%5Bwght%5D.ttf", variable: {wght: 700}} - - # ── Sans-serif ───────────────────────────────────────────────────────── - - - name: NotoSansExtended - description: "Sans-serif (Latin, Greek, Cyrillic, Georgian, Armenian, Ethiopic)" - intervals: latin-ext,greek,cyrillic,georgian,armenian,ethiopic - sizes: [10, 12, 14, 16, 18] - styles: - regular: {path: "builtinFonts/source/NotoSans/NotoSans-Regular.ttf"} - bold: {path: "builtinFonts/source/NotoSans/NotoSans-Bold.ttf"} - italic: {path: "builtinFonts/source/NotoSans/NotoSans-Italic.ttf"} - bolditalic: {path: "builtinFonts/source/NotoSans/NotoSans-BoldItalic.ttf"} - - - name: Inter - description: "Modern sans-serif (Latin, Greek, Cyrillic)" - intervals: latin-ext,greek,cyrillic - sizes: [10, 12, 14, 16, 18] - styles: - regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/inter/Inter%5Bopsz%2Cwght%5D.ttf", variable: {wght: 400, opsz: 14}} - bold: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/inter/Inter%5Bopsz%2Cwght%5D.ttf", variable: {wght: 700, opsz: 14}} - italic: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/inter/Inter-Italic%5Bopsz%2Cwght%5D.ttf", variable: {wght: 400, opsz: 14}} - bolditalic: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/inter/Inter-Italic%5Bopsz%2Cwght%5D.ttf", variable: {wght: 700, opsz: 14}} - - - name: SourceSans3 - description: "Adobe humanist sans-serif (Latin, Greek, Cyrillic)" - intervals: latin-ext,greek,cyrillic - sizes: [10, 12, 14, 16, 18] - styles: - regular: {url: "https://raw.githubusercontent.com/adobe-fonts/source-sans/release/TTF/SourceSans3-Regular.ttf"} - bold: {url: "https://raw.githubusercontent.com/adobe-fonts/source-sans/release/TTF/SourceSans3-Bold.ttf"} - italic: {url: "https://raw.githubusercontent.com/adobe-fonts/source-sans/release/TTF/SourceSans3-It.ttf"} - bolditalic: {url: "https://raw.githubusercontent.com/adobe-fonts/source-sans/release/TTF/SourceSans3-BoldIt.ttf"} - - - name: IBMPlexSans - description: "IBM corporate sans-serif (Latin, Greek, Cyrillic)" - intervals: latin-ext,greek,cyrillic - sizes: [10, 12, 14, 16, 18] - styles: - regular: {url: "https://raw.githubusercontent.com/IBM/plex/master/packages/plex-sans/fonts/complete/ttf/IBMPlexSans-Regular.ttf"} - bold: {url: "https://raw.githubusercontent.com/IBM/plex/master/packages/plex-sans/fonts/complete/ttf/IBMPlexSans-Bold.ttf"} - italic: {url: "https://raw.githubusercontent.com/IBM/plex/master/packages/plex-sans/fonts/complete/ttf/IBMPlexSans-Italic.ttf"} - bolditalic: {url: "https://raw.githubusercontent.com/IBM/plex/master/packages/plex-sans/fonts/complete/ttf/IBMPlexSans-BoldItalic.ttf"} - - - name: Alegreya - description: "Calligraphic serif/display (Latin, Greek, Cyrillic)" - intervals: latin-ext,greek,cyrillic - sizes: [10, 12, 14, 16, 18] - styles: - regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/alegreya/Alegreya%5Bwght%5D.ttf", variable: {wght: 400}} - bold: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/alegreya/Alegreya%5Bwght%5D.ttf", variable: {wght: 700}} - italic: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/alegreya/Alegreya-Italic%5Bwght%5D.ttf", variable: {wght: 400}} - bolditalic: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/alegreya/Alegreya-Italic%5Bwght%5D.ttf", variable: {wght: 700}} - - # ── Monospace ────────────────────────────────────────────────────────── - - - name: IBMPlexMono - description: "Monospace for code and technical reading (Latin, Greek, Cyrillic)" - intervals: latin-ext,greek,cyrillic - sizes: [10, 12, 14, 16, 18] - styles: - regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/ibmplexmono/IBMPlexMono-Regular.ttf"} - bold: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/ibmplexmono/IBMPlexMono-Bold.ttf"} - italic: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/ibmplexmono/IBMPlexMono-Italic.ttf"} - bolditalic: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/ibmplexmono/IBMPlexMono-BoldItalic.ttf"} - - - name: SourceCodePro - description: "Adobe monospace with excellent hinting (Latin)" - intervals: latin-ext - sizes: [10, 12, 14, 16, 18] - styles: - regular: {url: "https://raw.githubusercontent.com/adobe-fonts/source-code-pro/release/TTF/SourceCodePro-Regular.ttf"} - bold: {url: "https://raw.githubusercontent.com/adobe-fonts/source-code-pro/release/TTF/SourceCodePro-Bold.ttf"} - italic: {url: "https://raw.githubusercontent.com/adobe-fonts/source-code-pro/release/TTF/SourceCodePro-It.ttf"} - bolditalic: {url: "https://raw.githubusercontent.com/adobe-fonts/source-code-pro/release/TTF/SourceCodePro-BoldIt.ttf"} - - # ── Accessibility ────────────────────────────────────────────────────── - - - name: AtkinsonHyperlegibleNext - description: "Accessibility font for low vision (Latin)" - intervals: latin-ext - sizes: [10, 12, 14, 16, 18] - styles: - regular: {url: "https://raw.githubusercontent.com/googlefonts/atkinson-hyperlegible-next/main/fonts/ttf/AtkinsonHyperlegibleNext-Regular.ttf"} - bold: {url: "https://raw.githubusercontent.com/googlefonts/atkinson-hyperlegible-next/main/fonts/ttf/AtkinsonHyperlegibleNext-Bold.ttf"} - italic: {url: "https://raw.githubusercontent.com/googlefonts/atkinson-hyperlegible-next/main/fonts/ttf/AtkinsonHyperlegibleNext-Italic.ttf"} - bolditalic: {url: "https://raw.githubusercontent.com/googlefonts/atkinson-hyperlegible-next/main/fonts/ttf/AtkinsonHyperlegibleNext-BoldItalic.ttf"} - - - name: LexicaUltralegible - description: "Accessibility font for low vision / dyslexia (Latin)" - intervals: latin-ext - sizes: [10, 12, 14, 16, 18] - styles: - regular: {url: "https://raw.githubusercontent.com/jacobxperez/lexica-ultralegible/main/fonts/ttf/LexicaUltralegible-Regular.ttf"} - bold: {url: "https://raw.githubusercontent.com/jacobxperez/lexica-ultralegible/main/fonts/ttf/LexicaUltralegible-Bold.ttf"} - italic: {url: "https://raw.githubusercontent.com/jacobxperez/lexica-ultralegible/main/fonts/ttf/LexicaUltralegible-Italic.ttf"} - bolditalic: {url: "https://raw.githubusercontent.com/jacobxperez/lexica-ultralegible/main/fonts/ttf/LexicaUltralegible-BoldItalic.ttf"} - - # OpenDyslexic was previously baked into the firmware. It is now distributed - # as an SD-card font so users who don't need it don't pay the ~3MB flash cost. - # Sizes 8/10 retained for backward compatibility with the previous built-in mapping. - - name: OpenDyslexic - description: "Dyslexia-friendly font (Latin, Latin-Extended)" - intervals: latin-ext - sizes: [8, 10, 12, 14, 16] - styles: - regular: {path: "builtinFonts/source/OpenDyslexic/OpenDyslexic-Regular.otf"} - bold: {path: "builtinFonts/source/OpenDyslexic/OpenDyslexic-Bold.otf"} - italic: {path: "builtinFonts/source/OpenDyslexic/OpenDyslexic-Italic.otf"} - bolditalic: {path: "builtinFonts/source/OpenDyslexic/OpenDyslexic-BoldItalic.otf"} - - # ── CJK & Hangul ────────────────────────────────────────────────────── - - - name: NotoSansCJK - description: "Sans-serif (Chinese, Japanese, Korean)" - intervals: ascii,latin1,punctuation,cjk - sizes: [10, 12, 14, 16, 18] - styles: - regular: {url: "https://github.com/notofonts/noto-cjk/raw/main/Sans/OTF/SimplifiedChinese/NotoSansCJKsc-Regular.otf"} - - - name: NotoSerifCJK - description: "Serif (Chinese, Japanese, Korean)" - intervals: ascii,latin1,punctuation,cjk - sizes: [10, 12, 14, 16, 18] - styles: - regular: {url: "https://github.com/notofonts/noto-cjk/raw/main/Serif/OTF/SimplifiedChinese/NotoSerifCJKsc-Regular.otf"} - - - name: NotoSansHangul - description: "Sans-serif (Korean Hangul)" - intervals: ascii,latin1,punctuation,hangul - sizes: [10, 12, 14, 16, 18] - styles: - regular: {url: "https://github.com/notofonts/noto-cjk/raw/main/Sans/OTF/Korean/NotoSansCJKkr-Regular.otf"} - - # ── Additional scripts (no complex shaping needed) ───────────────────── - - - name: NotoSansArmenian - description: "Armenian script" - intervals: armenian - sizes: [10, 12, 14, 16, 18] - styles: - regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/notosansarmenian/NotoSansArmenian%5Bwdth%2Cwght%5D.ttf", variable: {wght: 400, wdth: 100}} - - - name: NotoSansGeorgian - description: "Georgian script" - intervals: georgian - sizes: [10, 12, 14, 16, 18] - styles: - regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/notosansgeorgian/NotoSansGeorgian%5Bwdth%2Cwght%5D.ttf", variable: {wght: 400, wdth: 100}} - - - name: NotoSansEthiopic - description: "Ethiopic/Ge'ez script" - intervals: ethiopic - sizes: [10, 12, 14, 16, 18] - styles: - regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/notosansethiopic/NotoSansEthiopic%5Bwdth%2Cwght%5D.ttf", variable: {wght: 400, wdth: 100}} - - - name: NotoSansCherokee - description: "Cherokee syllabary" - intervals: cherokee - sizes: [10, 12, 14, 16, 18] - styles: - regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/notosanscherokee/NotoSansCherokee%5Bwght%5D.ttf", variable: {wght: 400}} - - - name: NotoSansTifinagh - description: "Tifinagh script" - intervals: tifinagh - sizes: [10, 12, 14, 16, 18] - styles: - regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/notosanstifinagh/NotoSansTifinagh-Regular.ttf"} diff --git a/lib/EpdFont/scripts/build-sd-fonts.py b/lib/EpdFont/scripts/build-sd-fonts.py index 275fae28..30e1c8f1 100644 --- a/lib/EpdFont/scripts/build-sd-fonts.py +++ b/lib/EpdFont/scripts/build-sd-fonts.py @@ -225,6 +225,7 @@ def main(): parser.add_argument( "--output-dir", default=str(DEFAULT_OUTPUT), help="Output directory for .cpfont files" ) + parser.add_argument("--skip", action="store_true", help="Skip font generation completely (for testing manifest generation)") parser.add_argument("--only", help="Comma-separated family names to build (default: all)") parser.add_argument("--manifest", action="store_true", help="Also generate fonts.json manifest") parser.add_argument("--base-url", default="", help="Base URL for manifest (required with --manifest)") @@ -250,6 +251,7 @@ def main(): print(f"ERROR: Config not found: {config_path}", file=sys.stderr) sys.exit(1) + output_base = Path(args.output_dir) with open(config_path) as f: config = yaml.safe_load(f) @@ -258,73 +260,73 @@ def main(): print("ERROR: No families defined in config", file=sys.stderr) sys.exit(1) - # Filter if --only specified - if args.only: - only_names = {token.strip() for token in args.only.split(",") if token.strip()} - families = [f for f in families if f["name"] in only_names] - missing = only_names - {f["name"] for f in families} - if missing: - print(f"WARNING: families not found in config: {', '.join(missing)}", file=sys.stderr) - if not families: - print("ERROR: no matching families after --only filter", file=sys.stderr) - sys.exit(1) + failed = False + if not args.skip: + # Filter if --only specified + if args.only: + only_names = {token.strip() for token in args.only.split(",") if token.strip()} + families = [f for f in families if f["name"] in only_names] + missing = only_names - {f["name"] for f in families} + if missing: + print(f"WARNING: families not found in config: {', '.join(missing)}", file=sys.stderr) + if not families: + print("ERROR: no matching families after --only filter", file=sys.stderr) + sys.exit(1) - output_base = Path(args.output_dir) + if args.clean and output_base.exists(): + print(f"Cleaning {output_base}...") + shutil.rmtree(output_base) - if args.clean and output_base.exists(): - print(f"Cleaning {output_base}...") - shutil.rmtree(output_base) + output_base.mkdir(parents=True, exist_ok=True) - output_base.mkdir(parents=True, exist_ok=True) + # Download phase (sequential — avoids hammering servers) + print(f"\n=== Resolving {len(families)} font families ===\n") + for family in families: + for style_name, style_spec in family.get("styles", {}).items(): + if "url" in style_spec: + try: + resolve_font_path(style_spec, family["name"], style_name) + except Exception as e: + print(f"ERROR: {e}", file=sys.stderr) + sys.exit(1) - # Download phase (sequential — avoids hammering servers) - print(f"\n=== Resolving {len(families)} font families ===\n") - for family in families: - for style_name, style_spec in family.get("styles", {}).items(): - if "url" in style_spec: - try: - resolve_font_path(style_spec, family["name"], style_name) - except Exception as e: - print(f"ERROR: {e}", file=sys.stderr) - sys.exit(1) + # Build phase (parallel) + max_workers = args.jobs or len(families) + print(f"\n=== Building {len(families)} families ({max_workers} parallel jobs) ===\n") - # Build phase (parallel) - max_workers = args.jobs or len(families) - print(f"\n=== Building {len(families)} families ({max_workers} parallel jobs) ===\n") - - failed = [] - with ProcessPoolExecutor(max_workers=max_workers) as executor: - futures = { - executor.submit(build_family, family, output_base): family["name"] - for family in families - } - for future in as_completed(futures): - name = futures[future] - success = False - message = "" - try: - name, success, message = future.result() - except Exception as e: + failed = [] + with ProcessPoolExecutor(max_workers=max_workers) as executor: + futures = { + executor.submit(build_family, family, output_base): family["name"] + for family in families + } + for future in as_completed(futures): + name = futures[future] success = False - message = str(e) - if success: - # Count output files - family_dir = output_base / name - count = len(list(family_dir.glob("*.cpfont"))) - size = sum(f.stat().st_size for f in family_dir.glob("*.cpfont")) - print(f" OK: {name} ({count} files, {size / 1024 / 1024:.1f} MB)") - else: - print(f" FAILED: {name}: {message}", file=sys.stderr) - failed.append(name) + message = "" + try: + name, success, message = future.result() + except Exception as e: + success = False + message = str(e) + if success: + # Count output files + family_dir = output_base / name + count = len(list(family_dir.glob("*.cpfont"))) + size = sum(f.stat().st_size for f in family_dir.glob("*.cpfont")) + print(f" OK: {name} ({count} files, {size / 1024 / 1024:.1f} MB)") + else: + print(f" FAILED: {name}: {message}", file=sys.stderr) + failed.append(name) - # Summary - print("\n=== Summary ===\n") - total_files = len(list(output_base.rglob("*.cpfont"))) - total_size = sum(f.stat().st_size for f in output_base.rglob("*.cpfont")) - print(f"Total: {total_files} .cpfont files ({total_size / 1024 / 1024:.1f} MB)") + # Summary + print("\n=== Summary ===\n") + total_files = len(list(output_base.rglob("*.cpfont"))) + total_size = sum(f.stat().st_size for f in output_base.rglob("*.cpfont")) + print(f"Total: {total_files} .cpfont files ({total_size / 1024 / 1024:.1f} MB)") - if failed: - print(f"\nFailed families: {', '.join(failed)}", file=sys.stderr) + if failed: + print(f"\nFailed families: {', '.join(failed)}", file=sys.stderr) # Manifest if args.manifest: diff --git a/lib/I18n/I18n.cpp b/lib/I18n/I18n.cpp index 82de585f..0a9dfbb8 100644 --- a/lib/I18n/I18n.cpp +++ b/lib/I18n/I18n.cpp @@ -27,7 +27,7 @@ const char* I18n::get(StrId id) const { // Use generated helper function - no hardcoded switch needed! const LangStrings lang = getLanguageStrings(_language); - // If bit 15 of the offset is set, apply the offset to the English lookup table + // If bit 15 of the offset is set, apply the offset to the English lookup table const uint16_t off = lang.offsets[index]; if (off & 0x8000) return STRINGS_EN_DATA + (off & 0x7FFF); return lang.data + off; diff --git a/scripts/generate-font-manifest.py b/scripts/generate-font-manifest.py new file mode 100644 index 00000000..89f6a319 --- /dev/null +++ b/scripts/generate-font-manifest.py @@ -0,0 +1,253 @@ +#!/usr/bin/env python3 +from __future__ import annotations +"""Generate a fonts.json manifest from a directory of .cpfont files. + +Scans the input directory (flat or nested by family) for .cpfont files, reads +their binary headers to extract style metadata, and produces a JSON manifest +suitable for device-initiated font downloads. + +Usage: + python3 scripts/generate-font-manifest.py \ + --input lib/EpdFont/scripts/output \ + --base-url "https://example.com/fonts/" \ + --output dist/fonts.json + +The input directory may be flat (all .cpfont files in one dir) or nested +(family subdirectories). Family names are derived from filenames using the +convention _.cpfont. +""" + +import argparse +import json +import os +import struct +import sys +from pathlib import Path + +# --- .cpfont binary format constants --- +# Global header: 8s magic, H version, H flags, B styleCount, 19x reserved +GLOBAL_HEADER_FORMAT = "<8sHHB19x" +GLOBAL_HEADER_SIZE = struct.calcsize(GLOBAL_HEADER_FORMAT) # 32 bytes + +# Style TOC entry: B styleId, 3x pad, I intervalCount, I glyphCount, ... +# We only need the first byte (styleId) from each 32-byte entry. +STYLE_TOC_ENTRY_SIZE = 32 +STYLE_TOC_ENTRY_FORMAT = " dict[str, str]: + """Load family descriptions from sd-fonts.yaml config.""" + try: + import yaml + except ImportError: + print("WARNING: pyyaml not installed, cannot load descriptions from YAML", file=sys.stderr) + return {} + + with open(yaml_path) as f: + config = yaml.safe_load(f) + + return {f["name"]: f["description"] for f in config.get("families", []) if "description" in f} + + +def read_cpfont_styles(filepath: Path) -> list[str]: + """Read style names from a .cpfont file's binary header.""" + with open(filepath, "rb") as f: + header_data = f.read(GLOBAL_HEADER_SIZE) + if len(header_data) < GLOBAL_HEADER_SIZE: + print(f" WARNING: {filepath.name} too small, skipping", file=sys.stderr) + return [] + + magic, version, _flags, style_count = struct.unpack( + GLOBAL_HEADER_FORMAT, header_data + ) + + if magic != CPFONT_MAGIC: + print( + f" WARNING: {filepath.name} bad magic {magic!r}, skipping", + file=sys.stderr, + ) + return [] + + if version != CPFONT_VERSION: + print( + f" WARNING: {filepath.name} version {version} != {CPFONT_VERSION}, skipping", + file=sys.stderr, + ) + return [] + + styles = [] + for _ in range(style_count): + toc_data = f.read(STYLE_TOC_ENTRY_SIZE) + if len(toc_data) < STYLE_TOC_ENTRY_SIZE: + break + (style_id,) = struct.unpack(STYLE_TOC_ENTRY_FORMAT, toc_data) + name = STYLE_NAMES.get(style_id, f"unknown({style_id})") + styles.append(name) + + return styles + + +def parse_filename(filename: str) -> tuple[str, str] | None: + """Parse '_.cpfont' into (family, size_str). + + Returns None if the filename doesn't match the expected pattern. + """ + if not filename.endswith(".cpfont"): + return None + stem = filename[: -len(".cpfont")] + parts = stem.rsplit("_", 1) + if len(parts) != 2: + return None + family, size_str = parts + if not size_str.isdigit(): + return None + return family, size_str + + +def scan_cpfont_files(input_dir: Path) -> dict[str, list[Path]]: + """Scan input directory for .cpfont files, grouped by family name. + + Handles both flat and nested directory layouts. + """ + families: dict[str, list[Path]] = {} + + for path in sorted(input_dir.rglob("*.cpfont")): + if not path.is_file(): + continue + parsed = parse_filename(path.name) + if parsed is None: + print(f" WARNING: skipping {path.name} (unexpected filename format)", file=sys.stderr) + continue + family_name = parsed[0] + families.setdefault(family_name, []).append(path) + + return families + + +def build_manifest( + families: dict[str, list[Path]], base_url: str +) -> dict: + """Build the manifest dict from discovered font families.""" + manifest_families = [] + + for family_name in sorted(families.keys()): + files = families[family_name] + + # Read styles from the first file (all files in a family have the + # same styles since they're generated from the same source fonts). + styles = read_cpfont_styles(files[0]) if files else [] + + # Get description + description = FAMILY_DESCRIPTIONS.get(family_name) + if description is None: + print( + f" WARNING: no description for family '{family_name}', " + f"consider adding one to FAMILY_DESCRIPTIONS in {__file__}", + file=sys.stderr, + ) + description = family_name + + file_entries = [] + for filepath in sorted(files, key=lambda p: p.name): + file_entries.append( + { + "name": filepath.name, + "size": filepath.stat().st_size, + } + ) + + manifest_families.append( + { + "name": family_name, + "description": description, + "styles": styles, + "files": file_entries, + } + ) + + return { + "version": 1, + "baseUrl": base_url, + "families": manifest_families, + } + + +def main(): + parser = argparse.ArgumentParser( + description="Generate fonts.json manifest from .cpfont files" + ) + parser.add_argument( + "--input", + required=True, + help="Directory containing .cpfont files (flat or nested by family)", + ) + parser.add_argument( + "--base-url", + required=True, + help="URL prefix for font downloads (device concatenates baseUrl + filename)", + ) + parser.add_argument( + "--output", + required=True, + help="Output path for fonts.json", + ) + parser.add_argument( + "--descriptions-from", + default=None, + help="Path to sd-fonts.yaml to load family descriptions (default: use family name)", + ) + args = parser.parse_args() + + input_dir = Path(args.input) + if not input_dir.is_dir(): + print(f"ERROR: {input_dir} is not a directory", file=sys.stderr) + sys.exit(1) + + # Ensure base URL ends with / + base_url = args.base_url + if not base_url.endswith("/"): + base_url += "/" + + # Load descriptions from YAML config if provided + global FAMILY_DESCRIPTIONS + if args.descriptions_from: + desc_path = Path(args.descriptions_from) + if desc_path.exists(): + FAMILY_DESCRIPTIONS = load_descriptions_from_yaml(desc_path) + print(f"Loaded {len(FAMILY_DESCRIPTIONS)} descriptions from {desc_path}") + else: + print(f"WARNING: {desc_path} not found, using family names as descriptions", file=sys.stderr) + + print(f"Scanning {input_dir} for .cpfont files...") + families = scan_cpfont_files(input_dir) + + if not families: + print("ERROR: no .cpfont files found", file=sys.stderr) + sys.exit(1) + + print(f"Found {len(families)} font families:") + for name, files in sorted(families.items()): + print(f" {name}: {len(files)} files") + + manifest = build_manifest(families, base_url) + + output_path = Path(args.output) + output_path.parent.mkdir(parents=True, exist_ok=True) + with open(output_path, "w") as f: + json.dump(manifest, f, indent=2) + f.write("\n") + + print(f"Wrote {output_path}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/activities/settings/FontDownloadActivity.h b/src/activities/settings/FontDownloadActivity.h index 242d2481..33cdf934 100644 --- a/src/activities/settings/FontDownloadActivity.h +++ b/src/activities/settings/FontDownloadActivity.h @@ -8,7 +8,8 @@ #include "util/ButtonNavigator.h" #ifndef FONT_MANIFEST_URL -#define FONT_MANIFEST_URL "https://github.com/jpirnay/crosspoint-reader/blob/master/assets/sd-fonts/sd-fonts.yaml" +#define FONT_MANIFEST_URL \ + "https://raw.githubusercontent.com/jpirnay/crosspoint-reader/master/assets/sd-fonts/fonts.json" #endif class FontDownloadActivity : public Activity { diff --git a/src/network/CrossPointWebServer.cpp b/src/network/CrossPointWebServer.cpp index 54374b76..911ed6ba 100644 --- a/src/network/CrossPointWebServer.cpp +++ b/src/network/CrossPointWebServer.cpp @@ -37,7 +37,8 @@ constexpr uint16_t UDP_PORTS[] = {54982, 48123, 39001, 44044, 59678}; constexpr uint16_t LOCAL_UDP_PORT = 8134; #ifndef FONT_MANIFEST_URL -#define FONT_MANIFEST_URL "https://github.com/jpirnay/crosspoint-reader/blob/master/assets/sd-fonts/sd-fonts.yaml" +#define FONT_MANIFEST_URL \ + "https://raw.githubusercontent.com/jpirnay/crosspoint-reader/master/assets/sd-fonts/fonts.json" #endif // Static pointer for WebSocket callback (WebSocketsServer requires C-style callback)