#define USE_NO_MINGW_SETJMP_TWO_ARGS
#include "generic/config.h"
#define Z_PREFIX 
#include <png.h>
#undef Byte
#undef FAR

#ifndef PNG_GAMMA_THRESHOLD
#define PNG_GAMMA_THRESHOLD 0.05
#endif
#ifndef PNG_MAX_GAMMA
#define PNG_MAX_GAMMA 11
#endif

#ifndef png_jmpbuf
#  define png_jmpbuf(png_ptr) ((png_ptr)->jmpbuf)
#endif


#include "img.h"
#include "img_conv.h"
#include "Icon.h"

#ifdef BROKEN_PERL_PLATFORM
#undef      setjmp
#undef      longjmp
#define     setjmp _setjmp
#endif

#ifdef __cplusplus
extern "C" {
#endif

static char * pngext[] = { "png", nil };
static int    pngbpp[] = { 
	imRGB, 
	imbpp8 | imGrayScale, imbpp8,
	imbpp4 | imGrayScale, imbpp4,
	imbpp1 | imGrayScale, imbpp1,
	0 
};   
static char * features[] = { 
	"PLTE", "IHDR", 
#ifdef PNG_gAMA_SUPPORTED
	"gAMA", 
#endif
#ifdef PNG_iCCP_SUPPORTED   
	"iCCP",
#endif
#ifdef PNG_bKGD_SUPPORTED   
	"bKGD",
#endif   
#ifdef PNG_TEXT_SUPPORTED
	"tEXt", 
#ifdef PNG_zTXt_SUPPORTED
	"zTXt", 
#endif   
#endif
#ifdef PNG_oFFs_SUPPORTED   
	"oFFs", 
#endif
#ifdef PNG_pHYs_SUPPORTED   
	"pHYs", 
#endif
#ifdef PNG_sCAL_SUPPORTED   
	"sCAL", 
#endif
#ifdef PNG_sRGB_SUPPORTED   
	"sRGB", 
#endif
#ifdef PNG_tRNS_SUPPORTED   
	"tRNS",
#endif   
	nil
};

static char * loadOutput[] = { 
	"background",
	"blending",
	"gamma",
#ifdef PNG_iCCP_SUPPORTED   
	"iccp_name",
	"iccp_profile",
#endif   
	"interlaced",
#ifdef PNG_INTRAPIXEL_DIFFERENCING
	"mng_datastream",
#endif
#ifdef PNG_oFFs_SUPPORTED   
	"offset_x",
	"offset_y",
	"offset_dimension",
#endif   
#ifdef PNG_sRGB_SUPPORTED   
	"render_intent",
#endif   
#ifdef PNG_pHYs_SUPPORTED   
	"resolution_x",
	"resolution_y",
	"resolution_dimension",
#endif
#ifdef PNG_sCAL_SUPPORTED   
	"scale_x",
	"scale_y",
	"scale_unit",
#endif   
#ifdef PNG_TEXT_SUPPORTED
	"text",
#endif   
#ifdef PNG_tRNS_SUPPORTED   
	"transparency_table",
	"transparent_colors",
#endif   
	nil
};   

static ImgCodecInfo codec_info = {
	"PNG",
	"PNG development group, http://www.libpng.org",
#ifdef PNG_LIBPNG_VER_MAJOR
	PNG_LIBPNG_VER_MAJOR, 
#else
	PNG_LIBPNG_VER / 10000,
#endif
#ifdef PNG_LIBPNG_VER_MINOR
	PNG_LIBPNG_VER_MINOR, 
#else
	(PNG_LIBPNG_VER % 10000) / 100,
#endif
	/* version */
	pngext,  /* extension */
	"Portable Network Graphics",     /* file type */
	"PNG",  /* short type */
	features,    /* features  */
	"",     /* module */
	"",     /* package */
	IMG_LOAD_FROM_FILE | IMG_LOAD_FROM_STREAM | IMG_SAVE_TO_FILE | IMG_SAVE_TO_STREAM,
	pngbpp, /* save types */
	loadOutput
};

static double default_screen_gamma = 2.2;

static void * 
init( PImgCodecInfo * info, void * param)
{
	double LUT_exponent;               /* just the lookup table */
	double CRT_exponent = 2.2;         /* just the monitor */
	
	*info = &codec_info;

/* querying display gamma value - code from PNG specs */
#if defined(NeXT)
	LUT_exponent = 1.0 / 2.2;
	/*
	if (some_next_function_that_returns_gamma(&next_gamma))
		LUT_exponent = 1.0 / next_gamma;
	*/
#elif defined(sgi)
	LUT_exponent = 1.0 / 1.7;
	/* there doesn't seem to be any documented function to get the
		"gamma" value, so we do it the hard way - which is unreliable for remote 
		runs anyway */
	{
		FILE * infile = fopen("/etc/config/system.glGammaVal", "r");
		if (infile) {
			double sgi_gamma;
			char tmpline[80];

			fgets(tmpline, 80, infile);
			fclose(infile);
			sgi_gamma = atof(tmpline);
			if (sgi_gamma > 0.0)
					LUT_exponent = 1.0 / sgi_gamma;
		}
	}
#elif defined(Macintosh)
	LUT_exponent = 1.8 / 2.61;
	/*
	if (some_mac_function_that_returns_gamma(&mac_gamma))
		LUT_exponent = mac_gamma / 2.61;
	*/
#else
	LUT_exponent = 1.0;   /* assume no LUT:  most PCs */
#endif
	
	/* the defaults above give 1.0, 1.3, 1.5 and 2.2, respectively: */
	default_screen_gamma = LUT_exponent * CRT_exponent;

	/* paranoia checks */
	if ( default_screen_gamma < PNG_GAMMA_THRESHOLD || default_screen_gamma > PNG_MAX_GAMMA) 
		default_screen_gamma = 2.2;
	if ( default_screen_gamma < PNG_GAMMA_THRESHOLD || default_screen_gamma > PNG_MAX_GAMMA) 
		return nil;
	
	return (void*)1;
}


#define outc(x){ strncpy( fi-> errbuf, x, 256); return false;}
#define outcm(dd){ snprintf( fi-> errbuf, 256, "No enough memory (%d bytes)", (int)(dd)); return false;}

static HV *
load_defaults( PImgCodec c)
{
	HV * profile = newHV();
#ifdef PNG_gAMA_SUPPORTED   
	pset_f( gamma, 0.45455); 
#endif   
	pset_f( screen_gamma, default_screen_gamma);
	/*
	if background is not RGB, ( like clInvalid), use 
	whatever it is in the file.
	*/
	pset_f( background, clInvalid);
	/* alpha blending:
		1 - combine user or file background with color channel and alpha channel
		0 - load color channels and alpha channel as is, without blending
	*/
	pset_i( blending, 1); 
	return profile;
}

typedef struct _LoadRec {
	png_structp png_ptr;
	png_infop info_ptr;
	Byte * b8_4;
	Byte * line;
} LoadRec;

static void
#ifdef PNGAPI 
PNGAPI
#endif
warning_fn( png_structp png_ptr, png_const_charp msg) 
{ 
	warn( "%s", msg);
}

static void
#ifdef PNGAPI 
PNGAPI
#endif
error_fn( png_structp png_ptr, png_const_charp msg) 
{
	char * buf = ( char *) png_get_error_ptr( png_ptr); 
	if ( buf) strncpy( buf, msg, 256);
#if PNG_LIBPNG_VER_MAJOR == 1 && PNG_LIBPNG_VER_MINOR < 5
	longjmp( png_ptr-> jmpbuf, 1);
#else
	png_longjmp( png_ptr, 1);
#endif
}

static void
img_png_read (png_structp png_ptr, png_bytep data, png_size_t size)
{
	req_read( (( PImgLoadFileInstance) png_get_io_ptr(png_ptr))-> req, size, data);
}

static void
img_png_write (png_structp png_ptr, png_bytep data, png_size_t size)
{
	req_write( (( PImgLoadFileInstance) png_get_io_ptr(png_ptr))-> req, size, data);
}

static void
img_png_flush (png_structp png_ptr)
{
	req_flush( (( PImgLoadFileInstance) png_get_io_ptr(png_ptr))-> req);
}

static void * 
open_load( PImgCodec instance, PImgLoadFileInstance fi)
{
	LoadRec * l;
	unsigned char buf[8];

	if ( req_seek( fi-> req, 0, SEEK_SET) < 0) return false;
	if ( req_read( fi-> req, 8, buf) < 0) {
		req_seek( fi-> req, 0, SEEK_SET);
		return false;
	}
	if ( png_sig_cmp( buf, 0, 8) != 0) {
		req_seek( fi-> req, 0, SEEK_SET);
		return false;
	}

	fi-> stop = true;
	fi-> frameCount = 1;

	l = malloc( sizeof( LoadRec));
	if ( !l) outcm( sizeof( LoadRec));
	memset( l, 0, sizeof( LoadRec));

	if ( !( l-> png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING,
		fi-> errbuf, error_fn, warning_fn))) {
		free( l);
		return false;
	}

	if ( !( l-> info_ptr = png_create_info_struct(l->png_ptr))) {
		png_destroy_read_struct(&l->png_ptr, (png_infopp)NULL, (png_infopp)NULL);
		free( l);
		return false;
	}

	fi-> instance = l;
	if (setjmp(png_jmpbuf( l-> png_ptr))) {
		/* If we get here, we had a problem inside open_load */
		png_destroy_read_struct(&l-> png_ptr, &l-> info_ptr, (png_infopp)NULL);
		fi-> instance = nil;
		free( l);
		return false;
	}

	png_set_read_fn( l-> png_ptr, fi, img_png_read);
	png_set_sig_bytes( l-> png_ptr, 8);
	return l;
}

