Go library and CLI for encoding, decoding, and manipulating Xteink e-paper image and comic container formats.
| Format | Extension | Description |
|---|---|---|
| XTG | .xtg |
1-bit monochrome image (MSB-first, packed) |
| XTH | .xth |
2-bit grayscale image (4 levels, column-major dual bit-plane) |
| XTC | .xtc |
Comic container (multiple XTG pages + metadata) |
| XTCH | .xtch |
Comic container (multiple XTH pages + metadata) |
All formats use little-endian binary encoding. See SPEC.md for the full binary specification.
go get github.com/phrozen/xtxDownload a pre-built binary from the Releases page (Windows, macOS, Linux), or build from source:
go install github.com/phrozen/xtx/cmd/xtx@latestxtx <command> [flags] [arguments]
The output format is inferred from the file extension (.xtg = monochrome, .xth = grayscale).
# Monochrome (inferred from .xtg)
xtx encode photo.png page.xtg
# Grayscale with dithering (inferred from .xth)
xtx encode -d photo.png page.xth
# Resize to exact dimensions
xtx encode -resize 480x800 photo.png page.xth
# Resize by width, auto-calculate height (preserves aspect ratio)
xtx encode -resize 480x-1 photo.png page.xth
# Resize by height, auto-calculate width
xtx encode -resize -1x800 photo.png page.xth| Flag | Description |
|---|---|
-g |
Force XTH (grayscale) regardless of extension |
-dither str |
Dithering algorithm (none, floyd-steinberg, atkinson, sierra, sierra-lite, stucki) |
-resize WxH |
Resize before encoding; use -1 for auto (e.g. 480x-1) |
Output format is inferred from extension (.jpg/.jpeg = JPEG, default = PNG).
xtx decode page.xtg output.png
xtx decode page.xth output.jpgxtx info page.xtg
# Format: XTG (Monochrome)
# Size: 800 × 480
# Data size: 48000 bytes
xtx info comic.xtc
# Format: XTC (Comic Container)
# Pages: 52
# Direction: Left → Right
# Title: My Manga
# Author: Author Namextx pack -g -dither atkinson -title "My Comic" -author "Author" comic.xtc page1.png page2.png| Flag | Description |
|---|---|
-g |
Encode pages as XTH instead of XTG (auto-selects XTCH container) |
-dither str |
Dithering algorithm (none, floyd-steinberg, atkinson, sierra, sierra-lite, stucki) |
-dir N |
Reading direction: 0=LTR, 1=RTL, 2=TTB (default: 0) |
-title X |
Set comic title |
-author X |
Set comic author |
-publisher X |
Set publisher name |
-lang X |
Set language code (e.g. en, ja) |
-cover N |
Set cover page index, 0-based (default: 0) |
xtx unpack comic.xtc ./pages/
# Extracts page_000.png, page_001.png, ...Reads a CBZ (ZIP of images) and converts to an XTC container. The -format flag controls how source images are processed into pages:
| Format | Strategy | Direction |
|---|---|---|
comic |
Fit width, split height per-image (default) | LTR |
manga |
Fit width, split height per-image | RTL |
webtoon |
Continuous vertical stitching across images | TTB |
comic/manga: Each source image is resized to fit the page width, then split into multiple pages if it exceeds the page height. Page boundaries respect image boundaries — no content from one source image bleeds into the next.
webtoon: All source images are treated as a continuous vertical strip. Content flows across image boundaries, producing tightly packed pages.
# Western comic (default format, LTR)
xtx cbz -title "Batman" -author "DC Comics" volume1.cbz
# Japanese manga (RTL, per-image split)
xtx cbz -format manga -title "One Piece" -author "Oda" volume1.cbz
# Webtoon (TTB, continuous stitching)
xtx cbz -format webtoon -title "Solo Leveling" -author "Chugong" volume1.cbz
# Landscape reading: pages composed at 800×480 then rotated for e-reader
xtx cbz -format manga -landscape -title "Berserk" volume1.cbz| Flag | Description |
|---|---|
-o path |
Output file path (default: input with .xtc/.xtch extension) |
-format X |
Page format: comic, manga, or webtoon (default: comic) |
-landscape |
Landscape reading: swap page dimensions and rotate pages 90° |
-mono |
Encode as monochrome (XTG) instead of grayscale |
-dither str |
Dithering algorithm (none, floyd-steinberg, atkinson, sierra, sierra-lite, stucki) |
-width N |
Page width in pixels (default: 480) |
-height N |
Page height in pixels (default: 800) |
-title X |
Set comic title |
-author X |
Set comic author |
-publisher X |
Set publisher name |
-lang X |
Set language code (e.g. en, ja) |
-cover N |
Set cover page index, 0-based (default: 0) |
The library follows standard Go image idioms. Encode writes to an io.Writer, Decode reads from an io.Reader, and all image types implement image.Image.
import "github.com/phrozen/xtx"
// func Encode(w io.Writer, img image.Image, opts *Options) error
// Encode any image.Image as monochrome XTG (nil opts = defaults)
f, _ := os.Create("page.xtg")
err := xtx.Encode(f, img, nil)
// Encode as grayscale XTH with Floyd-Steinberg dithering
f, _ := os.Create("page.xth")
err := xtx.Encode(f, img, &xtx.Options{
Format: xtx.FormatGrayscale,
Dither: true,
})// func Decode(r io.Reader) (image.Image, error)
// Decode XTG or XTH (format auto-detected from magic bytes)
f, _ := os.Open("page.xtg")
img, err := xtx.Decode(f)
// XTG/XTH are also registered with image.RegisterFormat,
// so importing the package enables standard library decoding:
import _ "github.com/phrozen/xtx"
img, format, err := image.Decode(f) // format = "xtg" or "xth"Both Monochrome and Grayscale implement image.Image, draw.Image, and work with the standard library's image/draw package.
// Create a 1-bit monochrome image
mono := xtx.NewMonochrome(image.Rect(0, 0, 800, 480))
mono.SetMono(x, y, xtx.MonoBlack)
// Create a 2-bit grayscale image (4 levels)
gray := xtx.NewGrayscale(image.Rect(0, 0, 480, 800))
gray.SetGray4(x, y, xtx.Gray4{V: 2}) // light grey
// Floyd-Steinberg dithering via stdlib
draw.FloydSteinberg.Draw(mono, mono.Bounds(), src, src.Bounds().Min)| Type | Bits | Levels | Bit Values |
|---|---|---|---|
Mono |
1 | 2 | 0=black, 1=white |
Gray4 |
2 | 4 | 0=white, 1=dark grey, 2=light grey, 3=black |
The Gray4 level mapping follows the non-linear Xteink e-paper LUT (levels 1 and 2 are swapped relative to a linear ramp).
Note: The zero-value convention is inconsistent between formats —
0means black in Mono but white in Gray4. This matches the Xteink hardware LUT and cannot be changed. The library handles this internally, so users working withimage.Imagedon't need to worry about it.
comic := xtx.NewXTC(false) // true for XTCH variant
comic.SetDirection(xtx.RightToLeft)
comic.SetMetadata(xtx.Metadata{
Title: "My Manga",
Author: "Author Name",
})
// Add pages (accepts any image.Image, encodes internally)
comic.AddPage(page1) // as XTG (monochrome)
comic.AddPageXTH(page2) // as XTH (grayscale)
// Write container
err := comic.Encode(w)// Resize (uses approximate bilinear interpolation)
resized := xtx.Resize(img, 480, 800)
// Aspect-ratio preserving resize
resized := xtx.ResizeToWidth(img, 480)
resized := xtx.ResizeToHeight(img, 800)
// Rotation (90° clockwise / counter-clockwise)
rotated := xtx.RotateCW(img)
rotated := xtx.RotateCCW(img)
// Create a blank white page
page := xtx.NewWhitePage(480, 800)// Read and decode all images from a CBZ file
// (filters hidden files, sorts alphabetically)
images, err := xtx.ReadCBZ("volume1.cbz")Comic containers support three reading directions, matching the XTC specification:
| Value | Constant | Description |
|---|---|---|
0 |
LeftToRight |
Western comics (default) |
1 |
RightToLeft |
Japanese manga |
2 |
TopToBottom |
Webtoons / vertical scroll |
cmd/
└── xtx/
├── main.go CLI entry point and usage text
├── encode.go encode command
├── decode.go decode command
├── info.go info command
├── pack.go pack command
├── unpack.go unpack command
├── cbz.go cbz command
└── util.go shared helpers (image loading)
xtx.go Core types, constants, errors
color.go Mono and Gray4 color types and models
monochrome.go 1-bit image type (image.Image)
grayscale.go 2-bit image type (image.Image)
header.go 22-byte XTG/XTH header read/write/validate
encode.go Encode image.Image → XTG/XTH
decode.go Decode XTG/XTH → image.Image
xtc.go XTC/XTCH container builder and encoder
transform.go Resize, rotate, white page utilities
cbz.go CBZ archive reader
SPEC.md Full binary format specification
MIT — see LICENSE.