/* Eye Of MATE -- PNG Metadata Reader
 *
 * Copyright (C) 2008 The Free Software Foundation
 *
 * Author: Felix Riemann <friemann@svn.gnome.org>
 *
 * Based on the old EomMetadataReader code.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <string.h>
#include <zlib.h>

#include "eom-metadata-reader.h"
#include "eom-metadata-reader-png.h"
#include "eom-debug.h"

typedef enum {
	EMR_READ_MAGIC,
	EMR_READ_SIZE_HIGH_HIGH_BYTE,
	EMR_READ_SIZE_HIGH_LOW_BYTE,
	EMR_READ_SIZE_LOW_HIGH_BYTE,
	EMR_READ_SIZE_LOW_LOW_BYTE,
	EMR_READ_CHUNK_NAME,
	EMR_SKIP_BYTES,
	EMR_CHECK_CRC,
	EMR_SKIP_CRC,
	EMR_READ_XMP_ITXT,
	EMR_READ_ICCP,
	EMR_READ_SRGB,
	EMR_READ_CHRM,
	EMR_READ_GAMA,
	EMR_FINISHED
} EomMetadataReaderPngState;

#if 0
#define IS_FINISHED(priv) (priv->icc_chunk  != NULL && \
                           priv->xmp_chunk  != NULL)
#endif

struct _EomMetadataReaderPngPrivate {
	EomMetadataReaderPngState  state;

	/* data fields */
	guint32  icc_len;
	gpointer icc_chunk;

	gpointer xmp_chunk;
	guint32  xmp_len;

	guint32	 sRGB_len;
	gpointer sRGB_chunk;

	gpointer cHRM_chunk;
	guint32	 cHRM_len;

	guint32	 gAMA_len;
	gpointer gAMA_chunk;

	/* management fields */
	gsize      size;
	gsize      bytes_read;
	guint	   sub_step;
	guchar	   chunk_name[4];
	gpointer   *crc_chunk;
	guint32	   *crc_len;
	guint32    target_crc;
	gboolean   hasIHDR;
};

#define EOM_METADATA_READER_PNG_GET_PRIVATE(object) \
	(G_TYPE_INSTANCE_GET_PRIVATE ((object), EOM_TYPE_METADATA_READER_PNG, EomMetadataReaderPngPrivate))

static void
eom_metadata_reader_png_init_emr_iface (gpointer g_iface, gpointer iface_data);

G_DEFINE_TYPE_WITH_CODE (EomMetadataReaderPng, eom_metadata_reader_png,
			 G_TYPE_OBJECT,
			 G_IMPLEMENT_INTERFACE (EOM_TYPE_METADATA_READER,
			 		eom_metadata_reader_png_init_emr_iface))

static void
eom_metadata_reader_png_dispose (GObject *object)
{
	EomMetadataReaderPng *emr = EOM_METADATA_READER_PNG (object);
	EomMetadataReaderPngPrivate *priv = emr->priv;

	g_free (priv->xmp_chunk);
	priv->xmp_chunk = NULL;

	g_free (priv->icc_chunk);
	priv->icc_chunk = NULL;

	g_free (priv->sRGB_chunk);
	priv->sRGB_chunk = NULL;

	g_free (priv->cHRM_chunk);
	priv->cHRM_chunk = NULL;

	g_free (priv->gAMA_chunk);
	priv->gAMA_chunk = NULL;

	G_OBJECT_CLASS (eom_metadata_reader_png_parent_class)->dispose (object);
}

static void
eom_metadata_reader_png_init (EomMetadataReaderPng *obj)
{
	EomMetadataReaderPngPrivate *priv;

	priv = obj->priv =  EOM_METADATA_READER_PNG_GET_PRIVATE (obj);
	priv->icc_chunk = NULL;
	priv->icc_len = 0;
	priv->xmp_chunk = NULL;
	priv->xmp_len = 0;
	priv->sRGB_chunk = NULL;
	priv->sRGB_len = 0;
	priv->cHRM_chunk = NULL;
	priv->cHRM_len = 0;
	priv->gAMA_chunk = NULL;
	priv->gAMA_len = 0;

	priv->sub_step = 0;
	priv->state = EMR_READ_MAGIC;
	priv->hasIHDR = FALSE;
}

static void
eom_metadata_reader_png_class_init (EomMetadataReaderPngClass *klass)
{
	GObjectClass *object_class = (GObjectClass*) klass;

	object_class->dispose = eom_metadata_reader_png_dispose;

	g_type_class_add_private (klass, sizeof (EomMetadataReaderPngPrivate));
}

static gboolean
eom_metadata_reader_png_finished (EomMetadataReaderPng *emr)
{
	g_return_val_if_fail (EOM_IS_METADATA_READER_PNG (emr), TRUE);

	return (emr->priv->state == EMR_FINISHED);
}


static void
eom_metadata_reader_png_get_next_block (EomMetadataReaderPngPrivate* priv,
				    	guchar *chunk,
					int* i,
					const guchar *buf,
					int len,
					EomMetadataReaderPngState state)
{
	if (*i + priv->size < len) {
		/* read data in one block */
		memcpy ((guchar*) (chunk) + priv->bytes_read, &buf[*i], priv->size);
		priv->state = EMR_CHECK_CRC;
		*i = *i + priv->size - 1; /* the for-loop consumes the other byte */
		priv->size = 0;
	} else {
		int chunk_len = len - *i;
		memcpy ((guchar*) (chunk) + priv->bytes_read, &buf[*i], chunk_len);
		priv->bytes_read += chunk_len; /* bytes already read */
		priv->size = (*i + priv->size) - len; /* remaining data to read */
		*i = len - 1;
		priv->state = state;
	}
}

static void
eom_metadata_reader_png_consume (EomMetadataReaderPng *emr, const guchar *buf, guint len)
{
	EomMetadataReaderPngPrivate *priv;
 	int i;
	guint32 chunk_crc;
	static const gchar PNGMAGIC[8] = "\x89PNG\x0D\x0A\x1a\x0A";

	g_return_if_fail (EOM_IS_METADATA_READER_PNG (emr));

	priv = emr->priv;

	if (priv->state == EMR_FINISHED) return;

	for (i = 0; (i < len) && (priv->state != EMR_FINISHED); i++) {

		switch (priv->state) {
		case EMR_READ_MAGIC:
			/* Check PNG magic string */
			if (priv->sub_step < 8 &&
			    (gchar)buf[i] == PNGMAGIC[priv->sub_step]) {
			    	if (priv->sub_step == 7)
					priv->state = EMR_READ_SIZE_HIGH_HIGH_BYTE;
				priv->sub_step++;
			} else {
				priv->state = EMR_FINISHED;
			}
			break;
		case EMR_READ_SIZE_HIGH_HIGH_BYTE:
			/* Read the high byte of the size's high word */
			priv->size |= (buf[i] & 0xFF) << 24;
			priv->state = EMR_READ_SIZE_HIGH_LOW_BYTE;
			break;
		case EMR_READ_SIZE_HIGH_LOW_BYTE:
			/* Read the low byte of the size's high word */
			priv->size |= (buf[i] & 0xFF) << 16;
			priv->state = EMR_READ_SIZE_LOW_HIGH_BYTE;
			break;
		case EMR_READ_SIZE_LOW_HIGH_BYTE:
			/* Read the high byte of the size's low word */
			priv->size |= (buf [i] & 0xff) << 8;
			priv->state = EMR_READ_SIZE_LOW_LOW_BYTE;
			break;
		case EMR_READ_SIZE_LOW_LOW_BYTE:
			/* Read the high byte of the size's low word */
			priv->size |= (buf [i] & 0xff);
			/* The maximum chunk length is 2^31-1 */
			if (G_LIKELY (priv->size <= (guint32) 0x7fffffff)) {
				priv->state = EMR_READ_CHUNK_NAME;
				/* Make sure sub_step is 0 before next step */
				priv->sub_step = 0;
			} else {
				priv->state = EMR_FINISHED;
				eom_debug_message (DEBUG_IMAGE_DATA,
						   "chunk size larger than "
						   "2^31-1; stopping parser");
			}

			break;
		case EMR_READ_CHUNK_NAME:
			/* Read the 4-byte chunk name */
			if (priv->sub_step > 3)
				g_assert_not_reached ();

			priv->chunk_name[priv->sub_step] = buf[i];

			if (priv->sub_step++ != 3)
				break;

			if (G_UNLIKELY (!priv->hasIHDR)) {
				/* IHDR should be the first chunk in a PNG */
				if (priv->size == 13
				    && memcmp (priv->chunk_name, "IHDR", 4) == 0){
					priv->hasIHDR = TRUE;
				} else {
					/* Stop parsing if it is not */
					priv->state = EMR_FINISHED;
				}
			}

			/* Try to identify the chunk by its name.
			 * Already do some sanity checks where possible */
			if (memcmp (priv->chunk_name, "iTXt", 4) == 0 &&
			    priv->size > (22 + 54) && priv->xmp_chunk == NULL) {
				priv->state = EMR_READ_XMP_ITXT;
			} else if (memcmp (priv->chunk_name, "iCCP", 4) == 0 &&
				   priv->icc_chunk == NULL) {
				priv->state = EMR_READ_ICCP;
			} else if (memcmp (priv->chunk_name, "sRGB", 4) == 0 &&
				   priv->sRGB_chunk == NULL && priv->size == 1) {
				priv->state = EMR_READ_SRGB;
			} else if (memcmp (priv->chunk_name, "cHRM", 4) == 0 &&
				   priv->cHRM_chunk == NULL && priv->size == 32) {
				priv->state = EMR_READ_CHRM;
			} else if (memcmp (priv->chunk_name, "gAMA", 4) == 0 &&
				   priv->gAMA_chunk == NULL && priv->size == 4) {
				priv->state = EMR_READ_GAMA;
			} else if (memcmp (priv->chunk_name, "IEND", 4) == 0) {
				priv->state = EMR_FINISHED;
			} else {
				/* Skip chunk + 4-byte CRC32 value */
				priv->size += 4;
				priv->state = EMR_SKIP_BYTES;
			}
			priv->sub_step = 0;
			break;
		case EMR_SKIP_CRC:
			/* Skip the 4-byte CRC32 value following every chunk */
			priv->size = 4;
		case EMR_SKIP_BYTES:
		/* Skip chunk and start reading the size of the next one */
			eom_debug_message (DEBUG_IMAGE_DATA,
					   "Skip bytes: %" G_GSIZE_FORMAT,
					   priv->size);

			if (i + priv->size < len) {
				i = i + priv->size - 1; /* the for-loop consumes the other byte */
				priv->size = 0;
				priv->state = EMR_READ_SIZE_HIGH_HIGH_BYTE;
			}
			else {
				priv->size = (i + priv->size) - len;
				i = len - 1;
			}
			break;
		case EMR_CHECK_CRC:
			/* Read the chunks CRC32 value from the file,... */
			if (priv->sub_step == 0)
				priv->target_crc = 0;

			priv->target_crc |= buf[i] << ((3 - priv->sub_step) * 8);

			if (priv->sub_step++ != 3)
				break;

			/* ...generate the chunks CRC32,... */
			chunk_crc = crc32 (crc32 (0L, Z_NULL, 0), priv->chunk_name, 4);
			chunk_crc = crc32 (chunk_crc, *priv->crc_chunk, *priv->crc_len);

			eom_debug_message (DEBUG_IMAGE_DATA, "Checking CRC: Chunk: 0x%X - Target: 0x%X", chunk_crc, priv->target_crc);

			/* ...and check if they match. If they don't throw
			 * the chunk away and stop parsing. */
			if (priv->target_crc == chunk_crc) {
				priv->state = EMR_READ_SIZE_HIGH_HIGH_BYTE;
			} else {
				g_free (*priv->crc_chunk);
				*priv->crc_chunk = NULL;
				*priv->crc_len = 0;
				/* Stop parsing for security reasons */
				priv->state = EMR_FINISHED;
			}
			priv->sub_step = 0;
			break;
		case EMR_READ_XMP_ITXT:
			/* Extract an iTXt chunk possibly containing
			 * an XMP packet */
			eom_debug_message (DEBUG_IMAGE_DATA,
					   "Read XMP Chunk - size: %"
					   G_GSIZE_FORMAT, priv->size);

			if (priv->xmp_chunk == NULL) {
				priv->xmp_chunk = g_new0 (guchar, priv->size);
				priv->xmp_len = priv->size;
				priv->crc_len = &priv->xmp_len;
				priv->bytes_read = 0;
				priv->crc_chunk = &priv->xmp_chunk;
			}
			eom_metadata_reader_png_get_next_block (priv,
							    priv->xmp_chunk,
							    &i, buf, len,
							    EMR_READ_XMP_ITXT);

			if (priv->state == EMR_CHECK_CRC) {
				/* Check if it is actually an XMP chunk.
				 * Throw it away if not.
				 * The check has 4 extra \0's to check
				 * if the chunk is configured correctly. */
				if (memcmp (priv->xmp_chunk, "XML:com.adobe.xmp\0\0\0\0\0", 22) != 0) {
					priv->state = EMR_SKIP_CRC;
					g_free (priv->xmp_chunk);
					priv->xmp_chunk = NULL;
					priv->xmp_len = 0;
				}
			}
			break;
		case EMR_READ_ICCP:
			/* Extract an iCCP chunk containing a
			 * deflated ICC profile. */
			eom_debug_message (DEBUG_IMAGE_DATA,
					   "Read ICC Chunk - size: %"
					   G_GSIZE_FORMAT, priv->size);

			if (priv->icc_chunk == NULL) {
				priv->icc_chunk = g_new0 (guchar, priv->size);
				priv->icc_len = priv->size;
				priv->crc_len = &priv->icc_len;
				priv->bytes_read = 0;
				priv->crc_chunk = &priv->icc_chunk;
			}

			eom_metadata_reader_png_get_next_block (priv,
							    priv->icc_chunk,
							    &i, buf, len,
							    EMR_READ_ICCP);
			break;
		case EMR_READ_SRGB:
			/* Extract the sRGB chunk. Marks the image data as
			 * being in sRGB colorspace. */
			eom_debug_message (DEBUG_IMAGE_DATA,
					   "Read sRGB Chunk - value: %u", *(buf+i));

			if (priv->sRGB_chunk == NULL) {
				priv->sRGB_chunk = g_new0 (guchar, priv->size);
				priv->sRGB_len = priv->size;
				priv->crc_len = &priv->sRGB_len;
				priv->bytes_read = 0;
				priv->crc_chunk = &priv->sRGB_chunk;
			}

			eom_metadata_reader_png_get_next_block (priv,
							    priv->sRGB_chunk,
							    &i, buf, len,
							    EMR_READ_SRGB);
			break;
		case EMR_READ_CHRM:
			/* Extract the cHRM chunk. Contains the coordinates of
			 * the image's whitepoint and primary chromacities. */
			eom_debug_message (DEBUG_IMAGE_DATA,
					   "Read cHRM Chunk - size: %"
					   G_GSIZE_FORMAT, priv->size);

			if (priv->cHRM_chunk == NULL) {
				priv->cHRM_chunk = g_new0 (guchar, priv->size);
				priv->cHRM_len = priv->size;
				priv->crc_len = &priv->cHRM_len;
				priv->bytes_read = 0;
				priv->crc_chunk = &priv->cHRM_chunk;
			}

			eom_metadata_reader_png_get_next_block (priv,
							    priv->cHRM_chunk,
							    &i, buf, len,
							    EMR_READ_ICCP);
			break;
		case EMR_READ_GAMA:
			/* Extract the gAMA chunk containing the
			 * image's gamma value */
			eom_debug_message (DEBUG_IMAGE_DATA,
					   "Read gAMA-Chunk - size: %"
					   G_GSIZE_FORMAT, priv->size);

			if (priv->gAMA_chunk == NULL) {
				priv->gAMA_chunk = g_new0 (guchar, priv->size);
				priv->gAMA_len = priv->size;
				priv->crc_len = &priv->gAMA_len;
				priv->bytes_read = 0;
				priv->crc_chunk = &priv->gAMA_chunk;
			}

			eom_metadata_reader_png_get_next_block (priv,
							    priv->gAMA_chunk,
							    &i, buf, len,
							    EMR_READ_ICCP);
			break;
		default:
			g_assert_not_reached ();
		}
	}
}

#ifdef HAVE_EXEMPI

/* skip the chunk ID */
#define EOM_XMP_OFFSET (22)

static gpointer
eom_metadata_reader_png_get_xmp_data (EomMetadataReaderPng *emr )
{
	EomMetadataReaderPngPrivate *priv;
	XmpPtr xmp = NULL;

	g_return_val_if_fail (EOM_IS_METADATA_READER_PNG (emr), NULL);

	priv = emr->priv;

	if (priv->xmp_chunk != NULL) {
		xmp = xmp_new (priv->xmp_chunk+EOM_XMP_OFFSET,
			       priv->xmp_len-EOM_XMP_OFFSET);
	}

	return (gpointer) xmp;
}
#endif

#ifdef HAVE_LCMS

#define EXTRACT_DOUBLE_UINT_BLOCK_OFFSET(chunk,offset,divider) \
		(double)(GUINT32_FROM_BE(*((guint32*)((chunk)+((offset)*4))))/(double)(divider))

/* This is the amount of memory the inflate output buffer gets increased by
 * while decompressing the ICC profile */
#define EOM_ICC_INFLATE_BUFFER_STEP 1024

/* I haven't seen ICC profiles larger than 1MB yet.
 * A maximum output buffer of 5MB should be enough. */
#define EOM_ICC_INFLATE_BUFFER_LIMIT (1024*1024*5)

static gpointer
eom_metadata_reader_png_get_icc_profile (EomMetadataReaderPng *emr)
{
	EomMetadataReaderPngPrivate *priv;
	cmsHPROFILE profile = NULL;

	g_return_val_if_fail (EOM_IS_METADATA_READER_PNG (emr), NULL);

	priv = emr->priv;

	if (priv->icc_chunk) {
		gpointer outbuf;
		gsize offset = 0;
		z_stream zstr;
		int z_ret;

		/* Use default allocation functions */
		zstr.zalloc = Z_NULL;
		zstr.zfree = Z_NULL;
		zstr.opaque = Z_NULL;

		/* Skip the name of the ICC profile */
		while (*((guchar*)priv->icc_chunk+offset) != '\0')
			offset++;
		/* Ensure the compression method (deflate) */
		if (*((guchar*)priv->icc_chunk+(++offset)) != '\0')
			return NULL;
		++offset; //offset now points to the start of the deflated data

		/* Prepare the zlib data structure for decompression */
		zstr.next_in = priv->icc_chunk + offset;
		zstr.avail_in = priv->icc_len - offset;
		if (inflateInit (&zstr) != Z_OK) {
			return NULL;
		}

		/* Prepare output buffer and make zlib aware of it */
		outbuf = g_malloc (EOM_ICC_INFLATE_BUFFER_STEP);
		zstr.next_out = outbuf;
		zstr.avail_out = EOM_ICC_INFLATE_BUFFER_STEP;

		do {
			if (zstr.avail_out == 0) {
				/* The output buffer was not large enough to
				 * hold all the decompressed data. Increase its
				 * size and continue decompression. */
				gsize new_size = zstr.total_out + EOM_ICC_INFLATE_BUFFER_STEP;

				if (G_UNLIKELY (new_size > EOM_ICC_INFLATE_BUFFER_LIMIT)) {
					/* Enforce a memory limit for the output
					 * buffer to avoid possible OOM cases */
					inflateEnd (&zstr);
					g_free (outbuf);
					eom_debug_message (DEBUG_IMAGE_DATA, "ICC profile is too large. Ignoring.");
					return NULL;
				}
				outbuf = g_realloc(outbuf, new_size);
				zstr.avail_out = EOM_ICC_INFLATE_BUFFER_STEP;
				zstr.next_out = outbuf + zstr.total_out;
			}
			z_ret = inflate (&zstr, Z_SYNC_FLUSH);
		} while (z_ret == Z_OK);

		if (G_UNLIKELY (z_ret != Z_STREAM_END)) {
			eom_debug_message (DEBUG_IMAGE_DATA, "Error while inflating ICC profile: %s (%d)", zstr.msg, z_ret);
			inflateEnd (&zstr);
			g_free (outbuf);
			return NULL;
		}

		profile = cmsOpenProfileFromMem(outbuf, zstr.total_out);
		inflateEnd (&zstr);
		g_free (outbuf);

		eom_debug_message (DEBUG_LCMS, "PNG has %s ICC profile", profile ? "valid" : "invalid");
	}

	if (!profile && priv->sRGB_chunk) {
		eom_debug_message (DEBUG_LCMS, "PNG is sRGB");
		/* If the file has an sRGB chunk the image data is in the sRGB
		 * colorspace. lcms has a built-in sRGB profile. */

		profile = cmsCreate_sRGBProfile ();
	}

	if (!profile && priv->cHRM_chunk) {
		cmsCIExyY whitepoint;
		cmsCIExyYTRIPLE primaries;
		cmsToneCurve *gamma[3];
		double gammaValue = 2.2; // 2.2 should be a sane default gamma

		/* This uglyness extracts the chromacity and whitepoint values
		 * from a PNG's cHRM chunk. These can be accurate up to the
		 * 5th decimal point.
		 * They are saved as integer values multiplied by 100000. */

		eom_debug_message (DEBUG_LCMS, "Trying to calculate color profile");

		whitepoint.x = EXTRACT_DOUBLE_UINT_BLOCK_OFFSET (priv->cHRM_chunk, 0, 100000);
		whitepoint.y = EXTRACT_DOUBLE_UINT_BLOCK_OFFSET (priv->cHRM_chunk, 1, 100000);

		primaries.Red.x = EXTRACT_DOUBLE_UINT_BLOCK_OFFSET (priv->cHRM_chunk, 2, 100000);
		primaries.Red.y = EXTRACT_DOUBLE_UINT_BLOCK_OFFSET (priv->cHRM_chunk, 3, 100000);
		primaries.Green.x = EXTRACT_DOUBLE_UINT_BLOCK_OFFSET (priv->cHRM_chunk, 4, 100000);
		primaries.Green.y = EXTRACT_DOUBLE_UINT_BLOCK_OFFSET (priv->cHRM_chunk, 5, 100000);
		primaries.Blue.x = EXTRACT_DOUBLE_UINT_BLOCK_OFFSET (priv->cHRM_chunk, 6, 100000);
		primaries.Blue.y = EXTRACT_DOUBLE_UINT_BLOCK_OFFSET (priv->cHRM_chunk, 7, 100000);

		primaries.Red.Y = primaries.Green.Y = primaries.Blue.Y = 1.0;

		/* If the gAMA_chunk is present use its value which is saved
		 * the same way as the whitepoint. Use 2.2 as default value if
		 * the chunk is not present. */
		if (priv->gAMA_chunk)
			gammaValue = (double) 1.0/EXTRACT_DOUBLE_UINT_BLOCK_OFFSET (priv->gAMA_chunk, 0, 100000);

		gamma[0] = gamma[1] = gamma[2] = cmsBuildGamma (NULL, gammaValue);

		profile = cmsCreateRGBProfile (&whitepoint, &primaries, gamma);

		cmsFreeToneCurve(gamma[0]);
	}

	return profile;
}
#endif

static void
eom_metadata_reader_png_init_emr_iface (gpointer g_iface, gpointer iface_data)
{
	EomMetadataReaderInterface *iface;

	iface = (EomMetadataReaderInterface*) g_iface;

	iface->consume =
		(void (*) (EomMetadataReader *self, const guchar *buf, guint len))
			eom_metadata_reader_png_consume;
	iface->finished =
		(gboolean (*) (EomMetadataReader *self))
			eom_metadata_reader_png_finished;
#ifdef HAVE_LCMS
	iface->get_icc_profile =
		(cmsHPROFILE (*) (EomMetadataReader *self))
			eom_metadata_reader_png_get_icc_profile;
#endif
#ifdef HAVE_EXEMPI
	iface->get_xmp_ptr =
		(gpointer (*) (EomMetadataReader *self))
			eom_metadata_reader_png_get_xmp_data;
#endif
}