static Bool   
load( PImgCodec instance, PImgLoadFileInstance fi)
{
	dPROFILE;
	LoadRec * l = ( LoadRec *) fi-> instance;
	png_uint_32 width, height;
	int obd, bit_depth, color_type, interlace_type, bpp, number_passes, pass, filter;
	HV * profile;
	Color background;
	int channels, trns_n;
	Bool icon, blending, post_blending;
	Byte * alpha;
	png_bytep trns_t;
	png_color_16p trns_p;

	if (setjmp(png_jmpbuf( l-> png_ptr)) != 0) return false;

	profile = fi-> profile;

	png_read_info(l->png_ptr, l->info_ptr);

	png_get_IHDR(l->png_ptr, l->info_ptr, &width, &height, &bit_depth, &color_type,
		&interlace_type, NULL, &filter);
	obd = bit_depth;
	channels = (int) png_get_channels(l->png_ptr, l->info_ptr);
	icon = kind_of( fi-> object, CIcon);
	
	switch ( bit_depth) {
	case 1: case 4: case 8:
		break;
	case 2:
		png_set_packing(l->png_ptr);
		bit_depth = 4;
		break;
	case 16:   
		png_set_strip_16(l->png_ptr);
		bit_depth = 8;
		break;
	default:
		sprintf( fi-> errbuf, "Bit depth %d is not supported", bit_depth);
		return false;
	}

	
	switch ( color_type) {
	case PNG_COLOR_TYPE_GRAY:
		bpp = bit_depth | imGrayScale;
		break;
	case PNG_COLOR_TYPE_PALETTE:
		bpp = bit_depth;
		break;
	case PNG_COLOR_TYPE_RGB:
		png_set_bgr(l-> png_ptr);
		bpp = 24;
		break; 
	case PNG_COLOR_TYPE_GRAY_ALPHA:
		bpp = bit_depth | imGrayScale;
		break; 
	case PNG_COLOR_TYPE_RGB_ALPHA:
		png_set_bgr(l-> png_ptr);
		bpp = 24;
		break; 
	default:
		sprintf( fi-> errbuf, "Unknown file color type: %d", color_type);
		return false;
	}

	/* gamma stuff */
	{
		double screen_gamma = default_screen_gamma, gamma = 0.45455;
		Bool has_gamma = false;

		if ( pexist( screen_gamma)) {
			screen_gamma = pget_f( screen_gamma);
			if ( screen_gamma < PNG_GAMMA_THRESHOLD || screen_gamma > PNG_MAX_GAMMA) {
				sprintf( fi-> errbuf, "Error: screen_gamma value must be within %g..%g", PNG_GAMMA_THRESHOLD, (double)PNG_MAX_GAMMA);
				return false;
			} 
		}

#ifdef PNG_gAMA_SUPPORTED      
		if ( pexist( gamma)) {
			gamma = pget_f( gamma);
			if ( gamma < PNG_GAMMA_THRESHOLD || gamma > PNG_MAX_GAMMA) {
				sprintf( fi-> errbuf, "Error: gamma value must be within %g..%g", PNG_GAMMA_THRESHOLD, (double)PNG_MAX_GAMMA);
				return false;
			}
			has_gamma = true;
		} else if ( png_get_gAMA(l-> png_ptr, l-> info_ptr, &gamma)) {
			has_gamma = true;
			pset_f( gamma, gamma);
		}
#endif      

		if ( has_gamma) png_set_gamma(l->png_ptr, screen_gamma, gamma);
	}

	/* alpha blending */
	blending = pexist( blending) ? pget_B(blending) : 1;
	post_blending = false;
	if ( !icon && !blending)
		png_set_strip_alpha(l-> png_ptr);

	/* image background color */
	background = clInvalid;
#ifdef PNG_bKGD_SUPPORTED   
	if ( png_get_valid( l-> png_ptr, l-> info_ptr, PNG_INFO_bKGD)) {
		RGBColor r;
		png_color_16p pBackground;
		png_get_bKGD(l-> png_ptr, l-> info_ptr, &pBackground);
		
		if (obd == 16) {
			r.r = pBackground->red   >> 8;
			r.g = pBackground->green >> 8;
			r.b = pBackground->blue  >> 8;
		} else if (color_type == PNG_COLOR_TYPE_GRAY && obd < 8) {
			if (obd == 1)
					r.r = r.g = r.b = pBackground->gray? 255 : 0;
			else if (obd == 2)
					r.r = r.g = r.b = (255/3) * pBackground->gray;
			else 
					r.r = r.g = r.b = (255/15) * pBackground->gray;
		} else {
			r.r = (Byte)pBackground->red;
			r.g = (Byte)pBackground->green;
			r.b = (Byte)pBackground->blue;
		}
		background = ARGB(r.r, r.g, r.b);
	}
#endif   
	/* store to load output */
	if ( fi-> loadExtras) {
		HV * profile = fi-> frameProperties;
		if ( background != clInvalid)
			pset_i( background, background);
	}
	/* set blending background */
	if ( icon && pexist( background)) {
		strcpy( fi-> errbuf, "Option 'background' cannot be set when loading to an Icon object");
		return false;
	}
	if ( !icon && blending ) {
		png_color_16 p;
		/* override with user data */
		if ( pexist( background) && ((pget_i( background) & clSysFlag) == 0)) 
			background = pget_i( background);
		p.red   = (background & 0xFF0000) >> 16;
		p.green = (background & 0x00FF00) >> 8;
		p.blue  = (background & 0x0000FF);
		if (
			( color_type == PNG_COLOR_TYPE_GRAY_ALPHA) &&
			(( p.red != p.green) || (p.red != p.blue))
			) { /* backgrounding of GRAY_ALPHA with non-gray color can't be performed */
				/* into gray color space */
			png_set_gray_to_rgb(l-> png_ptr);
			png_set_bgr(l-> png_ptr);
			bpp = 24;
			channels = 4;
		}
		png_set_background(l->png_ptr, &p, PNG_BACKGROUND_GAMMA_SCREEN, 0, 1.0);
	}

	number_passes = png_set_interlace_handling(l-> png_ptr);

	/* loading tRNS information */
	trns_n = 0;
	trns_t = nil;
	trns_p = nil;
#ifdef PNG_tRNS_SUPPORTED   
	if ( png_get_tRNS( l-> png_ptr, l-> info_ptr, &trns_t, &trns_n, &trns_p)) {
		if ( fi-> loadExtras) {
			int i;
			AV * av = newAV();
			HV * profile = fi-> frameProperties;
			for ( i = 0; i < trns_n; i++) 
				av_push( av, newSViv( trns_p[i]. index));
			pset_sv_noinc( transparent_colors, newRV_noinc(( SV*) av));
			if ( trns_t) {
				av = newAV();
				for ( i = 0; i < trns_n; i++) 
					av_push( av, newSViv( trns_t[i]));
				pset_sv_noinc( transparency_table, newRV_noinc(( SV*) av));
			}
		}
	}
#endif   

	/* synchronize final settings */
	png_read_update_info(l-> png_ptr, l-> info_ptr);

	CImage( fi-> object)-> create_empty( fi-> object, 1, 1, bpp);

	/* palette, if any */
	if ( bpp < 24) {
		RGBColor * pal = PImage( fi-> object)-> palette;
		int num_palette, i;
		png_colorp palette;
		if (png_get_PLTE(l->png_ptr, l->info_ptr, &palette, &num_palette)) {
			if ( num_palette > 256) num_palette = 256;
			PImage( fi-> object)-> palSize = num_palette;
			for ( i = 0; i < num_palette; i++, palette++, pal++) {
				pal-> r = palette-> red;
				pal-> g = palette-> green;
				pal-> b = palette-> blue;
			}
		}
	} 

	if ( fi-> noImageData) {
		(void) hv_store( fi-> frameProperties, "width",  5, newSViv( width), 0);
		(void) hv_store( fi-> frameProperties, "height", 6, newSViv( height), 0);
		if ( fi-> loadExtras) { /* skip data and read info blocks */
			for (pass = 0; pass < number_passes; pass++) {
				int y;
				for (y = 0; y < height; y++) 
					png_read_row( l->png_ptr, NULL, NULL);
			}
			goto READ_END;
		}
		return true;
	}

	/* reading bits */
	CImage( fi-> object)-> create_empty( fi-> object, width, height, bpp);

	EVENT_HEADER_READY(fi);

	/* create buffer for 8 to 4 bpp conversion */
	if ( obd == 2) 
		if ( !( l-> b8_4 = malloc( width))) outcm( width);

	/* prepare buffers for alpha strip */
	if ( bpp == imByte && channels == 2) {
		if ( !( l-> line = malloc( width * 2))) outcm( width * 2);
	} else if (( bpp & imBPP) == 24 && channels == 4) {
		if ( !( l-> line = malloc( width * 4))) outcm( width * 4);
	} else if ( color_type & PNG_COLOR_MASK_ALPHA || 
					channels == 2 || 
					channels == 4) {
		blending = 0;
		icon     = 0;
		png_set_strip_alpha(l-> png_ptr);
		warn("Unknown alpha channel coding scheme (%d %d %d %x)", channels, color_type, obd, bpp); 
	}
	
	if (( color_type & PNG_COLOR_MASK_ALPHA) && icon ) { 
		CIcon( fi-> object)-> set_autoMasking( fi-> object, amNone );
		CIcon( fi-> object)-> set_maskType( fi-> object, imbpp8 );
		alpha = PIcon(fi->object)-> mask;
		if (( bpp & imBPP ) != 24 ) post_blending = true; /* cannot blend 8 bits inplace */
	} else
		alpha = NULL;

	/* cycle through scanlines  */
	for (pass = 0; pass < number_passes; pass++) {
		int y;
		Byte * data = PImage( fi-> object)-> data;
		Byte * a_data = alpha;
		
		EVENT_SCANLINES_RESET(fi);
		data += ( height - 1) * PImage( fi-> object)-> lineSize;
		if ( a_data)
			a_data += ( height - 1) * PIcon( fi-> object)-> maskLine;
		for (y = 0; y < height; y++) {
			if ( a_data ) {
				int i;
				Byte * dst = data, * src = l-> line, *a = a_data;
				png_read_row(l->png_ptr, l-> line, NULL);
				if ( channels == 4 ) {
					if ( blending ) {
						for ( i = 0; i < width; i++) {
							register uint16_t r = *src++;
							register uint16_t g = *src++;
							register uint16_t b = *src++;
							register uint16_t A = *src++;
							*dst++ = r * A >> 8;
							*dst++ = g * A >> 8;
							*dst++ = b * A >> 8;
							*a++   = A;
						}
					} else {
						for ( i = 0; i < width; i++) {
							*dst++ = *src++;
							*dst++ = *src++;
							*dst++ = *src++;
							*a++   = *src++;
						}
					}
				} else {
					for ( i = 0; i < width; i++) {
						*dst++ = *src++;
						*a++ = *src++;
					}
				}       
			} else {
				if ( obd == 2) { /* convert from 8-bit down to 4-bit */
					png_read_row(l->png_ptr, l-> b8_4, NULL);
					bc_byte_nibble_cr( l-> b8_4, data, width, map_stdcolorref);
				} else
					png_read_row(l->png_ptr, data, NULL);
			}
			data -= PImage( fi-> object)-> lineSize;
			if ( a_data) a_data -= PIcon( fi-> object)-> maskLine;

			EVENT_TOPDOWN_SCANLINES_READY(fi,1);
		}
		EVENT_TOPDOWN_SCANLINES_FINISHED(fi);
	}

	/* adjusting icon mask if possible */
	if ( icon && !alpha && trns_n ) {
		if ( trns_t) {
			if ( blending ) {
				PRGBColor p = PIcon( fi-> object)-> palette;
				int i, min_ix = 0, min_val = 255;
				/* transparency values per pixel table is present */
				for ( i = 0; i < trns_n; i++) {
					p[i]. r = ((uint16_t) p[i]. r * trns_t[i]) >> 8;
					p[i]. g = ((uint16_t) p[i]. g * trns_t[i]) >> 8;
					p[i]. b = ((uint16_t) p[i]. b * trns_t[i]) >> 8;
				}
			}
			PIcon( fi-> object)-> autoMasking = amNone;
		} else {
			PIcon( fi-> object)-> maskIndex = trns_p[0].index;
			PIcon( fi-> object)-> autoMasking = amMaskIndex;
		}
	}
	if ( post_blending )
		CIcon( fi-> object )-> premultiply_alpha( fi-> object, (SV*) NULL );

	/* misc extras  */
READ_END:   
		
	if ( fi-> loadExtras) {
		HV * profile = fi-> frameProperties;
		char * name, *pf;
		int ct, i;
		png_uint_32 pl, py;
		png_int_32 pli, pyi;
		png_textp tx;
		double scx, scy;

		(void)tx; (void)py; (void)pl; (void)pyi; (void)pli; (void)i; (void)ct;
		(void)pf; (void)name; (void)scx; (void)scy;

		png_read_end(l-> png_ptr, l-> info_ptr);

		pset_i( interlaced, interlace_type == PNG_INTERLACE_ADAM7);
#ifdef PNG_INTRAPIXEL_DIFFERENCING
		pset_i( mng_datastream, filter == PNG_INTRAPIXEL_DIFFERENCING);
#endif
	
#ifdef PNG_sRGB_SUPPORTED
		if ( png_get_sRGB( l-> png_ptr, l-> info_ptr, &i)) {
			char * c = "none";
			switch (i) {
			case PNG_sRGB_INTENT_SATURATION: c = "saturation"; break;
			case PNG_sRGB_INTENT_PERCEPTUAL: c = "perceptual"; break;
			case PNG_sRGB_INTENT_RELATIVE:   c = "relative"; break;
			case PNG_sRGB_INTENT_ABSOLUTE:   c = "absolute"; break;
			} 
			pset_c( render_intent, c);
		}
#endif      

#ifdef PNG_iCCP_SUPPORTED      
		if ( png_get_iCCP( l-> png_ptr, l-> info_ptr, &name, &ct, (png_bytepp)&pf, &pl)) {
			pset_c( iccp_name, name);
			if ( pf) pset_sv_noinc( iccp_profile, newSVpv( pf, pl));
		}
#endif

#ifdef PNG_TEXT_SUPPORTED      
		if ( png_get_text( l-> png_ptr, l-> info_ptr, &tx, &ct)) {
			HV * hash = newHV();
			for ( i = 0; i < ct; i++, tx++) 
				(void) hv_store( hash, tx-> key, strlen( tx-> key), newSVpv( tx-> text, tx-> text_length), 0);
			pset_sv_noinc( text, newRV_noinc(( SV *) hash));
		}
#endif      

#ifdef PNG_oFFs_SUPPORTED      
		if ( png_get_oFFs( l-> png_ptr, l-> info_ptr, &pli, &pyi, &i)) {
			pset_i( offset_x, pli);
			pset_i( offset_y, pyi);
			pset_c( offset_dimension, ( i == PNG_OFFSET_PIXEL) ? "pixel" : "micrometer");
		}
#endif      
	
#ifdef PNG_pHYs_SUPPORTED      
		if ( png_get_pHYs( l-> png_ptr, l-> info_ptr, &pl, &py, &i)) {
			pset_i( resolution_x, pl);
			pset_i( resolution_y, py);
			pset_c( resolution_dimension, ( i == PNG_RESOLUTION_METER) ? "meter" : "unknown");
		}
#endif      
	
#ifdef PNG_sCAL_SUPPORTED      
		if ( png_get_sCAL( l-> png_ptr, l-> info_ptr, &i, &scx, &scy)) {
			pset_f( scale_x, scx);
			pset_f( scale_y, scy);
			pset_c( scale_unit, 
				( i == PNG_SCALE_METER) ? "meter" :
				(( i == PNG_SCALE_RADIAN) ? "radian" : "unknown")
			);
		}
#endif      
	}

	return true;
}

