/*
 * Based on example.c from libpng by Guy Eric Schalnat, Andreas Dilger,
 * Glenn Randers-Pehrson et al.  Any bugs are my fault. -- smar@reptiles.org
 */

#include "xli.h"
#include "imagetypes.h"
#include "pbm.h"
#include <png.h>
#include <assert.h>

#define TITLE_KEYWORD "Title"

/* check to see if a file is a png file using png_check_sig() */
static int check_png(char *file_name)
{
	ZFILE *zfp;
	char buf[8];
	int ret;

	zfp = zopen(file_name);
	if (!zfp)
		return 0;
	ret = zread(zfp, buf, 8);
	zclose(zfp);

	if (ret != 8)
		return 0;

	ret = png_check_sig(buf, 8);

	return (ret);
}


static void describe_png(char *name, png_struct *png, png_info *info)
{
	int bd, ct;

	ct = png_get_color_type(png, info);
	bd = png_get_bit_depth(png, info);

	printf("%s is a%s %lux%lu %d bit deep %s PNG image",
		name,
		png_get_interlace_type(png, info) ? "n interlaced" : "",
		png_get_image_width(png, info),
		png_get_image_height(png, info),
		bd,
		(ct & PNG_COLOR_MASK_PALETTE) ? "paletted" :
			(ct & PNG_COLOR_MASK_COLOR) ? "RGB" : "grayscale");
	if (ct & PNG_COLOR_MASK_ALPHA)
		printf(" with an alpha channel");
	if (png_get_valid(png, info, PNG_INFO_tRNS))
		printf(" with transparency");
	if (png_get_valid(png, info, PNG_INFO_bKGD)) {
		png_color_16 *bg;
		int d;

		png_get_bKGD(png, info, &bg);
		d = (bd + 3) / 4;
		printf(", background = ");
		if (!(ct & PNG_COLOR_MASK_COLOR)) {
			printf("#%0*x", d, bg->gray);
		} else if (PNG_COLOR_TYPE_PALETTE == ct) {
			png_color *bp;
			int np;

			png_get_PLTE(png, info, &bp, &np);
			bp += bg->index;
			printf("index %d: #%02x%02x%02x", bg->index,
			       bp->red, bp->green, bp->blue);
		} else {
			printf("#%0*x%0*x%0*x", d, bg->red, d, bg->green,
				d, bg->blue);
		}
	}
	if (png_get_valid(png, info, PNG_INFO_gAMA)) {
		double g;

		png_get_gAMA(png, info, &g);
		printf(", gamma = %g", g);
	}
	printf("\n");
}


static void xli_png_read_data(png_struct *png, png_byte *data,
	png_size_t length)
{
	ZFILE *zfp;

	zfp = (ZFILE *) png_get_io_ptr(png);
	if (zread(zfp, (byte *) data, (int) length) != length)
		png_error(png, "unexpected EOF in xli_png_read_data");
}


static void xli_png_error(png_struct * png_ptr, png_const_charp error_msg)
{
	fputs(error_msg, stderr);
	putc('\n', stderr);
	longjmp(*((jmp_buf *) png_get_error_ptr(png_ptr)), 1);
}


