Lightweight open-source firmware for Xteink e-paper reader (X4 pro, X4, X3) • EPUB/FB2/MD/TXT support • Custom themes & fonts
C
478
818 commits
updated Sep 18, 2026
PapyriX is firmware for Xteink X3, X4, and X4 Pro e-paper readers. It uses one ESP32-C3 image for X3/X4 and one ESP32-S3 image for X4 Pro.
Warning: Some Xteink units (for example, units from AliExpress) lock USB flash. If USB flash is locked, you cannot update or recover through USB. Install, update, and do emergency recovery from the SD card. Flash through USB only on devices that have unlocked USB.

This project is not affiliated with Xteink. It is a community project.
| Device | Release file | Panel |
|---|---|---|
| Xteink X4 | papyrix-xteink-c3.bin | 800×480 SSD1677 |
| Xteink X3 | papyrix-xteink-c3.bin | 792×528 UC8253 or UC8279 |
| Xteink X4 Pro | papyrix-x4pro.bin | 800×480 UC8279 or UC8179 |
See the device support matrix for build targets and hardware services. Using the wrong binary can drive incorrect pins and can damage hardware.
Page caches use profile-specific folders. Moving an SD card between supported devices does not reuse incompatible rendered pages.
<pre>) and inline code (<code>, <tt>, <kbd>, <samp>) shown as italic (no monospace font in the firmware)/config/themes/)/config/fonts/, .epdfont format)/printouts on the SD card/trash). If you delete a book, the device moves it to /trash. It does not remove the book. You can browse to restore it or delete it permanently. You can empty the trash from the Cleanup menuTip: The Web UI folder create, upload, and rename functions change supported Unicode names to NFC. Names have a limit of 255 UTF-8 bytes. Full paths have a limit of 1023 bytes. CJK filenames are not supported. The device file-browser UI does not have CJK glyphs. For deep folder trees with supported non-Latin names, use exFAT, not FAT32.
See the user guide for operation procedures.
See the customization guide for themes and fonts.
Example theme files and font files are in docs/examples/.
Do you need to recover a device that does not start? Go to emergency recovery.
Download the binary that matches the device:
papyrix-xteink-c3.binpapyrix-x4pro.binThe usual installation method is papyrix-flasher:
papyrix-flasher flash papyrix-xteink-c3.bin
Do not flash an S3 image to a C3 device or a C3 image to an S3 device.
From SD card: You can also install or update with an SD card:
/firmware.bin to the root of your SD card.The device flashes the firmware from the SD card and restarts.
If the device does not start, copy the firmware as /force_update.bin to the SD card.
On the next start, the device flashes the file before it starts the UI.
You do not need to operate the device.
See the customization guide for more data.
pio) or VS Code + PlatformIO IDEInstall Node.js dependencies (for sleep screen scripts and logo scripts):
cd scripts && npm install
If you have Nix, shell.nix supplies all dependencies:
# 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 --update
PapyriX uses PlatformIO to build and flash the firmware. 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 development firmware
make build
# Build both release environments
make release
# Build, verify, and package deterministic release files in dist/
make package
Connect the device through unlocked USB. Build and flash the release firmware for the device:
make flash-xteink-c3 # X3 and X4
make flash-x4pro # X4 Pro
On X4 Pro, hold Power throughout flashing. Release Power after verification completes and the application starts. Close the serial monitor before flashing. To select a port:
PLATFORMIO_UPLOAD_PORT=/dev/ttyACM0 make flash-x4pro
make flash-release and make upload-release select X3/X4 only.
To install an existing release binary instead of building it, use:
# ESP32-C3: X3/X4
esptool.py --chip esp32c3 --port /dev/ttyACM0 --baud 460800 \
write_flash -z 0x10000 papyrix-xteink-c3.bin
# ESP32-S3: X4 Pro
esptool.py --chip esp32s3 --port /dev/ttyACM0 --baud 460800 \
write_flash -z 0x10000 papyrix-x4pro.bin
PlatformIO upload remains available for a connected development target:
pio run -e default --target upload
pio run -e x4pro --target upload
Replace /dev/ttyACM0 with the device port. Use COM3 on Windows or
/dev/tty.usbmodem* on macOS where applicable.
Build scripts are in the scripts/ directory.
Convert TTF/OTF fonts to the PapyriX .epdfont format with Python (you need uv):
# Basic conversion (outputs to current directory)
uv run scripts/fontconvert.py my-font -r MyFont-Regular.ttf --2bit
# Full font family with all reader sizes (14, 16, 18pt)
uv run scripts/fontconvert.py my-font -r Regular.ttf -b Bold.ttf --2bit --all-sizes -o /tmp/fonts/
# With Thai script support
uv run scripts/fontconvert.py my-font -r Regular.ttf --2bit --thai -o /tmp/fonts/
# With Arabic script support
uv run scripts/fontconvert.py my-font -r Regular.ttf --2bit --arabic -o /tmp/fonts/
# Generate C header instead of binary (for builtin fonts)
uv run scripts/fontconvert.py my_font 16 Regular.ttf --2bit > my_font_16_2b.h
Options: -r/--regular, -b/--bold, -i/--italic, -o/--output, -s/--size, --2bit, --all-sizes, --header, --thai, --arabic
See the customization guide for the full font conversion procedure.
Convert an image to the sleep screen BMP format (run cd scripts && npm install first):
# With 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 8
Options:
--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 the /sleep/ directory or as /sleep.bmp on the SD card.
Convert an image to a C header for the firmware logo (384x384 monochrome):
cd scripts && node convert-logo.mjs ../images/logo-light.jpg ../src/images/PapyrixLogo.h
Options: --invert, --threshold <0-255>, --rotate <0|90|180|270> (default: 270)
Two simulators let you test the Calibre Wireless Device feature with no 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.mjs
The device simulator listens for Calibre broadcasts and can receive books (saved to scripts/received_books/). The Calibre simulator sends discovery packets and sends test books to connected devices.
A standalone Go binary reads device logs with no PlatformIO. Pre-built binaries are on the releases page. You can also build from source:
cd tools/monitor && go build -o monitor .
Usage:
./monitor # Auto-detect port
./monitor -port /dev/ttyACM0 # Explicit port
./monitor -port /dev/ttyACM0 -log out.txt # Also save to file
./monitor -speed 921600 # Custom baud rate (default: 115200)
A desktop tool tests the content parse pipeline (EPUB, FB2, HTML, TXT, Markdown) with no flash to hardware. Use it to find parse defects, layout defects, or crashes.
# Build only
make reader-test
# Build and process a book
make reader-test FILE=book.epub OUTPUT=/tmp/cache
# Dump parsed text content of each page
tools/reader-test/build/reader-test --dump book.epub /tmp/cache
Options:
--dump — Print the parsed text of each page (use this to verify entity resolution, text extraction, and layout)# 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"
Make CHANGELOG.md from git tags and commit history:
make changelog
This makes a changelog grouped by version tags, with commit messages and author data.
PapyriX is made for the ESP32-C3 limit of approximately 380KB RAM. See docs/architecture.md for the architecture.
The device caches book data on the SD card. X4 uses /.papyrix/cache/, X3 uses
/.papyrix/cache/x3/, and X4 Pro uses /.papyrix/cache/x4pro/.
Each device-specific directory contains the book folders shown below.
<device-cache>/
├── epub_12471232/ # Each EPUB is cached to a subdirectory named `epub_<hash>`
│ ├── progress.bin # Stores reading progress (chapter, page, etc.)
│ ├── bookmarks.bin # Saved bookmarks (up to 20 per book)
│ ├── bookmarks.txt # Human-readable bookmark list (companion to bookmarks.bin)
│ ├── 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
│ └── ...
│
├── fb2_55667788/ # Each FB2 file is cached to a subdirectory named `fb2_<hash>`
│ ├── meta.bin # Cached metadata (title, author, TOC) for faster reloads
│ ├── progress.bin # Stores reading progress
│ ├── cover.bmp # Cover image (converted from adjacent image file)
│ ├── sections/ # Cached chapter pages (same format as EPUB sections)
│ │ ├── 0.bin
│ │ └── ...
│
│
├── 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)
│
├── html_12345678/ # Each HTML file is cached to a subdirectory named `html_<hash>`
│ ├── progress.bin # Stores current page number (4-byte, same as TXT/Markdown)
│ ├── pages_<fontId>.bin # Parsed pages (same format as Markdown/FB2 sections)
│ └── cover.bmp # Cover image (converted from adjacent image file)
│
└── epub_189013891/
To clear cached data, use Settings > Cleanup (see User Guide). You can also delete the .papyrix directory.
The cache does not clear automatically when you delete a book. If you move a book file, the device uses a new cache directory. This resets the reading progress.
See file formats for cache records. See the rendering pipeline for cache scheduling and ownership.
epub-to-xtc-converter — browser-based converter from EPUB to the Xteink native XTC/XTCH format. It uses CREngine WASM for accurate rendering.
Live version: liashkov.site/epub-to-xtc-converter
xteink-epub-optimizer — command-line tool that prepares EPUB files for the Xteink X4 limits (480×800 display, limited RAM):
# Optimize EPUB
python src/optimizer.py ./ebooks ./optimized
# Convert to XTCH format
python src/converter.py book.epub book.xtch --font fonts/MyFont.ttf
Contributions are welcome.
feature/your-feature)PapyriX is a fork of CrossPoint Reader by Dave Allie.
The IPP print server core (lib/Ipp) comes from the
crosspoint-reader fork by
Nishant Joshi, commit
df0d5e3.
X4 hardware data comes from bb_epaper by Larry Bank.
Markdown parse uses MD4C by Martin Mitáš.
CSS parser is adapted from microreader by CidVonHighwind.
Not affiliated with Xteink or a manufacturer of the X4 hardware.
C
69.0%
C++
29.3%
Lightweight open-source firmware for Xteink e-paper reader (X4 pro, X4, X3) • EPUB/FB2/MD/TXT support • Custom themes & fonts
C
478
818 commits
updated Sep 18, 2026
PapyriX is firmware for Xteink X3, X4, and X4 Pro e-paper readers. It uses one ESP32-C3 image for X3/X4 and one ESP32-S3 image for X4 Pro.
Warning: Some Xteink units (for example, units from AliExpress) lock USB flash. If USB flash is locked, you cannot update or recover through USB. Install, update, and do emergency recovery from the SD card. Flash through USB only on devices that have unlocked USB.

This project is not affiliated with Xteink. It is a community project.
| Device | Release file | Panel |
|---|---|---|
| Xteink X4 | papyrix-xteink-c3.bin | 800×480 SSD1677 |
| Xteink X3 | papyrix-xteink-c3.bin | 792×528 UC8253 or UC8279 |
| Xteink X4 Pro | papyrix-x4pro.bin | 800×480 UC8279 or UC8179 |
See the device support matrix for build targets and hardware services. Using the wrong binary can drive incorrect pins and can damage hardware.
Page caches use profile-specific folders. Moving an SD card between supported devices does not reuse incompatible rendered pages.
<pre>) and inline code (<code>, <tt>, <kbd>, <samp>) shown as italic (no monospace font in the firmware)/config/themes/)/config/fonts/, .epdfont format)/printouts on the SD card/trash). If you delete a book, the device moves it to /trash. It does not remove the book. You can browse to restore it or delete it permanently. You can empty the trash from the Cleanup menuTip: The Web UI folder create, upload, and rename functions change supported Unicode names to NFC. Names have a limit of 255 UTF-8 bytes. Full paths have a limit of 1023 bytes. CJK filenames are not supported. The device file-browser UI does not have CJK glyphs. For deep folder trees with supported non-Latin names, use exFAT, not FAT32.
See the user guide for operation procedures.
See the customization guide for themes and fonts.
Example theme files and font files are in docs/examples/.
Do you need to recover a device that does not start? Go to emergency recovery.
Download the binary that matches the device:
papyrix-xteink-c3.binpapyrix-x4pro.binThe usual installation method is papyrix-flasher:
papyrix-flasher flash papyrix-xteink-c3.bin
Do not flash an S3 image to a C3 device or a C3 image to an S3 device.
From SD card: You can also install or update with an SD card:
/firmware.bin to the root of your SD card.The device flashes the firmware from the SD card and restarts.
If the device does not start, copy the firmware as /force_update.bin to the SD card.
On the next start, the device flashes the file before it starts the UI.
You do not need to operate the device.
See the customization guide for more data.
pio) or VS Code + PlatformIO IDEInstall Node.js dependencies (for sleep screen scripts and logo scripts):
cd scripts && npm install
If you have Nix, shell.nix supplies all dependencies:
# 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 --update
PapyriX uses PlatformIO to build and flash the firmware. 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 development firmware
make build
# Build both release environments
make release
# Build, verify, and package deterministic release files in dist/
make package
Connect the device through unlocked USB. Build and flash the release firmware for the device:
make flash-xteink-c3 # X3 and X4
make flash-x4pro # X4 Pro
On X4 Pro, hold Power throughout flashing. Release Power after verification completes and the application starts. Close the serial monitor before flashing. To select a port:
PLATFORMIO_UPLOAD_PORT=/dev/ttyACM0 make flash-x4pro
make flash-release and make upload-release select X3/X4 only.
To install an existing release binary instead of building it, use:
# ESP32-C3: X3/X4
esptool.py --chip esp32c3 --port /dev/ttyACM0 --baud 460800 \
write_flash -z 0x10000 papyrix-xteink-c3.bin
# ESP32-S3: X4 Pro
esptool.py --chip esp32s3 --port /dev/ttyACM0 --baud 460800 \
write_flash -z 0x10000 papyrix-x4pro.bin
PlatformIO upload remains available for a connected development target:
pio run -e default --target upload
pio run -e x4pro --target upload
Replace /dev/ttyACM0 with the device port. Use COM3 on Windows or
/dev/tty.usbmodem* on macOS where applicable.
Build scripts are in the scripts/ directory.
Convert TTF/OTF fonts to the PapyriX .epdfont format with Python (you need uv):
# Basic conversion (outputs to current directory)
uv run scripts/fontconvert.py my-font -r MyFont-Regular.ttf --2bit
# Full font family with all reader sizes (14, 16, 18pt)
uv run scripts/fontconvert.py my-font -r Regular.ttf -b Bold.ttf --2bit --all-sizes -o /tmp/fonts/
# With Thai script support
uv run scripts/fontconvert.py my-font -r Regular.ttf --2bit --thai -o /tmp/fonts/
# With Arabic script support
uv run scripts/fontconvert.py my-font -r Regular.ttf --2bit --arabic -o /tmp/fonts/
# Generate C header instead of binary (for builtin fonts)
uv run scripts/fontconvert.py my_font 16 Regular.ttf --2bit > my_font_16_2b.h
Options: -r/--regular, -b/--bold, -i/--italic, -o/--output, -s/--size, --2bit, --all-sizes, --header, --thai, --arabic
See the customization guide for the full font conversion procedure.
Convert an image to the sleep screen BMP format (run cd scripts && npm install first):
# With 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 8
Options:
--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 the /sleep/ directory or as /sleep.bmp on the SD card.
Convert an image to a C header for the firmware logo (384x384 monochrome):
cd scripts && node convert-logo.mjs ../images/logo-light.jpg ../src/images/PapyrixLogo.h
Options: --invert, --threshold <0-255>, --rotate <0|90|180|270> (default: 270)
Two simulators let you test the Calibre Wireless Device feature with no 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.mjs
The device simulator listens for Calibre broadcasts and can receive books (saved to scripts/received_books/). The Calibre simulator sends discovery packets and sends test books to connected devices.
A standalone Go binary reads device logs with no PlatformIO. Pre-built binaries are on the releases page. You can also build from source:
cd tools/monitor && go build -o monitor .
Usage:
./monitor # Auto-detect port
./monitor -port /dev/ttyACM0 # Explicit port
./monitor -port /dev/ttyACM0 -log out.txt # Also save to file
./monitor -speed 921600 # Custom baud rate (default: 115200)
A desktop tool tests the content parse pipeline (EPUB, FB2, HTML, TXT, Markdown) with no flash to hardware. Use it to find parse defects, layout defects, or crashes.
# Build only
make reader-test
# Build and process a book
make reader-test FILE=book.epub OUTPUT=/tmp/cache
# Dump parsed text content of each page
tools/reader-test/build/reader-test --dump book.epub /tmp/cache
Options:
--dump — Print the parsed text of each page (use this to verify entity resolution, text extraction, and layout)# 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"
Make CHANGELOG.md from git tags and commit history:
make changelog
This makes a changelog grouped by version tags, with commit messages and author data.
PapyriX is made for the ESP32-C3 limit of approximately 380KB RAM. See docs/architecture.md for the architecture.
The device caches book data on the SD card. X4 uses /.papyrix/cache/, X3 uses
/.papyrix/cache/x3/, and X4 Pro uses /.papyrix/cache/x4pro/.
Each device-specific directory contains the book folders shown below.
<device-cache>/
├── epub_12471232/ # Each EPUB is cached to a subdirectory named `epub_<hash>`
│ ├── progress.bin # Stores reading progress (chapter, page, etc.)
│ ├── bookmarks.bin # Saved bookmarks (up to 20 per book)
│ ├── bookmarks.txt # Human-readable bookmark list (companion to bookmarks.bin)
│ ├── 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
│ └── ...
│
├── fb2_55667788/ # Each FB2 file is cached to a subdirectory named `fb2_<hash>`
│ ├── meta.bin # Cached metadata (title, author, TOC) for faster reloads
│ ├── progress.bin # Stores reading progress
│ ├── cover.bmp # Cover image (converted from adjacent image file)
│ ├── sections/ # Cached chapter pages (same format as EPUB sections)
│ │ ├── 0.bin
│ │ └── ...
│
│
├── 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)
│
├── html_12345678/ # Each HTML file is cached to a subdirectory named `html_<hash>`
│ ├── progress.bin # Stores current page number (4-byte, same as TXT/Markdown)
│ ├── pages_<fontId>.bin # Parsed pages (same format as Markdown/FB2 sections)
│ └── cover.bmp # Cover image (converted from adjacent image file)
│
└── epub_189013891/
To clear cached data, use Settings > Cleanup (see User Guide). You can also delete the .papyrix directory.
The cache does not clear automatically when you delete a book. If you move a book file, the device uses a new cache directory. This resets the reading progress.
See file formats for cache records. See the rendering pipeline for cache scheduling and ownership.
epub-to-xtc-converter — browser-based converter from EPUB to the Xteink native XTC/XTCH format. It uses CREngine WASM for accurate rendering.
Live version: liashkov.site/epub-to-xtc-converter
xteink-epub-optimizer — command-line tool that prepares EPUB files for the Xteink X4 limits (480×800 display, limited RAM):
# Optimize EPUB
python src/optimizer.py ./ebooks ./optimized
# Convert to XTCH format
python src/converter.py book.epub book.xtch --font fonts/MyFont.ttf
Contributions are welcome.
feature/your-feature)PapyriX is a fork of CrossPoint Reader by Dave Allie.
The IPP print server core (lib/Ipp) comes from the
crosspoint-reader fork by
Nishant Joshi, commit
df0d5e3.
X4 hardware data comes from bb_epaper by Larry Bank.
Markdown parse uses MD4C by Martin Mitáš.
CSS parser is adapted from microreader by CidVonHighwind.
Not affiliated with Xteink or a manufacturer of the X4 hardware.
C
69.0%
C++
29.3%