static void
close_load( PImgCodec instance, PImgLoadFileInstance fi)
{
	LoadRec * l = ( LoadRec *) fi-> instance;
	if ( l-> b8_4) free( l-> b8_4);
	if ( l-> line) free( l-> line);
	png_destroy_read_struct(&l-> png_ptr, &l-> info_ptr, (png_infopp)NULL);
	free( l);
}

static HV *
save_defaults( PImgCodec c)
{
	HV * profile = newHV();
	pset_i( background, clInvalid);
#ifdef PNG_gAMA_SUPPORTED   
	pset_f( gamma, 0.45455);
#endif
#ifdef PNG_iCCP_SUPPORTED   
	pset_c( iccp_name, "unspecified");
	pset_c( iccp_profile, "");
#endif   
	pset_i( interlaced, 0);
#ifdef PNG_INTRAPIXEL_DIFFERENCING
	pset_i( mng_datastream, 0);
#endif
#ifdef PNG_oFFs_SUPPORTED   
	pset_i( offset_x, 0);
	pset_i( offset_y, 0);
	pset_c( offset_dimension, "pixel");
#endif
#ifdef PNG_sRGB_SUPPORTED
	pset_c( render_intent, "none");
#endif   
#ifdef PNG_pHYs_SUPPORTED   
	pset_i( resolution_x, 0);
	pset_i( resolution_y, 0);
	pset_c( resolution_dimension, "meter");
#endif   
#ifdef PNG_sCAL_SUPPORTED
	pset_f( scale_x, 1);
	pset_f( scale_y, 1);
	pset_c( scale_unit, "unknown");
#endif   
#ifdef PNG_TEXT_SUPPORTED   
	pset_sv_noinc( text, newRV_noinc((SV*)newHV()));
#endif
#ifdef PNG_tRNS_SUPPORTED   
	pset_sv_noinc( transparent_color, newSViv(clInvalid));
	pset_sv_noinc( transparent_color_index, newSViv(-1));
#endif   
	return profile;
}

