feat: Add PNG cover image support for EPUB books (#827)

## 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>
This commit is contained in:
casualducko
2026-02-16 06:56:13 -05:00
committed by GitHub
parent 00666377de
commit 0bc6747483
9 changed files with 1000 additions and 69 deletions

View File

@@ -4,6 +4,7 @@
#include <HalStorage.h>
#include <JpegToBmpConverter.h>
#include <Logging.h>
#include <PngToBmpConverter.h>
#include <ZipFile.h>
#include "Epub/parsers/ContainerParser.h"
@@ -486,12 +487,44 @@ bool Epub::generateCoverBmp(bool cropped) const {
LOG_ERR("EBP", "Failed to generate BMP from cover image");
Storage.remove(getCoverBmpPath(cropped).c_str());
}
LOG_DBG("EBP", "Generated BMP from cover image, success: %s", success ? "yes" : "no");
LOG_DBG("EBP", "Generated BMP from JPG cover image, success: %s", success ? "yes" : "no");
return success;
} else {
LOG_ERR("EBP", "Cover image is not a supported format, skipping");
}
if (coverImageHref.substr(coverImageHref.length() - 4) == ".png") {
LOG_DBG("EBP", "Generating BMP from PNG cover image (%s mode)", cropped ? "cropped" : "fit");
const auto coverPngTempPath = getCachePath() + "/.cover.png";
FsFile coverPng;
if (!Storage.openFileForWrite("EBP", coverPngTempPath, coverPng)) {
return false;
}
readItemContentsToStream(coverImageHref, coverPng, 1024);
coverPng.close();
if (!Storage.openFileForRead("EBP", coverPngTempPath, coverPng)) {
return false;
}
FsFile coverBmp;
if (!Storage.openFileForWrite("EBP", getCoverBmpPath(cropped), coverBmp)) {
coverPng.close();
return false;
}
const bool success = PngToBmpConverter::pngFileToBmpStream(coverPng, coverBmp, cropped);
coverPng.close();
coverBmp.close();
Storage.remove(coverPngTempPath.c_str());
if (!success) {
LOG_ERR("EBP", "Failed to generate BMP from PNG cover image");
Storage.remove(getCoverBmpPath(cropped).c_str());
}
LOG_DBG("EBP", "Generated BMP from PNG cover image, success: %s", success ? "yes" : "no");
return success;
}
LOG_ERR("EBP", "Cover image is not a supported format, skipping");
return false;
}
@@ -549,6 +582,40 @@ bool Epub::generateThumbBmp(int height) const {
}
LOG_DBG("EBP", "Generated thumb BMP from JPG cover image, success: %s", success ? "yes" : "no");
return success;
} else if (coverImageHref.substr(coverImageHref.length() - 4) == ".png") {
LOG_DBG("EBP", "Generating thumb BMP from PNG cover image");
const auto coverPngTempPath = getCachePath() + "/.cover.png";
FsFile coverPng;
if (!Storage.openFileForWrite("EBP", coverPngTempPath, coverPng)) {
return false;
}
readItemContentsToStream(coverImageHref, coverPng, 1024);
coverPng.close();
if (!Storage.openFileForRead("EBP", coverPngTempPath, coverPng)) {
return false;
}
FsFile thumbBmp;
if (!Storage.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp)) {
coverPng.close();
return false;
}
int THUMB_TARGET_WIDTH = height * 0.6;
int THUMB_TARGET_HEIGHT = height;
const bool success =
PngToBmpConverter::pngFileTo1BitBmpStreamWithSize(coverPng, thumbBmp, THUMB_TARGET_WIDTH, THUMB_TARGET_HEIGHT);
coverPng.close();
thumbBmp.close();
Storage.remove(coverPngTempPath.c_str());
if (!success) {
LOG_ERR("EBP", "Failed to generate thumb BMP from PNG cover image");
Storage.remove(getThumbBmpPath(height).c_str());
}
LOG_DBG("EBP", "Generated thumb BMP from PNG cover image, success: %s", success ? "yes" : "no");
return success;
} else {
LOG_ERR("EBP", "Cover image is not a supported format, skipping thumbnail");
}

View File

