diff --git a/assets/sd-fonts/sd-fonts.yaml b/assets/sd-fonts/sd-fonts.yaml deleted file mode 120000 index 36b39c31..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/assets/sd-fonts/sd-fonts.yaml b/assets/sd-fonts/sd-fonts.yaml new file mode 100644 index 00000000..93716e4d --- /dev/null +++ b/assets/sd-fonts/sd-fonts.yaml @@ -0,0 +1,293 @@ +# 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/src/activities/settings/FontDownloadActivity.h b/src/activities/settings/FontDownloadActivity.h index b28d4511..242d2481 100644 --- a/src/activities/settings/FontDownloadActivity.h +++ b/src/activities/settings/FontDownloadActivity.h @@ -8,7 +8,7 @@ #include "util/ButtonNavigator.h" #ifndef FONT_MANIFEST_URL -#define FONT_MANIFEST_URL "https://github.com/jpirnay/crosspoint-reader/assets/sd-fonts/fonts.json" +#define FONT_MANIFEST_URL "https://github.com/jpirnay/crosspoint-reader/blob/master/assets/sd-fonts/sd-fonts.yaml" #endif class FontDownloadActivity : public Activity { diff --git a/src/network/CrossPointWebServer.cpp b/src/network/CrossPointWebServer.cpp index dcc169e1..54374b76 100644 --- a/src/network/CrossPointWebServer.cpp +++ b/src/network/CrossPointWebServer.cpp @@ -26,6 +26,7 @@ #include "html/SettingsPageHtml.generated.h" #include "html/WelcomePageHtml.generated.h" #include "html/js/jszip_minJs.generated.h" +#include "network/HttpDownloader.h" namespace { // Folders/files to hide from the web interface file browser @@ -35,6 +36,10 @@ constexpr size_t HIDDEN_ITEMS_COUNT = sizeof(HIDDEN_ITEMS) / sizeof(HIDDEN_ITEMS 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" +#endif + // Static pointer for WebSocket callback (WebSocketsServer requires C-style callback) CrossPointWebServer* wsInstance = nullptr; @@ -203,6 +208,8 @@ void CrossPointWebServer::begin() { // Font management endpoints server->on("/fonts", HTTP_GET, [this] { handleFontsPage(); }); server->on("/api/fonts", HTTP_GET, [this] { handleFontList(); }); + server->on("/api/fonts/manifest", HTTP_GET, [this] { handleFontManifest(); }); + server->on("/api/fonts/download", HTTP_POST, [this] { handleFontDownload(); }); server->on("/api/fonts/upload", HTTP_POST, [this] { handleFontUpload(); }, [this] { handleFontUploadData(); }); server->on("/api/fonts/delete", HTTP_POST, [this] { handleFontDelete(); }); @@ -1411,6 +1418,220 @@ void CrossPointWebServer::handlePostSettings() { // ---- Font Management API ---- +namespace { +struct RemoteManifestFile { + std::string name; + size_t size = 0; +}; + +struct RemoteManifestFamily { + std::string name; + std::string description; + std::vector files; + size_t totalSize = 0; + bool installed = false; + bool hasUpdate = false; +}; + +// Cap chosen so that FONTS_DIR + "/" + family + "__staging/" + filename stays +// well under the 128-byte path buffers used below. snprintf truncation checks +// are the actual guard; this just rejects obviously-bogus entries early. +static constexpr size_t MAX_FONT_FILE_NAME_LEN = 60; + +bool isValidFontFileName(const std::string& name) { + if (name.empty() || name.size() > MAX_FONT_FILE_NAME_LEN) return false; + if (name.find('/') != std::string::npos) return false; + if (name.find('\\') != std::string::npos) return false; + if (name.find("..") != std::string::npos) return false; + return true; +} + +bool fetchRemoteFontManifest(FontInstaller& installer, std::vector& outFamilies, + std::string& outBaseUrl, std::string& outError) { + static constexpr const char* MANIFEST_TMP = "/fonts_manifest_web.tmp"; + + auto result = HttpDownloader::downloadToFile(FONT_MANIFEST_URL, MANIFEST_TMP, nullptr); + if (result != HttpDownloader::OK) { + outError = "Failed to fetch font manifest"; + Storage.remove(MANIFEST_TMP); + return false; + } + + FsFile manifestFile; + if (!Storage.openFileForRead("WEB", MANIFEST_TMP, manifestFile)) { + outError = "Failed to open downloaded manifest"; + Storage.remove(MANIFEST_TMP); + return false; + } + + JsonDocument doc; + DeserializationError err = deserializeJson(doc, manifestFile); + manifestFile.close(); + Storage.remove(MANIFEST_TMP); + if (err) { + outError = "Failed to parse font manifest"; + return false; + } + + const int version = doc["version"] | 0; + if (version != 1) { + outError = "Unsupported manifest version"; + return false; + } + + outBaseUrl = doc["baseUrl"] | ""; + outFamilies.clear(); + + JsonArray familiesArr = doc["families"].as(); + outFamilies.reserve(familiesArr.size()); + + for (JsonObject fObj : familiesArr) { + RemoteManifestFamily family; + family.name = fObj["name"] | ""; + family.description = fObj["description"] | ""; + + if (!FontInstaller::isValidFamilyName(family.name.c_str())) { + LOG_ERR("WEB", "Manifest entry rejected, invalid family name: %s", family.name.c_str()); + continue; + } + + bool fileNamesOk = true; + for (JsonObject fileObj : fObj["files"].as()) { + RemoteManifestFile file; + file.name = fileObj["name"] | ""; + file.size = static_cast(fileObj["size"] | 0); + if (!isValidFontFileName(file.name)) { + LOG_ERR("WEB", "Manifest entry rejected, invalid file name in %s: %s", family.name.c_str(), file.name.c_str()); + fileNamesOk = false; + break; + } + family.totalSize += file.size; + family.files.push_back(std::move(file)); + } + if (!fileNamesOk) continue; + + family.installed = installer.isFamilyInstalled(family.name.c_str()); + family.hasUpdate = false; + if (family.installed) { + for (const auto& file : family.files) { + char path[128]; + FontInstaller::buildFontPath(family.name.c_str(), file.name.c_str(), path, sizeof(path)); + FsFile f; + if (Storage.openFileForRead("WEB", path, f)) { + const size_t actual = static_cast(f.size()); + f.close(); + if (actual != file.size) { + family.hasUpdate = true; + break; + } + } else { + family.hasUpdate = true; + break; + } + } + } + + outFamilies.push_back(std::move(family)); + } + + return true; +} + +bool installRemoteFamily(const RemoteManifestFamily& family, const std::string& baseUrl, FontInstaller& installer, + std::string& outError) { + if (!FontInstaller::isValidFamilyName(family.name.c_str())) { + outError = "Invalid family name"; + return false; + } + for (const auto& file : family.files) { + if (!isValidFontFileName(file.name)) { + outError = std::string("Invalid file name: ") + file.name; + return false; + } + } + + char liveDir[128]; + char stagingDir[128]; + char backupDir[128]; + int n; + n = snprintf(liveDir, sizeof(liveDir), "%s/%s", SdCardFontRegistry::FONTS_DIR, family.name.c_str()); + if (n < 0 || static_cast(n) >= sizeof(liveDir)) { + outError = "Family path too long"; + return false; + } + n = snprintf(stagingDir, sizeof(stagingDir), "%s/%s__staging", SdCardFontRegistry::FONTS_DIR, family.name.c_str()); + if (n < 0 || static_cast(n) >= sizeof(stagingDir)) { + outError = "Family path too long"; + return false; + } + n = snprintf(backupDir, sizeof(backupDir), "%s/%s__backup", SdCardFontRegistry::FONTS_DIR, family.name.c_str()); + if (n < 0 || static_cast(n) >= sizeof(backupDir)) { + outError = "Family path too long"; + return false; + } + + if (Storage.exists(stagingDir) && !Storage.removeDir(stagingDir)) { + outError = "Failed to prepare staging area"; + return false; + } + if (!Storage.mkdir(stagingDir)) { + outError = "Failed to create staging area"; + return false; + } + + for (const auto& file : family.files) { + esp_task_wdt_reset(); + + char stagedPath[128]; + int sn = snprintf(stagedPath, sizeof(stagedPath), "%s/%s", stagingDir, file.name.c_str()); + if (sn < 0 || static_cast(sn) >= sizeof(stagedPath)) { + Storage.removeDir(stagingDir); + outError = std::string("File path too long: ") + file.name; + return false; + } + const std::string url = baseUrl + file.name; + + auto result = HttpDownloader::downloadToFile(url, stagedPath, nullptr); + if (result != HttpDownloader::OK) { + Storage.removeDir(stagingDir); + outError = std::string("Download failed: ") + file.name; + return false; + } + + if (!installer.validateCpfontFile(stagedPath)) { + Storage.removeDir(stagingDir); + outError = std::string("Invalid font file: ") + file.name; + return false; + } + } + + const bool hadLiveDir = Storage.exists(liveDir); + if (Storage.exists(backupDir) && !Storage.removeDir(backupDir)) { + Storage.removeDir(stagingDir); + outError = "Failed to prepare backup area"; + return false; + } + if (hadLiveDir && !Storage.rename(liveDir, backupDir)) { + Storage.removeDir(stagingDir); + outError = "Failed to replace installed font"; + return false; + } + if (!Storage.rename(stagingDir, liveDir)) { + if (hadLiveDir && Storage.exists(backupDir)) { + Storage.rename(backupDir, liveDir); + } + Storage.removeDir(stagingDir); + outError = "Failed to finalize font install"; + return false; + } + if (Storage.exists(backupDir)) { + Storage.removeDir(backupDir); + } + + return true; +} +} // namespace + void CrossPointWebServer::handleFontsPage() const { sendHtmlContent(server.get(), FontsPageHtml, sizeof(FontsPageHtml)); LOG_DBG("WEB", "Served fonts page"); @@ -1455,6 +1676,118 @@ void CrossPointWebServer::handleFontList() { server->send(200, "application/json", json); } +void CrossPointWebServer::handleFontManifest() { + FontInstaller installer(sdFontSystem.registry()); + installer.refreshRegistry(); + + std::vector families; + std::string baseUrl; + std::string error; + if (!fetchRemoteFontManifest(installer, families, baseUrl, error)) { + JsonDocument errDoc; + errDoc["ok"] = false; + errDoc["error"] = error; + String out; + serializeJson(errDoc, out); + server->send(500, "application/json", out); + return; + } + + JsonDocument doc; + doc["ok"] = true; + doc["baseUrl"] = baseUrl; + JsonArray arr = doc["families"].to(); + for (const auto& family : families) { + JsonObject obj = arr.add(); + obj["name"] = family.name; + obj["description"] = family.description; + obj["installed"] = family.installed; + obj["hasUpdate"] = family.hasUpdate; + obj["totalSize"] = static_cast(family.totalSize); + obj["fileCount"] = static_cast(family.files.size()); + } + String out; + serializeJson(doc, out); + server->send(200, "application/json", out); +} + +void CrossPointWebServer::handleFontDownload() { + String body = server->arg("plain"); + JsonDocument req; + if (deserializeJson(req, body)) { + server->send(400, "application/json", "{\"ok\":false,\"error\":\"Invalid request\"}"); + return; + } + + const bool installAll = req["all"] | false; + const std::string requestedFamily = req["family"] | ""; + if (!installAll && requestedFamily.empty()) { + server->send(400, "application/json", "{\"ok\":false,\"error\":\"Missing family\"}"); + return; + } + + FontInstaller installer(sdFontSystem.registry()); + installer.refreshRegistry(); + + std::vector families; + std::string baseUrl; + std::string error; + if (!fetchRemoteFontManifest(installer, families, baseUrl, error)) { + JsonDocument errDoc; + errDoc["ok"] = false; + errDoc["error"] = error; + String out; + serializeJson(errDoc, out); + server->send(500, "application/json", out); + return; + } + + std::vector targets; + if (installAll) { + for (auto& family : families) { + if (!family.installed || family.hasUpdate) { + targets.push_back(&family); + } + } + } else { + for (auto& family : families) { + if (family.name == requestedFamily) { + targets.push_back(&family); + break; + } + } + if (targets.empty()) { + server->send(404, "application/json", "{\"ok\":false,\"error\":\"Family not found in manifest\"}"); + return; + } + } + + size_t installedCount = 0; + for (auto* family : targets) { + esp_task_wdt_reset(); + if (!installRemoteFamily(*family, baseUrl, installer, error)) { + JsonDocument errDoc; + errDoc["ok"] = false; + errDoc["error"] = error; + errDoc["family"] = family->name; + errDoc["installedCount"] = static_cast(installedCount); + String out; + serializeJson(errDoc, out); + server->send(500, "application/json", out); + return; + } + installedCount++; + } + + installer.refreshRegistry(); + JsonDocument res; + res["ok"] = true; + res["installedCount"] = static_cast(installedCount); + String out; + serializeJson(res, out); + server->send(200, "application/json", out); +} + void CrossPointWebServer::handleFontUploadData() { HTTPUpload& up = server->upload(); diff --git a/src/network/CrossPointWebServer.h b/src/network/CrossPointWebServer.h index 78421280..e73dce97 100644 --- a/src/network/CrossPointWebServer.h +++ b/src/network/CrossPointWebServer.h @@ -113,6 +113,8 @@ class CrossPointWebServer { // Font management handlers void handleFontsPage() const; void handleFontList(); + void handleFontManifest(); + void handleFontDownload(); void handleFontUpload(); void handleFontUploadData(); void handleFontDelete(); diff --git a/src/network/html/FontsPage.html b/src/network/html/FontsPage.html index a3023fcb..37dd9da1 100644 --- a/src/network/html/FontsPage.html +++ b/src/network/html/FontsPage.html @@ -104,6 +104,10 @@ color: white; } .btn-primary:hover { background: var(--accent-hover-color); } + .btn[disabled] { + opacity: 0.5; + cursor: default; + } .upload-form { display: flex; gap: 10px; @@ -137,6 +141,13 @@ font-size: 0.95em; } .notice a { color: inherit; font-weight: 600; } + .toolbar { + display: flex; + gap: 10px; + flex-wrap: wrap; + align-items: center; + margin-bottom: 12px; + } @@ -154,6 +165,16 @@