typedef struct _SaveRec {
	png_structp png_ptr;
	png_infop info_ptr;
	Byte * b8_4;
	Byte * line;
} SaveRec;

static void *
open_save( PImgCodec instance, PImgSaveFileInstance fi)
{
	SaveRec * l;
	
	l = malloc( sizeof( SaveRec));
	if ( !l) return nil;
	
	memset( l, 0, sizeof( SaveRec));
	
	if ( !( l-> png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING,
		fi-> errbuf, error_fn, warning_fn))) {
		free( l);
		return false;
	}

	if ( !( l-> info_ptr = png_create_info_struct(l->png_ptr))) {
		png_destroy_write_struct(&l->png_ptr, (png_infopp)NULL);
		free( l);
		return false;
	}

	fi-> instance = l;

	if (setjmp(png_jmpbuf( l-> png_ptr))) {
		/* If we get here, we had a problem inside open_load */
		png_destroy_write_struct(&l-> png_ptr, &l-> info_ptr);
		fi-> instance = nil;
		free( l);
		return false;
	}
	png_set_write_fn( l-> png_ptr, fi, img_png_write, img_png_flush);

	return l;
}

static Bool   
save( PImgCodec instance, PImgSaveFileInstance fi)
{
	dPROFILE;
	PIcon i = ( PIcon) fi-> object;
	SaveRec * l = ( SaveRec *) fi-> instance;
	HV * profile = fi-> objectExtras;
	Bool icon;
	Byte * alpha = NULL;
	
	if ( setjmp( png_jmpbuf( l-> png_ptr)) != 0) return false;
	icon = kind_of( fi-> object, CIcon);

	/* header */
	{
		int bit_depth, color_type, interlace = PNG_INTERLACE_NONE, 
			filter = PNG_FILTER_TYPE_DEFAULT;
		if (( i-> type & imBPP) == 24) {
			bit_depth = 8;
			color_type = PNG_COLOR_TYPE_RGB;
		} else {
			color_type = ( i-> type & imGrayScale) ?
				PNG_COLOR_TYPE_GRAY : PNG_COLOR_TYPE_PALETTE;
			bit_depth = i-> type & imBPP;
		}
		if ( pexist( interlaced) && pget_B( interlaced))
			interlace = PNG_INTERLACE_ADAM7;
#ifdef PNG_INTRAPIXEL_DIFFERENCING
		if ( pexist( mng_datastream) && pget_B( mng_datastream))
			filter = PNG_INTRAPIXEL_DIFFERENCING;
#endif
		if ( icon && i-> autoMasking != amMaskIndex && i-> autoMasking != amMaskColor ) {
			Bool autoConvert;
			{
				/* recognize autoConvert as the image subsystem does */
				HV * profile = fi-> extras;
				autoConvert = pexist( autoConvert) ? pget_B( autoConvert) : true;
			}

			/* png doesn't support paletted images with alpha channel? */
			if ( color_type == PNG_COLOR_TYPE_PALETTE) {
				if ( !autoConvert) 
					outc("Cannot apply alpha channel to a paletted image");
				CImage( i)-> set_type(( Handle) i, imRGB);
				if ( i-> type != imRGB) 
					outc("Failed converting image to type im::RGB");
				color_type = PNG_COLOR_TYPE_RGB;
			}

			/* and does not alpha with bit_depth other that 8 or 24 bits? */
			if ( bit_depth != 8) {
				if ( !autoConvert)
					outc( "Image depth must be of 8 bits to be saved with alpha channel");
				CImage( i)-> set_type(( Handle) i, imbpp8 | ( i-> type & imGrayScale));
				if (( i-> type & imBPP) != 8) 
					outc("Failed converting image to 8-bit");
				bit_depth = 8;
			}
			
			if ( i-> maskType != imbpp8) {
				if ( !autoConvert)
					outc("maskType is not of type im::bpp8");
				i-> self-> set_maskType( fi-> object, imbpp8);
				if ( i-> maskType != imbpp8 )
					outc("Failed converting icon mask to type im::bpp8");
			}
			if (( l-> line = malloc( i-> w * (( color_type == PNG_COLOR_TYPE_GRAY) ? 2 : 4)))) {
				alpha = i-> mask;
				color_type |= PNG_COLOR_MASK_ALPHA;
			}
		}
		png_set_IHDR( l-> png_ptr, l-> info_ptr, i-> w, i-> h, bit_depth, color_type, 
			interlace, PNG_COMPRESSION_TYPE_DEFAULT, filter);
	}

	/* palette */
	if ((( i-> type & imBPP) <= 8) && ((i-> type & imGrayScale) == 0)) {
		RGBColor * pal = i-> palette;
		int num_palette, j;
		png_color palette[256];
		
		num_palette = ( i-> palSize < 256) ? i-> palSize : 256;
		for ( j = 0; j < num_palette; j++, pal++) {
			palette[j].red   = pal-> r;
			palette[j].green = pal-> g;
			palette[j].blue  = pal-> b;
		}
		png_set_PLTE(l->png_ptr, l->info_ptr, palette, num_palette);
	}

	/* sRGB */
#ifdef PNG_sRGB_SUPPORTED   
	if ( pexist( render_intent)) {
		char * c = pget_c( render_intent);
		if ( stricmp( c, "none") != 0) {
			int i;
			if ( stricmp( c, "saturation") == 0) i = PNG_sRGB_INTENT_SATURATION; else
			if ( stricmp( c, "perceptual") == 0) i = PNG_sRGB_INTENT_PERCEPTUAL; else
			if ( stricmp( c, "relative")   == 0) i = PNG_sRGB_INTENT_RELATIVE; else
			if ( stricmp( c, "absolute")   == 0) i = PNG_sRGB_INTENT_ABSOLUTE; else {
				snprintf( fi-> errbuf, 256, "Unknown render_intent option '%s'", c);
				return false;
			}
		}
	}
#endif   

	/* gamma */
#ifdef PNG_gAMA_SUPPORTED   
	if ( pexist( gamma)) {
		double gamma = pget_f( gamma);
		if ( gamma < PNG_GAMMA_THRESHOLD || gamma > PNG_MAX_GAMMA) {
			sprintf( fi-> errbuf, "Gamma value must be within %g..%g", PNG_GAMMA_THRESHOLD, (double)PNG_MAX_GAMMA);
			return false;
		}
		png_set_gAMA( l-> png_ptr, l-> info_ptr, gamma);
	}
#endif   

	/* iccp */
#ifdef PNG_iCCP_SUPPORTED   
	if ( pexist( iccp_name) || pexist( iccp_profile)) {
		char * prf = nil;
		STRLEN plen = 0;
		char * name = pexist( iccp_name) ? pget_c( iccp_name) : "unspecified";
		if ( pexist( iccp_profile)) prf = SvPV( pget_sv( iccp_profile), plen);
		png_set_iCCP( l-> png_ptr, l-> info_ptr, name, 0, (void*) prf, plen);
	}
#endif   

	/* tRNS */
#ifdef PNG_tRNS_SUPPORTED   
	{
		png_color_16 trns_p[256];
		png_byte trns_t[256];
		int trns_n = 0;
		Color color = pexist( transparent_color) ? pget_i( transparent_color) : clInvalid;
		int   index = pexist( transparent_color_index) ? pget_i( transparent_color_index) : -1;

		if ( index > i-> palSize)
			index = i-> palSize - 1;
		if ( icon && (i-> autoMasking == amMaskIndex) && ( index < 0))
			index = i-> maskIndex;

		if ( color & clSysFlag)
			color = clInvalid;
		if ( icon && (i-> autoMasking == amMaskColor) && ( color == clInvalid))
			color = i-> maskColor;
		memset( trns_p, 0, sizeof( trns_p));
		if ( color != clInvalid || index >= 0) {
			memset( trns_t, 255, sizeof( trns_t));
			if (( i-> type & imBPP) < 24) {
				int x;
				if ( index < 0) {
					RGBColor r;
					r.r = (color & 0xFF0000) >> 16;
					r.g = (color & 0x00FF00) >> 8;
					r.b = (color & 0x0000FF);
					x = cm_nearest_color( r, i-> palSize, i-> palette);
				} else {
					x = index;
				}
				trns_t[x] = 0;
				trns_n = x + 1;
				trns_p[0].index = x;
				trns_p[x].index = x;
				png_set_tRNS(l->png_ptr, l-> info_ptr, trns_t, trns_n, trns_p);
			} else if ( color != clInvalid) {
				trns_p[0].red   = (color & 0xFF0000) >> 16;
				trns_p[0].green = (color & 0x00FF00) >> 8;
				trns_p[0].blue  = (color & 0x0000FF);
				png_set_tRNS(l->png_ptr, l-> info_ptr, nil, 1, trns_p);
			}
		
			/* maybe it is a good idea to adjust automatically backgound  */
			if ( !pexist( background) || (pget_i( background) & clSysFlag)) 
				pset_i( background, color);
		}
	}
#endif   
	
	/* bKGD */
#ifdef PNG_bKGD_SUPPORTED   
	if ( pexist( background)) {
		Color background = pget_i( background);
		if (( background & clSysFlag) == 0) {
			png_color_16 p;
			p.red   = (background & 0xFF0000) >> 16;
			p.green = (background & 0x00FF00) >> 8;
			p.blue  = (background & 0x0000FF);
			png_set_bKGD(l-> png_ptr, l-> info_ptr, &p);
		}
	}
#endif   

	/* text */
#ifdef PNG_TEXT_SUPPORTED   
	if ( pexist( text)) {
		HV * hv;
		HE * he;
		SV * sv = pget_sv( text);
		png_text * txt;
		int i = 0, len;
			
		if ( !SvOK(sv) || !SvROK(sv) || SvTYPE(SvRV(sv)) != SVt_PVHV)
			outc("'text' is not a hash");
		hv = ( HV*) SvRV( sv);
		len = HvKEYS(( HV*) hv);
		if ( !( txt = malloc( len * sizeof( png_text)))) outcm( len * sizeof( png_text));
		memset( txt, 0, len * sizeof( png_text));

		hv_iterinit( hv);
		for (;;) {
			STRLEN vlen;
			if (( he = hv_iternext( hv)) == nil) break;
			txt[i]. compression = 
#ifdef PNG_zTXt_SUPPORTED            
				PNG_TEXT_COMPRESSION_zTXt;
#else
				PNG_TEXT_COMPRESSION_NONE;
#endif
			txt[i]. key =  (char*) HeKEY( he);
			txt[i]. text = (char*) SvPV( HeVAL( he), vlen);
			txt[i]. text_length = vlen;
			i++;
		}
		png_set_text(l-> png_ptr, l-> info_ptr, txt, len);
		free( txt);
	}
#endif   

	/* offset */
#ifdef PNG_oFFs_SUPPORTED   
	if ( pexist( offset_x) || pexist( offset_y) || pexist( offset_dimension)) {
		int ox = pexist( offset_x) ? pget_i( offset_x) : 0;
		int oy = pexist( offset_y) ? pget_i( offset_y) : 0;
		int dm = PNG_OFFSET_PIXEL;
		
		if ( pexist( offset_dimension)) {
			char * c = pget_c( offset_dimension);
			if ( stricmp( c, "pixel") == 0) dm = PNG_OFFSET_PIXEL; else
			if ( stricmp( c, "micrometer") == 0) dm = PNG_OFFSET_MICROMETER; else {
				snprintf( fi-> errbuf, 256, "unknown offset_dimension '%s'", c);
				return false;
			}
		}
		png_set_oFFs( l-> png_ptr, l-> info_ptr, ox, oy, dm);   
	}
#endif   

	/* phys */
#ifdef PNG_pHYs_SUPPORTED   
	if ( pexist( resolution_x) || pexist( resolution_y) || pexist( resolution_dimension)) {
		int ox = pexist( resolution_x) ? pget_i( resolution_x) : 0;
		int oy = pexist( resolution_y) ? pget_i( resolution_y) : 0;
		int dm = PNG_RESOLUTION_METER;
		
		if ( pexist( resolution_dimension)) {
			char * c = pget_c( resolution_dimension);
			if ( stricmp( c, "unknown") == 0) dm = PNG_RESOLUTION_UNKNOWN; else
			if ( stricmp( c, "meter") == 0)   dm = PNG_RESOLUTION_METER; else {
				snprintf( fi-> errbuf, 256, "unknown resolution_dimension '%s'", c);
				return false;
			}
		}
		png_set_pHYs( l-> png_ptr, l-> info_ptr, ox, oy, dm);   
	}
#endif   
	
	/* scale */
#ifdef PNG_sCAL_SUPPORTED   
	if ( pexist( scale_x) || pexist( scale_y) || pexist( scale_unit)) {
		double ox = pexist( scale_x) ? pget_f( scale_x) : 1;
		double oy = pexist( scale_y) ? pget_f( scale_y) : 1;
		int dm = PNG_SCALE_UNKNOWN;
		
		if ( pexist( scale_unit)) {
			char * c = pget_c( scale_unit);
			if ( stricmp( c, "unknown") == 0) dm = PNG_SCALE_UNKNOWN; else
			if ( stricmp( c, "meter") == 0)   dm = PNG_SCALE_METER;   else 
			if ( stricmp( c, "radian") == 0)   dm = PNG_SCALE_RADIAN; else {
				snprintf( fi-> errbuf, 256, "unknown scale_unit '%s'", c);
				return false;
			}
		}
		png_set_sCAL( l-> png_ptr, l-> info_ptr, dm, ox, oy);   
	}
#endif   

	/* done with header, write it now */
	png_write_info(l-> png_ptr, l-> info_ptr);

	if (( i-> type & imBPP) == 24) png_set_bgr(l-> png_ptr);

	/* writing data */
	{
		int pass, num_pass = png_set_interlace_handling(l-> png_ptr);
		for (pass = 0; pass < num_pass; pass++) {
			int h = i-> h;
			Byte * data = i-> data + (i-> h - 1) * i-> lineSize;
			Byte * a_data = alpha;
			if ( a_data )
				a_data += ( h - 1) * i-> maskLine;
			while ( h--) {
				if ( a_data ) {
					int j;
					Byte * src = data, * dst = l-> line, *a = a_data;
					if ( i-> type == imRGB) {
						for ( j = 0; j < i-> w; j++) {
							*dst++ = *src++;
							*dst++ = *src++;
							*dst++ = *src++;
							*dst++ = *a++;
						}
					} else {
						for ( j = 0; j < i-> w; j++) {
							*dst++ = *src++;
							*dst++ = *a++;
						}
					}
					png_write_row( l-> png_ptr, l-> line);
				} else {
					png_write_row( l-> png_ptr, data);
				}
				data -= i-> lineSize;
				if ( a_data) a_data -= i-> maskLine;
			}
		}
	}

	/* end - now comments or time, if any */
	png_write_end(l-> png_ptr, NULL);
	return true;
}   