Image *pngLoad(char *fullname, ImageOptions * opt, boolean verbose)
{
	ZFILE *zfp;
	png_struct *png;
	png_info *info;
	Image *image = (Image *) 0;
	int orig_depth = 0;
	byte **row = (byte **) 0;
	jmp_buf jmpbuf;
	int bit_depth, color_type;

	/* open the file */
	if (!check_png(fullname))
		return (Image *) 0;

	zfp = zopen(fullname);
	if (!zfp) {
		perror("pngLoad");
		return (Image *) 0;
	}
	png = png_create_read_struct(PNG_LIBPNG_VER_STRING,
		(void *) &jmpbuf, xli_png_error, (png_error_ptr) 0);

	if (!png) {
		zclose(zfp);
		return (Image *) 0;
	}

	info = png_create_info_struct(png);
	if (!info) {
		zclose(zfp);
		png_destroy_read_struct(&png, (png_infopp) 0, (png_infopp) 0);
		return (Image *) 0;
	}

	/* scary non-local transfer of control action */
	if (setjmp(jmpbuf)) {
		png_destroy_read_struct(&png, &info, (png_infopp) 0);
		zclose(zfp);
		if (row)
			lfree((byte *) row);
		return image;
	}

	/* set up the input control */
	png_set_read_fn(png, (void *) zfp, xli_png_read_data);

	/* read the file information */
	png_read_info(png, info);

	if (verbose)
		describe_png(fullname, png, info);

	znocache(zfp);

	/* the rules:
	   bit depth over 8        ITRUE
	   rgb colour              ITRUE
	   1 bit gray              IBITMAP
	   other gray              IRGB
	   palette colour          IRGB
	 */

	bit_depth = png_get_bit_depth(png, info);
	color_type = png_get_color_type(png, info);
	if (bit_depth < 8 && (PNG_COLOR_TYPE_RGB == color_type ||
			PNG_COLOR_TYPE_RGB_ALPHA == color_type))
		png_set_expand(png);

	/* Set the background color to draw transparent and alpha images over */
	if ((color_type & PNG_COLOR_MASK_ALPHA) ||
			(png_get_valid(png, info, PNG_INFO_tRNS) && (opt->bg ||
			png_get_valid(png, info, PNG_INFO_bKGD)))) {
		png_color_16 bg;
		int expand = 0;
		int gflag = PNG_BACKGROUND_GAMMA_SCREEN;
		double gval = 1.0;

		bg.red = bg.green = bg.blue = bg.gray = 0;
		if (PNG_COLOR_TYPE_PALETTE == color_type || opt->bg)
			png_set_expand(png);

		if (opt->bg) {
			XColor xc;
			int shift = ((color_type & PNG_COLOR_MASK_ALPHA)
				&& 16 == bit_depth) ? 0 : 8;

			xc.flags = DoRed | DoGreen | DoBlue;
			xliParseXColor(&(globals.dinfo), opt->bg, &xc);
			bg.red = xc.red >> shift;
			bg.green = xc.green >> shift;
			bg.blue = xc.blue >> shift;
			bg.gray = (int) (xc.red * .299 + xc.green * .587 +
				xc.blue * .114 + .5) >> shift;
		} else if (png_get_valid(png, info, PNG_INFO_bKGD)) {
			png_color_16 *bgp;

			png_get_bKGD(png, info, &bgp);
			bg = *bgp;
			expand = 1;
			gflag = PNG_BACKGROUND_GAMMA_FILE;
		}
		png_set_background(png, &bg, gflag, expand, gval);
	}
	if (bit_depth < 8 && (bit_depth > 1 ||
			PNG_COLOR_TYPE_GRAY != color_type)) {
		if (PNG_COLOR_TYPE_GRAY == color_type)
			orig_depth = bit_depth;
		png_set_packing(png);
	}
	/* tell libpng to strip 16 bit depth files down to 8 bits */
	if (bit_depth > 8)
		png_set_strip_16(png);

	if (png_get_interlace_type(png, info))
		png_set_interlace_handling(png);

	/* update palette with transformations, update the info structure */
	png_read_update_info(png, info);
	bit_depth = png_get_bit_depth(png, info);
	color_type = png_get_color_type(png, info);

	/* allocate the memory to hold the image using the fields
	 *  of png_info.
	 */
	if (PNG_COLOR_TYPE_GRAY == color_type && 1 == bit_depth) {
		image = newBitImage(png_get_image_width(png, info),
			png_get_image_height(png, info));
		png_set_invert_mono(png);
	} else if (PNG_COLOR_TYPE_PALETTE == color_type) {
		int i, np;
		png_color *pp;

		image = newRGBImage(png_get_image_width(png, info),
			png_get_image_height(png, info), bit_depth);
		png_get_PLTE(png, info, &pp, &np);
		for (i = 0; i < np; ++i) {
			image->rgb.red[i] = pp[i].red * 0x101;
			image->rgb.green[i] = pp[i].green * 0x101;
			image->rgb.blue[i] = pp[i].blue * 0x101;
		}
		image->rgb.used = np;
	} else if (PNG_COLOR_TYPE_GRAY == color_type) {
		int i;
		int depth = orig_depth ? orig_depth : bit_depth;
		int maxval = (1 << depth) - 1;

		image = newRGBImage(png_get_image_width(png, info),
			png_get_image_height(png, info), depth);
		for (i = 0; i <= maxval; i++) {
			image->rgb.red[i] = PM_SCALE(i, maxval, 0xffff);
			image->rgb.green[i] = PM_SCALE(i, maxval, 0xffff);
			image->rgb.blue[i] = PM_SCALE(i, maxval, 0xffff);
		}
		image->rgb.used = maxval + 1;
	} else {
		image = newTrueImage(png_get_image_width(png, info),
			png_get_image_height(png, info));
	}

	if (png_get_valid(png, info, PNG_INFO_gAMA) && image->type != IBITMAP) {
		double g;

		png_get_gAMA(png, info, &g);
		image->gamma = 1.0 / g;
	}

	/* read the image */
	if (IBITMAP == image->type) {
		assert((image->width + 7) / 8 == png_get_rowbytes(png, info));
	} else {
		assert(image->width * image->pixlen ==
			png_get_rowbytes(png, info));
	}

	{
		int i;

		row = (byte **) lmalloc(image->height * sizeof(byte *));
		for (i = 0; i < image->height; ++i)
			row[i] = image->data + i * png_get_rowbytes(png, info);
	}

	png_read_image(png, row);

	/* read the rest of the file, getting any additional chunks in info */
	png_read_end(png, info);

	if (verbose && png_get_valid(png, info, PNG_INFO_tIME)) {
		png_time *tp;

		png_get_tIME(png, info, &tp);
		printf("Modification time: %d/%d/%d %02d:%02d:%02d",
		       tp->year, tp->month, tp->day,
		       tp->hour, tp->minute, tp->second);
	}

	{
		int i, num_text;
		char *title;
		png_text *text;

		title = opt->name;
		num_text = png_get_text(png, info, &text, (int *) 0);
		for (i = 0; i < num_text; ++i) {
			if (verbose)
				printf("\n%s: %s\n", text[i].key, text[i].text);
			if (!strcmp(text[i].key, TITLE_KEYWORD))
				title = text[i].text;
		}
		image->title = dupString(title);
	}

	/* clean up after the read, and free any memory allocated */
	png_destroy_read_struct(&png, &info, (png_infopp) 0);

	/* free the structures */
	lfree((byte *) row);

	/* close the file */
	zclose(zfp);

	/* that's it */
	return image;
}


