c++ - How to convert indexed 8 bit png image to 8 bit RGBA with libpng? - Stack Overflow

admin2025-04-29  2

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?

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?

Share Improve this question edited Jan 9 at 4:53 Vladyslav Rehan asked Jan 7 at 1:01 Vladyslav RehanVladyslav Rehan 2032 silver badges8 bronze badges 3
  • 2 The code is written in C++, not C. Tag the language you are using. – PaulMcKenzie Commented Jan 7 at 1:13
  • @JaMiT I have changed the textual details accordingly. – Vladyslav Rehan Commented Jan 7 at 10:39
  • Questions are for questions, not answers. If you want to post an answer, you may do so, but as an answer, not edited into the question. – JaMiT Commented Jan 9 at 3:26
Add a comment  | 

2 Answers 2

Reset to default 4

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 simple convert_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

So after Craig updated his answer, I have understood what is up. It is kinda shameful... but I have been using 1 << 16 as my filler byte in png_set_add_alpha, the 1 << 16 is equal to 65536, which in binary is 000000001 00000000 00000000. Note the last 2 null bytes. The png_set_add_alpha takes the png_uint32 as filler byte, but anyway, the maximum depth of alpha channel is 16 (2 bytes), and exactly 2 of these bytes were 0. Yes, because I did the 1 << 16 and for some reason thought that I will get 0xFF. So libpng understood it as convert paletted image to rgb and set its alpha to 0 so it will be transparent. What a shame.

Craig used 0xFE in his answer, it should be 0xFF (larger by 1) for maximum opacity, anyway.

转载请注明原文地址:http://anycun.com/QandA/1745936129a91351.html