Add Xteink X3 support + fix image fit-box orientation#3
Conversation
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>
|
Warning Review limit reached
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 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. 📝 WalkthroughWalkthroughThis 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. ChangesMulti-device EPUB optimization (X4 & X3)
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
image_processor.py (1)
322-324: 💤 Low valueUse explicit
Optionaltype annotation forgray_levelsparameter.Static analysis correctly flags that PEP 484 prohibits implicit
Optional. For consistency with thelist[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 valueConsider validating against
DEVICE_PROFILESkeys for maintainability.The hardcoded tuple
("x4", "x3")duplicates the device list defined inDEVICE_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
📒 Files selected for processing (8)
.gitignoreREADME.mdapp.pyepub_processor.pyimage_processor.pystatic/app.jsstatic/style.csstemplates/index.html
… DEVICE_PROFILES Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add Xteink X3 support + fix image fit-box orientation
Summary
Two related changes, both validated on real hardware (X4 + X3):
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_PROFILESdict,gray_levelsonImageOptions,_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 gamutapp.py:device=x4|x3query param on/process/{task_id}(validated, defaults to x4)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:
and the firmware's
XtcTypes.hdefinesDISPLAY_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
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
x4_optimized_*→epubkit_optimized_*(no longer X4-only)*.epubadded to.gitignore(keeps local test books out of the repo)style.css/app.jsCompatibility notes
ProcessingOptions/ImageOptionsfield defaults are unchanged for existing callers;device='x4'is the default everywhere.max_width/max_heightviaImageOptions.Summary by CodeRabbit
Release Notes
New Features
Documentation