Loading...

+
+

Download Fonts

+
+ + +
+

Catalog not loaded

+
+
+

Upload Font

@@ -176,6 +197,13 @@ return bytes + ' B'; } + function setStatus(id, text, ok) { + const el = document.getElementById(id); + el.className = ok === undefined ? '' : (ok ? 'status-ok' : 'status-err'); + el.style.display = 'block'; + el.textContent = text; + } + async function loadFonts() { try { const res = await fetch('/api/fonts'); @@ -230,10 +258,7 @@ async function deleteFamily(name) { if (!confirm('Delete font family "' + name + '"?')) return; - const status = document.getElementById('status'); - status.className = ''; - status.style.display = 'block'; - status.textContent = 'Deleting ' + name + '...'; + setStatus('status', 'Deleting ' + name + '...'); try { const res = await fetch('/api/fonts/delete', { method: 'POST', @@ -241,17 +266,115 @@ body: JSON.stringify({family: name}) }); if (res.ok) { - status.className = 'status-ok'; - status.textContent = 'Deleted "' + name + '".'; + setStatus('status', 'Deleted "' + name + '".', true); } else { - status.className = 'status-err'; - status.textContent = 'Failed to delete "' + name + '".'; + setStatus('status', 'Failed to delete "' + name + '".', false); } } catch (err) { - status.className = 'status-err'; - status.textContent = 'Delete error: ' + err.message; + setStatus('status', 'Delete error: ' + err.message, false); } await loadFonts(); + await loadRemoteManifest(); + } + + async function loadRemoteManifest() { + const el = document.getElementById('remoteFamilies'); + el.innerHTML = '

Loading catalog...

'; + try { + const res = await fetch('/api/fonts/manifest'); + const data = await res.json(); + if (!res.ok || !data.ok) { + throw new Error(data.error || 'catalog unavailable'); + } + + el.innerHTML = ''; + if (!Array.isArray(data.families) || data.families.length === 0) { + el.innerHTML = '

No remote fonts found

'; + return; + } + + data.families.forEach(f => { + const row = document.createElement('div'); + row.className = 'family'; + + const info = document.createElement('div'); + info.className = 'family-info'; + + const title = document.createElement('h3'); + title.textContent = f.name; + + const meta = document.createElement('span'); + meta.className = 'family-meta'; + const state = f.installed ? (f.hasUpdate ? 'Installed, update available' : 'Installed, up to date') : 'Not installed'; + const details = state + ' - ' + (f.fileCount || 0) + ' file(s), ' + formatSize(f.totalSize || 0); + meta.textContent = f.description ? (f.description + ' - ' + details) : details; + + info.appendChild(title); + info.appendChild(meta); + + const btn = document.createElement('button'); + btn.className = 'btn btn-primary'; + btn.dataset.family = f.name; + btn.textContent = f.installed ? (f.hasUpdate ? 'Update' : 'Reinstall') : 'Download'; + + row.appendChild(info); + row.appendChild(btn); + el.appendChild(row); + }); + + el.querySelectorAll('button[data-family]').forEach(btn => { + btn.addEventListener('click', async () => { + btn.disabled = true; + await downloadRemoteFamily(btn.dataset.family); + btn.disabled = false; + }); + }); + } catch (err) { + el.innerHTML = '

Failed to load catalog: ' + err.message + '

'; + } + } + + async function downloadRemoteFamily(family) { + setStatus('remoteStatus', 'Downloading "' + family + '"...'); + try { + const res = await fetch('/api/fonts/download', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({family}) + }); + const data = await res.json(); + if (!res.ok || !data.ok) { + throw new Error(data.error || 'download failed'); + } + setStatus('remoteStatus', 'Installed "' + family + '".', true); + await loadFonts(); + await loadRemoteManifest(); + } catch (err) { + setStatus('remoteStatus', 'Install failed for "' + family + '": ' + err.message, false); + } + } + + async function downloadAllRemoteUpdates() { + setStatus('remoteStatus', 'Downloading all available updates...'); + const btn = document.getElementById('downloadAllRemote'); + btn.disabled = true; + try { + const res = await fetch('/api/fonts/download', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({all: true}) + }); + const data = await res.json(); + if (!res.ok || !data.ok) { + throw new Error(data.error || 'download failed'); + } + setStatus('remoteStatus', 'Installed/updated ' + (data.installedCount || 0) + ' family(s).', true); + await loadFonts(); + await loadRemoteManifest(); + } catch (err) { + setStatus('remoteStatus', 'Download-all failed: ' + err.message, false); + } + btn.disabled = false; } // Derive family name from a .cpfont filename: take everything before the @@ -293,25 +416,20 @@ const status = document.getElementById('status'); const files = cpfontFilesOnly(document.getElementById('fontFiles').files); if (files.length === 0) { - status.className = 'status-err'; - status.style.display = 'block'; - status.textContent = 'No .cpfont files selected.'; + setStatus('status', 'No .cpfont files selected.', false); return; } const families = files.map(f => sanitizeFamily(familyFromFilename(f.name))); const uniqueFamilies = new Set(families); if (uniqueFamilies.size !== 1) { - status.className = 'status-err'; - status.style.display = 'block'; - status.textContent = 'Selected files belong to multiple families.'; + setStatus('status', 'Selected files belong to multiple families.', false); return; } const family = families[0]; - status.className = ''; - status.style.display = 'block'; + setStatus('status', 'Preparing upload...'); let uploaded = 0; for (const file of files) { @@ -323,27 +441,29 @@ const res = await fetch('/api/fonts/upload', { method: 'POST', body: formData }); const data = await res.json(); if (!data.ok) { - status.className = 'status-err'; - status.textContent = 'Failed on ' + file.name + ': ' + (data.error || 'unknown error'); + setStatus('status', 'Failed on ' + file.name + ': ' + (data.error || 'unknown error'), false); await loadFonts(); return; } } catch (err) { - status.className = 'status-err'; - status.textContent = 'Upload error on ' + file.name + ': ' + err.message; + setStatus('status', 'Upload error on ' + file.name + ': ' + err.message, false); await loadFonts(); return; } uploaded++; } - status.className = 'status-ok'; - status.textContent = 'Uploaded ' + uploaded + ' file' + (uploaded === 1 ? '' : 's') + - ' to family "' + family + '".'; + setStatus('status', 'Uploaded ' + uploaded + ' file' + (uploaded === 1 ? '' : 's') + + ' to family "' + family + '".', true); await loadFonts(); + await loadRemoteManifest(); }); + document.getElementById('refreshManifest').addEventListener('click', loadRemoteManifest); + document.getElementById('downloadAllRemote').addEventListener('click', downloadAllRemoteUpdates); + loadFonts(); + loadRemoteManifest();