improved sleep screen performance and cover art
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user