summaryrefslogtreecommitdiffstats
path: root/libgimp/gimpimagemetadata.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 18:30:19 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 18:30:19 +0000
commit5c1676dfe6d2f3c837a5e074117b45613fd29a72 (patch)
treecbffb45144febf451e54061db2b21395faf94bfe /libgimp/gimpimagemetadata.c
parentInitial commit. (diff)
downloadgimp-upstream.tar.xz
gimp-upstream.zip
Adding upstream version 2.10.34.upstream/2.10.34upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'libgimp/gimpimagemetadata.c')
-rw-r--r--libgimp/gimpimagemetadata.c1204
1 files changed, 1204 insertions, 0 deletions
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);
+}