Adding a Font to CrossPoint Reader
CrossPoint Reader is an open-source e-reader firmware for the Xteink X4 (ESP32-C3). Fonts are baked into the firmware as C headers at compile time, so adding a new one means converting it, generating an ID, and wiring it up in a few places. This is how I added Atkinson Hyperlegible.
Atkinson Hyperlegible is a typeface from the Braille Institute, designed for low-vision readers. It has become my go-to font in KOReader as well, so porting it to CrossPoint felt like the obvious first move when I started customising the firmware.
Prerequisites
You need Python with freetype-py and fonttools, plus Ruby for the ID script:
pip install freetype-py fonttools1. Get the Font Files
You can use any TTF or OTF files. I used the Fast Atkinson variant already installed on my system at ~/.local/share/fonts/. If you want the standard Atkinson Hyperlegible, download the TTFs from the Braille Institute and put them somewhere convenient.
2. Convert to C Headers
The converter is lib/EpdFont/scripts/fontconvert.py. Reader fonts use --2bit --compress --pnum. Run it once per size per style:
cd lib/EpdFont/scripts
FONT_DIR="$HOME/.local/share/fonts"
for size in 12 14 16 18; do
for style in Regular Bold Italic BoldItalic; do
name="atkinson_${size}_$(echo $style | tr '[:upper:]' '[:lower:]')"
python fontconvert.py \
"$name" "$size" \
"${FONT_DIR}/Fast_Atkinson_${style}.otf" \
--2bit --compress --pnum \
> "../builtinFonts/${name}.h"
done
doneAdjust the font path and filenames to match whichever variant you have.
3. Add Headers to all.h
All built-in font headers are included through lib/EpdFont/builtinFonts/all.h. Add your new headers there:
#include <builtinFonts/atkinson_12_bold.h>
#include <builtinFonts/atkinson_12_bolditalic.h>
#include <builtinFonts/atkinson_12_italic.h>
#include <builtinFonts/atkinson_12_regular.h>
// ... repeat for 14, 16, 18
4. Generate Font IDs
Font IDs are SHA-256 hashes of the generated headers, computed by build-font-ids.sh. Add a block for Atkinson to that script:
echo "#define ATKINSON_12_FONT_ID ($(
ruby -rdigest -e 'puts [
"./atkinson_12_regular.h",
"./atkinson_12_bold.h",
"./atkinson_12_bolditalic.h",
"./atkinson_12_italic.h",
].map{|f| Digest::SHA256.hexdigest(File.read(f)).to_i(16) }.sum % (2 ** 32) - (2 ** 31)'
))"
# ... repeat for 14, 16, 18Then regenerate src/fontIds.h:
bash lib/EpdFont/scripts/build-font-ids.sh > src/fontIds.h5. Wire Up the Font Objects
In src/main.cpp, inside the #ifndef OMIT_FONTS block, declare the font objects and families:
EpdFont atkinson12RegularFont(&atkinson_12_regular);
EpdFont atkinson12BoldFont(&atkinson_12_bold);
EpdFont atkinson12ItalicFont(&atkinson_12_italic);
EpdFont atkinson12BoldItalicFont(&atkinson_12_bolditalic);
EpdFontFamily atkinson12FontFamily(&atkinson12RegularFont, &atkinson12BoldFont,
&atkinson12ItalicFont, &atkinson12BoldItalicFont);
// ... repeat for 14, 16, 18
Then register them with the renderer in setupDisplayAndFonts(), still inside #ifndef OMIT_FONTS:
renderer.insertFont(ATKINSON_12_FONT_ID, atkinson12FontFamily);
renderer.insertFont(ATKINSON_14_FONT_ID, atkinson14FontFamily);
renderer.insertFont(ATKINSON_16_FONT_ID, atkinson16FontFamily);
renderer.insertFont(ATKINSON_18_FONT_ID, atkinson18FontFamily);6. Add to Settings
In src/CrossPointSettings.h, extend the enum:
enum FONT_FAMILY { NOTOSERIF = 0, NOTOSANS = 1, OPENDYSLEXIC = 2, ATKINSON = 3, FONT_FAMILY_COUNT };In src/CrossPointSettings.cpp, add an ATKINSON case to both switch statements, getReaderFontId() and getReaderLineCompression():
// getReaderFontId()
case ATKINSON:
switch (fontSize) {
case SMALL: return ATKINSON_12_FONT_ID;
case MEDIUM:
default: return ATKINSON_14_FONT_ID;
case LARGE: return ATKINSON_16_FONT_ID;
case EXTRA_LARGE: return ATKINSON_18_FONT_ID;
}
// getReaderLineCompression()
case ATKINSON:
switch (lineSpacing) {
case TIGHT: return 0.90f;
case NORMAL:
default: return 0.95f;
case WIDE: return 1.0f;
}In src/SettingsList.h, add STR_ATKINSON to the fontFamily options list:
SettingInfo::Enum(StrId::STR_FONT_FAMILY, &CrossPointSettings::fontFamily,
{StrId::STR_NOTO_SERIF, StrId::STR_NOTO_SANS, StrId::STR_OPEN_DYSLEXIC, StrId::STR_ATKINSON},
"fontFamily", StrId::STR_CAT_READER),7. Add the Translation String
In lib/I18n/translations/english.yaml:
STR_ATKINSON: "Atkinson Hyperlegible"Then regenerate the I18n headers:
python scripts/gen_i18n.py lib/I18n/translations lib/I18n/Build and Flash
pio run --target uploadThe font should now appear as "Atkinson Hyperlegible" in the reader's font selection menu. If you ever change any of the generated header files, re-run the ID script and rebuild. Otherwise the cached layout files in .crosspoint/ on the SD card will have mismatched IDs and the cache will need to be cleared.


