passage/minorGems/graphics/converters/PNGImageConverter.cpp
2025-10-03 02:19:59 -04:00

643 lines
15 KiB
C++

/*
* Modification History
*
* 2006-November-21 Jason Rohrer
* Created.
*
* 2010-May-18 Jason Rohrer
* String parameters as const to fix warnings.
*/
#include "PNGImageConverter.h"
#include "minorGems/util/SimpleVector.h"
#include "minorGems/graphics/RGBAImage.h"
//#include "lodepng.h"
//#include <zlib.h>
#include <png.h>
PNGImageConverter::PNGImageConverter( int inCompressionLevel )
: mCompressionLevel( inCompressionLevel ) {
// set up the CRC table
// code taken from the PNG spec:
// http://www.w3.org/TR/2003/REC-PNG-20031110/#D-CRCAppendix
unsigned long c;
int n, k;
for( n=0; n<256; n++ ) {
c = (unsigned long)n;
for( k=0; k<8; k++ ) {
if( c & 1 ) {
c = 0xedb88320L ^ (c >> 1);
}
else {
c = c >> 1;
}
}
mCRCTable[n] = c;
}
}
unsigned long PNGImageConverter::updateCRC(
unsigned long inCRC, unsigned char *inData, int inLength ) {
// code taken from the PNG spec:
// http://www.w3.org/TR/2003/REC-PNG-20031110/#D-CRCAppendix
unsigned long c = inCRC;
int n;
for( n=0; n<inLength; n++ ) {
c = mCRCTable[ (c ^ inData[n]) & 0xff ]
^
(c >> 8);
}
return c;
}
#define ADLER_BASE 65521 /* largest prime smaller than 65536 */
/**
* Updates an adler32 checksum.
* code found here http://www.ietf.org/rfc/rfc1950.txt
*
* New adlers should start with inAdler set to 1.
*
* @param inAdler the current state of the checksum.
* @param inData the data to add. Destroyed by caller.
* @param inLength the length of the data in bytes.
*
* @return the new checksum.
*/
unsigned long updateAdler32( unsigned long inAdler,
unsigned char *inData, int inLength ) {
unsigned long s1 = inAdler & 0xffff;
unsigned long s2 = (inAdler >> 16) & 0xffff;
int n;
for (n = 0; n < inLength; n++) {
s1 = (s1 + inData[n]) % ADLER_BASE;
s2 = (s2 + s1) % ADLER_BASE;
}
return (s2 << 16) + s1;
}
void PNGImageConverter::writeChunk(
const char inChunkType[4], unsigned char *inData,
unsigned long inNumBytes, OutputStream *inStream ) {
// chunk layout:
// 4-byte length
// 4-char type
// data
// 4-byte CRC (applied to type and data parts)
// write the length
writeBigEndianLong( inNumBytes, inStream );
inStream->write( (unsigned char *)inChunkType, 4 );
// start the crc
unsigned long crc = updateCRC( mStartCRC,
(unsigned char *)inChunkType, 4 );
if( inData != NULL ) {
// chunk has data
inStream->write( inData, inNumBytes );
crc = updateCRC( crc, inData, inNumBytes );
}
// final step: invert the CRC
crc = crc ^ 0xffffffffL;
// now write the CRC
writeBigEndianLong( crc, inStream );
}
// callbacks for libpng io
void libpngWriteCallback( png_structp png_ptr,
png_bytep data, png_size_t length ) {
// unpack our extra parameter
void *write_io_ptr = png_get_io_ptr( png_ptr );
OutputStream *inStream = (OutputStream *)write_io_ptr;
inStream->write( data, length );
}
void libpngFlushCallback( png_structp png_ptr ) {
// do nothing?
}
void PNGImageConverter::formatImage( Image *inImage,
OutputStream *inStream ) {
int numChannels = inImage->getNumChannels();
// make sure the image is in the right format
if( numChannels != 3 &&
numChannels != 4 ) {
printf( "Only 3- and 4-channel images can be converted to " );
printf( "the PNG format.\n" );
return;
}
int w = inImage->getWidth();
int h = inImage->getHeight();
//RGBAImage rgbaImage( inImage );
unsigned char *imageBytes = RGBAImage::getRGBABytes( inImage );
// libpng implementation
// adapted from this:
// http://zarb.org/~gc/html/libpng.html
// get pointers to rows
unsigned char **rows = new unsigned char *[h];
for( int y=0; y<h; y++ ) {
rows[y] = &( imageBytes[ y * (w * 4) ] );
}
png_structp png_ptr;
png_infop info_ptr;
// initialize structures
png_ptr = png_create_write_struct( PNG_LIBPNG_VER_STRING,
NULL, NULL, NULL );
if( !png_ptr ) {
delete [] imageBytes;
delete [] rows;
printf( "PNG Writing: png_create_write_struct failed\n" );
return;
}
png_set_compression_level( png_ptr, mCompressionLevel );
info_ptr = png_create_info_struct( png_ptr );
if( !info_ptr ) {
delete [] imageBytes;
delete [] rows;
png_destroy_write_struct( &png_ptr, NULL );
printf( "PNG Writing: png_create_info_struct failed\n" );
return;
}
// weird way that libpng handles errors with a jump
if( setjmp( png_jmpbuf( png_ptr ) ) ) {
delete [] imageBytes;
delete [] rows;
png_destroy_write_struct( &png_ptr, &info_ptr );
printf( "PNG Writing: error when setting writing funciton\n" );
return;
}
//png_init_io(png_ptr, fp);
// set our write callback (since we're not writing directly to file)
// pass the stream as the extra void* parameter
png_set_write_fn( png_ptr,
(void*)inStream,
libpngWriteCallback,
libpngFlushCallback );
// write header
if( setjmp( png_jmpbuf( png_ptr ) ) ) {
delete [] imageBytes;
delete [] rows;
png_destroy_write_struct( &png_ptr, &info_ptr );
printf( "PNG Writing: error writing header\n" );
return;
}
png_set_IHDR( png_ptr, info_ptr, w, h,
8, PNG_COLOR_TYPE_RGB_ALPHA, PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE );
png_write_info( png_ptr, info_ptr );
// write bytes
// write header
if( setjmp( png_jmpbuf( png_ptr ) ) ) {
delete [] imageBytes;
delete [] rows;
png_destroy_write_struct( &png_ptr, &info_ptr );
printf( "PNG Writing: error writing image bytes\n" );
return;
}
png_write_image( png_ptr, rows );
// end write
if( setjmp( png_jmpbuf( png_ptr ) ) ) {
delete [] imageBytes;
delete [] rows;
png_destroy_write_struct( &png_ptr, &info_ptr );
printf( "PNG Writing: error ending write\n" );
return;
}
png_write_end( png_ptr, NULL );
png_destroy_write_struct( &png_ptr, &info_ptr );
delete [] imageBytes;
delete [] rows;
if( true ) {
return;
}
/*
// LodePNG implementation
unsigned char *encodedBytes;
size_t encodedSize;
unsigned int error = LodePNG_encode32( &encodedBytes, &encodedSize,
imageBytes, w, h );
delete [] imageBytes;
if( error ) {
delete [] encodedBytes;
printf( "Error encoding PNG image\n" );
return;
}
inStream->write( encodedBytes, encodedSize );
delete [] encodedBytes;
if( true ) {
return;
}
*/
// same for all PNG images
// used to check for basic transmission errors, such as line-end flipping
unsigned char pngSignature[8] = { 0x89, 0x50, 0x4E, 0x47,
0x0D, 0x0A, 0x1A, 0x0A };
inStream->write( pngSignature, 8 );
// data for IHDR chunk
unsigned char headerData[13];
// width
headerData[0] = (w >> 24) & 0xff;
headerData[1] = (w >> 16) & 0xff;
headerData[2] = (w >> 8) & 0xff;
headerData[3] = w & 0xff;
// height
headerData[4] = (h >> 24) & 0xff;
headerData[5] = (h >> 16) & 0xff;
headerData[6] = (h >> 8) & 0xff;
headerData[7] = h & 0xff;
// bit depth
headerData[8] = 8;
// color type
// 2 = truecolor (RGB)
headerData[9] = 2;
// compression method
// method 0 (deflate)
headerData[10] = 0;
// filter method
// method 0 supports 5 filter types
headerData[11] = 0;
// no interlace
headerData[12] = 0;
writeChunk( "IHDR", headerData, 13, inStream );
int numRawBytes = w * h * 3;
// extra byte per scanline for filter type
numRawBytes += h;
unsigned char *rawScanlineBytes = new unsigned char[ numRawBytes ];
// ignore alpha channel
double *channels[3];
int i;
for( i=0; i<3; i++ ) {
channels[i] = inImage->getChannel( i );
}
int pixelNumber = 0;
for( int y=0; y<h; y++ ) {
// each scanline starts with filter type byte
// using filter type 0 (no filtering)
int index = y * ( w * 3 + 1 );
// filter type
rawScanlineBytes[ index ] = 0;
index++;
for( int x=0; x<w; x++ ) {
for( i=0; i<3; i++ ) {
rawScanlineBytes[ index ] =
(unsigned char)( 255 * channels[i][pixelNumber] );
index ++;
}
pixelNumber++;
}
}
// zlib block contains
// zlib compression method (1 byte)
// flags (1 byte)
// compressed data blocks
// check value (4 bytes) (not CRC)
SimpleVector<unsigned char> zlibBlock;
// compression method 8 (deflate)
// with a LZ77 window size parameter of w=7
// LZ77 window size is then 2^( w + 8 ), or in this case 32768
zlibBlock.push_back( 0x78 );
// flags
// compression level 0 (2 bytes = 00b)
// no preset dictionary (1 byte = 0b)
// check bits for compression method (5 bits)
// Should be such that if the 8-bit compression method, followed
// by the 8-bit flags field, is viewed as a 16-bit number,
// it is an even multiple of 31
// For our settings, check bits of 00001 works
//zlibBlock.push_back( 0x01 );
// hack: mimic zlib here
zlibBlock.push_back( 0xda );
// now ready for compressed data blocks
int rawDataIndex = 0;
// length field is 16 bits
int maxUncompressedBlockLength = 65535;
while( rawDataIndex < numRawBytes ) {
// push another deflate block
// first bit BFINAL
// only 1 for final block
// next two bits BTYPE
// BTYPE=00 is an uncompressed block
// remaining 5 bits ignored
// Thus, we put 0x00 for uncompressed blocks that are not final
// and 0x80 for final uncompressed block
int bytesLeft = numRawBytes - rawDataIndex;
int bytesInBlock;
if( bytesLeft <= maxUncompressedBlockLength ) {
// final
// hack: when comparing output with zlib, noticed that it doesn't
// set 0x80 for the final block, instead it uses 0x01
// For some reason, this was making the PNG unreadable
// zlibBlock.push_back( 0x80 );
zlibBlock.push_back( 0x01 );
bytesInBlock = bytesLeft;
}
else {
// not final
zlibBlock.push_back( 0x00 );
bytesInBlock = maxUncompressedBlockLength;
}
// length in least-significant-byte-first order
unsigned char firstLengthByte = bytesInBlock & 0xff;
unsigned char secondLengthByte = (bytesInBlock >> 8) & 0xff;
zlibBlock.push_back( firstLengthByte );
zlibBlock.push_back( secondLengthByte );
// those same length bytes inverted
// (called "one's compliment" in the spec
zlibBlock.push_back( firstLengthByte ^ 0xff);
zlibBlock.push_back( secondLengthByte ^ 0xff );
// now the uncompressed data
for( int b=0; b< bytesInBlock; b++ ) {
zlibBlock.push_back( rawScanlineBytes[ rawDataIndex ] );
rawDataIndex++;
}
}
// finally, adler32 of original data
unsigned long adler = updateAdler32( 1L, rawScanlineBytes, numRawBytes );
zlibBlock.push_back( (adler >> 24) & 0xff );
zlibBlock.push_back( (adler >> 16) & 0xff );
zlibBlock.push_back( (adler >> 8) & 0xff );
zlibBlock.push_back( adler & 0xff );
// the zlib block is now complete
/*
// check against real zlib implementation
z_stream zStream;
zStream.next_in = rawScanlineBytes;
zStream.avail_in = numRawBytes;
zStream.total_in = 0;
int outSize = 2 * numRawBytes + 100;
unsigned char *zlibOutBuffer = new unsigned char[ outSize ];
zStream.next_out = zlibOutBuffer;
zStream.avail_out = outSize;
zStream.total_out = 0;
zStream.data_type = Z_BINARY;
zStream.zalloc = Z_NULL;
zStream.zfree = Z_NULL;
// init the stream
// no compression
int result;
//result = deflateInit( &zStream, Z_DEFAULT_COMPRESSION);
result = deflateInit( &zStream, Z_NO_COMPRESSION);
if( result != Z_OK ) {
printf( "zlib deflateInit error: %s\n", zStream.msg );
}
// deflate and flush
result = deflate( &zStream, Z_FINISH );
if( result != Z_STREAM_END ) {
printf( "zlib deflate error (%d): %s\n", result, zStream.msg );
}
printf( "Total in = %d, total out = %d\n",
zStream.total_in, zStream.total_out );
printf( "Our raw bytes (%d):\n", numRawBytes );
int b;
for( b=0; b<numRawBytes; b++ ) {
printf( "%02x ", rawScanlineBytes[ b ] );
}
printf( "\n\nOur zlib bytes (%d) (adler = %d) :\n",
zlibBlock.size(), adler );
for( b=0; b<zlibBlock.size(); b++ ) {
printf( "%02x ", *( zlibBlock.getElement( b ) ) );
}
printf( "\n\nTheir zlib bytes (%d) (adler = %d) :\n",
(int)( zStream.total_out ), zStream.adler );
for( b=0; b<zStream.total_out; b++ ) {
printf( "%02x ", zlibOutBuffer[ b ] );
}
int minBytes = zStream.total_out;
if( minBytes > zlibBlock.size() ) {
minBytes = zlibBlock.size();
}
for( b=0; b<minBytes; b++ ) {
if( zlibOutBuffer[b] != *( zlibBlock.getElement( b ) ) ) {
printf( "mismatch at byte %d\n", b );
printf( "Theirs: %02x, ours: %02x\n", zlibOutBuffer[b],
*( zlibBlock.getElement( b ) ) );
}
}
printf( "\n\n" );
delete [] zlibOutBuffer;
result = deflateEnd( &zStream );
if( result != Z_OK ) {
printf( "zlib deflateEnd error (%d): %s\n", result, zStream.msg );
}
*/
// the zlib block is the data of an IDAT chunk
unsigned char *zlibBytes = zlibBlock.getElementArray();
writeChunk( "IDAT", zlibBytes, zlibBlock.size(), inStream );
delete [] zlibBytes;
// no data in end chunk
writeChunk( "IEND", NULL, 0, inStream );
delete [] rawScanlineBytes;
// done
}
Image *PNGImageConverter::deformatImage( InputStream *inStream ) {
printf( "ERROR: reading PNG images is not supported by "
"PNGImageConverter\n" );
return NULL;
}