From 3c57dd931145d43f2b0aef96c4d178135956bf91 Mon Sep 17 00:00:00 2001
From: Daniel Baumann <daniel.baumann@progress-linux.org>
Date: Fri, 19 Apr 2024 05:13:10 +0200
Subject: Adding upstream version 2.10.36.

Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
---
 libgimp/gimpimagemetadata.c | 1204 +++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 1204 insertions(+)
 create mode 100644 libgimp/gimpimagemetadata.c

(limited to 'libgimp/gimpimagemetadata.c')

diff --git a/libgimp/gimpimagemetadata.c b/libgimp/gimpimagemetadata.c
new file mode 100644
index 0000000..5adb20d
--- /dev/null
+++ b/libgimp/gimpimagemetadata.c
@@ -0,0 +1,1204 @@
+/* LIBGIMP - The GIMP Library
+ * Copyright (C) 1995-2000 Peter Mattis and Spencer Kimball
+ *
+ * gimpimagemetadata.c
+ *
+ * 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 <string.h>
+#include <sys/time.h>
+
+#include <gtk/gtk.h>
+#include <gexiv2/gexiv2.h>
+
+#include "gimp.h"
+#include "gimpui.h"
+#include "gimpimagemetadata.h"
+
+#include "libgimp-intl.h"
+
+
+typedef struct
+{
+  gchar *tag;
+  gint  type;
+} XmpStructs;
+
+static gchar *     gimp_image_metadata_interpret_comment    (gchar *comment);
+
+static void        gimp_image_metadata_rotate        (gint32             image_ID,
+                                                      GExiv2Orientation  orientation);
+static GdkPixbuf * gimp_image_metadata_rotate_pixbuf (GdkPixbuf         *pixbuf,
+                                                      GExiv2Orientation  orientation);
+static void        gimp_image_metadata_rotate_query  (gint32             image_ID,
+                                                      const gchar       *mime_type,
+                                                      GimpMetadata      *metadata,
+                                                      gboolean           interactive);
+static gboolean    gimp_image_metadata_rotate_dialog (gint32             image_ID,
+                                                      GExiv2Orientation  orientation,
+                                                      const gchar       *parasite_name);
+
+
+/*  public functions  */
+
+/**
+ * gimp_image_metadata_load_prepare:
+ * @image_ID:  The image
+ * @mime_type: The loaded file's mime-type
+ * @file:      The file to load the metadata from
+ * @error:     Return location for error
+ *
+ * Loads and returns metadata from @file to be passed into
+ * gimp_image_metadata_load_finish().
+ *
+ * Returns: The file's metadata.
+ *
+ * Since: 2.10
+ */
+GimpMetadata *
+gimp_image_metadata_load_prepare (gint32        image_ID,
+                                  const gchar  *mime_type,
+                                  GFile        *file,
+                                  GError      **error)
+{
+  GimpMetadata *metadata;
+
+  g_return_val_if_fail (image_ID > 0, NULL);
+  g_return_val_if_fail (mime_type != NULL, NULL);
+  g_return_val_if_fail (G_IS_FILE (file), NULL);
+  g_return_val_if_fail (error == NULL || *error == NULL, NULL);
+
+  metadata = gimp_metadata_load_from_file (file, error);
+
+  return metadata;
+}
+
+static gchar *
+gimp_image_metadata_interpret_comment (gchar *comment)
+{
+  /* Exiv2 can return unwanted text at the start of a comment
+   * taken from Exif.Photo.UserComment since 0.27.3.
+   * Let's remove that part and return NULL if there
+   * is nothing else left as comment. */
+
+  if (comment && g_str_has_prefix (comment, "charset=Ascii "))
+    {
+      gchar *real_comment;
+
+      /* Skip "charset=Ascii " (length 14) to find the real comment */
+      real_comment = comment + 14;
+      if (real_comment[0] == '\0' ||
+          ! g_strcmp0 (real_comment, "binary comment"))
+        {
+          g_free (comment);
+          return NULL;
+        }
+      else
+        {
+          real_comment = g_strdup (real_comment);
+          g_free (comment);
+          return real_comment;
+        }
+    }
+
+  return comment;
+}
+
+/**
+ * gimp_image_metadata_load_finish:
+ * @image_ID:    The image
+ * @mime_type:   The loaded file's mime-type
+ * @metadata:    The metadata to set on the image
+ * @flags:       Flags to specify what of the metadata to apply to the image
+ * @interactive: Whether this function is allowed to query info with dialogs
+ *
+ * Applies the @metadata previously loaded with
+ * gimp_image_metadata_load_prepare() to the image, taking into account
+ * the passed @flags.
+ *
+ * Since: 2.10
+ */
+void
+gimp_image_metadata_load_finish (gint32                 image_ID,
+                                 const gchar           *mime_type,
+                                 GimpMetadata          *metadata,
+                                 GimpMetadataLoadFlags  flags,
+                                 gboolean               interactive)
+{
+  g_return_if_fail (image_ID > 0);
+  g_return_if_fail (mime_type != NULL);
+  g_return_if_fail (GEXIV2_IS_METADATA (metadata));
+
+  if (flags & GIMP_METADATA_LOAD_COMMENT)
+    {
+      gchar *comment;
+
+      comment = gexiv2_metadata_get_tag_interpreted_string (GEXIV2_METADATA (metadata),
+                                                            "Exif.Photo.UserComment");
+      comment = gimp_image_metadata_interpret_comment (comment);
+
+      if (! comment)
+        comment = gexiv2_metadata_get_tag_interpreted_string (GEXIV2_METADATA (metadata),
+                                                              "Exif.Image.ImageDescription");
+
+      if (comment)
+        {
+          GimpParasite *parasite;
+
+          parasite = gimp_parasite_new ("gimp-comment",
+                                        GIMP_PARASITE_PERSISTENT,
+                                        strlen (comment) + 1,
+                                        comment);
+          g_free (comment);
+
+          gimp_image_attach_parasite (image_ID, parasite);
+          gimp_parasite_free (parasite);
+        }
+    }
+
+  if (flags & GIMP_METADATA_LOAD_RESOLUTION)
+    {
+      gdouble   xres;
+      gdouble   yres;
+      GimpUnit  unit;
+
+      if (gimp_metadata_get_resolution (metadata, &xres, &yres, &unit))
+        {
+          gimp_image_set_resolution (image_ID, xres, yres);
+          gimp_image_set_unit (image_ID, unit);
+        }
+    }
+
+  if (flags & GIMP_METADATA_LOAD_ORIENTATION)
+    {
+      gimp_image_metadata_rotate_query (image_ID, mime_type,
+                                        metadata, interactive);
+      /* Drop the orientation metadata in all cases, whether you rotated
+       * or not. See commit 8dcf258ffc on master.
+       */
+      gexiv2_metadata_set_orientation (GEXIV2_METADATA (metadata),
+                                       GEXIV2_ORIENTATION_NORMAL);
+    }
+
+  if (flags & GIMP_METADATA_LOAD_COLORSPACE)
+    {
+      GimpColorProfile *profile = gimp_image_get_color_profile (image_ID);
+
+      /* only look for colorspace information from metadata if the
+       * image didn't contain an embedded color profile
+       */
+      if (! profile)
+        {
+          GimpMetadataColorspace colorspace;
+
+          colorspace = gimp_metadata_get_colorspace (metadata);
+
+          switch (colorspace)
+            {
+            case GIMP_METADATA_COLORSPACE_UNSPECIFIED:
+            case GIMP_METADATA_COLORSPACE_UNCALIBRATED:
+            case GIMP_METADATA_COLORSPACE_SRGB:
+              /* use sRGB, a NULL profile will do the right thing  */
+              break;
+
+            case GIMP_METADATA_COLORSPACE_ADOBERGB:
+              profile = gimp_color_profile_new_rgb_adobe ();
+              break;
+            }
+
+          if (profile)
+            gimp_image_set_color_profile (image_ID, profile);
+        }
+
+      if (profile)
+        g_object_unref (profile);
+    }
+
+  gimp_image_set_metadata (image_ID, metadata);
+}
+
+/**
+ * gimp_image_metadata_save_prepare:
+ * @image_ID:        The image
+ * @mime_type:       The saved file's mime-type
+ * @suggested_flags: Suggested default values for the @flags passed to
+ *                   gimp_image_metadata_save_finish()
+ *
+ * Gets the image metadata for saving it using
+ * gimp_image_metadata_save_finish().
+ *
+ * The @suggested_flags are determined from what kind of metadata
+ * (Exif, XMP, ...) is actually present in the image and the preferences
+ * for metadata exporting.
+ * The calling application may still update @available_flags, for
+ * instance to follow the settings from a previous export in the same
+ * session, or a previous export of the same image. But it should not
+ * override the preferences without a good reason since it is a data
+ * leak.
+ *
+ * The suggested value for GIMP_METADATA_SAVE_THUMBNAIL is determined by
+ * whether there was a thumbnail in the previously imported image.
+ *
+ * Returns: The image's metadata, prepared for saving.
+ *
+ * Since: 2.10
+ */
+GimpMetadata *
+gimp_image_metadata_save_prepare (gint32                 image_ID,
+                                  const gchar           *mime_type,
+                                  GimpMetadataSaveFlags *suggested_flags)
+{
+  GimpMetadata *metadata;
+
+  g_return_val_if_fail (image_ID > 0, NULL);
+  g_return_val_if_fail (mime_type != NULL, NULL);
+  g_return_val_if_fail (suggested_flags != NULL, NULL);
+
+  *suggested_flags = GIMP_METADATA_SAVE_ALL;
+
+  metadata = gimp_image_get_metadata (image_ID);
+
+  if (metadata)
+    {
+      GDateTime          *datetime;
+      const GimpParasite *comment_parasite;
+      const gchar        *comment = NULL;
+      gint                image_width;
+      gint                image_height;
+      gdouble             xres;
+      gdouble             yres;
+      gchar               buffer[32];
+      gchar              *datetime_buf = NULL;
+      GExiv2Metadata     *g2metadata = GEXIV2_METADATA (metadata);
+
+      image_width  = gimp_image_width  (image_ID);
+      image_height = gimp_image_height (image_ID);
+
+      datetime = g_date_time_new_now_local ();
+
+      comment_parasite = gimp_image_get_parasite (image_ID, "gimp-comment");
+      if (comment_parasite)
+        comment = gimp_parasite_data (comment_parasite);
+
+      /* Exif */
+
+      if (! gimp_export_exif () ||
+          ! gexiv2_metadata_has_exif (g2metadata))
+        *suggested_flags &= ~GIMP_METADATA_SAVE_EXIF;
+
+      if (comment)
+        {
+          gexiv2_metadata_set_tag_string (g2metadata,
+                                          "Exif.Photo.UserComment",
+                                          comment);
+          gexiv2_metadata_set_tag_string (g2metadata,
+                                          "Exif.Image.ImageDescription",
+                                          comment);
+        }
+
+      g_snprintf (buffer, sizeof (buffer),
+                  "%d:%02d:%02d %02d:%02d:%02d",
+                  g_date_time_get_year (datetime),
+                  g_date_time_get_month (datetime),
+                  g_date_time_get_day_of_month (datetime),
+                  g_date_time_get_hour (datetime),
+                  g_date_time_get_minute (datetime),
+                  g_date_time_get_second (datetime));
+      gexiv2_metadata_set_tag_string (g2metadata,
+                                      "Exif.Image.DateTime",
+                                      buffer);
+
+      gexiv2_metadata_set_tag_string (g2metadata,
+                                      "Exif.Image.Software",
+                                      PACKAGE_STRING);
+
+      gimp_metadata_set_pixel_size (metadata,
+                                    image_width, image_height);
+
+      gimp_image_get_resolution (image_ID, &xres, &yres);
+      gimp_metadata_set_resolution (metadata, xres, yres,
+                                    gimp_image_get_unit (image_ID));
+
+      /* XMP */
+
+      if (! gimp_export_xmp () ||
+          ! gexiv2_metadata_has_xmp (g2metadata))
+        *suggested_flags &= ~GIMP_METADATA_SAVE_XMP;
+
+      gexiv2_metadata_set_tag_string (g2metadata,
+                                      "Xmp.dc.Format",
+                                      mime_type);
+
+      /* XMP uses datetime in ISO 8601 format */
+      datetime_buf = g_date_time_format (datetime, "%Y:%m:%dT%T\%:z");
+
+      gexiv2_metadata_set_tag_string (g2metadata,
+                                      "Xmp.xmp.ModifyDate",
+                                      datetime_buf);
+      gexiv2_metadata_set_tag_string (g2metadata,
+                                      "Xmp.xmp.MetadataDate",
+                                      datetime_buf);
+
+      if (! g_strcmp0 (mime_type, "image/tiff"))
+        {
+          /* TIFF specific XMP data */
+
+          g_snprintf (buffer, sizeof (buffer), "%d", image_width);
+          gexiv2_metadata_set_tag_string (g2metadata,
+                                          "Xmp.tiff.ImageWidth",
+                                          buffer);
+
+          g_snprintf (buffer, sizeof (buffer), "%d", image_height);
+          gexiv2_metadata_set_tag_string (g2metadata,
+                                          "Xmp.tiff.ImageLength",
+                                          buffer);
+
+          gexiv2_metadata_set_tag_string (g2metadata,
+                                          "Xmp.tiff.DateTime",
+                                          datetime_buf);
+        }
+
+      /* IPTC */
+
+      if (! gimp_export_iptc () ||
+          ! gexiv2_metadata_has_iptc (g2metadata))
+        *suggested_flags &= ~GIMP_METADATA_SAVE_IPTC;
+
+      g_free (datetime_buf);
+      g_date_time_unref (datetime);
+
+      /* EXIF Thumbnail */
+
+      if (gexiv2_metadata_has_exif (g2metadata))
+        {
+          gchar *value;
+
+          /* Check a required tag for a thumbnail to be present. */
+          value = gexiv2_metadata_get_tag_string (g2metadata,
+                                                 "Exif.Thumbnail.ImageLength");
+
+          if (! value)
+            *suggested_flags &= ~GIMP_METADATA_SAVE_THUMBNAIL;
+          else
+            g_free (value);
+        }
+      else
+        {
+          *suggested_flags &= ~GIMP_METADATA_SAVE_THUMBNAIL;
+        }
+    }
+
+  /* Thumbnail */
+
+  if (FALSE /* FIXME if (original image had a thumbnail) */)
+    *suggested_flags &= ~GIMP_METADATA_SAVE_THUMBNAIL;
+
+  /* Color profile */
+
+  if (! gimp_export_color_profile ())
+    *suggested_flags &= ~GIMP_METADATA_SAVE_COLOR_PROFILE;
+
+  return metadata;
+}
+
+static const gchar *
+gimp_fix_xmp_tag (const gchar *tag)
+{
+  gchar *substring;
+
+  /* Due to problems using /Iptc4xmpExt namespace (/iptcExt is used
+   * instead by Exiv2) we replace all occurrences with /iptcExt which
+   * is valid but less common. Not doing so would cause saving xmp
+   * metadata to fail. This has to be done after getting the values
+   * from the source metadata since that source uses the original
+   * tag names and would otherwise return NULL as value.
+   * /Iptc4xmpExt length = 12
+   * /iptcExt     length =  8
+   */
+
+  substring = strstr (tag, "/Iptc4xmpExt");
+  while (substring)
+    {
+      gint len_tag = strlen (tag);
+      gint len_end;
+
+      len_end = len_tag - (substring - tag) - 12;
+      strncpy (substring, "/iptcExt", 8);
+      substring += 8;
+      /* Using memmove: we have overlapping source and dest */
+      memmove (substring, substring+4, len_end);
+      substring[len_end] = '\0';
+      g_debug ("Fixed tag value: %s", tag);
+
+      /* Multiple occurrences are possible: e.g.:
+       * Xmp.iptcExt.ImageRegion[3]/Iptc4xmpExt:RegionBoundary/Iptc4xmpExt:rbVertices[1]/Iptc4xmpExt:rbX
+       */
+      substring = strstr (tag, "/Iptc4xmpExt");
+    }
+  return tag;
+}
+
+static void
+gimp_image_metadata_copy_tag (GExiv2Metadata *src,
+                              GExiv2Metadata *dest,
+                              const gchar    *tag)
+{
+  gchar **values = gexiv2_metadata_get_tag_multiple (src, tag);
+
+  if (values)
+    {
+      gchar *temp_tag;
+
+      /* Xmp always seems to return multiple values */
+      if (g_str_has_prefix (tag, "Xmp."))
+        temp_tag = (gchar *) gimp_fix_xmp_tag (g_strdup (tag));
+      else
+        temp_tag = g_strdup (tag);
+
+      g_debug ("Copy multi tag %s, first value: %s", temp_tag, values[0]);
+      gexiv2_metadata_set_tag_multiple (dest, temp_tag, (const gchar **) values);
+      g_free (temp_tag);
+      g_strfreev (values);
+    }
+  else
+    {
+      gchar *value = gexiv2_metadata_get_tag_string (src, tag);
+
+      if (value)
+        {
+          g_debug ("Copy tag %s, value: %s", tag, value);
+          gexiv2_metadata_set_tag_string (dest, tag, value);
+          g_free (value);
+        }
+    }
+}
+
+static gint
+gimp_natural_sort_compare (gconstpointer left,
+                           gconstpointer right)
+{
+  gint   compare;
+  gchar *left_key  = g_utf8_collate_key_for_filename ((gchar *) left, -1);
+  gchar *right_key = g_utf8_collate_key_for_filename ((gchar *) right, -1);
+
+  compare = g_strcmp0 (left_key, right_key);
+  g_free (left_key);
+  g_free (right_key);
+
+  return compare;
+}
+
+static GList*
+gimp_image_metadata_convert_tags_to_list (gchar **xmp_tags)
+{
+  GList *list = NULL;
+  gint   i;
+
+  for (i = 0; xmp_tags[i] != NULL; i++)
+    {
+      g_debug ("Tag: %s, tag type: %s", xmp_tags[i], gexiv2_metadata_get_tag_type(xmp_tags[i]));
+      list = g_list_prepend (list, xmp_tags[i]);
+    }
+  return list;
+}
+
+static GExiv2StructureType
+gimp_image_metadata_get_xmp_struct_type (const gchar *tag)
+{
+  g_debug ("Struct type for tag: %s, type: %s", tag, gexiv2_metadata_get_tag_type (tag));
+
+  if (! g_strcmp0 (gexiv2_metadata_get_tag_type (tag), "XmpSeq"))
+    {
+      return GEXIV2_STRUCTURE_XA_SEQ;
+    }
+
+  return GEXIV2_STRUCTURE_XA_BAG;
+}
+
+static void
+gimp_image_metadata_set_xmp_structs (GList          *xmp_list,
+                                     GExiv2Metadata *metadata)
+{
+  GList *list;
+  gchar *prev_one = NULL;
+  gchar *prev_two = NULL;
+
+  for (list = xmp_list; list != NULL; list = list->next)
+    {
+      gchar **tag_split;
+
+      /*
+       * Most tags with structs have only one struct part, like:
+       * Xmp.xmpMM.History[1]...
+       * However there are also Xmp tags that have two
+       * structs in one tag, e.g.:
+       * Xmp.crs.GradientBasedCorrections[1]/crs:CorrectionMasks[1]...
+       */
+      tag_split = g_strsplit ((gchar *) list->data, "[1]", 3);
+      /* Check if there are at least two parts but don't catch xxx[2]/yyy[1]/zzz */
+      if (tag_split && tag_split[1] && ! strstr (tag_split[0], "["))
+        {
+          if (! prev_one || strcmp (tag_split[0], prev_one) != 0)
+            {
+              GExiv2StructureType  type;
+
+              g_free (prev_one);
+              prev_one = g_strdup (tag_split[0]);
+
+              type = gimp_image_metadata_get_xmp_struct_type (gimp_fix_xmp_tag (tag_split[0]));
+              gexiv2_metadata_set_xmp_tag_struct (GEXIV2_METADATA (metadata),
+                                                  prev_one, type);
+            }
+          if (tag_split[2] && (!prev_two || strcmp (tag_split[1], prev_two) != 0))
+            {
+              gchar               *second_struct;
+              GExiv2StructureType  type;
+
+              g_free (prev_two);
+              prev_two = g_strdup (tag_split[1]);
+              second_struct = g_strdup_printf ("%s[1]%s", prev_one, gimp_fix_xmp_tag(prev_two));
+
+              type = gimp_image_metadata_get_xmp_struct_type (gimp_fix_xmp_tag (tag_split[1]));
+              gexiv2_metadata_set_xmp_tag_struct (GEXIV2_METADATA (metadata),
+                                                  second_struct, type);
+              g_free (second_struct);
+            }
+        }
+
+      g_strfreev (tag_split);
+    }
+  g_free (prev_one);
+  g_free (prev_two);
+}
+
+/**
+ * gimp_image_metadata_save_finish:
+ * @image_ID:  The image
+ * @mime_type: The saved file's mime-type
+ * @metadata:  The metadata to set on the image
+ * @flags:     Flags to specify what of the metadata to save
+ * @file:      The file to load the metadata from
+ * @error:     Return location for error message
+ *
+ * Saves the @metadata retrieved from the image with
+ * gimp_image_metadata_save_prepare() to @file, taking into account
+ * the passed @flags.
+ *
+ * Return value: Whether the save was successful.
+ *
+ * Since: 2.10
+ */
+gboolean
+gimp_image_metadata_save_finish (gint32                  image_ID,
+                                 const gchar            *mime_type,
+                                 GimpMetadata           *metadata,
+                                 GimpMetadataSaveFlags   flags,
+                                 GFile                  *file,
+                                 GError                **error)
+{
+  GimpMetadata   *new_metadata;
+  GExiv2Metadata *new_g2metadata;
+  gboolean        support_exif;
+  gboolean        support_xmp;
+  gboolean        support_iptc;
+  gboolean        success = FALSE;
+  gint            i;
+
+  g_return_val_if_fail (image_ID > 0, FALSE);
+  g_return_val_if_fail (mime_type != NULL, FALSE);
+  g_return_val_if_fail (GEXIV2_IS_METADATA (metadata), FALSE);
+  g_return_val_if_fail (G_IS_FILE (file), FALSE);
+  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+  if (! (flags & (GIMP_METADATA_SAVE_EXIF |
+                  GIMP_METADATA_SAVE_XMP  |
+                  GIMP_METADATA_SAVE_IPTC |
+                  GIMP_METADATA_SAVE_THUMBNAIL)))
+    return TRUE;
+
+  /* read metadata from saved file */
+  new_metadata = gimp_metadata_load_from_file (file, error);
+  new_g2metadata = GEXIV2_METADATA (new_metadata);
+
+  if (! new_metadata)
+    return FALSE;
+
+  support_exif = gexiv2_metadata_get_supports_exif (new_g2metadata);
+  support_xmp  = gexiv2_metadata_get_supports_xmp  (new_g2metadata);
+  support_iptc = gexiv2_metadata_get_supports_iptc (new_g2metadata);
+
+  if ((flags & GIMP_METADATA_SAVE_EXIF) && support_exif)
+    {
+      gchar **exif_data = gexiv2_metadata_get_exif_tags (GEXIV2_METADATA (metadata));
+
+      for (i = 0; exif_data[i] != NULL; i++)
+        {
+          if (! gexiv2_metadata_has_tag (new_g2metadata, exif_data[i]) &&
+              gimp_metadata_is_tag_supported (exif_data[i], mime_type))
+            {
+              gimp_image_metadata_copy_tag (GEXIV2_METADATA (metadata),
+                                            new_g2metadata,
+                                            exif_data[i]);
+            }
+        }
+
+      g_strfreev (exif_data);
+    }
+
+  if ((flags & GIMP_METADATA_SAVE_XMP) && support_xmp)
+    {
+      gchar         **xmp_data;
+      struct timeval  timer_usec;
+      gint64          timestamp_usec;
+      gchar           ts[128];
+      GList          *xmp_list = NULL;
+      GList          *list;
+
+      gettimeofday (&timer_usec, NULL);
+      timestamp_usec = ((gint64) timer_usec.tv_sec) * 1000000ll +
+                        (gint64) timer_usec.tv_usec;
+      g_snprintf (ts, sizeof (ts), "%" G_GINT64_FORMAT, timestamp_usec);
+
+      gimp_metadata_add_xmp_history (metadata, "");
+
+      gexiv2_metadata_set_tag_string (GEXIV2_METADATA (metadata),
+                                      "Xmp.GIMP.TimeStamp",
+                                      ts);
+
+      gexiv2_metadata_set_tag_string (GEXIV2_METADATA (metadata),
+                                      "Xmp.xmp.CreatorTool",
+                                      N_("GIMP 2.10"));
+
+      gexiv2_metadata_set_tag_string (GEXIV2_METADATA (metadata),
+                                      "Xmp.GIMP.Version",
+                                      GIMP_VERSION);
+
+      gexiv2_metadata_set_tag_string (GEXIV2_METADATA (metadata),
+                                      "Xmp.GIMP.API",
+                                      GIMP_API_VERSION);
+
+      gexiv2_metadata_set_tag_string (GEXIV2_METADATA (metadata),
+                                      "Xmp.GIMP.Platform",
+#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
+                                      );
+
+      xmp_data = gexiv2_metadata_get_xmp_tags (GEXIV2_METADATA (metadata));
+
+      xmp_list = gimp_image_metadata_convert_tags_to_list (xmp_data);
+      xmp_list = g_list_sort (xmp_list, (GCompareFunc) gimp_natural_sort_compare);
+      gimp_image_metadata_set_xmp_structs (xmp_list, new_g2metadata);
+
+      for (list = xmp_list; list != NULL; list = list->next)
+        {
+          if (! gexiv2_metadata_has_tag (new_g2metadata, (gchar *) list->data) &&
+              gimp_metadata_is_tag_supported ((gchar *) list->data, mime_type))
+            {
+              gimp_image_metadata_copy_tag (GEXIV2_METADATA (metadata),
+                                            new_g2metadata,
+                                            (gchar *) list->data);
+            }
+          else
+            g_debug ("Ignored tag: %s", (gchar *) list->data);
+        }
+
+      g_list_free (xmp_list);
+      g_strfreev (xmp_data);
+    }
+
+  if ((flags & GIMP_METADATA_SAVE_IPTC) && support_iptc)
+    {
+      gchar **iptc_data = gexiv2_metadata_get_iptc_tags (GEXIV2_METADATA (metadata));
+
+      for (i = 0; iptc_data[i] != NULL; i++)
+        {
+          if (! gexiv2_metadata_has_tag (new_g2metadata, iptc_data[i]) &&
+              gimp_metadata_is_tag_supported (iptc_data[i], mime_type))
+            {
+              gimp_image_metadata_copy_tag (GEXIV2_METADATA (metadata),
+                                            new_g2metadata,
+                                            iptc_data[i]);
+            }
+        }
+
+      g_strfreev (iptc_data);
+    }
+
+  if (flags & GIMP_METADATA_SAVE_THUMBNAIL && support_exif)
+    {
+      GdkPixbuf *thumb_pixbuf;
+      gchar     *thumb_buffer;
+      gint       image_width;
+      gint       image_height;
+      gsize      count;
+      gint       thumbw;
+      gint       thumbh;
+
+#define EXIF_THUMBNAIL_SIZE 256
+
+      image_width  = gimp_image_width  (image_ID);
+      image_height = gimp_image_height (image_ID);
+
+      if (image_width > image_height)
+        {
+          thumbw = EXIF_THUMBNAIL_SIZE;
+          thumbh = EXIF_THUMBNAIL_SIZE * image_height / image_width;
+        }
+      else
+        {
+          thumbh = EXIF_THUMBNAIL_SIZE;
+          thumbw = EXIF_THUMBNAIL_SIZE * image_width / image_height;
+        }
+
+      thumb_pixbuf = gimp_image_get_thumbnail (image_ID, thumbw, thumbh,
+                                               GIMP_PIXBUF_KEEP_ALPHA);
+
+      if (gdk_pixbuf_save_to_buffer (thumb_pixbuf, &thumb_buffer, &count,
+                                     "jpeg", NULL,
+                                     "quality", "75",
+                                     NULL))
+        {
+          gchar buffer[32];
+
+          gexiv2_metadata_set_exif_thumbnail_from_buffer (new_g2metadata,
+                                                          (guchar *) thumb_buffer,
+                                                          count);
+
+          g_snprintf (buffer, sizeof (buffer), "%d", thumbw);
+          gexiv2_metadata_set_tag_string (new_g2metadata,
+                                          "Exif.Thumbnail.ImageWidth",
+                                          buffer);
+
+          g_snprintf (buffer, sizeof (buffer), "%d", thumbh);
+          gexiv2_metadata_set_tag_string (new_g2metadata,
+                                          "Exif.Thumbnail.ImageLength",
+                                          buffer);
+
+          gexiv2_metadata_set_tag_string (new_g2metadata,
+                                          "Exif.Thumbnail.BitsPerSample",
+                                          "8 8 8");
+          gexiv2_metadata_set_tag_string (new_g2metadata,
+                                          "Exif.Thumbnail.SamplesPerPixel",
+                                          "3");
+          gexiv2_metadata_set_tag_string (new_g2metadata,
+                                          "Exif.Thumbnail.PhotometricInterpretation",
+                                          "6"); /* old jpeg */
+          gexiv2_metadata_set_tag_string (new_g2metadata,
+                                          "Exif.Thumbnail.NewSubfileType",
+                                          "1"); /* reduced resolution image */
+
+          g_free (thumb_buffer);
+        }
+
+      g_object_unref (thumb_pixbuf);
+    }
+  else
+    {
+      /* Remove Thumbnail */
+      gexiv2_metadata_erase_exif_thumbnail (new_g2metadata);
+    }
+
+  if (flags & GIMP_METADATA_SAVE_COLOR_PROFILE)
+    {
+      /* nothing to do, but if we ever need to modify metadata based
+       * on the exported color profile, this is probably the place to
+       * add it
+       */
+    }
+
+  success = gimp_metadata_save_to_file (new_metadata, file, error);
+
+  g_object_unref (new_metadata);
+
+  return success;
+}
+
+gint32
+gimp_image_metadata_load_thumbnail (GFile   *file,
+                                    GError **error)
+{
+  GimpMetadata *metadata;
+  GInputStream *input_stream;
+  GdkPixbuf    *pixbuf;
+  guint8       *thumbnail_buffer;
+  gint          thumbnail_size;
+  gint32        image_ID = -1;
+
+  g_return_val_if_fail (G_IS_FILE (file), -1);
+  g_return_val_if_fail (error == NULL || *error == NULL, -1);
+
+  metadata = gimp_metadata_load_from_file (file, error);
+  if (! metadata)
+    return -1;
+
+  if (! gexiv2_metadata_get_exif_thumbnail (GEXIV2_METADATA (metadata),
+                                            &thumbnail_buffer,
+                                            &thumbnail_size))
+    {
+      g_object_unref (metadata);
+      return -1;
+    }
+
+  input_stream = g_memory_input_stream_new_from_data (thumbnail_buffer,
+                                                      thumbnail_size,
+                                                      (GDestroyNotify) g_free);
+  pixbuf = gdk_pixbuf_new_from_stream (input_stream, NULL, error);
+  g_object_unref (input_stream);
+
+  if (pixbuf)
+    {
+      gint32 layer_ID;
+
+      image_ID = gimp_image_new (gdk_pixbuf_get_width  (pixbuf),
+                                 gdk_pixbuf_get_height (pixbuf),
+                                 GIMP_RGB);
+      gimp_image_undo_disable (image_ID);
+
+      layer_ID = gimp_layer_new_from_pixbuf (image_ID, _("Background"),
+                                             pixbuf,
+                                             100.0,
+                                             gimp_image_get_default_new_layer_mode (image_ID),
+                                             0.0, 0.0);
+      g_object_unref (pixbuf);
+
+      gimp_image_insert_layer (image_ID, layer_ID, -1, 0);
+
+      gimp_image_metadata_rotate (image_ID,
+                                  gexiv2_metadata_get_orientation (GEXIV2_METADATA (metadata)));
+    }
+
+  g_object_unref (metadata);
+
+  return image_ID;
+}
+
+
+/*  private functions  */
+
+static void
+gimp_image_metadata_rotate (gint32             image_ID,
+                            GExiv2Orientation  orientation)
+{
+  switch (orientation)
+    {
+    case GEXIV2_ORIENTATION_UNSPECIFIED:
+    case GEXIV2_ORIENTATION_NORMAL:  /* standard orientation, do nothing */
+      break;
+
+    case GEXIV2_ORIENTATION_HFLIP:
+      gimp_image_flip (image_ID, GIMP_ORIENTATION_HORIZONTAL);
+      break;
+
+    case GEXIV2_ORIENTATION_ROT_180:
+      gimp_image_rotate (image_ID, GIMP_ROTATE_180);
+      break;
+
+    case GEXIV2_ORIENTATION_VFLIP:
+      gimp_image_flip (image_ID, GIMP_ORIENTATION_VERTICAL);
+      break;
+
+    case GEXIV2_ORIENTATION_ROT_90_HFLIP:  /* flipped diagonally around '\' */
+      gimp_image_rotate (image_ID, GIMP_ROTATE_90);
+      gimp_image_flip (image_ID, GIMP_ORIENTATION_HORIZONTAL);
+      break;
+
+    case GEXIV2_ORIENTATION_ROT_90:  /* 90 CW */
+      gimp_image_rotate (image_ID, GIMP_ROTATE_90);
+      break;
+
+    case GEXIV2_ORIENTATION_ROT_90_VFLIP:  /* flipped diagonally around '/' */
+      gimp_image_rotate (image_ID, GIMP_ROTATE_90);
+      gimp_image_flip (image_ID, GIMP_ORIENTATION_VERTICAL);
+      break;
+
+    case GEXIV2_ORIENTATION_ROT_270:  /* 90 CCW */
+      gimp_image_rotate (image_ID, GIMP_ROTATE_270);
+      break;
+
+    default: /* shouldn't happen */
+      break;
+    }
+}
+
+static GdkPixbuf *
+gimp_image_metadata_rotate_pixbuf (GdkPixbuf         *pixbuf,
+                                   GExiv2Orientation  orientation)
+{
+  GdkPixbuf *rotated = NULL;
+  GdkPixbuf *temp;
+
+  switch (orientation)
+    {
+    case GEXIV2_ORIENTATION_UNSPECIFIED:
+    case GEXIV2_ORIENTATION_NORMAL:  /* standard orientation, do nothing */
+      rotated = g_object_ref (pixbuf);
+      break;
+
+    case GEXIV2_ORIENTATION_HFLIP:
+      rotated = gdk_pixbuf_flip (pixbuf, TRUE);
+      break;
+
+    case GEXIV2_ORIENTATION_ROT_180:
+      rotated = gdk_pixbuf_rotate_simple (pixbuf, GDK_PIXBUF_ROTATE_UPSIDEDOWN);
+      break;
+
+    case GEXIV2_ORIENTATION_VFLIP:
+      rotated = gdk_pixbuf_flip (pixbuf, FALSE);
+      break;
+
+    case GEXIV2_ORIENTATION_ROT_90_HFLIP:  /* flipped diagonally around '\' */
+      temp = gdk_pixbuf_rotate_simple (pixbuf, GDK_PIXBUF_ROTATE_CLOCKWISE);
+      rotated = gdk_pixbuf_flip (temp, TRUE);
+      g_object_unref (temp);
+      break;
+
+    case GEXIV2_ORIENTATION_ROT_90:  /* 90 CW */
+      rotated = gdk_pixbuf_rotate_simple (pixbuf, GDK_PIXBUF_ROTATE_CLOCKWISE);
+      break;
+
+    case GEXIV2_ORIENTATION_ROT_90_VFLIP:  /* flipped diagonally around '/' */
+      temp = gdk_pixbuf_rotate_simple (pixbuf, GDK_PIXBUF_ROTATE_CLOCKWISE);
+      rotated = gdk_pixbuf_flip (temp, FALSE);
+      g_object_unref (temp);
+      break;
+
+    case GEXIV2_ORIENTATION_ROT_270:  /* 90 CCW */
+      rotated = gdk_pixbuf_rotate_simple (pixbuf, GDK_PIXBUF_ROTATE_COUNTERCLOCKWISE);
+      break;
+
+    default: /* shouldn't happen */
+      break;
+    }
+
+  return rotated;
+}
+
+static void
+gimp_image_metadata_rotate_query (gint32        image_ID,
+                                  const gchar  *mime_type,
+                                  GimpMetadata *metadata,
+                                  gboolean      interactive)
+{
+  GimpParasite      *parasite;
+  gchar             *parasite_name;
+  GExiv2Orientation  orientation;
+  gboolean           query = interactive;
+
+  orientation = gexiv2_metadata_get_orientation (GEXIV2_METADATA (metadata));
+
+  if (orientation <= GEXIV2_ORIENTATION_NORMAL ||
+      orientation >  GEXIV2_ORIENTATION_MAX)
+    return;
+
+  parasite_name = g_strdup_printf ("gimp-metadata-exif-rotate(%s)", mime_type);
+
+  parasite = gimp_get_parasite (parasite_name);
+
+  if (parasite)
+    {
+      if (strncmp (gimp_parasite_data (parasite), "yes",
+                   gimp_parasite_data_size (parasite)) == 0)
+        {
+          query = FALSE;
+        }
+      else if (strncmp (gimp_parasite_data (parasite), "no",
+                        gimp_parasite_data_size (parasite)) == 0)
+        {
+          gimp_parasite_free (parasite);
+          g_free (parasite_name);
+          return;
+        }
+
+      gimp_parasite_free (parasite);
+    }
+
+  if (query && ! gimp_image_metadata_rotate_dialog (image_ID,
+                                                    orientation,
+                                                    parasite_name))
+    {
+      g_free (parasite_name);
+      return;
+    }
+
+  g_free (parasite_name);
+
+  gimp_image_metadata_rotate (image_ID, orientation);
+  gexiv2_metadata_set_orientation (GEXIV2_METADATA (metadata),
+                                   GEXIV2_ORIENTATION_NORMAL);
+}
+
+static gboolean
+gimp_image_metadata_rotate_dialog (gint32             image_ID,
+                                   GExiv2Orientation  orientation,
+                                   const gchar       *parasite_name)
+{
+  GtkWidget *dialog;
+  GtkWidget *main_vbox;
+  GtkWidget *vbox;
+  GtkWidget *label;
+  GtkWidget *toggle;
+  GdkPixbuf *pixbuf;
+  gchar     *name;
+  gchar     *title;
+  gint       response;
+
+  name = gimp_image_get_name (image_ID);
+  title = g_strdup_printf (_("Rotate %s?"), name);
+  g_free (name);
+
+  dialog = gimp_dialog_new (title, "gimp-metadata-rotate-dialog",
+                            NULL, 0, NULL, NULL,
+
+                            _("_Keep Original"), GTK_RESPONSE_CANCEL,
+                            _("_Rotate"),        GTK_RESPONSE_OK,
+
+                            NULL);
+
+  g_free (title);
+
+  gtk_dialog_set_alternative_button_order (GTK_DIALOG (dialog),
+                                           GTK_RESPONSE_OK,
+                                           GTK_RESPONSE_CANCEL,
+                                           -1);
+
+  gtk_window_set_resizable (GTK_WINDOW (dialog), FALSE);
+  gimp_window_set_transient (GTK_WINDOW (dialog));
+
+  main_vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12);
+  gtk_container_set_border_width (GTK_CONTAINER (main_vbox), 12);
+  gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))),
+                      main_vbox, FALSE, FALSE, 0);
+  gtk_widget_show (main_vbox);
+
+#define THUMBNAIL_SIZE 128
+
+  pixbuf = gimp_image_get_thumbnail (image_ID,
+                                     THUMBNAIL_SIZE, THUMBNAIL_SIZE,
+                                     GIMP_PIXBUF_SMALL_CHECKS);
+
+  if (pixbuf)
+    {
+      GdkPixbuf *rotated;
+      GtkWidget *hbox;
+      GtkWidget *image;
+
+      hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12);
+      gtk_box_set_homogeneous (GTK_BOX (hbox), TRUE);
+      gtk_box_pack_start (GTK_BOX (main_vbox), hbox, FALSE, FALSE, 0);
+      gtk_widget_show (hbox);
+
+      vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
+      gtk_box_pack_start (GTK_BOX (hbox), vbox, TRUE, TRUE, 0);
+      gtk_widget_show (vbox);
+
+      label = gtk_label_new (_("Original"));
+      gtk_label_set_ellipsize (GTK_LABEL (label), PANGO_ELLIPSIZE_MIDDLE);
+      gimp_label_set_attributes (GTK_LABEL (label),
+                                 PANGO_ATTR_STYLE,  PANGO_STYLE_ITALIC,
+                                 -1);
+      gtk_box_pack_end (GTK_BOX (vbox), label, FALSE, FALSE, 0);
+      gtk_widget_show (label);
+
+      image = gtk_image_new_from_pixbuf (pixbuf);
+      gtk_box_pack_end (GTK_BOX (vbox), image, FALSE, FALSE, 0);
+      gtk_widget_show (image);
+
+      vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
+      gtk_box_pack_start (GTK_BOX (hbox), vbox, TRUE, TRUE, 0);
+      gtk_widget_show (vbox);
+
+      label = gtk_label_new (_("Rotated"));
+      gimp_label_set_attributes (GTK_LABEL (label),
+                                 PANGO_ATTR_STYLE,  PANGO_STYLE_ITALIC,
+                                 -1);
+      gtk_box_pack_end (GTK_BOX (vbox), label, FALSE, FALSE, 0);
+      gtk_widget_show (label);
+
+      rotated = gimp_image_metadata_rotate_pixbuf (pixbuf, orientation);
+      g_object_unref (pixbuf);
+
+      image = gtk_image_new_from_pixbuf (rotated);
+      g_object_unref (rotated);
+
+      gtk_box_pack_end (GTK_BOX (vbox), image, FALSE, FALSE, 0);
+      gtk_widget_show (image);
+    }
+
+  label = g_object_new (GTK_TYPE_LABEL,
+                        "label",   _("This image contains Exif orientation "
+                                     "metadata."),
+                        "wrap",    TRUE,
+                        "justify", GTK_JUSTIFY_LEFT,
+                        "xalign",  0.0,
+                        "yalign",  0.5,
+                        NULL);
+  gimp_label_set_attributes (GTK_LABEL (label),
+                             PANGO_ATTR_SCALE,  PANGO_SCALE_LARGE,
+                             PANGO_ATTR_WEIGHT, PANGO_WEIGHT_BOLD,
+                             -1);
+  /* eek */
+  gtk_widget_set_size_request (GTK_WIDGET (label),
+                               2 * THUMBNAIL_SIZE + 12, -1);
+  gtk_box_pack_start (GTK_BOX (main_vbox), label, FALSE, FALSE, 0);
+  gtk_widget_show (label);
+
+  label = g_object_new (GTK_TYPE_LABEL,
+                        "label",   _("Would you like to rotate the image?"),
+                        "wrap",    TRUE,
+                        "justify", GTK_JUSTIFY_LEFT,
+                        "xalign",  0.0,
+                        "yalign",  0.5,
+                        NULL);
+  /* eek */
+  gtk_widget_set_size_request (GTK_WIDGET (label),
+                               2 * THUMBNAIL_SIZE + 12, -1);
+  gtk_box_pack_start (GTK_BOX (main_vbox), label, FALSE, FALSE, 0);
+  gtk_widget_show (label);
+
+  toggle = gtk_check_button_new_with_mnemonic (_("_Don't ask me again"));
+  gtk_box_pack_end (GTK_BOX (main_vbox), toggle, FALSE, FALSE, 0);
+  gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), FALSE);
+  gtk_widget_show (toggle);
+
+  response = gimp_dialog_run (GIMP_DIALOG (dialog));
+
+  if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (toggle)))
+    {
+      GimpParasite *parasite;
+      const gchar  *str = (response == GTK_RESPONSE_OK) ? "yes" : "no";
+
+      parasite = gimp_parasite_new (parasite_name,
+                                    GIMP_PARASITE_PERSISTENT,
+                                    strlen (str), str);
+      gimp_attach_parasite (parasite);
+      gimp_parasite_free (parasite);
+    }
+
+  gtk_widget_destroy (dialog);
+
+  return (response == GTK_RESPONSE_OK);
+}
-- 
cgit v1.2.3