A lightweight, user-friendly firmware fork for the Xteink X4 e-paper display reader. Built using PlatformIO and targeting the ESP32-C3 microcontroller.
Papyrix is a fork of CrossPoint Reader, focused on creating a light, streamlined reading experience with improved UI defaults.
E-paper devices are fantastic for reading, but most commercially available readers are closed systems with limited customisation. The Xteink X4 is an affordable e-paper device, however the official firmware remains closed.
Papyrix aims to:
- Provide a lightweight, open-source alternative to the official firmware.
- Offer a document reader capable of handling EPUB content on constrained hardware.
- Support customisable font, layout, and display options.
- Run purely on the Xteink X4 hardware.
This project is not affiliated with Xteink; it's built as a community project.
- EPUB 2 and EPUB 3 parsing (nav.xhtml with NCX fallback)
- CSS stylesheet parsing (text-align, font-style, font-weight, text-indent, margins)
- XTC/XTCH native format support
- Markdown (.md, .markdown) file support with formatting
- Plain text (.txt, .text) file support
- Saved reading position
- Book cover display (JPG/JPEG/PNG/BMP, case-insensitive)
- Table of contents navigation
- Image support within EPUB (JPEG/PNG)
- Configurable font sizes (Small/Medium/Large)
- Paragraph alignment (Justified/Left/Center/Right)
- Text layout presets (Compact/Standard/Large) for indentation and spacing
- Soft hyphen support for text layout
- CJK (Chinese/Japanese/Korean) text layout
- Thai text rendering with proper mark positioning
- Knuth-Plass line breaking algorithm (TeX-quality justified text)
- Text anti-aliasing toggle (grayscale text rendering)
- Cover dithering toggle (1-bit black/white vs grayscale covers)
- Pages per refresh setting (1/5/10/15/30)
- 4 screen orientations
- Custom themes from SD card (
/config/themes/) - Custom fonts from SD card (
/config/fonts/, .epdfont format) - Custom sleep screens (Dark/Light/Custom/Cover modes)
- Button remapping (side and front buttons)
- Power button page turn (one-handed reading)
- WiFi file transfer (web server)
- Calibre Wireless Device - Send books from Calibre desktop
- Cleanup menu (clear caches, fonts, factory reset)
- System info (version, uptime, memory, storage)
- exFAT and FAT32 SD card support
- UTF-8 filenames (Cyrillic, etc.)
- File explorer with nested folders
- Hidden system folders filtering (LOST.DIR, $RECYCLE.BIN, etc.)
See the user guide for operating instructions, and the customization guide for themes and fonts. Example theme and font files are available in docs/examples/.
The easiest way to install Papyrix is using papyrix-flasher — a cross-platform CLI tool with auto-detection and embedded bootloader. Download the latest release for your platform and run:
papyrix-flasher flash firmware.binSee Development below.
- PlatformIO Core (
pio) or VS Code + PlatformIO IDE - Node.js 18+ (for build scripts: font conversion, sleep screen, logo)
- USB-C cable for flashing the ESP32-C3
- Xteink X4
Install Node.js dependencies:
cd scripts && npm installIf you have Nix installed, all dependencies are provided via shell.nix:
# Enter development environment
nix-shell
# Or run commands directly
nix-shell --run "make build"
nix-shell --run "make check"First-time Nix setup:
# Install Nix (if not installed)
sh <(curl -L https://nixos.org/nix/install) --daemon
# Add nixpkgs channel
nix-channel --add https://nixos.org/channels/nixos-unstable nixpkgs
nix-channel --updatePapyrix uses PlatformIO for building and flashing the firmware. To get started, clone the repository:
git clone --recursive https://github.com/pliashkou/papyrix
# Or, if you've already cloned without --recursive:
git submodule update --init --recursive
# Build firmware
make build
# Build release firmware
make release
# Or using PlatformIO directly
pio runConnect your Xteink X4 to your computer via USB-C and run the following command.
make flash
# Or using PlatformIO directly
pio run --target uploadYou can also flash using esptool directly (useful if you have a pre-built firmware binary):
esptool.py --chip esp32c3 --port /dev/ttyACM0 --baud 460800 \
write_flash -z 0x0 firmware.binReplace /dev/ttyACM0 with your device port (e.g., COM3 on Windows, /dev/tty.usbmodem* on macOS).
All build scripts are in the scripts/ directory and require Node.js 18+.
# Install dependencies (one time)
cd scripts && npm installConvert TTF/OTF fonts to Papyrix .epdfont format:
cd scripts
# Basic conversion
node convert-fonts.mjs my-font -r MyFont-Regular.ttf
# Full font family with all reader sizes
node convert-fonts.mjs my-font -r Regular.ttf -b Bold.ttf -i Italic.ttf --all-sizes
# Variable font with specific weight (e.g., Roboto variable font)
node convert-fonts.mjs roboto -r Roboto-VariableFont_wdth,wght.ttf --var wght=400
node convert-fonts.mjs roboto-bold -r Roboto-VariableFont_wdth,wght.ttf --var wght=700
# Generate HTML preview to check font rendering
node convert-fonts.mjs my-font -r MyFont-Regular.ttf --preview
# CJK/Thai fonts - use .bin format (streamed from SD card)
node convert-fonts.mjs noto-sans-cjk -r NotoSansSC-Regular.ttf --bin --size 24Options: -r/--regular, -b/--bold, -i/--italic, -o/--output, -s/--size, --2bit, --all-sizes, --bin, --var, --preview
See customization guide for detailed font conversion instructions.
Convert any image to sleep screen BMP format:
# Via Makefile
make sleep-screen INPUT=photo.jpg OUTPUT=sleep.bmp
make sleep-screen INPUT=photo.jpg OUTPUT=sleep.bmp ARGS='--dither --bits 8'
# Or directly
cd scripts && node create-sleep-screen.mjs photo.jpg sleep.bmp --dither --bits 8Options:
--orientation portrait|landscape- Screen orientation (default: portrait)--bits 2|4|8- Output bit depth (default: 4)--dither- Enable Floyd-Steinberg dithering--fit contain|cover|stretch- Resize mode (default: contain)
Copy the output BMP to /sleep/ directory or as /sleep.bmp on the SD card.
Convert image to C header for firmware logo (128x128 monochrome):
cd scripts && node convert-logo.mjs logo.png ../src/images/PapyrixLogo.hOptions: --invert, --threshold <0-255>, --rotate <0|90|180|270>
Two simulators are provided for testing the Calibre Wireless Device feature without real hardware:
cd scripts
# Simulate a Papyrix device (for testing Calibre desktop connection)
node device-simulator.mjs
# Simulate Calibre desktop (for testing device firmware)
node calibre-simulator.mjsThe device simulator listens for Calibre broadcasts and can receive books (saved to scripts/received_books/). The Calibre simulator broadcasts discovery packets and sends test books to connected devices.
# With auto-generated notes from commits
make gh-release VERSION=0.1.1
# With custom notes
make gh-release VERSION=0.1.1 NOTES="Release notes here"Generate CHANGELOG.md from git tags and commit history:
make changelogThis creates a changelog grouped by version tags, with commit messages and author information.
Papyrix is designed for the ESP32-C3's ~380KB RAM constraint. See docs/architecture.md for detailed architecture documentation.
- State Machine: 10 pre-allocated states (Home, Reader, Settings, etc.) with lifecycle hooks
- Dual-Boot System: UI mode (full features) vs Reader mode (minimal, maximum RAM) - device restarts between modes
- Content Providers: Unified
ContentHandleinterface for EPUB, XTC, TXT, and Markdown formats - PageCache: Partial page caching with background pre-rendering
The ESP32 WiFi stack allocates ~100KB and fragments heap memory in a way that cannot be recovered at runtime. After using WiFi features (File Transfer or Calibre Wireless), the device automatically restarts to reclaim memory.
Hash-based lookups: EPUB spine/TOC and glyph caches use FNV-1a hashing for O(1) lookups.
EPUB indexing: Manifest item lookup uses an in-memory hash map for O(1) resolution of spine itemrefs. TOC-to-spine mapping also uses a hash map for O(1) href-to-index resolution.
XTC rendering: 1-bit monochrome pages use byte-level processing. All-white bytes (common in margins) are skipped entirely.
Group5 compression: 1-bit image data uses CCITT Group5 compression for fast decompression and reduced SD card I/O.
Word width caching: 512-entry cache in GfxRenderer avoids repeated font measurements.
The first time chapters of a book are loaded, they are cached to the SD card. Subsequent loads are served from the cache. This cache directory exists at .papyrix on the SD card. The structure is as follows:
.papyrix/
├── epub_12471232/ # Each EPUB is cached to a subdirectory named `epub_<hash>`
│ ├── progress.bin # Stores reading progress (chapter, page, etc.)
│ ├── cover.bmp # Book cover image (once generated)
│ ├── book.bin # Book metadata (title, author, spine, table of contents, etc.)
│ ├── sections/ # All chapter data is stored in the sections subdirectory
│ │ ├── 0.bin # Chapter data (screen count, all text layout info, etc.)
│ │ ├── 1.bin # files are named by their index in the spine
│ │ └── ...
│ └── images/ # Cached inline images (converted to 2-bit BMP)
│ ├── 123456.bmp # Images named by hash of source path
│ └── ...
│
├── txt_98765432/ # Each TXT file is cached to a subdirectory named `txt_<hash>`
│ ├── progress.bin # Stores current page number (4-byte uint32)
│ ├── index.bin # Page index (byte offsets for each page start)
│ └── cover.bmp # Cover image (converted from book.jpg/png/bmp or cover.jpg/png/bmp)
│
├── md_12345678/ # Each Markdown file is cached to a subdirectory named `md_<hash>`
│ ├── progress.bin # Stores current page number (2-byte uint16)
│ ├── section.bin # Parsed pages (same format as EPUB sections)
│ └── cover.bmp # Cover image (converted from README.jpg/png/bmp or cover.jpg/png/bmp)
│
└── epub_189013891/
To clear cached data, use Settings > Cleanup:
- Clear Book Cache — Delete all cached book data and reading progress
- Clear Device Storage — Erase internal flash storage (requires restart)
- Factory Reset — Erase all data (caches, settings, WiFi, fonts) and restart
Alternatively, deleting the .papyrix directory manually will clear the book cache.
Due the way it's currently implemented, the cache is not automatically cleared when a book is deleted and moving a book file will use a new cache directory, resetting the reading progress.
For more details on the internal file structures, see the file formats document.
epub-to-xtc-converter — browser-based converter from EPUB to Xteink's native XTC/XTCH format. Uses CREngine WASM for accurate rendering.
- Device presets for Xteink X4/X3 (480x800)
- Font selection from Google Fonts or custom TTF/OTF
- Configurable margins, line height, hyphenation (42 languages)
- Dark mode and dithering options
- Batch processing and ZIP export
Live version: liashkov.site/epub-to-xtc-converter
xteink-epub-optimizer — command-line tool to optimize EPUB files for the Xteink X4's constraints (480×800 display, limited RAM):
- CSS Sanitization - Removes complex layouts (floats, flexbox, grid)
- Font Removal - Strips embedded fonts to reduce file size
- Image Optimization - Grayscale conversion, resizing to 480px max width
- XTC/XTCH Conversion - Convert EPUBs to Xteink's native format
# Optimize EPUB
python src/optimizer.py ./ebooks ./optimized
# Convert to XTCH format
python src/converter.py book.epub book.xtch --font fonts/MyFont.ttfContributions are very welcome!
- Fork the repo
- Create a branch (
feature/your-feature) - Make changes
- Submit a PR
Papyrix is a fork of CrossPoint Reader by Dave Allie.
X4 hardware insights from bb_epaper by Larry Bank.
Markdown parsing using MD4C by Martin Mitáš.
CSS parser adapted from microreader by CidVonHighwind.
Not affiliated with Xteink or any manufacturer of the X4 hardware.