int pngIdent(char *fullname, char *name)
{
	ZFILE *zfp;
	png_struct *png;
	png_info *info;
	jmp_buf jmpbuf;

	if (!check_png(fullname))
		return 0;

	/* open the file */
	zfp = zopen(fullname);
	if (!zfp) {
		perror("pngIdent");
		return 0;
	}
	/* allocate the necessary structures */
	png = png_create_read_struct(PNG_LIBPNG_VER_STRING,
		(void *) &jmpbuf, xli_png_error, (png_error_ptr) 0);

	if (!png) {
		zclose(zfp);
		return 0;
	}
	info = png_create_info_struct(png);
	if (!info) {
		zclose(zfp);
		png_destroy_read_struct(&png, (png_infopp) 0, (png_infopp) 0);
		return 0;
	}
	/* set error handling */
	if (setjmp(jmpbuf)) {
		png_destroy_read_struct(&png, &info, (png_infopp) 0);
		zclose(zfp);
		return 0;
	}
	/* set up the input control */
	png_set_read_fn(png, (void *) zfp, xli_png_read_data);

	/* read the file information */
	png_read_info(png, info);

	describe_png(fullname, png, info);

	/* clean up after the read, and free any memory allocated */
	png_destroy_read_struct(&png, &info, (png_infopp) 0);

	/* close the file */
	zclose(zfp);

	/* that's it */
	return 1;
}
