diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index 095d0546..265943ff 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #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"); } diff --git a/lib/Epub/Epub/blocks/ImageBlock.cpp b/lib/Epub/Epub/blocks/ImageBlock.cpp index 17605b12..aa42865b 100644 --- a/lib/Epub/Epub/blocks/ImageBlock.cpp +++ b/lib/Epub/Epub/blocks/ImageBlock.cpp @@ -1,8 +1,7 @@ #include "ImageBlock.h" -#include #include -#include +#include #include #include @@ -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) { diff --git a/lib/Epub/Epub/converters/ImageDecoderFactory.cpp b/lib/Epub/Epub/converters/ImageDecoderFactory.cpp index 43b01626..6c7cde26 100644 --- a/lib/Epub/Epub/converters/ImageDecoderFactory.cpp +++ b/lib/Epub/Epub/converters/ImageDecoderFactory.cpp @@ -1,6 +1,6 @@ #include "ImageDecoderFactory.h" -#include +#include #include #include @@ -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; } diff --git a/lib/Epub/Epub/converters/ImageToFramebufferDecoder.cpp b/lib/Epub/Epub/converters/ImageToFramebufferDecoder.cpp index 5af3b73a..bee6f272 100644 --- a/lib/Epub/Epub/converters/ImageToFramebufferDecoder.cpp +++ b/lib/Epub/Epub/converters/ImageToFramebufferDecoder.cpp @@ -1,18 +1,17 @@ #include "ImageToFramebufferDecoder.h" -#include -#include +#include 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()); -} \ No newline at end of file + LOG_ERR("IMG", "Warning: Unsupported feature '%s' in image '%s'. Image may not display correctly.", feature.c_str(), + imagePath.c_str()); +} diff --git a/lib/Epub/Epub/converters/JpegToFramebufferConverter.cpp b/lib/Epub/Epub/converters/JpegToFramebufferConverter.cpp index e0c52ef5..8251156e 100644 --- a/lib/Epub/Epub/converters/JpegToFramebufferConverter.cpp +++ b/lib/Epub/Epub/converters/JpegToFramebufferConverter.cpp @@ -1,7 +1,7 @@ #include "JpegToFramebufferConverter.h" #include -#include +#include #include #include #include @@ -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 diff --git a/lib/Epub/Epub/converters/PixelCache.h b/lib/Epub/Epub/converters/PixelCache.h index b3cf91b3..c911818d 100644 --- a/lib/Epub/Epub/converters/PixelCache.h +++ b/lib/Epub/Epub/converters/PixelCache.h @@ -1,8 +1,7 @@ #pragma once -#include -#include -#include +#include +#include #include #include @@ -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; } diff --git a/lib/Epub/Epub/converters/PngToFramebufferConverter.cpp b/lib/Epub/Epub/converters/PngToFramebufferConverter.cpp index d76dc1da..f54e5e66 100644 --- a/lib/Epub/Epub/converters/PngToFramebufferConverter.cpp +++ b/lib/Epub/Epub/converters/PngToFramebufferConverter.cpp @@ -1,7 +1,7 @@ #include "PngToFramebufferConverter.h" #include -#include +#include #include #include #include @@ -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(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) { diff --git a/lib/PngToBmpConverter/PngToBmpConverter.cpp b/lib/PngToBmpConverter/PngToBmpConverter.cpp new file mode 100644 index 00000000..e93e0b89 --- /dev/null +++ b/lib/PngToBmpConverter/PngToBmpConverter.cpp @@ -0,0 +1,858 @@ +#include "PngToBmpConverter.h" + +#include +#include +#include + +#include +#include + +#include "BitmapHelpers.h" + +// ============================================================================ +// IMAGE PROCESSING OPTIONS - Same as JpegToBmpConverter for consistency +// ============================================================================ +constexpr bool USE_8BIT_OUTPUT = false; +constexpr bool USE_ATKINSON = true; +constexpr bool USE_FLOYD_STEINBERG = false; +constexpr bool USE_PRESCALE = true; +constexpr int TARGET_MAX_WIDTH = 480; +constexpr int TARGET_MAX_HEIGHT = 800; +// ============================================================================ + +// PNG constants +static constexpr uint8_t PNG_SIGNATURE[8] = {137, 80, 78, 71, 13, 10, 26, 10}; + +// PNG color types +enum PngColorType : uint8_t { + PNG_COLOR_GRAYSCALE = 0, + PNG_COLOR_RGB = 2, + PNG_COLOR_PALETTE = 3, + PNG_COLOR_GRAYSCALE_ALPHA = 4, + PNG_COLOR_RGBA = 6, +}; + +// PNG filter types +enum PngFilter : uint8_t { + PNG_FILTER_NONE = 0, + PNG_FILTER_SUB = 1, + PNG_FILTER_UP = 2, + PNG_FILTER_AVERAGE = 3, + PNG_FILTER_PAETH = 4, +}; + +// Read a big-endian 32-bit value from file +static bool readBE32(FsFile& file, uint32_t& value) { + uint8_t buf[4]; + if (file.read(buf, 4) != 4) return false; + value = (static_cast(buf[0]) << 24) | (static_cast(buf[1]) << 16) | + (static_cast(buf[2]) << 8) | buf[3]; + return true; +} + +// BMP writing helpers (same as JpegToBmpConverter) +inline void write16(Print& out, const uint16_t value) { + out.write(value & 0xFF); + out.write((value >> 8) & 0xFF); +} + +inline void write32(Print& out, const uint32_t value) { + out.write(value & 0xFF); + out.write((value >> 8) & 0xFF); + out.write((value >> 16) & 0xFF); + out.write((value >> 24) & 0xFF); +} + +inline void write32Signed(Print& out, const int32_t value) { + out.write(value & 0xFF); + out.write((value >> 8) & 0xFF); + out.write((value >> 16) & 0xFF); + out.write((value >> 24) & 0xFF); +} + +static void writeBmpHeader8bit(Print& bmpOut, const int width, const int height) { + const int bytesPerRow = (width + 3) / 4 * 4; + const int imageSize = bytesPerRow * height; + const uint32_t paletteSize = 256 * 4; + const uint32_t fileSize = 14 + 40 + paletteSize + imageSize; + + bmpOut.write('B'); + bmpOut.write('M'); + write32(bmpOut, fileSize); + write32(bmpOut, 0); + write32(bmpOut, 14 + 40 + paletteSize); + + write32(bmpOut, 40); + write32Signed(bmpOut, width); + write32Signed(bmpOut, -height); + write16(bmpOut, 1); + write16(bmpOut, 8); + write32(bmpOut, 0); + write32(bmpOut, imageSize); + write32(bmpOut, 2835); + write32(bmpOut, 2835); + write32(bmpOut, 256); + write32(bmpOut, 256); + + for (int i = 0; i < 256; i++) { + bmpOut.write(static_cast(i)); + bmpOut.write(static_cast(i)); + bmpOut.write(static_cast(i)); + bmpOut.write(static_cast(0)); + } +} + +static void writeBmpHeader1bit(Print& bmpOut, const int width, const int height) { + const int bytesPerRow = (width + 31) / 32 * 4; + const int imageSize = bytesPerRow * height; + const uint32_t fileSize = 62 + imageSize; + + bmpOut.write('B'); + bmpOut.write('M'); + write32(bmpOut, fileSize); + write32(bmpOut, 0); + write32(bmpOut, 62); + + write32(bmpOut, 40); + write32Signed(bmpOut, width); + write32Signed(bmpOut, -height); + write16(bmpOut, 1); + write16(bmpOut, 1); + write32(bmpOut, 0); + write32(bmpOut, imageSize); + write32(bmpOut, 2835); + write32(bmpOut, 2835); + write32(bmpOut, 2); + write32(bmpOut, 2); + + uint8_t palette[8] = {0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00}; + for (const uint8_t i : palette) { + bmpOut.write(i); + } +} + +static void writeBmpHeader2bit(Print& bmpOut, const int width, const int height) { + const int bytesPerRow = (width * 2 + 31) / 32 * 4; + const int imageSize = bytesPerRow * height; + const uint32_t fileSize = 70 + imageSize; + + bmpOut.write('B'); + bmpOut.write('M'); + write32(bmpOut, fileSize); + write32(bmpOut, 0); + write32(bmpOut, 70); + + write32(bmpOut, 40); + write32Signed(bmpOut, width); + write32Signed(bmpOut, -height); + write16(bmpOut, 1); + write16(bmpOut, 2); + write32(bmpOut, 0); + write32(bmpOut, imageSize); + write32(bmpOut, 2835); + write32(bmpOut, 2835); + write32(bmpOut, 4); + write32(bmpOut, 4); + + uint8_t palette[16] = {0x00, 0x00, 0x00, 0x00, 0x55, 0x55, 0x55, 0x00, + 0xAA, 0xAA, 0xAA, 0x00, 0xFF, 0xFF, 0xFF, 0x00}; + for (const uint8_t i : palette) { + bmpOut.write(i); + } +} + +// Paeth predictor function per PNG spec +static inline uint8_t paethPredictor(uint8_t a, uint8_t b, uint8_t c) { + int p = static_cast(a) + b - c; + int pa = p > a ? p - a : a - p; + int pb = p > b ? p - b : b - p; + int pc = p > c ? p - c : c - p; + if (pa <= pb && pa <= pc) return a; + if (pb <= pc) return b; + return c; +} + +// Context for streaming PNG decompression +struct PngDecodeContext { + FsFile& file; + + // PNG image properties + uint32_t width; + uint32_t height; + uint8_t bitDepth; + uint8_t colorType; + uint8_t bytesPerPixel; // after expanding sub-byte depths + uint32_t rawRowBytes; // bytes per raw row (without filter byte) + + // Scanline buffers + uint8_t* currentRow; // current defiltered scanline + uint8_t* previousRow; // previous defiltered scanline + + // zlib decompression state + mz_stream zstream; + bool zstreamInitialized; + + // Chunk reading state + uint32_t chunkBytesRemaining; // bytes left in current IDAT chunk + bool idatFinished; // no more IDAT chunks + + // File read buffer for feeding zlib + uint8_t readBuf[2048]; + + // Palette for indexed color (type 3) + uint8_t palette[256 * 3]; + int paletteSize; +}; + +// Read the next IDAT chunk header, skipping non-IDAT chunks +// Returns true if an IDAT chunk was found +static bool findNextIdatChunk(PngDecodeContext& ctx) { + while (true) { + uint32_t chunkLen; + if (!readBE32(ctx.file, chunkLen)) return false; + + uint8_t chunkType[4]; + if (ctx.file.read(chunkType, 4) != 4) return false; + + if (memcmp(chunkType, "IDAT", 4) == 0) { + ctx.chunkBytesRemaining = chunkLen; + return true; + } + + // Skip this chunk's data + 4-byte CRC + // Use seek to skip efficiently + if (!ctx.file.seekCur(chunkLen + 4)) return false; + + // If we hit IEND, there are no more chunks + if (memcmp(chunkType, "IEND", 4) == 0) { + return false; + } + } +} + +// Feed compressed data to zlib from IDAT chunks +// Returns number of bytes made available in zstream, or -1 on error +static int feedZlibInput(PngDecodeContext& ctx) { + if (ctx.idatFinished) return 0; + + // If current IDAT chunk is exhausted, skip its CRC and find next + while (ctx.chunkBytesRemaining == 0) { + // Skip 4-byte CRC of previous IDAT + if (!ctx.file.seekCur(4)) return -1; + + if (!findNextIdatChunk(ctx)) { + ctx.idatFinished = true; + return 0; + } + } + + // Read from current IDAT chunk + size_t toRead = sizeof(ctx.readBuf); + if (toRead > ctx.chunkBytesRemaining) toRead = ctx.chunkBytesRemaining; + + int bytesRead = ctx.file.read(ctx.readBuf, toRead); + if (bytesRead <= 0) return -1; + + ctx.chunkBytesRemaining -= bytesRead; + ctx.zstream.next_in = ctx.readBuf; + ctx.zstream.avail_in = bytesRead; + + return bytesRead; +} + +// Decompress exactly 'needed' bytes into 'dest' +static bool decompressBytes(PngDecodeContext& ctx, uint8_t* dest, size_t needed) { + ctx.zstream.next_out = dest; + ctx.zstream.avail_out = needed; + + while (ctx.zstream.avail_out > 0) { + if (ctx.zstream.avail_in == 0) { + int fed = feedZlibInput(ctx); + if (fed < 0) return false; + if (fed == 0) { + // Try one more inflate to flush + int ret = mz_inflate(&ctx.zstream, MZ_SYNC_FLUSH); + if (ctx.zstream.avail_out == 0) break; + return false; + } + } + + int ret = mz_inflate(&ctx.zstream, MZ_SYNC_FLUSH); + if (ret != MZ_OK && ret != MZ_STREAM_END && ret != MZ_BUF_ERROR) { + LOG_ERR("PNG", "zlib inflate error: %d", ret); + return false; + } + if (ret == MZ_STREAM_END) break; + } + + return ctx.zstream.avail_out == 0; +} + +// Decode one scanline: decompress filter byte + raw bytes, then unfilter +static bool decodeScanline(PngDecodeContext& ctx) { + // Decompress filter byte + uint8_t filterType; + if (!decompressBytes(ctx, &filterType, 1)) return false; + + // Decompress raw row data into currentRow + if (!decompressBytes(ctx, ctx.currentRow, ctx.rawRowBytes)) return false; + + // Apply reverse filter + const int bpp = ctx.bytesPerPixel; + + switch (filterType) { + case PNG_FILTER_NONE: + break; + + case PNG_FILTER_SUB: + for (uint32_t i = bpp; i < ctx.rawRowBytes; i++) { + ctx.currentRow[i] += ctx.currentRow[i - bpp]; + } + break; + + case PNG_FILTER_UP: + for (uint32_t i = 0; i < ctx.rawRowBytes; i++) { + ctx.currentRow[i] += ctx.previousRow[i]; + } + break; + + case PNG_FILTER_AVERAGE: + for (uint32_t i = 0; i < ctx.rawRowBytes; i++) { + uint8_t a = (i >= static_cast(bpp)) ? ctx.currentRow[i - bpp] : 0; + uint8_t b = ctx.previousRow[i]; + ctx.currentRow[i] += (a + b) / 2; + } + break; + + case PNG_FILTER_PAETH: + for (uint32_t i = 0; i < ctx.rawRowBytes; i++) { + uint8_t a = (i >= static_cast(bpp)) ? ctx.currentRow[i - bpp] : 0; + uint8_t b = ctx.previousRow[i]; + uint8_t c = (i >= static_cast(bpp)) ? ctx.previousRow[i - bpp] : 0; + ctx.currentRow[i] += paethPredictor(a, b, c); + } + break; + + default: + LOG_ERR("PNG", "Unknown filter type: %d", filterType); + return false; + } + + return true; +} + +// Batch-convert an entire scanline to grayscale. +// Branches once on colorType/bitDepth, then runs a tight loop for the whole row. +static void convertScanlineToGray(const PngDecodeContext& ctx, uint8_t* grayRow) { + const uint8_t* src = ctx.currentRow; + const uint32_t w = ctx.width; + + switch (ctx.colorType) { + case PNG_COLOR_GRAYSCALE: + if (ctx.bitDepth == 8) { + memcpy(grayRow, src, w); + } else if (ctx.bitDepth == 16) { + for (uint32_t x = 0; x < w; x++) grayRow[x] = src[x * 2]; + } else { + const int ppb = 8 / ctx.bitDepth; + const uint8_t mask = (1 << ctx.bitDepth) - 1; + for (uint32_t x = 0; x < w; x++) { + int shift = (ppb - 1 - (x % ppb)) * ctx.bitDepth; + grayRow[x] = (src[x / ppb] >> shift & mask) * 255 / mask; + } + } + break; + + case PNG_COLOR_RGB: + if (ctx.bitDepth == 8) { + // Fast path: most common EPUB cover format + for (uint32_t x = 0; x < w; x++) { + const uint8_t* p = src + x * 3; + grayRow[x] = (p[0] * 25 + p[1] * 50 + p[2] * 25) / 100; + } + } else { + for (uint32_t x = 0; x < w; x++) { + grayRow[x] = (src[x * 6] * 25 + src[x * 6 + 2] * 50 + src[x * 6 + 4] * 25) / 100; + } + } + break; + + case PNG_COLOR_PALETTE: { + const int ppb = 8 / ctx.bitDepth; + const uint8_t mask = (1 << ctx.bitDepth) - 1; + const uint8_t* pal = ctx.palette; + const int palSize = ctx.paletteSize; + for (uint32_t x = 0; x < w; x++) { + int shift = (ppb - 1 - (x % ppb)) * ctx.bitDepth; + uint8_t idx = (src[x / ppb] >> shift) & mask; + if (idx >= palSize) idx = 0; + grayRow[x] = (pal[idx * 3] * 25 + pal[idx * 3 + 1] * 50 + pal[idx * 3 + 2] * 25) / 100; + } + break; + } + + case PNG_COLOR_GRAYSCALE_ALPHA: + if (ctx.bitDepth == 8) { + for (uint32_t x = 0; x < w; x++) grayRow[x] = src[x * 2]; + } else { + for (uint32_t x = 0; x < w; x++) grayRow[x] = src[x * 4]; + } + break; + + case PNG_COLOR_RGBA: + if (ctx.bitDepth == 8) { + for (uint32_t x = 0; x < w; x++) { + const uint8_t* p = src + x * 4; + grayRow[x] = (p[0] * 25 + p[1] * 50 + p[2] * 25) / 100; + } + } else { + for (uint32_t x = 0; x < w; x++) { + grayRow[x] = (src[x * 8] * 25 + src[x * 8 + 2] * 50 + src[x * 8 + 4] * 25) / 100; + } + } + break; + + default: + memset(grayRow, 128, w); + break; + } +} + +bool PngToBmpConverter::pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOut, int targetWidth, int targetHeight, + bool oneBit, bool crop) { + LOG_DBG("PNG", "Converting PNG to %s BMP (target: %dx%d)", oneBit ? "1-bit" : "2-bit", targetWidth, targetHeight); + + // Verify PNG signature + uint8_t sig[8]; + if (pngFile.read(sig, 8) != 8 || memcmp(sig, PNG_SIGNATURE, 8) != 0) { + LOG_ERR("PNG", "Invalid PNG signature"); + return false; + } + + // Read IHDR chunk + uint32_t ihdrLen; + if (!readBE32(pngFile, ihdrLen)) return false; + + uint8_t ihdrType[4]; + if (pngFile.read(ihdrType, 4) != 4 || memcmp(ihdrType, "IHDR", 4) != 0) { + LOG_ERR("PNG", "Missing IHDR chunk"); + return false; + } + + uint32_t width, height; + if (!readBE32(pngFile, width) || !readBE32(pngFile, height)) return false; + + uint8_t ihdrRest[5]; + if (pngFile.read(ihdrRest, 5) != 5) return false; + + uint8_t bitDepth = ihdrRest[0]; + uint8_t colorType = ihdrRest[1]; + uint8_t compression = ihdrRest[2]; + uint8_t filter = ihdrRest[3]; + uint8_t interlace = ihdrRest[4]; + + // Skip IHDR CRC + pngFile.seekCur(4); + + LOG_DBG("PNG", "Image: %ux%u, depth=%u, color=%u, interlace=%u", width, height, bitDepth, colorType, interlace); + + if (compression != 0 || filter != 0) { + LOG_ERR("PNG", "Unsupported compression/filter method"); + return false; + } + + if (interlace != 0) { + LOG_ERR("PNG", "Interlaced PNGs not supported"); + return false; + } + + // Safety limits + constexpr int MAX_IMAGE_WIDTH = 2048; + constexpr int MAX_IMAGE_HEIGHT = 3072; + + if (width > MAX_IMAGE_WIDTH || height > MAX_IMAGE_HEIGHT || width == 0 || height == 0) { + LOG_ERR("PNG", "Image too large or zero (%ux%u)", width, height); + return false; + } + + // Calculate bytes per pixel and raw row bytes + uint8_t bytesPerPixel; + uint32_t rawRowBytes; + + switch (colorType) { + case PNG_COLOR_GRAYSCALE: + if (bitDepth == 16) { + bytesPerPixel = 2; + rawRowBytes = width * 2; + } else if (bitDepth == 8) { + bytesPerPixel = 1; + rawRowBytes = width; + } else { + // Sub-byte: 1, 2, or 4 bits + bytesPerPixel = 1; + rawRowBytes = (width * bitDepth + 7) / 8; + } + break; + case PNG_COLOR_RGB: + bytesPerPixel = (bitDepth == 16) ? 6 : 3; + rawRowBytes = width * bytesPerPixel; + break; + case PNG_COLOR_PALETTE: + bytesPerPixel = 1; + rawRowBytes = (width * bitDepth + 7) / 8; + break; + case PNG_COLOR_GRAYSCALE_ALPHA: + bytesPerPixel = (bitDepth == 16) ? 4 : 2; + rawRowBytes = width * bytesPerPixel; + break; + case PNG_COLOR_RGBA: + bytesPerPixel = (bitDepth == 16) ? 8 : 4; + rawRowBytes = width * bytesPerPixel; + break; + default: + LOG_ERR("PNG", "Unsupported color type: %d", colorType); + return false; + } + + // Validate raw row bytes won't cause memory issues + if (rawRowBytes > 16384) { + LOG_ERR("PNG", "Row too large: %u bytes", rawRowBytes); + return false; + } + + // Initialize decode context + PngDecodeContext ctx = {.file = pngFile, + .width = width, + .height = height, + .bitDepth = bitDepth, + .colorType = colorType, + .bytesPerPixel = bytesPerPixel, + .rawRowBytes = rawRowBytes, + .currentRow = nullptr, + .previousRow = nullptr, + .zstream = {}, + .zstreamInitialized = false, + .chunkBytesRemaining = 0, + .idatFinished = false, + .readBuf = {}, + .palette = {}, + .paletteSize = 0}; + + // Allocate scanline buffers + ctx.currentRow = static_cast(malloc(rawRowBytes)); + ctx.previousRow = static_cast(calloc(rawRowBytes, 1)); + if (!ctx.currentRow || !ctx.previousRow) { + LOG_ERR("PNG", "Failed to allocate scanline buffers (%u bytes each)", rawRowBytes); + free(ctx.currentRow); + free(ctx.previousRow); + return false; + } + + // Scan for PLTE chunk (palette) and first IDAT chunk + // We need to read chunks until we find IDAT, collecting PLTE along the way + bool foundIdat = false; + while (!foundIdat) { + uint32_t chunkLen; + if (!readBE32(pngFile, chunkLen)) break; + + uint8_t chunkType[4]; + if (pngFile.read(chunkType, 4) != 4) break; + + if (memcmp(chunkType, "PLTE", 4) == 0) { + int entries = chunkLen / 3; + if (entries > 256) entries = 256; + ctx.paletteSize = entries; + size_t palBytes = entries * 3; + pngFile.read(ctx.palette, palBytes); + // Skip any remaining palette data + if (chunkLen > palBytes) pngFile.seekCur(chunkLen - palBytes); + pngFile.seekCur(4); // CRC + } else if (memcmp(chunkType, "IDAT", 4) == 0) { + ctx.chunkBytesRemaining = chunkLen; + foundIdat = true; + } else if (memcmp(chunkType, "IEND", 4) == 0) { + break; + } else { + // Skip unknown chunk + pngFile.seekCur(chunkLen + 4); + } + } + + if (!foundIdat) { + LOG_ERR("PNG", "No IDAT chunk found"); + free(ctx.currentRow); + free(ctx.previousRow); + return false; + } + + // Initialize zlib decompression + memset(&ctx.zstream, 0, sizeof(ctx.zstream)); + if (mz_inflateInit(&ctx.zstream) != MZ_OK) { + LOG_ERR("PNG", "Failed to initialize zlib"); + free(ctx.currentRow); + free(ctx.previousRow); + return false; + } + ctx.zstreamInitialized = true; + + // Calculate output dimensions (same logic as JpegToBmpConverter) + int outWidth = width; + int outHeight = height; + uint32_t scaleX_fp = 65536; + uint32_t scaleY_fp = 65536; + bool needsScaling = false; + + if (targetWidth > 0 && targetHeight > 0 && + (static_cast(width) > targetWidth || static_cast(height) > targetHeight)) { + const float scaleToFitWidth = static_cast(targetWidth) / width; + const float scaleToFitHeight = static_cast(targetHeight) / height; + float scale = 1.0; + if (crop) { + scale = (scaleToFitWidth > scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight; + } else { + scale = (scaleToFitWidth < scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight; + } + + outWidth = static_cast(width * scale); + outHeight = static_cast(height * scale); + if (outWidth < 1) outWidth = 1; + if (outHeight < 1) outHeight = 1; + + scaleX_fp = (static_cast(width) << 16) / outWidth; + scaleY_fp = (static_cast(height) << 16) / outHeight; + needsScaling = true; + + LOG_DBG("PNG", "Pre-scaling %ux%u -> %dx%d (fit to %dx%d)", width, height, outWidth, outHeight, targetWidth, + targetHeight); + } + + // Write BMP header + int bytesPerRow; + if (USE_8BIT_OUTPUT && !oneBit) { + writeBmpHeader8bit(bmpOut, outWidth, outHeight); + bytesPerRow = (outWidth + 3) / 4 * 4; + } else if (oneBit) { + writeBmpHeader1bit(bmpOut, outWidth, outHeight); + bytesPerRow = (outWidth + 31) / 32 * 4; + } else { + writeBmpHeader2bit(bmpOut, outWidth, outHeight); + bytesPerRow = (outWidth * 2 + 31) / 32 * 4; + } + + // Allocate BMP row buffer + auto* rowBuffer = static_cast(malloc(bytesPerRow)); + if (!rowBuffer) { + LOG_ERR("PNG", "Failed to allocate row buffer"); + mz_inflateEnd(&ctx.zstream); + free(ctx.currentRow); + free(ctx.previousRow); + return false; + } + + // Create ditherers (same as JpegToBmpConverter) + AtkinsonDitherer* atkinsonDitherer = nullptr; + FloydSteinbergDitherer* fsDitherer = nullptr; + Atkinson1BitDitherer* atkinson1BitDitherer = nullptr; + + if (oneBit) { + atkinson1BitDitherer = new Atkinson1BitDitherer(outWidth); + } else if (!USE_8BIT_OUTPUT) { + if (USE_ATKINSON) { + atkinsonDitherer = new AtkinsonDitherer(outWidth); + } else if (USE_FLOYD_STEINBERG) { + fsDitherer = new FloydSteinbergDitherer(outWidth); + } + } + + // Scaling accumulators + uint32_t* rowAccum = nullptr; + uint16_t* rowCount = nullptr; + int currentOutY = 0; + uint32_t nextOutY_srcStart = 0; + + if (needsScaling) { + rowAccum = new uint32_t[outWidth](); + rowCount = new uint16_t[outWidth](); + nextOutY_srcStart = scaleY_fp; + } + + // Allocate grayscale row buffer - batch-convert each scanline to avoid + // per-pixel getPixelGray() switch overhead in the hot loops + auto* grayRow = static_cast(malloc(width)); + if (!grayRow) { + LOG_ERR("PNG", "Failed to allocate grayscale row buffer"); + delete[] rowAccum; + delete[] rowCount; + delete atkinsonDitherer; + delete fsDitherer; + delete atkinson1BitDitherer; + free(rowBuffer); + mz_inflateEnd(&ctx.zstream); + free(ctx.currentRow); + free(ctx.previousRow); + return false; + } + + bool success = true; + + // Process each scanline + for (uint32_t y = 0; y < height; y++) { + // Decode one scanline + if (!decodeScanline(ctx)) { + LOG_ERR("PNG", "Failed to decode scanline %u", y); + success = false; + break; + } + + // Batch-convert entire scanline to grayscale (one branch, tight loop) + convertScanlineToGray(ctx, grayRow); + + if (!needsScaling) { + // Direct output (no scaling) + memset(rowBuffer, 0, bytesPerRow); + + if (USE_8BIT_OUTPUT && !oneBit) { + for (int x = 0; x < outWidth; x++) { + rowBuffer[x] = adjustPixel(grayRow[x]); + } + } else if (oneBit) { + for (int x = 0; x < outWidth; x++) { + const uint8_t bit = + atkinson1BitDitherer ? atkinson1BitDitherer->processPixel(grayRow[x], x) : quantize1bit(grayRow[x], x, y); + const int byteIndex = x / 8; + const int bitOffset = 7 - (x % 8); + rowBuffer[byteIndex] |= (bit << bitOffset); + } + if (atkinson1BitDitherer) atkinson1BitDitherer->nextRow(); + } else { + for (int x = 0; x < outWidth; x++) { + const uint8_t gray = adjustPixel(grayRow[x]); + uint8_t twoBit; + if (atkinsonDitherer) { + twoBit = atkinsonDitherer->processPixel(gray, x); + } else if (fsDitherer) { + twoBit = fsDitherer->processPixel(gray, x); + } else { + twoBit = quantize(gray, x, y); + } + const int byteIndex = (x * 2) / 8; + const int bitOffset = 6 - ((x * 2) % 8); + rowBuffer[byteIndex] |= (twoBit << bitOffset); + } + if (atkinsonDitherer) + atkinsonDitherer->nextRow(); + else if (fsDitherer) + fsDitherer->nextRow(); + } + bmpOut.write(rowBuffer, bytesPerRow); + } else { + // Area-averaging scaling (same as JpegToBmpConverter) + for (int outX = 0; outX < outWidth; outX++) { + const int srcXStart = (static_cast(outX) * scaleX_fp) >> 16; + const int srcXEnd = (static_cast(outX + 1) * scaleX_fp) >> 16; + + int sum = 0; + int count = 0; + for (int srcX = srcXStart; srcX < srcXEnd && srcX < static_cast(width); srcX++) { + sum += grayRow[srcX]; + count++; + } + + if (count == 0 && srcXStart < static_cast(width)) { + sum = grayRow[srcXStart]; + count = 1; + } + + rowAccum[outX] += sum; + rowCount[outX] += count; + } + + // Check if we've crossed into the next output row + const uint32_t srcY_fp = static_cast(y + 1) << 16; + + if (srcY_fp >= nextOutY_srcStart && currentOutY < outHeight) { + memset(rowBuffer, 0, bytesPerRow); + + if (USE_8BIT_OUTPUT && !oneBit) { + for (int x = 0; x < outWidth; x++) { + const uint8_t gray = (rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0; + rowBuffer[x] = adjustPixel(gray); + } + } else if (oneBit) { + for (int x = 0; x < outWidth; x++) { + const uint8_t gray = (rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0; + const uint8_t bit = + atkinson1BitDitherer ? atkinson1BitDitherer->processPixel(gray, x) : quantize1bit(gray, x, currentOutY); + const int byteIndex = x / 8; + const int bitOffset = 7 - (x % 8); + rowBuffer[byteIndex] |= (bit << bitOffset); + } + if (atkinson1BitDitherer) atkinson1BitDitherer->nextRow(); + } else { + for (int x = 0; x < outWidth; x++) { + const uint8_t gray = adjustPixel((rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0); + uint8_t twoBit; + if (atkinsonDitherer) { + twoBit = atkinsonDitherer->processPixel(gray, x); + } else if (fsDitherer) { + twoBit = fsDitherer->processPixel(gray, x); + } else { + twoBit = quantize(gray, x, currentOutY); + } + const int byteIndex = (x * 2) / 8; + const int bitOffset = 6 - ((x * 2) % 8); + rowBuffer[byteIndex] |= (twoBit << bitOffset); + } + if (atkinsonDitherer) + atkinsonDitherer->nextRow(); + else if (fsDitherer) + fsDitherer->nextRow(); + } + + bmpOut.write(rowBuffer, bytesPerRow); + currentOutY++; + + memset(rowAccum, 0, outWidth * sizeof(uint32_t)); + memset(rowCount, 0, outWidth * sizeof(uint16_t)); + + nextOutY_srcStart = static_cast(currentOutY + 1) * scaleY_fp; + } + } + + // Swap current/previous row buffers + uint8_t* temp = ctx.previousRow; + ctx.previousRow = ctx.currentRow; + ctx.currentRow = temp; + } + + // Clean up + free(grayRow); + delete[] rowAccum; + delete[] rowCount; + delete atkinsonDitherer; + delete fsDitherer; + delete atkinson1BitDitherer; + free(rowBuffer); + mz_inflateEnd(&ctx.zstream); + free(ctx.currentRow); + free(ctx.previousRow); + + if (success) { + LOG_DBG("PNG", "Successfully converted PNG to BMP"); + } + return success; +} + +bool PngToBmpConverter::pngFileToBmpStream(FsFile& pngFile, Print& bmpOut, bool crop) { + return pngFileToBmpStreamInternal(pngFile, bmpOut, TARGET_MAX_WIDTH, TARGET_MAX_HEIGHT, false, crop); +} + +bool PngToBmpConverter::pngFileToBmpStreamWithSize(FsFile& pngFile, Print& bmpOut, int targetMaxWidth, + int targetMaxHeight) { + return pngFileToBmpStreamInternal(pngFile, bmpOut, targetMaxWidth, targetMaxHeight, false); +} + +bool PngToBmpConverter::pngFileTo1BitBmpStreamWithSize(FsFile& pngFile, Print& bmpOut, int targetMaxWidth, + int targetMaxHeight) { + return pngFileToBmpStreamInternal(pngFile, bmpOut, targetMaxWidth, targetMaxHeight, true, true); +} diff --git a/lib/PngToBmpConverter/PngToBmpConverter.h b/lib/PngToBmpConverter/PngToBmpConverter.h new file mode 100644 index 00000000..73914e59 --- /dev/null +++ b/lib/PngToBmpConverter/PngToBmpConverter.h @@ -0,0 +1,14 @@ +#pragma once + +class FsFile; +class Print; + +class PngToBmpConverter { + static bool pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOut, int targetWidth, int targetHeight, bool oneBit, + bool crop = true); + + public: + static bool pngFileToBmpStream(FsFile& pngFile, Print& bmpOut, bool crop = true); + static bool pngFileToBmpStreamWithSize(FsFile& pngFile, Print& bmpOut, int targetMaxWidth, int targetMaxHeight); + static bool pngFileTo1BitBmpStreamWithSize(FsFile& pngFile, Print& bmpOut, int targetMaxWidth, int targetMaxHeight); +};