fix: handle PNG alpha channel by blending with white background

Transparent pixels in PNGs were being rendered incorrectly because alpha
was ignored. This fix alpha-blends all pixel types with white (255) for
proper e-ink display:
- Indexed PNGs with tRNS chunk (alpha at palette[768+index])
- RGBA PNGs (alpha from 4th byte)
- Gray+Alpha PNGs (alpha from 2nd byte)
This commit is contained in:
Martin Brook 2026-02-03 15:30:33 +00:00
parent a2d32640f2
commit 4b1b4fb6b3

View File

@ -95,8 +95,9 @@ bool PngToFramebufferConverter::getDimensionsStatic(const std::string& imagePath
return true; return true;
} }
// Helper to get grayscale from PNG pixel data // Helper to get grayscale from PNG pixel data, with alpha blending to white background.
static uint8_t getGrayFromPixel(uint8_t* pPixels, int x, int pixelType, uint8_t* palette) { // For indexed PNGs with tRNS chunk, alpha values are stored at palette[768] onwards.
static uint8_t getGrayFromPixel(uint8_t* pPixels, int x, int pixelType, uint8_t* palette, int hasAlpha) {
switch (pixelType) { switch (pixelType) {
case PNG_PIXEL_GRAYSCALE: case PNG_PIXEL_GRAYSCALE:
return pPixels[x]; return pPixels[x];
@ -110,17 +111,28 @@ static uint8_t getGrayFromPixel(uint8_t* pPixels, int x, int pixelType, uint8_t*
uint8_t paletteIndex = pPixels[x]; uint8_t paletteIndex = pPixels[x];
if (palette) { if (palette) {
uint8_t* p = &palette[paletteIndex * 3]; uint8_t* p = &palette[paletteIndex * 3];
return (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8); uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
// Alpha values for indexed PNGs are stored after RGB data (at offset 768)
if (hasAlpha) {
uint8_t alpha = palette[768 + paletteIndex];
return (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
}
return gray;
} }
return paletteIndex; return paletteIndex;
} }
case PNG_PIXEL_GRAY_ALPHA: case PNG_PIXEL_GRAY_ALPHA: {
return pPixels[x * 2]; uint8_t gray = pPixels[x * 2];
uint8_t alpha = pPixels[x * 2 + 1];
return (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
}
case PNG_PIXEL_TRUECOLOR_ALPHA: { case PNG_PIXEL_TRUECOLOR_ALPHA: {
uint8_t* p = &pPixels[x * 4]; uint8_t* p = &pPixels[x * 4];
return (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8); uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
uint8_t alpha = p[3];
return (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
} }
default: default:
@ -158,7 +170,7 @@ int pngDrawCallback(PNGDRAW* pDraw) {
int srcX = (int)(dstX / ctx->scale); int srcX = (int)(dstX / ctx->scale);
if (srcX >= ctx->srcWidth) srcX = ctx->srcWidth - 1; if (srcX >= ctx->srcWidth) srcX = ctx->srcWidth - 1;
uint8_t gray = getGrayFromPixel(pPixels, srcX, pixelType, pDraw->pPalette); uint8_t gray = getGrayFromPixel(pPixels, srcX, pixelType, pDraw->pPalette, pDraw->iHasAlpha);
uint8_t ditheredGray; uint8_t ditheredGray;
if (ctx->config->useDithering) { if (ctx->config->useDithering) {
@ -215,10 +227,6 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
warnUnsupportedFeature("bit depth (" + std::to_string(png.getBpp()) + "bpp)", imagePath); warnUnsupportedFeature("bit depth (" + std::to_string(png.getBpp()) + "bpp)", imagePath);
} }
if (png.hasAlpha()) {
warnUnsupportedFeature("alpha channel", imagePath);
}
// Allocate cache buffer using SCALED dimensions // Allocate cache buffer using SCALED dimensions
ctx.caching = !config.cachePath.empty(); ctx.caching = !config.cachePath.empty();
if (ctx.caching) { if (ctx.caching) {