- the i_get_file_background() and i_get_file_backgroundf() APIs now
return int to indicate whether the i_background tag was found.
+ - PNG rework
+ - improve error reporting
+ - add png_interlace, png_bits tags
+ - read paletted images as paletted images, including transparency
+ - read 1 bit greyscale images as a type suitable for other file
+ handlers to write as bilevel
+ - read 16 bit/sample PNG as 16-bit/sample Imager images
+ - write "bilevel" paletted images as 1 bit grayscale images
+ - write paletted images as paletted images
+ - write 16-bit (or higher)/sample images as 16-bit/sample PNG
+ images
+ - improved metadata support
+ https://rt.cpan.org/Ticket/Display.html?id=29268
+
Imager 0.89 - 18 Mar 2012
===========
PNG/README
PNG/t/00load.t
PNG/t/10png.t Test png support
+PNG/testimg/badcrc.png
+PNG/testimg/bilevel.png
+PNG/testimg/comment.png
+PNG/testimg/cover.png
+PNG/testimg/cover16.png
+PNG/testimg/cover16i.png
+PNG/testimg/coveri.png
+PNG/testimg/coverpal.png
+PNG/testimg/coverpali.png
+PNG/testimg/gray.png
+PNG/testimg/graya.png
+PNG/testimg/pal.png
PNG/testimg/palette.png
PNG/testimg/palette_out.png
+PNG/testimg/paltrans.png
+PNG/testimg/rgb16.png
+PNG/testimg/rgb8.png
+PNG/testimg/rgb8i.png
pnm.c
polygon.c
ppport.h
/meta\.tmp$
^imconfig\.h$
+# generated if we build them in their own directory
+^(PNG|TIFF|FT2|W32|GIF)/blib/
+
# generated from .im files
^combine\.c$
^compose\.c$
+
+ - improve error reporting to actually report the error text from
+ libpng.
+
Imager-File-PNG 0.84
====================
plan to continue receiving mail at that address.
https://rt.cpan.org/Ticket/Display.html?id=68591
+ - Makefile.PL updates to report library detection info back to the
+ main Imager Makefile.PL.
+ https://rt.cpan.org/Ticket/Display.html?id=9675
+
Imager-File-PNG 0.78
====================
README
t/00load.t
t/10png.t
+testimg/badcrc.png
+testimg/bilevel.png
+testimg/comment.png
+testimg/cover.png
+testimg/cover16.png
+testimg/cover16i.png
+testimg/coveri.png
+testimg/coverpal.png
+testimg/coverpali.png
+testimg/gray.png
+testimg/graya.png
+testimg/pal.png
testimg/palette.png
testimg/palette_out.png
+testimg/paltrans.png
+testimg/rgb16.png
+testimg/rgb8.png
+testimg/rgb8i.png
^blib/
^pm_to_blib$
^Makefile$
+^Makefile\.old$
^MANIFEST\.bak$
# test products
^testout
+\.gcov$
+\.gcda$
+\.gcno$
+
# system
^\.svn/
Imager::ImgRaw im
Imager::IO ig
+unsigned
+i_png_lib_version()
+
BOOT:
PERL_INITIALIZE_IMAGER_CALLBACKS;
#include "impng.h"
#include "png.h"
-
-/* Check to see if a file is a PNG file using png_sig_cmp(). png_sig_cmp()
- * returns zero if the image is a PNG and nonzero if it isn't a PNG.
- *
- * The function check_if_png() shown here, but not used, returns nonzero (true)
- * if the file can be opened and is a PNG, 0 (false) otherwise.
- *
- * If this call is successful, and you are going to keep the file open,
- * you should call png_set_sig_bytes(png_ptr, PNG_BYTES_TO_CHECK); once
- * you have created the png_ptr, so that libpng knows your application
- * has read that many bytes from the start of the file. Make sure you
- * don't call png_set_sig_bytes() with more than 8 bytes read or give it
- * an incorrect number of bytes read, or you will either have read too
- * many bytes (your fault), or you are telling libpng to read the wrong
- * number of magic bytes (also your fault).
- *
- * Many applications already read the first 2 or 4 bytes from the start
- * of the image to determine the file type, so it would be easiest just
- * to pass the bytes to png_sig_cmp() or even skip that if you know
- * you have a PNG file, and call png_set_sig_bytes().
- */
+#include <stdlib.h>
/* this is a way to get number of channels from color space
* Color code to channel number */
static int CC2C[PNG_COLOR_MASK_PALETTE|PNG_COLOR_MASK_COLOR|PNG_COLOR_MASK_ALPHA];
#define PNG_BYTES_TO_CHECK 4
-
+static i_img *
+read_direct8(png_structp png_ptr, png_infop info_ptr, int channels, i_img_dim width, i_img_dim height);
+
+static i_img *
+read_direct16(png_structp png_ptr, png_infop info_ptr, int channels, i_img_dim width, i_img_dim height);
+
+static i_img *
+read_paletted(png_structp png_ptr, png_infop info_ptr, int channels, i_img_dim width, i_img_dim height);
+
+static i_img *
+read_bilevel(png_structp png_ptr, png_infop info_ptr, i_img_dim width, i_img_dim height);
+
+static int
+write_direct8(png_structp png_ptr, png_infop info_ptr, i_img *im);
+
+static int
+write_direct16(png_structp png_ptr, png_infop info_ptr, i_img *im);
+
+static int
+write_paletted(png_structp png_ptr, png_infop info_ptr, i_img *im, int bits);
+
+static int
+write_bilevel(png_structp png_ptr, png_infop info_ptr, i_img *im);
+
+static void
+get_png_tags(i_img *im, png_structp png_ptr, png_infop info_ptr, int bit_depth, int color_type);
+
+static int
+set_png_tags(i_img *im, png_structp png_ptr, png_infop info_ptr);
+
+static const char *
+get_string2(i_img_tags *tags, const char *name, char *buf, size_t *size);
+
+unsigned
+i_png_lib_version(void) {
+ return png_access_version_number();
+}
static void
wiol_read_data(png_structp png_ptr, png_bytep data, png_size_t length) {
io_glue *ig = png_get_io_ptr(png_ptr);
- int rc = i_io_read(ig, data, length);
+ ssize_t rc = i_io_read(ig, data, length);
if (rc != length) png_error(png_ptr, "Read overflow error on an iolayer source.");
}
static void
wiol_write_data(png_structp png_ptr, png_bytep data, png_size_t length) {
- int rc;
+ ssize_t rc;
io_glue *ig = png_get_io_ptr(png_ptr);
rc = i_io_write(ig, data, length);
if (rc != length) png_error(png_ptr, "Write error on an iolayer source.");
static void
wiol_flush_data(png_structp png_ptr) {
- /* XXX : This needs to be added to the io layer */
+ io_glue *ig = png_get_io_ptr(png_ptr);
+ if (!i_io_flush(ig))
+ png_error(png_ptr, "Error flushing output");
}
+static void
+error_handler(png_structp png_ptr, png_const_charp msg) {
+ mm_log((1, "PNG error: '%s'\n", msg));
-/* Check function demo
-
-int
-check_if_png(char *file_name, FILE **fp) {
- char buf[PNG_BYTES_TO_CHECK];
- if ((*fp = fopen(file_name, "rb")) != NULL) return 0;
- if (fread(buf, 1, PNG_BYTES_TO_CHECK, *fp) != PNG_BYTES_TO_CHECK) return 0;
- return(!png_sig_cmp((png_bytep)buf, (png_size_t)0, PNG_BYTES_TO_CHECK));
+ i_push_error(0, msg);
+ longjmp(png_jmpbuf(png_ptr), 1);
}
+
+/*
+
+ For writing a warning might have information about an error, so send
+ it to the error stack.
+
*/
+static void
+write_warn_handler(png_structp png_ptr, png_const_charp msg) {
+ mm_log((1, "PNG write warning '%s'\n", msg));
+
+ i_push_error(0, msg);
+}
+
+#define PNG_DIM_MAX 0x7fffffffL
undef_int
i_writepng_wiol(i_img *im, io_glue *ig) {
png_structp png_ptr;
png_infop info_ptr = NULL;
- int width,height,y;
+ i_img_dim width,height,y;
volatile int cspace,channels;
- double xres, yres;
- int aspect_only, have_res;
+ unsigned char *data;
+ unsigned char * volatile vdata = NULL;
+ int bits;
+ int is_bilevel = 0, zero_is_white;
mm_log((1,"i_writepng(im %p ,ig %p)\n", im, ig));
height = im->ysize;
width = im->xsize;
+ /* if we ever have 64-bit i_img_dim
+ * the libpng docs state that png_set_user_limits() can be used to
+ * override the PNG_USER_*_MAX limits, but as implemented they
+ * don't. We check against the theoretical limit of PNG here, and
+ * try to override the limits below, in case the libpng
+ * implementation ever matches the documentation.
+ *
+ * https://sourceforge.net/tracker/?func=detail&atid=105624&aid=3314943&group_id=5624
+ * fixed in libpng 1.5.3
+ */
+ if (width > PNG_DIM_MAX || height > PNG_DIM_MAX) {
+ i_push_error(0, "Image too large for PNG");
+ return 0;
+ }
+
channels=im->channels;
- if (channels > 2) { cspace = PNG_COLOR_TYPE_RGB; channels-=3; }
- else { cspace=PNG_COLOR_TYPE_GRAY; channels--; }
-
- if (channels) cspace|=PNG_COLOR_MASK_ALPHA;
- mm_log((1,"cspace=%d\n",cspace));
+ if (i_img_is_monochrome(im, &zero_is_white)) {
+ is_bilevel = 1;
+ bits = 1;
+ cspace = PNG_COLOR_TYPE_GRAY;
+ mm_log((1, "i_writepng: bilevel output\n"));
+ }
+ else if (im->type == i_palette_type) {
+ int colors = i_colorcount(im);
+
+ cspace = PNG_COLOR_TYPE_PALETTE;
+ bits = 1;
+ while ((1 << bits) < colors) {
+ bits += bits;
+ }
+ mm_log((1, "i_writepng: paletted output\n"));
+ }
+ else {
+ switch (channels) {
+ case 1:
+ cspace = PNG_COLOR_TYPE_GRAY;
+ break;
+ case 2:
+ cspace = PNG_COLOR_TYPE_GRAY_ALPHA;
+ break;
+ case 3:
+ cspace = PNG_COLOR_TYPE_RGB;
+ break;
+ case 4:
+ cspace = PNG_COLOR_TYPE_RGB_ALPHA;
+ break;
+ default:
+ fprintf(stderr, "Internal error, channels = %d\n", channels);
+ abort();
+ }
+ bits = im->bits > 8 ? 16 : 8;
+ mm_log((1, "i_writepng: direct output\n"));
+ }
- channels = im->channels;
+ mm_log((1,"i_writepng: cspace=%d, bits=%d\n",cspace, bits));
/* Create and initialize the png_struct with the desired error handler
* functions. If you want to use the default stderr and longjump method,
* in case we are using dynamically linked libraries. REQUIRED.
*/
- png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING,NULL,NULL,NULL);
+ png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL,
+ error_handler, write_warn_handler);
if (png_ptr == NULL) return 0;
*/
if (setjmp(png_jmpbuf(png_ptr))) {
png_destroy_write_struct(&png_ptr, &info_ptr);
+ if (vdata)
+ myfree(vdata);
return(0);
}
* currently be PNG_COMPRESSION_TYPE_BASE and PNG_FILTER_TYPE_BASE. REQUIRED
*/
- png_set_IHDR(png_ptr, info_ptr, width, height, 8, cspace,
+ /* by default, libpng (not PNG) limits the image size to a maximum
+ * 1000000 pixels in each direction, but Imager doesn't.
+ * Configure libpng to avoid that limit.
+ */
+ png_set_user_limits(png_ptr, width, height);
+
+ png_set_IHDR(png_ptr, info_ptr, width, height, bits, cspace,
PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);
- have_res = 1;
- if (i_tags_get_float(&im->tags, "i_xres", 0, &xres)) {
- if (i_tags_get_float(&im->tags, "i_yres", 0, &yres))
- ; /* nothing to do */
- else
- yres = xres;
+ if (!set_png_tags(im, png_ptr, info_ptr)) {
+ png_destroy_write_struct(&png_ptr, &info_ptr);
+ return 0;
}
- else {
- if (i_tags_get_float(&im->tags, "i_yres", 0, &yres))
- xres = yres;
- else
- have_res = 0;
+
+ if (is_bilevel) {
+ if (!write_bilevel(png_ptr, info_ptr, im)) {
+ png_destroy_write_struct(&png_ptr, &info_ptr);
+ return 0;
+ }
}
- if (have_res) {
- aspect_only = 0;
- i_tags_get_int(&im->tags, "i_aspect_only", 0, &aspect_only);
- xres /= 0.0254;
- yres /= 0.0254;
- png_set_pHYs(png_ptr, info_ptr, xres + 0.5, yres + 0.5,
- aspect_only ? PNG_RESOLUTION_UNKNOWN : PNG_RESOLUTION_METER);
+ else if (im->type == i_palette_type) {
+ if (!write_paletted(png_ptr, info_ptr, im, bits)) {
+ png_destroy_write_struct(&png_ptr, &info_ptr);
+ return 0;
+ }
}
-
- png_write_info(png_ptr, info_ptr);
-
- if (!im->virtual && im->type == i_direct_type && im->bits == i_8_bits) {
- for (y = 0; y < height; y++)
- png_write_row(png_ptr, (png_bytep) &(im->idata[channels*width*y]));
+ else if (bits == 16) {
+ if (!write_direct16(png_ptr, info_ptr, im)) {
+ png_destroy_write_struct(&png_ptr, &info_ptr);
+ return 0;
+ }
}
else {
- unsigned char *data = mymalloc(im->xsize * im->channels);
- for (y = 0; y < height; y++) {
- i_gsamp(im, 0, im->xsize, y, data, NULL, im->channels);
- png_write_row(png_ptr, (png_bytep)data);
+ if (!write_direct8(png_ptr, info_ptr, im)) {
+ png_destroy_write_struct(&png_ptr, &info_ptr);
+ return 0;
}
- myfree(data);
}
png_write_end(png_ptr, info_ptr);
return(1);
}
+typedef struct {
+ char *warnings;
+} i_png_read_state, *i_png_read_statep;
+static void
+read_warn_handler(png_structp, png_const_charp);
-static void get_png_tags(i_img *im, png_structp png_ptr, png_infop info_ptr);
+static void
+cleanup_read_state(i_png_read_statep);
i_img*
i_readpng_wiol(io_glue *ig) {
png_infop info_ptr;
png_uint_32 width, height;
int bit_depth, color_type, interlace_type;
- int number_passes,y;
- int channels,pass;
+ int channels;
unsigned int sig_read;
+ i_png_read_state rs;
+ rs.warnings = NULL;
sig_read = 0;
mm_log((1,"i_readpng_wiol(ig %p)\n", ig));
+ i_clear_error();
- png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING,NULL,NULL,NULL);
+ png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, &rs,
+ error_handler, read_warn_handler);
+ if (!png_ptr) {
+ i_push_error(0, "Cannot create PNG read structure");
+ return NULL;
+ }
png_set_read_fn(png_ptr, (png_voidp) (ig), wiol_read_data);
info_ptr = png_create_info_struct(png_ptr);
if (info_ptr == NULL) {
png_destroy_read_struct(&png_ptr, (png_infopp)NULL, (png_infopp)NULL);
+ i_push_error(0, "Cannot create PNG info structure");
return NULL;
}
if (im) i_img_destroy(im);
mm_log((1,"i_readpng_wiol: error.\n"));
png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp)NULL);
+ cleanup_read_state(&rs);
return NULL;
}
+
+ /* we do our own limit checks */
+ png_set_user_limits(png_ptr, PNG_DIM_MAX, PNG_DIM_MAX);
png_set_sig_bytes(png_ptr, sig_read);
png_read_info(png_ptr, info_ptr);
return NULL;
}
+ if (color_type == PNG_COLOR_TYPE_PALETTE) {
+ im = read_paletted(png_ptr, info_ptr, channels, width, height);
+ }
+ else if (color_type == PNG_COLOR_TYPE_GRAY
+ && bit_depth == 1
+ && !png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) {
+ im = read_bilevel(png_ptr, info_ptr, width, height);
+ }
+ else if (bit_depth == 16) {
+ im = read_direct16(png_ptr, info_ptr, channels, width, height);
+ }
+ else {
+ im = read_direct8(png_ptr, info_ptr, channels, width, height);
+ }
+
+ if (im)
+ get_png_tags(im, png_ptr, info_ptr, bit_depth, color_type);
+
+ png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp)NULL);
+
+ if (im) {
+ if (rs.warnings) {
+ i_tags_set(&im->tags, "png_warnings", rs.warnings, -1);
+ }
+ }
+ cleanup_read_state(&rs);
+
+ mm_log((1,"(%p) <- i_readpng_wiol\n", im));
+
+ return im;
+}
+
+static i_img *
+read_direct8(png_structp png_ptr, png_infop info_ptr, int channels,
+ i_img_dim width, i_img_dim height) {
+ i_img * volatile vim = NULL;
+ int color_type = png_get_color_type(png_ptr, info_ptr);
+ int bit_depth = png_get_bit_depth(png_ptr, info_ptr);
+ i_img_dim y;
+ int number_passes, pass;
+ i_img *im;
+ unsigned char *line;
+ unsigned char * volatile vline = NULL;
+
+ if (setjmp(png_jmpbuf(png_ptr))) {
+ if (vim) i_img_destroy(vim);
+ if (vline) myfree(vline);
+
+ return NULL;
+ }
+
+ number_passes = png_set_interlace_handling(png_ptr);
+ mm_log((1,"number of passes=%d\n",number_passes));
+
png_set_strip_16(png_ptr);
png_set_packing(png_ptr);
- if (color_type == PNG_COLOR_TYPE_PALETTE) png_set_expand(png_ptr);
- if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) png_set_expand(png_ptr);
+ if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8)
+ png_set_expand(png_ptr);
+
if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) {
channels++;
mm_log((1, "image has transparency, adding alpha: channels = %d\n", channels));
png_set_expand(png_ptr);
}
+ png_read_update_info(png_ptr, info_ptr);
+
+ im = vim = i_img_8_new(width,height,channels);
+ if (!im) {
+ png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp)NULL);
+ return NULL;
+ }
+
+ line = vline = mymalloc(channels * width);
+ for (pass = 0; pass < number_passes; pass++) {
+ for (y = 0; y < height; y++) {
+ if (pass > 0)
+ i_gsamp(im, 0, width, y, line, NULL, channels);
+ png_read_row(png_ptr,(png_bytep)line, NULL);
+ i_psamp(im, 0, width, y, line, NULL, channels);
+ }
+ }
+ myfree(line);
+ vline = NULL;
+
+ png_read_end(png_ptr, info_ptr);
+
+ return im;
+}
+
+static i_img *
+read_direct16(png_structp png_ptr, png_infop info_ptr, int channels,
+ i_img_dim width, i_img_dim height) {
+ i_img * volatile vim = NULL;
+ i_img_dim x, y;
+ int number_passes, pass;
+ i_img *im;
+ unsigned char *line;
+ unsigned char * volatile vline = NULL;
+ unsigned *bits_line;
+ unsigned * volatile vbits_line = NULL;
+ size_t row_bytes;
+
+ if (setjmp(png_jmpbuf(png_ptr))) {
+ if (vim) i_img_destroy(vim);
+ if (vline) myfree(vline);
+ if (vbits_line) myfree(vbits_line);
+
+ return NULL;
+ }
+
number_passes = png_set_interlace_handling(png_ptr);
mm_log((1,"number of passes=%d\n",number_passes));
+
+ if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) {
+ channels++;
+ mm_log((1, "image has transparency, adding alpha: channels = %d\n", channels));
+ png_set_expand(png_ptr);
+ }
+
png_read_update_info(png_ptr, info_ptr);
- im = i_img_8_new(width,height,channels);
+ im = vim = i_img_16_new(width,height,channels);
if (!im) {
png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp)NULL);
return NULL;
}
-
- for (pass = 0; pass < number_passes; pass++)
- for (y = 0; y < height; y++) { png_read_row(png_ptr,(png_bytep) &(im->idata[channels*width*y]), NULL); }
+
+ row_bytes = png_get_rowbytes(png_ptr, info_ptr);
+ line = vline = mymalloc(row_bytes);
+ memset(line, 0, row_bytes);
+ bits_line = vbits_line = mymalloc(sizeof(unsigned) * width * channels);
+ for (pass = 0; pass < number_passes; pass++) {
+ for (y = 0; y < height; y++) {
+ if (pass > 0) {
+ i_gsamp_bits(im, 0, width, y, bits_line, NULL, channels, 16);
+ for (x = 0; x < width * channels; ++x) {
+ line[x*2] = bits_line[x] >> 8;
+ line[x*2+1] = bits_line[x] & 0xff;
+ }
+ }
+ png_read_row(png_ptr,(png_bytep)line, NULL);
+ for (x = 0; x < width * channels; ++x)
+ bits_line[x] = (line[x*2] << 8) + line[x*2+1];
+ i_psamp_bits(im, 0, width, y, bits_line, NULL, channels, 16);
+ }
+ }
+ myfree(line);
+ myfree(bits_line);
+ vline = NULL;
+ vbits_line = NULL;
png_read_end(png_ptr, info_ptr);
+
+ return im;
+}
+
+static i_img *
+read_bilevel(png_structp png_ptr, png_infop info_ptr,
+ i_img_dim width, i_img_dim height) {
+ i_img * volatile vim = NULL;
+ i_img_dim x, y;
+ int number_passes, pass;
+ i_img *im;
+ unsigned char *line;
+ unsigned char * volatile vline = NULL;
+ i_color palette[2];
+
+ if (setjmp(png_jmpbuf(png_ptr))) {
+ if (vim) i_img_destroy(vim);
+ if (vline) myfree(vline);
+
+ return NULL;
+ }
+
+ number_passes = png_set_interlace_handling(png_ptr);
+ mm_log((1,"number of passes=%d\n",number_passes));
+
+ png_set_packing(png_ptr);
+
+ png_set_expand(png_ptr);
+
+ png_read_update_info(png_ptr, info_ptr);
- get_png_tags(im, png_ptr, info_ptr);
+ im = vim = i_img_pal_new(width, height, 1, 256);
+ if (!im) {
+ png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp)NULL);
+ return NULL;
+ }
- png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp)NULL);
+ palette[0].channel[0] = palette[0].channel[1] = palette[0].channel[2] =
+ palette[0].channel[3] = 0;
+ palette[1].channel[0] = palette[1].channel[1] = palette[1].channel[2] =
+ palette[1].channel[3] = 255;
+ i_addcolors(im, palette, 2);
- mm_log((1,"(0x%08X) <- i_readpng_scalar\n", im));
+ line = vline = mymalloc(width);
+ memset(line, 0, width);
+ for (pass = 0; pass < number_passes; pass++) {
+ for (y = 0; y < height; y++) {
+ if (pass > 0) {
+ i_gpal(im, 0, width, y, line);
+ /* expand indexes back to 0/255 */
+ for (x = 0; x < width; ++x)
+ line[x] = line[x] ? 255 : 0;
+ }
+ png_read_row(png_ptr,(png_bytep)line, NULL);
+
+ /* back to palette indexes */
+ for (x = 0; x < width; ++x)
+ line[x] = line[x] ? 1 : 0;
+ i_ppal(im, 0, width, y, line);
+ }
+ }
+ myfree(line);
+ vline = NULL;
+ png_read_end(png_ptr, info_ptr);
+
return im;
}
-static void get_png_tags(i_img *im, png_structp png_ptr, png_infop info_ptr) {
+/* FIXME: do we need to unscale palette color values from the
+ supplied alphas? */
+static i_img *
+read_paletted(png_structp png_ptr, png_infop info_ptr, int channels,
+ i_img_dim width, i_img_dim height) {
+ i_img * volatile vim = NULL;
+ int color_type = png_get_color_type(png_ptr, info_ptr);
+ int bit_depth = png_get_bit_depth(png_ptr, info_ptr);
+ i_img_dim y;
+ int number_passes, pass;
+ i_img *im;
+ unsigned char *line;
+ unsigned char * volatile vline = NULL;
+ int num_palette, i;
+ png_colorp png_palette;
+ png_bytep png_pal_trans;
+ png_color_16p png_color_trans;
+ int num_pal_trans;
+
+ if (setjmp(png_jmpbuf(png_ptr))) {
+ if (vim) i_img_destroy(vim);
+ if (vline) myfree(vline);
+
+ return NULL;
+ }
+
+ number_passes = png_set_interlace_handling(png_ptr);
+ mm_log((1,"number of passes=%d\n",number_passes));
+
+ png_set_strip_16(png_ptr);
+ png_set_packing(png_ptr);
+
+ if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8)
+ png_set_expand(png_ptr);
+
+ if (!png_get_PLTE(png_ptr, info_ptr, &png_palette, &num_palette)) {
+ i_push_error(0, "Paletted image with no PLTE chunk");
+ return NULL;
+ }
+
+ if (png_get_tRNS(png_ptr, info_ptr, &png_pal_trans, &num_pal_trans,
+ &png_color_trans)) {
+ channels++;
+ }
+ else {
+ num_pal_trans = 0;
+ }
+
+ png_read_update_info(png_ptr, info_ptr);
+
+ im = vim = i_img_pal_new(width, height, channels, 256);
+ if (!im) {
+ png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp)NULL);
+ return NULL;
+ }
+
+ for (i = 0; i < num_palette; ++i) {
+ i_color c;
+
+ c.rgba.r = png_palette[i].red;
+ c.rgba.g = png_palette[i].green;
+ c.rgba.b = png_palette[i].blue;
+ if (i < num_pal_trans)
+ c.rgba.a = png_pal_trans[i];
+ else
+ c.rgba.a = 255;
+ i_addcolors(im, &c, 1);
+ }
+
+ line = vline = mymalloc(width);
+ for (pass = 0; pass < number_passes; pass++) {
+ for (y = 0; y < height; y++) {
+ if (pass > 0)
+ i_gpal(im, 0, width, y, line);
+ png_read_row(png_ptr,(png_bytep)line, NULL);
+ i_ppal(im, 0, width, y, line);
+ }
+ }
+ myfree(line);
+ vline = NULL;
+
+ png_read_end(png_ptr, info_ptr);
+
+ return im;
+}
+
+struct png_text_name {
+ const char *keyword;
+ const char *tagname;
+};
+
+static const struct png_text_name
+text_tags[] = {
+ { "Author", "png_author" },
+ { "Comment", "i_comment" },
+ { "Copyright", "png_copyright" },
+ { "Creation Time", "png_creation_time" },
+ { "Description", "png_description" },
+ { "Disclaimer", "png_disclaimer" },
+ { "Software", "png_software" },
+ { "Source", "png_source" },
+ { "Title", "png_title" },
+ { "Warning", "png_warning" }
+};
+
+static const int text_tags_count = sizeof(text_tags) / sizeof(*text_tags);
+
+static const char * const
+chroma_tags[] = {
+ "png_chroma_white_x",
+ "png_chroma_white_y",
+ "png_chroma_red_x",
+ "png_chroma_red_y",
+ "png_chroma_green_x",
+ "png_chroma_green_y",
+ "png_chroma_blue_x",
+ "png_chroma_blue_y"
+};
+
+static const int chroma_tag_count = sizeof(chroma_tags) / sizeof(*chroma_tags);
+
+static void
+get_png_tags(i_img *im, png_structp png_ptr, png_infop info_ptr,
+ int bit_depth, int color_type) {
png_uint_32 xres, yres;
int unit_type;
i_tags_setn(&im->tags, "i_aspect_only", 1);
}
}
+ {
+ int interlace = png_get_interlace_type(png_ptr, info_ptr);
+
+ i_tags_setn(&im->tags, "png_interlace", interlace != PNG_INTERLACE_NONE);
+ switch (interlace) {
+ case PNG_INTERLACE_NONE:
+ i_tags_set(&im->tags, "png_interlace_name", "none", -1);
+ break;
+
+ case PNG_INTERLACE_ADAM7:
+ i_tags_set(&im->tags, "png_interlace_name", "adam7", -1);
+ break;
+
+ default:
+ i_tags_set(&im->tags, "png_interlace_name", "unknown", -1);
+ break;
+ }
+ }
+
+ /* the various readers can call png_set_expand(), libpng will make
+ it's internal record of bit_depth at least 8 in that case */
+ i_tags_setn(&im->tags, "png_bits", bit_depth);
+
+ if (png_get_valid(png_ptr, info_ptr, PNG_INFO_sRGB)) {
+ int intent;
+ if (png_get_sRGB(png_ptr, info_ptr, &intent)) {
+ i_tags_setn(&im->tags, "png_srgb_intent", intent);
+ }
+ }
+ else {
+ /* Ignore these if there's an sRGB chunk, libpng simulates
+ their existence if there's an sRGB chunk, and the PNG spec says
+ that these are ignored if the sRGB is present, so ignore them.
+ */
+ double gamma;
+ double chroma[8];
+
+ if (png_get_gAMA(png_ptr, info_ptr, &gamma)) {
+ i_tags_set_float2(&im->tags, "png_gamma", 0, gamma, 4);
+ }
+
+ if (png_get_cHRM(png_ptr, info_ptr, chroma+0, chroma+1,
+ chroma+2, chroma+3, chroma+4, chroma+5,
+ chroma+6, chroma+7)) {
+ int i;
+
+ for (i = 0; i < chroma_tag_count; ++i)
+ i_tags_set_float2(&im->tags, chroma_tags[i], 0, chroma[i], 4);
+ }
+ }
+
+ {
+ int num_text;
+ png_text *text;
+
+ if (png_get_text(png_ptr, info_ptr, &text, &num_text)) {
+ int i;
+ int custom_index = 0;
+ for (i = 0; i < num_text; ++i) {
+ int j;
+ int found = 0;
+ int compressed = text[i].compression == PNG_ITXT_COMPRESSION_zTXt
+ || text[i].compression == PNG_TEXT_COMPRESSION_zTXt;
+
+ for (j = 0; j < text_tags_count; ++j) {
+ if (strcmp(text_tags[j].keyword, text[i].key) == 0) {
+ char tag_name[50];
+ i_tags_set(&im->tags, text_tags[j].tagname, text[i].text, -1);
+ if (compressed) {
+ sprintf(tag_name, "%s_compressed", text_tags[j].tagname);
+ i_tags_setn(&im->tags, tag_name, 1);
+ }
+ found = 1;
+ break;
+ }
+ }
+
+ if (!found) {
+ char tag_name[50];
+ sprintf(tag_name, "png_text%d_key", custom_index);
+ i_tags_set(&im->tags, tag_name, text[i].key, -1);
+ sprintf(tag_name, "png_text%d_text", custom_index);
+ i_tags_set(&im->tags, tag_name, text[i].text, -1);
+ sprintf(tag_name, "png_text%d_type", custom_index);
+ i_tags_set(&im->tags, tag_name,
+ (text[i].compression == PNG_TEXT_COMPRESSION_NONE
+ || text[i].compression == PNG_TEXT_COMPRESSION_zTXt) ?
+ "text" : "itxt", -1);
+ if (compressed) {
+ sprintf(tag_name, "png_text%d_compressed", custom_index);
+ i_tags_setn(&im->tags, tag_name, 1);
+ }
+ ++custom_index;
+ }
+ }
+ }
+ }
+
+ {
+ png_time *mod_time;
+
+ if (png_get_tIME(png_ptr, info_ptr, &mod_time)) {
+ char time_formatted[80];
+
+ sprintf(time_formatted, "%d-%02d-%02dT%02d:%02d:%02d",
+ mod_time->year, mod_time->month, mod_time->day,
+ mod_time->hour, mod_time->minute, mod_time->second);
+ i_tags_set(&im->tags, "png_time", time_formatted, -1);
+ }
+ }
+
+ {
+ png_color_16 *back;
+ i_color c;
+
+ if (png_get_bKGD(png_ptr, info_ptr, &back)) {
+ switch (color_type) {
+ case PNG_COLOR_TYPE_GRAY:
+ case PNG_COLOR_TYPE_GRAY_ALPHA:
+ {
+ /* lib png stores the raw gray value rather than scaling it
+ to 16-bit (or 8), we use 8-bit color for i_background */
+
+ int gray;
+ switch (bit_depth) {
+ case 16:
+ gray = back->gray >> 8;
+ break;
+ case 8:
+ gray = back->gray;
+ break;
+ case 4:
+ gray = 0x11 * back->gray;
+ break;
+ case 2:
+ gray = 0x55 * back->gray;
+ break;
+ case 1:
+ gray = back->gray ? 0xFF : 0;
+ break;
+ default:
+ gray = 0;
+ }
+ c.rgb.r = c.rgb.g = c.rgb.b = gray;
+ break;
+ }
+
+ case PNG_COLOR_TYPE_RGB:
+ case PNG_COLOR_TYPE_RGB_ALPHA:
+ {
+ c.rgb.r = bit_depth == 16 ? (back->red >> 8) : back->red;
+ c.rgb.g = bit_depth == 16 ? (back->green >> 8) : back->green;
+ c.rgb.b = bit_depth == 16 ? (back->blue >> 8) : back->blue;
+ break;
+ }
+
+ case PNG_COLOR_TYPE_PALETTE:
+ c.rgb.r = back->red;
+ c.rgb.g = back->green;
+ c.rgb.b = back->blue;
+ break;
+ }
+
+ c.rgba.a = 255;
+ i_tags_set_color(&im->tags, "i_background", 0, &c);
+ }
+ }
+}
+
+#define GET_STR_BUF_SIZE 40
+
+static int
+set_png_tags(i_img *im, png_structp png_ptr, png_infop info_ptr) {
+ double xres, yres;
+ int aspect_only, have_res = 1;
+
+ if (i_tags_get_float(&im->tags, "i_xres", 0, &xres)) {
+ if (i_tags_get_float(&im->tags, "i_yres", 0, &yres))
+ ; /* nothing to do */
+ else
+ yres = xres;
+ }
+ else {
+ if (i_tags_get_float(&im->tags, "i_yres", 0, &yres))
+ xres = yres;
+ else
+ have_res = 0;
+ }
+ if (have_res) {
+ aspect_only = 0;
+ i_tags_get_int(&im->tags, "i_aspect_only", 0, &aspect_only);
+ xres /= 0.0254;
+ yres /= 0.0254;
+ png_set_pHYs(png_ptr, info_ptr, xres + 0.5, yres + 0.5,
+ aspect_only ? PNG_RESOLUTION_UNKNOWN : PNG_RESOLUTION_METER);
+ }
+
+ {
+ int intent;
+ if (i_tags_get_int(&im->tags, "png_srgb_intent", 0, &intent)) {
+ if (intent < 0 || intent >= PNG_sRGB_INTENT_LAST) {
+ i_push_error(0, "tag png_srgb_intent out of range");
+ return 0;
+ }
+ png_set_sRGB(png_ptr, info_ptr, intent);
+ }
+ else {
+ double chroma[8], gamma;
+ int i;
+ int found_chroma_count = 0;
+
+ for (i = 0; i < chroma_tag_count; ++i) {
+ if (i_tags_get_float(&im->tags, chroma_tags[i], 0, chroma+i))
+ ++found_chroma_count;
+ }
+
+ if (found_chroma_count) {
+ if (found_chroma_count != chroma_tag_count) {
+ i_push_error(0, "all png_chroma_* tags must be supplied or none");
+ return 0;
+ }
+
+ png_set_cHRM(png_ptr, info_ptr, chroma[0], chroma[1], chroma[2],
+ chroma[3], chroma[4], chroma[5], chroma[6], chroma[7]);
+ }
+
+ if (i_tags_get_float(&im->tags, "png_gamma", 0, &gamma)) {
+ png_set_gAMA(png_ptr, info_ptr, gamma);
+ }
+ }
+ }
+
+ {
+ /* png_set_text() is sparsely documented, it isn't indicated whether
+ multiple calls add to or replace the lists of texts, and
+ whether the text/keyword data is copied or not.
+
+ Examining the linpng code reveals that png_set_text() adds to
+ the list and that the text is copied.
+ */
+ int i;
+
+ /* do our standard tags */
+ for (i = 0; i < text_tags_count; ++i) {
+ char buf[GET_STR_BUF_SIZE];
+ size_t size;
+ const char *data;
+
+ data = get_string2(&im->tags, text_tags[i].tagname, buf, &size);
+ if (data) {
+ png_text text;
+ int compression = size > 1000;
+ char compress_tag[40];
+
+ if (memchr(data, '\0', size)) {
+ i_push_errorf(0, "tag %s may not contain NUL characters", text_tags[i].tagname);
+ return 0;
+ }
+
+ sprintf(compress_tag, "%s_compressed", text_tags[i].tagname);
+ i_tags_get_int(&im->tags, compress_tag, 0, &compression);
+
+ text.compression = compression ? PNG_TEXT_COMPRESSION_zTXt
+ : PNG_TEXT_COMPRESSION_NONE;
+ text.key = (char *)text_tags[i].keyword;
+ text.text_length = size;
+ text.text = (char *)data;
+#ifdef PNG_iTXt_SUPPORTED
+ text.itxt_length = 0;
+ text.lang = NULL;
+ text.lang_key = NULL;
+#endif
+
+ png_set_text(png_ptr, info_ptr, &text, 1);
+ }
+ }
+
+ /* for non-standard tags ensure keywords are limited to 1 to 79
+ characters */
+ i = 0;
+ while (1) {
+ char tag_name[50];
+ char key_buf[GET_STR_BUF_SIZE], value_buf[GET_STR_BUF_SIZE];
+ const char *key, *value;
+ size_t key_size, value_size;
+
+ sprintf(tag_name, "png_text%d_key", i);
+ key = get_string2(&im->tags, tag_name, key_buf, &key_size);
+
+ if (key) {
+ size_t k;
+ if (key_size < 1 || key_size > 79) {
+ i_push_errorf(0, "tag %s must be between 1 and 79 characters in length", tag_name);
+ return 0;
+ }
+
+ if (key[0] == ' ' || key[key_size-1] == ' ') {
+ i_push_errorf(0, "tag %s may not contain leading or trailing spaces", tag_name);
+ return 0;
+ }
+
+ if (strstr(key, " ")) {
+ i_push_errorf(0, "tag %s may not contain consecutive spaces", tag_name);
+ return 0;
+ }
+
+ for (k = 0; k < key_size; ++k) {
+ if (key[k] < 32 || key[k] > 126 && key[k] < 161) {
+ i_push_errorf(0, "tag %s may only contain Latin1 characters 32-126, 161-255", tag_name);
+ return 0;
+ }
+ }
+ }
+
+ sprintf(tag_name, "png_text%d_text", i);
+ value = get_string2(&im->tags, tag_name, value_buf, &value_size);
+
+ if (value) {
+ if (memchr(value, '\0', value_size)) {
+ i_push_errorf(0, "tag %s may not contain NUL characters", tag_name);
+ return 0;
+ }
+ }
+
+ if (key && value) {
+ png_text text;
+ int compression = value_size > 1000;
+
+ sprintf(tag_name, "png_text%d_compressed", i);
+ i_tags_get_int(&im->tags, tag_name, 0, &compression);
+
+ text.compression = compression ? PNG_TEXT_COMPRESSION_zTXt
+ : PNG_TEXT_COMPRESSION_NONE;
+ text.key = (char *)key;
+ text.text_length = value_size;
+ text.text = (char *)value;
+#ifdef PNG_iTXt_SUPPORTED
+ text.itxt_length = 0;
+ text.lang = NULL;
+ text.lang_key = NULL;
+#endif
+
+ png_set_text(png_ptr, info_ptr, &text, 1);
+ }
+ else if (key) {
+ i_push_errorf(0, "tag png_text%d_key found but not png_text%d_text", i, i);
+ return 0;
+ }
+ else if (value) {
+ i_push_errorf(0, "tag png_text%d_text found but not png_text%d_key", i, i);
+ return 0;
+ }
+ else {
+ break;
+ }
+ ++i;
+ }
+ }
+
+ {
+ char buf[GET_STR_BUF_SIZE];
+ size_t time_size;
+ const char *timestr = get_string2(&im->tags, "png_time", buf, &time_size);
+
+ if (timestr) {
+ int year, month, day, hour, minute, second;
+ png_time mod_time;
+
+ if (sscanf(timestr, "%d-%d-%dT%d:%d:%d", &year, &month, &day, &hour, &minute, &second) == 6) {
+ /* rough validation */
+ if (month < 1 || month > 12
+ || day < 1 || day > 31
+ || hour < 0 || hour > 23
+ || minute < 0 || minute > 59
+ || second < 0 || second > 60) {
+ i_push_error(0, "invalid date/time for png_time");
+ return 0;
+ }
+ mod_time.year = year;
+ mod_time.month = month;
+ mod_time.day = day;
+ mod_time.hour = hour;
+ mod_time.minute = minute;
+ mod_time.second = second;
+
+ png_set_tIME(png_ptr, info_ptr, &mod_time);
+ }
+ else {
+ i_push_error(0, "png_time must be formatted 'y-m-dTh:m:s'");
+ return 0;
+ }
+ }
+ }
+
+ {
+ /* no bKGD support yet, maybe later
+ it may be simpler to do it in the individual writers
+ */
+ }
+
+ return 1;
+}
+
+static const char *
+get_string2(i_img_tags *tags, const char *name, char *buf, size_t *size) {
+ int index;
+
+ if (i_tags_find(tags, name, 0, &index)) {
+ const i_img_tag *entry = tags->tags + index;
+
+ if (entry->data) {
+ *size = entry->size;
+
+ return entry->data;
+ }
+ else {
+ *size = sprintf(buf, "%d", entry->idata);
+
+ return buf;
+ }
+ }
+ return NULL;
+}
+
+static int
+write_direct8(png_structp png_ptr, png_infop info_ptr, i_img *im) {
+ unsigned char *data, *volatile vdata = NULL;
+ i_img_dim y;
+
+ if (setjmp(png_jmpbuf(png_ptr))) {
+ if (vdata)
+ myfree(vdata);
+
+ return 0;
+ }
+
+ png_write_info(png_ptr, info_ptr);
+
+ vdata = data = mymalloc(im->xsize * im->channels);
+ for (y = 0; y < im->ysize; y++) {
+ i_gsamp(im, 0, im->xsize, y, data, NULL, im->channels);
+ png_write_row(png_ptr, (png_bytep)data);
+ }
+ myfree(data);
+
+ return 1;
+}
+
+static int
+write_direct16(png_structp png_ptr, png_infop info_ptr, i_img *im) {
+ unsigned *data, *volatile vdata = NULL;
+ unsigned char *tran_data, * volatile vtran_data = NULL;
+ i_img_dim samples_per_row = im->xsize * im->channels;
+
+ i_img_dim y;
+
+ if (setjmp(png_jmpbuf(png_ptr))) {
+ if (vdata)
+ myfree(vdata);
+ if (vtran_data)
+ myfree(vtran_data);
+
+ return 0;
+ }
+
+ png_write_info(png_ptr, info_ptr);
+
+ vdata = data = mymalloc(samples_per_row * sizeof(unsigned));
+ vtran_data = tran_data = mymalloc(samples_per_row * 2);
+ for (y = 0; y < im->ysize; y++) {
+ i_img_dim i;
+ unsigned char *p = tran_data;
+ i_gsamp_bits(im, 0, im->xsize, y, data, NULL, im->channels, 16);
+ for (i = 0; i < samples_per_row; ++i) {
+ p[0] = data[i] >> 8;
+ p[1] = data[i] & 0xff;
+ p += 2;
+ }
+ png_write_row(png_ptr, (png_bytep)tran_data);
+ }
+ myfree(tran_data);
+ myfree(data);
+
+ return 1;
+}
+
+static int
+write_paletted(png_structp png_ptr, png_infop info_ptr, i_img *im, int bits) {
+ unsigned char *data, *volatile vdata = NULL;
+ i_img_dim y;
+ unsigned char pal_map[256];
+ png_color pcolors[256];
+ i_color colors[256];
+ int count = i_colorcount(im);
+ int i;
+
+ if (setjmp(png_jmpbuf(png_ptr))) {
+ if (vdata)
+ myfree(vdata);
+
+ return 0;
+ }
+
+ i_getcolors(im, 0, colors, count);
+ if (im->channels < 3) {
+ /* convert the greyscale palette to color */
+ int i;
+ for (i = 0; i < count; ++i) {
+ i_color *c = colors + i;
+ c->channel[3] = c->channel[1];
+ c->channel[2] = c->channel[1] = c->channel[0];
+ }
+ }
+
+ if (i_img_has_alpha(im)) {
+ int i;
+ int bottom_index = 0, top_index = count-1;
+
+ /* fill out the palette map */
+ for (i = 0; i < count; ++i)
+ pal_map[i] = i;
+
+ /* the PNG spec suggests sorting the palette by alpha, but that's
+ unnecessary - all we want to do is move the opaque entries to
+ the end */
+ while (bottom_index < top_index) {
+ if (colors[bottom_index].rgba.a == 255) {
+ pal_map[bottom_index] = top_index;
+ pal_map[top_index--] = bottom_index;
+ }
+ ++bottom_index;
+ }
+ }
+
+ for (i = 0; i < count; ++i) {
+ int srci = i_img_has_alpha(im) ? pal_map[i] : i;
+
+ pcolors[i].red = colors[srci].rgb.r;
+ pcolors[i].green = colors[srci].rgb.g;
+ pcolors[i].blue = colors[srci].rgb.b;
+ }
+
+ png_set_PLTE(png_ptr, info_ptr, pcolors, count);
+
+ if (i_img_has_alpha(im)) {
+ unsigned char trans[256];
+ int i;
+
+ for (i = 0; i < count && colors[pal_map[i]].rgba.a != 255; ++i) {
+ trans[i] = colors[pal_map[i]].rgba.a;
+ }
+ png_set_tRNS(png_ptr, info_ptr, trans, i, NULL);
+ }
+
+ png_write_info(png_ptr, info_ptr);
+
+ png_set_packing(png_ptr);
+
+ vdata = data = mymalloc(im->xsize);
+ for (y = 0; y < im->ysize; y++) {
+ i_gpal(im, 0, im->xsize, y, data);
+ if (i_img_has_alpha(im)) {
+ i_img_dim x;
+ for (x = 0; x < im->xsize; ++x)
+ data[x] = pal_map[data[x]];
+ }
+ png_write_row(png_ptr, (png_bytep)data);
+ }
+ myfree(data);
+
+ return 1;
+}
+
+static int
+write_bilevel(png_structp png_ptr, png_infop info_ptr, i_img *im) {
+ unsigned char *data, *volatile vdata = NULL;
+ i_img_dim y;
+
+ if (setjmp(png_jmpbuf(png_ptr))) {
+ if (vdata)
+ myfree(vdata);
+
+ return 0;
+ }
+
+ png_write_info(png_ptr, info_ptr);
+
+ png_set_packing(png_ptr);
+
+ vdata = data = mymalloc(im->xsize);
+ for (y = 0; y < im->ysize; y++) {
+ i_gsamp(im, 0, im->xsize, y, data, NULL, 1);
+ png_write_row(png_ptr, (png_bytep)data);
+ }
+ myfree(data);
+
+ return 1;
+}
+
+static void
+read_warn_handler(png_structp png_ptr, png_const_charp msg) {
+ i_png_read_statep rs = (i_png_read_statep)png_get_error_ptr(png_ptr);
+ char *workp;
+ size_t new_size;
+
+ mm_log((1, "PNG read warning '%s'\n", msg));
+
+ /* in case this is part of an error report */
+ i_push_error(0, msg);
+
+ /* and save in the warnings so if we do manage to succeed, we
+ * can save it as a tag
+ */
+ new_size = (rs->warnings ? strlen(rs->warnings) : 0)
+ + 1 /* NUL */
+ + strlen(msg) /* new text */
+ + 1; /* newline */
+ workp = myrealloc(rs->warnings, new_size);
+ if (!rs->warnings)
+ *workp = '\0';
+ strcat(workp, msg);
+ strcat(workp, "\n");
+ rs->warnings = workp;
+}
+
+static void
+cleanup_read_state(i_png_read_statep rs) {
+ if (rs->warnings)
+ myfree(rs->warnings);
}
i_img *i_readpng_wiol(io_glue *ig);
undef_int i_writepng_wiol(i_img *im, io_glue *ig);
+unsigned i_png_lib_version(void);
#endif
use strict;
use Imager qw(:all);
use Test::More;
-use Imager::Test qw(test_image_raw test_image);
+use Imager::Test qw(test_image_raw test_image is_image is_imaged test_image_16 test_image_double);
+
+my $debug_writes = 1;
-d "testout" or mkdir "testout";
init_log("testout/t102png.log",1);
-$Imager::formats{"png"}
- or plan skip_all => "No png support";
+plan tests => 248;
+
+# this loads Imager::File::PNG too
+ok($Imager::formats{"png"}, "must have png format");
-plan tests => 35;
+diag("Library version " . Imager::File::PNG::i_png_lib_version());
my $green = i_color_new(0, 255, 0, 255);
my $blue = i_color_new(0, 0, 255, 255);
open(FH,">testout/t102.png") || die "cannot open testout/t102.png for writing\n";
binmode(FH);
my $IO = Imager::io_new_fd(fileno(FH));
-ok(Imager::File::PNG::i_writepng_wiol($img, $IO), "write");
+ok(Imager::File::PNG::i_writepng_wiol($img, $IO), "write")
+ or diag(Imager->_error_as_msg());
close(FH);
open(FH,"testout/t102.png") || die "cannot open testout/t102.png\n";
my $im = test_image();
my $fail_close = sub {
Imager::i_push_error(0, "synthetic close failure");
+ print "# closecb called\n";
return 0;
};
ok(!$im->write(type => "png", callback => sub { 1 },
ok(grep($_ eq 'png', Imager->write_types), "check png in write types");
}
+{ # read error reporting
+ my $im = Imager->new;
+ ok(!$im->read(file => "testimg/badcrc.png", type => "png"),
+ "read png with bad CRC chunk should fail");
+ is($im->errstr, "IHDR: CRC error", "check error message");
+}
+
+{ # write error reporting
+ my $im = test_image();
+ ok(!$im->write(type => "png", callback => limited_write(1), buffered => 0),
+ "write limited to 1 byte should fail");
+ is($im->errstr, "Write error on an iolayer source.: limit reached",
+ "check error message");
+}
+
+SKIP:
+{ # https://sourceforge.net/tracker/?func=detail&aid=3314943&group_id=5624&atid=105624
+ # large images
+ Imager::File::PNG::i_png_lib_version() >= 10503
+ or skip("older libpng limits image sizes", 12);
+
+ {
+ my $im = Imager->new(xsize => 1000001, ysize => 1, channels => 1);
+ ok($im, "make a wide image");
+ my $data;
+ ok($im->write(data => \$data, type => "png"),
+ "write wide image as png")
+ or diag("write wide: " . $im->errstr);
+ my $im2 = Imager->new;
+ ok($im->read(data => $data, type => "png"),
+ "read wide image as png")
+ or diag("read wide: " . $im->errstr);
+ is($im->getwidth, 1000001, "check width");
+ is($im->getheight, 1, "check height");
+ is($im->getchannels, 1, "check channels");
+ }
+
+ {
+ my $im = Imager->new(xsize => 1, ysize => 1000001, channels => 1);
+ ok($im, "make a tall image");
+ my $data;
+ ok($im->write(data => \$data, type => "png"),
+ "write wide image as png")
+ or diag("write tall: " . $im->errstr);
+ my $im2 = Imager->new;
+ ok($im->read(data => $data, type => "png"),
+ "read tall image as png")
+ or diag("read tall: " . $im->errstr);
+ is($im->getwidth, 1, "check width");
+ is($im->getheight, 1000001, "check height");
+ is($im->getchannels, 1, "check channels");
+ }
+}
+
+{ # test grayscale read as greyscale
+ my $im = Imager->new;
+ ok($im->read(file => "testimg/gray.png", type => "png"),
+ "read grayscale");
+ is($im->getchannels, 1, "check channel count");
+ is($im->type, "direct", "check type");
+ is($im->bits, 8, "check bits");
+ is($im->tags(name => "png_bits"), 8, "check png_bits tag");
+ is($im->tags(name => "png_interlace"), 0, "check png_interlace tag");
+}
+
+{ # test grayscale + alpha read as greyscale + alpha
+ my $im = Imager->new;
+ ok($im->read(file => "testimg/graya.png", type => "png"),
+ "read grayscale + alpha");
+ is($im->getchannels, 2, "check channel count");
+ is($im->type, "direct", "check type");
+ is($im->bits, 8, "check bits");
+ is($im->tags(name => "png_bits"), 8, "check png_bits tag");
+ is($im->tags(name => "png_interlace"), 0, "check png_interlace tag");
+}
+
+{ # test paletted + alpha read as paletted
+ my $im = Imager->new;
+ ok($im->read(file => "testimg/paltrans.png", type => "png"),
+ "read paletted with alpha");
+ is($im->getchannels, 4, "check channel count");
+ is($im->type, "paletted", "check type");
+ is($im->tags(name => "png_bits"), 8, "check png_bits tag");
+ is($im->tags(name => "png_interlace"), 0, "check png_interlace tag");
+}
+
+{ # test paletted read as paletted
+ my $im = Imager->new;
+ ok($im->read(file => "testimg/pal.png", type => "png"),
+ "read paletted");
+ is($im->getchannels, 3, "check channel count");
+ is($im->type, "paletted", "check type");
+ is($im->tags(name => "png_bits"), 8, "check png_bits tag");
+ is($im->tags(name => "png_interlace"), 0, "check png_interlace tag");
+}
+
+{ # test 16-bit rgb read as 16 bit
+ my $im = Imager->new;
+ ok($im->read(file => "testimg/rgb16.png", type => "png"),
+ "read 16-bit rgb");
+ is($im->getchannels, 3, "check channel count");
+ is($im->type, "direct", "check type");
+ is($im->tags(name => "png_interlace"), 0, "check png_interlace tag");
+ is($im->bits, 16, "check bits");
+ is($im->tags(name => "png_bits"), 16, "check png_bits tag");
+}
+
+{ # test 1-bit grey read as mono
+ my $im = Imager->new;
+ ok($im->read(file => "testimg/bilevel.png", type => "png"),
+ "read bilevel png");
+ is($im->getchannels, 1, "check channel count");
+ is($im->tags(name => "png_interlace"), 0, "check png_interlace tag");
+ is($im->type, "paletted", "check type");
+ ok($im->is_bilevel, "should be bilevel");
+ is($im->tags(name => "png_bits"), 1, "check png_bits tag");
+}
+
+SKIP:
+{ # test interlaced read as interlaced and matches original
+ my $im_i = Imager->new(file => "testimg/rgb8i.png", filetype => "png");
+ ok($im_i, "read interlaced")
+ or skip("Could not read rgb8i.png: " . Imager->errstr, 7);
+ is($im_i->getchannels, 3, "check channel count");
+ is($im_i->type, "direct", "check type");
+ is($im_i->tags(name => "png_bits"), 8, "check png_bits");
+ is($im_i->tags(name => "png_interlace"), 1, "check png_interlace");
+
+ my $im = Imager->new(file => "testimg/rgb8.png", filetype => "png");
+ ok($im, "read non-interlaced")
+ or skip("Could not read testimg/rgb8.png: " . Imager->errstr, 2);
+ is($im->tags(name => "png_interlace"), 0, "check png_interlace");
+ is_image($im_i, $im, "compare interlaced and non-interlaced");
+}
+
+{
+ my @match =
+ (
+ [ "cover.png", "coveri.png" ],
+ [ "cover16.png", "cover16i.png" ],
+ [ "coverpal.png", "coverpali.png" ],
+ );
+ for my $match (@match) {
+ my ($normal, $interlace) = @$match;
+
+ my $n_im = Imager->new(file => "testimg/$normal");
+ ok($n_im, "read $normal")
+ or diag "reading $normal: ", Imager->errstr;
+ my $i_im = Imager->new(file => "testimg/$interlace");
+ ok($i_im, "read $interlace")
+ or diag "reading $interlace: ", Imager->errstr;
+ SKIP:
+ {
+ $n_im && $i_im
+ or skip("Couldn't read a file", 1);
+ is_image($i_im, $n_im, "check normal and interlace files read the same");
+ }
+ }
+}
+
+{
+ my $interlace = 0;
+ for my $name ("cover.png", "coveri.png") {
+ SKIP: {
+ my $im = Imager->new(file => "testimg/$name");
+ ok($im, "read $name")
+ or diag "Failed to read $name: ", Imager->errstr;
+ $im
+ or skip("Couldn't load $name", 5);
+ is($im->tags(name => "i_format"), "png", "$name: i_format");
+ is($im->tags(name => "png_bits"), 8, "$name: png_bits");
+ is($im->tags(name => "png_interlace"), $interlace,
+ "$name: png_interlace");
+ is($im->getchannels, 4, "$name: four channels");
+ is($im->type, "direct", "$name: direct type");
+
+ is_deeply([ $im->getsamples(y => 0, width => 5) ],
+ [ ( 255, 255, 0, 255 ), ( 255, 255, 0, 191 ),
+ ( 255, 255, 0, 127 ), ( 255, 255, 0, 63 ),
+ ( 0, 0, 0, 0) ],
+ "$name: check expected samples row 0");
+ is_deeply([ $im->getsamples(y => 1, width => 5) ],
+ [ ( 255, 0, 0, 255 ), ( 255, 0, 0, 191 ),
+ ( 255, 0, 0, 127 ), ( 255, 0, 0, 63 ),
+ ( 0, 0, 0, 0) ],
+ "$name: check expected samples row 1");
+ }
+ $interlace = 1;
+ }
+}
+
+{
+ my $interlace = 0;
+ for my $name ("coverpal.png", "coverpali.png") {
+ SKIP: {
+ my $im = Imager->new(file => "testimg/$name");
+ ok($im, "read $name")
+ or diag "Failed to read $name: ", Imager->errstr;
+ $im
+ or skip("Couldn't load $name", 5);
+ is($im->tags(name => "i_format"), "png", "$name: i_format");
+ is($im->tags(name => "png_bits"), 4, "$name: png_bits");
+ is($im->tags(name => "png_interlace"), $interlace,
+ "$name: png_interlace");
+ is($im->getchannels, 4, "$name: four channels");
+ is($im->type, "paletted", "$name: paletted type");
+
+ is_deeply([ $im->getsamples(y => 0, width => 5) ],
+ [ ( 255, 255, 0, 255 ), ( 255, 255, 0, 191 ),
+ ( 255, 255, 0, 127 ), ( 255, 255, 0, 63 ),
+ ( 0, 0, 0, 0) ],
+ "$name: check expected samples row 0");
+ is_deeply([ $im->getsamples(y => 1, width => 5) ],
+ [ ( 255, 0, 0, 255 ), ( 255, 0, 0, 191 ),
+ ( 255, 0, 0, 127 ), ( 255, 0, 0, 63 ),
+ ( 0, 0, 0, 0) ],
+ "$name: check expected samples row 1");
+ }
+ $interlace = 1;
+ }
+}
+
+{
+ my $interlace = 0;
+ for my $name ("cover16.png", "cover16i.png") {
+ SKIP: {
+ my $im = Imager->new(file => "testimg/$name");
+ ok($im, "read $name")
+ or diag "Failed to read $name: ", Imager->errstr;
+ $im
+ or skip("Couldn't load $name", 5);
+ is($im->tags(name => "i_format"), "png", "$name: i_format");
+ is($im->tags(name => "png_bits"), 16, "$name: png_bits");
+ is($im->tags(name => "png_interlace"), $interlace,
+ "$name: png_interlace");
+ is($im->getchannels, 4, "$name: four channels");
+ is($im->type, "direct", "$name: direct type");
+
+ is_deeply([ $im->getsamples(y => 0, width => 5, type => "16bit") ],
+ [ ( 65535, 65535, 0, 65535 ), ( 65535, 65535, 0, 49087 ),
+ ( 65535, 65535, 0, 32639 ), ( 65535, 65535, 0, 16191 ),
+ ( 65535, 65535, 65535, 0) ],
+ "$name: check expected samples row 0");
+ is_deeply([ $im->getsamples(y => 1, width => 5, type => "16bit") ],
+ [ ( 65535, 0, 0, 65535 ), ( 65535, 0, 0, 49087 ),
+ ( 65535, 0, 0, 32639 ), ( 65535, 0, 0, 16191 ),
+ ( 65535, 65535, 65535, 0) ],
+ "$name: check expected samples row 1");
+ }
+ $interlace = 1;
+ }
+}
+
+{
+ my $pim = Imager->new(xsize => 5, ysize => 2, channels => 3, type => "paletted");
+ ok($pim, "make a 3 channel paletted image");
+ ok($pim->addcolors(colors => [ qw(000000 FFFFFF FF0000 00FF00 0000FF) ]),
+ "add some colors");
+ is($pim->setscanline(y => 0, type => "index",
+ pixels => [ 0, 1, 2, 4, 3 ]), 5, "set some pixels");
+ is($pim->setscanline(y => 1, type => "index",
+ pixels => [ 4, 1, 0, 4, 2 ]), 5, "set some more pixels");
+ ok($pim->write(file => "testout/pal3.png"),
+ "write to testout/pal3.png")
+ or diag("Cannot save testout/pal3.png: ".$pim->errstr);
+ my $in = Imager->new(file => "testout/pal3.png");
+ ok($in, "read it back in")
+ or diag("Cann't read pal3.png back: " . Imager->errstr);
+ is_image($pim, $in, "check it matches");
+ is($in->type, "paletted", "make sure the result is paletted");
+ is($in->tags(name => "png_bits"), 4, "4 bit representation");
+}
+
+{
+ # make sure the code that pushes maxed alpha to the end doesn't break
+ my $pim = Imager->new(xsize => 8, ysize => 2, channels => 4, type => "paletted");
+ ok($pim, "make a 4 channel paletted image");
+ ok($pim->addcolors
+ (colors => [ NC(255, 255, 0, 128), qw(000000 FFFFFF FF0000 00FF00 0000FF),
+ NC(0, 0, 0, 0), NC(255, 0, 128, 64) ]),
+ "add some colors");
+ is($pim->setscanline(y => 0, type => "index",
+ pixels => [ 5, 0, 1, 7, 2, 4, 6, 3 ]), 8,
+ "set some pixels");
+ is($pim->setscanline(y => 1, type => "index",
+ pixels => [ 7, 4, 6, 1, 0, 4, 2, 5 ]), 8,
+ "set some more pixels");
+ ok($pim->write(file => "testout/pal4.png"),
+ "write to testout/pal4.png")
+ or diag("Cannot save testout/pal4.png: ".$pim->errstr);
+ my $in = Imager->new(file => "testout/pal4.png");
+ ok($in, "read it back in")
+ or diag("Cann't read pal4.png back: " . Imager->errstr);
+ is_image($pim, $in, "check it matches");
+ is($in->type, "paletted", "make sure the result is paletted");
+ is($in->tags(name => "png_bits"), 4, "4 bit representation");
+}
+
+{
+ my $pim = Imager->new(xsize => 8, ysize => 2, channels => 1, type => "paletted");
+ ok($pim, "make a 1 channel paletted image");
+ ok($pim->addcolors(colors => [ map NC($_, 0, 0), 0, 7, 127, 255 ]),
+ "add some colors^Wgreys");
+ is($pim->setscanline(y => 0, type => "index",
+ pixels => [ 0, 2, 1, 3, 2, 1, 0, 3 ]), 8,
+ "set some pixels");
+ is($pim->setscanline(y => 1, type => "index",
+ pixels => [ 3, 0, 2, 1, 0, 0, 2, 3 ]), 8,
+ "set some more pixels");
+ ok($pim->write(file => "testout/pal1.png"),
+ "write to testout/pal1.png")
+ or diag("Cannot save testout/pal1.png: ".$pim->errstr);
+ my $in = Imager->new(file => "testout/pal1.png");
+ ok($in, "read it back in")
+ or diag("Cann't read pal1.png back: " . Imager->errstr);
+ # PNG doesn't have a paletted greyscale type, so it's written as
+ # paletted color, convert our source image for the comparison
+ my $cmpim = $pim->convert(preset => "rgb");
+ is_image($in, $cmpim, "check it matches");
+ is($in->type, "paletted", "make sure the result is paletted");
+ is($in->tags(name => "png_bits"), 2, "2 bit representation");
+}
+
+{
+ my $pim = Imager->new(xsize => 8, ysize => 2, channels => 2, type => "paletted");
+ ok($pim, "make a 2 channel paletted image");
+ ok($pim->addcolors(colors => [ NC(0, 255, 0), NC(128, 255, 0), NC(255, 255, 0), NC(128, 128, 0) ]),
+ "add some colors^Wgreys")
+ or diag("adding colors: " . $pim->errstr);
+ is($pim->setscanline(y => 0, type => "index",
+ pixels => [ 0, 2, 1, 3, 2, 1, 0, 3 ]), 8,
+ "set some pixels");
+ is($pim->setscanline(y => 1, type => "index",
+ pixels => [ 3, 0, 2, 1, 0, 0, 2, 3 ]), 8,
+ "set some more pixels");
+ ok($pim->write(file => "testout/pal2.png"),
+ "write to testout/pal2.png")
+ or diag("Cannot save testout/pal2.png: ".$pim->errstr);
+ my $in = Imager->new(file => "testout/pal2.png");
+ ok($in, "read it back in")
+ or diag("Can't read pal1.png back: " . Imager->errstr);
+ # PNG doesn't have a paletted greyscale type, so it's written as
+ # paletted color, convert our source image for the comparison
+ my $cmpim = $pim->convert(preset => "rgb");
+ is_image($in, $cmpim, "check it matches");
+ is($in->type, "paletted", "make sure the result is paletted");
+ is($in->tags(name => "png_bits"), 2, "2 bit representation");
+}
+
+{
+ my $imbase = test_image();
+ my $mono = $imbase->convert(preset => "gray")
+ ->to_paletted(make_colors => "mono", translate => "errdiff");
+
+ ok($mono->write(file => "testout/bilevel.png"),
+ "write bilevel.png");
+ my $in = Imager->new(file => "testout/bilevel.png");
+ ok($in, "read it back in")
+ or diag("Can't read bilevel.png: " . Imager->errstr);
+ is_image($in, $mono, "check it matches");
+ is($in->type, "paletted", "make sure the result is paletted");
+ is($in->tags(name => "png_bits"), 1, "1 bit representation");
+}
+
+SKIP:
+{
+ my $im = test_image_16();
+ ok($im->write(file => "testout/rgb16.png", type => "png"),
+ "write 16-bit/sample image")
+ or diag("Could not write rgb16.png: ".$im->errstr);
+ my $in = Imager->new(file => "testout/rgb16.png")
+ or diag("Could not read rgb16.png: ".Imager->errstr);
+ ok($in, "read rgb16.png back in")
+ or skip("Could not load image to check", 4);
+ is_imaged($in, $im, 0, "check image matches");
+ is($in->bits, 16, "check we got a 16-bit image");
+ is($in->type, "direct", "check it's direct");
+ is($in->tags(name => "png_bits"), 16, "check png_bits");
+}
+
+SKIP:
+{
+ my $im = test_image_double();
+ my $cmp = $im->to_rgb16;
+ ok($im->write(file => "testout/rgbdbl.png", type => "png"),
+ "write double/sample image - should write as 16-bit/sample")
+ or diag("Could not write rgbdbl.png: ".$im->errstr);
+ my $in = Imager->new(file => "testout/rgbdbl.png")
+ or diag("Could not read rgbdbl.png: ".Imager->errstr);
+ ok($in, "read pngdbl.png back in")
+ or skip("Could not load image to check", 4);
+ is_imaged($in, $cmp, 0, "check image matches");
+ is($in->bits, 16, "check we got a 16-bit image");
+ is($in->type, "direct", "check it's direct");
+ is($in->tags(name => "png_bits"), 16, "check png_bits");
+}
+
+SKIP:
+{
+ my $im = Imager->new(file => "testimg/comment.png");
+ ok($im, "read file with comment")
+ or diag("Cannot read comment.png: ".Imager->errstr);
+ $im
+ or skip("Cannot test tags file I can't read", 5);
+ is($im->tags(name => "i_comment"), "Test comment", "check i_comment");
+ is($im->tags(name => "png_interlace"), "0", "no interlace");
+ is($im->tags(name => "png_interlace_name"), "none", "no interlace (text)");
+ is($im->tags(name => "png_srgb_intent"), "0", "srgb perceptual");
+ is($im->tags(name => "png_time"), "2012-04-16T07:37:36",
+ "modification time");
+ is($im->tags(name => "i_background"), "color(255,255,255,255)",
+ "background color");
+}
+
+SKIP:
+{ # test tag writing
+ my $im = Imager->new(xsize => 1, ysize => 1);
+ ok($im->write(file => "testout/tags.png",
+ i_comment => "A Comment",
+ png_author => "An Author",
+ png_author_compressed => 1,
+ png_copyright => "A Copyright",
+ png_creation_time => "16 April 2012 22:56:30+1000",
+ png_description => "A Description",
+ png_disclaimer => "A Disclaimer",
+ png_software => "Some Software",
+ png_source => "A Source",
+ png_title => "A Title",
+ png_warning => "A Warning",
+ png_text0_key => "Custom Key",
+ png_text0_text => "Custom Value",
+ png_text0_compressed => 1,
+ png_text1_key => "Custom Key2",
+ png_text1_text => "Another Custom Value",
+ png_time => "2012-04-20T00:15:10",
+ ),
+ "write with many tags")
+ or diag("Cannot write with many tags: ", $im->errstr);
+
+ my $imr = Imager->new(file => "testout/tags.png");
+ ok($imr, "read it back in")
+ or skip("Couldn't read it back: ". Imager->errstr, 1);
+
+ is_deeply({ map @$_, $imr->tags },
+ {
+ i_format => "png",
+ i_comment => "A Comment",
+ png_author => "An Author",
+ png_author_compressed => 1,
+ png_copyright => "A Copyright",
+ png_creation_time => "16 April 2012 22:56:30+1000",
+ png_description => "A Description",
+ png_disclaimer => "A Disclaimer",
+ png_software => "Some Software",
+ png_source => "A Source",
+ png_title => "A Title",
+ png_warning => "A Warning",
+ png_text0_key => "Custom Key",
+ png_text0_text => "Custom Value",
+ png_text0_compressed => 1,
+ png_text0_type => "text",
+ png_text1_key => "Custom Key2",
+ png_text1_text => "Another Custom Value",
+ png_text1_type => "text",
+ png_time => "2012-04-20T00:15:10",
+ png_interlace => 0,
+ png_interlace_name => "none",
+ png_bits => 8,
+ }, "check tags are what we expected");
+}
+
+SKIP:
+{ # cHRM test
+ my $im = Imager->new(xsize => 1, ysize => 1);
+ ok($im->write(file => "testout/tagschrm.png", type => "png",
+ png_chroma_white_x => 0.3,
+ png_chroma_white_y => 0.32,
+ png_chroma_red_x => 0.7,
+ png_chroma_red_y => 0.28,
+ png_chroma_green_x => 0.075,
+ png_chroma_green_y => 0.8,
+ png_chroma_blue_x => 0.175,
+ png_chroma_blue_y => 0.05),
+ "write cHRM chunk");
+ my $imr = Imager->new(file => "testout/tagschrm.png", ftype => "png");
+ ok($imr, "read tagschrm.png")
+ or diag("reading tagschrm.png: ".Imager->errstr);
+ $imr
+ or skip("read of tagschrm.png failed", 1);
+ is_deeply({ map @$_, $imr->tags },
+ {
+ i_format => "png",
+ png_interlace => 0,
+ png_interlace_name => "none",
+ png_bits => 8,
+ png_chroma_white_x => 0.3,
+ png_chroma_white_y => 0.32,
+ png_chroma_red_x => 0.7,
+ png_chroma_red_y => 0.28,
+ png_chroma_green_x => 0.075,
+ png_chroma_green_y => 0.8,
+ png_chroma_blue_x => 0.175,
+ png_chroma_blue_y => 0.05,
+ }, "check chroma tags written");
+}
+
+{ # gAMA
+ my $im = Imager->new(xsize => 1, ysize => 1);
+ ok($im->write(file => "testout/tagsgama.png", type => "png",
+ png_gamma => 2.22),
+ "write with png_gammma tag");
+ my $imr = Imager->new(file => "testout/tagsgama.png", ftype => "png");
+ ok($imr, "read tagsgama.png")
+ or diag("reading tagsgama.png: ".Imager->errstr);
+ $imr
+ or skip("read of tagsgama.png failed", 1);
+ is_deeply({ map @$_, $imr->tags },
+ {
+ i_format => "png",
+ png_interlace => 0,
+ png_interlace_name => "none",
+ png_bits => 8,
+ png_gamma => "2.22",
+ }, "check gamma tag written");
+}
+
+{ # various bad tag failures
+ my @tests =
+ (
+ [
+ [ png_chroma_white_x => 0.5 ],
+ "all png_chroma_* tags must be supplied or none"
+ ],
+ [
+ [ png_srgb_intent => 4 ],
+ "tag png_srgb_intent out of range"
+ ],
+ [
+ [ i_comment => "test\0with nul" ],
+ "tag i_comment may not contain NUL characters"
+ ],
+ [
+ [ png_text0_key => "" ],
+ "tag png_text0_key must be between 1 and 79 characters in length"
+ ],
+ [
+ [ png_text0_key => ("x" x 80) ],
+ "tag png_text0_key must be between 1 and 79 characters in length"
+ ],
+ [
+ [ png_text0_key => " x" ],
+ "tag png_text0_key may not contain leading or trailing spaces"
+ ],
+ [
+ [ png_text0_key => "x " ],
+ "tag png_text0_key may not contain leading or trailing spaces"
+ ],
+ [
+ [ png_text0_key => "x y" ],
+ "tag png_text0_key may not contain consecutive spaces"
+ ],
+ [
+ [ png_text0_key => "\x7F" ],
+ "tag png_text0_key may only contain Latin1 characters 32-126, 161-255"
+ ],
+ [
+ [ png_text0_key => "x", png_text0_text => "a\0b" ],
+ "tag png_text0_text may not contain NUL characters"
+ ],
+ [
+ [ png_text0_key => "test" ],
+ "tag png_text0_key found but not png_text0_text"
+ ],
+ [
+ [ png_text0_text => "test" ],
+ "tag png_text0_text found but not png_text0_key"
+ ],
+ [
+ [ png_time => "bad format" ],
+ "png_time must be formatted 'y-m-dTh:m:s'"
+ ],
+ [
+ [ png_time => "2012-13-01T00:00:00" ],
+ "invalid date/time for png_time"
+ ],
+ );
+ my $im = Imager->new(xsize => 1, ysize => 1);
+ for my $test (@tests) {
+ my ($tags, $error) = @$test;
+ my $im2 = $im->copy;
+ my $data;
+ ok(!$im2->write(data => \$data, type => "png", @$tags),
+ "expect $error");
+ is($im2->errstr, $error, "check error message");
+ }
+}
+
+sub limited_write {
+ my ($limit) = @_;
+
+ return
+ sub {
+ my ($data) = @_;
+ $limit -= length $data;
+ if ($limit >= 0) {
+ print "# write of ", length $data, " bytes successful ($limit left)\n" if $debug_writes;
+ return 1;
+ }
+ else {
+ print "# write of ", length $data, " bytes failed\n";
+ Imager::i_push_error(0, "limit reached");
+ return;
+ }
+ };
+}
+
--- /dev/null
+#!perl
+use strict;
+use IO::Uncompress::Inflate qw(inflate);
+use Getopt::Long;
+
+my $dumpall = 0;
+my $image = 0;
+GetOptions(dumpall => \$dumpall,
+ image => \$image);
+
+my $file = shift
+ or die "Usage: $0 filename\n";
+
+open my $fh, "<", $file
+ or die "$0: cannot open '$file': $!\n";
+
+binmode $fh;
+
+my $head;
+read($fh, $head, 8) == 8
+ or die "Cann't read header: $!\n";
+
+my $offset = 0;
+dump_data($offset, $head);
+print " Header\n";
+$offset += length $head;
+unless ($head eq "\x89PNG\x0d\x0A\cZ\x0A") {
+ die "Header isn't a PNG header\n";
+}
+
+my $colour_type;
+my $bits;
+my $sline_len;
+my $sline_left = 0;
+my $row = 0;
+while (my ($dlen, $data, $len, $type, $payload, $crc) = read_chunk($fh)) {
+ dump_data($offset, $data);
+ $offset += $dlen;
+ my $calc_crc = crc($type . $payload);
+ my $src_crc = unpack("N", $crc);
+
+ $type =~ s/([^ -\x7f])/sprintf("\\x%02x", ord $1)/ge;
+ print " Type: $type\n";
+ print " Length: $len\n";
+ printf " CRC: %x (calculated: %x)\n", $src_crc, $calc_crc;
+ if ($type eq 'IHDR') {
+ my ($w, $h, $d, $ct, $comp, $filter, $inter) =
+ unpack("NNCCCCC", $payload);
+ print <<EOS;
+ Width : $w
+ Height: $h
+ Depth: $d
+ Colour type: $ct
+ Filter: $filter
+ Interlace: $inter
+EOS
+ $colour_type = $ct;
+ $bits = $d;
+ my $channels = $ct == 2 ? 3 : $ct == 4 ? 2 : $ct == 6 ? 4 : 1;
+ my $bitspp = $channels * $d;
+ $sline_len = int((($w * $bitspp) + 7) / 8);
+ ++$sline_len; # filter byte
+ print " Line length: $sline_len\n";
+ }
+ elsif ($type eq 'sRGB') {
+ print " Rendering intent: ", ord($payload), "\n";
+ }
+ elsif ($type eq 'PLTE') {
+ my $index = 0;
+ while ($index * 3 < $len) {
+ my @rgb = unpack("CCC", substr($payload, $index * 3, 3));
+ print " $index: @rgb\n";
+ ++$index;
+ }
+ }
+ elsif ($type eq 'tRNS') {
+ if ($colour_type == 0) {
+ my $g = unpack("n", $payload);
+ printf " Grey: %d (%x)\n", $g, $g;
+ }
+ elsif ($colour_type == 2) {
+ my @rgb = unpack("nnn", $payload);
+ printf " RGB: %d, %d, %d (%x, %x, %x)\n", @rgb, @rgb;
+ }
+ elsif ($colour_type == 3) {
+ my $index = 0;
+ for my $alpha (unpack("C*", $payload)) {
+ print " Index: $index: $alpha\n";
+ ++$index;
+ }
+ }
+ else {
+ print " Unexpected tRNS for colour type $colour_type\n";
+ }
+ }
+ elsif ($type eq 'pHYs') {
+ my ($hres, $vres, $unit) = unpack("NNC", $payload);
+ my $unitname = $unit == 1 ? "metre" : "unknown";
+ print <<EOS;
+ hRes: $hres / $unitname
+ vRes: $vres / $unitname
+ Unit: $unit ($unitname)
+EOS
+ }
+ elsif ($type eq 'tEXt') {
+ my ($key, $value) = split /\0/, $payload, 2;
+ print <<EOS;
+ Keyword: $key
+ Value: $value
+EOS
+ do_more_text($key, $value);
+ }
+ elsif ($type eq 'zTXt') {
+ my ($key, $rest) = split /\0/, $payload, 2;
+ my $ctype = ord $rest;
+ my $ztxt = substr($rest, 1);
+ my $value = do_inflate($ztxt);
+ print <<EOS;
+ Keyword: $key
+ Value: $value
+EOS
+ do_more_text($key, $value);
+ }
+ elsif ($type eq 'tIME') {
+ my @when = unpack("nCCCCC", $payload);
+ printf " Date: %d-%02d-%02d %02d:%02d:%02d\n", @when;
+ }
+ elsif ($type eq 'bKGD') {
+ if ($colour_type == 2 || $colour_type == 6) {
+ my @rgb = unpack("nnn", $payload);
+ printf " Background: rgb$bits(%d,%d,%d)\n", @rgb;
+ }
+ elsif ($colour_type == 0 || $colour_type == 4) {
+ my $g = unpack("n", $payload);
+ printf " Background: grey$bits(%d)\n", $g;
+ }
+ if ($colour_type == 3) {
+ my $index = unpack("C", $payload);
+ printf " Background: index(%d)\n", $index;
+ }
+ }
+ elsif ($type eq "IDAT" && $image) {
+ $sline_len
+ or die "IDAT before IHDR!?";
+ my $raw = do_inflate($payload);
+ if ($sline_left) {
+ print " Continuing $row:\n";
+ print " ", unpack("H*", substr($raw, 0, $sline_left, "")), "\n";
+ $sline_left = 0;
+ ++$row;
+ }
+ while (length $raw >= $sline_len) {
+ my $row_data = substr($raw, 0, $sline_len, "");
+ my ($filter, $data) = unpack("CH*", $row_data);
+ print " Row $row, filter $filter\n";
+ print " $data\n";
+ ++$row;
+ }
+ if (length $raw) {
+ $sline_left = $sline_len - length $raw;
+ my ($filter, $data) = unpack("CH*", $raw);
+ print " Row $row, filter $filter (partial)\n";
+ print " $data\n" if length $data;
+ }
+ }
+
+ $type eq "IEND"
+ and last;
+}
+
+sub do_more_text {
+ my ($key, $text) = @_;
+
+ if ($key eq 'Raw profile type xmp'
+ && $text =~ s/^\s*xmp\s+\d+\s+//) {
+ print " XMP: ", pack("H*", join('', split ' ', $text)), "\n";
+ }
+}
+
+sub read_chunk {
+ my ($fh) = @_;
+
+ my $rlen;
+ read($fh, $rlen, 4)
+ or die "Cannot read chunk length\n";
+ my $len = unpack("N", $rlen);
+ my $type;
+ read($fh, $type, 4)
+ or die "Cannot read chunk type\n";
+ my $payload = "";
+ if ($rlen) {
+ read($fh, $payload, $len) == $len
+ or die "Cannot read payload\n";
+ }
+ my $crc;
+ read($fh, $crc, 4) == 4
+ or die "Cannot read CRC\n";
+
+ return ( $len + 12, $rlen . $type . $payload . $crc, $len, $type, $payload, $crc );
+}
+
+sub dump_data {
+ my ($offset, $data) = @_;
+
+ if (length $data > 28) {
+ if ($dumpall) {
+ for my $i (0 .. int((15 + length $data) / 16) - 1) {
+ my $row = substr($data, $i * 16, 16);
+ (my $clean = $row) =~ tr/ -~/./c;
+ printf("%08x: %-32s %s\n", $offset, unpack("H*", $row), $clean);
+ }
+ }
+ else {
+ printf "%08x: %s...\n", $offset, unpack("H*", substr($data, 0, 26));
+ }
+ }
+ else {
+ printf "%08x: %s\n", $offset, unpack("H*", $data), "\n";
+ }
+}
+
+#unsigned long crc_table[256];
+my @crc_table;
+
+#/* Flag: has the table been computed? Initially false. */
+# int crc_table_computed = 0;
+
+# /* Make the table for a fast CRC. */
+# void make_crc_table(void)
+# {
+sub make_crc_table {
+# unsigned long c;
+# int n, k;
+#
+# for (n = 0; n < 256; n++) {
+ for my $n (0 .. 255) {
+# c = (unsigned long) n;
+ my $c = $n;
+# for (k = 0; k < 8; k++) {
+ for my $k (0 .. 7) {
+# if (c & 1)
+# c = 0xedb88320L ^ (c >> 1);
+# else
+# c = c >> 1;
+ if ($c & 1) {
+ $c = 0xedb88320 ^ ($c >> 1);
+ }
+ else {
+ $c = $c >> 1;
+ }
+# }
+ }
+# crc_table[n] = c;
+ $crc_table[$n] = $c;
+# }
+ }
+# crc_table_computed = 1;
+# }
+}
+
+# /* Update a running CRC with the bytes buf[0..len-1]--the CRC
+# should be initialized to all 1's, and the transmitted value
+# is the 1's complement of the final running CRC (see the
+# crc() routine below). */
+
+# unsigned long update_crc(unsigned long crc, unsigned char *buf,
+# int len)
+# {
+sub update_crc {
+ my ($crc, $data) = @_;
+# unsigned long c = crc;
+# int n;
+
+# if (!crc_table_computed)
+# make_crc_table();
+ @crc_table or make_crc_table();
+# for (n = 0; n < len; n++) {
+# c = crc_table[(c ^ buf[n]) & 0xff] ^ (c >> 8);
+# }
+ for my $code (unpack("C*", $data)) {
+ $crc = $crc_table[($crc ^ $code) & 0xFF] ^ ($crc >> 8);
+ }
+# return c;
+# }
+ return $crc;
+}
+
+# /* Return the CRC of the bytes buf[0..len-1]. */
+# unsigned long crc(unsigned char *buf, int len)
+# {
+# return update_crc(0xffffffffL, buf, len) ^ 0xffffffffL;
+# }
+
+sub crc {
+ my $data = shift;
+
+ return update_crc(0xFFFFFFFF, $data) ^ 0xFFFFFFFF;
+}
+
+sub do_inflate {
+ my $z = shift;
+ my $out = '';
+ inflate(\$z, \$out);
+
+ return $out;
+}
+
+=head HEAD
+
+pngdump.pl - dump the structure of a PNG image file.
+
+=head1 SYNOPSIS
+
+ perl [-dumpall] [-image] pngdump.pl filename
+
+=head1 DESCRIPTION
+
+Dumps the structure of a PNG image file, listing chunk types, length,
+CRC and optionally the entire content of each chunk.
+
+Options:
+
+=over
+
+=item *
+
+C<-dumpall> - dump the entire contents of each chunk rather than just
+the leading bytes.
+
+=item *
+
+C<-image> - decompress the image data (IDAT chunk) and break the
+result into rows, listing the filter and filtered (raw) data for each
+row.
+
+=back
+
+Several chunk types are extracted and displayed in a more readable
+form.
+
+=cut
=head2 PNG
-There are no PNG specific tags.
+=head3 PNG Image modes
+
+PNG files can be read and written in the following modes:
+
+=over
+
+=item *
+
+bi-level - written as a 1-bit per sample gray scale image
+
+=item *
+
+paletted - Imager gray scale paletted images are written as RGB
+paletted images. PNG palettes can include alpha values for each entry
+and this is honored as an Imager four channel paletted image.
+
+=item *
+
+8 and 16-bit per sample gray scale, optionally with an alpha channel.
+
+=item *
+
+8 and 16-bit per sample RGB, optionally with an alpha channel.
+
+=back
+
+Unlike GIF, there is no automatic conversion to a paletted image,
+since PNG supports direct color.
+
+=head3 PNG Text tags
+
+Text tags are retrieved from and written to PNG C<tEXT> or C<zTXT>
+chunks. The following standard tags from the PNG specification are
+directly supported:
+
+=over
+
+=item *
+
+C<i_comment>X<tags,i_comment> - keyword of "Comment".
+
+=item *
+
+C<png_author>X<tags,PNG,png_author> - keyword "Author".
+
+=item *
+
+C<png_copyright>X<tags,PNG,png_copyright> - keyword "Copyright".
+
+=item *
+
+C<png_creation_time>X<tags,PNG,png_creation_time> - keyword "Creation Time".
+
+=item *
+
+C<png_description>X<tags,PNG,png_description> - keyword "Description".
+
+=item *
+
+C<png_disclaimer>X<tags,PNG,png_disclaimer> - keyword "Disclaimer".
+
+=item *
+
+C<png_software>X<tags,PNG,png_software> - keyword "Software".
+
+=item *
+
+C<png_title>X<tags,PNG,png_title> - keyword "Title".
+
+=item *
+
+C<png_warning>X<tags,PNG,png_warning> - keyword "Warning".
+
+=back
+
+Each of these tags has a corresponding C< I<base-tag-name>_compressed
+>> tag, eg. C<png_comment_compressed>. When reading, if the PNG chunk
+is compressed this tag will be set to 1, but is otherwise unset. When
+writing, Imager will honor the compression tag if set and non-zero,
+otherwise the chunk text will be compressed if the value is longer
+than 1000 characters, as recommended by the C<libpng> documentation.
+
+PNG C<tEXT> or C<zTXT> chunks outside of those above are read into or
+written from Imager tags named like:
+
+=over
+
+=item *
+
+C<< png_textI<N>_key >> - the key for the text chunk. This can be 1
+to 79 characters, may not contain any leading, trailing or consecutive
+spaces, and may contain only Latin-1 characters from 32-126, 161-255.
+
+=item *
+
+C<< png_textI<N>_text >> - the text for the text chunk. This may not
+contain any C<NUL> characters.
+
+=item *
+
+C<< png_textI<N>_compressed >> - whether or not the text chunk is
+compressed. This behaves similarly to the C<<
+I<base-tag-name>_compressed >> tags described above.
+
+=back
+
+Where I<N> starts from 0. When writing both the C<..._key> and
+C<..._text> tags must be present or the write will fail. If the key
+or text do not satisfy the requirements above the write will fail.
+
+=head3 Other PNG metadata tags
+
+=over
+
+=item *
+
+X<tags, png_interlace>C<png_interlace>, C<png_interlace_name> - only
+set when reading, C<png_interlace> is set to the type of interlacing
+used by the file, 0 for one, 1 for Adam7. C<png_interlace_name> is
+set to a keyword describing the interlacing, either C<none> or
+C<adam7>.
+
+=item *
+
+X<tags, png_srgb_intent>C<png_srgb_intent> - the sRGB rendering intent
+for the image. an integer from 0 to 3, per the PNG specification. If
+this chunk is found in the PNG file the C<gAMA> and C<cHRM> are
+ignored and the C<png_gamme> and C<png_chroma_...> tags are not set.
+Similarly when writing if C<png_srgb_intent> is set the C<gAMA> and
+C<cHRM> chunks are not written.
+
+=item *
+
+C<tags, png_gamma>C<png_gamma> - the gamma of the image. This value is
+not currently used by Imager when processing the image, but this may
+change in the future.
+
+=item *
+
+X<tags, png_chroma_...>C<png_chroma_white_x>, C<png_chroma_white_y>,
+C<png_chroma_red_x>, C<png_chroma_red_y>, C<png_chroma_green_x>,
+C<png_chroma_green_y>, C<png_chroma_blue_x>, C<png_chroma_blue_y> -
+the primary chromaticities of the image, defining the color model.
+This is currently not used by Imager when processing the image, but
+this may change in the future.
+
+=item *
+
+C<i_xres>, C<i_yres>, C<i_aspect_only> - processed per
+I<Imager::ImageTypes/CommonTags>.
+
+=item *
+
+X<tags, png_bits>C<png_bits> - the number of bits per sample in the
+representation. Ignored when writing.
+
+=item *
+
+X<tags, png_time>X<png_time> - the creation time of the file formatted
+as C<< I<year>-I<month>-I<day>TI<hour>:I<minute>:I<second> >>. This
+is stored as time data structure in the file, not a string. If you
+set C<png_time> and it cannot be parsed as above, writing the PNG file
+will fail.
+
+=item *
+
+C<i_background> - set from the C<sBKG> when reading an image file.
+
+=back
=head2 ICO (Microsoft Windows Icon) and CUR (Microsoft Windows Cursor)
BMP
Blit
CGI
+chromaticities
CMYK
CPAN
FreeType
RGB
RGBA
SGI
+sRGB
TGA
TIFF
UTF-8