Support transparent PNG

This commit is contained in:
jpirnay
2026-03-02 10:52:22 +01:00
parent 1119030bb0
commit b531077b8c
11 changed files with 627 additions and 2 deletions
+1
View File
@@ -273,6 +273,7 @@ STR_SUNLIGHT_FADING_FIX: "Sunlight Fading Fix"
STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons"
STR_OPDS_BROWSER: "OPDS Browser"
STR_COVER_CUSTOM: "Cover + Custom"
STR_PAGE_OVERLAY: "Page overlay"
STR_RECENTS: "Recents"
STR_MENU_RECENT_BOOKS: "Recent Books"
STR_NO_RECENT_BOOKS: "No recent books"
+1
View File
@@ -24,6 +24,7 @@ class CrossPointSettings {
COVER = 3,
BLANK = 4,
COVER_CUSTOM = 5,
OVERLAY = 6,
SLEEP_SCREEN_MODE_COUNT
};
enum SLEEP_SCREEN_COVER_MODE { FIT = 0, CROP = 1, SLEEP_SCREEN_COVER_MODE_COUNT };
+1 -1
View File
@@ -16,7 +16,7 @@ inline const std::vector<SettingInfo>& getSettingsList() {
// --- Display ---
SettingInfo::Enum(StrId::STR_SLEEP_SCREEN, &CrossPointSettings::sleepScreen,
{StrId::STR_DARK, StrId::STR_LIGHT, StrId::STR_CUSTOM, StrId::STR_COVER, StrId::STR_NONE_OPT,
StrId::STR_COVER_CUSTOM},
StrId::STR_COVER_CUSTOM, StrId::STR_PAGE_OVERLAY},
"sleepScreen", StrId::STR_CAT_DISPLAY),
SettingInfo::Enum(StrId::STR_SLEEP_COVER_MODE, &CrossPointSettings::sleepScreenCoverMode,
{StrId::STR_FIT, StrId::STR_CROP}, "sleepScreenCoverMode", StrId::STR_CAT_DISPLAY),
+286 -1
View File
@@ -4,19 +4,142 @@
#include <GfxRenderer.h>
#include <HalStorage.h>
#include <I18n.h>
#include <PNGdec.h>
#include <Txt.h>
#include <Xtc.h>
#include <new>
#include "CrossPointSettings.h"
#include "CrossPointState.h"
#include "components/UITheme.h"
#include "fontIds.h"
#include "images/Logo120.h"
#include "reader/EpubReaderActivity.h"
#include "reader/TxtReaderActivity.h"
#include "reader/XtcReaderActivity.h"
#include "util/StringUtils.h"
namespace {
// Context passed through PNGdec's decode() user-pointer to the per-scanline draw callback.
struct PngOverlayCtx {
const GfxRenderer* renderer;
int screenW;
int screenH;
int srcWidth;
int dstWidth;
int dstX;
int dstY;
float yScale;
int lastDstY;
};
// PNGdec file I/O callbacks — mirror the pattern in PngToFramebufferConverter.cpp.
void* pngSleepOpen(const char* filename, int32_t* size) {
FsFile* f = new FsFile();
if (!Storage.openFileForRead("SLP", std::string(filename), *f)) {
delete f;
return nullptr;
}
*size = f->size();
return f;
}
void pngSleepClose(void* handle) {
FsFile* f = reinterpret_cast<FsFile*>(handle);
if (f) {
f->close();
delete f;
}
}
int32_t pngSleepRead(PNGFILE* pFile, uint8_t* pBuf, int32_t len) {
FsFile* f = reinterpret_cast<FsFile*>(pFile->fHandle);
return f ? f->read(pBuf, len) : 0;
}
int32_t pngSleepSeek(PNGFILE* pFile, int32_t pos) {
FsFile* f = reinterpret_cast<FsFile*>(pFile->fHandle);
if (!f) return -1;
return f->seek(pos);
}
// Per-scanline draw callback for PNG overlay compositing.
// Transparent pixels (alpha < 128) are skipped so the reader page shows through.
// Opaque pixels are drawn in their grayscale brightness (dark → black, light → white).
int pngOverlayDraw(PNGDRAW* pDraw) {
PngOverlayCtx* ctx = reinterpret_cast<PngOverlayCtx*>(pDraw->pUser);
const int destY = ctx->dstY + (int)(pDraw->y * ctx->yScale);
if (destY == ctx->lastDstY) return 1; // skip duplicate rows from Y scaling
ctx->lastDstY = destY;
if (destY < 0 || destY >= ctx->screenH) return 1;
const int srcWidth = ctx->srcWidth;
const int dstWidth = ctx->dstWidth;
const uint8_t* pixels = pDraw->pPixels;
const int pixelType = pDraw->iPixelType;
const int hasAlpha = pDraw->iHasAlpha;
int srcX = 0, error = 0;
for (int dstX = 0; dstX < dstWidth; dstX++) {
const int outX = ctx->dstX + dstX;
if (outX >= 0 && outX < ctx->screenW) {
uint8_t alpha = 255, gray = 0;
switch (pixelType) {
case PNG_PIXEL_TRUECOLOR_ALPHA: {
const uint8_t* p = &pixels[srcX * 4];
alpha = p[3];
gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
break;
}
case PNG_PIXEL_GRAY_ALPHA:
gray = pixels[srcX * 2];
alpha = pixels[srcX * 2 + 1];
break;
case PNG_PIXEL_TRUECOLOR: {
const uint8_t* p = &pixels[srcX * 3];
gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
break;
}
case PNG_PIXEL_GRAYSCALE:
gray = pixels[srcX];
break;
case PNG_PIXEL_INDEXED:
if (pDraw->pPalette) {
const uint8_t idx = pixels[srcX];
const uint8_t* p = &pDraw->pPalette[idx * 3];
gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
if (hasAlpha) alpha = pDraw->pPalette[768 + idx];
}
break;
default:
gray = pixels[srcX];
break;
}
if (alpha >= 128) {
ctx->renderer->drawPixel(outX, destY, gray < 128); // true = black, false = white
}
// alpha < 128: transparent — leave the reader page pixel intact
}
// Bresenham-style X stepping (handles downscaling; 1:1 when srcWidth == dstWidth)
error += srcWidth;
while (error >= dstWidth) {
error -= dstWidth;
srcX++;
}
}
return 1;
}
} // namespace
void SleepActivity::onEnter() {
Activity::onEnter();
GUI.drawPopup(renderer, tr(STR_ENTERING_SLEEP));
// For OVERLAY mode the popup is suppressed so the frame buffer (reader page) stays intact
if (SETTINGS.sleepScreen != CrossPointSettings::SLEEP_SCREEN_MODE::OVERLAY) {
GUI.drawPopup(renderer, tr(STR_ENTERING_SLEEP));
}
switch (SETTINGS.sleepScreen) {
case (CrossPointSettings::SLEEP_SCREEN_MODE::BLANK):
@@ -26,6 +149,8 @@ void SleepActivity::onEnter() {
case (CrossPointSettings::SLEEP_SCREEN_MODE::COVER):
case (CrossPointSettings::SLEEP_SCREEN_MODE::COVER_CUSTOM):
return renderCoverSleepScreen();
case (CrossPointSettings::SLEEP_SCREEN_MODE::OVERLAY):
return renderOverlaySleepScreen();
default:
return renderDefaultSleepScreen();
}
@@ -284,3 +409,163 @@ void SleepActivity::renderBlankSleepScreen() const {
renderer.clearScreen();
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
}
void SleepActivity::renderOverlaySleepScreen() const {
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
// Step 1: Ensure the frame buffer contains the reader page.
// When coming from a reader activity the frame buffer already holds the page.
// When coming from a non-reader activity we re-render it from the saved progress.
if (!APP_STATE.lastSleepFromReader && !APP_STATE.openEpubPath.empty()) {
const auto& path = APP_STATE.openEpubPath;
bool rendered = false;
if (StringUtils::checkFileExtension(path, ".xtc") || StringUtils::checkFileExtension(path, ".xtch")) {
rendered = XtcReaderActivity::drawCurrentPageToBuffer(path, renderer);
} else if (StringUtils::checkFileExtension(path, ".txt")) {
rendered = TxtReaderActivity::drawCurrentPageToBuffer(path, renderer);
} else if (StringUtils::checkFileExtension(path, ".epub")) {
rendered = EpubReaderActivity::drawCurrentPageToBuffer(path, renderer);
}
if (!rendered) {
LOG_DBG("SLP", "Page re-render failed, using white background");
renderer.clearScreen();
}
}
// Step 2: Load the overlay image using the same selection logic as renderCustomSleepScreen.
// BMP: white pixels are skipped (transparent via drawBitmap), black pixels composited on top.
// PNG: pixels with alpha < 128 are skipped; opaque pixels are drawn with their grayscale value.
auto tryDrawOverlay = [&](const std::string& filename) -> bool {
FsFile file;
if (!Storage.openFileForRead("SLP", filename, file)) return false;
Bitmap bitmap(file, true);
if (bitmap.parseHeaders() != BmpReaderError::Ok) {
file.close();
return false;
}
int x, y;
float cropX = 0, cropY = 0;
if (bitmap.getWidth() > pageWidth || bitmap.getHeight() > pageHeight) {
float ratio = static_cast<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight());
const float screenRatio = static_cast<float>(pageWidth) / static_cast<float>(pageHeight);
if (ratio > screenRatio) {
x = 0;
y = std::round((static_cast<float>(pageHeight) - static_cast<float>(pageWidth) / ratio) / 2);
} else {
x = std::round((static_cast<float>(pageWidth) - static_cast<float>(pageHeight) * ratio) / 2);
y = 0;
}
} else {
x = (pageWidth - bitmap.getWidth()) / 2;
y = (pageHeight - bitmap.getHeight()) / 2;
}
// Draw without clearScreen so the reader page remains in the frame buffer beneath
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY);
file.close();
return true;
};
auto tryDrawPngOverlay = [&](const std::string& filename) -> bool {
constexpr size_t MIN_FREE_HEAP = 60 * 1024; // PNG decoder ~42 KB + overhead
if (ESP.getFreeHeap() < MIN_FREE_HEAP) {
LOG_ERR("SLP", "Not enough heap for PNG overlay decoder");
return false;
}
PNG* png = new (std::nothrow) PNG();
if (!png) return false;
int rc = png->open(filename.c_str(), pngSleepOpen, pngSleepClose, pngSleepRead, pngSleepSeek, pngOverlayDraw);
if (rc != PNG_SUCCESS) {
LOG_DBG("SLP", "PNG open failed: %s (%d)", filename.c_str(), rc);
delete png;
return false;
}
const int srcW = png->getWidth(), srcH = png->getHeight();
float yScale = 1.0f;
int dstW = srcW, dstH = srcH;
if (srcW > pageWidth || srcH > pageHeight) {
const float scaleX = (float)pageWidth / srcW, scaleY = (float)pageHeight / srcH;
const float scale = (scaleX < scaleY) ? scaleX : scaleY;
dstW = (int)(srcW * scale);
dstH = (int)(srcH * scale);
yScale = (float)dstH / srcH;
}
PngOverlayCtx ctx;
ctx.renderer = &renderer;
ctx.screenW = pageWidth;
ctx.screenH = pageHeight;
ctx.srcWidth = srcW;
ctx.dstWidth = dstW;
ctx.dstX = (pageWidth - dstW) / 2;
ctx.dstY = (pageHeight - dstH) / 2;
ctx.yScale = yScale;
ctx.lastDstY = -1;
rc = png->decode(&ctx, 0);
png->close();
delete png;
return rc == PNG_SUCCESS;
};
// Try /sleep/ directory first (random selection, same as renderCustomSleepScreen).
// Accepts both .bmp and .png files; .bmp headers are validated during the scan.
bool overlayDrawn = false;
auto dir = Storage.open("/sleep");
if (dir && dir.isDirectory()) {
std::vector<std::string> files;
char name[500];
for (auto file = dir.openNextFile(); file; file = dir.openNextFile()) {
if (file.isDirectory()) { file.close(); continue; }
file.getName(name, sizeof(name));
auto filename = std::string(name);
if (filename[0] == '.') { file.close(); continue; }
const auto flen = filename.length();
const bool isBmp = flen >= 4 && filename.substr(flen - 4) == ".bmp";
const bool isPng = flen >= 4 && filename.substr(flen - 4) == ".png";
if (!isBmp && !isPng) { file.close(); continue; }
if (isBmp) {
Bitmap bmp(file);
if (bmp.parseHeaders() != BmpReaderError::Ok) { file.close(); continue; }
}
files.emplace_back(filename);
file.close();
}
const auto numFiles = files.size();
if (numFiles > 0) {
auto randomFileIndex = random(numFiles);
while (numFiles > 1 && randomFileIndex == APP_STATE.lastSleepImage) {
randomFileIndex = random(numFiles);
}
APP_STATE.lastSleepImage = randomFileIndex;
APP_STATE.saveToFile();
const std::string selected = "/sleep/" + files[randomFileIndex];
const auto slen = selected.length();
if (slen >= 4 && selected.substr(slen - 4) == ".png") {
overlayDrawn = tryDrawPngOverlay(selected);
} else {
overlayDrawn = tryDrawOverlay(selected);
}
}
}
if (dir) dir.close();
if (!overlayDrawn) {
overlayDrawn = tryDrawOverlay("/sleep.bmp");
}
if (!overlayDrawn) {
overlayDrawn = tryDrawPngOverlay("/sleep.png");
}
if (!overlayDrawn) {
LOG_DBG("SLP", "No overlay image found, displaying page without overlay");
}
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
}
@@ -15,4 +15,5 @@ class SleepActivity final : public Activity {
void renderCoverSleepScreen() const;
void renderBitmapSleepScreen(const Bitmap& bitmap) const;
void renderBlankSleepScreen() const;
void renderOverlaySleepScreen() const;
};
@@ -829,3 +829,65 @@ void EpubReaderActivity::restoreSavedPosition() {
}
requestUpdate();
}
bool EpubReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, GfxRenderer& renderer) {
auto epub = std::make_shared<Epub>(filePath, "/.crosspoint");
// skip CSS (second arg) since we only need layout/section data
if (!epub->load(true, false)) {
LOG_DBG("SLP", "EPUB: failed to load %s", filePath.c_str());
return false;
}
epub->setupCacheDir();
// Load saved spine index and page number
int spineIndex = 0, pageNumber = 0;
FsFile f;
if (Storage.openFileForRead("SLP", epub->getCachePath() + "/progress.bin", f)) {
uint8_t data[6];
if (f.read(data, 6) == 6) {
spineIndex = data[0] | (data[1] << 8);
pageNumber = data[2] | (data[3] << 8);
}
f.close();
}
if (spineIndex < 0 || spineIndex >= epub->getSpineItemsCount()) spineIndex = 0;
// Apply the reader orientation so margins match what the reader would produce
applyReaderOrientation(renderer, SETTINGS.orientation);
// Compute margins exactly as render() does
int marginTop, marginRight, marginBottom, marginLeft;
renderer.getOrientedViewableTRBL(&marginTop, &marginRight, &marginBottom, &marginLeft);
marginTop += SETTINGS.screenMargin;
marginLeft += SETTINGS.screenMargin;
marginRight += SETTINGS.screenMargin;
const uint8_t statusBarHeight = UITheme::getInstance().getStatusBarHeight();
marginBottom += std::max(SETTINGS.screenMargin, statusBarHeight);
const uint16_t viewportWidth = renderer.getScreenWidth() - marginLeft - marginRight;
const uint16_t viewportHeight = renderer.getScreenHeight() - marginTop - marginBottom;
// Load the cached section file (won't rebuild if cache missing — too slow for sleep)
auto section = std::unique_ptr<Section>(new Section(epub, spineIndex, renderer));
if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle)) {
LOG_DBG("SLP", "EPUB: section cache not found for spine %d", spineIndex);
return false;
}
if (pageNumber < 0 || pageNumber >= section->pageCount) pageNumber = 0;
section->currentPage = pageNumber;
auto page = section->loadPageFromSectionFile();
if (!page) {
LOG_DBG("SLP", "EPUB: failed to load page %d", pageNumber);
return false;
}
renderer.clearScreen();
page->render(renderer, SETTINGS.getReaderFontId(), marginLeft, marginTop);
// No displayBuffer call — caller (SleepActivity) handles that after compositing the overlay
return true;
}
@@ -58,4 +58,9 @@ class EpubReaderActivity final : public Activity {
void loop() override;
void render(RenderLock&& lock) override;
bool isReaderActivity() const override { return true; }
// Renders the last saved page to the frame buffer without flushing to display.
// Used by SleepActivity to prepare the background for the overlay sleep mode.
// Returns false if the page cannot be loaded (missing cache / file error).
static bool drawCurrentPageToBuffer(const std::string& filePath, GfxRenderer& renderer);
};
+185
View File
@@ -600,3 +600,188 @@ void TxtReaderActivity::savePageIndexCache() const {
f.close();
LOG_DBG("TRS", "Saved page index cache: %d pages", totalPages);
}
bool TxtReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, GfxRenderer& renderer) {
Txt txt(filePath, "/.crosspoint");
if (!txt.load()) {
LOG_DBG("SLP", "TXT: failed to load %s", filePath.c_str());
return false;
}
// Compute layout values that match what initializeReader() produces
const int fontId = SETTINGS.getReaderFontId();
const uint8_t screenMargin = SETTINGS.screenMargin;
const uint8_t paragraphAlignment = SETTINGS.paragraphAlignment;
int marginTop, marginRight, marginBottom, marginLeft;
renderer.getOrientedViewableTRBL(&marginTop, &marginRight, &marginBottom, &marginLeft);
marginTop += screenMargin;
marginLeft += screenMargin;
marginRight += screenMargin;
marginBottom += std::max(screenMargin, static_cast<uint8_t>(UITheme::getInstance().getStatusBarHeight()));
const int vw = renderer.getScreenWidth() - marginLeft - marginRight;
const int vh = renderer.getScreenHeight() - marginTop - marginBottom;
const int lineHeight = renderer.getLineHeight(fontId);
const int linesPerPage = std::max(1, vh / lineHeight);
// Load the page offset index from cache (must already exist from normal reading)
std::string cachePath = txt.getCachePath() + "/index.bin";
FsFile cacheFile;
if (!Storage.openFileForRead("SLP", cachePath, cacheFile)) {
LOG_DBG("SLP", "TXT: no page index cache");
return false;
}
uint32_t magic;
serialization::readPod(cacheFile, magic);
if (magic != CACHE_MAGIC) {
cacheFile.close();
return false;
}
uint8_t version;
serialization::readPod(cacheFile, version);
if (version != CACHE_VERSION) {
cacheFile.close();
return false;
}
uint32_t cachedFileSize;
serialization::readPod(cacheFile, cachedFileSize);
if (cachedFileSize != txt.getFileSize()) {
cacheFile.close();
return false;
}
int32_t cachedVw, cachedLpp, cachedFontId, cachedMargin;
serialization::readPod(cacheFile, cachedVw);
serialization::readPod(cacheFile, cachedLpp);
serialization::readPod(cacheFile, cachedFontId);
serialization::readPod(cacheFile, cachedMargin);
if (cachedVw != vw || cachedLpp != linesPerPage || cachedFontId != fontId || cachedMargin != screenMargin) {
LOG_DBG("SLP", "TXT: cache invalid (settings changed)");
cacheFile.close();
return false;
}
uint8_t cachedAlignment;
serialization::readPod(cacheFile, cachedAlignment);
if (cachedAlignment != paragraphAlignment) {
cacheFile.close();
return false;
}
uint32_t numPages;
serialization::readPod(cacheFile, numPages);
if (numPages == 0) {
cacheFile.close();
return false;
}
std::vector<size_t> pageOffsets;
pageOffsets.reserve(numPages);
for (uint32_t i = 0; i < numPages; i++) {
uint32_t offset;
serialization::readPod(cacheFile, offset);
pageOffsets.push_back(offset);
}
cacheFile.close();
// Load saved page number from progress file
int savedPage = 0;
FsFile progFile;
if (Storage.openFileForRead("SLP", txt.getCachePath() + "/progress.bin", progFile)) {
uint8_t data[4];
if (progFile.read(data, 4) == 4) {
savedPage = data[0] + (data[1] << 8);
}
progFile.close();
}
if (savedPage < 0 || savedPage >= static_cast<int>(numPages)) savedPage = 0;
// Load the page lines from file
std::vector<std::string> pageLines;
const size_t fileSize = txt.getFileSize();
size_t offset = pageOffsets[savedPage];
if (offset >= fileSize) {
LOG_DBG("SLP", "TXT: page offset out of bounds");
return false;
}
// Replicate loadPageAtOffset() logic with local layout variables
size_t chunkSize = std::min(CHUNK_SIZE, fileSize - offset);
auto* buffer = static_cast<uint8_t*>(malloc(chunkSize + 1));
if (!buffer) return false;
if (!txt.readContent(buffer, offset, chunkSize)) {
free(buffer);
return false;
}
buffer[chunkSize] = '\0';
size_t pos = 0;
while (pos < chunkSize && static_cast<int>(pageLines.size()) < linesPerPage) {
size_t lineEnd = pos;
while (lineEnd < chunkSize && buffer[lineEnd] != '\n') lineEnd++;
bool lineComplete = (lineEnd < chunkSize) || (offset + lineEnd >= fileSize);
if (!lineComplete && !pageLines.empty()) break;
size_t lineContentLen = lineEnd - pos;
bool hasCR = (lineContentLen > 0 && buffer[pos + lineContentLen - 1] == '\r');
size_t displayLen = hasCR ? lineContentLen - 1 : lineContentLen;
std::string line(reinterpret_cast<char*>(buffer + pos), displayLen);
size_t lineBytePos = 0;
while (!line.empty() && static_cast<int>(pageLines.size()) < linesPerPage) {
if (renderer.getTextWidth(fontId, line.c_str()) <= vw) {
pageLines.push_back(line);
lineBytePos = displayLen;
line.clear();
break;
}
size_t breakPos = line.length();
while (breakPos > 0 && renderer.getTextWidth(fontId, line.substr(0, breakPos).c_str()) > vw) {
size_t spacePos = line.rfind(' ', breakPos - 1);
if (spacePos != std::string::npos && spacePos > 0) {
breakPos = spacePos;
} else {
breakPos--;
while (breakPos > 0 && (line[breakPos] & 0xC0) == 0x80) breakPos--;
}
}
if (breakPos == 0) breakPos = 1;
pageLines.push_back(line.substr(0, breakPos));
size_t skipChars = breakPos;
if (breakPos < line.length() && line[breakPos] == ' ') skipChars++;
lineBytePos += skipChars;
line = line.substr(skipChars);
}
pos = line.empty() ? lineEnd + 1 : pos + lineBytePos;
}
free(buffer);
if (pageLines.empty()) return false;
// Render lines to frame buffer (no displayBuffer call)
renderer.clearScreen();
int y = marginTop;
for (const auto& line : pageLines) {
if (!line.empty()) {
int x = marginLeft;
switch (paragraphAlignment) {
case CrossPointSettings::CENTER_ALIGN:
x = marginLeft + (vw - renderer.getTextWidth(fontId, line.c_str())) / 2;
break;
case CrossPointSettings::RIGHT_ALIGN:
x = marginLeft + vw - renderer.getTextWidth(fontId, line.c_str());
break;
default:
break;
}
renderer.drawText(fontId, x, y, line.c_str());
}
y += lineHeight;
}
return true;
}
@@ -49,4 +49,9 @@ class TxtReaderActivity final : public Activity {
void loop() override;
void render(RenderLock&&) override;
bool isReaderActivity() const override { return true; }
// Renders the last saved page to the frame buffer without flushing to display.
// Used by SleepActivity to prepare the background for the overlay sleep mode.
// Returns false if the page cannot be loaded (missing cache / file error).
static bool drawCurrentPageToBuffer(const std::string& filePath, GfxRenderer& renderer);
};
@@ -351,3 +351,78 @@ void XtcReaderActivity::loadProgress() {
f.close();
}
}
bool XtcReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, GfxRenderer& renderer) {
Xtc xtc(filePath, "/.crosspoint");
if (!xtc.load()) {
LOG_DBG("SLP", "XTC: failed to load %s", filePath.c_str());
return false;
}
// Load saved page number
uint32_t savedPage = 0;
FsFile f;
if (Storage.openFileForRead("SLP", xtc.getCachePath() + "/progress.bin", f)) {
uint8_t data[4];
if (f.read(data, 4) == 4) {
savedPage = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24);
}
f.close();
}
if (savedPage >= xtc.getPageCount()) savedPage = 0;
const uint16_t pageWidth = xtc.getPageWidth();
const uint16_t pageHeight = xtc.getPageHeight();
const uint8_t bitDepth = xtc.getBitDepth();
// Only use the 1-bit BW path; grayscale is not needed as a background under the overlay
const size_t pageBufferSize = (bitDepth == 2) ? ((static_cast<size_t>(pageWidth) * pageHeight + 7) / 8) * 2
: ((pageWidth + 7) / 8) * pageHeight;
uint8_t* pageBuffer = static_cast<uint8_t*>(malloc(pageBufferSize));
if (!pageBuffer) {
LOG_ERR("SLP", "XTC: failed to allocate page buffer");
return false;
}
if (xtc.loadPage(savedPage, pageBuffer, pageBufferSize) == 0) {
LOG_ERR("SLP", "XTC: failed to load page %lu", savedPage);
free(pageBuffer);
return false;
}
renderer.clearScreen();
if (bitDepth == 2) {
// 2-bit XTH: draw all non-white pixels as black (BW pass only)
const size_t planeSize = (static_cast<size_t>(pageWidth) * pageHeight + 7) / 8;
const uint8_t* plane1 = pageBuffer;
const uint8_t* plane2 = pageBuffer + planeSize;
const size_t colBytes = (pageHeight + 7) / 8;
for (uint16_t y = 0; y < pageHeight; y++) {
for (uint16_t x = 0; x < pageWidth; x++) {
const size_t colIndex = pageWidth - 1 - x;
const size_t byteInCol = y / 8;
const size_t bitInByte = 7 - (y % 8);
const size_t byteOffset = colIndex * colBytes + byteInCol;
const uint8_t bit1 = (plane1[byteOffset] >> bitInByte) & 1;
const uint8_t bit2 = (plane2[byteOffset] >> bitInByte) & 1;
if ((bit1 << 1) | bit2) {
renderer.drawPixel(x, y, true);
}
}
}
} else {
// 1-bit XTG: draw black pixels
const size_t srcRowBytes = (pageWidth + 7) / 8;
for (uint16_t srcY = 0; srcY < pageHeight; srcY++) {
for (uint16_t srcX = 0; srcX < pageWidth; srcX++) {
const bool isBlack = !((pageBuffer[srcY * srcRowBytes + srcX / 8] >> (7 - srcX % 8)) & 1);
if (isBlack) renderer.drawPixel(srcX, srcY, true);
}
}
}
free(pageBuffer);
return true;
}
@@ -29,4 +29,9 @@ class XtcReaderActivity final : public Activity {
void loop() override;
void render(RenderLock&&) override;
bool isReaderActivity() const override { return true; }
// Renders the last saved page to the frame buffer without flushing to display.
// Used by SleepActivity to prepare the background for the overlay sleep mode.
// Returns false if the page cannot be loaded (missing cache / file error).
static bool drawCurrentPageToBuffer(const std::string& filePath, GfxRenderer& renderer);
};