@@ -1,8 +1,7 @@
#include "ImageBlock.h"
#include <FsHelpers.h>
#include <GfxRenderer.h>
#include <HardwareSerial.h>
#include <Logging.h>
#include <SDCardManager.h>
#include <Serialization.h>
@@ -47,8 +46,8 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x,
int widthDiff = abs(cachedWidth - expectedWidth);
int heightDiff = abs(cachedHeight - expectedHeight);
if (widthDiff > 1 || heightDiff > 1) {
Serial.printf("[%lu] [IMG] Cache dimension mismatch: %dx%d vs %dx%d\n", millis(), cachedWidth, cachedHeight,
expectedWidth, expectedHeight);
LOG_ERR("IMG", "Cache dimension mismatch: %dx%d vs %dx%d", cachedWidth, cachedHeight, expectedWidth,
expectedHeight);
cacheFile.close();
return false;
}
@@ -57,20 +56,20 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x,
expectedWidth = cachedWidth;
expectedHeight = cachedHeight;
Serial.printf("[%lu] [IMG] Loading from cache: %s (%dx%d)\n", millis(), cachePath.c_str(), cachedWidth, 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) {
Serial.printf("[%lu] [IMG] Failed to allocate row buffer\n", millis());
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) {
Serial.printf("[%lu] [IMG] Cache read error at row %d\n", millis(), row);
LOG_ERR("IMG", "Cache read error at row %d", row);
free(rowBuffer);
cacheFile.close();
return false;
@@ -88,22 +87,22 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x,
free(rowBuffer);
cacheFile.close();
Serial.printf("[%lu] [IMG] Cache render complete\n", millis());
LOG_DBG("IMG", "Cache render complete");
return true;
}
} // namespace
void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
Serial.printf("[%lu] [IMG] Rendering image at %d,%d: %s (%dx%d)\n", millis(), x, y, imagePath.c_str(), width, height);
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) {
Serial.printf("[%lu] [IMG] Invalid render position: (%d,%d) size (%dx%d) screen (%dx%d)\n", millis(), x, y, width,
height, screenWidth, screenHeight);
LOG_ERR("IMG", "Invalid render position: (%d,%d) size (%dx%d) screen (%dx%d)", x, y, width, height, screenWidth,
screenHeight);
return;
}
@@ -117,18 +116,18 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
// Check if image file exists
FsFile file;
if (!Storage.openFileForRead("IMG", imagePath, file)) {
Serial.printf("[%lu] [IMG] Image file not found: %s\n", millis(), imagePath.c_str());
LOG_ERR("IMG", "Image file not found: %s", imagePath.c_str());
return;
}
size_t fileSize = file.size();
file.close();
if (fileSize == 0) {
Serial.printf("[%lu] [IMG] Image file is empty: %s\n", millis(), imagePath.c_str());
LOG_ERR("IMG", "Image file is empty: %s", imagePath.c_str());
return;
}
Serial.printf("[%lu] [IMG] Decoding and caching: %s\n", millis(), imagePath.c_str());
LOG_DBG("IMG", "Decoding and caching: %s", imagePath.c_str());
RenderConfig config;
config.x = x;
@@ -143,19 +142,19 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(imagePath);
if (!decoder) {
Serial.printf("[%lu] [IMG] No decoder found for image: %s\n", millis(), imagePath.c_str());
LOG_ERR("IMG", "No decoder found for image: %s", imagePath.c_str());
return;
}
Serial.printf("[%lu] [IMG] Using %s decoder\n", millis(), decoder->getFormatName());
LOG_DBG("IMG", "Using %s decoder", decoder->getFormatName());
bool success = decoder->decodeToFramebuffer(imagePath, renderer, config);
if (!success) {
Serial.printf("[%lu] [IMG] Failed to decode image: %s\n", millis(), imagePath.c_str());
LOG_ERR("IMG", "Failed to decode image: %s", imagePath.c_str());
return;
}
Serial.printf("[%lu] [IMG] Decode successful\n", millis());
LOG_DBG("IMG", "Decode successful");
}
bool ImageBlock::serialize(FsFile& file) {

View File

@@ -1,6 +1,6 @@
#include "ImageDecoderFactory.h"
#include <HardwareSerial.h>
#include <Logging.h>
#include <memory>
#include <string>
@@ -35,7 +35,7 @@ ImageToFramebufferDecoder* ImageDecoderFactory::getDecoder(const std::string& im
return pngDecoder.get();
}
Serial.printf("[%lu] [DEC] No decoder found for image: %s\n", millis(), imagePath.c_str());
LOG_ERR("DEC", "No decoder found for image: %s", imagePath.c_str());
return nullptr;
}

View File

