Skip to content

Add Xteink X3 support + fix image fit-box orientation#3

Open
casualducko wants to merge 6 commits into
b1rdmania:mainfrom
casualducko:x3-support
Open

Add Xteink X3 support + fix image fit-box orientation#3
casualducko wants to merge 6 commits into
b1rdmania:mainfrom
casualducko:x3-support

Conversation

@casualducko

@casualducko casualducko commented Jun 6, 2026

Copy link
Copy Markdown

Add Xteink X3 support + fix image fit-box orientation

Summary

Two related changes, both validated on real hardware (X4 + X3):

  1. X3 support (additive) — a device toggle in the UI with an X3 profile: 528×792 screen, same 4-level SSD1677 grayscale treatment as the X4.
  2. Portrait fit-box fix (changes existing X4 output) — images now fit the display orientation (X4: 480×800, X3: 528×792) instead of the panel scan orientation (800×480). On-device, this is the difference between a cover that fills the screen and one that renders as a small box in the corner.

1. X3 support

The X3's display is 528×792 portrait (3.68", ~259 PPI — confirmed by the PPI math against Xteink's official spec; some review sites incorrectly report it as sharing the X4's 480×800). Same SSD1677 controller and ESP32-C3 as the X4, so the existing 4-level grayscale pipeline applies unchanged — the profile only differs in dimensions.

Implementation:

  • image_processor.py: DEVICE_PROFILES dict, gray_levels on ImageOptions, _quantize_to_levels() generalizes the existing 4-level quantizer to any palette (machinery for future devices with different gray depths)
  • epub_processor.py: ProcessingOptions.device; generated covers use device dimensions and are quantized to the device palette so they match the display gamut
  • app.py: device=x4|x3 query param on /process/{task_id} (validated, defaults to x4)
  • UI: X4/X3 segmented toggle above the presets; device choice is independent of the Quick/Full/Custom preset

A finding worth documenting (now in the README): stock Xteink firmware — on both the X3 and X4 — does not render images inside EPUBs at all. Hardware-tested on both devices with an unmodified store EPUB and a diagnostic EPUB covering 8 encoding variants (odd/even widths, optimized/standard Huffman tables, 4:2:0/4:4:4 subsampling, single-channel JPEG, small dimensions, PNG, BMP) — every image page renders blank on stock, including the publisher's original JPEG, while text renders normally. EPUB image optimization therefore benefits CrossPoint-family firmware (CrossPoint, CrossInk, …), which renders 4-level grayscale images on both panels. This has presumably always been true of this tool's image pipeline; it's just now documented.

2. Portrait fit-box fix

The existing code fit images within 800×480 (panel scan orientation). But the readers display portrait — CrossPoint's converter profiles are:

