Xteink-X4-crosspoint-reader/CUSTOM_FONTS.md

4.0 KiB

Custom Font Implementation Walkthrough

This document outlines the custom font implementation in the CrossPoint Reader codebase. The system allows users to load custom TrueType/OpenType fonts (converted to a binary format) from an SD card and select them via the settings UI.

System Overview

The custom font system consists of four main components:

  1. Font Converter (fontconvert.py): A Python script that pre-processes standard fonts into a custom optimized binary format (.epdfont).
  2. Font Manager (FontManager): Scans the SD card for valid font files and manages loaded font families.
  3. Font Loader (CustomEpdFont): Handles the low-level reading of the binary format, including on-demand caching of glyph bitmaps to save RAM.
  4. UI & Integration: A settings activity to select fonts and integration into the main rendering loop.

1. Font Conversion & Format

To optimize for the limited RAM of the ESP32 and the specific requirements of E-Ink displays, fonts are not loaded directly as TTF/OTF files. Instead, they are pre-processed.

  • Script: lib/EpdFont/scripts/fontconvert.py
  • Input: TTF/OTF files.
  • Output: .epdfont binary file.
  • Format Details:
    • Header: Contains metadata (magic "EPDF", version, metrics, offsets).
    • Intervals: Unicode ranges supported by the font.
    • Glyphs: Metrics for each character (width, height, advance, offsets).
    • Bitmaps: 1-bit or 2-bit (antialiased) pixel data for glyphs.

2. Storage & Discovery

Fonts are stored on the SD card in the /fonts directory.

  • Location: /fonts
  • Naming Convention: Family-Style-Size.epdfont
    • Example: Literata-Regular-14.epdfont
    • Example: Literata-BoldItalic-14.epdfont
  • Manager: src/managers/FontManager.cpp
    • Scans the /fonts directory on startup/demand.
    • Groups files into Family -> Size -> Styles (Regular, Bold, Italic, BoldItalic).
    • Exposes available families to the UI.

3. Low-Level Implementation (RAM Optimization)

The core logic resides in lib/EpdFont/CustomEpdFont.cpp.

  • Inheritance: CustomEpdFont inherits from EpdFont.
  • Metadata in RAM: When a font is loaded, only the header and glyph metrics (width, height, etc.) are loaded into RAM.
  • Bitmaps on Disk: Pixel data remains on the SD card.
  • LRU Cache: A small Least Recently Used (LRU) cache (MAX_CACHE_SIZE = 30) holds frequently used glyph bitmaps in RAM.
    • Hit: Returns cached bitmap.
    • Miss: Reads the bitmap from the SD card at the specific offset, caches it, and returns it.
  • Benefit: Allows using large fonts with extensive character sets (e.g., CJK) without exhausting the ESP32's heap.

4. User Interface & Selection

The user selects a font through a dedicated Settings activity.

  • File: src/activities/settings/CustomFontSelectionActivity.cpp
  • Flow:
    1. Lists available font families retrieved from FontManager.
    2. User selects a family.
    3. Selection is saved to SETTINGS.customFontFamilyName.

5. Main Integration

The selected font is applied during the system startup or when settings change.

  • File: src/main.cpp
  • Function: setupDisplayAndFonts()
  • Logic:
    1. Checks if SETTINGS.fontFamily is set to FONT_CUSTOM.
    2. Calls FontManager::getInstance().getCustomFontFamily(...) with the saved name and current font size.
    3. If found, the font is dynamically inserted into the global renderer with a generated ID.
    4. The renderer then uses this font for standard text rendering.

Code Path Summary

  1. SD Card: SD:/fonts/MyFont-Regular-14.epdfont
  2. Wait: FontManager::scanFonts() finds the file.
  3. Select: User picks "MyFont" in CustomFontSelectionActivity.
  4. Load: main.cpp calls renderer.insertFont(..., FontManager.getCustomFontFamily("MyFont", 14))
  5. Render: CustomEpdFont::getGlyphBitmap() fetches pixels from SD -> Cache -> Screen.