@@ -1 +0,0 @@
|
|||||||
../../.skills/SKILL.md
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
../../.skills/SKILL.md
|
||||||
@@ -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"}
|
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../../lib/EpdFont/scripts/sd-fonts.yaml
|
||||||
@@ -25,6 +25,7 @@ import argparse
|
|||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from concurrent.futures import ProcessPoolExecutor, as_completed
|
from concurrent.futures import ProcessPoolExecutor, as_completed
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -34,7 +35,8 @@ import yaml
|
|||||||
SCRIPT_DIR = Path(__file__).parent
|
SCRIPT_DIR = Path(__file__).parent
|
||||||
FONTCONVERT = SCRIPT_DIR / "fontconvert_sdcard.py"
|
FONTCONVERT = SCRIPT_DIR / "fontconvert_sdcard.py"
|
||||||
EPDFONTS_DIR = SCRIPT_DIR.parent # lib/EpdFont
|
EPDFONTS_DIR = SCRIPT_DIR.parent # lib/EpdFont
|
||||||
DEFAULT_CONFIG = SCRIPT_DIR / "sd-fonts.yaml"
|
PRIMARY_CONFIG = SCRIPT_DIR / "sd-fonts.yaml"
|
||||||
|
DEFAULT_CONFIG = PRIMARY_CONFIG
|
||||||
DEFAULT_OUTPUT = SCRIPT_DIR / "output"
|
DEFAULT_OUTPUT = SCRIPT_DIR / "output"
|
||||||
DOWNLOAD_DIR = SCRIPT_DIR / "downloaded_fonts"
|
DOWNLOAD_DIR = SCRIPT_DIR / "downloaded_fonts"
|
||||||
INSTANCE_DIR = SCRIPT_DIR / "instanced_fonts"
|
INSTANCE_DIR = SCRIPT_DIR / "instanced_fonts"
|
||||||
@@ -99,6 +101,11 @@ def resolve_font_path(style_spec: dict, family_name: str, style_name: str) -> Pa
|
|||||||
raise FileNotFoundError(f"{family_name}/{style_name}: {resolved} not found")
|
raise FileNotFoundError(f"{family_name}/{style_name}: {resolved} not found")
|
||||||
elif "url" in style_spec:
|
elif "url" in style_spec:
|
||||||
url = style_spec["url"]
|
url = style_spec["url"]
|
||||||
|
parsed = urllib.parse.urlparse(url)
|
||||||
|
if parsed.scheme not in ("http", "https"):
|
||||||
|
raise ValueError(
|
||||||
|
f"{family_name}/{style_name}: invalid URL scheme '{parsed.scheme or '<empty>'}'"
|
||||||
|
)
|
||||||
# Derive a stable filename from the URL
|
# Derive a stable filename from the URL
|
||||||
filename = url.rsplit("/", 1)[-1]
|
filename = url.rsplit("/", 1)[-1]
|
||||||
dest = DOWNLOAD_DIR / family_name / filename
|
dest = DOWNLOAD_DIR / family_name / filename
|
||||||
@@ -229,6 +236,9 @@ def main():
|
|||||||
parser.add_argument("--clean", action="store_true", help="Clean output directory before building")
|
parser.add_argument("--clean", action="store_true", help="Clean output directory before building")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# The assets copy is expected to be a link to PRIMARY_CONFIG.
|
||||||
|
# This script reads PRIMARY_CONFIG directly.
|
||||||
|
|
||||||
if args.manifest and not args.base_url:
|
if args.manifest and not args.base_url:
|
||||||
parser.error("--base-url is required when using --manifest")
|
parser.error("--base-url is required when using --manifest")
|
||||||
|
|
||||||
@@ -287,7 +297,14 @@ def main():
|
|||||||
for family in families
|
for family in families
|
||||||
}
|
}
|
||||||
for future in as_completed(futures):
|
for future in as_completed(futures):
|
||||||
name, success, message = future.result()
|
name = futures[future]
|
||||||
|
success = False
|
||||||
|
message = ""
|
||||||
|
try:
|
||||||
|
name, success, message = future.result()
|
||||||
|
except Exception as e:
|
||||||
|
success = False
|
||||||
|
message = str(e)
|
||||||
if success:
|
if success:
|
||||||
# Count output files
|
# Count output files
|
||||||
family_dir = output_base / name
|
family_dir = output_base / name
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ FontInstaller::FontInstaller(SdCardFontRegistry& registry) : registry_(registry)
|
|||||||
bool FontInstaller::isValidFamilyName(const char* name) {
|
bool FontInstaller::isValidFamilyName(const char* name) {
|
||||||
if (name == nullptr || name[0] == '\0') return false;
|
if (name == nullptr || name[0] == '\0') return false;
|
||||||
|
|
||||||
|
const size_t nameLen = strlen(name);
|
||||||
|
if (nameLen == 0 || nameLen > MAX_FAMILY_NAME_LEN) return false;
|
||||||
|
|
||||||
if (strstr(name, "..") != nullptr) return false;
|
if (strstr(name, "..") != nullptr) return false;
|
||||||
if (strchr(name, '/') != nullptr) return false;
|
if (strchr(name, '/') != nullptr) return false;
|
||||||
if (strchr(name, '\\') != nullptr) return false;
|
if (strchr(name, '\\') != nullptr) return false;
|
||||||
@@ -27,6 +30,19 @@ bool FontInstaller::isValidFamilyName(const char* name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool FontInstaller::ensureFamilyDir(const char* familyName) {
|
bool FontInstaller::ensureFamilyDir(const char* familyName) {
|
||||||
|
if (!isValidFamilyName(familyName)) {
|
||||||
|
LOG_ERR("FONT", "Invalid family name: %s", familyName ? familyName : "<null>");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t baseLen = strlen(SdCardFontRegistry::FONTS_DIR);
|
||||||
|
const size_t familyLen = strlen(familyName);
|
||||||
|
const size_t neededLen = baseLen + 1 + familyLen + 1; // "/" + NUL
|
||||||
|
if (neededLen > 128) {
|
||||||
|
LOG_ERR("FONT", "Family dir path too long: %s", familyName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!Storage.exists(SdCardFontRegistry::FONTS_DIR)) {
|
if (!Storage.exists(SdCardFontRegistry::FONTS_DIR)) {
|
||||||
if (!Storage.mkdir(SdCardFontRegistry::FONTS_DIR)) {
|
if (!Storage.mkdir(SdCardFontRegistry::FONTS_DIR)) {
|
||||||
LOG_ERR("FONT", "Failed to create fonts dir: %s", SdCardFontRegistry::FONTS_DIR);
|
LOG_ERR("FONT", "Failed to create fonts dir: %s", SdCardFontRegistry::FONTS_DIR);
|
||||||
@@ -79,6 +95,13 @@ FontInstaller::Error FontInstaller::deleteFamily(const char* familyName) {
|
|||||||
return Error::INVALID_FAMILY_NAME;
|
return Error::INVALID_FAMILY_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const size_t baseLen = strlen(SdCardFontRegistry::FONTS_DIR);
|
||||||
|
const size_t familyLen = strlen(familyName);
|
||||||
|
if (baseLen + 1 + familyLen + 1 > 128) {
|
||||||
|
LOG_ERR("FONT", "Family dir path too long: %s", familyName);
|
||||||
|
return Error::INVALID_FAMILY_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
char dirPath[128];
|
char dirPath[128];
|
||||||
snprintf(dirPath, sizeof(dirPath), "%s/%s", SdCardFontRegistry::FONTS_DIR, familyName);
|
snprintf(dirPath, sizeof(dirPath), "%s/%s", SdCardFontRegistry::FONTS_DIR, familyName);
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ class FontInstaller {
|
|||||||
|
|
||||||
explicit FontInstaller(SdCardFontRegistry& registry);
|
explicit FontInstaller(SdCardFontRegistry& registry);
|
||||||
|
|
||||||
|
static constexpr size_t MAX_FAMILY_NAME_LEN = 64;
|
||||||
|
|
||||||
/// Validate a family name: alphanumeric + hyphen + underscore only, no path traversal.
|
/// Validate a family name: alphanumeric + hyphen + underscore only, no path traversal.
|
||||||
static bool isValidFamilyName(const char* name);
|
static bool isValidFamilyName(const char* name);
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "SdCardFontGlobals.h"
|
#include "SdCardFontGlobals.h"
|
||||||
#include "activities/network/WifiSelectionActivity.h"
|
#include "activities/network/WifiSelectionActivity.h"
|
||||||
@@ -183,10 +185,26 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) {
|
|||||||
}
|
}
|
||||||
requestUpdateAndWait();
|
requestUpdateAndWait();
|
||||||
|
|
||||||
if (!fontInstaller_.ensureFamilyDir(family.name.c_str())) {
|
char liveDir[128];
|
||||||
|
char stagingDir[128];
|
||||||
|
char backupDir[128];
|
||||||
|
snprintf(liveDir, sizeof(liveDir), "%s/%s", SdCardFontRegistry::FONTS_DIR, family.name.c_str());
|
||||||
|
snprintf(stagingDir, sizeof(stagingDir), "%s/%s__staging", SdCardFontRegistry::FONTS_DIR, family.name.c_str());
|
||||||
|
snprintf(backupDir, sizeof(backupDir), "%s/%s__backup", SdCardFontRegistry::FONTS_DIR, family.name.c_str());
|
||||||
|
|
||||||
|
if (Storage.exists(stagingDir) && !Storage.removeDir(stagingDir)) {
|
||||||
|
LOG_ERR("FONT", "Failed to clean staging dir: %s", stagingDir);
|
||||||
RenderLock lock(*this);
|
RenderLock lock(*this);
|
||||||
state_ = ERROR;
|
state_ = ERROR;
|
||||||
errorMessage_ = "Failed to create font directory";
|
errorMessage_ = "Failed to prepare staging area";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Storage.mkdir(stagingDir)) {
|
||||||
|
LOG_ERR("FONT", "Failed to create staging dir: %s", stagingDir);
|
||||||
|
RenderLock lock(*this);
|
||||||
|
state_ = ERROR;
|
||||||
|
errorMessage_ = "Failed to create staging area";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,12 +219,12 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) {
|
|||||||
}
|
}
|
||||||
requestUpdateAndWait();
|
requestUpdateAndWait();
|
||||||
|
|
||||||
char destPath[128];
|
char stagedPath[128];
|
||||||
FontInstaller::buildFontPath(family.name.c_str(), file.name.c_str(), destPath, sizeof(destPath));
|
snprintf(stagedPath, sizeof(stagedPath), "%s/%s", stagingDir, file.name.c_str());
|
||||||
|
|
||||||
std::string url = baseUrl_ + file.name;
|
std::string url = baseUrl_ + file.name;
|
||||||
|
|
||||||
auto result = HttpDownloader::downloadToFile(url, destPath, [this](size_t downloaded, size_t total) {
|
auto result = HttpDownloader::downloadToFile(url, stagedPath, [this](size_t downloaded, size_t total) {
|
||||||
fileProgress_ = downloaded;
|
fileProgress_ = downloaded;
|
||||||
fileTotal_ = total;
|
fileTotal_ = total;
|
||||||
requestUpdate(true);
|
requestUpdate(true);
|
||||||
@@ -214,20 +232,16 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) {
|
|||||||
|
|
||||||
if (result != HttpDownloader::OK) {
|
if (result != HttpDownloader::OK) {
|
||||||
LOG_ERR("FONT", "Download failed: %s (%d)", file.name.c_str(), result);
|
LOG_ERR("FONT", "Download failed: %s (%d)", file.name.c_str(), result);
|
||||||
fontInstaller_.deleteFamily(family.name.c_str());
|
Storage.removeDir(stagingDir);
|
||||||
family.installed = false;
|
|
||||||
family.hasUpdate = false;
|
|
||||||
RenderLock lock(*this);
|
RenderLock lock(*this);
|
||||||
state_ = ERROR;
|
state_ = ERROR;
|
||||||
errorMessage_ = "Download failed: " + file.name;
|
errorMessage_ = "Download failed: " + file.name;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fontInstaller_.validateCpfontFile(destPath)) {
|
if (!fontInstaller_.validateCpfontFile(stagedPath)) {
|
||||||
LOG_ERR("FONT", "Invalid .cpfont: %s", destPath);
|
LOG_ERR("FONT", "Invalid .cpfont: %s", stagedPath);
|
||||||
fontInstaller_.deleteFamily(family.name.c_str());
|
Storage.removeDir(stagingDir);
|
||||||
family.installed = false;
|
|
||||||
family.hasUpdate = false;
|
|
||||||
RenderLock lock(*this);
|
RenderLock lock(*this);
|
||||||
state_ = ERROR;
|
state_ = ERROR;
|
||||||
errorMessage_ = "Invalid font file: " + file.name;
|
errorMessage_ = "Invalid font file: " + file.name;
|
||||||
@@ -235,8 +249,45 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bool hadLiveDir = Storage.exists(liveDir);
|
||||||
|
|
||||||
|
if (Storage.exists(backupDir) && !Storage.removeDir(backupDir)) {
|
||||||
|
LOG_ERR("FONT", "Failed to clean backup dir: %s", backupDir);
|
||||||
|
Storage.removeDir(stagingDir);
|
||||||
|
RenderLock lock(*this);
|
||||||
|
state_ = ERROR;
|
||||||
|
errorMessage_ = "Failed to prepare backup area";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hadLiveDir && !Storage.rename(liveDir, backupDir)) {
|
||||||
|
LOG_ERR("FONT", "Failed to move live family to backup: %s", liveDir);
|
||||||
|
Storage.removeDir(stagingDir);
|
||||||
|
RenderLock lock(*this);
|
||||||
|
state_ = ERROR;
|
||||||
|
errorMessage_ = "Failed to replace installed font";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Storage.rename(stagingDir, liveDir)) {
|
||||||
|
LOG_ERR("FONT", "Failed to activate staged family: %s", stagingDir);
|
||||||
|
if (hadLiveDir && Storage.exists(backupDir)) {
|
||||||
|
Storage.rename(backupDir, liveDir);
|
||||||
|
}
|
||||||
|
Storage.removeDir(stagingDir);
|
||||||
|
RenderLock lock(*this);
|
||||||
|
state_ = ERROR;
|
||||||
|
errorMessage_ = "Failed to finalize font install";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Storage.exists(backupDir) && !Storage.removeDir(backupDir)) {
|
||||||
|
LOG_INF("FONT", "Failed to remove backup dir after successful install: %s", backupDir);
|
||||||
|
}
|
||||||
|
|
||||||
fontInstaller_.refreshRegistry();
|
fontInstaller_.refreshRegistry();
|
||||||
family.installed = true;
|
family.installed = true;
|
||||||
|
family.hasUpdate = false;
|
||||||
|
|
||||||
RenderLock lock(*this);
|
RenderLock lock(*this);
|
||||||
state_ = COMPLETE;
|
state_ = COMPLETE;
|
||||||
|
|||||||
@@ -12,17 +12,22 @@
|
|||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
|
#include "FontInstaller.h"
|
||||||
#include "OpdsServerStore.h"
|
#include "OpdsServerStore.h"
|
||||||
|
#include "SdCardFontGlobals.h"
|
||||||
|
#include "SdCardFontRegistry.h"
|
||||||
#include "SettingsList.h"
|
#include "SettingsList.h"
|
||||||
#include "SystemStatus.h"
|
#include "SystemStatus.h"
|
||||||
#include "WebDAVHandler.h"
|
#include "WebDAVHandler.h"
|
||||||
#include "WifiCredentialStore.h"
|
#include "WifiCredentialStore.h"
|
||||||
#include "html/FilesPageHtml.generated.h"
|
#include "html/FilesPageHtml.generated.h"
|
||||||
|
#include "html/FontsPageHtml.generated.h"
|
||||||
#include "html/HomePageHtml.generated.h"
|
#include "html/HomePageHtml.generated.h"
|
||||||
#include "html/SettingsPageHtml.generated.h"
|
#include "html/SettingsPageHtml.generated.h"
|
||||||
#include "html/WelcomePageHtml.generated.h"
|
#include "html/WelcomePageHtml.generated.h"
|
||||||
#include "html/js/jszip_minJs.generated.h"
|
#include "html/js/jszip_minJs.generated.h"
|
||||||
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// Folders/files to hide from the web interface file browser
|
// Folders/files to hide from the web interface file browser
|
||||||
// Note: Items starting with "." are automatically hidden
|
// Note: Items starting with "." are automatically hidden
|
||||||
@@ -196,6 +201,12 @@ void CrossPointWebServer::begin() {
|
|||||||
server->on("/api/settings", HTTP_GET, [this] { handleGetSettings(); });
|
server->on("/api/settings", HTTP_GET, [this] { handleGetSettings(); });
|
||||||
server->on("/api/settings", HTTP_POST, [this] { handlePostSettings(); });
|
server->on("/api/settings", HTTP_POST, [this] { handlePostSettings(); });
|
||||||
|
|
||||||
|
// Font management endpoints
|
||||||
|
server->on("/fonts", HTTP_GET, [this] { handleFontsPage(); });
|
||||||
|
server->on("/api/fonts", HTTP_GET, [this] { handleFontList(); });
|
||||||
|
server->on("/api/fonts/upload", HTTP_POST, [this] { handleFontUpload(); }, [this] { handleFontUploadData(); });
|
||||||
|
server->on("/api/fonts/delete", HTTP_POST, [this] { handleFontDelete(); });
|
||||||
|
|
||||||
// OPDS server endpoints
|
// OPDS server endpoints
|
||||||
server->on("/api/opds", HTTP_GET, [this] { handleGetOpdsServers(); });
|
server->on("/api/opds", HTTP_GET, [this] { handleGetOpdsServers(); });
|
||||||
server->on("/api/opds", HTTP_POST, [this] { handlePostOpdsServer(); });
|
server->on("/api/opds", HTTP_POST, [this] { handlePostOpdsServer(); });
|
||||||
@@ -1399,6 +1410,193 @@ void CrossPointWebServer::handlePostSettings() {
|
|||||||
server->send(200, "text/plain", String("Applied ") + String(applied) + " setting(s)");
|
server->send(200, "text/plain", String("Applied ") + String(applied) + " setting(s)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Font Management API ----
|
||||||
|
|
||||||
|
void CrossPointWebServer::handleFontsPage() const {
|
||||||
|
sendHtmlContent(server.get(), FontsPageHtml, sizeof(FontsPageHtml));
|
||||||
|
LOG_DBG("WEB", "Served fonts page");
|
||||||
|
}
|
||||||
|
|
||||||
|
void CrossPointWebServer::handleFontList() {
|
||||||
|
FontInstaller installer(sdFontSystem.registry());
|
||||||
|
installer.refreshRegistry();
|
||||||
|
const auto& families = sdFontSystem.registry().getFamilies();
|
||||||
|
|
||||||
|
JsonDocument doc;
|
||||||
|
JsonArray arr = doc["families"].to<JsonArray>();
|
||||||
|
doc["maxFamilies"] = SdCardFontRegistry::MAX_SD_FAMILIES;
|
||||||
|
|
||||||
|
for (const auto& family : families) {
|
||||||
|
JsonObject fObj = arr.add<JsonObject>();
|
||||||
|
fObj["name"] = family.name;
|
||||||
|
|
||||||
|
JsonArray sizes = fObj["sizes"].to<JsonArray>();
|
||||||
|
for (uint8_t s : family.availableSizes()) {
|
||||||
|
sizes.add(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonArray files = fObj["files"].to<JsonArray>();
|
||||||
|
for (const auto& file : family.files) {
|
||||||
|
JsonObject fileObj = files.add<JsonObject>();
|
||||||
|
const char* name = strrchr(file.path.c_str(), '/');
|
||||||
|
fileObj["name"] = name ? name + 1 : file.path.c_str();
|
||||||
|
|
||||||
|
FsFile f;
|
||||||
|
if (Storage.openFileForRead("WEB", file.path.c_str(), f)) {
|
||||||
|
fileObj["size"] = static_cast<unsigned long>(f.size());
|
||||||
|
f.close();
|
||||||
|
} else {
|
||||||
|
fileObj["size"] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String json;
|
||||||
|
serializeJson(doc, json);
|
||||||
|
server->send(200, "application/json", json);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CrossPointWebServer::handleFontUploadData() {
|
||||||
|
HTTPUpload& up = server->upload();
|
||||||
|
|
||||||
|
switch (up.status) {
|
||||||
|
case UPLOAD_FILE_START: {
|
||||||
|
esp_task_wdt_reset();
|
||||||
|
String family = server->arg("family");
|
||||||
|
fontUpload.valid = false;
|
||||||
|
fontUpload.magicChecked = false;
|
||||||
|
fontUpload.bytesWritten = 0;
|
||||||
|
fontUpload.bufferPos = 0;
|
||||||
|
|
||||||
|
if (!FontInstaller::isValidFamilyName(family.c_str())) {
|
||||||
|
LOG_ERR("WEB", "Invalid font family name: %s", family.c_str());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
String filename = up.filename;
|
||||||
|
if (!filename.endsWith(".cpfont")) {
|
||||||
|
LOG_ERR("WEB", "Not a .cpfont file: %s", filename.c_str());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
fontUpload.familyName = family.c_str();
|
||||||
|
|
||||||
|
FontInstaller installer(sdFontSystem.registry());
|
||||||
|
if (!installer.ensureFamilyDir(family.c_str())) {
|
||||||
|
LOG_ERR("WEB", "Failed to create font family dir");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
char path[128];
|
||||||
|
FontInstaller::buildFontPath(family.c_str(), filename.c_str(), path, sizeof(path));
|
||||||
|
fontUpload.filePath = path;
|
||||||
|
|
||||||
|
if (!Storage.openFileForWrite("WEB", path, fontUpload.file)) {
|
||||||
|
LOG_ERR("WEB", "Failed to open font file for write: %s", path);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
fontUpload.valid = true;
|
||||||
|
LOG_DBG("WEB", "Font upload started: %s -> %s", filename.c_str(), path);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case UPLOAD_FILE_WRITE: {
|
||||||
|
if (!fontUpload.valid) break;
|
||||||
|
esp_task_wdt_reset();
|
||||||
|
|
||||||
|
if (!fontUpload.magicChecked && up.currentSize >= 8) {
|
||||||
|
if (memcmp(up.buf, "CPFONT\0\0", 8) != 0) {
|
||||||
|
LOG_ERR("WEB", "Invalid .cpfont magic bytes");
|
||||||
|
fontUpload.valid = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
fontUpload.magicChecked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t remaining = up.currentSize;
|
||||||
|
const uint8_t* src = up.buf;
|
||||||
|
while (remaining > 0) {
|
||||||
|
size_t space = FontUploadState::BUFFER_SIZE - fontUpload.bufferPos;
|
||||||
|
size_t chunk = (remaining < space) ? remaining : space;
|
||||||
|
memcpy(fontUpload.buffer.data() + fontUpload.bufferPos, src, chunk);
|
||||||
|
fontUpload.bufferPos += chunk;
|
||||||
|
src += chunk;
|
||||||
|
remaining -= chunk;
|
||||||
|
|
||||||
|
if (fontUpload.bufferPos >= FontUploadState::BUFFER_SIZE) {
|
||||||
|
fontUpload.file.write(fontUpload.buffer.data(), fontUpload.bufferPos);
|
||||||
|
fontUpload.bytesWritten += fontUpload.bufferPos;
|
||||||
|
fontUpload.bufferPos = 0;
|
||||||
|
esp_task_wdt_reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case UPLOAD_FILE_END: {
|
||||||
|
if (fontUpload.valid && fontUpload.bufferPos > 0) {
|
||||||
|
fontUpload.file.write(fontUpload.buffer.data(), fontUpload.bufferPos);
|
||||||
|
fontUpload.bytesWritten += fontUpload.bufferPos;
|
||||||
|
fontUpload.bufferPos = 0;
|
||||||
|
}
|
||||||
|
fontUpload.file.close();
|
||||||
|
|
||||||
|
if (!fontUpload.valid && !fontUpload.filePath.empty()) {
|
||||||
|
Storage.remove(fontUpload.filePath.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("WEB", "Font upload end: valid=%d, %zu bytes", fontUpload.valid, fontUpload.bytesWritten);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case UPLOAD_FILE_ABORTED: {
|
||||||
|
fontUpload.file.close();
|
||||||
|
if (!fontUpload.filePath.empty()) {
|
||||||
|
Storage.remove(fontUpload.filePath.c_str());
|
||||||
|
}
|
||||||
|
fontUpload.valid = false;
|
||||||
|
LOG_DBG("WEB", "Font upload aborted");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CrossPointWebServer::handleFontUpload() {
|
||||||
|
if (fontUpload.valid) {
|
||||||
|
FontInstaller installer(sdFontSystem.registry());
|
||||||
|
installer.refreshRegistry();
|
||||||
|
server->send(200, "application/json", "{\"ok\":true}");
|
||||||
|
LOG_DBG("WEB", "Font upload complete: %s", fontUpload.filePath.c_str());
|
||||||
|
} else {
|
||||||
|
server->send(400, "application/json", "{\"error\":\"Invalid .cpfont file\"}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CrossPointWebServer::handleFontDelete() {
|
||||||
|
String body = server->arg("plain");
|
||||||
|
JsonDocument doc;
|
||||||
|
DeserializationError err = deserializeJson(doc, body);
|
||||||
|
|
||||||
|
if (err || !doc["family"].is<const char*>()) {
|
||||||
|
server->send(400, "application/json", "{\"error\":\"Invalid request\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* familyName = doc["family"];
|
||||||
|
FontInstaller installer(sdFontSystem.registry());
|
||||||
|
auto result = installer.deleteFamily(familyName);
|
||||||
|
|
||||||
|
if (result == FontInstaller::Error::OK) {
|
||||||
|
installer.refreshRegistry();
|
||||||
|
server->send(200, "application/json", "{\"ok\":true}");
|
||||||
|
LOG_DBG("WEB", "Deleted font family: %s", familyName);
|
||||||
|
} else {
|
||||||
|
server->send(500, "application/json", "{\"error\":\"Delete failed\"}");
|
||||||
|
LOG_ERR("WEB", "Failed to delete font family: %s", familyName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Wi-Fi Credentials API ----
|
// ---- Wi-Fi Credentials API ----
|
||||||
|
|
||||||
void CrossPointWebServer::handleGetWifiNetworks() const {
|
void CrossPointWebServer::handleGetWifiNetworks() const {
|
||||||
|
|||||||
@@ -110,6 +110,27 @@ class CrossPointWebServer {
|
|||||||
void handleGetSettings() const;
|
void handleGetSettings() const;
|
||||||
void handlePostSettings();
|
void handlePostSettings();
|
||||||
|
|
||||||
|
// Font management handlers
|
||||||
|
void handleFontsPage() const;
|
||||||
|
void handleFontList();
|
||||||
|
void handleFontUpload();
|
||||||
|
void handleFontUploadData();
|
||||||
|
void handleFontDelete();
|
||||||
|
|
||||||
|
struct FontUploadState {
|
||||||
|
FsFile file;
|
||||||
|
std::string familyName;
|
||||||
|
std::string filePath;
|
||||||
|
bool valid = false;
|
||||||
|
bool magicChecked = false;
|
||||||
|
size_t bytesWritten = 0;
|
||||||
|
static constexpr size_t BUFFER_SIZE = 4096;
|
||||||
|
std::vector<uint8_t> buffer;
|
||||||
|
size_t bufferPos = 0;
|
||||||
|
|
||||||
|
FontUploadState() { buffer.resize(BUFFER_SIZE); }
|
||||||
|
} fontUpload;
|
||||||
|
|
||||||
// OPDS server handlers
|
// OPDS server handlers
|
||||||
void handleGetOpdsServers() const;
|
void handleGetOpdsServers() const;
|
||||||
void handlePostOpdsServer();
|
void handlePostOpdsServer();
|
||||||
|
|||||||
@@ -181,22 +181,44 @@
|
|||||||
const res = await fetch('/api/fonts');
|
const res = await fetch('/api/fonts');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const el = document.getElementById('families');
|
const el = document.getElementById('families');
|
||||||
|
el.innerHTML = '';
|
||||||
if (!data.families || data.families.length === 0) {
|
if (!data.families || data.families.length === 0) {
|
||||||
el.innerHTML = '<p class="empty">No fonts installed</p>';
|
const p = document.createElement('p');
|
||||||
|
p.className = 'empty';
|
||||||
|
p.textContent = 'No fonts installed';
|
||||||
|
el.appendChild(p);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
el.innerHTML = data.families.map(f => `
|
|
||||||
<div class="family">
|
data.families.forEach(f => {
|
||||||
<div class="family-info">
|
const familyRow = document.createElement('div');
|
||||||
<h3>${f.name}</h3>
|
familyRow.className = 'family';
|
||||||
<span class="family-meta">
|
|
||||||
${f.sizes.join(', ')}pt ·
|
const info = document.createElement('div');
|
||||||
${f.files.map(fi => formatSize(fi.size)).join(' + ')}
|
info.className = 'family-info';
|
||||||
</span>
|
|
||||||
</div>
|
const title = document.createElement('h3');
|
||||||
<button class="btn btn-danger" data-family="${f.name}">Delete</button>
|
title.textContent = f.name;
|
||||||
</div>
|
|
||||||
`).join('');
|
const meta = document.createElement('span');
|
||||||
|
meta.className = 'family-meta';
|
||||||
|
const sizes = Array.isArray(f.sizes) ? f.sizes.join(', ') : '';
|
||||||
|
const fileSizes = Array.isArray(f.files) ? f.files.map(fi => formatSize(fi.size)).join(' + ') : '';
|
||||||
|
meta.textContent = sizes + 'pt - ' + fileSizes;
|
||||||
|
|
||||||
|
info.appendChild(title);
|
||||||
|
info.appendChild(meta);
|
||||||
|
|
||||||
|
const delBtn = document.createElement('button');
|
||||||
|
delBtn.className = 'btn btn-danger';
|
||||||
|
delBtn.dataset.family = f.name;
|
||||||
|
delBtn.textContent = 'Delete';
|
||||||
|
|
||||||
|
familyRow.appendChild(info);
|
||||||
|
familyRow.appendChild(delBtn);
|
||||||
|
el.appendChild(familyRow);
|
||||||
|
});
|
||||||
|
|
||||||
el.querySelectorAll('button[data-family]').forEach(btn => {
|
el.querySelectorAll('button[data-family]').forEach(btn => {
|
||||||
btn.addEventListener('click', () => deleteFamily(btn.dataset.family));
|
btn.addEventListener('click', () => deleteFamily(btn.dataset.family));
|
||||||
});
|
});
|
||||||
@@ -272,7 +294,16 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const family = sanitizeFamily(familyFromFilename(files[0].name));
|
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.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const family = families[0];
|
||||||
|
|
||||||
status.className = '';
|
status.className = '';
|
||||||
status.style.display = 'block';
|
status.style.display = 'block';
|
||||||
|
|||||||
@@ -107,6 +107,7 @@
|
|||||||
<div class="nav-links">
|
<div class="nav-links">
|
||||||
<a href="/files">Open File Manager</a>
|
<a href="/files">Open File Manager</a>
|
||||||
<a href="/settings">Settings</a>
|
<a href="/settings">Settings</a>
|
||||||
|
<a href="/fonts">Font Manager</a>
|
||||||
<a href="/systeminfo">System Info</a>
|
<a href="/systeminfo">System Info</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer">Fast start page designed for weak connections</div>
|
<div class="footer">Fast start page designed for weak connections</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user