c++libpng

How to convert indexed 8 bit png image to 8 bit RGBA with libpng?


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:

enter image description here

When I run mentioned code, it says

$ ./plt
depth: 8
color: 3
size: 36x94

and I get just the transparent 32 bit rgba image:

yes it is an transparent image

If I toggle off the #if on line 24 (alpha channel adding), I get next one:

enter image description here

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?


Solution

  • 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