I am writing program with C++ that manipulates PNG Files. I am trying to convert any input PNG image to 8 or 16bit RGBA, depending on input images depth. But let's just stick to 8bit for now.
So I have made test indexed image in GIMP, and been trying to convert it to 8bit RGBA for hours without success. Here is the code:
#include <iostream>
#include <cstdio>
#include <png.h>
int main(int argc, char** argv) {
// reading
png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
png_infop info = png_create_info_struct(png);
std::FILE* fp = std::fopen("paletted.png", "rb");
png_init_io(png, fp);
png_read_info(png, info);
png_uint_32 width, height;
int depth, color;
png_get_IHDR(png, info, &width, &height, &depth, &color, nullptr, nullptr, nullptr);
std::cout << "depth: " << depth << "\ncolor: "
<< color << "\nsize: " << width << 'x' << height << std::endl;
png_set_expand(png); // makes rgb from indexed
#if 1 // input image does not have tRNS but anyway
if (png_get_valid(png, info, PNG_INFO_tRNS))
png_set_tRNS_to_alpha(png);
else {
std::cout << "No tRNS chunk, adding alpha chanel\n";
png_set_add_alpha(png, 1 << 16, PNG_FILLER_AFTER); // now it should be rgba
}
#endif
png_read_update_info(png, info);
png_bytepp rows = new png_bytep[height];
for (int i = 0; i < height; i++)
rows[i] = new png_byte[width*4]; // multiplying by 4 because rgba is 4 bytes per pixel
png_read_image(png, rows);
png_read_end(png, nullptr);
std::fclose(fp);
png_destroy_read_struct(&png, &info, nullptr);
// writting
#define OPUTPUT_TYPE PNG_COLOR_TYPE_RGBA // try plain rgb
png = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
info = png_create_info_struct(png);
png_set_IHDR(
png, info, width, height, 8, OPUTPUT_TYPE,
PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT,
PNG_FILTER_TYPE_DEFAULT
);
fp = std::fopen("paletted1.png", "wb");
png_init_io(png, fp);
png_write_info(png, info);
png_write_image(png, rows);
png_write_end(png, nullptr);
png_destroy_write_struct(&png, &info);
for (int i = 0; i < height; i++)
delete[] rows[i];
delete[] rows;
return 0;
}
And a CMake solution:
cmake_minimum_required(VERSION 3.25)
project(pngpaletted LANGUAGES CXX)
find_package(PNG REQUIRED)
add_executable(plt main.cpp)
target_link_libraries(plt PNG::PNG)
Here is an input image:
When I run mentioned code, it says
$ ./plt
depth: 8
color: 3
size: 36x94
and I get just the transparent 32 bit rgba image:
If I toggle off the #if
on line 24 (alpha channel adding), I get next one:
Seems like there is an rgb image written to rgba buffer.
Mkay, lets then change OUTPUT_TYPE
on line 47 to PNG_COLOR_TYPE_RGB
. Resultant image looks just like input one but in 8bit rgb (no alpha channel!).
Seems like libpng just erases data from png_set_expand
after the png_set_add_alpha
is called (or set, if you like).
What to do? How to expand paletted images to 8 or 16 bit RGBA with standard libpng routines?
The issue was the difference between an RGB pixel (3 bytes / pixel) image and an RGBA pixel (4 bytes / pixel).
When I converted the output paletted1.png
to a .ppm
and dumped it with a hex editor, the colors had sporadic spacing.
It helps to dump the row/column buffer(s) in hex at each stage [so I added that to the code below].
After reading in the image the (redacted) buffer is RGB format and looks like:
0: 99/00/00 99/00/00 99/00/00 99/00/00 99/00/00 99/00/00 99/00/00 99/00/00
99/00/00 99/00/00 99/00/00 99/00/00 99/00/00 99/00/00 99/00/00 99/00/00
99/00/00 99/00/00 99/00/00 99/00/00 99/00/00 99/00/00 99/00/00 99/00/00
99/00/00 99/00/00 99/00/00 99/00/00 99/00/00 99/00/00 99/00/00 99/00/00
99/00/00 99/00/00 99/00/00 99/00/00
....
7: 99/00/00 99/00/00 99/00/00 99/00/00 99/00/00 99/00/00 99/00/00 99/00/00
99/00/00 99/00/00 99/00/00 99/00/00 99/00/00 99/00/00 99/00/00 99/00/00
00/33/FF 00/33/FF 00/33/FF 99/00/00 99/00/00 99/00/00 99/00/00 99/00/00
99/00/00 99/00/00 99/00/00 99/00/00 99/00/00 99/00/00 99/00/00 99/00/00
99/00/00 99/00/00 99/00/00 99/00/00
It must be converted into RGBA format:
0: 99/00/00/FF 99/00/00/FF 99/00/00/FF 99/00/00/FF 99/00/00/FF 99/00/00/FF
99/00/00/FF 99/00/00/FF 99/00/00/FF 99/00/00/FF 99/00/00/FF 99/00/00/FF
99/00/00/FF 99/00/00/FF 99/00/00/FF 99/00/00/FF 99/00/00/FF 99/00/00/FF
99/00/00/FF 99/00/00/FF 99/00/00/FF 99/00/00/FF 99/00/00/FF 99/00/00/FF
99/00/00/FF 99/00/00/FF 99/00/00/FF 99/00/00/FF 99/00/00/FF 99/00/00/FF
99/00/00/FF 99/00/00/FF 99/00/00/FF 99/00/00/FF 99/00/00/FF 99/00/00/FF
....
7: 99/00/00/FF 99/00/00/FF 99/00/00/FF 99/00/00/FF 99/00/00/FF 99/00/00/FF
99/00/00/FF 99/00/00/FF 99/00/00/FF 99/00/00/FF 99/00/00/FF 99/00/00/FF
99/00/00/FF 99/00/00/FF 99/00/00/FF 99/00/00/FF 00/33/FF/FF 00/33/FF/FF
00/33/FF/FF 99/00/00/FF 99/00/00/FF 99/00/00/FF 99/00/00/FF 99/00/00/FF
99/00/00/FF 99/00/00/FF 99/00/00/FF 99/00/00/FF 99/00/00/FF 99/00/00/FF
99/00/00/FF 99/00/00/FF 99/00/00/FF 99/00/00/FF 99/00/00/FF 99/00/00/FF
The following was part of my original answer:
I don't know if there is a
libpng
function to do such conversion. And, if there is, can it do the conversion in-place or does it need two buffers? So, I opted to create a second buffer and transform the pixels into that using a simpleconvert_image
function that I wrote.
Edit: After some additional investigation, the TL;DR is: use png_set_add_alpha
. I found this [somewhat] by looking in the libpng
manual, but, ironically, scrolling through png.h
was more useful. I've updated the code below to use that function.
With that change, here is the changed first dump:
0: 99/00/00/FE 99/00/00/FE 99/00/00/FE 99/00/00/FE 99/00/00/FE 99/00/00/FE
99/00/00/FE 99/00/00/FE 99/00/00/FE 99/00/00/FE 99/00/00/FE 99/00/00/FE
99/00/00/FE 99/00/00/FE 99/00/00/FE 99/00/00/FE 99/00/00/FE 99/00/00/FE
99/00/00/FE 99/00/00/FE 99/00/00/FE 99/00/00/FE 99/00/00/FE 99/00/00/FE
99/00/00/FE 99/00/00/FE 99/00/00/FE 99/00/00/FE 99/00/00/FE 99/00/00/FE
99/00/00/FE 99/00/00/FE 99/00/00/FE 99/00/00/FE 99/00/00/FE 99/00/00/FE
....
7: 99/00/00/FE 99/00/00/FE 99/00/00/FE 99/00/00/FE 99/00/00/FE 99/00/00/FE
99/00/00/FE 99/00/00/FE 99/00/00/FE 99/00/00/FE 99/00/00/FE 99/00/00/FE
99/00/00/FE 99/00/00/FE 99/00/00/FE 99/00/00/FE 00/33/FF/FE 00/33/FF/FE
00/33/FF/FE 99/00/00/FE 99/00/00/FE 99/00/00/FE 99/00/00/FE 99/00/00/FE
99/00/00/FE 99/00/00/FE 99/00/00/FE 99/00/00/FE 99/00/00/FE 99/00/00/FE
99/00/00/FE 99/00/00/FE 99/00/00/FE 99/00/00/FE 99/00/00/FE 99/00/00/FE
Here is the refactored code. It produces a correct RGBA image file. It now uses png_set_add_alpha
by default. To use my original convert_image
compile with -DOLD_CONVERT
#include <iostream>
#include <cstdio>
#include <png.h>
void
dump_image(png_bytepp image,png_uint_32 width,png_uint_32 height,int bpp)
{
for (png_uint_32 y = 0; y < height; ++y) {
png_bytep row = image[y];
int len = printf("%4d:",y);
for (png_uint_32 x = 0; x < width; ++x) {
for (int color = 0; color < bpp; ++color)
len += printf("%c%2.2X",(color == 0) ? ' ' : '/',*row++);
if (len >= 70) {
printf("\n");
len = printf(" ");
}
}
printf("\n");
}
}
png_bytepp
new_buffer(png_uint_32 width,png_uint_32 height)
{
png_bytepp rows = new png_bytep[height];
// multiplying by 4 because rgba is 4 bytes per pixel
for (int i = 0; i < height; i++)
rows[i] = new png_byte[width * 4];
return rows;
}
void
convert_image(png_bytepp img4,png_bytepp img3,
png_uint_32 width,png_uint_32 height)
{
for (png_uint_32 y = 0; y < height; ++y) {
png_bytep row4 = img4[y];
png_bytep row3 = img3[y];
for (png_uint_32 x = 0; x < width; ++x) {
row4[0] = row3[0];
row4[1] = row3[1];
row4[2] = row3[2];
row4[3] = 0xFF;
row3 += 3;
row4 += 4;
}
}
}
#define OUTPUT_TYPE PNG_COLOR_TYPE_RGBA // try plain rgb
int
main(int argc, char **argv)
{
// reading
png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING,
nullptr, nullptr, nullptr);
png_infop info = png_create_info_struct(png);
std::FILE * fp = std::fopen("paletted.png", "rb");
png_init_io(png, fp);
png_read_info(png, info);
png_uint_32 width, height;
int depth, color;
png_get_IHDR(png, info, &width, &height, &depth, &color,
nullptr, nullptr, nullptr);
std::cout << "depth: " << depth << "\n";
std::cout << "color: " << color << "\n";
std::cout << "size: " << width << 'x' << height << std::endl;
png_set_expand(png); // makes rgb from indexed
#if 0
// input image does not have tRNS but anyway
if (png_get_valid(png, info, PNG_INFO_tRNS))
png_set_tRNS_to_alpha(png);
else {
std::cout << "No tRNS chunk, adding alpha chanel\n";
// now it should be rgba
png_set_add_alpha(png, 1 << 16, PNG_FILLER_AFTER);
}
#endif
// have libpng add alpha to input while reading
// NOTE: 0xFE is just to make it more distinctive in the dump
#if OLD_CONVERT == 0
png_set_add_alpha(png,0xFE,PNG_FILLER_AFTER);
#endif
png_read_update_info(png, info);
png_bytepp rows = new_buffer(width,height);
png_read_image(png, rows);
png_read_end(png, nullptr);
#if OLD_CONVERT
dump_image(rows, width, height, 3);
#else
dump_image(rows, width, height, 4);
#endif
std::fclose(fp);
png_destroy_read_struct(&png, &info, nullptr);
#if OLD_CONVERT
png_bytepp row4 = new_buffer(width,height);
convert_image(row4,rows,width,height);
dump_image(row4, width, height, 4);
#endif
// writing
png = png_create_write_struct(PNG_LIBPNG_VER_STRING,
nullptr, nullptr, nullptr);
#if 0
png_set_expand(png); // makes rgb from indexed
#endif
info = png_create_info_struct(png);
png_set_IHDR(png, info, width, height, 8, OUTPUT_TYPE, PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
fp = std::fopen("paletted1.png", "wb");
png_init_io(png, fp);
// input image does not have tRNS but anyway
#if 0
if (png_get_valid(png, info, PNG_INFO_tRNS))
png_set_tRNS_to_alpha(png);
else {
std::cout << "No tRNS chunk, adding alpha chanel\n";
png_set_add_alpha(png, 1 << 16, PNG_FILLER_AFTER); // now it should be rgba
}
#endif
png_write_info(png, info);
#if OLD_CONVERT
png_write_image(png, row4);
#else
png_write_image(png, rows);
#endif
png_write_end(png, nullptr);
png_destroy_write_struct(&png, &info);
for (int i = 0; i < height; i++)
delete[]rows[i];
delete[]rows;
return 0;
}
In the code above, I've used cpp
conditionals to denote old vs. new code:
#if 0
// old code
#else
// new code
#endif
#if 1
// new code
#endif
Note: this can be cleaned up by running the file through unifdef -k