Files
gimp/libgimp/gimpimagemetadata.c
Jacob Boerema 34502e3829 libgimp: fix #1632 GIMP should not write to IPTC tag DateCreated
GIMP was saving the last changed/saved date to IPTC tag DateCreated,
which should only be used for the original creating date of the image
and thus should not be changed by GIMP.

After discussion in the cited issue, there is no tag in IPTC that we can
use, so we remove saving modified date from the IPTC metadata.

Instead we add two XMP tags, one for modified date and the other for the
date that metadata was changed. Since we do both when exporting, both are
saved with the same date/time in ISO 8601 format.

This also fixes another issue where we were not storing the timezone offset
for Xmp.tiff.DateTime. Since this has the same format as the other
XMP tags, we fix this together with this issue.
2022-06-06 21:34:34 -04:00

1186 lines
39 KiB
C

/* 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);
if (metadata)
{
gexiv2_metadata_erase_exif_thumbnail (GEXIV2_METADATA (metadata));
}
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);
}
/* 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)
{
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);
}
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);
}