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?
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
.