summaryrefslogtreecommitdiffstats
path: root/libgimpbase/gimpmetadata.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 03:13:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 03:13:10 +0000
commit3c57dd931145d43f2b0aef96c4d178135956bf91 (patch)
tree3de698981e9f0cc2c4f9569b19a5f3595e741f6b /libgimpbase/gimpmetadata.c
parentInitial commit. (diff)
downloadgimp-3c57dd931145d43f2b0aef96c4d178135956bf91.tar.xz
gimp-3c57dd931145d43f2b0aef96c4d178135956bf91.zip
Adding upstream version 2.10.36.upstream/2.10.36
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'libgimpbase/gimpmetadata.c')
-rw-r--r--libgimpbase/gimpmetadata.c1816
1 files changed, 1816 insertions, 0 deletions
diff --git a/libgimpbase/gimpmetadata.c b/libgimpbase/gimpmetadata.c
new file mode 100644
index 0000000..9f4f53b
--- /dev/null
+++ b/libgimpbase/gimpmetadata.c
@@ -0,0 +1,1816 @@
+/* LIBGIMPBASE - The GIMP Basic Library
+ * Copyright (C) 1995-1997 Peter Mattis and Spencer Kimball
+ *
+ * gimpmetadata.c
+ * Copyright (C) 2013 Hartmut Kuhse <hartmutkuhse@src.gnome.org>
+ * Michael Natterer <mitch@gimp.org>
+ *
+ * This library is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+#include <gio/gio.h>
+#include <gexiv2/gexiv2.h>
+
+#include "libgimpmath/gimpmath.h"
+
+#include "gimpbasetypes.h"
+
+#include "gimplimits.h"
+#include "gimpmetadata.h"
+#include "gimpunit.h"
+
+#include "libgimp/libgimp-intl.h"
+
+typedef struct _GimpMetadataClass GimpMetadataClass;
+typedef struct _GimpMetadataPrivate GimpMetadataPrivate;
+
+struct _GimpMetadata
+{
+ GExiv2Metadata parent_instance;
+};
+
+struct _GimpMetadataPrivate
+{
+ /* dummy entry to avoid a critical warning due to size 0 */
+ gpointer _gimp_reserved1;
+};
+
+struct _GimpMetadataClass
+{
+ GExiv2MetadataClass parent_class;
+
+ /* Padding for future expansion */
+ void (*_gimp_reserved1) (void);
+ void (*_gimp_reserved2) (void);
+ void (*_gimp_reserved3) (void);
+ void (*_gimp_reserved4) (void);
+ void (*_gimp_reserved5) (void);
+ void (*_gimp_reserved6) (void);
+ void (*_gimp_reserved7) (void);
+ void (*_gimp_reserved8) (void);
+};
+
+/**
+ * SECTION: gimpmetadata
+ * @title: GimpMetadata
+ * @short_description: Basic functions for handling #GimpMetadata objects.
+ * @see_also: gimp_image_metadata_load_prepare(),
+ * gimp_image_metadata_load_finish(),
+ * gimp_image_metadata_save_prepare(),
+ * gimp_image_metadata_save_finish().
+ *
+ * Basic functions for handling #GimpMetadata objects.
+ **/
+
+
+#define GIMP_METADATA_ERROR gimp_metadata_error_quark ()
+
+static GQuark gimp_metadata_error_quark (void);
+static void gimp_metadata_copy_tag (GExiv2Metadata *src,
+ GExiv2Metadata *dest,
+ const gchar *tag);
+static void gimp_metadata_copy_tags (GExiv2Metadata *src,
+ GExiv2Metadata *dest,
+ const gchar **tags);
+static void gimp_metadata_add (GimpMetadata *src,
+ GimpMetadata *dest);
+
+
+static const gchar *tiff_tags[] =
+{
+ "Xmp.tiff",
+ "Exif.Image.ImageWidth",
+ "Exif.Image.ImageLength",
+ "Exif.Image.BitsPerSample",
+ "Exif.Image.Compression",
+ "Exif.Image.PhotometricInterpretation",
+ "Exif.Image.FillOrder",
+ "Exif.Image.SamplesPerPixel",
+ "Exif.Image.StripOffsets",
+ "Exif.Image.RowsPerStrip",
+ "Exif.Image.StripByteCounts",
+ "Exif.Image.PlanarConfiguration"
+};
+
+static const gchar *jpeg_tags[] =
+{
+ "Exif.Image.JPEGProc",
+ "Exif.Image.JPEGInterchangeFormat",
+ "Exif.Image.JPEGInterchangeFormatLength",
+ "Exif.Image.JPEGRestartInterval",
+ "Exif.Image.JPEGLosslessPredictors",
+ "Exif.Image.JPEGPointTransforms",
+ "Exif.Image.JPEGQTables",
+ "Exif.Image.JPEGDCTables",
+ "Exif.Image.JPEGACTables"
+};
+
+static const gchar *unsupported_tags[] =
+{
+ "Exif.Image.SubIFDs",
+ "Exif.Image.ClipPath",
+ "Exif.Image.XClipPathUnits",
+ "Exif.Image.YClipPathUnits",
+ "Exif.Image.XPTitle",
+ "Exif.Image.XPComment",
+ "Exif.Image.XPAuthor",
+ "Exif.Image.XPKeywords",
+ "Exif.Image.XPSubject",
+ "Exif.Image.DNGVersion",
+ "Exif.Image.DNGBackwardVersion",
+ "Exif.Iop",
+ /* FIXME Even though adding the tags below fixes the issue it's not very flexible.
+ It might be better in the long run if there was a way for a user to configure which
+ tags to block or a way for us to detect problems with tags before writing them. */
+ /* Issues #1367, #2253. Offending tag is PreviewOffset but the other Preview tags
+ (PreviewResolution, PreviewLength, PreviewImageBorders) also make no sense because
+ we are not including a Pentax specific preview image. */
+ "Exif.Pentax.Preview",
+ "Exif.PentaxDng.Preview",
+ /* Never save the complete brand specific MakerNote data. We load and
+ * should only save the specific brand tags inside the MakerNote.
+ * Sometimes the MakerNote is invalid or exiv2 doesn't know how to parse
+ * it. In that case we still get the (invalid) MakerNote, but not the
+ * individual tags or just a subset of them.
+ * If there are recognized brand specific tags, exiv2 will create the
+ * required MakerNote itself (which in can still be invalid but that's an
+ * exiv2 issue not ours). */
+ "Exif.Photo.MakerNote",
+ "Exif.MakerNote.ByteOrder",
+ "Exif.MakerNote.Offset",
+ /* Photoshop resources can contain sensitive data. We should not save the
+ * unedited original state. */
+ "Exif.Image.ImageResources",
+ "Exif.Image.0x935c",
+};
+
+static const guint8 minimal_exif[] =
+{
+ 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01,
+ 0x01, 0x01, 0x00, 0x5a, 0x00, 0x5a, 0x00, 0x00, 0xff, 0xe1
+};
+
+static const guint8 wilber_jpg[] =
+{
+ 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01,
+ 0x01, 0x01, 0x00, 0x5a, 0x00, 0x5a, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43,
+ 0x00, 0x50, 0x37, 0x3c, 0x46, 0x3c, 0x32, 0x50, 0x46, 0x41, 0x46, 0x5a,
+ 0x55, 0x50, 0x5f, 0x78, 0xc8, 0x82, 0x78, 0x6e, 0x6e, 0x78, 0xf5, 0xaf,
+ 0xb9, 0x91, 0xc8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x55, 0x5a,
+ 0x5a, 0x78, 0x69, 0x78, 0xeb, 0x82, 0x82, 0xeb, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x10, 0x00, 0x10, 0x03,
+ 0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00,
+ 0x16, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x01, 0x02, 0xff, 0xc4, 0x00,
+ 0x1e, 0x10, 0x00, 0x01, 0x05, 0x00, 0x02, 0x03, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x03, 0x11, 0x31,
+ 0x04, 0x12, 0x51, 0x61, 0x71, 0xff, 0xc4, 0x00, 0x14, 0x01, 0x01, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0xff, 0xc4, 0x00, 0x14, 0x11, 0x01, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11,
+ 0x00, 0x3f, 0x00, 0x18, 0xa0, 0x0e, 0x6d, 0xbc, 0xf5, 0xca, 0xf7, 0x78,
+ 0xb6, 0xfe, 0x3b, 0x23, 0xb2, 0x1d, 0x64, 0x68, 0xf0, 0x8a, 0x39, 0x4b,
+ 0x74, 0x9c, 0xa5, 0x5f, 0x35, 0x8a, 0xb2, 0x7e, 0xa0, 0xff, 0xd9, 0x00
+};
+
+static const guint wilber_jpg_len = G_N_ELEMENTS (wilber_jpg);
+
+G_DEFINE_TYPE_WITH_PRIVATE (GimpMetadata, gimp_metadata, GEXIV2_TYPE_METADATA)
+
+
+static void
+gimp_metadata_class_init (GimpMetadataClass *klass)
+{
+ if (! gexiv2_metadata_register_xmp_namespace ("http://ns.adobe.com/DICOM/",
+ "DICOM"))
+ {
+ g_printerr ("Failed to register XMP namespace 'DICOM'\n");
+ }
+
+ if (! gexiv2_metadata_register_xmp_namespace ("http://darktable.sf.net/",
+ "darktable"))
+ {
+ g_printerr ("Failed to register XMP namespace 'darktable'\n");
+ }
+
+ /* Usage example Xmp.GIMP.tagname */
+ if (! gexiv2_metadata_register_xmp_namespace ("http://www.gimp.org/xmp/",
+ "GIMP"))
+ {
+ g_printerr ("Failed to register XMP namespace 'GIMP'\n");
+ }
+}
+
+static void
+gimp_metadata_init (GimpMetadata *metadata)
+{
+}
+
+/**
+ * gimp_metadata_get_guid:
+ *
+ * Generate Version 4 UUID/GUID.
+ *
+ * Return value: The new GUID/UUID string.
+ *
+ * Since: 2.10
+ */
+gchar *
+gimp_metadata_get_guid (void)
+{
+ GRand *rand;
+ gint bake;
+ gchar *GUID;
+ const gchar *szHex = "0123456789abcdef-";
+
+ rand = g_rand_new ();
+
+#define DALLOC 36
+
+ GUID = g_malloc0 (DALLOC + 1);
+
+ for (bake = 0; bake < DALLOC; bake++)
+ {
+ gint r = g_rand_int (rand) % 16;
+ gchar c = ' ';
+
+ switch (bake)
+ {
+ default:
+ c = szHex [r];
+ break;
+
+ case 19 :
+ c = szHex [(r & 0x03) | 0x08];
+ break;
+
+ case 8:
+ case 13:
+ case 18:
+ case 23:
+ c = '-';
+ break;
+
+ case 14:
+ c = '4';
+ break;
+ }
+
+ GUID[bake] = (bake < DALLOC) ? c : 0x00;
+ }
+
+ g_rand_free (rand);
+
+ return GUID;
+}
+
+/**
+ * gimp_metadata_add_history:
+ *
+ * Add XMP mm History data to file metadata.
+ *
+ * Since: 2.10
+ */
+void
+gimp_metadata_add_xmp_history (GimpMetadata *metadata,
+ gchar *state_status)
+{
+ time_t now;
+ struct tm *now_tm;
+ gchar *tmp;
+ char timestr[256];
+ char tzstr[7];
+ gchar iid_data[256];
+ gchar strdata[1024];
+ gchar tagstr[1024];
+ gchar *uuid;
+ gchar *did;
+ gchar *odid;
+ gint id_count;
+ gint found;
+ gint lastfound;
+ gint count;
+ int ii;
+
+ static const gchar *tags[] =
+ {
+ "Xmp.xmpMM.InstanceID",
+ "Xmp.xmpMM.DocumentID",
+ "Xmp.xmpMM.OriginalDocumentID",
+ "Xmp.xmpMM.History"
+ };
+
+ static const gchar *history_tags[] =
+ {
+ "/stEvt:action",
+ "/stEvt:instanceID",
+ "/stEvt:when",
+ "/stEvt:softwareAgent",
+ "/stEvt:changed"
+ };
+
+ g_return_if_fail (GIMP_IS_METADATA (metadata));
+
+ /* Update new Instance ID */
+ uuid = gimp_metadata_get_guid ();
+
+ strcpy (iid_data, "xmp.iid:");
+ strcat (iid_data, uuid);
+
+ gexiv2_metadata_set_tag_string (GEXIV2_METADATA (metadata),
+ tags[0], iid_data);
+ g_free (uuid);
+
+ /* Update new Document ID if none found */
+ did = gexiv2_metadata_get_tag_interpreted_string (GEXIV2_METADATA (metadata),
+ tags[1]);
+ if (! did || ! strlen (did))
+ {
+ gchar did_data[256];
+
+ uuid = gimp_metadata_get_guid ();
+
+ strcpy (did_data, "gimp:docid:gimp:");
+ strcat (did_data, uuid);
+
+ gexiv2_metadata_set_tag_string (GEXIV2_METADATA (metadata),
+ tags[1], did_data);
+ g_free (uuid);
+ }
+
+ /* Update new Original Document ID if none found */
+ odid = gexiv2_metadata_get_tag_interpreted_string (GEXIV2_METADATA (metadata),
+ tags[2]);
+ if (! odid || ! strlen (odid))
+ {
+ gchar did_data[256];
+ gchar *uuid = gimp_metadata_get_guid ();
+
+ strcpy (did_data, "xmp.did:");
+ strcat (did_data, uuid);
+
+ gexiv2_metadata_set_tag_string (GEXIV2_METADATA (metadata),
+ tags[2], did_data);
+ g_free (uuid);
+ }
+
+ /* Handle Xmp.xmpMM.History */
+
+ gexiv2_metadata_set_xmp_tag_struct (GEXIV2_METADATA (metadata),
+ tags[3],
+ GEXIV2_STRUCTURE_XA_SEQ);
+
+ /* Find current number of entries for Xmp.xmpMM.History */
+ found = 0;
+ for (count = 1; count < 65536; count++)
+ {
+ lastfound = 0;
+ for (ii = 0; ii < 5; ii++)
+ {
+ g_snprintf (tagstr, sizeof (tagstr), "%s[%d]%s",
+ tags[3], count, history_tags[ii]);
+
+ if (gexiv2_metadata_has_tag (GEXIV2_METADATA (metadata),
+ tagstr))
+ {
+ lastfound = 1;
+ }
+ }
+
+ if (lastfound == 0)
+ break;
+
+ found++;
+ }
+
+ id_count = found + 1;
+
+ memset (tagstr, 0, sizeof (tagstr));
+ memset (strdata, 0, sizeof (strdata));
+
+ g_snprintf (tagstr, sizeof (tagstr), "%s[%d]%s",
+ tags[3], id_count, history_tags[0]);
+
+ gexiv2_metadata_set_tag_string (GEXIV2_METADATA (metadata),
+ tagstr, "saved");
+
+ memset (tagstr, 0, sizeof (tagstr));
+ memset (strdata, 0, sizeof (strdata));
+
+ uuid = gimp_metadata_get_guid ();
+
+ g_snprintf (tagstr, sizeof (tagstr), "%s[%d]%s",
+ tags[3], id_count, history_tags[1]);
+ g_snprintf (strdata, sizeof (strdata), "xmp.iid:%s",
+ uuid);
+
+ gexiv2_metadata_set_tag_string (GEXIV2_METADATA (metadata),
+ tagstr, strdata);
+ g_free(uuid);
+
+ memset (tagstr, 0, sizeof (tagstr));
+ memset (strdata, 0, sizeof (strdata));
+
+ g_snprintf (tagstr, sizeof (tagstr), "%s[%d]%s",
+ tags[3], id_count, history_tags[2]);
+
+ /* get local time */
+ time (&now);
+ now_tm = localtime (&now);
+
+ /* get timezone and fix format */
+ strftime (tzstr, 7, "%z", now_tm);
+ tzstr[6] = '\0';
+ tzstr[5] = tzstr[4];
+ tzstr[4] = tzstr[3];
+ tzstr[3] = ':';
+
+ /* get current time and timezone string */
+ strftime (timestr, 256, "%Y-%m-%dT%H:%M:%S", now_tm);
+ tmp = g_strdup_printf ("%s%s", timestr, tzstr);
+ gexiv2_metadata_set_tag_string (GEXIV2_METADATA (metadata),
+ tagstr, tmp);
+ g_free (tmp);
+
+ memset (tagstr, 0, sizeof (tagstr));
+ memset (strdata, 0, sizeof (strdata));
+
+ g_snprintf (tagstr, sizeof (tagstr), "%s[%d]%s",
+ tags[3], id_count, history_tags[3]);
+
+ gexiv2_metadata_set_tag_string (GEXIV2_METADATA (metadata),
+ tagstr,
+ "Gimp 2.10 "
+#if defined(_WIN32) || defined(__CYGWIN__) || defined(__MINGW32__)
+ "(Windows)");
+#elif defined(__linux__)
+ "(Linux)");
+#elif defined(__APPLE__) && defined(__MACH__)
+ "(Mac OS)");
+#elif defined(unix) || defined(__unix__) || defined(__unix)
+ "(Unix)");
+#else
+ "(Unknown)");
+#endif
+
+ memset (tagstr, 0, sizeof (tagstr));
+ memset (strdata, 0, sizeof (strdata));
+
+ g_snprintf (tagstr, sizeof (tagstr), "%s[%d]%s",
+ tags[3], id_count, history_tags[4]);
+
+ strcpy (strdata, "/");
+ strcat (strdata, state_status);
+
+ gexiv2_metadata_set_tag_string (GEXIV2_METADATA (metadata),
+ tagstr, strdata);
+}
+
+/**
+ * gimp_metadata_new:
+ *
+ * Creates a new #GimpMetadata instance.
+ *
+ * Return value: The new #GimpMetadata.
+ *
+ * Since: 2.10
+ */
+GimpMetadata *
+gimp_metadata_new (void)
+{
+ GimpMetadata *metadata = NULL;
+
+ if (gexiv2_initialize ())
+ {
+ metadata = g_object_new (GIMP_TYPE_METADATA, NULL);
+
+ if (! gexiv2_metadata_open_buf (GEXIV2_METADATA (metadata),
+ wilber_jpg, wilber_jpg_len,
+ NULL))
+ {
+ g_object_unref (metadata);
+
+ return NULL;
+ }
+ }
+
+ return metadata;
+}
+
+/**
+ * gimp_metadata_duplicate:
+ * @metadata: The object to duplicate, or %NULL.
+ *
+ * Duplicates a #GimpMetadata instance.
+ *
+ * Return value: The new #GimpMetadata, or %NULL if @metadata is %NULL.
+ *
+ * Since: 2.10
+ */
+GimpMetadata *
+gimp_metadata_duplicate (GimpMetadata *metadata)
+{
+ GimpMetadata *new_metadata = NULL;
+
+ g_return_val_if_fail (metadata == NULL || GIMP_IS_METADATA (metadata), NULL);
+
+ if (metadata)
+ {
+ gchar *xml;
+
+ xml = gimp_metadata_serialize (metadata);
+ new_metadata = gimp_metadata_deserialize (xml);
+ g_free (xml);
+ }
+
+ return new_metadata;
+}
+
+typedef struct
+{
+ gchar name[1024];
+ gboolean base64;
+ gboolean excessive_message_shown;
+ GimpMetadata *metadata;
+} GimpMetadataParseData;
+
+static const gchar*
+gimp_metadata_attribute_name_to_value (const gchar **attribute_names,
+ const gchar **attribute_values,
+ const gchar *name)
+{
+ while (*attribute_names)
+ {
+ if (! strcmp (*attribute_names, name))
+ {
+ return *attribute_values;
+ }
+
+ attribute_names++;
+ attribute_values++;
+ }
+
+ return NULL;
+}
+
+static void
+gimp_metadata_deserialize_start_element (GMarkupParseContext *context,
+ const gchar *element_name,
+ const gchar **attribute_names,
+ const gchar **attribute_values,
+ gpointer user_data,
+ GError **error)
+{
+ GimpMetadataParseData *parse_data = user_data;
+
+ if (! strcmp (element_name, "tag"))
+ {
+ const gchar *name;
+ const gchar *encoding;
+
+ name = gimp_metadata_attribute_name_to_value (attribute_names,
+ attribute_values,
+ "name");
+ encoding = gimp_metadata_attribute_name_to_value (attribute_names,
+ attribute_values,
+ "encoding");
+
+ if (! name)
+ {
+ g_set_error (error, GIMP_METADATA_ERROR, 1001,
+ "Element 'tag' does not contain required attribute 'name'.");
+ return;
+ }
+
+ strncpy (parse_data->name, name, sizeof (parse_data->name));
+ parse_data->name[sizeof (parse_data->name) - 1] = 0;
+
+ parse_data->base64 = (encoding && ! strcmp (encoding, "base64"));
+ }
+}
+
+static void
+gimp_metadata_deserialize_end_element (GMarkupParseContext *context,
+ const gchar *element_name,
+ gpointer user_data,
+ GError **error)
+{
+}
+
+static void
+gimp_metadata_deserialize_text (GMarkupParseContext *context,
+ const gchar *text,
+ gsize text_len,
+ gpointer user_data,
+ GError **error)
+{
+ GimpMetadataParseData *parse_data = user_data;
+ const gchar *current_element;
+
+ current_element = g_markup_parse_context_get_element (context);
+
+ if (! g_strcmp0 (current_element, "tag"))
+ {
+ gchar *value = g_strndup (text, text_len);
+
+ if (parse_data->base64)
+ {
+ guchar *decoded;
+ gsize len;
+
+ decoded = g_base64_decode (value, &len);
+
+ if (decoded[len - 1] == '\0')
+ {
+ g_free (value);
+ value = (gchar *) decoded;
+ }
+ else
+ {
+ g_clear_pointer (&value, g_free);
+ g_clear_pointer (&decoded, g_free);
+ }
+ }
+
+ if (value)
+ {
+ GExiv2Metadata *g2_metadata = GEXIV2_METADATA (parse_data->metadata);
+ gchar **values;
+
+ values = gexiv2_metadata_get_tag_multiple (g2_metadata,
+ parse_data->name);
+
+ if (values)
+ {
+ guint length = g_strv_length (values);
+
+ if (length > 1000 &&
+ ! g_strcmp0 (parse_data->name, "Xmp.photoshop.DocumentAncestors"))
+ {
+ /* Issue #8025, see also #7464 Some XCF images can have huge
+ * amounts of this tag, apparently due to a bug in PhotoShop.
+ * This makes deserializing it in the way we currently do
+ * too slow. Until we can change this let's ignore everything
+ * but the first 1000 values when serializing. */
+
+ if (! parse_data->excessive_message_shown)
+ {
+ g_message ("Excessive number of Xmp.photoshop.DocumentAncestors tags found. "
+ "Only keeping the first 1000 values.");
+ parse_data->excessive_message_shown = TRUE;
+ }
+ }
+ else
+ {
+ values = g_renew (gchar *, values, length + 2);
+ values[length] = value;
+ values[length + 1] = NULL;
+
+ gexiv2_metadata_set_tag_multiple (g2_metadata,
+ parse_data->name,
+ (const gchar **) values);
+ }
+ g_strfreev (values);
+ }
+ else
+ {
+ gexiv2_metadata_set_tag_string (GEXIV2_METADATA (g2_metadata),
+ parse_data->name,
+ value);
+ g_free (value);
+ }
+ }
+ }
+}
+
+static void
+gimp_metadata_deserialize_error (GMarkupParseContext *context,
+ GError *error,
+ gpointer user_data)
+{
+ g_printerr ("Metadata parse error: %s\n", error->message);
+}
+
+/**
+ * gimp_metadata_deserialize:
+ * @metadata_xml: A string of serialized metadata XML.
+ *
+ * Deserializes a string of XML that has been created by
+ * gimp_metadata_serialize().
+ *
+ * Return value: The new #GimpMetadata.
+ *
+ * Since: 2.10
+ */
+GimpMetadata *
+gimp_metadata_deserialize (const gchar *metadata_xml)
+{
+ GimpMetadata *metadata;
+ GMarkupParser markup_parser;
+ GimpMetadataParseData parse_data;
+ GMarkupParseContext *context;
+
+ g_return_val_if_fail (metadata_xml != NULL, NULL);
+
+ metadata = gimp_metadata_new ();
+
+ parse_data.metadata = metadata;
+ parse_data.excessive_message_shown = FALSE;
+
+ markup_parser.start_element = gimp_metadata_deserialize_start_element;
+ markup_parser.end_element = gimp_metadata_deserialize_end_element;
+ markup_parser.text = gimp_metadata_deserialize_text;
+ markup_parser.passthrough = NULL;
+ markup_parser.error = gimp_metadata_deserialize_error;
+
+ context = g_markup_parse_context_new (&markup_parser, 0, &parse_data, NULL);
+
+ g_markup_parse_context_parse (context,
+ metadata_xml, strlen (metadata_xml),
+ NULL);
+
+ g_markup_parse_context_unref (context);
+
+ return metadata;
+}
+
+static gchar *
+gimp_metadata_escape (const gchar *name,
+ const gchar *value,
+ gboolean *base64)
+{
+ if (! g_utf8_validate (value, -1, NULL))
+ {
+ gchar *encoded;
+
+ encoded = g_base64_encode ((const guchar *) value, strlen (value) + 1);
+
+ g_printerr ("Invalid UTF-8 in metadata value %s, encoding as base64: %s\n",
+ name, encoded);
+
+ *base64 = TRUE;
+
+ return encoded;
+ }
+
+ *base64 = FALSE;
+
+ return g_markup_escape_text (value, -1);
+}
+
+static void
+gimp_metadata_append_tag (GString *string,
+ const gchar *name,
+ gchar *value,
+ gboolean base64)
+{
+ if (value)
+ {
+ if (base64)
+ {
+ g_string_append_printf (string, " <tag name=\"%s\" encoding=\"base64\">%s</tag>\n",
+ name, value);
+ }
+ else
+ {
+ g_string_append_printf (string, " <tag name=\"%s\">%s</tag>\n",
+ name, value);
+ }
+
+ g_free (value);
+ }
+}
+
+/**
+ * gimp_metadata_serialize:
+ * @metadata: A #GimpMetadata instance.
+ *
+ * Serializes @metadata into an XML string that can later be deserialized
+ * using gimp_metadata_deserialize().
+ *
+ * Return value: The serialized XML string.
+ *
+ * Since: 2.10
+ */
+gchar *
+gimp_metadata_serialize (GimpMetadata *metadata)
+{
+ GString *string;
+ gchar **exif_data = NULL;
+ gchar **iptc_data = NULL;
+ gchar **xmp_data = NULL;
+ gchar *value;
+ gchar *escaped;
+ gboolean base64;
+ gint i;
+
+ g_return_val_if_fail (GIMP_IS_METADATA (metadata), NULL);
+
+ string = g_string_new (NULL);
+
+ g_string_append (string, "<?xml version='1.0' encoding='UTF-8'?>\n");
+ g_string_append (string, "<metadata>\n");
+
+ exif_data = gexiv2_metadata_get_exif_tags (GEXIV2_METADATA (metadata));
+
+ if (exif_data)
+ {
+ for (i = 0; exif_data[i] != NULL; i++)
+ {
+ value = gexiv2_metadata_get_tag_string (GEXIV2_METADATA (metadata),
+ exif_data[i]);
+ escaped = gimp_metadata_escape (exif_data[i], value, &base64);
+ g_free (value);
+
+ gimp_metadata_append_tag (string, exif_data[i], escaped, base64);
+ }
+
+ g_strfreev (exif_data);
+ }
+
+ xmp_data = gexiv2_metadata_get_xmp_tags (GEXIV2_METADATA (metadata));
+
+ if (xmp_data)
+ {
+ for (i = 0; xmp_data[i] != NULL; i++)
+ {
+ /* XmpText is always a single value, but structures like
+ * XmpBag and XmpSeq can have multiple values that need to be
+ * treated separately or else saving will do things wrong. */
+ if (! g_strcmp0 (gexiv2_metadata_get_tag_type (xmp_data[i]), "XmpText"))
+ {
+ value = gexiv2_metadata_get_tag_string (GEXIV2_METADATA (metadata),
+ xmp_data[i]);
+ escaped = gimp_metadata_escape (xmp_data[i], value, &base64);
+ g_free (value);
+
+ gimp_metadata_append_tag (string, xmp_data[i], escaped, base64);
+ }
+ else
+ {
+ gchar **values;
+
+ values = gexiv2_metadata_get_tag_multiple (GEXIV2_METADATA (metadata),
+ xmp_data[i]);
+
+ if (values)
+ {
+ gint vi;
+ gint cnt = 0;
+
+ if (! g_strcmp0 (xmp_data[i], "Xmp.photoshop.DocumentAncestors"))
+ {
+ /* Issue #7464 Some images can have huge amounts of this
+ * tag (more than 100000 in certain cases), apparently
+ * due to a bug in PhotoShop. This makes deserializing it
+ * in the way we currently do too slow. Until we can
+ * change this let's remove everything but the first 1000
+ * values when serializing. */
+ cnt = g_strv_length (values);
+
+ if (cnt > 1000)
+ {
+ g_message ("Excessive number of Xmp.photoshop.DocumentAncestors tags found: %d. "
+ "Only keeping the first 1000 values.", cnt);
+ }
+ }
+
+ for (vi = 0; values[vi] != NULL && (cnt <= 1000 || vi < 1000); vi++)
+ {
+ escaped = gimp_metadata_escape (xmp_data[i], values[vi], &base64);
+ gimp_metadata_append_tag (string, xmp_data[i], escaped, base64);
+ }
+
+ g_strfreev (values);
+ }
+ }
+ }
+ g_strfreev (xmp_data);
+ }
+
+ iptc_data = gexiv2_metadata_get_iptc_tags (GEXIV2_METADATA (metadata));
+
+ if (iptc_data)
+ {
+ gchar **iptc_tags = iptc_data;
+ gchar *last_tag = NULL;
+
+ while (*iptc_tags)
+ {
+ gchar **values;
+
+ if (last_tag && ! strcmp (*iptc_tags, last_tag))
+ {
+ iptc_tags++;
+ continue;
+ }
+ last_tag = *iptc_tags;
+
+ values = gexiv2_metadata_get_tag_multiple (GEXIV2_METADATA (metadata),
+ *iptc_tags);
+
+ if (values)
+ {
+ for (i = 0; values[i] != NULL; i++)
+ {
+ escaped = gimp_metadata_escape (*iptc_tags, values[i], &base64);
+ gimp_metadata_append_tag (string, *iptc_tags, escaped, base64);
+ }
+
+ g_strfreev (values);
+ }
+
+ iptc_tags++;
+ }
+
+ g_strfreev (iptc_data);
+ }
+
+ g_string_append (string, "</metadata>\n");
+
+ return g_string_free (string, FALSE);
+}
+
+/**
+ * gimp_metadata_load_from_file:
+ * @file: The #GFile to load the metadata from
+ * @error: Return location for error message
+ *
+ * Loads #GimpMetadata from @file.
+ *
+ * Return value: The loaded #GimpMetadata.
+ *
+ * Since: 2.10
+ */
+GimpMetadata *
+gimp_metadata_load_from_file (GFile *file,
+ GError **error)
+{
+ GimpMetadata *meta = NULL;
+ gchar *path;
+ gchar *filename;
+
+ g_return_val_if_fail (G_IS_FILE (file), NULL);
+ g_return_val_if_fail (error == NULL || *error == NULL, NULL);
+
+ path = g_file_get_path (file);
+
+ if (! path)
+ {
+ g_set_error (error, GIMP_METADATA_ERROR, 0,
+ _("Can load metadata only from local files"));
+ return NULL;
+ }
+
+ filename = g_strdup (path);
+
+ g_free (path);
+
+ if (gexiv2_initialize ())
+ {
+ meta = g_object_new (GIMP_TYPE_METADATA, NULL);
+
+ if (! gexiv2_metadata_open_path (GEXIV2_METADATA (meta), filename, error))
+ {
+ g_object_unref (meta);
+ g_free (filename);
+
+ return NULL;
+ }
+ }
+
+ g_free (filename);
+
+ return meta;
+}
+
+/**
+ * gimp_metadata_save_to_file:
+ * @metadata: A #GimpMetadata instance.
+ * @file: The file to save the metadata to
+ * @error: Return location for error message
+ *
+ * Saves @metadata to @file.
+ *
+ * Return value: %TRUE on success, %FALSE otherwise.
+ *
+ * Since: 2.10
+ */
+gboolean
+gimp_metadata_save_to_file (GimpMetadata *metadata,
+ GFile *file,
+ GError **error)
+{
+ gchar *path;
+ gchar *filename;
+ gboolean success;
+
+ g_return_val_if_fail (GIMP_IS_METADATA (metadata), FALSE);
+ g_return_val_if_fail (G_IS_FILE (file), FALSE);
+ g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+ path = g_file_get_path (file);
+
+ if (! path)
+ {
+ g_set_error (error, GIMP_METADATA_ERROR, 0,
+ _("Can save metadata only to local files"));
+ return FALSE;
+ }
+
+ filename = g_strdup (path);
+
+ g_free (path);
+
+ success = gexiv2_metadata_save_file (GEXIV2_METADATA (metadata),
+ filename, error);
+
+ g_free (filename);
+
+ return success;
+}
+
+/**
+ * gimp_metadata_set_from_exif:
+ * @metadata: A #GimpMetadata instance.
+ * @exif_data: The blob of Exif data to set
+ * @exif_data_length: Length of @exif_data, in bytes
+ * @error: Return location for error message
+ *
+ * Sets the tags from a piece of Exif data on @metadata.
+ *
+ * Return value: %TRUE on success, %FALSE otherwise.
+ *
+ * Since: 2.10
+ */
+gboolean
+gimp_metadata_set_from_exif (GimpMetadata *metadata,
+ const guchar *exif_data,
+ gint exif_data_length,
+ GError **error)
+{
+
+ GByteArray *exif_bytes;
+ GimpMetadata *exif_metadata;
+ guint8 data_size[2] = { 0, };
+ const guint8 eoi[2] = { 0xff, 0xd9 };
+
+ g_return_val_if_fail (GIMP_IS_METADATA (metadata), FALSE);
+ g_return_val_if_fail (exif_data != NULL || exif_data_length == 0, FALSE);
+ g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+ if (exif_data_length < 0 || exif_data_length + 2 >= 65536)
+ {
+ g_set_error (error, GIMP_METADATA_ERROR, 0,
+ _("Invalid Exif data size."));
+ return FALSE;
+ }
+
+ data_size[0] = ((exif_data_length + 2) & 0xFF00) >> 8;
+ data_size[1] = ((exif_data_length + 2) & 0x00FF);
+
+ exif_bytes = g_byte_array_new ();
+ exif_bytes = g_byte_array_append (exif_bytes,
+ minimal_exif, G_N_ELEMENTS (minimal_exif));
+ exif_bytes = g_byte_array_append (exif_bytes,
+ data_size, 2);
+ exif_bytes = g_byte_array_append (exif_bytes,
+ (guint8 *) exif_data, exif_data_length);
+ exif_bytes = g_byte_array_append (exif_bytes, eoi, 2);
+
+ exif_metadata = gimp_metadata_new ();
+
+ if (! gexiv2_metadata_open_buf (GEXIV2_METADATA (exif_metadata),
+ exif_bytes->data, exif_bytes->len, error))
+ {
+ g_object_unref (exif_metadata);
+ g_byte_array_free (exif_bytes, TRUE);
+ return FALSE;
+ }
+
+ if (! gexiv2_metadata_has_exif (GEXIV2_METADATA (exif_metadata)))
+ {
+ g_set_error (error, GIMP_METADATA_ERROR, 0,
+ _("Parsing Exif data failed."));
+ g_object_unref (exif_metadata);
+ g_byte_array_free (exif_bytes, TRUE);
+ return FALSE;
+ }
+
+ gimp_metadata_add (exif_metadata, metadata);
+ g_object_unref (exif_metadata);
+ g_byte_array_free (exif_bytes, TRUE);
+
+ return TRUE;
+}
+
+/**
+ * gimp_metadata_set_from_iptc:
+ * @metadata: A #GimpMetadata instance.
+ * @iptc_data: The blob of Ipc data to set
+ * @iptc_data_length:Length of @iptc_data, in bytes
+ * @error: Return location for error message
+ *
+ * Sets the tags from a piece of IPTC data on @metadata.
+ *
+ * Return value: %TRUE on success, %FALSE otherwise.
+ *
+ * Since: 2.10
+ */
+gboolean
+gimp_metadata_set_from_iptc (GimpMetadata *metadata,
+ const guchar *iptc_data,
+ gint iptc_data_length,
+ GError **error)
+{
+ GimpMetadata *iptc_metadata;
+
+ g_return_val_if_fail (GIMP_IS_METADATA (metadata), FALSE);
+ g_return_val_if_fail (iptc_data != NULL || iptc_data_length == 0, FALSE);
+ g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+ iptc_metadata = gimp_metadata_new ();
+
+ if (! gexiv2_metadata_open_buf (GEXIV2_METADATA (iptc_metadata),
+ iptc_data, iptc_data_length, error))
+ {
+ g_object_unref (iptc_metadata);
+ return FALSE;
+ }
+
+ if (! gexiv2_metadata_has_iptc (GEXIV2_METADATA (iptc_metadata)))
+ {
+ g_set_error (error, GIMP_METADATA_ERROR, 0,
+ _("Parsing IPTC data failed."));
+ g_object_unref (iptc_metadata);
+ return FALSE;
+ }
+
+ gimp_metadata_add (iptc_metadata, metadata);
+ g_object_unref (iptc_metadata);
+
+ return TRUE;
+}
+
+/**
+ * gimp_metadata_set_from_xmp:
+ * @metadata: A #GimpMetadata instance.
+ * @xmp_data: The blob of Exif data to set
+ * @xmp_data_length: Length of @exif_data, in bytes
+ * @error: Return location for error message
+ *
+ * Sets the tags from a piece of XMP data on @metadata.
+ *
+ * Return value: %TRUE on success, %FALSE otherwise.
+ *
+ * Since: 2.10
+ */
+gboolean
+gimp_metadata_set_from_xmp (GimpMetadata *metadata,
+ const guchar *xmp_data,
+ gint xmp_data_length,
+ GError **error)
+{
+ GimpMetadata *xmp_metadata;
+
+ g_return_val_if_fail (GIMP_IS_METADATA (metadata), FALSE);
+ g_return_val_if_fail (xmp_data != NULL || xmp_data_length == 0, FALSE);
+ g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+ xmp_metadata = gimp_metadata_new ();
+
+ if (! gexiv2_metadata_open_buf (GEXIV2_METADATA (xmp_metadata),
+ xmp_data, xmp_data_length, error))
+ {
+ g_object_unref (xmp_metadata);
+ return FALSE;
+ }
+
+ if (! gexiv2_metadata_has_xmp (GEXIV2_METADATA (xmp_metadata)))
+ {
+ g_set_error (error, GIMP_METADATA_ERROR, 0,
+ _("Parsing XMP data failed."));
+ g_object_unref (xmp_metadata);
+ return FALSE;
+ }
+
+ gimp_metadata_add (xmp_metadata, metadata);
+ g_object_unref (xmp_metadata);
+
+ return TRUE;
+}
+
+/**
+ * gimp_metadata_set_pixel_size:
+ * @metadata: A #GimpMetadata instance.
+ * @width: Width in pixels
+ * @height: Height in pixels
+ *
+ * Sets Exif.Image.ImageWidth and Exif.Image.ImageLength on @metadata.
+ * If already present, also sets Exif.Photo.PixelXDimension and
+ * Exif.Photo.PixelYDimension.
+ *
+ * Since: 2.10
+ */
+void
+gimp_metadata_set_pixel_size (GimpMetadata *metadata,
+ gint width,
+ gint height)
+{
+ gchar buffer[32];
+
+ g_return_if_fail (GIMP_IS_METADATA (metadata));
+
+ g_snprintf (buffer, sizeof (buffer), "%d", width);
+ gexiv2_metadata_set_tag_string (GEXIV2_METADATA (metadata),
+ "Exif.Image.ImageWidth", buffer);
+ if (gexiv2_metadata_has_tag (GEXIV2_METADATA (metadata),
+ "Exif.Photo.PixelXDimension"))
+ {
+ gexiv2_metadata_set_tag_string (GEXIV2_METADATA (metadata),
+ "Exif.Photo.PixelXDimension",
+ buffer);
+ }
+
+ g_snprintf (buffer, sizeof (buffer), "%d", height);
+ gexiv2_metadata_set_tag_string (GEXIV2_METADATA (metadata),
+ "Exif.Image.ImageLength", buffer);
+ if (gexiv2_metadata_has_tag (GEXIV2_METADATA (metadata),
+ "Exif.Photo.PixelYDimension"))
+ {
+ gexiv2_metadata_set_tag_string (GEXIV2_METADATA (metadata),
+ "Exif.Photo.PixelYDimension",
+ buffer);
+ }
+}
+
+/**
+ * gimp_metadata_set_bits_per_sample:
+ * @metadata: A #GimpMetadata instance.
+ * @bits_per_sample: Bits per pixel, per component
+ *
+ * Sets Exif.Image.BitsPerSample on @metadata.
+ *
+ * Since: 2.10
+ */
+void
+gimp_metadata_set_bits_per_sample (GimpMetadata *metadata,
+ gint bits_per_sample)
+{
+ gchar buffer[32];
+
+ g_return_if_fail (GIMP_IS_METADATA (metadata));
+
+ g_snprintf (buffer, sizeof (buffer), "%d %d %d",
+ bits_per_sample, bits_per_sample, bits_per_sample);
+ gexiv2_metadata_set_tag_string (GEXIV2_METADATA (metadata),
+ "Exif.Image.BitsPerSample", buffer);
+}
+
+/**
+ * gimp_metadata_get_resolution:
+ * @metadata: A #GimpMetadata instance.
+ * @xres: Return location for the X Resolution, in ppi
+ * @yres: Return location for the Y Resolution, in ppi
+ * @unit: Return location for the unit unit
+ *
+ * Returns values based on Exif.Image.XResolution,
+ * Exif.Image.YResolution and Exif.Image.ResolutionUnit of @metadata.
+ *
+ * Return value: %TRUE on success, %FALSE otherwise.
+ *
+ * Since: 2.10
+ */
+gboolean
+gimp_metadata_get_resolution (GimpMetadata *metadata,
+ gdouble *xres,
+ gdouble *yres,
+ GimpUnit *unit)
+{
+ gint xnom, xdenom;
+ gint ynom, ydenom;
+
+ g_return_val_if_fail (GIMP_IS_METADATA (metadata), FALSE);
+
+ if (gexiv2_metadata_get_exif_tag_rational (GEXIV2_METADATA (metadata),
+ "Exif.Image.XResolution",
+ &xnom, &xdenom) &&
+ gexiv2_metadata_get_exif_tag_rational (GEXIV2_METADATA (metadata),
+ "Exif.Image.YResolution",
+ &ynom, &ydenom))
+ {
+ gchar *un;
+ gint exif_unit = 2;
+
+ un = gexiv2_metadata_get_tag_string (GEXIV2_METADATA (metadata),
+ "Exif.Image.ResolutionUnit");
+ if (un)
+ {
+ exif_unit = atoi (un);
+ g_free (un);
+ }
+
+ if (xnom != 0 && xdenom != 0 &&
+ ynom != 0 && ydenom != 0)
+ {
+ gdouble xresolution = (gdouble) xnom / (gdouble) xdenom;
+ gdouble yresolution = (gdouble) ynom / (gdouble) ydenom;
+
+ if (exif_unit == 3)
+ {
+ xresolution *= 2.54;
+ yresolution *= 2.54;
+ }
+
+ if (xresolution >= GIMP_MIN_RESOLUTION &&
+ xresolution <= GIMP_MAX_RESOLUTION &&
+ yresolution >= GIMP_MIN_RESOLUTION &&
+ yresolution <= GIMP_MAX_RESOLUTION)
+ {
+ if (xres)
+ *xres = xresolution;
+
+ if (yres)
+ *yres = yresolution;
+
+ if (unit)
+ {
+ if (exif_unit == 3)
+ *unit = GIMP_UNIT_MM;
+ else
+ *unit = GIMP_UNIT_INCH;
+ }
+
+ return TRUE;
+ }
+ }
+ }
+
+ return FALSE;
+}
+
+/**
+ * gimp_metadata_set_resolution:
+ * @metadata: A #GimpMetadata instance.
+ * @xres: The image's X Resolution, in ppi
+ * @yres: The image's Y Resolution, in ppi
+ * @unit: The image's unit
+ *
+ * Sets Exif.Image.XResolution, Exif.Image.YResolution and
+ * Exif.Image.ResolutionUnit of @metadata.
+ *
+ * Since: 2.10
+ */
+void
+gimp_metadata_set_resolution (GimpMetadata *metadata,
+ gdouble xres,
+ gdouble yres,
+ GimpUnit unit)
+{
+ gchar buffer[32];
+ gint exif_unit;
+ gint factor;
+
+ g_return_if_fail (GIMP_IS_METADATA (metadata));
+
+ if (gimp_unit_is_metric (unit))
+ {
+ xres /= 2.54;
+ yres /= 2.54;
+
+ exif_unit = 3;
+ }
+ else
+ {
+ exif_unit = 2;
+ }
+
+ for (factor = 1; factor <= 100 /* arbitrary */; factor++)
+ {
+ if (fabs (xres * factor - ROUND (xres * factor)) < 0.01 &&
+ fabs (yres * factor - ROUND (yres * factor)) < 0.01)
+ break;
+ }
+
+ gexiv2_metadata_set_exif_tag_rational (GEXIV2_METADATA (metadata),
+ "Exif.Image.XResolution",
+ ROUND (xres * factor), factor);
+
+ gexiv2_metadata_set_exif_tag_rational (GEXIV2_METADATA (metadata),
+ "Exif.Image.YResolution",
+ ROUND (yres * factor), factor);
+
+ g_snprintf (buffer, sizeof (buffer), "%d", exif_unit);
+ gexiv2_metadata_set_tag_string (GEXIV2_METADATA (metadata),
+ "Exif.Image.ResolutionUnit", buffer);
+}
+
+/**
+ * gimp_metadata_get_colorspace:
+ * @metadata: A #GimpMetadata instance.
+ *
+ * Returns values based on Exif.Photo.ColorSpace, Xmp.exif.ColorSpace,
+ * Exif.Iop.InteroperabilityIndex, Exif.Nikon3.ColorSpace,
+ * Exif.Canon.ColorSpace of @metadata.
+ *
+ * Return value: The colorspace specified by above tags.
+ *
+ * Since: 2.10
+ */
+GimpMetadataColorspace
+gimp_metadata_get_colorspace (GimpMetadata *metadata)
+{
+ glong exif_cs = -1;
+
+ g_return_val_if_fail (GIMP_IS_METADATA (metadata),
+ GIMP_METADATA_COLORSPACE_UNSPECIFIED);
+
+ /* the logic here was mostly taken from darktable and libkexiv2 */
+
+ if (gexiv2_metadata_has_tag (GEXIV2_METADATA (metadata),
+ "Exif.Photo.ColorSpace"))
+ {
+ exif_cs = gexiv2_metadata_get_tag_long (GEXIV2_METADATA (metadata),
+ "Exif.Photo.ColorSpace");
+ }
+ else if (gexiv2_metadata_has_tag (GEXIV2_METADATA (metadata),
+ "Xmp.exif.ColorSpace"))
+ {
+ exif_cs = gexiv2_metadata_get_tag_long (GEXIV2_METADATA (metadata),
+ "Xmp.exif.ColorSpace");
+ }
+
+ if (exif_cs == 0x01)
+ {
+ return GIMP_METADATA_COLORSPACE_SRGB;
+ }
+ else if (exif_cs == 0x02)
+ {
+ return GIMP_METADATA_COLORSPACE_ADOBERGB;
+ }
+ else
+ {
+ if (exif_cs == 0xffff)
+ {
+ gchar *iop_index;
+
+ iop_index = gexiv2_metadata_get_tag_string (GEXIV2_METADATA (metadata),
+ "Exif.Iop.InteroperabilityIndex");
+
+ if (! g_strcmp0 (iop_index, "R03"))
+ {
+ g_free (iop_index);
+
+ return GIMP_METADATA_COLORSPACE_ADOBERGB;
+ }
+ else if (! g_strcmp0 (iop_index, "R98"))
+ {
+ g_free (iop_index);
+
+ return GIMP_METADATA_COLORSPACE_SRGB;
+ }
+
+ g_free (iop_index);
+ }
+
+ if (gexiv2_metadata_has_tag (GEXIV2_METADATA (metadata),
+ "Exif.Nikon3.ColorSpace"))
+ {
+ glong nikon_cs;
+
+ nikon_cs = gexiv2_metadata_get_tag_long (GEXIV2_METADATA (metadata),
+ "Exif.Nikon3.ColorSpace");
+
+ if (nikon_cs == 0x01)
+ {
+ return GIMP_METADATA_COLORSPACE_SRGB;
+ }
+ else if (nikon_cs == 0x02)
+ {
+ return GIMP_METADATA_COLORSPACE_ADOBERGB;
+ }
+ }
+
+ if (gexiv2_metadata_has_tag (GEXIV2_METADATA (metadata),
+ "Exif.Canon.ColorSpace"))
+ {
+ glong canon_cs;
+
+ canon_cs = gexiv2_metadata_get_tag_long (GEXIV2_METADATA (metadata),
+ "Exif.Canon.ColorSpace");
+
+ if (canon_cs == 0x01)
+ {
+ return GIMP_METADATA_COLORSPACE_SRGB;
+ }
+ else if (canon_cs == 0x02)
+ {
+ return GIMP_METADATA_COLORSPACE_ADOBERGB;
+ }
+ }
+
+ if (exif_cs == 0xffff)
+ return GIMP_METADATA_COLORSPACE_UNCALIBRATED;
+ }
+
+ return GIMP_METADATA_COLORSPACE_UNSPECIFIED;
+}
+
+/**
+ * gimp_metadata_set_colorspace:
+ * @metadata: A #GimpMetadata instance.
+ * @colorspace: The color space.
+ *
+ * Sets Exif.Photo.ColorSpace, Xmp.exif.ColorSpace,
+ * Exif.Iop.InteroperabilityIndex, Exif.Nikon3.ColorSpace,
+ * Exif.Canon.ColorSpace of @metadata.
+ *
+ * Since: 2.10
+ */
+void
+gimp_metadata_set_colorspace (GimpMetadata *metadata,
+ GimpMetadataColorspace colorspace)
+{
+ GExiv2Metadata *g2metadata = GEXIV2_METADATA (metadata);
+
+ switch (colorspace)
+ {
+ case GIMP_METADATA_COLORSPACE_UNSPECIFIED:
+ gexiv2_metadata_clear_tag (g2metadata, "Exif.Photo.ColorSpace");
+ gexiv2_metadata_clear_tag (g2metadata, "Xmp.exif.ColorSpace");
+ gexiv2_metadata_clear_tag (g2metadata, "Exif.Iop.InteroperabilityIndex");
+ gexiv2_metadata_clear_tag (g2metadata, "Exif.Nikon3.ColorSpace");
+ gexiv2_metadata_clear_tag (g2metadata, "Exif.Canon.ColorSpace");
+ break;
+
+ case GIMP_METADATA_COLORSPACE_UNCALIBRATED:
+ gexiv2_metadata_set_tag_long (g2metadata, "Exif.Photo.ColorSpace", 0xffff);
+ if (gexiv2_metadata_has_tag (g2metadata, "Xmp.exif.ColorSpace"))
+ gexiv2_metadata_set_tag_long (g2metadata, "Xmp.exif.ColorSpace", 0xffff);
+ gexiv2_metadata_clear_tag (g2metadata, "Exif.Iop.InteroperabilityIndex");
+ gexiv2_metadata_clear_tag (g2metadata, "Exif.Nikon3.ColorSpace");
+ gexiv2_metadata_clear_tag (g2metadata, "Exif.Canon.ColorSpace");
+ break;
+
+ case GIMP_METADATA_COLORSPACE_SRGB:
+ gexiv2_metadata_set_tag_long (g2metadata, "Exif.Photo.ColorSpace", 0x01);
+
+ if (gexiv2_metadata_has_tag (g2metadata, "Xmp.exif.ColorSpace"))
+ gexiv2_metadata_set_tag_long (g2metadata, "Xmp.exif.ColorSpace", 0x01);
+
+ if (gexiv2_metadata_has_tag (g2metadata, "Exif.Iop.InteroperabilityIndex"))
+ gexiv2_metadata_set_tag_string (g2metadata,
+ "Exif.Iop.InteroperabilityIndex", "R98");
+
+ if (gexiv2_metadata_has_tag (g2metadata, "Exif.Nikon3.ColorSpace"))
+ gexiv2_metadata_set_tag_long (g2metadata, "Exif.Nikon3.ColorSpace", 0x01);
+
+ if (gexiv2_metadata_has_tag (g2metadata, "Exif.Canon.ColorSpace"))
+ gexiv2_metadata_set_tag_long (g2metadata, "Exif.Canon.ColorSpace", 0x01);
+ break;
+
+ case GIMP_METADATA_COLORSPACE_ADOBERGB:
+ gexiv2_metadata_set_tag_long (g2metadata, "Exif.Photo.ColorSpace", 0x02);
+
+ if (gexiv2_metadata_has_tag (g2metadata, "Xmp.exif.ColorSpace"))
+ gexiv2_metadata_set_tag_long (g2metadata, "Xmp.exif.ColorSpace", 0x02);
+
+ if (gexiv2_metadata_has_tag (g2metadata, "Exif.Iop.InteroperabilityIndex"))
+ gexiv2_metadata_set_tag_string (g2metadata,
+ "Exif.Iop.InteroperabilityIndex", "R03");
+
+ if (gexiv2_metadata_has_tag (g2metadata, "Exif.Nikon3.ColorSpace"))
+ gexiv2_metadata_set_tag_long (g2metadata, "Exif.Nikon3.ColorSpace", 0x02);
+
+ if (gexiv2_metadata_has_tag (g2metadata, "Exif.Canon.ColorSpace"))
+ gexiv2_metadata_set_tag_long (g2metadata, "Exif.Canon.ColorSpace", 0x02);
+ break;
+ }
+}
+
+/**
+ * gimp_metadata_is_tag_supported:
+ * @tag: A metadata tag name
+ * @mime_type: A mime type
+ *
+ * Returns whether @tag is supported in a file of type @mime_type.
+ *
+ * Return value: %TRUE if the @tag supported with @mime_type, %FALSE otherwise.
+ *
+ * Since: 2.10
+ */
+gboolean
+gimp_metadata_is_tag_supported (const gchar *tag,
+ const gchar *mime_type)
+{
+ gint j;
+
+ g_return_val_if_fail (tag != NULL, FALSE);
+ g_return_val_if_fail (mime_type != NULL, FALSE);
+
+ for (j = 0; j < G_N_ELEMENTS (unsupported_tags); j++)
+ {
+ if (g_str_has_prefix (tag, unsupported_tags[j]))
+ {
+ return FALSE;
+ }
+ }
+
+ if (! strcmp (mime_type, "image/jpeg"))
+ {
+ for (j = 0; j < G_N_ELEMENTS (tiff_tags); j++)
+ {
+ if (g_str_has_prefix (tag, tiff_tags[j]))
+ {
+ return FALSE;
+ }
+ }
+ }
+ else if (! strcmp (mime_type, "image/tiff"))
+ {
+ for (j = 0; j < G_N_ELEMENTS (jpeg_tags); j++)
+ {
+ if (g_str_has_prefix (tag, jpeg_tags[j]))
+ {
+ return FALSE;
+ }
+ }
+ }
+
+ return TRUE;
+}
+
+
+/* private functions */
+
+static GQuark
+gimp_metadata_error_quark (void)
+{
+ static GQuark quark = 0;
+
+ if (G_UNLIKELY (quark == 0))
+ quark = g_quark_from_static_string ("gimp-metadata-error-quark");
+
+ return quark;
+}
+
+static void
+gimp_metadata_copy_tag (GExiv2Metadata *src,
+ GExiv2Metadata *dest,
+ const gchar *tag)
+{
+#if GEXIV2_CHECK_VERSION(0, 12, 2)
+ gchar **values;
+ GError *error = NULL;
+
+ values = gexiv2_metadata_try_get_tag_multiple (src, tag, &error);
+
+ if (error)
+ {
+ g_printerr ("%s: %s\n", G_STRFUNC, error->message);
+ g_clear_error (&error);
+ g_strfreev (values);
+ }
+ else if (values)
+ {
+ gexiv2_metadata_try_set_tag_multiple (dest, tag, (const gchar **) values, &error);
+ if (error)
+ {
+ g_warning ("%s: failed to set multiple metadata '%s': %s\n",
+ G_STRFUNC, tag, error->message);
+ g_clear_error (&error);
+ }
+
+ g_strfreev (values);
+ }
+ else
+ {
+ gchar *value = gexiv2_metadata_try_get_tag_string (src, tag, &error);
+
+ if (value)
+ {
+ gexiv2_metadata_try_set_tag_string (dest, tag, value, &error);
+ if (error)
+ {
+ g_warning ("%s: failed to set metadata '%s': %s\n",
+ G_STRFUNC, tag, error->message);
+ g_clear_error (&error);
+ }
+ g_free (value);
+ }
+ else if (error)
+ {
+ g_warning ("%s: failed to get metadata '%s': %s\n",
+ G_STRFUNC, tag, error->message);
+ g_clear_error (&error);
+ }
+ }
+#else
+ gchar **values = gexiv2_metadata_get_tag_multiple (src, tag);
+
+ if (values)
+ {
+ gexiv2_metadata_set_tag_multiple (dest, tag, (const gchar **) values);
+ g_strfreev (values);
+ }
+ else
+ {
+ gchar *value = gexiv2_metadata_get_tag_string (src, tag);
+
+ if (value)
+ {
+ gexiv2_metadata_set_tag_string (dest, tag, value);
+ g_free (value);
+ }
+ }
+#endif
+}
+
+static void
+gimp_metadata_copy_tags (GExiv2Metadata *src,
+ GExiv2Metadata *dest,
+ const gchar **tags)
+{
+ gint i;
+
+ for (i = 0; tags[i] != NULL; i++)
+ {
+ /* don't copy the same tag multiple times */
+ if (i > 0 && ! strcmp (tags[i], tags[i - 1]))
+ continue;
+
+ gimp_metadata_copy_tag (src, dest, tags[i]);
+ }
+ }
+
+static void
+gimp_metadata_add (GimpMetadata *src,
+ GimpMetadata *dest)
+{
+ GExiv2Metadata *g2src = GEXIV2_METADATA (src);
+ GExiv2Metadata *g2dest = GEXIV2_METADATA (dest);
+
+ if (gexiv2_metadata_get_supports_exif (g2src) &&
+ gexiv2_metadata_get_supports_exif (g2dest))
+ {
+ gchar **exif_tags = gexiv2_metadata_get_exif_tags (g2src);
+
+ if (exif_tags)
+ {
+ gimp_metadata_copy_tags (g2src, g2dest,
+ (const gchar **) exif_tags);
+ g_strfreev (exif_tags);
+ }
+ }
+
+ if (gexiv2_metadata_get_supports_xmp (g2src) &&
+ gexiv2_metadata_get_supports_xmp (g2dest))
+ {
+ gchar **xmp_tags = gexiv2_metadata_get_xmp_tags (g2src);
+
+ if (xmp_tags)
+ {
+ gimp_metadata_copy_tags (g2src, g2dest,
+ (const gchar **) xmp_tags);
+ g_strfreev (xmp_tags);
+ }
+ }
+
+ if (gexiv2_metadata_get_supports_iptc (g2src) &&
+ gexiv2_metadata_get_supports_iptc (g2dest))
+ {
+ gchar **iptc_tags = gexiv2_metadata_get_iptc_tags (g2src);
+
+ if (iptc_tags)
+ {
+ gimp_metadata_copy_tags (g2src, g2dest,
+ (const gchar **) iptc_tags);
+ g_strfreev (iptc_tags);
+ }
+ }
+}