mirror of
https://github.com/crosspoint-reader/crosspoint-reader.git
synced 2026-04-29 10:26:52 -07:00
## Summary - EPUB books with PNG cover images now display covers on the home screen instead of blank rectangles - Adds `PngToBmpConverter` library mirroring the existing `JpegToBmpConverter` pattern - Uses miniz (already in the project) for streaming zlib decompression of PNG IDAT data - Supports all PNG color types (Grayscale, RGB, RGBA, Palette, Gray+Alpha) - Optimized for ESP32-C3: batch grayscale conversion, 2KB read buffer, same area-averaging scaling and Atkinson dithering as the JPEG path ## Changes - **New:** `lib/PngToBmpConverter/PngToBmpConverter.h` — Public API matching JpegToBmpConverter's interface - **New:** `lib/PngToBmpConverter/PngToBmpConverter.cpp` — Streaming PNG decoder + BMP converter - **Modified:** `lib/Epub/Epub.cpp` — Added `.png` handling in `generateCoverBmp()` and `generateThumbBmp()` ## Test plan - [x] Tested with EPUB files using PNG covers — covers appear correctly on home screen - [ ] Verify with various PNG color types (most stock EPUBs use 8-bit RGB) - [ ] Confirm no regressions with JPEG cover EPUBs <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit **New Features** - Added PNG format support for EPUB cover and thumbnail images. PNG files are automatically processed and cached alongside existing supported formats. This enhancement enables users to leverage PNG cover artwork when generating EPUB files, improving workflow flexibility and compatibility with common image sources. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Nik Outchcunis <outchy@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Dave Allie <dave@daveallie.com>
175 lines
5.4 KiB
C++
175 lines
5.4 KiB
C++
#include "ImageBlock.h"
|
|
|
|
#include <GfxRenderer.h>
|
|
#include <Logging.h>
|
|
#include <SDCardManager.h>
|
|
#include <Serialization.h>
|
|
|
|
#include "../converters/DitherUtils.h"
|
|
#include "../converters/ImageDecoderFactory.h"
|
|
|
|
// Cache file format:
|
|
// - uint16_t width
|
|
// - uint16_t height
|
|
// - uint8_t pixels[...] - 2 bits per pixel, packed (4 pixels per byte), row-major order
|
|
|
|
ImageBlock::ImageBlock(const std::string& imagePath, int16_t width, int16_t height)
|
|
: imagePath(imagePath), width(width), height(height) {}
|
|
|
|
bool ImageBlock::imageExists() const { return Storage.exists(imagePath.c_str()); }
|
|
|
|
namespace {
|
|
|
|
std::string getCachePath(const std::string& imagePath) {
|
|
// Replace extension with .pxc (pixel cache)
|
|
size_t dotPos = imagePath.rfind('.');
|
|
if (dotPos != std::string::npos) {
|
|
return imagePath.substr(0, dotPos) + ".pxc";
|
|
}
|
|
return imagePath + ".pxc";
|
|
}
|
|
|
|
bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, int y, int expectedWidth,
|
|
int expectedHeight) {
|
|
FsFile cacheFile;
|
|
if (!Storage.openFileForRead("IMG", cachePath, cacheFile)) {
|
|
return false;
|
|
}
|
|
|
|
uint16_t cachedWidth, cachedHeight;
|
|
if (cacheFile.read(&cachedWidth, 2) != 2 || cacheFile.read(&cachedHeight, 2) != 2) {
|
|
cacheFile.close();
|
|
return false;
|
|
}
|
|
|
|
// Verify dimensions are close (allow 1 pixel tolerance for rounding differences)
|
|
int widthDiff = abs(cachedWidth - expectedWidth);
|
|
int heightDiff = abs(cachedHeight - expectedHeight);
|
|
if (widthDiff > 1 || heightDiff > 1) {
|
|
LOG_ERR("IMG", "Cache dimension mismatch: %dx%d vs %dx%d", cachedWidth, cachedHeight, expectedWidth,
|
|
expectedHeight);
|
|
cacheFile.close();
|
|
return false;
|
|
}
|
|
|
|
// Use cached dimensions for rendering (they're the actual decoded size)
|
|
expectedWidth = cachedWidth;
|
|
expectedHeight = cachedHeight;
|
|
|
|
LOG_DBG("IMG", "Loading from cache: %s (%dx%d)", cachePath.c_str(), cachedWidth, cachedHeight);
|
|
|
|
// Read and render row by row to minimize memory usage
|
|
const int bytesPerRow = (cachedWidth + 3) / 4; // 2 bits per pixel, 4 pixels per byte
|
|
uint8_t* rowBuffer = (uint8_t*)malloc(bytesPerRow);
|
|
if (!rowBuffer) {
|
|
LOG_ERR("IMG", "Failed to allocate row buffer");
|
|
cacheFile.close();
|
|
return false;
|
|
}
|
|
|
|
for (int row = 0; row < cachedHeight; row++) {
|
|
if (cacheFile.read(rowBuffer, bytesPerRow) != bytesPerRow) {
|
|
LOG_ERR("IMG", "Cache read error at row %d", row);
|
|
free(rowBuffer);
|
|
cacheFile.close();
|
|
return false;
|
|
}
|
|
|
|
int destY = y + row;
|
|
for (int col = 0; col < cachedWidth; col++) {
|
|
int byteIdx = col / 4;
|
|
int bitShift = 6 - (col % 4) * 2; // MSB first within byte
|
|
uint8_t pixelValue = (rowBuffer[byteIdx] >> bitShift) & 0x03;
|
|
|
|
drawPixelWithRenderMode(renderer, x + col, destY, pixelValue);
|
|
}
|
|
}
|
|
|
|
free(rowBuffer);
|
|
cacheFile.close();
|
|
LOG_DBG("IMG", "Cache render complete");
|
|
return true;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
|
LOG_DBG("IMG", "Rendering image at %d,%d: %s (%dx%d)", x, y, imagePath.c_str(), width, height);
|
|
|
|
const int screenWidth = renderer.getScreenWidth();
|
|
const int screenHeight = renderer.getScreenHeight();
|
|
|
|
// Bounds check render position using logical screen dimensions
|
|
if (x < 0 || y < 0 || x + width > screenWidth || y + height > screenHeight) {
|
|
LOG_ERR("IMG", "Invalid render position: (%d,%d) size (%dx%d) screen (%dx%d)", x, y, width, height, screenWidth,
|
|
screenHeight);
|
|
return;
|
|
}
|
|
|
|
// Try to render from cache first
|
|
std::string cachePath = getCachePath(imagePath);
|
|
if (renderFromCache(renderer, cachePath, x, y, width, height)) {
|
|
return; // Successfully rendered from cache
|
|
}
|
|
|
|
// No cache - need to decode the image
|
|
// Check if image file exists
|
|
FsFile file;
|
|
if (!Storage.openFileForRead("IMG", imagePath, file)) {
|
|
LOG_ERR("IMG", "Image file not found: %s", imagePath.c_str());
|
|
return;
|
|
}
|
|
size_t fileSize = file.size();
|
|
file.close();
|
|
|
|
if (fileSize == 0) {
|
|
LOG_ERR("IMG", "Image file is empty: %s", imagePath.c_str());
|
|
return;
|
|
}
|
|
|
|
LOG_DBG("IMG", "Decoding and caching: %s", imagePath.c_str());
|
|
|
|
RenderConfig config;
|
|
config.x = x;
|
|
config.y = y;
|
|
config.maxWidth = width;
|
|
config.maxHeight = height;
|
|
config.useGrayscale = true;
|
|
config.useDithering = true;
|
|
config.performanceMode = false;
|
|
config.useExactDimensions = true; // Use pre-calculated dimensions to avoid rounding mismatches
|
|
config.cachePath = cachePath; // Enable caching during decode
|
|
|
|
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(imagePath);
|
|
if (!decoder) {
|
|
LOG_ERR("IMG", "No decoder found for image: %s", imagePath.c_str());
|
|
return;
|
|
}
|
|
|
|
LOG_DBG("IMG", "Using %s decoder", decoder->getFormatName());
|
|
|
|
bool success = decoder->decodeToFramebuffer(imagePath, renderer, config);
|
|
if (!success) {
|
|
LOG_ERR("IMG", "Failed to decode image: %s", imagePath.c_str());
|
|
return;
|
|
}
|
|
|
|
LOG_DBG("IMG", "Decode successful");
|
|
}
|
|
|
|
bool ImageBlock::serialize(FsFile& file) {
|
|
serialization::writeString(file, imagePath);
|
|
serialization::writePod(file, width);
|
|
serialization::writePod(file, height);
|
|
return true;
|
|
}
|
|
|
|
std::unique_ptr<ImageBlock> ImageBlock::deserialize(FsFile& file) {
|
|
std::string path;
|
|
serialization::readString(file, path);
|
|
int16_t w, h;
|
|
serialization::readPod(file, w);
|
|
serialization::readPod(file, h);
|
|
return std::unique_ptr<ImageBlock>(new ImageBlock(path, w, h));
|
|
}
|