imagecore/formats/internal/png.cpp (438 lines of code) (raw):
/*
* MIT License
*
* Copyright (c) 2017 Twitter
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include "png.h"
#include "imagecore/utils/mathutils.h"
#include "imagecore/utils/securemath.h"
#include "imagecore/image/interleaved.h"
#include "libpng16/png.h"
#include <stdlib.h>
namespace imagecore {
REGISTER_IMAGE_READER(ImageReaderPNG);
REGISTER_IMAGE_WRITER(ImageWriterPNG);
void pngRead(png_structp png_ptr, png_bytep outBuffer, png_size_t numBytes)
{
ImageReader::Storage* io = (ImageReader::Storage*)png_get_io_ptr(png_ptr);
uint64_t bytesRead = io->read(outBuffer, (unsigned int)numBytes);
if( bytesRead != numBytes ) {
png_error(png_ptr, "EOF");
return;
}
}
bool ImageReaderPNG::Factory::matchesSignature(const uint8_t* sig, unsigned int sigLen)
{
if( png_sig_cmp(sig, 0, sigLen) == 0 ) {
return true;
}
return false;
}
ImageReaderPNG::ImageReaderPNG()
: m_Source(NULL)
, m_Width(0)
, m_Height(0)
, m_TotalRowsRead(0)
, m_NativeColorModel(kColorModel_RGBX)
, m_PNGDecompress(NULL)
, m_PNGInfo(NULL)
{
m_PNGDecompress = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
m_PNGInfo = png_create_info_struct(m_PNGDecompress);
}
ImageReaderPNG::~ImageReaderPNG()
{
if( m_PNGDecompress != NULL ) {
png_destroy_read_struct(&m_PNGDecompress, &m_PNGInfo, NULL);
m_PNGDecompress = NULL;
m_PNGInfo = NULL;
}
}
bool ImageReaderPNG::initWithStorage(Storage* source)
{
m_Source = source;
return true;
}
bool ImageReaderPNG::readHeader()
{
if( setjmp(png_jmpbuf(m_PNGDecompress)) ) {
return false;
}
png_set_read_fn(m_PNGDecompress, m_Source, pngRead);
png_read_info(m_PNGDecompress, m_PNGInfo);
m_Width = png_get_image_width(m_PNGDecompress, m_PNGInfo);
m_Height = png_get_image_height(m_PNGDecompress, m_PNGInfo);
unsigned int colorType = png_get_color_type(m_PNGDecompress, m_PNGInfo);
if( colorType == PNG_COLOR_TYPE_GRAY ) {
// Gray only, not gray with alpha.
m_NativeColorModel = kColorModel_Grayscale;
} else if( colorType == PNG_COLOR_TYPE_RGB_ALPHA || colorType == PNG_COLOR_TYPE_GRAY_ALPHA || png_get_valid(m_PNGDecompress, m_PNGInfo, PNG_INFO_tRNS) ) {
m_NativeColorModel = kColorModel_RGBA;
} else {
m_NativeColorModel = kColorModel_RGBX;
}
return true;
}
bool ImageReaderPNG::beginRead(unsigned int outputWidth, unsigned int outputHeight, EImageColorModel outputColorModel)
{
if( !supportsOutputColorModel(outputColorModel) ) {
return false;
}
if( outputWidth != m_Width || outputHeight != m_Height ) {
return false;
}
if( setjmp(png_jmpbuf(m_PNGDecompress)) ) {
return false;
}
unsigned int color_type = png_get_color_type(m_PNGDecompress, m_PNGInfo);
unsigned int bit_depth = png_get_bit_depth(m_PNGDecompress, m_PNGInfo);
if( bit_depth == 16 ) {
png_set_strip_16(m_PNGDecompress);
}
if( color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8 ) {
png_set_expand_gray_1_2_4_to_8(m_PNGDecompress);
}
if( Image::colorModelIsRGBA(outputColorModel) ) {
if( color_type == PNG_COLOR_TYPE_PALETTE ) {
png_set_palette_to_rgb(m_PNGDecompress);
}
if( png_get_valid(m_PNGDecompress, m_PNGInfo, PNG_INFO_tRNS) ) {
png_set_tRNS_to_alpha(m_PNGDecompress);
} else if( color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_PALETTE ) {
png_set_filler(m_PNGDecompress, 255, PNG_FILLER_AFTER);
}
if( color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA ) {
png_set_gray_to_rgb(m_PNGDecompress);
}
}
png_read_update_info(m_PNGDecompress, m_PNGInfo);
return true;
}
unsigned int ImageReaderPNG::readRows(Image* dest, unsigned int destRow, unsigned int numRows)
{
if( !supportsOutputColorModel(dest->getColorModel()) ) {
return false;
}
ImageInterleaved* destImage = dest->asInterleaved();
if( png_get_interlace_type(m_PNGDecompress, m_PNGInfo) != PNG_INTERLACE_NONE ) {
// We don't support sequential reading of interlaced PNGs.
return false;
}
if( setjmp(png_jmpbuf(m_PNGDecompress)) ) {
return false;
}
unsigned int destPitch = 0;
uint8_t* destBuffer = destImage->lockRect(m_Width, numRows, destPitch);
SECURE_ASSERT(!Image::colorModelIsRGBA(destImage->getColorModel()) || destImage->getComponentSize() == 4);
SECURE_ASSERT(!Image::colorModelIsGrayscale(destImage->getColorModel()) || destImage->getComponentSize() == 1);
SECURE_ASSERT(SafeUMul(m_Width, destImage->getComponentSize()) <= destPitch);
SECURE_ASSERT(destBuffer && destPitch);
unsigned int destHeight = destImage->getHeight();
SECURE_ASSERT(destRow + numRows <= destHeight);
unsigned int allocSize = SafeUMul((unsigned int)sizeof(png_bytep), numRows);
png_bytep* rowPointers = (png_bytep*)malloc(allocSize);
if( rowPointers == NULL ) {
return false;
}
// libpng asks for an array of scanline pointers, into each of which it'll write width * size bytes.
unsigned int finalRow = destRow + numRows;
for( unsigned int y = destRow; y < finalRow; y++ ) {
rowPointers[y] = destBuffer + destPitch * y;
}
png_read_rows(m_PNGDecompress, rowPointers, NULL, numRows);
m_TotalRowsRead += numRows;
return numRows;
}
bool ImageReaderPNG::endRead()
{
if( setjmp(png_jmpbuf(m_PNGDecompress)) ) {
return false;
}
if( m_TotalRowsRead == m_Height ) {
png_read_end(m_PNGDecompress, m_PNGInfo);
}
png_destroy_read_struct(&m_PNGDecompress, &m_PNGInfo, NULL);
return true;
}
bool ImageReaderPNG::readImage(Image* destImage)
{
if( !supportsOutputColorModel(destImage->getColorModel()) ) {
return false;
}
if( !beginRead(m_Width, m_Height, destImage->getColorModel()) ) {
return false;
}
unsigned int destHeight = destImage->getHeight();
unsigned int allocSize = SafeUMul((unsigned int)sizeof(png_bytep), destHeight);
png_bytep* rowPointers = (png_bytep*)malloc(allocSize);
if( rowPointers == NULL ) {
return false;
}
if( setjmp(png_jmpbuf(m_PNGDecompress)) ) {
free(rowPointers);
return false;
}
ImageInterleaved* image = destImage->asInterleaved();
// Security checks - make sure we don't exceed the dest capacity, and that nothing has somehow changed since we read the header.
SECURE_ASSERT(destImage->getWidth() == m_Width && destImage->getHeight() == m_Height);
SECURE_ASSERT(m_Width == png_get_image_width(m_PNGDecompress, m_PNGInfo) && m_Height == png_get_image_height(m_PNGDecompress, m_PNGInfo));
SECURE_ASSERT(!Image::colorModelIsRGBA(destImage->getColorModel()) || image->getComponentSize() == 4);
SECURE_ASSERT(!Image::colorModelIsGrayscale(destImage->getColorModel()) || image->getComponentSize() == 1);
unsigned int destPitch = 0;
uint8_t* destBuffer = image->lockRect(m_Width, m_Height, destPitch);
SECURE_ASSERT(SafeUMul(m_Width, image->getComponentSize()) <= destPitch);
SECURE_ASSERT(destBuffer && destPitch);
// libpng asks for an array of scanline pointers, into each of which it'll write width * components * depth bytes.
for( unsigned int y = 0; y < destHeight; y++ ) {
rowPointers[y] = destBuffer + y * destPitch;
}
png_read_image(m_PNGDecompress, rowPointers);
image->unlockRect();
free(rowPointers);
rowPointers = NULL;
m_TotalRowsRead += m_Height;
if( !endRead() ) {
return false;
}
return true;
}
EImageFormat ImageReaderPNG::getFormat()
{
return kImageFormat_PNG;
}
const char* ImageReaderPNG::getFormatName()
{
return "PNG";
}
unsigned int ImageReaderPNG::getWidth()
{
return m_Width;
}
unsigned int ImageReaderPNG::getHeight()
{
return m_Height;
}
EImageColorModel ImageReaderPNG::getNativeColorModel()
{
return m_NativeColorModel;
}
bool ImageReaderPNG::supportsOutputColorModel(EImageColorModel colorModel)
{
return Image::colorModelIsRGBA(colorModel) || colorModel == m_NativeColorModel;
}
////
bool ImageWriterPNG::Factory::matchesExtension(const char *extension)
{
return strcasecmp(extension, "png") == 0;
}
EImageFormat ImageWriterPNG::Factory::getFormat()
{
return kImageFormat_PNG;
}
bool ImageWriterPNG::Factory::appropriateForInputFormat(EImageFormat inputFormat)
{
return true;
}
bool ImageWriterPNG::Factory::supportsInputColorModel(EImageColorModel colorModel)
{
return Image::colorModelIsGrayscale(colorModel) || Image::colorModelIsRGBA(colorModel);
}
ImageWriterPNG::ImageWriterPNG()
: m_SourceReader(NULL)
, m_WriteOptions(0)
{
}
ImageWriterPNG::~ImageWriterPNG()
{
}
static void png_write_data(png_structp png_ptr, png_bytep data, png_size_t length)
{
ImageWriter::Storage* storage = (ImageWriter::Storage*) png_get_io_ptr(png_ptr);
storage->write(data, (unsigned int) length);
}
static void png_flush(png_structp png_ptr)
{
ImageWriter::Storage* storage = (ImageWriter::Storage*) png_get_io_ptr(png_ptr);
storage->flush();
}
bool ImageWriterPNG::initWithStorage(ImageWriter::Storage* output)
{
m_PNGCompress = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
m_PNGInfo = png_create_info_struct(m_PNGCompress);
if( setjmp(png_jmpbuf(m_PNGCompress)) ) {
return false;
}
png_set_write_fn(m_PNGCompress, output, png_write_data, png_flush);
return true;
}
void ImageWriterPNG::setSourceReader(ImageReader* hintReader)
{
m_SourceReader = hintReader;
}
void ImageWriterPNG::setWriteOptions(unsigned int options)
{
m_WriteOptions |= options;
}
bool ImageWriterPNG::copyLossless(ImageReader* reader)
{
if( reader->getFormat() != EImageFormat::kImageFormat_PNG ) {
bool ret = ImageWriter::copyLossless(reader); // call baseclass for non png->png cleans
if (!ret) {
png_destroy_write_struct(&m_PNGCompress, &m_PNGInfo);
}
return ret;
}
ImageReaderPNG* readerPNG = (ImageReaderPNG*)reader;
if( setjmp(png_jmpbuf(readerPNG->m_PNGDecompress)) ) {
png_destroy_read_struct(&readerPNG->m_PNGDecompress, &readerPNG->m_PNGInfo, NULL);
png_destroy_write_struct(&m_PNGCompress, &m_PNGInfo);
return false;
}
uint32_t pitch = (uint32_t)png_get_rowbytes(readerPNG->m_PNGDecompress, readerPNG->m_PNGInfo);
int32_t width;
int32_t height;
int32_t bitDepth;
int32_t colorType;
int32_t interlace;
int32_t compressionType;
int32_t filterMethod; // this is actually filter method not filter type, even though libpng docs refer to it as filter type. The actual filter type needs to be set separately using png_set_filter function
png_get_IHDR(readerPNG->m_PNGDecompress, readerPNG->m_PNGInfo, (png_uint_32*)&width, (png_uint_32*)&height, &bitDepth, &colorType, &interlace, &compressionType, &filterMethod);
uint32_t allocSize = SafeUMul(pitch, height);
uint8_t* imageData = (uint8_t*)malloc(allocSize);
if( imageData == NULL ) {
png_destroy_read_struct(&readerPNG->m_PNGDecompress, &readerPNG->m_PNGInfo, NULL);
png_destroy_write_struct(&m_PNGCompress, &m_PNGInfo);
return false;
}
allocSize = SafeUMul((unsigned int)sizeof(png_bytep), height);
png_bytep* rowPointers = (png_bytep*)malloc(allocSize);
if( rowPointers == NULL ) {
png_destroy_read_struct(&readerPNG->m_PNGDecompress, &readerPNG->m_PNGInfo, NULL);
png_destroy_write_struct(&m_PNGCompress, &m_PNGInfo);
free(imageData);
return false;
}
// libpng asks for an array of scanline pointers, into each of which it'll write width * components * depth bytes.
for( unsigned int y = 0; y < height; y++ ) {
const uint32_t rowOffset = SafeUMul(y, pitch);
rowPointers[y] = imageData + rowOffset;
}
if( setjmp(png_jmpbuf(readerPNG->m_PNGDecompress)) ) {
png_destroy_read_struct(&readerPNG->m_PNGDecompress, &readerPNG->m_PNGInfo, NULL);
png_destroy_write_struct(&m_PNGCompress, &m_PNGInfo);
free(imageData);
free(rowPointers);
return false;
}
png_read_image(readerPNG->m_PNGDecompress, rowPointers);
png_set_IHDR(m_PNGCompress, m_PNGInfo, width, height, bitDepth, colorType, PNG_INTERLACE_NONE, compressionType, filterMethod);
uint32_t filterType = PNG_ALL_FILTERS;
if( colorType == PNG_COLOR_TYPE_PALETTE ) {
png_colorp palette;
int32_t numEntries;
png_get_PLTE(readerPNG->m_PNGDecompress, readerPNG->m_PNGInfo, &palette, &numEntries);
png_set_PLTE(m_PNGCompress, m_PNGInfo, palette, numEntries);
png_bytep atRNS = NULL;
png_color_16p ctRNS = NULL;
int numtRNS = 0;
png_get_tRNS(readerPNG->m_PNGDecompress, readerPNG->m_PNGInfo, &atRNS, &numtRNS, &ctRNS);
png_set_tRNS(m_PNGCompress, m_PNGInfo, atRNS, numtRNS, ctRNS);
filterType = 0;
}
if( setjmp(png_jmpbuf(m_PNGCompress)) ) {
png_destroy_read_struct(&readerPNG->m_PNGDecompress, &readerPNG->m_PNGInfo, NULL);
png_destroy_write_struct(&m_PNGCompress, &m_PNGInfo);
free(rowPointers);
free(imageData);
return false;
}
png_write_info(m_PNGCompress, m_PNGInfo);
png_set_filter(m_PNGCompress, PNG_FILTER_TYPE_BASE, filterType);
png_set_compression_level(m_PNGCompress, 9); // maximum compression, tests showed 6 out of 13400 png images had to be cleaned, because transform produced higher sizes.
png_write_rows(m_PNGCompress, rowPointers, height);
readerPNG->endRead();
png_write_end(m_PNGCompress, NULL);
png_destroy_write_struct(&m_PNGCompress, &m_PNGInfo);
free(rowPointers);
free(imageData);
return true;
}
bool ImageWriterPNG::beginWrite(unsigned int width, unsigned int height, EImageColorModel colorModel)
{
if( setjmp(png_jmpbuf(m_PNGCompress)) ) {
return false;
}
if( !Image::colorModelIsRGBA(colorModel) && !Image::colorModelIsGrayscale(colorModel) ) {
return false;
}
unsigned int colorType = 0;
if( colorModel == kColorModel_RGBX && m_SourceReader != NULL && m_SourceReader->getNativeColorModel() == kColorModel_RGBA ) {
colorType = PNG_COLOR_TYPE_RGBA;
} else if( colorModel == kColorModel_RGBA && m_SourceReader != NULL && m_SourceReader->getNativeColorModel() == kColorModel_RGBX ) {
colorType = PNG_COLOR_TYPE_RGB;
} else if( colorModel == kColorModel_RGBA ) {
colorType = PNG_COLOR_TYPE_RGBA;
} else if( colorModel == kColorModel_RGBX ) {
colorType = PNG_COLOR_TYPE_RGB;
} else if( colorModel == kColorModel_Grayscale ) {
colorType = PNG_COLOR_TYPE_GRAY;
} else {
SECURE_ASSERT(0);
}
png_set_IHDR(m_PNGCompress, m_PNGInfo, width, height, 8, colorType, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
png_write_info(m_PNGCompress, m_PNGInfo);
applyCompressionSettings();
if( colorType == PNG_COLOR_TYPE_RGB ) {
png_set_filler(m_PNGCompress, 0, PNG_FILLER_AFTER);
}
return true;
}
unsigned int ImageWriterPNG::writeRows(Image* source, unsigned int sourceRow, unsigned int numRows)
{
EImageColorModel colorModel = source->getColorModel();
if( !Image::colorModelIsInterleaved(colorModel) ) {
return false;
}
if( setjmp(png_jmpbuf(m_PNGCompress)) ) {
return false;
}
unsigned int allocSize = SafeUMul((unsigned int)sizeof(png_bytep), numRows);
png_bytep* rowPointers = (png_bytep*)malloc(allocSize);
if( rowPointers == NULL ) {
return false;
}
const uint8_t* sourceBuffer = source->asInterleaved()->getBytes();
unsigned int sourcePitch = source->asInterleaved()->getPitch();
unsigned int finalRow = sourceRow + numRows;
for( unsigned int y = sourceRow; y < finalRow; y++ ) {
rowPointers[y] = (uint8_t*)(sourceBuffer + sourcePitch * y);
}
png_write_rows(m_PNGCompress, rowPointers, numRows);
free(rowPointers);
return numRows;
}
bool ImageWriterPNG::endWrite()
{
if( setjmp(png_jmpbuf(m_PNGCompress)) ) {
return false;
}
png_write_end(m_PNGCompress, NULL);
png_destroy_write_struct(&m_PNGCompress, &m_PNGInfo);
return true;
}
bool ImageWriterPNG::writeImage(Image* sourceImage)
{
unsigned int sourceWidth = sourceImage->getWidth();
unsigned int sourceHeight = sourceImage->getHeight();
if( !beginWrite(sourceWidth, sourceHeight, sourceImage->getColorModel()) ) {
return false;
}
if( writeRows(sourceImage, 0, sourceHeight) != sourceHeight ) {
return false;
}
if( !endWrite() ) {
return false;
}
return true;
}
void ImageWriterPNG::applyCompressionSettings()
{
if( m_WriteOptions & ImageWriter::kWriteOption_ForcePNGRunLengthEncoding ) {
// specialized settings for images that have large areas of fixed color gradient
png_set_filter(m_PNGCompress, PNG_FILTER_TYPE_BASE, PNG_ALL_FILTERS);
png_set_compression_level(m_PNGCompress, 4);
png_set_compression_strategy(m_PNGCompress, 3);
} else {
// Learned parameters that provide the best size / speed performance for typical uploads.
png_set_filter(m_PNGCompress, PNG_FILTER_TYPE_BASE, PNG_FILTER_SUB | PNG_FILTER_UP);
png_set_compression_level(m_PNGCompress, 4);
png_set_compression_strategy(m_PNGCompress, 0);
}
}
}