improved sleep screen performance and cover art

This commit is contained in:
cottongin 2026-01-24 20:48:13 -05:00
parent ecff988a29
commit 6bedc4ffec
No known key found for this signature in database
GPG Key ID: 0ECC91FE4655C262
7 changed files with 376 additions and 89 deletions

View File

@ -82,6 +82,10 @@ const char* Bitmap::errorToString(BmpReaderError err) {
BmpReaderError Bitmap::parseHeaders() {
if (!file) return BmpReaderError::FileInvalid;
// Store file size for cache validation
fileSize = file.size();
if (!file.seek(0)) return BmpReaderError::SeekStartFailed;
// --- BMP FILE HEADER ---
@ -263,17 +267,24 @@ BmpReaderError Bitmap::rewindToData() const {
return BmpReaderError::Ok;
}
bool Bitmap::detectPerimeterIsBlack() const {
// Detect if the 1-pixel perimeter of the image is mostly black or white.
// Returns true if mostly black (luminance < 128), false if mostly white.
EdgeLuminance Bitmap::detectEdgeLuminance(int depth) const {
// Detect average luminance for each edge of the image.
// Samples 'depth' pixels from each edge for more stable averages.
// Returns per-edge luminance values (0-255).
if (width <= 0 || height <= 0) return false;
EdgeLuminance result = {128, 128, 128, 128}; // Default to neutral gray
if (width <= 0 || height <= 0) return result;
if (depth < 1) depth = 1;
if (depth > width / 2) depth = width / 2;
if (depth > height / 2) depth = height / 2;
auto* rowBuffer = static_cast<uint8_t*>(malloc(rowBytes));
if (!rowBuffer) return false;
if (!rowBuffer) return result;
int blackCount = 0;
int whiteCount = 0;
// Accumulators for each edge
uint32_t topSum = 0, bottomSum = 0, leftSum = 0, rightSum = 0;
int topCount = 0, bottomCount = 0, leftCount = 0, rightCount = 0;
// Helper lambda to get luminance from a pixel at position x in rowBuffer
auto getLuminance = [&](int x) -> uint8_t {
@ -299,16 +310,6 @@ bool Bitmap::detectPerimeterIsBlack() const {
}
};
// Helper to classify and count a pixel
auto countPixel = [&](int x) {
const uint8_t lum = getLuminance(x);
if (lum < 128) {
blackCount++;
} else {
whiteCount++;
}
};
// Helper to seek to a specific image row (accounting for top-down vs bottom-up)
auto seekToRow = [&](int imageRow) -> bool {
// In bottom-up BMP (topDown=false), row 0 in file is the bottom row of image
@ -317,35 +318,56 @@ bool Bitmap::detectPerimeterIsBlack() const {
return file.seek(bfOffBits + static_cast<uint32_t>(fileRow) * rowBytes);
};
// Sample top row (image row 0) - all pixels
if (seekToRow(0) && file.read(rowBuffer, rowBytes) == rowBytes) {
for (int x = 0; x < width; x++) {
countPixel(x);
}
}
// Sample bottom row (image row height-1) - all pixels
if (height > 1) {
if (seekToRow(height - 1) && file.read(rowBuffer, rowBytes) == rowBytes) {
// Sample top rows (image rows 0 to depth-1) - all pixels
for (int row = 0; row < depth && row < height; row++) {
if (seekToRow(row) && file.read(rowBuffer, rowBytes) == rowBytes) {
for (int x = 0; x < width; x++) {
countPixel(x);
topSum += getLuminance(x);
topCount++;
}
}
}
// Sample left and right edges from intermediate rows
for (int y = 1; y < height - 1; y++) {
// Sample bottom rows (image rows height-depth to height-1) - all pixels
for (int row = height - depth; row < height; row++) {
if (row >= depth && row >= 0) { // Avoid overlap with top rows
if (seekToRow(row) && file.read(rowBuffer, rowBytes) == rowBytes) {
for (int x = 0; x < width; x++) {
bottomSum += getLuminance(x);
bottomCount++;
}
}
}
}
// Sample left and right edges from all rows
for (int y = 0; y < height; y++) {
if (seekToRow(y) && file.read(rowBuffer, rowBytes) == rowBytes) {
countPixel(0); // Left edge
countPixel(width - 1); // Right edge
// Left edge (first 'depth' pixels)
for (int x = 0; x < depth && x < width; x++) {
leftSum += getLuminance(x);
leftCount++;
}
// Right edge (last 'depth' pixels)
for (int x = width - depth; x < width; x++) {
if (x >= depth) { // Avoid overlap with left edge
rightSum += getLuminance(x);
rightCount++;
}
}
}
}
free(rowBuffer);
// Calculate averages
if (topCount > 0) result.top = static_cast<uint8_t>(topSum / topCount);
if (bottomCount > 0) result.bottom = static_cast<uint8_t>(bottomSum / bottomCount);
if (leftCount > 0) result.left = static_cast<uint8_t>(leftSum / leftCount);
if (rightCount > 0) result.right = static_cast<uint8_t>(rightSum / rightCount);
// Rewind file position for subsequent drawing
rewindToData();
// Return true if perimeter is mostly black
return blackCount > whiteCount;
return result;
}

View File

@ -6,6 +6,14 @@
#include "BitmapHelpers.h"
// Per-edge average luminance values (0-255)
struct EdgeLuminance {
uint8_t top;
uint8_t bottom;
uint8_t left;
uint8_t right;
};
enum class BmpReaderError : uint8_t {
Ok = 0,
FileInvalid,
@ -37,7 +45,7 @@ class Bitmap {
BmpReaderError parseHeaders();
BmpReaderError readNextRow(uint8_t* data, uint8_t* rowBuffer) const;
BmpReaderError rewindToData() const;
bool detectPerimeterIsBlack() const;
EdgeLuminance detectEdgeLuminance(int depth = 2) const;
int getWidth() const { return width; }
int getHeight() const { return height; }
bool isTopDown() const { return topDown; }
@ -45,6 +53,7 @@ class Bitmap {
int getRowBytes() const { return rowBytes; }
bool is1Bit() const { return bpp == 1; }
uint16_t getBpp() const { return bpp; }
uint32_t getFileSize() const { return fileSize; }
private:
static uint16_t readLE16(FsFile& f);
@ -58,6 +67,7 @@ class Bitmap {
uint32_t bfOffBits = 0;
uint16_t bpp = 0;
int rowBytes = 0;
uint32_t fileSize = 0;
uint8_t paletteLum[256] = {};
// Floyd-Steinberg dithering state (mutable for const methods)

View File

@ -144,6 +144,44 @@ void GfxRenderer::fillRect(const int x, const int y, const int width, const int
}
}
void GfxRenderer::fillRectGray(const int x, const int y, const int width, const int height,
const uint8_t grayLevel) const {
// Fill rectangle with 4-level grayscale value.
// The grayscale encoding for 4 levels uses 3 passes:
// - Level 0 (black): BW=black, MSB=skip, LSB=skip
// - Level 1 (dark gray): BW=black, MSB=white, LSB=white
// - Level 2 (light gray): BW=black, MSB=white, LSB=skip
// - Level 3 (white): BW=skip, MSB=skip, LSB=skip
if (width <= 0 || height <= 0) return;
switch (renderMode) {
case BW:
// In BW mode, fill with black for levels 0-2, skip for level 3
if (grayLevel < 3) {
fillRect(x, y, width, height, true); // true = black
}
// Level 3 = white, which is the default clear color, so skip
break;
case GRAYSCALE_MSB:
// In MSB mode (buffer cleared to black), fill with white for levels 1-2
if (grayLevel == 1 || grayLevel == 2) {
fillRect(x, y, width, height, false); // false = white
}
// Levels 0 and 3 stay black (which is correct for 0, will combine correctly for 3)
break;
case GRAYSCALE_LSB:
// In LSB mode (buffer cleared to black), fill with white for level 1 only
if (grayLevel == 1) {
fillRect(x, y, width, height, false); // false = white
}
// Levels 0, 2, 3 stay black
break;
}
}
void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, const int width, const int height) const {
// TODO: Rotate bits
int rotatedX = 0;

View File

@ -65,6 +65,9 @@ class GfxRenderer {
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;
// Fill rectangle with 4-level grayscale (0=black, 1=dark gray, 2=light gray, 3=white)
// Handles current render mode (BW, GRAYSCALE_MSB, GRAYSCALE_LSB)
void fillRectGray(int x, int y, int width, int height, uint8_t grayLevel) 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, float cropX = 0,
float cropY = 0, bool invert = false) const;

View File

@ -50,6 +50,13 @@ class BookManager {
*/
static std::string getArchivedBookOriginalPath(const std::string& archivedFilename);
/**
* Get the full cache directory path for a book
* @param bookPath Full path to the book file
* @return Cache directory path (e.g., "/.crosspoint/epub_123456")
*/
static std::string getCacheDir(const std::string& bookPath);
private:
// Extract filename from a full path
static std::string getFilename(const std::string& path);
@ -63,9 +70,6 @@ class BookManager {
// Get cache directory prefix for a file type (epub_, txt_, xtc_)
static std::string getCachePrefix(const std::string& path);
// Get the full cache directory path for a book
static std::string getCacheDir(const std::string& bookPath);
// Write the .meta file for an archived book
static bool writeMetaFile(const std::string& archivedPath, const std::string& originalPath);

View File

@ -6,6 +6,9 @@
#include <Txt.h>
#include <Xtc.h>
#include <cmath>
#include "BookManager.h"
#include "CrossPointSettings.h"
#include "CrossPointState.h"
#include "fontIds.h"
@ -13,10 +16,16 @@
#include "util/StringUtils.h"
namespace {
// Perimeter cache file format:
// Edge luminance cache file format (per-BMP):
// - 4 bytes: uint32_t file size (for cache invalidation)
// - 1 byte: result (0 = white perimeter, 1 = black perimeter)
constexpr size_t PERIM_CACHE_SIZE = 5;
// - 4 bytes: EdgeLuminance (top, bottom, left, right)
constexpr size_t EDGE_CACHE_SIZE = 8;
// Book-level edge cache file format (stored in book's cache directory):
// - 4 bytes: uint32_t cover BMP file size (for cache invalidation)
// - 4 bytes: EdgeLuminance (top, bottom, left, right)
// - 1 byte: cover mode (0=FIT, 1=CROP) - for EPUB mode invalidation
constexpr size_t BOOK_EDGE_CACHE_SIZE = 9;
} // namespace
void SleepActivity::onEnter() {
@ -202,8 +211,9 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::str
// Image is taller than screen ratio - fit to height
scale = static_cast<float>(pageHeight) / static_cast<float>(bitmap.getHeight());
}
fillWidth = static_cast<int>(bitmap.getWidth() * scale);
fillHeight = static_cast<int>(bitmap.getHeight() * scale);
// Use ceil to ensure fill area covers all drawn pixels
fillWidth = static_cast<int>(std::ceil(bitmap.getWidth() * scale));
fillHeight = static_cast<int>(std::ceil(bitmap.getHeight() * scale));
// Center the scaled image
x = (pageWidth - fillWidth) / 2;
@ -212,31 +222,72 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::str
fillWidth, fillHeight, x, y);
}
// Detect perimeter color and clear to matching background
const bool isBlackPerimeter = getPerimeterIsBlack(bitmap, bmpPath);
const uint8_t clearColor = isBlackPerimeter ? 0x00 : 0xFF;
// Get edge luminance values (from cache or calculate)
const EdgeLuminance edges = getEdgeLuminance(bitmap, bmpPath);
const uint8_t topGray = quantizeGray(edges.top);
const uint8_t bottomGray = quantizeGray(edges.bottom);
const uint8_t leftGray = quantizeGray(edges.left);
const uint8_t rightGray = quantizeGray(edges.right);
Serial.printf("[%lu] [SLP] drawing to %d x %d\n", millis(), x, y);
renderer.clearScreen(clearColor);
Serial.printf("[%lu] [SLP] Edge luminance: T=%d B=%d L=%d R=%d -> gray levels T=%d B=%d L=%d R=%d\n",
millis(), edges.top, edges.bottom, edges.left, edges.right,
topGray, bottomGray, leftGray, rightGray);
// If background is black, fill the image area with white first so white pixels render correctly
if (isBlackPerimeter) {
renderer.fillRect(x, y, fillWidth, fillHeight, false); // false = white
// Clear screen to white first (default background)
renderer.clearScreen(0xFF);
// Fill letterbox regions with edge colors (BW pass)
// Top letterbox
if (y > 0) {
renderer.fillRectGray(0, 0, pageWidth, y, topGray);
}
// Bottom letterbox
if (y + fillHeight < pageHeight) {
renderer.fillRectGray(0, y + fillHeight, pageWidth, pageHeight - y - fillHeight, bottomGray);
}
// Left letterbox
if (x > 0) {
renderer.fillRectGray(0, y, x, fillHeight, leftGray);
}
// Right letterbox
if (x + fillWidth < pageWidth) {
renderer.fillRectGray(x + fillWidth, y, pageWidth - x - fillWidth, fillHeight, rightGray);
}
Serial.printf("[%lu] [SLP] drawing bitmap at %d, %d\n", millis(), x, y);
renderer.drawBitmap(bitmap, x, y, drawWidth, drawHeight, cropX, cropY);
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
if (bitmap.hasGreyscale()) {
// Grayscale LSB pass
bitmap.rewindToData();
renderer.clearScreen(0x00);
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
// Fill letterbox regions for LSB pass
if (y > 0) renderer.fillRectGray(0, 0, pageWidth, y, topGray);
if (y + fillHeight < pageHeight)
renderer.fillRectGray(0, y + fillHeight, pageWidth, pageHeight - y - fillHeight, bottomGray);
if (x > 0) renderer.fillRectGray(0, y, x, fillHeight, leftGray);
if (x + fillWidth < pageWidth)
renderer.fillRectGray(x + fillWidth, y, pageWidth - x - fillWidth, fillHeight, rightGray);
renderer.drawBitmap(bitmap, x, y, drawWidth, drawHeight, cropX, cropY);
renderer.copyGrayscaleLsbBuffers();
// Grayscale MSB pass
bitmap.rewindToData();
renderer.clearScreen(0x00);
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
// Fill letterbox regions for MSB pass
if (y > 0) renderer.fillRectGray(0, 0, pageWidth, y, topGray);
if (y + fillHeight < pageHeight)
renderer.fillRectGray(0, y + fillHeight, pageWidth, pageHeight - y - fillHeight, bottomGray);
if (x > 0) renderer.fillRectGray(0, y, x, fillHeight, leftGray);
if (x + fillWidth < pageWidth)
renderer.fillRectGray(x + fillWidth, y, pageWidth - x - fillWidth, fillHeight, rightGray);
renderer.drawBitmap(bitmap, x, y, drawWidth, drawHeight, cropX, cropY);
renderer.copyGrayscaleMsbBuffers();
@ -250,8 +301,17 @@ void SleepActivity::renderCoverSleepScreen() const {
return renderDefaultSleepScreen();
}
const bool cropped = SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP;
// Try to use cached edge data to skip book metadata loading
if (tryRenderCachedCoverSleep(APP_STATE.openEpubPath, cropped)) {
return;
}
// Cache miss - need to load book metadata and generate cover
Serial.printf("[%lu] [SLP] Cache miss, loading book metadata\n", millis());
std::string coverBmpPath;
bool cropped = SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP;
// Check if the current book is XTC, TXT, or EPUB
if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".xtc") ||
@ -305,7 +365,34 @@ void SleepActivity::renderCoverSleepScreen() const {
if (SdMan.openFileForRead("SLP", coverBmpPath, file)) {
Bitmap bitmap(file);
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
// Render the bitmap - this will calculate and cache edge luminance per-BMP
renderBitmapSleepScreen(bitmap, coverBmpPath);
// Also save to book-level edge cache for faster subsequent sleeps
const std::string edgeCachePath = getBookEdgeCachePath(APP_STATE.openEpubPath);
if (!edgeCachePath.empty()) {
// Get the edge luminance that was just calculated (it's now cached per-BMP)
const EdgeLuminance edges = getEdgeLuminance(bitmap, coverBmpPath);
const uint32_t bmpFileSize = bitmap.getFileSize();
const uint8_t coverMode = cropped ? 1 : 0;
FsFile cacheFile;
if (SdMan.openFileForWrite("SLP", edgeCachePath, cacheFile)) {
uint8_t cacheData[BOOK_EDGE_CACHE_SIZE];
cacheData[0] = bmpFileSize & 0xFF;
cacheData[1] = (bmpFileSize >> 8) & 0xFF;
cacheData[2] = (bmpFileSize >> 16) & 0xFF;
cacheData[3] = (bmpFileSize >> 24) & 0xFF;
cacheData[4] = edges.top;
cacheData[5] = edges.bottom;
cacheData[6] = edges.left;
cacheData[7] = edges.right;
cacheData[8] = coverMode;
cacheFile.write(cacheData, BOOK_EDGE_CACHE_SIZE);
cacheFile.close();
Serial.printf("[%lu] [SLP] Saved book-level edge cache to %s\n", millis(), edgeCachePath.c_str());
}
}
return;
}
}
@ -318,7 +405,7 @@ void SleepActivity::renderBlankSleepScreen() const {
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
}
std::string SleepActivity::getPerimeterCachePath(const std::string& bmpPath) {
std::string SleepActivity::getEdgeCachePath(const std::string& bmpPath) {
// Convert "/dir/file.bmp" to "/dir/.file.bmp.perim"
const size_t lastSlash = bmpPath.find_last_of('/');
if (lastSlash == std::string::npos) {
@ -330,67 +417,182 @@ std::string SleepActivity::getPerimeterCachePath(const std::string& bmpPath) {
return dir + "." + filename + ".perim";
}
bool SleepActivity::getPerimeterIsBlack(const Bitmap& bitmap, const std::string& bmpPath) const {
const std::string cachePath = getPerimeterCachePath(bmpPath);
uint8_t SleepActivity::quantizeGray(uint8_t lum) {
// Quantize luminance (0-255) to 4-level grayscale (0-3)
// Thresholds tuned for X4 display gray levels
if (lum < 43) return 0; // black
if (lum < 128) return 1; // dark gray
if (lum < 213) return 2; // light gray
return 3; // white
}
EdgeLuminance SleepActivity::getEdgeLuminance(const Bitmap& bitmap, const std::string& bmpPath) const {
const std::string cachePath = getEdgeCachePath(bmpPath);
EdgeLuminance result = {128, 128, 128, 128}; // Default to neutral gray
// Try to read from cache
FsFile cacheFile;
if (SdMan.openFileForRead("SLP", cachePath, cacheFile)) {
uint8_t cacheData[PERIM_CACHE_SIZE];
if (cacheFile.read(cacheData, PERIM_CACHE_SIZE) == PERIM_CACHE_SIZE) {
uint8_t cacheData[EDGE_CACHE_SIZE];
if (cacheFile.read(cacheData, EDGE_CACHE_SIZE) == EDGE_CACHE_SIZE) {
// Extract cached file size
const uint32_t cachedSize = static_cast<uint32_t>(cacheData[0]) |
(static_cast<uint32_t>(cacheData[1]) << 8) |
(static_cast<uint32_t>(cacheData[2]) << 16) |
(static_cast<uint32_t>(cacheData[3]) << 24);
// Get current BMP file size
FsFile bmpFile;
uint32_t currentSize = 0;
if (SdMan.openFileForRead("SLP", bmpPath, bmpFile)) {
currentSize = bmpFile.size();
bmpFile.close();
}
// Get current BMP file size from already-opened bitmap
const uint32_t currentSize = bitmap.getFileSize();
// Validate cache
if (cachedSize == currentSize && currentSize > 0) {
const bool result = cacheData[4] != 0;
Serial.printf("[%lu] [SLP] Perimeter cache hit for %s: %s\n", millis(), bmpPath.c_str(),
result ? "black" : "white");
result.top = cacheData[4];
result.bottom = cacheData[5];
result.left = cacheData[6];
result.right = cacheData[7];
Serial.printf("[%lu] [SLP] Edge cache hit for %s: T=%d B=%d L=%d R=%d\n", millis(), bmpPath.c_str(),
result.top, result.bottom, result.left, result.right);
cacheFile.close();
return result;
}
Serial.printf("[%lu] [SLP] Perimeter cache invalid (size mismatch: %lu vs %lu)\n", millis(),
Serial.printf("[%lu] [SLP] Edge cache invalid (size mismatch: %lu vs %lu)\n", millis(),
static_cast<unsigned long>(cachedSize), static_cast<unsigned long>(currentSize));
}
cacheFile.close();
}
// Cache miss - calculate perimeter
Serial.printf("[%lu] [SLP] Calculating perimeter for %s\n", millis(), bmpPath.c_str());
const bool isBlack = bitmap.detectPerimeterIsBlack();
Serial.printf("[%lu] [SLP] Perimeter detected: %s\n", millis(), isBlack ? "black" : "white");
// Cache miss - calculate edge luminance
Serial.printf("[%lu] [SLP] Calculating edge luminance for %s\n", millis(), bmpPath.c_str());
result = bitmap.detectEdgeLuminance(2); // Sample 2 pixels deep for stability
Serial.printf("[%lu] [SLP] Edge luminance detected: T=%d B=%d L=%d R=%d\n", millis(),
result.top, result.bottom, result.left, result.right);
// Get BMP file size for cache
FsFile bmpFile;
uint32_t fileSize = 0;
if (SdMan.openFileForRead("SLP", bmpPath, bmpFile)) {
fileSize = bmpFile.size();
bmpFile.close();
}
// Get BMP file size from already-opened bitmap for cache
const uint32_t fileSize = bitmap.getFileSize();
// Save to cache
if (fileSize > 0 && SdMan.openFileForWrite("SLP", cachePath, cacheFile)) {
uint8_t cacheData[PERIM_CACHE_SIZE];
uint8_t cacheData[EDGE_CACHE_SIZE];
cacheData[0] = fileSize & 0xFF;
cacheData[1] = (fileSize >> 8) & 0xFF;
cacheData[2] = (fileSize >> 16) & 0xFF;
cacheData[3] = (fileSize >> 24) & 0xFF;
cacheData[4] = isBlack ? 1 : 0;
cacheFile.write(cacheData, PERIM_CACHE_SIZE);
cacheData[4] = result.top;
cacheData[5] = result.bottom;
cacheData[6] = result.left;
cacheData[7] = result.right;
cacheFile.write(cacheData, EDGE_CACHE_SIZE);
cacheFile.close();
Serial.printf("[%lu] [SLP] Saved perimeter cache to %s\n", millis(), cachePath.c_str());
Serial.printf("[%lu] [SLP] Saved edge cache to %s\n", millis(), cachePath.c_str());
}
return isBlack;
return result;
}
std::string SleepActivity::getBookEdgeCachePath(const std::string& bookPath) {
const std::string cacheDir = BookManager::getCacheDir(bookPath);
if (cacheDir.empty()) {
return "";
}
return cacheDir + "/edge.bin";
}
std::string SleepActivity::getCoverBmpPath(const std::string& cacheDir, const std::string& bookPath, bool cropped) {
if (cacheDir.empty()) {
return "";
}
// EPUB uses different paths for fit vs crop modes
if (StringUtils::checkFileExtension(bookPath, ".epub")) {
return cropped ? (cacheDir + "/cover_crop.bmp") : (cacheDir + "/cover_fit.bmp");
}
// XTC and TXT use a single cover.bmp
return cacheDir + "/cover.bmp";
}
bool SleepActivity::tryRenderCachedCoverSleep(const std::string& bookPath, bool cropped) const {
// Try to render cover sleep screen using cached edge data without loading book metadata
const std::string edgeCachePath = getBookEdgeCachePath(bookPath);
if (edgeCachePath.empty()) {
Serial.println("[SLP] Cannot get edge cache path");
return false;
}
// Check if edge cache exists
FsFile cacheFile;
if (!SdMan.openFileForRead("SLP", edgeCachePath, cacheFile)) {
Serial.println("[SLP] No edge cache file found");
return false;
}
// Read cache data
uint8_t cacheData[BOOK_EDGE_CACHE_SIZE];
if (cacheFile.read(cacheData, BOOK_EDGE_CACHE_SIZE) != BOOK_EDGE_CACHE_SIZE) {
Serial.println("[SLP] Edge cache file too small");
cacheFile.close();
return false;
}
cacheFile.close();
// Extract cached values
const uint32_t cachedBmpSize = static_cast<uint32_t>(cacheData[0]) |
(static_cast<uint32_t>(cacheData[1]) << 8) |
(static_cast<uint32_t>(cacheData[2]) << 16) |
(static_cast<uint32_t>(cacheData[3]) << 24);
EdgeLuminance cachedEdges;
cachedEdges.top = cacheData[4];
cachedEdges.bottom = cacheData[5];
cachedEdges.left = cacheData[6];
cachedEdges.right = cacheData[7];
const uint8_t cachedCoverMode = cacheData[8];
// Check if cover mode matches (for EPUB)
const uint8_t currentCoverMode = cropped ? 1 : 0;
if (StringUtils::checkFileExtension(bookPath, ".epub") && cachedCoverMode != currentCoverMode) {
Serial.printf("[SLP] Cover mode changed (cached=%d, current=%d), invalidating cache\n",
cachedCoverMode, currentCoverMode);
return false;
}
// Construct cover BMP path
const std::string cacheDir = BookManager::getCacheDir(bookPath);
const std::string coverBmpPath = getCoverBmpPath(cacheDir, bookPath, cropped);
if (coverBmpPath.empty()) {
Serial.println("[SLP] Cannot construct cover BMP path");
return false;
}
// Try to open the cover BMP
FsFile bmpFile;
if (!SdMan.openFileForRead("SLP", coverBmpPath, bmpFile)) {
Serial.printf("[SLP] Cover BMP not found: %s\n", coverBmpPath.c_str());
return false;
}
// Check if BMP file size matches cache
const uint32_t currentBmpSize = bmpFile.size();
if (currentBmpSize != cachedBmpSize || currentBmpSize == 0) {
Serial.printf("[SLP] BMP size mismatch (cached=%lu, current=%lu)\n",
static_cast<unsigned long>(cachedBmpSize), static_cast<unsigned long>(currentBmpSize));
bmpFile.close();
return false;
}
// Parse bitmap headers
Bitmap bitmap(bmpFile);
if (bitmap.parseHeaders() != BmpReaderError::Ok) {
Serial.println("[SLP] Failed to parse cached cover BMP");
bmpFile.close();
return false;
}
Serial.printf("[%lu] [SLP] Using cached cover sleep: %s (T=%d B=%d L=%d R=%d)\n", millis(),
coverBmpPath.c_str(), cachedEdges.top, cachedEdges.bottom, cachedEdges.left, cachedEdges.right);
// Render the bitmap with cached edge values
// We call renderBitmapSleepScreen which will use getEdgeLuminance internally,
// but since the per-BMP cache should also exist (same values), it will be a cache hit
renderBitmapSleepScreen(bitmap, coverBmpPath);
return true;
}

View File

@ -1,9 +1,9 @@
#pragma once
#include "../Activity.h"
#include <string>
#include <Bitmap.h>
class Bitmap;
#include <string>
class SleepActivity final : public Activity {
public:
@ -19,7 +19,15 @@ class SleepActivity final : public Activity {
void renderBitmapSleepScreen(const Bitmap& bitmap, const std::string& bmpPath) const;
void renderBlankSleepScreen() const;
// Perimeter detection caching helpers
static std::string getPerimeterCachePath(const std::string& bmpPath);
bool getPerimeterIsBlack(const Bitmap& bitmap, const std::string& bmpPath) const;
// Edge luminance caching helpers
static std::string getEdgeCachePath(const std::string& bmpPath);
EdgeLuminance getEdgeLuminance(const Bitmap& bitmap, const std::string& bmpPath) const;
// Book-level edge cache helpers (for skipping book metadata loading)
static std::string getBookEdgeCachePath(const std::string& bookPath);
static std::string getCoverBmpPath(const std::string& cacheDir, const std::string& bookPath, bool cropped);
bool tryRenderCachedCoverSleep(const std::string& bookPath, bool cropped) const;
// Quantize luminance (0-255) to 4-level grayscale (0-3)
static uint8_t quantizeGray(uint8_t lum);
};