Eunchurn Park f9b604f04e
Add XTC/XTCH ebook format support (#135)
## Summary

* **What is the goal of this PR?**

Add support for XTC (XTeink X4 native) ebook format, which contains
pre-rendered 480x800 1-bit bitmap pages optimized for e-ink displays.

* **What changes are included?**

- New `lib/Xtc/` library with XtcParser for reading XTC files
- XtcReaderActivity for displaying XTC pages on e-ink display
- XTC file detection in FileSelectionActivity
- Cover BMP generation from first XTC page
- Correct XTG page header structure (22 bytes) and bit polarity handling

## Additional Context

- XTC files contain pre-rendered bitmap pages with embedded status bar
(page numbers, progress %)
- XTG page header: 22 bytes (magic + dimensions + reserved fields +
bitmap size)
- Bit polarity: 0 = black, 1 = white
- No runtime text rendering needed - pages display directly on e-ink
- Faster page display compared to EPUB since no parsing/rendering
required
- Memory efficient: loads one page at a time (48KB per page)
- Tested with XTC files generated from https://x4converter.rho.sh/
- Verified correct page alignment and color rendering
- Please report any issues if you test with XTC files from other
sources.

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-29 01:56:05 +11:00

98 lines
4.0 KiB
C++

#pragma once
#include <EInkDisplay.h>
#include <EpdFontFamily.h>
#include <FS.h>
#include <map>
#include "Bitmap.h"
class GfxRenderer {
public:
enum RenderMode { BW, GRAYSCALE_LSB, GRAYSCALE_MSB };
// Logical screen orientation from the perspective of callers
enum Orientation {
Portrait, // 480x800 logical coordinates (current default)
LandscapeClockwise, // 800x480 logical coordinates, rotated 180° (swap top/bottom)
PortraitInverted, // 480x800 logical coordinates, inverted
LandscapeCounterClockwise // 800x480 logical coordinates, native panel orientation
};
private:
static constexpr size_t BW_BUFFER_CHUNK_SIZE = 8000; // 8KB chunks to allow for non-contiguous memory
static constexpr size_t BW_BUFFER_NUM_CHUNKS = EInkDisplay::BUFFER_SIZE / BW_BUFFER_CHUNK_SIZE;
static_assert(BW_BUFFER_CHUNK_SIZE * BW_BUFFER_NUM_CHUNKS == EInkDisplay::BUFFER_SIZE,
"BW buffer chunking does not line up with display buffer size");
EInkDisplay& einkDisplay;
RenderMode renderMode;
Orientation orientation;
uint8_t* bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr};
std::map<int, EpdFontFamily> fontMap;
void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, const int* y, bool pixelState,
EpdFontStyle style) const;
void freeBwBufferChunks();
void rotateCoordinates(int x, int y, int* rotatedX, int* rotatedY) const;
public:
explicit GfxRenderer(EInkDisplay& einkDisplay) : einkDisplay(einkDisplay), renderMode(BW), orientation(Portrait) {}
~GfxRenderer() = default;
static constexpr int VIEWABLE_MARGIN_TOP = 9;
static constexpr int VIEWABLE_MARGIN_RIGHT = 3;
static constexpr int VIEWABLE_MARGIN_BOTTOM = 3;
static constexpr int VIEWABLE_MARGIN_LEFT = 3;
// Setup
void insertFont(int fontId, EpdFontFamily font);
// Orientation control (affects logical width/height and coordinate transforms)
void setOrientation(const Orientation o) { orientation = o; }
Orientation getOrientation() const { return orientation; }
// Screen ops
int getScreenWidth() const;
int getScreenHeight() const;
void displayBuffer(EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH) const;
// EXPERIMENTAL: Windowed update - display only a rectangular region
void displayWindow(int x, int y, int width, int height) const;
void invertScreen() const;
void clearScreen(uint8_t color = 0xFF) const;
// Drawing
void drawPixel(int x, int y, bool state = true) const;
void drawLine(int x1, int y1, int x2, int y2, bool state = true) const;
void drawRect(int x, int y, int width, int height, bool state = true) const;
void fillRect(int x, int y, int width, int height, bool state = true) const;
void drawImage(const uint8_t bitmap[], int x, int y, int width, int height) const;
void drawBitmap(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight) const;
// Text
int getTextWidth(int fontId, const char* text, EpdFontStyle style = REGULAR) const;
void drawCenteredText(int fontId, int y, const char* text, bool black = true, EpdFontStyle style = REGULAR) const;
void drawText(int fontId, int x, int y, const char* text, bool black = true, EpdFontStyle style = REGULAR) const;
int getSpaceWidth(int fontId) const;
int getFontAscenderSize(int fontId) const;
int getLineHeight(int fontId) const;
// UI Components
void drawButtonHints(int fontId, const char* btn1, const char* btn2, const char* btn3, const char* btn4) const;
// Grayscale functions
void setRenderMode(const RenderMode mode) { this->renderMode = mode; }
void copyGrayscaleLsbBuffers() const;
void copyGrayscaleMsbBuffers() const;
void displayGrayBuffer() const;
bool storeBwBuffer(); // Returns true if buffer was stored successfully
void restoreBwBuffer();
void cleanupGrayscaleWithFrameBuffer() const;
// Low level functions
uint8_t* getFrameBuffer() const;
static size_t getBufferSize();
void grayscaleRevert() const;
void getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const;
};