feat: Add PNG cover image support for EPUB books (#827)
Cherry-pick upstream PR #827 with conflict resolution for mod/master: - Add PngToBmpConverter library for PNG cover → BMP conversion - Add PNG thumbnail generation in generateThumbBmp() - Fix generateCoverBmp() PNG block to use effectiveCoverImageHref (consistent with mod's fallback cover candidate probing) - Add .png to getCoverCandidates() extensions - Use LOG_ERR macro in ImageToFramebufferDecoder (mod standard) - Upstream image converter refinements (ImageBlock, PixelCache, JpegToFramebufferConverter, PngToFramebufferConverter) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
#include <HalStorage.h>
|
||||
#include <JpegToBmpConverter.h>
|
||||
#include <Logging.h>
|
||||
#include <PngToBmpConverter.h>
|
||||
#include <ZipFile.h>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -518,12 +519,45 @@ 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");
|
||||
}
|
||||
|
||||
bool isPng = lowerHref.substr(lowerHref.length() - 4) == ".png";
|
||||
if (isPng) {
|
||||
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(effectiveCoverImageHref, 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;
|
||||
}
|
||||
|
||||
@@ -611,9 +645,46 @@ bool Epub::generateThumbBmp(int height) const {
|
||||
}
|
||||
LOG_DBG("EBP", "Generated thumb BMP from JPG cover image, success: %s", success ? "yes" : "no");
|
||||
return success;
|
||||
} else {
|
||||
LOG_ERR("EBP", "Cover image is not a supported format, skipping thumbnail");
|
||||
}
|
||||
|
||||
bool isPng = lowerHref.substr(lowerHref.length() - 4) == ".png";
|
||||
if (isPng) {
|
||||
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(effectiveCoverImageHref, 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;
|
||||
}
|
||||
|
||||
LOG_ERR("EBP", "Cover image is not a supported format, skipping thumbnail");
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -970,7 +1041,7 @@ bool Epub::isValidThumbnailBmp(const std::string& bmpPath) {
|
||||
|
||||
std::vector<std::string> Epub::getCoverCandidates() const {
|
||||
std::vector<std::string> coverDirectories = {".", "images", "Images", "OEBPS", "OEBPS/images", "OEBPS/Images"};
|
||||
std::vector<std::string> coverExtensions = {".jpg", ".jpeg"}; // add ".png" when PNG cover support is implemented
|
||||
std::vector<std::string> coverExtensions = {".jpg", ".jpeg", ".png"};
|
||||
std::vector<std::string> coverCandidates;
|
||||
for (const auto& ext : coverExtensions) {
|
||||
for (const auto& dir : coverDirectories) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
858
lib/PngToBmpConverter/PngToBmpConverter.cpp
Normal file
858
lib/PngToBmpConverter/PngToBmpConverter.cpp
Normal file
@@ -0,0 +1,858 @@
|
||||
#include "PngToBmpConverter.h"
|
||||
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
#include <miniz.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#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<uint32_t>(buf[0]) << 24) | (static_cast<uint32_t>(buf[1]) << 16) |
|
||||
(static_cast<uint32_t>(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<uint8_t>(i));
|
||||
bmpOut.write(static_cast<uint8_t>(i));
|
||||
bmpOut.write(static_cast<uint8_t>(i));
|
||||
bmpOut.write(static_cast<uint8_t>(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<int>(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<uint32_t>(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<uint32_t>(bpp)) ? ctx.currentRow[i - bpp] : 0;
|
||||
uint8_t b = ctx.previousRow[i];
|
||||
uint8_t c = (i >= static_cast<uint32_t>(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<uint8_t*>(malloc(rawRowBytes));
|
||||
ctx.previousRow = static_cast<uint8_t*>(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<int>(width) > targetWidth || static_cast<int>(height) > targetHeight)) {
|
||||
const float scaleToFitWidth = static_cast<float>(targetWidth) / width;
|
||||
const float scaleToFitHeight = static_cast<float>(targetHeight) / height;
|
||||
float scale = 1.0;
|
||||
if (crop) {
|
||||
scale = (scaleToFitWidth > scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight;
|
||||
} else {
|
||||
scale = (scaleToFitWidth < scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight;
|
||||
}
|
||||
|
||||
outWidth = static_cast<int>(width * scale);
|
||||
outHeight = static_cast<int>(height * scale);
|
||||
if (outWidth < 1) outWidth = 1;
|
||||
if (outHeight < 1) outHeight = 1;
|
||||
|
||||
scaleX_fp = (static_cast<uint32_t>(width) << 16) / outWidth;
|
||||
scaleY_fp = (static_cast<uint32_t>(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<uint8_t*>(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<uint8_t*>(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<uint32_t>(outX) * scaleX_fp) >> 16;
|
||||
const int srcXEnd = (static_cast<uint32_t>(outX + 1) * scaleX_fp) >> 16;
|
||||
|
||||
int sum = 0;
|
||||
int count = 0;
|
||||
for (int srcX = srcXStart; srcX < srcXEnd && srcX < static_cast<int>(width); srcX++) {
|
||||
sum += grayRow[srcX];
|
||||
count++;
|
||||
}
|
||||
|
||||
if (count == 0 && srcXStart < static_cast<int>(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<uint32_t>(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<uint32_t>(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);
|
||||
}
|
||||
14
lib/PngToBmpConverter/PngToBmpConverter.h
Normal file
14
lib/PngToBmpConverter/PngToBmpConverter.h
Normal file
@@ -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);
|
||||
};
|
||||
Reference in New Issue
Block a user