@@ -1,18 +1,17 @@
#include "ImageToFramebufferDecoder.h"
#include <Arduino.h>
#include <HardwareSerial.h>
#include <Logging.h>
bool ImageToFramebufferDecoder::validateImageDimensions(int width, int height, const std::string& format) {
if (width * height > MAX_SOURCE_PIXELS) {
Serial.printf("[%lu] [IMG] Image too large (%dx%d = %d pixels %s), max supported: %d pixels\n", millis(), width,
height, width * height, format.c_str(), MAX_SOURCE_PIXELS);
LOG_ERR("IMG", "Image too large (%dx%d = %d pixels %s), max supported: %d pixels", width, height, width * height,
format.c_str(), MAX_SOURCE_PIXELS);
return false;
}
return true;
}
void ImageToFramebufferDecoder::warnUnsupportedFeature(const std::string& feature, const std::string& imagePath) {
Serial.printf("[%lu] [IMG] Warning: Unsupported feature '%s' in image '%s'. Image may not display correctly.\n",
millis(), feature.c_str(), imagePath.c_str());
}
LOG_ERR("IMG", "Warning: Unsupported feature '%s' in image '%s'. Image may not display correctly.", feature.c_str(),
imagePath.c_str());
}

View File

@@ -1,7 +1,7 @@
#include "JpegToFramebufferConverter.h"
#include <GfxRenderer.h>
#include <HardwareSerial.h>
#include <Logging.h>
#include <SDCardManager.h>
#include <SdFat.h>
#include <picojpeg.h>
@@ -23,7 +23,7 @@ struct JpegContext {
bool JpegToFramebufferConverter::getDimensionsStatic(const std::string& imagePath, ImageDimensions& out) {
FsFile file;
if (!Storage.openFileForRead("JPG", imagePath, file)) {
Serial.printf("[%lu] [JPG] Failed to open file for dimensions: %s\n", millis(), imagePath.c_str());
LOG_ERR("JPG", "Failed to open file for dimensions: %s", imagePath.c_str());
return false;
}
@@ -34,23 +34,23 @@ bool JpegToFramebufferConverter::getDimensionsStatic(const std::string& imagePat
file.close();
if (status != 0) {
Serial.printf("[%lu] [JPG] Failed to init JPEG for dimensions: %d\n", millis(), status);
LOG_ERR("JPG", "Failed to init JPEG for dimensions: %d", status);
return false;
}
out.width = imageInfo.m_width;
out.height = imageInfo.m_height;
Serial.printf("[%lu] [JPG] Image dimensions: %dx%d\n", millis(), out.width, out.height);
LOG_DBG("JPG", "Image dimensions: %dx%d", out.width, out.height);
return true;
}
bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer,
const RenderConfig& config) {
Serial.printf("[%lu] [JPG] Decoding JPEG: %s\n", millis(), imagePath.c_str());
LOG_DBG("JPG", "Decoding JPEG: %s", imagePath.c_str());
FsFile file;
if (!Storage.openFileForRead("JPG", imagePath, file)) {
Serial.printf("[%lu] [JPG] Failed to open file: %s\n", millis(), imagePath.c_str());
LOG_ERR("JPG", "Failed to open file: %s", imagePath.c_str());
return false;
}
@@ -59,7 +59,7 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
int status = pjpeg_decode_init(&imageInfo, jpegReadCallback, &context, 0);
if (status != 0) {
Serial.printf("[%lu] [JPG] picojpeg init failed: %d\n", millis(), status);
LOG_ERR("JPG", "picojpeg init failed: %d", status);
file.close();
return false;
}
@@ -93,12 +93,11 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
destHeight = (int)(imageInfo.m_height * scale);
}
Serial.printf("[%lu] [JPG] JPEG %dx%d -> %dx%d (scale %.2f), scan type: %d, MCU: %dx%d\n", millis(),
imageInfo.m_width, imageInfo.m_height, destWidth, destHeight, scale, imageInfo.m_scanType,
imageInfo.m_MCUWidth, imageInfo.m_MCUHeight);
LOG_DBG("JPG", "JPEG %dx%d -> %dx%d (scale %.2f), scan type: %d, MCU: %dx%d", imageInfo.m_width, imageInfo.m_height,
destWidth, destHeight, scale, imageInfo.m_scanType, imageInfo.m_MCUWidth, imageInfo.m_MCUHeight);
if (!imageInfo.m_pMCUBufR || !imageInfo.m_pMCUBufG || !imageInfo.m_pMCUBufB) {
Serial.printf("[%lu] [JPG] Null buffer pointers in imageInfo\n", millis());
LOG_ERR("JPG", "Null buffer pointers in imageInfo");
file.close();
return false;
}
@@ -111,7 +110,7 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
bool caching = !config.cachePath.empty();
if (caching) {
if (!cache.allocate(destWidth, destHeight, config.x, config.y)) {
Serial.printf("[%lu] [JPG] Failed to allocate cache buffer, continuing without caching\n", millis());
LOG_ERR("JPG", "Failed to allocate cache buffer, continuing without caching");
caching = false;
}
}
@@ -125,7 +124,7 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
break;
}
if (status != 0) {
Serial.printf("[%lu] [JPG] MCU decode failed: %d\n", millis(), status);
LOG_ERR("JPG", "MCU decode failed: %d", status);
file.close();
return false;
}
@@ -254,7 +253,7 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
}
}
Serial.printf("[%lu] [JPG] Decoding complete\n", millis());
LOG_DBG("JPG", "Decoding complete");
file.close();
// Write cache file if caching was enabled

