Merge remote-tracking branch 'origin/master' into feat-sdfonts-ui
# Conflicts: # assets/sd-fonts/sd-fonts.yaml # assets/sd-fonts/sd-fonts.yaml~HEAD
This commit is contained in:
+293
-293
@@ -1,293 +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"}
|
||||
# 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"}
|
||||
|
||||
+89
-38
@@ -52,6 +52,21 @@ void SdCardFont::freeStyleMiniData(PerStyle& s) {
|
||||
s.miniIntervalCount = 0;
|
||||
s.miniGlyphCount = 0;
|
||||
s.miniMode = PerStyle::MiniMode::NONE;
|
||||
// Clear dangling pointers in miniData and stubData (kern data points to freed mini arrays)
|
||||
s.miniData.kernLeftClasses = nullptr;
|
||||
s.miniData.kernRightClasses = nullptr;
|
||||
s.miniData.kernMatrix = nullptr;
|
||||
s.miniData.kernLeftEntryCount = 0;
|
||||
s.miniData.kernRightEntryCount = 0;
|
||||
s.miniData.kernLeftClassCount = 0;
|
||||
s.miniData.kernRightClassCount = 0;
|
||||
s.stubData.kernLeftClasses = nullptr;
|
||||
s.stubData.kernRightClasses = nullptr;
|
||||
s.stubData.kernMatrix = nullptr;
|
||||
s.stubData.kernLeftEntryCount = 0;
|
||||
s.stubData.kernRightEntryCount = 0;
|
||||
s.stubData.kernLeftClassCount = 0;
|
||||
s.stubData.kernRightClassCount = 0;
|
||||
// NOTE: reportedMissCount is intentionally NOT reset here. The merge path
|
||||
// calls freeStyleMiniData() to swap mini buffers, and resetting the miss
|
||||
// tracker every paragraph would re-spam the log for the same 4 missing cps.
|
||||
@@ -66,11 +81,17 @@ void SdCardFont::freeStyleMiniData(PerStyle& s) {
|
||||
void SdCardFont::freeStyleKernLigatureData(PerStyle& s) {
|
||||
delete[] s.kernLeftClasses;
|
||||
s.kernLeftClasses = nullptr;
|
||||
s.kernClassesLoaded = false;
|
||||
delete[] s.kernRightClasses;
|
||||
s.kernRightClasses = nullptr;
|
||||
delete[] s.ligaturePairs;
|
||||
s.ligaturePairs = nullptr;
|
||||
s.kernLigLoaded = false;
|
||||
s.ligLoaded = false;
|
||||
// Clear dangling pointers in EpdFontData structs
|
||||
s.stubData.ligaturePairs = nullptr;
|
||||
s.stubData.ligaturePairCount = 0;
|
||||
s.miniData.ligaturePairs = nullptr;
|
||||
s.miniData.ligaturePairCount = 0;
|
||||
}
|
||||
|
||||
void SdCardFont::freeStyleMiniKern(PerStyle& s) {
|
||||
@@ -134,14 +155,17 @@ void SdCardFont::applyKernLigaturePointers(const PerStyle& s, EpdFontData& data)
|
||||
data.ligaturePairCount = s.header.ligaturePairCount;
|
||||
}
|
||||
|
||||
bool SdCardFont::loadStyleKernLigatureData(PerStyle& s) {
|
||||
if (s.kernLigLoaded) return true;
|
||||
bool hasKern = s.header.kernLeftEntryCount > 0;
|
||||
bool hasLig = s.header.ligaturePairCount > 0;
|
||||
if (!hasKern && !hasLig) {
|
||||
s.kernLigLoaded = true;
|
||||
return true;
|
||||
}
|
||||
bool SdCardFont::loadStyleKernLigatureData(PerStyle& s, bool ligatureOnly) {
|
||||
// During metadata-only (layout) prewarms, skip the kern class tables: the kern
|
||||
// matrix is never built at layout time so getKerning() returns 0 regardless.
|
||||
// Skipping them saves ~4KB per style (~17KB total for 4 styles), preventing OOM
|
||||
// on low-heap devices when long paragraphs try to grow their word vector.
|
||||
const bool wantKern = !ligatureOnly && s.header.kernLeftEntryCount > 0;
|
||||
const bool wantLig = s.header.ligaturePairCount > 0;
|
||||
|
||||
const bool kernDone = !wantKern || s.kernClassesLoaded;
|
||||
const bool ligDone = !wantLig || s.ligLoaded;
|
||||
if (kernDone && ligDone) return true;
|
||||
|
||||
FsFile file;
|
||||
if (!Storage.openFileForRead("SDCF", filePath_, file)) {
|
||||
@@ -149,72 +173,79 @@ bool SdCardFont::loadStyleKernLigatureData(PerStyle& s) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasKern) {
|
||||
if (wantKern && !s.kernClassesLoaded) {
|
||||
// Load only the small class-lookup tables (~3KB each). The full matrix
|
||||
// (~36KB contiguous for Literata) is built per-page from SD in
|
||||
// buildMiniKernMatrix().
|
||||
s.kernLeftClasses = new (std::nothrow) EpdKernClassEntry[s.header.kernLeftEntryCount];
|
||||
s.kernRightClasses = new (std::nothrow) EpdKernClassEntry[s.header.kernRightEntryCount];
|
||||
EpdKernClassEntry* newLeft = new (std::nothrow) EpdKernClassEntry[s.header.kernLeftEntryCount];
|
||||
EpdKernClassEntry* newRight = new (std::nothrow) EpdKernClassEntry[s.header.kernRightEntryCount];
|
||||
|
||||
if (!s.kernLeftClasses || !s.kernRightClasses) {
|
||||
if (!newLeft || !newRight) {
|
||||
delete[] newLeft;
|
||||
delete[] newRight;
|
||||
LOG_ERR("SDCF", "Failed to allocate kern classes (%u+%u bytes)", s.header.kernLeftEntryCount * 3u,
|
||||
s.header.kernRightEntryCount * 3u);
|
||||
freeStyleKernLigatureData(s);
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!file.seekSet(s.kernLeftFileOffset)) {
|
||||
delete[] newLeft;
|
||||
delete[] newRight;
|
||||
LOG_ERR("SDCF", "Failed to seek to kern data");
|
||||
freeStyleKernLigatureData(s);
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
size_t leftSz = s.header.kernLeftEntryCount * sizeof(EpdKernClassEntry);
|
||||
size_t rightSz = s.header.kernRightEntryCount * sizeof(EpdKernClassEntry);
|
||||
if (file.read(reinterpret_cast<uint8_t*>(s.kernLeftClasses), leftSz) != static_cast<int>(leftSz) ||
|
||||
file.read(reinterpret_cast<uint8_t*>(s.kernRightClasses), rightSz) != static_cast<int>(rightSz)) {
|
||||
if (file.read(reinterpret_cast<uint8_t*>(newLeft), leftSz) != static_cast<int>(leftSz) ||
|
||||
file.read(reinterpret_cast<uint8_t*>(newRight), rightSz) != static_cast<int>(rightSz)) {
|
||||
delete[] newLeft;
|
||||
delete[] newRight;
|
||||
LOG_ERR("SDCF", "Failed to read kern classes");
|
||||
freeStyleKernLigatureData(s);
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
s.kernLeftClasses = newLeft;
|
||||
s.kernRightClasses = newRight;
|
||||
s.kernClassesLoaded = true;
|
||||
}
|
||||
|
||||
if (hasLig) {
|
||||
s.ligaturePairs = new (std::nothrow) EpdLigaturePair[s.header.ligaturePairCount];
|
||||
if (!s.ligaturePairs) {
|
||||
if (wantLig && !s.ligLoaded) {
|
||||
EpdLigaturePair* newLig = new (std::nothrow) EpdLigaturePair[s.header.ligaturePairCount];
|
||||
if (!newLig) {
|
||||
LOG_ERR("SDCF", "Failed to allocate ligature pairs");
|
||||
freeStyleKernLigatureData(s);
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
if (!file.seekSet(s.ligatureFileOffset)) {
|
||||
delete[] newLig;
|
||||
LOG_ERR("SDCF", "Failed to seek to ligature data");
|
||||
freeStyleKernLigatureData(s);
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
size_t sz = s.header.ligaturePairCount * sizeof(EpdLigaturePair);
|
||||
if (file.read(reinterpret_cast<uint8_t*>(s.ligaturePairs), sz) != static_cast<int>(sz)) {
|
||||
if (file.read(reinterpret_cast<uint8_t*>(newLig), sz) != static_cast<int>(sz)) {
|
||||
delete[] newLig;
|
||||
LOG_ERR("SDCF", "Failed to read ligature pairs");
|
||||
freeStyleKernLigatureData(s);
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
s.ligaturePairs = newLig;
|
||||
s.ligLoaded = true;
|
||||
|
||||
// Make ligatures visible to the stub (used when no mini data built yet).
|
||||
// Kern stays nullptr on the stub — it is only wired in miniData via
|
||||
// applyKernLigaturePointers() after buildMiniKernMatrix() runs.
|
||||
s.stubData.ligaturePairs = s.ligaturePairs;
|
||||
s.stubData.ligaturePairCount = s.header.ligaturePairCount;
|
||||
}
|
||||
|
||||
file.close();
|
||||
s.kernLigLoaded = true;
|
||||
|
||||
// Make ligatures visible to the stub (used when no mini data built yet).
|
||||
// Kern stays nullptr on the stub — it is only wired in miniData via
|
||||
// applyKernLigaturePointers() after buildMiniKernMatrix() runs.
|
||||
s.stubData.ligaturePairs = s.ligaturePairs;
|
||||
s.stubData.ligaturePairCount = s.header.ligaturePairCount;
|
||||
|
||||
LOG_DBG("SDCF", "Kern classes + lig loaded: kernL=%u, kernR=%u, ligs=%u", s.header.kernLeftEntryCount,
|
||||
s.header.kernRightEntryCount, s.header.ligaturePairCount);
|
||||
LOG_DBG("SDCF", "Kern/lig loaded: kernL=%u kernR=%u ligs=%u ligOnly=%d",
|
||||
s.kernClassesLoaded ? s.header.kernLeftEntryCount : 0u,
|
||||
s.kernClassesLoaded ? s.header.kernRightEntryCount : 0u, s.ligLoaded ? s.header.ligaturePairCount : 0u,
|
||||
ligatureOnly);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -653,7 +684,7 @@ int SdCardFont::prewarm(const char* utf8Text, uint8_t styleMask, bool metadataOn
|
||||
if (!(styleMask & (1 << si)) || !styles_[si].present) continue;
|
||||
auto& s = styles_[si];
|
||||
|
||||
loadStyleKernLigatureData(s);
|
||||
loadStyleKernLigatureData(s, /*ligatureOnly=*/true);
|
||||
if (s.ligaturePairs && s.header.ligaturePairCount > 0) {
|
||||
for (uint8_t li = 0; li < s.header.ligaturePairCount && cpCount < MAX_PAGE_GLYPHS; li++) {
|
||||
uint32_t leftCp = s.ligaturePairs[li].pair >> 16;
|
||||
@@ -736,6 +767,18 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
|
||||
// For metadata-only calls, METADATA or FULL cache both satisfy layout queries.
|
||||
// For full (bitmap) calls, only FULL satisfies — METADATA lacks bitmap data.
|
||||
if (metadataOnly || s.miniMode == PerStyle::MiniMode::FULL) {
|
||||
// Ensure ligature metadata is wired if requested but not yet wired.
|
||||
if (loadKernLigatureData && !s.ligLoaded) {
|
||||
loadStyleKernLigatureData(s, /*ligatureOnly=*/true);
|
||||
}
|
||||
if (loadKernLigatureData && s.ligLoaded && s.miniData.ligaturePairs == nullptr) {
|
||||
if (s.miniMode == PerStyle::MiniMode::FULL) {
|
||||
applyKernLigaturePointers(s, s.miniData);
|
||||
} else {
|
||||
s.miniData.ligaturePairs = s.ligaturePairs;
|
||||
s.miniData.ligaturePairCount = s.header.ligaturePairCount;
|
||||
}
|
||||
}
|
||||
// Already wired into miniData; nothing else to do.
|
||||
return 0;
|
||||
}
|
||||
@@ -1143,7 +1186,7 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
|
||||
kernLigOk = buildMiniKernMatrix(s, codepoints, cpCount);
|
||||
}
|
||||
} else if (loadKernLigatureData) {
|
||||
loadStyleKernLigatureData(s);
|
||||
loadStyleKernLigatureData(s, /*ligatureOnly=*/true);
|
||||
// Don't set kernLigOk → mini kern matrix stays null on miniData, but
|
||||
// ligatures are still resident on stubData (set in loadStyleKernLigatureData).
|
||||
}
|
||||
@@ -1161,7 +1204,7 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
|
||||
if (kernLigOk) {
|
||||
// Full prewarm: wire mini kern matrix + class tables + ligatures.
|
||||
applyKernLigaturePointers(s, s.miniData);
|
||||
} else if (loadKernLigatureData && s.kernLigLoaded) {
|
||||
} else if (loadKernLigatureData && s.ligLoaded) {
|
||||
// Layout-only prewarm: wire ligatures so applyLigatures() works (e.g. "fi"
|
||||
// measures correctly). Skip the kern matrix — getKerning() returns 0
|
||||
// cleanly when kernMatrix is null. Per-pair kern is applied at render time.
|
||||
@@ -1173,6 +1216,8 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
|
||||
|
||||
s.epdFont.data = &s.miniData;
|
||||
s.miniMode = metadataOnly ? PerStyle::MiniMode::METADATA : PerStyle::MiniMode::FULL;
|
||||
LOG_DBG("SDCF", "prewarmStyle %u: mode→%s glyphs=%u bitmap=%p", styleIdx, metadataOnly ? "METADATA" : "FULL",
|
||||
validCount, s.miniBitmap);
|
||||
|
||||
// Accumulate stats
|
||||
stats_.sdReadTimeMs += sdTime;
|
||||
@@ -1236,6 +1281,10 @@ const EpdGlyph* SdCardFont::onGlyphMiss(void* ctx, uint32_t codepoint) {
|
||||
const auto& s = self->styles_[styleIdx];
|
||||
if (!s.fullIntervals) return nullptr;
|
||||
|
||||
// Diagnostic: log first miss per codepoint+style to show why it bypassed prewarm
|
||||
LOG_DBG("SDCF", "onGlyphMiss: U+%04X style %u miniMode=%u miniIntervals=%u bitmap=%p", codepoint, styleIdx,
|
||||
(uint8_t)s.miniMode, s.miniIntervalCount, s.miniBitmap);
|
||||
|
||||
// Check overflow cache first (matching both codepoint and style)
|
||||
for (uint32_t i = 0; i < self->overflowCount_; i++) {
|
||||
if (self->overflow_[i].codepoint == codepoint && self->overflow_[i].styleIdx == styleIdx) {
|
||||
@@ -1309,6 +1358,8 @@ const EpdGlyph* SdCardFont::onGlyphMiss(void* ctx, uint32_t codepoint) {
|
||||
|
||||
// All reads succeeded — commit to slot (evict old entry if at capacity)
|
||||
if (wasAtCapacity) {
|
||||
LOG_DBG("SDCF", "Overflow: evicting U+%04X style %u from slot %u", self->overflow_[slot].codepoint,
|
||||
self->overflow_[slot].styleIdx, slot);
|
||||
delete[] self->overflow_[slot].bitmap;
|
||||
}
|
||||
self->overflow_[slot].glyph = tempGlyph;
|
||||
|
||||
@@ -118,7 +118,8 @@ class SdCardFont {
|
||||
EpdKernClassEntry* kernLeftClasses = nullptr;
|
||||
EpdKernClassEntry* kernRightClasses = nullptr;
|
||||
EpdLigaturePair* ligaturePairs = nullptr;
|
||||
bool kernLigLoaded = false;
|
||||
bool ligLoaded = false; ///< ligaturePairs resident
|
||||
bool kernClassesLoaded = false; ///< kernLeft/RightClasses resident (skipped during metadata-only prewarm)
|
||||
|
||||
// Stub EpdFontData returned when not prewarmed
|
||||
EpdFontData stubData{};
|
||||
@@ -204,7 +205,7 @@ class SdCardFont {
|
||||
void freeStyleAll(PerStyle& s);
|
||||
void freeStyleKernLigatureData(PerStyle& s);
|
||||
void freeStyleMiniKern(PerStyle& s);
|
||||
bool loadStyleKernLigatureData(PerStyle& s);
|
||||
bool loadStyleKernLigatureData(PerStyle& s, bool ligatureOnly = false);
|
||||
bool buildMiniKernMatrix(PerStyle& s, const uint32_t* codepoints, uint32_t cpCount);
|
||||
void applyKernLigaturePointers(const PerStyle& s, EpdFontData& data) const;
|
||||
void applyGlyphMissCallback(uint8_t styleIdx);
|
||||
|
||||
@@ -215,7 +215,14 @@ void ParsedText::layoutAndExtractLines(
|
||||
}
|
||||
|
||||
// Apply fixed transforms before any per-line layout work.
|
||||
applyParagraphIndent();
|
||||
// Paragraph indent only applies to the first layout pass; skip on continuations.
|
||||
if (!isContinuation_) {
|
||||
applyParagraphIndent();
|
||||
}
|
||||
// Bionic transform is incremental: applyBionicReadingTransform() is a no-op
|
||||
// for already-transformed words (bionicTransformedUpTo_ == words.size()) and
|
||||
// only processes raw words appended since the last flush, so it is always safe
|
||||
// to call regardless of isContinuation_.
|
||||
if (bionicReadingEnabled) {
|
||||
applyBionicReadingTransform();
|
||||
}
|
||||
@@ -242,6 +249,15 @@ void ParsedText::layoutAndExtractLines(
|
||||
}
|
||||
|
||||
const int pageWidth = viewportWidth;
|
||||
|
||||
// Compute firstLineIndent once here so all layout helpers use the same value.
|
||||
// On a continuation flush the remaining words are mid-paragraph, so no indent.
|
||||
const int firstLineIndent =
|
||||
!isContinuation_ && blockStyle.textIndentDefined &&
|
||||
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
|
||||
? std::min(std::max<int>(static_cast<int>(blockStyle.textIndent), -(pageWidth - 1)), pageWidth - 1)
|
||||
: 0;
|
||||
|
||||
auto wordWidths = calculateWordWidths(renderer, fontId);
|
||||
|
||||
std::vector<size_t> lineBreakIndices;
|
||||
@@ -252,9 +268,9 @@ void ParsedText::layoutAndExtractLines(
|
||||
// Use greedy layout that can split words mid-loop when a hyphenated prefix fits.
|
||||
lineBreakIndices =
|
||||
computeHyphenatedLineBreaks(renderer, fontId, pageWidth, wordWidths, wordContinues, lineEndsWithHyphenatedWord,
|
||||
splitPrefixWordIndexes, splitInsertedHyphen);
|
||||
splitPrefixWordIndexes, splitInsertedHyphen, firstLineIndent);
|
||||
} else {
|
||||
lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, wordWidths, wordContinues);
|
||||
lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, wordWidths, wordContinues, firstLineIndent);
|
||||
lineEndsWithHyphenatedWord.assign(lineBreakIndices.size(), false);
|
||||
splitPrefixWordIndexes.assign(lineBreakIndices.size(), -1);
|
||||
splitInsertedHyphen.assign(lineBreakIndices.size(), false);
|
||||
@@ -264,7 +280,7 @@ void ParsedText::layoutAndExtractLines(
|
||||
for (size_t i = 0; i < lineCount; ++i) {
|
||||
const bool lineEndedWithHyphenation = i < lineEndsWithHyphenatedWord.size() ? lineEndsWithHyphenatedWord[i] : false;
|
||||
const auto result = extractLine(i, pageWidth, wordWidths, wordContinues, lineBreakIndices, processLine, renderer,
|
||||
fontId, lineEndedWithHyphenation, false);
|
||||
fontId, lineEndedWithHyphenation, false, firstLineIndent);
|
||||
|
||||
if (result == LineProcessResult::RetryWithoutHyphenation && lineEndedWithHyphenation) {
|
||||
const size_t lineStart = i > 0 ? lineBreakIndices[i - 1] : 0;
|
||||
@@ -309,8 +325,8 @@ void ParsedText::layoutAndExtractLines(
|
||||
|
||||
// Keep previous lines fixed; recompute only this specific line without hyphenation.
|
||||
// Suppression is intentionally line-local.
|
||||
const size_t retryBreak =
|
||||
computeSingleLineBreakNoHyphen(renderer, fontId, pageWidth, wordWidths, wordContinues, lineStart);
|
||||
const size_t retryBreak = computeSingleLineBreakNoHyphen(renderer, fontId, pageWidth, wordWidths, wordContinues,
|
||||
lineStart, firstLineIndent);
|
||||
|
||||
lineBreakIndices.resize(i + 1);
|
||||
lineEndsWithHyphenatedWord.resize(i + 1);
|
||||
@@ -330,7 +346,7 @@ void ParsedText::layoutAndExtractLines(
|
||||
LOG_DBG("PTX", "Rerendering line %u with hyphenation suppressed, retry attempt: %s", static_cast<unsigned>(i),
|
||||
retryPreview.c_str());
|
||||
extractLine(i, pageWidth, wordWidths, wordContinues, lineBreakIndices, processLine, renderer, fontId, false,
|
||||
true);
|
||||
true, firstLineIndent);
|
||||
|
||||
// Resume regular hyphenation from the first word after the retried line.
|
||||
const size_t resumeIndex = lineBreakIndices[i];
|
||||
@@ -355,13 +371,23 @@ void ParsedText::layoutAndExtractLines(
|
||||
}
|
||||
}
|
||||
|
||||
// Remove consumed words so size() reflects only remaining words
|
||||
// Remove consumed words so size() reflects only remaining words, then
|
||||
// release excess capacity. Without shrink_to_fit the vector retains a
|
||||
// large allocation from before the flush; the next paragraph fills it
|
||||
// back up and eventually needs an even larger contiguous realloc.
|
||||
if (lineCount > 0) {
|
||||
const size_t consumed = lineBreakIndices[lineCount - 1];
|
||||
words.erase(words.begin(), words.begin() + consumed);
|
||||
wordStyles.erase(wordStyles.begin(), wordStyles.begin() + consumed);
|
||||
wordContinues.erase(wordContinues.begin(), wordContinues.begin() + consumed);
|
||||
words.shrink_to_fit();
|
||||
wordStyles.shrink_to_fit();
|
||||
wordContinues.shrink_to_fit();
|
||||
// All remaining words were already transformed before the flush; reset the
|
||||
// watermark so that words appended by addWord() are processed next time.
|
||||
bionicTransformedUpTo_ = words.size();
|
||||
}
|
||||
isContinuation_ = !includeLastLine;
|
||||
}
|
||||
|
||||
std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& renderer, const int fontId) {
|
||||
@@ -376,20 +402,12 @@ std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& rendere
|
||||
}
|
||||
|
||||
std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, const int fontId, const int pageWidth,
|
||||
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec) {
|
||||
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec,
|
||||
const int firstLineIndent) {
|
||||
if (words.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Calculate first line indent (only for left/justified text).
|
||||
// Explicit CSS text-indent always applies — author intent overrides the extraParagraphSpacing
|
||||
// toggle. Only the implicit EmSpace fallback in applyParagraphIndent() is gated on it.
|
||||
const int firstLineIndent =
|
||||
blockStyle.textIndentDefined &&
|
||||
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
|
||||
? std::min(std::max<int>(static_cast<int>(blockStyle.textIndent), -(pageWidth - 1)), pageWidth - 1)
|
||||
: 0;
|
||||
|
||||
// Ensure any word that would overflow even as the first entry on a line is split using fallback hyphenation.
|
||||
for (size_t i = 0; i < wordWidths.size(); ++i) {
|
||||
// First word needs to fit in reduced width if there's an indent
|
||||
@@ -510,19 +528,14 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
|
||||
|
||||
size_t ParsedText::computeSingleLineBreakNoHyphen(const GfxRenderer& renderer, const int fontId, const int pageWidth,
|
||||
const std::vector<uint16_t>& wordWidths,
|
||||
const std::vector<bool>& continuesVec,
|
||||
const size_t lineStartIndex) const {
|
||||
const std::vector<bool>& continuesVec, const size_t lineStartIndex,
|
||||
const int firstLineIndent) const {
|
||||
// One-line non-hyphenating breaker used by the page-boundary retry path.
|
||||
if (lineStartIndex >= wordWidths.size()) {
|
||||
return lineStartIndex;
|
||||
}
|
||||
|
||||
const int firstLineIndent =
|
||||
lineStartIndex == 0 && blockStyle.textIndentDefined &&
|
||||
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
|
||||
? std::min(std::max<int>(static_cast<int>(blockStyle.textIndent), -(pageWidth - 1)), pageWidth - 1)
|
||||
: 0;
|
||||
const int effectivePageWidth = pageWidth - firstLineIndent;
|
||||
const int effectivePageWidth = pageWidth - (lineStartIndex == 0 ? firstLineIndent : 0);
|
||||
|
||||
size_t currentIndex = lineStartIndex;
|
||||
int lineWidth = 0;
|
||||
@@ -577,22 +590,26 @@ void ParsedText::applyParagraphIndent() {
|
||||
}
|
||||
|
||||
void ParsedText::applyBionicReadingTransform() {
|
||||
if (words.empty()) {
|
||||
// Only transform words that haven't been processed yet. On a fresh block
|
||||
// bionicTransformedUpTo_ == 0 so all words are processed. After an
|
||||
// intermediate flush, only the new raw words appended since the last flush
|
||||
// (indices bionicTransformedUpTo_..words.size()-1) need transformation.
|
||||
if (words.empty() || bionicTransformedUpTo_ >= words.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<std::string> transformedWords;
|
||||
std::vector<EpdFontFamily::Style> transformedStyles;
|
||||
std::vector<bool> transformedContinues;
|
||||
transformedWords.reserve(words.size() * 2);
|
||||
transformedStyles.reserve(wordStyles.size() * 2);
|
||||
transformedContinues.reserve(wordContinues.size() * 2);
|
||||
const size_t suffixStart = bionicTransformedUpTo_;
|
||||
std::vector<std::string> transformedSuffix;
|
||||
std::vector<EpdFontFamily::Style> transformedSuffixStyles;
|
||||
std::vector<bool> transformedSuffixContinues;
|
||||
transformedSuffix.reserve((words.size() - suffixStart) * 2);
|
||||
transformedSuffixStyles.reserve(transformedSuffix.capacity());
|
||||
transformedSuffixContinues.reserve(transformedSuffix.capacity());
|
||||
|
||||
for (size_t i = 0; i < words.size(); ++i) {
|
||||
for (size_t i = suffixStart; i < words.size(); ++i) {
|
||||
std::string source = std::move(words[i]);
|
||||
const auto originalStyle = wordStyles[i];
|
||||
const bool originalAttachToPrevious = wordContinues[i];
|
||||
const char* raw = source.c_str();
|
||||
|
||||
const auto spans = tokenizeBionicWord(source);
|
||||
if (spans.empty()) {
|
||||
@@ -603,12 +620,7 @@ void ParsedText::applyBionicReadingTransform() {
|
||||
for (size_t spanIndex = 0; spanIndex < spans.size(); ++spanIndex) {
|
||||
const TokenSpan span = spans[spanIndex];
|
||||
const size_t spanLength = span.end - span.start;
|
||||
std::string token;
|
||||
if (spans.size() == 1 && spanIndex == 0) {
|
||||
token = std::move(source);
|
||||
} else {
|
||||
token.assign(raw + span.start, spanLength);
|
||||
}
|
||||
std::string token = source.substr(span.start, spanLength);
|
||||
|
||||
if (span.isWord) {
|
||||
const unsigned char* ptr = reinterpret_cast<const unsigned char*>(token.c_str());
|
||||
@@ -630,47 +642,42 @@ void ParsedText::applyBionicReadingTransform() {
|
||||
std::string suffix(reinterpret_cast<const char*>(prefixEnd), token.size() - prefixByteCount);
|
||||
token.resize(prefixByteCount);
|
||||
const auto boldStyle = static_cast<EpdFontFamily::Style>(originalStyle | EpdFontFamily::BOLD);
|
||||
transformedWords.push_back(std::move(token));
|
||||
transformedStyles.push_back(boldStyle);
|
||||
transformedContinues.push_back(attachToPrevious);
|
||||
transformedSuffix.push_back(std::move(token));
|
||||
transformedSuffixStyles.push_back(boldStyle);
|
||||
transformedSuffixContinues.push_back(attachToPrevious);
|
||||
|
||||
transformedWords.push_back(std::move(suffix));
|
||||
transformedStyles.push_back(originalStyle);
|
||||
transformedContinues.push_back(true);
|
||||
transformedSuffix.push_back(std::move(suffix));
|
||||
transformedSuffixStyles.push_back(originalStyle);
|
||||
transformedSuffixContinues.push_back(true);
|
||||
attachToPrevious = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
transformedWords.push_back(std::move(token));
|
||||
transformedStyles.push_back(originalStyle);
|
||||
transformedContinues.push_back(attachToPrevious);
|
||||
transformedSuffix.push_back(std::move(token));
|
||||
transformedSuffixStyles.push_back(originalStyle);
|
||||
transformedSuffixContinues.push_back(attachToPrevious);
|
||||
attachToPrevious = true;
|
||||
}
|
||||
}
|
||||
|
||||
words = std::move(transformedWords);
|
||||
wordStyles = std::move(transformedStyles);
|
||||
wordContinues = std::move(transformedContinues);
|
||||
// Replace the (now move-emptied) suffix with the transformed version.
|
||||
words.resize(suffixStart);
|
||||
wordStyles.resize(suffixStart);
|
||||
wordContinues.resize(suffixStart);
|
||||
words.insert(words.end(), std::make_move_iterator(transformedSuffix.begin()),
|
||||
std::make_move_iterator(transformedSuffix.end()));
|
||||
wordStyles.insert(wordStyles.end(), transformedSuffixStyles.begin(), transformedSuffixStyles.end());
|
||||
wordContinues.insert(wordContinues.end(), transformedSuffixContinues.begin(), transformedSuffixContinues.end());
|
||||
bionicTransformedUpTo_ = words.size();
|
||||
}
|
||||
|
||||
// Builds break indices while opportunistically splitting the word that would overflow the current line.
|
||||
std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& renderer, const int fontId,
|
||||
const int pageWidth, std::vector<uint16_t>& wordWidths,
|
||||
std::vector<bool>& continuesVec,
|
||||
std::vector<bool>& lineEndsWithHyphenatedWord,
|
||||
std::vector<int>& splitPrefixWordIndexes,
|
||||
std::vector<bool>& splitInsertedHyphen) {
|
||||
// Calculate first line indent (only for left/justified text).
|
||||
// Explicit CSS text-indent always applies — author intent overrides the extraParagraphSpacing
|
||||
// toggle. Only the implicit EmSpace fallback in applyParagraphIndent() is gated on it.
|
||||
const int firstLineIndent =
|
||||
blockStyle.textIndentDefined &&
|
||||
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
|
||||
? std::min(std::max<int>(static_cast<int>(blockStyle.textIndent), -(pageWidth - 1)), pageWidth - 1)
|
||||
: 0;
|
||||
|
||||
std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(
|
||||
const GfxRenderer& renderer, const int fontId, const int pageWidth, std::vector<uint16_t>& wordWidths,
|
||||
std::vector<bool>& continuesVec, std::vector<bool>& lineEndsWithHyphenatedWord,
|
||||
std::vector<int>& splitPrefixWordIndexes, std::vector<bool>& splitInsertedHyphen, const int firstLineIndent) {
|
||||
// Pre-compute inter-word gaps to avoid repeated codepoint scanning and renderer
|
||||
// calls in the inner loop. When hyphenateWordAtIndex inserts a new word, we insert
|
||||
// a placeholder gap (0) at that position to keep the vector in sync; the remainder
|
||||
@@ -949,20 +956,14 @@ ParsedText::LineProcessResult ParsedText::extractLine(
|
||||
const std::vector<bool>& continuesVec, const std::vector<size_t>& lineBreakIndices,
|
||||
const std::function<LineProcessResult(std::shared_ptr<TextBlock>, bool, bool)>& processLine,
|
||||
const GfxRenderer& renderer, const int fontId, const bool lineEndsWithHyphenatedWord,
|
||||
const bool suppressHyphenationRetry) {
|
||||
const bool suppressHyphenationRetry, const int firstLineIndent) {
|
||||
const size_t lineBreak = lineBreakIndices[breakIndex];
|
||||
const size_t lastBreakAt = breakIndex > 0 ? lineBreakIndices[breakIndex - 1] : 0;
|
||||
const size_t lineWordCount = lineBreak - lastBreakAt;
|
||||
|
||||
// Calculate first line indent (only for left/justified text).
|
||||
// Explicit CSS text-indent always applies — author intent overrides the extraParagraphSpacing
|
||||
// toggle. Only the implicit EmSpace fallback in applyParagraphIndent() is gated on it.
|
||||
const bool isFirstLine = breakIndex == 0;
|
||||
const int firstLineIndent =
|
||||
isFirstLine && blockStyle.textIndentDefined &&
|
||||
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
|
||||
? std::min(std::max<int>(static_cast<int>(blockStyle.textIndent), -(pageWidth - 1)), pageWidth - 1)
|
||||
: 0;
|
||||
// Apply indent only to line 0 of the layout pass; firstLineIndent is already
|
||||
// 0 for continuation flushes (computed once in layoutAndExtractLines).
|
||||
const int lineIndent = (breakIndex == 0) ? firstLineIndent : 0;
|
||||
|
||||
// Calculate total word width for this line, count actual word gaps,
|
||||
// and accumulate total natural gap widths (including space kerning adjustments).
|
||||
@@ -989,7 +990,7 @@ ParsedText::LineProcessResult ParsedText::extractLine(
|
||||
}
|
||||
|
||||
// Calculate spacing (account for indent reducing effective page width on first line)
|
||||
const int effectivePageWidth = pageWidth - firstLineIndent;
|
||||
const int effectivePageWidth = pageWidth - lineIndent;
|
||||
// A line is only truly last when it consumes all paragraph words.
|
||||
// During single-line retry we may temporarily pass a truncated break vector,
|
||||
// so relying only on breakIndex would incorrectly disable justification.
|
||||
@@ -1003,7 +1004,7 @@ ParsedText::LineProcessResult ParsedText::extractLine(
|
||||
|
||||
// Calculate initial x position (first line starts at indent for left/justified text;
|
||||
// may be negative for hanging indents, e.g. margin-left:3em; text-indent:-1em).
|
||||
auto xpos = static_cast<int16_t>(firstLineIndent);
|
||||
auto xpos = static_cast<int16_t>(lineIndent);
|
||||
if (blockStyle.alignment == CssTextAlign::Right) {
|
||||
xpos = effectivePageWidth - lineWordWidthSum - totalNaturalGaps;
|
||||
} else if (blockStyle.alignment == CssTextAlign::Center) {
|
||||
|
||||
@@ -27,16 +27,19 @@ class ParsedText {
|
||||
bool extraParagraphSpacing;
|
||||
bool hyphenationEnabled;
|
||||
bool bionicReadingEnabled;
|
||||
bool isContinuation_ = false; ///< true after an intermediate flush; suppresses re-applying paragraph indent
|
||||
size_t bionicTransformedUpTo_ = 0; ///< words[0..bionicTransformedUpTo_) have already been bionic-transformed
|
||||
|
||||
void applyParagraphIndent();
|
||||
void applyBionicReadingTransform();
|
||||
std::vector<size_t> computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
|
||||
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec);
|
||||
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec,
|
||||
int firstLineIndent);
|
||||
std::vector<size_t> computeHyphenatedLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
|
||||
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec,
|
||||
std::vector<bool>& lineEndsWithHyphenatedWord,
|
||||
std::vector<int>& splitPrefixWordIndexes,
|
||||
std::vector<bool>& splitInsertedHyphen);
|
||||
std::vector<bool>& splitInsertedHyphen, int firstLineIndent);
|
||||
// Recompute hyphenated breaks for a suffix that starts at startIndex.
|
||||
// Used after a single-line retry so later lines keep normal hyphenation.
|
||||
std::vector<size_t> computeHyphenatedLineBreaksFromIndex(const GfxRenderer& renderer, int fontId, int pageWidth,
|
||||
@@ -49,7 +52,7 @@ class ParsedText {
|
||||
// Used only for the page-boundary retry line.
|
||||
size_t computeSingleLineBreakNoHyphen(const GfxRenderer& renderer, int fontId, int pageWidth,
|
||||
const std::vector<uint16_t>& wordWidths, const std::vector<bool>& continuesVec,
|
||||
size_t lineStartIndex) const;
|
||||
size_t lineStartIndex, int firstLineIndent) const;
|
||||
bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, const GfxRenderer& renderer, int fontId,
|
||||
std::vector<uint16_t>& wordWidths, bool allowFallbackBreaks,
|
||||
bool* outInsertedHyphen = nullptr);
|
||||
@@ -57,7 +60,8 @@ class ParsedText {
|
||||
size_t breakIndex, int pageWidth, const std::vector<uint16_t>& wordWidths, const std::vector<bool>& continuesVec,
|
||||
const std::vector<size_t>& lineBreakIndices,
|
||||
const std::function<LineProcessResult(std::shared_ptr<TextBlock>, bool, bool)>& processLine,
|
||||
const GfxRenderer& renderer, int fontId, bool lineEndsWithHyphenatedWord, bool suppressHyphenationRetry);
|
||||
const GfxRenderer& renderer, int fontId, bool lineEndsWithHyphenatedWord, bool suppressHyphenationRetry,
|
||||
int firstLineIndent);
|
||||
std::vector<uint16_t> calculateWordWidths(const GfxRenderer& renderer, int fontId);
|
||||
|
||||
public:
|
||||
@@ -74,6 +78,7 @@ class ParsedText {
|
||||
BlockStyle& getBlockStyle() { return blockStyle; }
|
||||
size_t size() const { return words.size(); }
|
||||
bool isEmpty() const { return words.empty(); }
|
||||
bool isContinuation() const { return isContinuation_; }
|
||||
void layoutAndExtractLines(
|
||||
const GfxRenderer& renderer, int fontId, uint16_t viewportWidth,
|
||||
const std::function<LineProcessResult(std::shared_ptr<TextBlock>, bool, bool)>& processLine,
|
||||
|
||||
+142
-1
@@ -36,6 +36,134 @@ inline uint32_t paragraphLutEntryOffset(uint32_t lutStart, uint16_t page) {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace {
|
||||
constexpr uint32_t FNV_PRIME = 0x01000193; // 16777619
|
||||
constexpr uint32_t FNV_OFFSET_BASIS = 0x811C9DC5; // 2166136261
|
||||
|
||||
uint32_t fnv1a(const uint8_t* data, size_t length) {
|
||||
uint32_t hash = FNV_OFFSET_BASIS;
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
hash ^= data[i];
|
||||
hash *= FNV_PRIME;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
uint32_t Section::calculatePropertyHash(int fontId, float lineCompression, bool extraParagraphSpacing,
|
||||
uint8_t paragraphAlignment, uint16_t viewportWidth, uint16_t viewportHeight,
|
||||
bool hyphenationEnabled, bool embeddedStyle, bool bionicReadingEnabled,
|
||||
uint8_t imageRendering) {
|
||||
uint8_t buffer[64];
|
||||
size_t offset = 0;
|
||||
|
||||
auto append = [&](const void* ptr, size_t size) {
|
||||
memcpy(buffer + offset, ptr, size);
|
||||
offset += size;
|
||||
};
|
||||
|
||||
append(&fontId, sizeof(fontId));
|
||||
append(&lineCompression, sizeof(lineCompression));
|
||||
append(&extraParagraphSpacing, sizeof(extraParagraphSpacing));
|
||||
append(¶graphAlignment, sizeof(paragraphAlignment));
|
||||
append(&viewportWidth, sizeof(viewportWidth));
|
||||
append(&viewportHeight, sizeof(viewportHeight));
|
||||
append(&hyphenationEnabled, sizeof(hyphenationEnabled));
|
||||
append(&embeddedStyle, sizeof(embeddedStyle));
|
||||
append(&bionicReadingEnabled, sizeof(bionicReadingEnabled));
|
||||
append(&imageRendering, sizeof(imageRendering));
|
||||
|
||||
return fnv1a(buffer, offset);
|
||||
}
|
||||
|
||||
std::string Section::getSectionFilePath(uint32_t propertyHash) const {
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "%d_%08x", spineIndex, propertyHash);
|
||||
return epub->getCachePath() + "/sections/" + buf + ".bin";
|
||||
}
|
||||
|
||||
std::string Section::getImageBasePath(uint32_t propertyHash) const {
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "img_%d_%08x_", spineIndex, propertyHash);
|
||||
return epub->getCachePath() + "/" + buf;
|
||||
}
|
||||
|
||||
struct SectionVariant {
|
||||
std::string filename;
|
||||
uint16_t date;
|
||||
uint16_t time;
|
||||
};
|
||||
|
||||
void Section::evictOldVariants() const {
|
||||
// We keep up to 5 most recently accessed/modified variants to prevent SD card bloat
|
||||
constexpr size_t MAX_VARIANTS = 5;
|
||||
|
||||
std::string sectionsDir = epub->getCachePath() + "/sections";
|
||||
auto files = Storage.listFiles(sectionsDir.c_str(), 100);
|
||||
|
||||
std::vector<SectionVariant> variants;
|
||||
|
||||
// Find all cache variants belonging to this spineIndex
|
||||
char prefix[16];
|
||||
snprintf(prefix, sizeof(prefix), "%d_", spineIndex);
|
||||
size_t prefixLen = strlen(prefix);
|
||||
|
||||
for (const auto& file : files) {
|
||||
if (file.startsWith(prefix) && file.endsWith(".bin")) {
|
||||
HalFile hf = Storage.open((sectionsDir + "/" + file.c_str()).c_str(), O_RDONLY);
|
||||
if (hf) {
|
||||
uint16_t md, mt;
|
||||
if (hf.getModifyDateTime(&md, &mt)) {
|
||||
variants.push_back({file.c_str(), md, mt});
|
||||
} else {
|
||||
// If we can't get modified time, assume it's very old to evict it
|
||||
variants.push_back({file.c_str(), 0, 0});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (variants.size() <= MAX_VARIANTS) return;
|
||||
|
||||
// Sort descending by modified date and time
|
||||
std::sort(variants.begin(), variants.end(), [](const SectionVariant& a, const SectionVariant& b) {
|
||||
if (a.date != b.date) return a.date > b.date;
|
||||
return a.time > b.time;
|
||||
});
|
||||
|
||||
// Delete everything after MAX_VARIANTS limit
|
||||
for (size_t i = MAX_VARIANTS; i < variants.size(); ++i) {
|
||||
std::string targetPath = sectionsDir + "/" + variants[i].filename;
|
||||
Storage.remove(targetPath.c_str());
|
||||
LOG_DBG("SCT", "Evicted old section cache: %s", targetPath.c_str());
|
||||
|
||||
// Extract the hash to also clean up associated images
|
||||
// Filename format: spineIndex_hash.bin
|
||||
size_t underscore = variants[i].filename.find('_');
|
||||
size_t dot = variants[i].filename.find('.');
|
||||
if (underscore != std::string::npos && dot != std::string::npos && dot > underscore) {
|
||||
std::string hashStr = variants[i].filename.substr(underscore + 1, dot - underscore - 1);
|
||||
uint32_t parsedHash = strtoul(hashStr.c_str(), nullptr, 16);
|
||||
if (parsedHash != 0 || hashStr == "00000000") {
|
||||
std::string imgBasePath = getImageBasePath(parsedHash);
|
||||
// Find and delete matching images
|
||||
auto rootFiles = Storage.listFiles(epub->getCachePath().c_str(), 100);
|
||||
size_t lastSlash = imgBasePath.find_last_of('/');
|
||||
std::string imgPrefix = (lastSlash != std::string::npos) ? imgBasePath.substr(lastSlash + 1) : imgBasePath;
|
||||
|
||||
for (const auto& rf : rootFiles) {
|
||||
if (rf.startsWith(imgPrefix.c_str())) {
|
||||
Storage.remove((epub->getCachePath() + "/" + rf.c_str()).c_str());
|
||||
LOG_DBG("SCT", "Evicted old image cache: %s", rf.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
|
||||
if (!file) {
|
||||
LOG_ERR("SCT", "File not open for writing page %d", pageCount);
|
||||
@@ -89,6 +217,11 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
||||
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
|
||||
const bool bionicReadingEnabled, const uint8_t imageRendering) {
|
||||
uint32_t propertyHash =
|
||||
calculatePropertyHash(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||
viewportHeight, hyphenationEnabled, embeddedStyle, bionicReadingEnabled, imageRendering);
|
||||
filePath = getSectionFilePath(propertyHash);
|
||||
|
||||
if (!Storage.openFileForRead("SCT", filePath, file)) {
|
||||
return false;
|
||||
}
|
||||
@@ -195,6 +328,11 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
||||
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
|
||||
const bool bionicReadingEnabled, const uint8_t imageRendering,
|
||||
const std::function<void(int)>& progressFn) {
|
||||
uint32_t propertyHash =
|
||||
calculatePropertyHash(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||
viewportHeight, hyphenationEnabled, embeddedStyle, bionicReadingEnabled, imageRendering);
|
||||
filePath = getSectionFilePath(propertyHash);
|
||||
|
||||
const uint32_t phaseTotalStart = millis();
|
||||
const auto localPath = epub->getSpineItem(spineIndex).href;
|
||||
|
||||
@@ -222,7 +360,10 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
||||
// Derive the content base directory and image cache path prefix for the parser
|
||||
size_t lastSlash = localPath.find_last_of('/');
|
||||
std::string contentBase = (lastSlash != std::string::npos) ? localPath.substr(0, lastSlash + 1) : "";
|
||||
std::string imageBasePath = epub->getCachePath() + "/img_" + std::to_string(spineIndex) + "_";
|
||||
std::string imageBasePath = getImageBasePath(propertyHash);
|
||||
|
||||
// Evict old variants for this spine to keep cache size controlled
|
||||
evictOldVariants();
|
||||
|
||||
CssParser* cssParser = nullptr;
|
||||
if (embeddedStyle) {
|
||||
|
||||
+15
-4
@@ -38,15 +38,26 @@ class Section {
|
||||
// Caller is responsible for closing `outFile`. Returns false on any I/O or validation error.
|
||||
bool readParagraphLutHeader(FsFile& outFile, uint16_t& outCount, uint32_t& outLutStart) const;
|
||||
|
||||
// Calculates a stable hash for a given set of rendering properties.
|
||||
// Used to suffix cache files so multiple variants can coexist safely without constant recompilation.
|
||||
static uint32_t calculatePropertyHash(int fontId, float lineCompression, bool extraParagraphSpacing,
|
||||
uint8_t paragraphAlignment, uint16_t viewportWidth, uint16_t viewportHeight,
|
||||
bool hyphenationEnabled, bool embeddedStyle, bool bionicReadingEnabled,
|
||||
uint8_t imageRendering);
|
||||
|
||||
// Computes the active file path for this section based on rendering properties
|
||||
std::string getSectionFilePath(uint32_t propertyHash) const;
|
||||
// Computes the image base path for extract images related to this specific section variant
|
||||
std::string getImageBasePath(uint32_t propertyHash) const;
|
||||
// Garbage collection: Keep only the most recent N variants per chapter
|
||||
void evictOldVariants() const;
|
||||
|
||||
public:
|
||||
uint16_t pageCount = 0;
|
||||
int currentPage = 0;
|
||||
|
||||
explicit Section(const std::shared_ptr<Epub>& epub, const int spineIndex, GfxRenderer& renderer)
|
||||
: epub(epub),
|
||||
spineIndex(spineIndex),
|
||||
renderer(renderer),
|
||||
filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {}
|
||||
: epub(epub), spineIndex(spineIndex), renderer(renderer) {}
|
||||
~Section() = default;
|
||||
bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
||||
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
|
||||
|
||||
@@ -189,6 +189,20 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
||||
currentTextBlock->addWord(partWordBuffer, fontStyle, false, nextWordContinues);
|
||||
partWordBufferIndex = 0;
|
||||
nextWordContinues = false;
|
||||
|
||||
if (currentTextBlock->size() > 96) {
|
||||
LOG_DBG("EHP", "Text block too long, splitting into multiple pages");
|
||||
const int horizontalInset = currentTextBlock->getBlockStyle().totalHorizontalInset();
|
||||
const uint16_t effectiveWidth =
|
||||
(horizontalInset < viewportWidth) ? static_cast<uint16_t>(viewportWidth - horizontalInset) : viewportWidth;
|
||||
currentTextBlock->layoutAndExtractLines(
|
||||
renderer, fontId, effectiveWidth,
|
||||
[this](const std::shared_ptr<TextBlock>& textBlock, const bool lineEndsWithHyphenatedWord,
|
||||
const bool suppressHyphenationRetry) {
|
||||
return addLineToPage(textBlock, lineEndsWithHyphenatedWord, suppressHyphenationRetry);
|
||||
},
|
||||
false);
|
||||
}
|
||||
}
|
||||
|
||||
// Emit the current page, keeping paragraphLutPerPage and completedPageCount in lockstep.
|
||||
@@ -1206,25 +1220,6 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
|
||||
|
||||
self->partWordBuffer[self->partWordBufferIndex++] = s[i];
|
||||
}
|
||||
|
||||
// If we have > 750 words buffered up, perform the layout and consume out all but the last line
|
||||
// There should be enough here to build out 1-2 full pages and doing this will free up a lot of
|
||||
// memory.
|
||||
// Spotted when reading Intermezzo, there are some really long text blocks in there.
|
||||
if (self->currentTextBlock->size() > 750) {
|
||||
LOG_DBG("EHP", "Text block too long, splitting into multiple pages");
|
||||
const int horizontalInset = self->currentTextBlock->getBlockStyle().totalHorizontalInset();
|
||||
const uint16_t effectiveWidth = (horizontalInset < self->viewportWidth)
|
||||
? static_cast<uint16_t>(self->viewportWidth - horizontalInset)
|
||||
: self->viewportWidth;
|
||||
self->currentTextBlock->layoutAndExtractLines(
|
||||
self->renderer, self->fontId, effectiveWidth,
|
||||
[self](const std::shared_ptr<TextBlock>& textBlock, const bool lineEndsWithHyphenatedWord,
|
||||
const bool suppressHyphenationRetry) {
|
||||
return self->addLineToPage(textBlock, lineEndsWithHyphenatedWord, suppressHyphenationRetry);
|
||||
},
|
||||
false);
|
||||
}
|
||||
}
|
||||
|
||||
void XMLCALL ChapterHtmlSlimParser::defaultHandlerExpand(void* userData, const XML_Char* s, const int len) {
|
||||
@@ -1591,13 +1586,17 @@ void ChapterHtmlSlimParser::makePages() {
|
||||
|
||||
const int lineHeight = renderer.getLineHeight(fontId) * lineCompression;
|
||||
|
||||
// Apply top spacing before the paragraph (stored in pixels)
|
||||
// Apply top spacing before the paragraph — skip for continuation fragments
|
||||
// (words left over after an intermediate flush): the top margin was already
|
||||
// applied before the first set of lines from this logical paragraph.
|
||||
const BlockStyle& blockStyle = currentTextBlock->getBlockStyle();
|
||||
if (blockStyle.marginTop > 0) {
|
||||
currentPageNextY += blockStyle.marginTop;
|
||||
}
|
||||
if (blockStyle.paddingTop > 0) {
|
||||
currentPageNextY += blockStyle.paddingTop;
|
||||
if (!currentTextBlock->isContinuation()) {
|
||||
if (blockStyle.marginTop > 0) {
|
||||
currentPageNextY += blockStyle.marginTop;
|
||||
}
|
||||
if (blockStyle.paddingTop > 0) {
|
||||
currentPageNextY += blockStyle.paddingTop;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate effective width accounting for horizontal margins/padding
|
||||
|
||||
@@ -583,6 +583,7 @@ STR_BTN_ACT_PAGE_BACK_10: "Skip 10 Pages Back"
|
||||
STR_BTN_ACT_GO_HOME: "Go Home"
|
||||
STR_BTN_ACT_SLEEP: "Sleep"
|
||||
STR_BTN_ACT_FORCE_REFRESH: "Refresh Screen"
|
||||
STR_BTN_ACT_FORCE_FAST_REFRESH: "Fast Refresh Screen"
|
||||
STR_BTN_ACT_OPEN_TOC: "Open Table of Contents"
|
||||
STR_BTN_ACT_OPEN_BOOKMARKS: "Open Bookmarks"
|
||||
STR_BTN_ACT_STAR_PAGE: "Star Page"
|
||||
@@ -608,4 +609,4 @@ STR_KB_HINT_UPPER_SECONDARY: "Hold SELECT for UPPERCASE or secondary char"
|
||||
STR_KB_HINT_LOWER_SECONDARY: "Hold SELECT for lowercase or secondary char"
|
||||
STR_KB_HINT_URL_SNIPPETS: "Press URL for snippets"
|
||||
STR_BIONIC_READING: "Bionic Reading"
|
||||
STR_BTN_ACT_TOGGLE_BIONIC_READING: "Toggle Bionic Reading"
|
||||
STR_BTN_ACT_TOGGLE_BIONIC_READING: "Toggle Bionic Reading"
|
||||
|
||||
@@ -359,7 +359,7 @@ STR_SLEEP_SCREEN_SET: "Standby-Bild aktualisiert!"
|
||||
STR_IMAGE_DISPLAY_BW: ">> S/W"
|
||||
STR_IMAGE_DISPLAY_GRAYSCALE: ">> Grau"
|
||||
STR_READER_BOOKMARKS: "Lesezeichen & Fußnoten"
|
||||
STR_READER_OVERRIDES: "Buchspezifische Überschreibungen"
|
||||
STR_READER_OVERRIDES: "Buchspezifische Einstellungen"
|
||||
STR_GLOBAL_BOOKMARKS: "Lesezeichen"
|
||||
STR_NO_GLOBAL_BOOKMARKS: "Noch keine Lesezeichen"
|
||||
STR_READER_UTILS: "Hilfsfunktionen"
|
||||
@@ -369,9 +369,9 @@ STR_NO_STARRED_PAGES: "Keine markierten Seiten"
|
||||
STR_RENAME: "Umbenennen"
|
||||
STR_PAGE_PREFIX: "S"
|
||||
STR_CAPTIVE_PORTAL_DETECTED: "Anmeldung erforderlich"
|
||||
STR_CAPTIVE_PORTAL_HINT_1: "Netzwerk erfordert Browseranmeldung. Auf einem anderen Gerät,"
|
||||
STR_CAPTIVE_PORTAL_HINT_2: "öffnen Sie die untenstehende URL zur Autorisierung und drücken Sie dann OK."
|
||||
STR_CAPTIVE_PORTAL_DONE: "Ich bin autorisiert"
|
||||
STR_CAPTIVE_PORTAL_HINT_1: "Netzwerk erfordert Anmeldung. Authorisieren Sie dieses Gerät,"
|
||||
STR_CAPTIVE_PORTAL_HINT_2: "indem Sie die URL auf einem anderen Gerät öffnen und fahren dann fort."
|
||||
STR_CAPTIVE_PORTAL_DONE: "Erledigt"
|
||||
|
||||
STR_READER_TOOLS: "Werkzeuge"
|
||||
STR_READER_NAVIGATION: "Navigation"
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
|
||||
namespace xtc {
|
||||
@@ -193,10 +194,15 @@ XtcError XtcParser::readFirstPageInfo() {
|
||||
return XtcError::CORRUPTED_HEADER;
|
||||
}
|
||||
|
||||
// Verify the file is large enough to contain the full page table
|
||||
// Verify the file is large enough to contain the full page table.
|
||||
// Some encoders (e.g. xtcjs.app) place the page table at offset 0x30, overlapping
|
||||
// the chapterOffset/padding tail of the documented 56-byte header. Accept any
|
||||
// offset at or beyond the chapterOffset field (0x30) so long as it doesn't
|
||||
// overlap fields we actually parse.
|
||||
constexpr uint64_t kMinPageTableOffset = offsetof(XtcHeader, chapterOffset);
|
||||
const uint64_t fileSize = m_file.size64();
|
||||
const uint64_t pageTableSize = static_cast<uint64_t>(m_header.pageCount) * sizeof(PageTableEntry);
|
||||
if (m_header.pageTableOffset < sizeof(XtcHeader) || m_header.pageTableOffset > fileSize ||
|
||||
if (m_header.pageTableOffset < kMinPageTableOffset || m_header.pageTableOffset > fileSize ||
|
||||
pageTableSize > fileSize - m_header.pageTableOffset) {
|
||||
LOG_DBG("XTC", "Page table exceeds file bounds");
|
||||
return XtcError::CORRUPTED_HEADER;
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ build_flags =
|
||||
# Default is (320*4+1)*2=2562, we need more for larger images
|
||||
-DPNG_MAX_BUFFERED_PIXELS=16416
|
||||
-Wno-bidi-chars
|
||||
-Wl,--wrap=panic_print_backtrace,--wrap=panic_abort
|
||||
-Wl,--wrap=panic_print_backtrace,--wrap=panic_abort,--wrap=bootloader_common_check_efuse_blk_validity
|
||||
-fno-exceptions
|
||||
|
||||
build_unflags =
|
||||
|
||||
@@ -278,6 +278,7 @@ class CrossPointSettings {
|
||||
BTN_GO_HOME,
|
||||
BTN_SLEEP,
|
||||
BTN_FORCE_REFRESH,
|
||||
BTN_FORCE_FAST_REFRESH,
|
||||
BTN_OPEN_TOC,
|
||||
BTN_OPEN_BOOKMARKS,
|
||||
BTN_STAR_PAGE,
|
||||
|
||||
+7
-16
@@ -140,13 +140,13 @@ inline const std::vector<SettingInfo> list = {
|
||||
// All entries share the same ordered action-label list; the submenu groups them behind
|
||||
// a single placeholder row in the device UI.
|
||||
// Shared action options (everything except the first "default" entry).
|
||||
#define BTN_ACT_OPTIONS \
|
||||
StrId::STR_BTN_ACT_PAGE_FORWARD, StrId::STR_BTN_ACT_PAGE_BACK, StrId::STR_BTN_ACT_PAGE_FORWARD_10, \
|
||||
StrId::STR_BTN_ACT_PAGE_BACK_10, StrId::STR_BTN_ACT_GO_HOME, StrId::STR_BTN_ACT_SLEEP, \
|
||||
StrId::STR_BTN_ACT_FORCE_REFRESH, StrId::STR_BTN_ACT_OPEN_TOC, StrId::STR_BTN_ACT_OPEN_BOOKMARKS, \
|
||||
StrId::STR_BTN_ACT_STAR_PAGE, StrId::STR_BTN_ACT_FOOTNOTES, StrId::STR_BTN_ACT_NEXT_SECTION, \
|
||||
StrId::STR_BTN_ACT_PREV_SECTION, StrId::STR_BTN_ACT_EXIT_READER, StrId::STR_BTN_ACT_READER_MENU, \
|
||||
StrId::STR_BTN_ACT_TOGGLE_BIONIC_READING, StrId::STR_BTN_ACT_KOREADER_SYNC
|
||||
#define BTN_ACT_OPTIONS \
|
||||
StrId::STR_BTN_ACT_PAGE_FORWARD, StrId::STR_BTN_ACT_PAGE_BACK, StrId::STR_BTN_ACT_PAGE_FORWARD_10, \
|
||||
StrId::STR_BTN_ACT_PAGE_BACK_10, StrId::STR_BTN_ACT_GO_HOME, StrId::STR_BTN_ACT_SLEEP, \
|
||||
StrId::STR_BTN_ACT_FORCE_REFRESH, StrId::STR_BTN_ACT_FORCE_FAST_REFRESH, StrId::STR_BTN_ACT_OPEN_TOC, \
|
||||
StrId::STR_BTN_ACT_OPEN_BOOKMARKS, StrId::STR_BTN_ACT_STAR_PAGE, StrId::STR_BTN_ACT_FOOTNOTES, \
|
||||
StrId::STR_BTN_ACT_NEXT_SECTION, StrId::STR_BTN_ACT_PREV_SECTION, StrId::STR_BTN_ACT_EXIT_READER, \
|
||||
StrId::STR_BTN_ACT_READER_MENU, StrId::STR_BTN_ACT_TOGGLE_BIONIC_READING, StrId::STR_BTN_ACT_KOREADER_SYNC
|
||||
|
||||
// Back button: short=exit reader, double=ignore, long=go home
|
||||
SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortBack, {StrId::STR_BTN_DEF_EXIT_READER},
|
||||
@@ -276,15 +276,6 @@ inline const std::vector<SettingInfo> list = {
|
||||
SettingInfo::Toggle(StrId::STR_KO_SYNC_ON_BOOK_CLOSE, &CrossPointSettings::koSyncOnBookClose, "koSyncOnBookClose",
|
||||
StrId::STR_KOREADER_SYNC),
|
||||
|
||||
// --- OPDS Browser (web-only, uses CrossPointSettings char arrays) ---
|
||||
SettingInfo::String(StrId::STR_OPDS_SERVER_URL, SETTINGS.opdsServerUrl, sizeof(SETTINGS.opdsServerUrl),
|
||||
"opdsServerUrl", StrId::STR_OPDS_BROWSER),
|
||||
SettingInfo::String(StrId::STR_USERNAME, SETTINGS.opdsUsername, sizeof(SETTINGS.opdsUsername), "opdsUsername",
|
||||
StrId::STR_OPDS_BROWSER),
|
||||
SettingInfo::String(StrId::STR_PASSWORD, SETTINGS.opdsPassword, sizeof(SETTINGS.opdsPassword), "opdsPassword",
|
||||
StrId::STR_OPDS_BROWSER)
|
||||
.withObfuscated(),
|
||||
|
||||
// --- Status Bar Settings (web-only, uses StatusBarSettingsActivity) ---
|
||||
SettingInfo::Toggle(StrId::STR_CHAPTER_PAGE_COUNT, &CrossPointSettings::statusBarChapterPageCount,
|
||||
"statusBarChapterPageCount", StrId::STR_CUSTOMISE_STATUS_BAR),
|
||||
|
||||
@@ -398,7 +398,7 @@ void OpdsBookBrowserActivity::chooseBookFormat(const OpdsEntry& book) {
|
||||
|
||||
selectedBookIndex = selectorIndex;
|
||||
formatSelectorIndex = 0;
|
||||
formatSelectionLabels = buildOpdsFormatSelectionLabels(book.acquisitionLinks, SETTINGS.opdsServerUrl);
|
||||
formatSelectionLabels = buildOpdsFormatSelectionLabels(book.acquisitionLinks, server.url);
|
||||
state = BrowserState::FORMAT_SELECTION;
|
||||
requestUpdate();
|
||||
}
|
||||
@@ -411,9 +411,8 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book, const OpdsAcqu
|
||||
downloadTotal = 0;
|
||||
requestUpdate(true);
|
||||
|
||||
std::string downloadUrl = (acquisition.href.rfind("http", 0) == 0)
|
||||
? acquisition.href
|
||||
: UrlUtils::buildUrl(SETTINGS.opdsServerUrl, acquisition.href);
|
||||
std::string downloadUrl =
|
||||
(acquisition.href.rfind("http", 0) == 0) ? acquisition.href : UrlUtils::buildUrl(server.url, acquisition.href);
|
||||
std::string filename = "/" +
|
||||
StringUtils::sanitizeFilename((book.author.empty() ? "" : book.author + " - ") + book.title) +
|
||||
acquisition.fileExtension;
|
||||
|
||||
+14
-1
@@ -323,6 +323,11 @@ void loop() {
|
||||
powerManager.setPowerSaving(false); // Restore normal CPU frequency on user activity
|
||||
}
|
||||
|
||||
// Power-hold timer for sleep. Hoisted above the screenshot block so the
|
||||
// screenshot path can clear it and avoid a stale POWER press triggering sleep
|
||||
// after the screenshot completes.
|
||||
static unsigned long powerHoldStart = 0;
|
||||
|
||||
static bool screenshotButtonsReleased = true;
|
||||
if (gpio.isPressed(HalGPIO::BTN_POWER) && gpio.isPressed(HalGPIO::BTN_DOWN)) {
|
||||
if (screenshotButtonsReleased) {
|
||||
@@ -331,6 +336,10 @@ void loop() {
|
||||
RenderLock lock;
|
||||
ScreenshotUtil::takeScreenshot(renderer);
|
||||
}
|
||||
// Discard the POWER+DOWN presses so they don't fire Short/Long events
|
||||
// (e.g. page turn, sleep) once the user releases the combo.
|
||||
buttonEventManager.drain();
|
||||
powerHoldStart = 0;
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
@@ -350,7 +359,6 @@ void loop() {
|
||||
// (wake-up press) is never misinterpreted as a "go to sleep" press.
|
||||
// The power button long-press is not user-remappable, so this path always owns it.
|
||||
// Sleep mapped to other buttons is handled by the dispatcher's BTN_SLEEP case below.
|
||||
static unsigned long powerHoldStart = 0;
|
||||
if (gpio.wasPressed(HalGPIO::BTN_POWER)) {
|
||||
powerHoldStart = millis();
|
||||
LOG_DBG("MAIN", "loop: power button press detected (fresh edge)");
|
||||
@@ -499,6 +507,11 @@ void loop() {
|
||||
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
||||
break;
|
||||
}
|
||||
case BA::BTN_FORCE_FAST_REFRESH: {
|
||||
RenderLock lock;
|
||||
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
|
||||
break;
|
||||
}
|
||||
case BA::BTN_OPEN_TOC:
|
||||
activityManager.dispatchButtonAction(BA::BTN_OPEN_TOC);
|
||||
break;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// Override the prebuilt libbootloader_support.a implementation.
|
||||
// The X3's validation code misreads the new image's esp_app_desc_t through a
|
||||
// misaligned bootloader_mmap pointer, producing garbage eFuse block revision
|
||||
// values that fail the check. Safe to skip: the eFuse block revision gate is
|
||||
// a manufacturing concern, not a runtime safety issue.
|
||||
#include <esp_err.h>
|
||||
esp_err_t __wrap_bootloader_common_check_efuse_blk_validity(uint32_t min_rev_full, uint32_t max_rev_full) {
|
||||
(void)min_rev_full;
|
||||
(void)max_rev_full;
|
||||
return ESP_OK;
|
||||
}
|
||||
Reference in New Issue
Block a user