This document is generated by AI, and there may be issues with the specific details.
- Version: 1.0
- Date: 2025-01-XX
- Purpose: Public technical specification for format conversion
- Target Platform: ESP32 E-Paper Display Devices
Four proprietary file formats designed for ESP32 e-paper displays:
| Format | Extension | Purpose | Description |
|---|---|---|---|
| XTG | .xtg |
Monochrome Image | 1-bit per pixel bitmap |
| XTH | .xth |
4-Level Grayscale Image | 2-bit per pixel bitmap |
| XTC | .xtc |
Comic Container | Container format with multiple XTG pages |
| XTCH | .xtch |
Comic Container Variant | Variant of XTC format |
All multi-byte values are stored in Little-Endian format.
- Little-Endian: Least significant byte at lowest address
- Example:
uint16_t 0x1234→[0x34, 0x12] - Example:
uint32_t 0x12345678→[0x78, 0x56, 0x34, 0x12]
XTG stores 1-bit per pixel monochrome bitmaps optimized for e-paper displays.
| Offset | Size | Type | Field | Description | Value |
|---|---|---|---|---|---|
| 0x00 | 4 | uint32_t | mark | File identifier | 0x00475458 ("XTG\0") |
| 0x04 | 2 | uint16_t | width | Image width (pixels) | 1-65535 |
| 0x06 | 2 | uint16_t | height | Image height (pixels) | 1-65535 |
| 0x08 | 1 | uint8_t | colorMode | Color mode | 0=monochrome |
| 0x09 | 1 | uint8_t | compression | Compression | 0=uncompressed |
| 0x0A | 4 | uint32_t | dataSize | Image data size (bytes) | Calculated |
| 0x0E | 8 | uint64_t | md5 | MD5 checksum (first 8 bytes) | Optional |
- Location: After header (offset 22 bytes)
- Format: Bitmap data, 1 bit per pixel
- Data Size Calculation:
dataSize = ((width + 7) / 8) * height - Pixel Storage:
- Rows stored top to bottom
- Each row stored left to right
- 8 pixels per byte (MSB first)
- Bit order: bit 7 (MSB) = leftmost pixel, bit 0 (LSB) = rightmost pixel
| Bit Value | Display Color |
|---|---|
| 0 | Black |
| 1 | White |
Note: When using inverted bitmap drawing, the display may invert these values (1=black, 0=white).
XTH stores 2-bit per pixel grayscale bitmaps for 4-level grayscale e-paper displays.
Same structure as XTG, but with different file identifier:
| Offset | Size | Type | Field | Description | Value |
|---|---|---|---|---|---|
| 0x00 | 4 | uint32_t | mark | File identifier | 0x00485458 ("XTH\0") |
| 0x04 | 2 | uint16_t | width | Image width (pixels) | 1-65535 |
| 0x06 | 2 | uint16_t | height | Image height (pixels) | 1-65535 |
| 0x08 | 1 | uint8_t | colorMode | Color mode | 0=monochrome |
| 0x09 | 1 | uint8_t | compression | Compression | 0=uncompressed |
| 0x0A | 4 | uint32_t | dataSize | Image data size (bytes) | Calculated |
| 0x0E | 8 | uint64_t | md5 | MD5 checksum (first 8 bytes) | Optional |
- Location: After header (offset 22 bytes)
- Format: Two bit planes, 2 bits per pixel total
- Data Size Calculation:
dataSize = ((width * height + 7) / 8) * 2(rounded up to bytes) - Storage:
- First bit plane: offset 22, size
(width * height + 7) / 8bytes (rounded up) - Second bit plane: immediately after first plane, same size
- Each bit plane stores pixels in vertical scan order (column-major):
- Columns scanned from right to left (x = width-1 to 0)
- 8 vertical pixels packed per byte (MSB = topmost pixel in group)
- See "Data Storage Details" section for complete specification
- First bit plane: offset 22, size
IMPORTANT: The Xteink e-paper LUT has non-linear grayscale mapping with swapped middle values:
| Pixel Value | Binary | Bit1 | Bit2 | LUT Level | Display Color |
|---|---|---|---|---|---|
| 0 | 00 |
0 | 0 | L0 | White |
| 1 | 01 |
0 | 1 | L1 | Dark Grey |
| 2 | 10 |
1 | 0 | L2 | Light Grey |
| 3 | 11 |
1 | 1 | L3 | Black |
Note: Values 1 and 2 are swapped compared to typical linear grayscale ordering. This matches the Xteink device's e-paper LUT behavior.
Bit Plane Usage:
- Bit1 (first plane): Sent via command
0x24 - Bit2 (second plane): Sent via command
0x26
Pixel Value Calculation: pixelValue = (bit1 << 1) | bit2
XTC is a container format storing multiple XTG-format pages for comic/PDF reading.
| Offset | Size | Type | Field | Description | Value |
|---|---|---|---|---|---|
| 0x00 | 4 | uint32_t | mark | File identifier | 0x00435458 ("XTC\0") |
| 0x04 | 2 | uint16_t | version | Version number | 0x0100 (v1.0) |
| 0x06 | 2 | uint16_t | pageCount | Total pages | 1-65535 |
| 0x08 | 1 | uint8_t | readDirection | Reading direction | 0-2 |
| 0x09 | 1 | uint8_t | hasMetadata | Has metadata | 0-1 |
| 0x0A | 1 | uint8_t | hasThumbnails | Has thumbnails | 0-1 |
| 0x0B | 1 | uint8_t | hasChapters | Has chapters | 0-1 |
| 0x0C | 4 | uint32_t | currentPage | Current page (1-based) | 0-65535 |
| 0x10 | 8 | uint64_t | metadataOffset | Metadata offset | Byte offset |
| 0x18 | 8 | uint64_t | indexOffset | Index table offset | Byte offset |
| 0x20 | 8 | uint64_t | dataOffset | Data area offset | Byte offset |
| 0x28 | 8 | uint64_t | thumbOffset | Thumbnail offset | Byte offset |
| 0x30 | 8 | uint64_t | chapterOffset | Chapter data offset | Byte offset |
| Value | Direction | Description |
|---|---|---|
| 0 | L→R | Left to right (normal) |
| 1 | R→L | Right to left (Japanese manga) |
| 2 | Top→Bottom | Top to bottom (vertical) |
If hasMetadata == 1, stored at metadataOffset:
| Offset | Size | Type | Field | Description |
|---|---|---|---|---|
| 0x00 | 128 | char[] | title | Title (UTF-8, null-terminated) |
| 0x80 | 64 | char[] | author | Author (UTF-8, null-terminated) |
| 0xC0 | 32 | char[] | publisher | Publisher/source (UTF-8, null-terminated) |
| 0xE0 | 16 | char[] | language | Language code (e.g., "zh-CN", "en-US") |
| 0xF0 | 4 | uint32_t | createTime | Creation time (Unix timestamp) |
| 0xF4 | 2 | uint16_t | coverPage | Cover page (0-based, 0xFFFF=none) |
| 0xF6 | 2 | uint16_t | chapterCount | Number of chapters |
| 0xF8 | 8 | uint64_t | reserved | Reserved (zero-filled) |
If hasChapters == 1, stored at chapterOffset:
| Offset | Size | Type | Field | Description |
|---|---|---|---|---|
| 0x00 | 80 | char[] | chapterName | Chapter name (UTF-8, null-terminated) |
| 0x50 | 2 | uint16_t | startPage | Start page (0-based) |
| 0x52 | 2 | uint16_t | endPage | End page (0-based, inclusive) |
| 0x54 | 4 | uint32_t | reserved1 | Reserved 1 (zero-filled) |
| 0x58 | 4 | uint32_t | reserved2 | Reserved 2 (zero-filled) |
| 0x5C | 4 | uint32_t | reserved3 | Reserved 3 (zero-filled) |
Number of chapters specified by XtcMetadata.chapterCount.
Stored at indexOffset, one entry per page:
| Offset | Size | Type | Field | Description |
|---|---|---|---|---|
| 0x00 | 8 | uint64_t | offset | XTG/XTH image offset (absolute, from file start) |
| 0x08 | 4 | uint32_t | size | XTG/XTH image size in bytes (including 22-byte header) |
| 0x0C | 2 | uint16_t | width | Image width (pixels) |
| 0x0E | 2 | uint16_t | height | Image height (pixels) |
Total index table size: pageCount * 16 bytes.
All XTG/XTH page images stored starting at dataOffset. Each page's data is stored contiguously, with position specified by the index table's offset field (absolute offset from file start).
If hasThumbnails == 1, thumbnails stored at thumbOffset. Thumbnails are also in XTG format with the same index structure.
[Header: 56 bytes]
[Metadata: 256 bytes] (optional, at metadataOffset)
[Chapters: N × 96 bytes] (optional, at chapterOffset)
[Page Index Table: pageCount × 16 bytes] (at indexOffset)
[Data Area: All XTG page data] (at dataOffset)
[Thumbnail Area] (optional, at thumbOffset)
Note: The actual order of sections in the file is determined by the offset fields in the header. The typical layout places metadata first, then chapters, then the page index, then page data, and finally thumbnails.
XTCH is identical to XTC in all aspects except the file identifier.
Same structure as XTC, only mark field differs:
| Offset | Size | Type | Field | Description | Value |
|---|---|---|---|---|---|
| 0x00 | 4 | uint32_t | mark | File identifier | 0x48435458 ("XTCH") |
| Format | Identifier (Little-Endian) | ASCII |
|---|---|---|
| XTC | 0x00435458 | "XTC\0" |
| XTCH | 0x48435458 | "XTCH" |
All other structures, fields, and data formats are identical to XTC.
- All string fields use UTF-8 encoding
- Strings are null-terminated (
\0) - Remaining space in fixed-size string fields is zero-filled
- Rows: top to bottom
- Pixels per row: left to right
- 8 pixels per byte
- Bit order: MSB (bit 7) = leftmost pixel, LSB (bit 0) = rightmost pixel
Example: 10-pixel wide image
- 2 bytes per row (rounded up)
- Byte 1: pixels 0-7 (bit 7 = pixel 0, bit 0 = pixel 7)
- Byte 2: pixels 8-9 (bit 7 = pixel 8, bit 6 = pixel 9, unused bits = 0)
- Two bit planes stored sequentially
- First plane: All pixels' Bit1 (pixel value bit 1), packed 8 pixels per byte
- Second plane: All pixels' Bit2 (pixel value bit 0), packed 8 pixels per byte
- IMPORTANT: Uses vertical scan order (column-major), NOT row-major:
- Columns are scanned from right to left (x = width-1 down to 0)
- Within each column, 8 vertical pixels are packed per byte (top to bottom)
- Each byte contains 8 vertically-adjacent pixels, MSB (bit 7) = topmost pixel in group
- This scan order matches the Xteink e-paper display refresh pattern
Pixel Value Calculation: pixelValue = (bit1 << 1) | bit2
Example: 480×800 pixel image
- Total pixels: 384000
- Each column: 800 pixels = 100 bytes (800/8 = 100 vertical groups)
- Total columns: 480 (scanned right to left)
- Each bit plane: 480 × 100 = 48000 bytes
- First plane: offset 22, size 48000 bytes (contains Bit1 for all pixels)
- Second plane: offset 48022, size 48000 bytes (contains Bit2 for all pixels)
- Total data size: 96000 bytes
-
Prepare Source Image:
- Convert to grayscale
- Resize to target resolution
- For XTG: Apply threshold/binarization
- For XTH: Map grayscale to 4 levels (0-3)
-
Generate Bitmap Data:
- XTG: Pack 1 bit per pixel, calculate
dataSize = ((width + 7) / 8) * height - XTH: Generate two bit planes, calculate
dataSize = ((width * height + 7) / 8) * 2
- XTG: Pack 1 bit per pixel, calculate
-
Write File:
- Write header with correct file identifier
- Write bitmap data
- Ensure Little-Endian byte order for all multi-byte values
- Read header to get page count and offsets
- Read page index table to locate each page
- Extract XTG data for each page from data area
- Convert each XTG page using XTG conversion process
| Format | Little-Endian uint32_t | Byte Sequence | ASCII |
|---|---|---|---|
| XTG | 0x00475458 | [0x58, 0x54, 0x47, 0x00] |
"XTG\0" |
| XTH | 0x00485458 | [0x58, 0x54, 0x48, 0x00] |
"XTH\0" |
| XTC | 0x00435458 | [0x58, 0x54, 0x43, 0x00] |
"XTC\0" |
| XTCH | 0x48435458 | [0x58, 0x54, 0x43, 0x48] |
"XTCH" |
- All offsets are byte offsets from the start of the file
- Page numbers in XTC/XTCH are 1-based for display, but 0-based in internal structures
- Reserved fields should be zero-filled
- MD5 checksum field is optional and may be zero
- Compression is currently not implemented (compression field = 0)
FYI: chapters feature is not implemented yet in the Xteink firmware (as of 3.1.0).