View File

@@ -1,8 +1,7 @@
#pragma once
#include <HardwareSerial.h>
#include <SDCardManager.h>
#include <SdFat.h>
#include <HalStorage.h>
#include <Logging.h>
#include <stdint.h>
#include <cstring>
@@ -32,14 +31,13 @@ struct PixelCache {
bytesPerRow = (w + 3) / 4; // 2 bits per pixel, 4 pixels per byte
size_t bufferSize = (size_t)bytesPerRow * h;
if (bufferSize > MAX_CACHE_BYTES) {
Serial.printf("[%lu] [IMG] Cache buffer too large: %d bytes for %dx%d (limit %d)\n", millis(), bufferSize, w, h,
MAX_CACHE_BYTES);
LOG_ERR("IMG", "Cache buffer too large: %d bytes for %dx%d (limit %d)", bufferSize, w, h, MAX_CACHE_BYTES);
return false;
}
buffer = (uint8_t*)malloc(bufferSize);
if (buffer) {
memset(buffer, 0, bufferSize);
Serial.printf("[%lu] [IMG] Allocated cache buffer: %d bytes for %dx%d\n", millis(), bufferSize, w, h);
LOG_DBG("IMG", "Allocated cache buffer: %d bytes for %dx%d", bufferSize, w, h);
}
return buffer != nullptr;
}
@@ -60,7 +58,7 @@ struct PixelCache {
FsFile cacheFile;
if (!Storage.openFileForWrite("IMG", cachePath, cacheFile)) {
Serial.printf("[%lu] [IMG] Failed to open cache file for writing: %s\n", millis(), cachePath.c_str());
LOG_ERR("IMG", "Failed to open cache file for writing: %s", cachePath.c_str());
return false;
}
@@ -71,8 +69,7 @@ struct PixelCache {
cacheFile.write(buffer, bytesPerRow * height);
cacheFile.close();
Serial.printf("[%lu] [IMG] Cache written: %s (%dx%d, %d bytes)\n", millis(), cachePath.c_str(), width, height,
4 + bytesPerRow * height);
LOG_DBG("IMG", "Cache written: %s (%dx%d, %d bytes)", cachePath.c_str(), width, height, 4 + bytesPerRow * height);
return true;
}

View File

@@ -1,7 +1,7 @@
#include "PngToFramebufferConverter.h"
#include <GfxRenderer.h>
#include <HardwareSerial.h>
#include <Logging.h>
#include <PNGdec.h>
#include <SDCardManager.h>
#include <SdFat.h>
@@ -216,14 +216,13 @@ int pngDrawCallback(PNGDRAW* pDraw) {
bool PngToFramebufferConverter::getDimensionsStatic(const std::string& imagePath, ImageDimensions& out) {
size_t freeHeap = ESP.getFreeHeap();
if (freeHeap < MIN_FREE_HEAP_FOR_PNG) {
Serial.printf("[%lu] [PNG] Not enough heap for PNG decoder (%u free, need %u)\n", millis(), freeHeap,
MIN_FREE_HEAP_FOR_PNG);
LOG_ERR("PNG", "Not enough heap for PNG decoder (%u free, need %u)", freeHeap, MIN_FREE_HEAP_FOR_PNG);
return false;
}
PNG* png = new (std::nothrow) PNG();
if (!png) {
Serial.printf("[%lu] [PNG] Failed to allocate PNG decoder for dimensions\n", millis());
LOG_ERR("PNG", "Failed to allocate PNG decoder for dimensions");
return false;
}
@@ -231,7 +230,7 @@ bool PngToFramebufferConverter::getDimensionsStatic(const std::string& imagePath
nullptr);
if (rc != 0) {
Serial.printf("[%lu] [PNG] Failed to open PNG for dimensions: %d\n", millis(), rc);
LOG_ERR("PNG", "Failed to open PNG for dimensions: %d", rc);
delete png;
return false;
}
@@ -246,19 +245,18 @@ bool PngToFramebufferConverter::getDimensionsStatic(const std::string& imagePath
bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer,
const RenderConfig& config) {
Serial.printf("[%lu] [PNG] Decoding PNG: %s\n", millis(), imagePath.c_str());
LOG_DBG("PNG", "Decoding PNG: %s", imagePath.c_str());
size_t freeHeap = ESP.getFreeHeap();
if (freeHeap < MIN_FREE_HEAP_FOR_PNG) {
Serial.printf("[%lu] [PNG] Not enough heap for PNG decoder (%u free, need %u)\n", millis(), freeHeap,
MIN_FREE_HEAP_FOR_PNG);
LOG_ERR("PNG", "Not enough heap for PNG decoder (%u free, need %u)", freeHeap, MIN_FREE_HEAP_FOR_PNG);
return false;
}
// Heap-allocate PNG decoder (~42 KB) - freed at end of function
PNG* png = new (std::nothrow) PNG();
if (!png) {
Serial.printf("[%lu] [PNG] Failed to allocate PNG decoder\n", millis());
LOG_ERR("PNG", "Failed to allocate PNG decoder");
return false;
}
@@ -271,7 +269,7 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
int rc = png->open(imagePath.c_str(), pngOpenWithHandle, pngCloseWithHandle, pngReadWithHandle, pngSeekWithHandle,
pngDrawCallback);
if (rc != PNG_SUCCESS) {
Serial.printf("[%lu] [PNG] Failed to open PNG: %d\n", millis(), rc);
LOG_ERR("PNG", "Failed to open PNG: %d", rc);
delete png;
return false;
}
@@ -303,8 +301,8 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
}
ctx.lastDstY = -1; // Reset row tracking
Serial.printf("[%lu] [PNG] PNG %dx%d -> %dx%d (scale %.2f), bpp: %d\n", millis(), ctx.srcWidth, ctx.srcHeight,
ctx.dstWidth, ctx.dstHeight, ctx.scale, png->getBpp());
LOG_DBG("PNG", "PNG %dx%d -> %dx%d (scale %.2f), bpp: %d", ctx.srcWidth, ctx.srcHeight, ctx.dstWidth, ctx.dstHeight,
ctx.scale, png->getBpp());
if (png->getBpp() != 8) {
warnUnsupportedFeature("bit depth (" + std::to_string(png->getBpp()) + "bpp)", imagePath);
@@ -314,7 +312,7 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
const size_t grayBufSize = PNG_MAX_BUFFERED_PIXELS / 2;
ctx.grayLineBuffer = static_cast<uint8_t*>(malloc(grayBufSize));
if (!ctx.grayLineBuffer) {
Serial.printf("[%lu] [PNG] Failed to allocate gray line buffer\n", millis());
LOG_ERR("PNG", "Failed to allocate gray line buffer");
png->close();
delete png;
return false;
@@ -324,7 +322,7 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
ctx.caching = !config.cachePath.empty();
if (ctx.caching) {
if (!ctx.cache.allocate(ctx.dstWidth, ctx.dstHeight, config.x, config.y)) {
Serial.printf("[%lu] [PNG] Failed to allocate cache buffer, continuing without caching\n", millis());
LOG_ERR("PNG", "Failed to allocate cache buffer, continuing without caching");
ctx.caching = false;
}
}
@@ -337,7 +335,7 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
ctx.grayLineBuffer = nullptr;
if (rc != PNG_SUCCESS) {
Serial.printf("[%lu] [PNG] Decode failed: %d\n", millis(), rc);
LOG_ERR("PNG", "Decode failed: %d", rc);
png->close();
delete png;
return false;
@@ -345,7 +343,7 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
png->close();
delete png;
Serial.printf("[%lu] [PNG] PNG decoding complete - render time: %lu ms\n", millis(), decodeTime);
LOG_DBG("PNG", "PNG decoding complete - render time: %lu ms", decodeTime);
// Write cache file if caching was enabled and buffer was allocated
if (ctx.caching) {