Turn EPUB and PDF into beautiful books on your Xteink e-reader.
Xteink can read EPUB natively, but the typography is rough. xtctool gives you complete control over fonts, spacing, margins, and layout - making your books actually pleasant to read.
Built for Xteink X4 - 4.3" e-ink reader, 480×800 resolution.
xtctool works with CommonMark markdown. Use pandoc to convert EPUB first:
# Step 1: Install pandoc (one-time setup)
# macOS: brew install pandoc
# Linux: apt install pandoc
# Windows: https://pandoc.org/installing.html
# Step 2: Convert EPUB to markdown
pandoc "book.epub" --extract-media ./images -t commonmark -o book.md
# Step 3: Convert to e-paper format
xtctool convert book.md -o book.xtc -c config.toml- PDF documents
- Markdown (CommonMark only)
- Typst documents
- Images (PNG, JPG)
Have EPUB, DOCX, or HTML? → Convert to markdown with pandoc first (workflow above)
# Install uv if you haven't
curl -LsSf https://astral.sh/uv/install.sh | sh
# Create virtual environment and install
uv venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
uv pip install -e .
# Or install with optional dependencies
uv pip install -e ".[markdown,performance]" # Full installationOptional dependencies:
[markdown]- Typst & Jinja2 for Markdown/EPUB rendering[performance]- numba for ~10x faster dithering
# Create virtual environment (recommended)
python -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
# Install xtctool
pip install -e ".[markdown,performance]"# Install with development dependencies
uv pip install -e ".[dev,markdown,performance]"Core dependencies (automatically installed):
- PyMuPDF (fitz) - PDF rendering
- Pillow - Image processing
- NumPy - Array operations
- Click - CLI interface
- tqdm - Progress bars
Optional dependencies:
[markdown]- Typst (0.2+) & Jinja2 (3.0+) for Markdown/Typst rendering[performance]- numba for 10x faster dithering
# After installation, activate your virtual environment
source .venv/bin/activate # On Windows: .venv\Scripts\activate
# Convert PDF to e-paper format
xtctool convert document.pdf -o document.xtc
# Convert images to XTC container
xtctool convert page1.png page2.jpg page3.png -o book.xtc
# Upload to your device
xtctool upload book.xtc --host 192.168.1.100The convert command handles all format conversions through a unified pipeline:
xtctool convert [OPTIONS] SOURCES... -o OUTPUTArguments:
SOURCES: One or more input files (PDF, PNG, JPG, XTC, XTH, XTG, TYP, MD)-o, --output: Output file path (required)-c, --config: Optional configuration file (TOML format)
Output format is determined by the file extension:
.xth- Single 4-level grayscale page (or multiple numbered files).xtg- Single 1-bit monochrome page (or multiple numbered files).xtc- Multi-page container with metadata.png- Debug output (decoded frames).pdf- Debug output (multi-page decoded frames)
Convert PDF to 4-level grayscale pages:
# Single page PDF → single XTH file
xtctool convert page.pdf -o output.xth
# Multi-page PDF → numbered XTH files (output_001.xth, output_002.xth, ...)
xtctool convert document.pdf -o output.xthConvert PDF to XTC container:
# All pages in one container with metadata
xtctool convert manga.pdf -o manga.xtc -c config.tomlRender markdown then convert to XTC:
# Convert README.md to XTC using default template
xtctool convert README.md -o readme.xtc -c config.tomlConvert images to XTC:
# Multiple images → XTC container
xtctool convert page1.png page2.jpg page3.png -o comic.xtcConvert to 1-bit monochrome (XTG):
# Use XTG for pure black and white
xtctool convert input.pdf -o output.xtgDecode frames for debugging:
# Convert XTC back to PNG images for inspection
xtctool convert input.xtc -o debug.png
# Convert XTC back to multi-page PDF
xtctool convert input.xtc -o debug.pdfMix different input formats:
# Combine PDFs, images, and existing frames
xtctool convert cover.png chapter1.pdf chapter2.pdf -o book.xtcConcatenate existing XTH/XTG/XTC files:
# Combine multiple XTH/XTG/XTC into one XTC
xtctool convert part1.xtc part2.xth part3.xtg -o full.xtcSelect specific pages from documents using colon syntax:
# Convert only pages 1-10 from a PDF
xtctool convert book.pdf:1-10 -o chapter1.xtc
# Extract first 3 pages for testing
xtctool convert document.md:-3 -o preview.xtcSupports single pages (5), ranges (1-10), lists (1,5,10), complex (1-4,7,10-12), and open-ended (5-, -3). Works with PDF, Markdown, Typst, and XTC files.
Upload files directly to ESP32 e-paper devices:
xtctool upload FILE --host HOST [OPTIONS]Options:
--host, -h: Device IP address (required)--port, -p: Device port (default: 80)--remote-path, -r: Remote file path (default: same as local filename)
Examples:
# Basic upload
xtctool upload comic.xtc --host 192.168.1.100
# Upload to specific path
xtctool upload page.xth --host 192.168.1.100 --remote-path /comics/page1.xth
# Upload to different port
xtctool upload output.xtc --host 192.168.1.100 --port 8080Create a config.toml file to manage conversion settings:
# config.toml
[output]
# Output dimensions
width = 480
height = 800
# XTC metadata (only used for .xtc output)
title = "My Comic"
author = "Author Name"
publisher = "Publisher Name"
language = "en-US"
direction = "ltr" # ltr, rtl, or ttb
[pdf]
# PDF rendering resolution in DPI
resolution = 144
[xth]
# 4-level grayscale thresholds (0-255)
threshold1 = 85 # Below this: Black
threshold2 = 170 # T1-T2: Dark gray
threshold3 = 255 # T2-T3: Light gray, Above: White
invert = false
dither = true
dither_strength = 0.8
[xtg]
# 1-bit monochrome threshold (0-255)
threshold = 128
invert = false
dither = true
dither_strength = 0.8Then use it with:
xtctool convert input.pdf -o output.xtc -c config.toml[output] section:
width,height: Target dimensions in pixelstitle,author,publisher,language: XTC metadatadirection: Reading direction (ltr,rtl,ttb)
[pdf] section:
resolution: PDF rendering DPI (higher = better quality, larger files)
[xth] section (4-level grayscale):
threshold1,threshold2,threshold3: Grayscale quantization thresholdsinvert: Invert black/whitedither: Enable Floyd-Steinberg ditheringdither_strength: Dithering intensity (0.0-1.0)
[xtg] section (1-bit monochrome):
threshold: Binarization thresholdinvert: Invert black/whitedither: Enable Floyd-Steinberg ditheringdither_strength: Dithering intensity (0.0-1.0)
[typst] section (Markdown/Typst rendering):
ppi: Rendering resolution (higher = better quality via supersampling)template: Template file for Markdown rendering (default:default.typ.jinja)font,font_size,line_spacing: Typography settingsmargin_left,margin_right,margin_top,margin_bottom: Page marginsshow_page_numbers,show_toc: Display options
Convert Markdown and Typst documents to e-paper formats with full typography control.
xtctool supports CommonMark only (via cmarker).
Not supported:
- Pandoc Markdown with extensions (
{.class},{#id}, etc.) - GitHub Flavored Markdown (GFM) tables/checkboxes
- Other non-standard markdown flavors
Converting other formats: Use pandoc to convert to CommonMark first (see EPUB workflow at top of README).
# EPUB, DOCX, HTML, etc. → CommonMark
pandoc input.epub -t commonmark -o output.md
pandoc document.docx -t commonmark -o output.md
pandoc page.html -t commonmark -o output.md# Convert Markdown to XTC (with default template)
xtctool convert document.md -o document.xtc
# Convert Typst file to XTC
xtctool convert document.typ -o document.xtc
# Use custom configuration
xtctool convert README.md -o readme.xtc -c config.tomlMarkdown rendering:
- Your
.mdfile is wrapped in a Typst template (Jinja2) - The template uses
cmarkerto render markdown with Typst styling - Typst compiles to high-resolution PNG (with supersampling for quality)
- Image is downsampled and dithered to e-paper format
Typst rendering:
- Your
.typfile is compiled directly by Typst - Supports multi-file projects (e.g.,
#includedirectives work) - Same supersampling and dithering pipeline
Templates are installed with xtctool in xtctool/templates/:
default.typ.jinja- Default template with configurable typography
Using custom templates:
-
Create your template (e.g.,
my-template.typ.jinja):#set page(width: {{ width_pt }}pt, height: {{ height_pt }}pt) #set text(font: "{{ font }}", size: {{ font_size }}pt) #import "@preview/cmarker:0.1.7" #cmarker.render(read("{{ markdown_file }}")) -
Specify in config:
[typst] template = "/path/to/my-template.typ.jinja"
Available template variables:
width_pt,height_pt- Page dimensions in pointswidth_px,height_px- Page dimensions in pixelsppi- Rendering resolutionfont,font_size,line_spacing- Typographymargin_left,margin_right,margin_top,margin_bottom- Marginsmarkdown_file- Path to markdown file (for templates)
See xtctool/templates/README.md for more details.
Recommended settings for text-heavy documents:
[output]
# Use BOX resampling for cleaner text (not LANCZOS)
resample_method = "BOX"
[typst]
# Higher PPI = better quality via supersampling
# 288 renders at 4x resolution, then downsamples
ppi = 288
# Typography
font = "Liberation Serif" # or "Times New Roman", "Georgia", etc.
font_size = 18
line_spacing = 1.1Why BOX resampling?
- LANCZOS causes blur and ringing artifacts in text
- BOX averages pixels cleanly when downsampling from high-res render
- Results in sharper, more readable text on e-paper
- PDF (
.pdf) - Rendered page-by-page using PyMuPDF - Images (
.png,.jpg,.jpeg) - Standard image formats - Markdown (
.md) - Rendered via Typst templates (requires[markdown]extra) - Typst (
.typ) - Native Typst documents (requires[markdown]extra) - XTC (
.xtc) - Multi-page container (can be decoded) - XTH (
.xth) - 4-level grayscale frame (can be reprocessed) - XTG (
.xtg) - 1-bit monochrome frame (can be reprocessed)
- 2 bits per pixel (4 grayscale levels: black, dark gray, light gray, white)
- Vertical bitplane encoding for efficient e-paper rendering
- Optimized for Xteink e-paper display LUT
- File size:
width × height × 2 bits + header
- 1 bit per pixel (pure black and white)
- Row-major bitmap encoding
- Smallest file size for simple graphics
- File size:
width × height × 1 bit + header
- Stores multiple XTH or XTG pages
- Supports metadata (title, author, publisher, language)
- Configurable reading direction (LTR, RTL, TTB)
- Index table for fast page access
- File size: sum of all pages + metadata + index
For XTH (4-level grayscale), three thresholds define the levels:
[xth]
threshold1 = 85 # 0-85 → Black
threshold2 = 170 # 86-170 → Dark gray
threshold3 = 255 # 171-255 → Light grayTips:
- Darker images: Decrease all thresholds (
60, 140, 220) - Lighter images: Increase all thresholds (
100, 180, 255) - High contrast: Increase spacing (
50, 150, 250) - Low contrast: Decrease spacing (
100, 140, 180)
Dithering improves perceived quality by distributing quantization errors:
[xth]
dither = true
dither_strength = 0.8 # 0.0 (off) to 1.0 (full)Strength guide:
1.0- Full dithering, best for photos0.8- Balanced (default)0.5- Subtle dithering0.0- No dithering (posterization)
Performance: Install numba for ~10x faster dithering:
pip install ".[performance]"Decode frames back to images for quality inspection:
# Decode single XTH/XTG to PNG
xtctool convert output.xth -o debug.png
# Decode XTC to multi-page PDF (lossless, high quality)
xtctool convert output.xtc -o debug.pdf
# Decode XTC to numbered PNGs
xtctool convert output.xtc -o debug.png # Creates debug_001.png, debug_002.png, ...Debug PDFs now use lossless compression (quality=100) to show true frame quality without JPEG artifacts.
# Install with dev dependencies
uv pip install -e ".[dev]"
# Run all tests
uv run pytest
# Run with coverage
uv run pytest --cov=xtctool
# Run specific test file
uv run pytest tests/test_pdf_pipeline.py -v# Format code
black xtctool/
# Type checking
mypy xtctool/
# Lint
ruff check xtctool/xtctool/
├── assets/ # Conversion pipeline assets
│ ├── pdf.py # PDF to images
│ ├── image.py # Image processing
│ ├── xtframe.py # XTH/XTG frames
│ └── xtcontainer.py # XTC containers
├── core/ # Format encoders/decoders
│ ├── xth.py # XTH format (4-level)
│ ├── xtg.py # XTG format (1-bit)
│ └── xtc.py # XTC container
├── utils/ # Utility modules
│ └── pdf.py # PDF renderer (PyMuPDF)
├── algo/ # Algorithms
│ └── dithering.py # Floyd-Steinberg dithering
├── cli/ # Command-line interface
│ ├── convert.py # Convert command
│ └── upload.py # Upload command
└── debug/ # Debug utilities
└── output.py # Frame decoding
- Magic:
0x00485458("XTH\0") - Encoding: Two vertical bitplanes (column-major, right-to-left)
- Pixel mapping for Xteink e-paper:
00(0) → White01(1) → Dark Gray10(2) → Light Gray11(3) → Black
- Magic:
0x00475458("XTG\0") - Encoding: Row-major bitmap (1 bit per pixel)
- Pixel mapping:
0= White,1= Black
- Magic:
0x00435458("XTC\0") - Structure: Header → Metadata → Page Index → Pages
- Supports: Multiple pages, metadata, reading direction
See XTC-XTG-XTH-XTCH.md for detailed format specifications, or refer to the XTC/XTH format specification for additional technical details.
Issue: Low quality output
- Increase PDF resolution:
resolution = 200in config - Adjust dithering strength:
dither_strength = 0.9 - Check debug output:
xtctool convert output.xtc -o debug.pdf
Issue: Slow conversion
- Install numba for faster dithering:
pip install ".[performance]" - Lower PDF resolution:
resolution = 120
Issue: Images too dark/light
- Adjust XTH thresholds in config.toml
- For dark images: decrease thresholds
- For light images: increase thresholds
Issue: Wrong size/aspect ratio or cropping
- Manual resolution tuning required: The PDF rendering resolution must be manually matched to your target display dimensions
- Calculate the appropriate DPI based on your PDF's page size to fit the target resolution (e.g., 480×800)
- For example: A Letter-size PDF (8.5"×11") at 144 DPI renders to ~1224×1584 pixels, which needs scaling to fit 480×800
- Adjust
resolutionin config.toml to achieve the desired output size - Refer to the format specification for display dimension requirements
Issue: Upload fails
- Check device IP:
ping 192.168.1.100 - Verify device is running HTTP server
- Check firewall settings
This project is licensed under the GNU GPLv3 License - see the LICENSE file for details.
Contributions welcome! Please:
- Fork the repository
- Create a feature branch
- Make your changes with tests
- Submit a pull request
For bugs and feature requests, please open an issue on GitHub.
- XTC/XTG/XTH format specification
- Xteink e-paper display project
- Xteink X4 - compact 4.3" e-ink reader
- PyMuPDF for efficient PDF rendering
- Typst and cmarker for Markdown rendering
About Xteink X4:
