c++compressionpngzlib

Writing a png image from scratch in C++ after compression via zlib


My goal is to write a png image from a starting bitmap image. I know the existence of many libraries, but I need to write it from scratch.

The first function I have implemented is "applyNoneFilter"

uchar* applyNoneFilter(const uchar* input, int width, int height)
{
    uint8_t* output = new uchar[width * (height + 1)];
    for (int y = 0; y < height; y++)
    {
        output[y * (width + 1)] = 0;
        memcpy(output + y * (width + 1), input + y * width, width);
    }

    return output;
}

I pass the image data via img.ptr(), the raw data from an image loaded via OpenCV. The function just add a 0 before each scanlines, implementing the first step in png creation: filtering.

I pass the filtered output to the writeCompressedDataToPNG function I wrote:

void writeCompressedDataToPNG(const uint8_t* input, const std::string& filename, uint32_t width, uint32_t height) {
    std::ofstream outFile(filename, std::ios::binary);
    if (!outFile) {
        throw std::runtime_error("Failed to open file for writing");
    }

    // PNG Header
    const unsigned char pngHeader[8] = { '\211', 'P', 'N', 'G', '\r', '\n', '\032', '\n' };
    outFile.write(reinterpret_cast<const char*>(pngHeader), 8);

    // IHDR Chunk
    unsigned char ihdrChunk[25] = {
      0x00, 0x00, 0x00, 0x0D, // Length of IHDR data
      'I', 'H', 'D', 'R',
      0x00, 0x00, 0x00, 0x00, // Width placeholder
      0x00, 0x00, 0x00, 0x00, // Height placeholder
      0x08,                   // Bit depth: 8
      0x00,                   // Color type: 0 (grayscale)
      0x00,                   // Compression method: Deflate
      0x00,                   // Filter method: No filtering
      0x00,                   // Interlace method: 0
      0x00, 0x00, 0x00, 0x00  // CRC placeholder
    };

    intToBigEndian(width, &ihdrChunk[8]);
    intToBigEndian(height, &ihdrChunk[12]);

    uint32_t crc = crc32(0, ihdrChunk + 4, 17);
    intToBigEndian(crc, &ihdrChunk[21]);

    outFile.write(reinterpret_cast<const char*>(ihdrChunk), 25);


    // IDAT chunk
    uLongf compression_size = compressBound(width * (height + 1));
    std::vector<uchar> compressed(compression_size);
    int result = compress(compressed.data(), &compression_size, input, width * (height + 1));
    if (result != Z_OK)
    {
        std::cerr << "Compression failed" << std::endl;
        exit(-1);
    }

    compressed.resize(compression_size);
    uint32_t chunkLength = compressed.size();
    unsigned char chunkLengthBytes[4];
    intToBigEndian(chunkLength, chunkLengthBytes);

    outFile.write(reinterpret_cast<const char*>(chunkLengthBytes), 4);

    unsigned char chunkType[4] = { 'I', 'D', 'A', 'T' };
    outFile.write(reinterpret_cast<const char*>(chunkType), 4);

    uint32_t crc_idat = crc32(0, chunkType, 4); // Include "IDAT" chunk type

    outFile.write(reinterpret_cast<const char*>(compressed.data()), compressed.size());
    crc_idat = crc32(crc_idat, compressed.data(), compressed.size());

    unsigned char crcBytes[4];
    intToBigEndian(crc_idat, crcBytes);
    outFile.write(reinterpret_cast<const char*>(crcBytes), 4);

    // IEND Chunk
    const unsigned char iendChunk[12] = {
        0x00, 0x00, 0x00, 0x00,
        'I', 'E', 'N', 'D',
        0xAE, 0x42, 0x60, 0x82
    };
    outFile.write(reinterpret_cast<const char*>(iendChunk), 12);

    outFile.close();
}

The function intToBigEndian is this one, it convert an integer to a bigendian representation according to the png specifications.

void intToBigEndian(uint32_t value, unsigned char* buffer) {
    buffer[0] = (value >> 24) & 0xFF;
    buffer[1] = (value >> 16) & 0xFF;
    buffer[2] = (value >> 8) & 0xFF;
    buffer[3] = value & 0xFF;
}

It creates an image that I can correctly visualize in the Windows image visualizer, when I try to load it via code with OpenCV imread I got the error libpng error: bad adaptive filter value.

I understand that I messed up the IDAT chunk writing, in particular the filter. But I cannot see the problem. I have also checked the crc with png-file-chunk-inspector and everything appears to be fine. So, where is my error?


Solution

  • The problem stems from this line:

    memcpy(output + y * (width + 1), input + y * width, width);
    

    the corrected implementation of applyNoneFilter:

    uchar* applyNoneFilter(const uchar* input, int width, int height)
    {
        uint8_t* output = new uchar[height * (width + 1)];
        for (int y = 0; y < height; y++)
        {
            output[y * (width + 1)] = 0; // Set filter type to 0 for "None"
            memcpy(output + y * (width + 1) + 1, input + y * width, width);
            // Copy the scanline starting from the second byte of the output buffer
        }
        return output;
    }
    

    The first byte of each scanline in the filtered output must be the filter type (in this case, 0 for the None filter). In your original code, memcpy overwrites the 0 that was set for the filter type because you started copying at output + y * (width + 1), instead of output + y * (width + 1) + 1.