const DEVICE_PROFILES = {
  X4: { width: 480, height: 800, label: 'X4' },
  X3: { width: 528, height: 792, label: 'X3' },
  // …

and the firmware's XtcTypes.h defines DISPLAY_WIDTH = 480; DISPLAY_HEIGHT = 800 (its debug tooling rotates the raw landscape framebuffer 270° for screenshots).

With the old 800×480 box, a portrait cover was capped at 480px tall (e.g. 320×480), and the firmware renders images at native size without upscaling — verified on an X4: the old-box cover draws as a small box occupying less than half the screen, while the same cover processed with the portrait box (480×721) fills the display.

This also fixes Light Novel mode: rotated landscape pages now fill the full 480×800 instead of being squeezed to 480px tall.

Hardware validation

Test Device / firmware Result
Portrait vs landscape fit box A/B X4, CrossInk (CrossPoint fork) Portrait fills screen; landscape-box cover renders as a small box
X3-processed EPUB X3, CrossInk Images render with correct dimensions and tone
X3-processed EPUB X3, stock Image pages blank — stock does not render EPUB images (text renders fine)
Original unprocessed EPUB (control) X3 + X4, stock Image pages equally blank on both — confirms stock limitation, not a pipeline regression
Portrait vs landscape fit box A/B X4, stock Both blank — stock X4 does not render EPUB images either
8-variant encoding diagnostic X3 + X4, stock All 8 blank on both devices — rules out odd-width/Huffman/subsampling/format/size causes; stock EPUB rendering is text-only

Pipeline-level verification (2.5MB store EPUB, 13 images): both profiles process in ~0.5s; all images baseline JPEG within the fit box; mimetype first ZIP entry stored uncompressed; all 30 XHTML files well-formed with identical word counts before/after; OPF/NCX parse cleanly.

Incidental changes

  • Batch ZIP renamed x4_optimized_*epubkit_optimized_* (no longer X4-only)
  • *.epub added to .gitignore (keeps local test books out of the repo)
  • Cache-busters bumped on style.css / app.js

Compatibility notes

  • ProcessingOptions / ImageOptions field defaults are unchanged for existing callers; device='x4' is the default everywhere.
  • The fit-box change intentionally affects existing X4 users' output (see Hardware validation above). Anyone who preferred the old behavior can pass custom max_width/max_height via ImageOptions.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added device selection UI to choose between X4 and X3 e-reader optimization
    • Implemented device-specific image processing tailored to each device's screen dimensions
    • Extended support for both 480×800 and 528×792 display formats
  • Documentation

    • Updated documentation to reflect support for both device models

casualducko and others added 5 commits June 6, 2026 15:31
X3 profile: 528x792 display, 2-level B/W Floyd-Steinberg dithering
(stock X3 firmware only renders 1-bit; gray data would be discarded).

Also fixes the image fit box orientation for both devices: panels scan
landscape but the readers display portrait, so images now fit 480x800
(X4) / 528x792 (X3), matching the CrossPoint reference converter.
Portrait illustrations previously got crushed to 480px tall and
upscaled blurrily by the reader; light novel pages now fill the full
screen. Generated covers are quantized to the device palette so gray
borders/text survive low-bit-depth displays.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hardware testing on stock X3 firmware showed it does not render EPUB
images at all (verified with an unmodified store EPUB and 8 encoding
variants: odd/even width, Huffman tables, subsampling, single-channel
JPEG, small dims, PNG, BMP — all blank). The 2-level B/W dithering
existed to protect stock users from gray-data loss, but stock users
never see EPUB images; the only X3 users who do run CrossPoint-family
firmware, which renders 4-level grayscale. Switch the X3 profile to
the same 4-level palette as the X4 (dims remain 528x792) and note the
stock limitation in the README.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stock X4 firmware tested with the same control + 8-variant diagnostic
as the X3: no EPUB images render on either device. EPUB image
optimization benefits CrossPoint-family firmware only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@casualducko, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 51 minutes and 35 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 85ba69c6-9453-4907-89a7-167f5be78122

📥 Commits

Reviewing files that changed from the base of the PR and between 0f6501b and e640e94.

📒 Files selected for processing (2)
  • app.py
  • image_processor.py
📝 Walkthrough

Walkthrough

This PR extends the EPUB processing pipeline to support multiple e-ink devices. It refactors image processing from X4-specific to device-profile-driven architecture, enabling the Xteink X3 (528×792) alongside the existing X4 (480×800). The changes include device profile definitions, generalized quantization, updated EPUB processing, HTTP API enhancement, frontend device selection UI, and documentation updates.

Changes

Multi-device EPUB optimization (X4 & X3)

Layer / File(s) Summary
Device profile infrastructure and ImageOptions factory
image_processor.py
DEVICE_PROFILES dictionary defines X4 and X3 portrait dimensions and grayscale levels; module constants derive from profiles; ImageOptions.for_device() factory method constructs processing options from a named device profile with overrides.
Generalized quantization engine
image_processor.py
_quantize_to_levels(img, levels) replaces fixed 4-level quantization, accepting arbitrary palette sizes and performing Floyd–Steinberg dithering for device-specific grayscale control.
Image processing with device-specific quantization and sizing limits
image_processor.py
process_image() enforces 1024×1024 hard JPEG limit with clamping details, applies optional auto-contrast, and quantizes using configurable gray_levels with detailed output descriptions.
Cover image generation with device quantization
image_processor.py
generate_cover_image() accepts optional gray_levels parameter and conditionally quantizes covers to device-specific palettes before saving.
EPUB processing pipeline device integration
epub_processor.py
ProcessingOptions gains device field; process_epub() calls ImageOptions.for_device(options.device) and extracts cover dimensions/grayscale from device profiles based on selected device.
HTTP API device parameter and validation
app.py
/process/{task_id} endpoint accepts device parameter (default "x4"), validates against allowed set, and passes to ProcessingOptions; /download-all ZIP filenames changed from x4_optimized_* to epubkit_optimized_*.
Frontend device selection state and integration
static/app.js
Tracks selectedDevice state, implements .device-btn click handler, extends options with device field, and sends device parameter in processing requests.
Device toggle UI styling and HTML markup
static/style.css, templates/index.html
Adds .device-bar CSS with split-button styling, responsive mobile stacking; inserts X4/X3 device selection buttons into Options panel; updates cache-busting query parameters (?v=3).
Documentation and configuration updates
README.md, templates/index.html, .gitignore
Updates README to describe dual-device support and device-specific resizing; revises About section and preset descriptions; adds *.epub to .gitignore.

Sequence Diagram

sequenceDiagram
  participant Frontend as Frontend (app.js)
  participant API as HTTP API (app.py)
  participant Pipeline as EPUB Pipeline (epub_processor.py)
  participant ImageProc as Image Processor (image_processor.py)
  participant DeviceProfile as Device Profiles
  
  Frontend->>API: POST /process/{task_id}?device=x3
  API->>API: Validate device ∈ {x4, x3}
  API->>Pipeline: process_epub(options.device=x3)
  Pipeline->>DeviceProfile: Lookup DEVICE_PROFILES[x3]
  Pipeline->>ImageProc: ImageOptions.for_device(x3)
  ImageProc->>ImageProc: _quantize_to_levels(img, gray_levels)
  ImageProc->>Frontend: Processed image with x3 dimensions
  Pipeline->>DeviceProfile: Lookup cover dims for x3
  Pipeline->>ImageProc: generate_cover_image(..., gray_levels)
  ImageProc->>Frontend: Device-specific EPUB
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A rabbit hops through profiles new,
X3 and X4 in clearer view,
Device dimensions now take flight,
Quantized in shades of e-ink bright!
✨ Multi-device magic, oh what a sight! 📱

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly reflects the two main changes in the PR: adding X3 device support and fixing image fit-box orientation to use portrait dimensions.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
image_processor.py (1)

322-324: 💤 Low value

Use explicit Optional type annotation for gray_levels parameter.

Static analysis correctly flags that PEP 484 prohibits implicit Optional. For consistency with the list[int] type hint used in _quantize_to_levels, consider using the explicit union syntax.

🔧 Suggested fix
 def generate_cover_image(title: str, author: str,
                          width: int = X4_WIDTH, height: int = X4_HEIGHT,
-                         gray_levels: list = None) -> bytes:
+                         gray_levels: list[int] | None = None) -> bytes:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@image_processor.py` around lines 322 - 324, The function signature for
generate_cover_image currently uses a bare Noneable parameter for gray_levels;
update its type annotation to an explicit optional union (e.g.,
Optional[list[int]] or list[int] | None depending on your Python version) to
match the explicit list[int] usage in _quantize_to_levels and satisfy PEP 484;
adjust any imports (from typing import Optional) if needed and ensure all
internal usage assumes a list[int] when not None.

Source: Linters/SAST tools

app.py (1)

157-159: 💤 Low value

Consider validating against DEVICE_PROFILES keys for maintainability.

The hardcoded tuple ("x4", "x3") duplicates the device list defined in DEVICE_PROFILES (image_processor.py). If a new device is added to the profiles, this validation must be updated separately.

♻️ Optional: Validate against the canonical device list
+from image_processor import DEVICE_PROFILES
+
 ...
 
-    if device not in ("x4", "x3"):
-        raise HTTPException(status_code=400, detail="Unknown device (expected 'x4' or 'x3')")
+    if device not in DEVICE_PROFILES:
+        allowed = ", ".join(f"'{d}'" for d in DEVICE_PROFILES)
+        raise HTTPException(status_code=400, detail=f"Unknown device (expected {allowed})")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app.py` around lines 157 - 159, Replace the hardcoded device tuple check with
a validation against the canonical DEVICE_PROFILES mapping from
image_processor.py: import DEVICE_PROFILES and use its keys (or membership of
DEVICE_PROFILES) to verify the incoming device in the same place where the
current check exists (the block containing the if device not in ("x4", "x3")).
This ensures new devices added to DEVICE_PROFILES are automatically accepted and
removes duplicate source-of-truth; keep the raised HTTPException unchanged for
failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@app.py`:
- Around line 157-159: Replace the hardcoded device tuple check with a
validation against the canonical DEVICE_PROFILES mapping from
image_processor.py: import DEVICE_PROFILES and use its keys (or membership of
DEVICE_PROFILES) to verify the incoming device in the same place where the
current check exists (the block containing the if device not in ("x4", "x3")).
This ensures new devices added to DEVICE_PROFILES are automatically accepted and
removes duplicate source-of-truth; keep the raised HTTPException unchanged for
failures.

In `@image_processor.py`:
- Around line 322-324: The function signature for generate_cover_image currently
uses a bare Noneable parameter for gray_levels; update its type annotation to an
explicit optional union (e.g., Optional[list[int]] or list[int] | None depending
on your Python version) to match the explicit list[int] usage in
_quantize_to_levels and satisfy PEP 484; adjust any imports (from typing import
Optional) if needed and ensure all internal usage assumes a list[int] when not
None.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e73da9d1-c1c8-4162-ac8e-05629bb3fadc

📥 Commits

Reviewing files that changed from the base of the PR and between 84c4aa9 and 0f6501b.

📒 Files selected for processing (8)
  • .gitignore
  • README.md
  • app.py
  • epub_processor.py
  • image_processor.py
  • static/app.js
  • static/style.css
  • templates/index.html

… DEVICE_PROFILES

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant