diff options
Diffstat (limited to 'src/eom-metadata-reader-png.c')
-rw-r--r-- | src/eom-metadata-reader-png.c | 648 |
1 files changed, 648 insertions, 0 deletions
diff --git a/src/eom-metadata-reader-png.c b/src/eom-metadata-reader-png.c new file mode 100644 index 0000000..2092f62 --- /dev/null +++ b/src/eom-metadata-reader-png.c @@ -0,0 +1,648 @@ +/* Eye Of MATE -- PNG Metadata Reader + * + * Copyright (C) 2008 The Free Software Foundation + * + * Author: Felix Riemann <[email protected]> + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, 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; + } + + cmsErrorAction (LCMS_ERROR_SHOW); + + 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; + LPGAMMATABLE 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 (256, gammaValue); + + profile = cmsCreateRGBProfile (&whitepoint, &primaries, gamma); + + cmsFreeGamma(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 +} |