static void 
close_save( PImgCodec instance, PImgSaveFileInstance fi)
{
	SaveRec * l = ( SaveRec *) fi-> instance;
	png_destroy_write_struct(&l-> png_ptr, &l-> info_ptr);
	if ( l-> b8_4) free( l-> b8_4);
	if ( l-> line) free( l-> line);
	free( l);
}

void 
apc_img_codec_png( void )
{
	struct ImgCodecVMT vmt;

	int ver_release=
#ifdef PNG_LIBPNG_VER_RELEASE
	PNG_LIBPNG_VER_RELEASE
#else
	PNG_LIBPNG_VER % 100
#endif
	;

	if ( png_access_version_number() != codec_info.versionMaj * 10000 + codec_info.versionMin * 100 + ver_release) {
		unsigned int v = (unsigned int) png_access_version_number();
		warn("Application built with libpng-%d.%d.%d but running with %d.%d.%d\n", 
			codec_info.versionMaj, codec_info.versionMin, ver_release,
			v / 10000, (v % 10000) / 100, v % 100);
		return;
	}

	memcpy( &vmt, &CNullImgCodecVMT, sizeof( CNullImgCodecVMT));

	vmt. init          = init;

	vmt. load_defaults = load_defaults;
	vmt. open_load     = open_load;
	vmt. load          = load; 
	vmt. close_load    = close_load; 

	vmt. save_defaults = save_defaults;
	vmt. open_save     = open_save;
	vmt. save          = save; 
	vmt. close_save    = close_save;
	
	apc_img_register( &vmt, nil);
}


#ifdef __cplusplus
}
#endif
