From 0c3b218886342e44275b087c41faf3b6a2b7f664 Mon Sep 17 00:00:00 2001 From: Tomas Bzatek Date: Mon, 13 May 2019 22:16:28 +0200 Subject: jpeg-utils: Port to gexiv2 The gexiv2 library is just a GObject wrapper around exiv2 library. It's a healthy project that continually keeps up with exiv2 API changes. Adopted by e.g. GIMP there's certain guarantee of future maintenance. This allows us to get rid of C++ code, making it more readable and predictable. --- src/jpeg-utils.c | 992 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 992 insertions(+) create mode 100644 src/jpeg-utils.c (limited to 'src/jpeg-utils.c') diff --git a/src/jpeg-utils.c b/src/jpeg-utils.c new file mode 100644 index 0000000..64cfdff --- /dev/null +++ b/src/jpeg-utils.c @@ -0,0 +1,992 @@ +/* Cataract - Static web photo gallery generator + * Copyright (C) 2008 Tomas Bzatek + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#define _XOPEN_SOURCE + +#include "config.h" + +#include +#include +#include +#include + +#include + +#ifdef HAVE_IMAGEMAGICK_7 +# include +#else +# include +#endif + +#include "jpeg-utils.h" +#include "gallery-utils.h" + + +struct ExifDataPrivate { + GExiv2Metadata *metadata; +}; + + +/* + * Thread-safe ImageMagick and exiv2 libraries initialization and cleanup + */ +void +init_jpeg_utils (void) +{ + MagickWandGenesis(); + g_assert (gexiv2_initialize () == TRUE); +} + +void +destroy_jpeg_utils (void) +{ + MagickWandTerminus(); +} + + +static void +shift_time (struct tm *tm, int offset_min) +{ + time_t t; + + if (offset_min != 0) { + /* FIXME: converting between time formats could make some data lost, better to operate over struct tm directly */ + t = mktime (tm); + if (t == (time_t) -1) { + log_error ("Cannot shift time %p by %d minutes\n", tm, offset_min); + return; + } + + t += offset_min * 60; + + localtime_r (&t, tm); + } +} + +static struct tm * +parse_exif_date (const char *str) +{ + struct tm *tm; + char *res; + + tm = (struct tm *) g_malloc0 (sizeof (struct tm)); + + res = strptime (str, "%Y:%m:%d %H:%M:%S", tm); + if (res == NULL || *res != '\0') + return NULL; + + mktime (tm); + + if (tm->tm_isdst) + shift_time (tm, -60); + + return tm; +} + +static struct tm * +parse_exif_date_with_overrides (const char *str, ExifData *exif) +{ + struct tm *tt; + + if (exif->override_datetime != (time_t) -1) { + tt = (struct tm *) g_malloc0 (sizeof (struct tm)); + localtime_r (&exif->override_datetime, tt); + return tt; + } + + tt = parse_exif_date (str); + if (tt) { + shift_time (tt, exif->timezone_shift); + return tt; + } + + return NULL; +} + +static gchar * +format_exif_time (struct tm *tm, const gchar *format_string) +{ + char conv[1024]; + + memset (&conv, 0, sizeof(conv)); + if (strftime (&conv[0], sizeof(conv), + format_string ? format_string : "%Y:%m:%d %H:%M:%S", + tm)) + return g_strdup (&conv[0]); + + return NULL; +} + +/* + * EXIF and IPTC info retrieval, keeps the source file open until freed + */ +ExifData * +read_exif (const gchar *filename) +{ + ExifData *data; + GError *error = NULL; + + g_return_val_if_fail (filename != NULL, NULL); + + data = exif_data_new_empty (); + data->priv->metadata = gexiv2_metadata_new (); + if (! gexiv2_metadata_open_path (data->priv->metadata, filename, &error)) { + log_error ("read_exif: %s\n", error->message); + g_error_free (error); + exif_data_free (data); + return NULL; + } + + return data; +} + +ExifData * +exif_data_new_empty () +{ + ExifData *data; + + data = (ExifData*) g_malloc0 (sizeof (ExifData)); + data->priv = (ExifDataPrivate*) g_malloc0 (sizeof (ExifDataPrivate)); + + return data; +} + +void +exif_data_free (ExifData *data) +{ + if (data) { + g_free (data->override_copyright); + g_free (data->external_exif_data); + g_free (data->override_artist_name); + g_clear_object (&data->priv->metadata); + g_free (data->priv); + g_free (data); + } +} + +static const gchar * +get_real_key_name (const gchar *key) +{ + struct StrKeyPair { + const gchar *from; + const gchar *to; + }; + static const struct StrKeyPair conv[] = { + { EXIF_CANON_CAMERA_TEMP, "Exif.CanonSi.0x000c" }, + }; + + guint i; + + for (i = 0; i < G_N_ELEMENTS (conv); i++) + if (g_str_equal (conv[i].from, key)) + return conv[i].to; + + return key; +} + +/* + * Retrieves value of the specified key or NULL if the key does not exist. + * The key argument belongs to Exiv2 namespace - see https://exiv2.org/metadata.html + */ +gchar * +get_exif_data (ExifData *exif, const gchar *key) +{ + g_return_val_if_fail (exif != NULL, NULL); + g_return_val_if_fail (key != NULL, NULL); + + key = get_real_key_name (key); + + if (g_str_equal (key, JPEG_COMMENT)) + return exif->override_copyright ? g_strdup (exif->override_copyright) : gexiv2_metadata_get_comment (exif->priv->metadata); + + if (g_str_equal (key, EXIF_ARTIST) && exif->override_artist_name) + return g_strdup (exif->override_artist_name); + + if (g_str_equal (key, EXIF_APERTURE) && exif->override_aperture != -1) + return g_strdup_printf ("%f", exif->override_aperture); + + if (g_str_equal (key, EXIF_FOCAL_LENGTH) && exif->override_focal_length != -1) + return g_strdup_printf ("%d/%d", (gint) (exif->override_focal_length * 1000000.0), 1000000); + + if ((exif->override_datetime != (time_t) -1 || exif->timezone_shift) && + (g_str_equal (key, EXIF_DATETIME) || + g_str_equal (key, "Exif.Photo.DateTimeOriginal") || + g_str_equal (key, "Exif.Photo.DateTimeDigitized") || + g_str_equal (key, "Exif.Image.DateTime"))) + { + gchar *val; + struct tm *tt; + + val = gexiv2_metadata_get_tag_string (exif->priv->metadata, key); + if (! val) + return NULL; + + tt = parse_exif_date_with_overrides (val, exif); + g_free (val); + if (tt) { + gchar *res = format_exif_time (tt, NULL); + g_free (tt); + return res; + } + } + + return gexiv2_metadata_get_tag_string (exif->priv->metadata, key); +} + +gchar * +get_exif_data_fixed (ExifData *exif, const gchar *key) +{ + g_return_val_if_fail (exif != NULL, NULL); + g_return_val_if_fail (key != NULL, NULL); + + if (g_str_equal (key, EXIF_APERTURE)) { + gdouble aperture = exif->override_aperture != -1 ? aperture = exif->override_aperture : gexiv2_metadata_get_fnumber (exif->priv->metadata); + if (aperture != -1.0) + return g_strdup_printf ("ƒ/%.1f", aperture); + } + + if (g_str_equal (key, EXIF_EXPOSURE)) { + gint nom, den; + + if (gexiv2_metadata_get_exposure_time (exif->priv->metadata, &nom, &den)) { + gdouble val = (gdouble) nom / (gdouble) den; + if (val < 0.5) + return g_strdup_printf ("1/%.0f s", 1/val); + else + return g_strdup_printf ("%.1f s", val); + } + } + + if (g_str_equal (key, EXIF_FLASH)) { + glong val = gexiv2_metadata_get_tag_long (exif->priv->metadata, EXIF_FLASH); + if (val > 0 && (val & 1) == 1) + return g_strdup ("Flash fired"); + else + return g_strdup ("--"); + } + + if (g_str_equal (key, EXIF_FOCAL_LENGTH)) { + gdouble val; + val = exif->override_focal_length != -1 ? exif->override_focal_length : gexiv2_metadata_get_focal_length (exif->priv->metadata); + if (val >= 0) + return g_strdup_printf ("%.0f mm", val); + } + + if (g_str_equal (key, EXIF_ISO)) { + gint val = gexiv2_metadata_get_iso_speed (exif->priv->metadata); + if (val > 0) + return g_strdup_printf ("%d", val); + } + + if (g_str_equal (key, EXIF_CANON_CAMERA_TEMP)) { + if (gexiv2_metadata_has_tag (exif->priv->metadata, "Exif.CanonSi.0x000c")) { + glong val = gexiv2_metadata_get_tag_long (exif->priv->metadata, "Exif.CanonSi.0x000c"); + if (val > 0) + return g_strdup_printf ("%ld °C", val - 128); + } + } + + if (g_str_equal (key, EXIF_DATETIME) || + g_str_equal (key, "Exif.Photo.DateTimeOriginal") || + g_str_equal (key, "Exif.Photo.DateTimeDigitized") || + g_str_equal (key, "Exif.Image.DateTime")) { + gchar *val; + + val = gexiv2_metadata_get_tag_string (exif->priv->metadata, "Exif.Photo.DateTimeOriginal"); + if (! val || strlen (val) == 0) + val = gexiv2_metadata_get_tag_string (exif->priv->metadata, "Exif.Photo.DateTimeDigitized"); + if (! val || strlen (val) == 0) + val = gexiv2_metadata_get_tag_string (exif->priv->metadata, "Exif.Image.DateTime"); /* usually a modification date */ + if (val && strlen (val) > 0) { + struct tm *tt; + + tt = parse_exif_date_with_overrides (val, exif); + g_free (val); + if (tt) { + gchar *res = format_exif_time (tt, exif->datetime_format ? exif->datetime_format : "%c"); + g_free (tt); + return res; + } + } + g_free (val); + } + + /* fall back to plain value retrieval */ + return get_exif_data (exif, key); +} + +/* + * Returns TRUE if the image contains the key specified + */ +gboolean +exif_has_key (ExifData *exif, const gchar *key) +{ + g_return_val_if_fail (exif != NULL, FALSE); + g_return_val_if_fail (key != NULL, FALSE); + + key = get_real_key_name (key); + + if (g_str_equal (key, JPEG_COMMENT)) { + gchar *comment = gexiv2_metadata_get_comment (exif->priv->metadata); + gboolean ret = comment && strlen (comment) > 0; + g_free (comment); + return ret; + } + + if (g_str_equal (key, EXIF_CANON_CAMERA_TEMP)) + return gexiv2_metadata_has_tag (exif->priv->metadata, "Exif.CanonSi.0x000c") && + gexiv2_metadata_get_tag_long (exif->priv->metadata, "Exif.CanonSi.0x000c") > 0; + + return gexiv2_metadata_has_tag (exif->priv->metadata, key); +} + + +static void +autorotate_image (MagickWand *magick_wand) +{ + MagickBooleanType b; + PixelWand *pixel_wand; + ExceptionType severity; + gchar *description; + + pixel_wand = NewPixelWand (); + b = PixelSetColor (pixel_wand, "#000000"); + if (b == MagickFalse) { + description = MagickGetException (magick_wand, &severity); + log_error ("autorotate_image: Error creating pixel wand: %s %s %ld %s\n", GetMagickModule(), description); + MagickRelinquishMemory (description); + } + + b = MagickTrue; + switch (MagickGetImageOrientation (magick_wand)) + { + case TopRightOrientation: + b = MagickFlopImage (magick_wand); + break; + case BottomRightOrientation: + b = MagickRotateImage (magick_wand, pixel_wand, 180.0); + break; + case BottomLeftOrientation: + b = MagickFlipImage (magick_wand); + break; + case LeftTopOrientation: + b = MagickTransposeImage (magick_wand); + break; + case RightTopOrientation: + b = MagickRotateImage (magick_wand, pixel_wand, 90.0); + break; + case RightBottomOrientation: + b = MagickTransverseImage (magick_wand); + break; + case LeftBottomOrientation: + b = MagickRotateImage (magick_wand, pixel_wand, 270.0); + break; + default: + break; + } + + if (b == MagickFalse) { + description = MagickGetException (magick_wand, &severity); + log_error ("autorotate_image: Error rotating image: %s %s %ld %s\n", GetMagickModule(), description); + MagickRelinquishMemory (description); + } + + b = MagickSetImageOrientation (magick_wand, TopLeftOrientation); + if (b == MagickFalse) { + description = MagickGetException (magick_wand, &severity); + log_error ("autorotate_image: Error saving orientation: %s %s %ld %s\n", GetMagickModule(), description); + MagickRelinquishMemory (description); + } + + DestroyPixelWand (pixel_wand); +} + +static gchar ** +parse_cmd_args (const gchar *resize_opts, const gchar *prepend, const gchar *append, const gchar *file_in, const gchar *file_out, unsigned long size_x, unsigned long size_y) +{ + gchar *in; + gchar **s; + gchar *f; + + in = g_strdup_printf ("%s %s %s %s %s", + prepend ? prepend : "", + file_in ? file_in : "", + resize_opts, + file_out ? file_out : "", + append ? append : ""); + while (g_strstr_len (in, -1, "${WIDTH}")) { + f = g_strdup_printf ("%lu", size_x); + str_replace (&in, "${WIDTH}", f); + g_free (f); + } + while (g_strstr_len (in, -1, "${HEIGHT}")) { + f = g_strdup_printf ("%lu", size_y); + str_replace (&in, "${HEIGHT}", f); + g_free (f); + } + /* ImageMagick doesn't like empty elements */ + str_trim_inside (&in); + s = g_strsplit (in, " ", -1); + g_free (in); + + return s; +} + +/* + * resize_image: resize image pointed by src and save result to dst + */ +gboolean +resize_image (const gchar *src, const gchar *dst, + unsigned long size_x, unsigned long size_y, + int quality, + gboolean thumbnail, + gboolean autorotate, + gboolean hidpi_strict_dimensions, + ExifData *exif, + gchar *resize_opts) +{ + MagickWand *magick_wand; + ImageInfo *image_info; + ExceptionInfo *exception_info; + ExceptionType severity; + unsigned long w, h; + unsigned long new_w, new_h; + double source_aspect, target_aspect; + gchar *description; + gchar **cmd_args; + gchar *res_id = NULL; + gchar *mpr_res_id; + + g_assert (src != NULL); + g_assert (dst != NULL); + + /* Read an image. */ + magick_wand = NewMagickWand(); + if (MagickReadImage (magick_wand, src) == MagickFalse) { + description = MagickGetException (magick_wand, &severity); + log_error ("Error reading image: %s %s %ld %s\n", GetMagickModule(), description); + MagickRelinquishMemory (description); + DestroyMagickWand (magick_wand); + return FALSE; + } + + if (autorotate) + autorotate_image (magick_wand); + + /* Don't resize if smaller than desired size */ + if (hidpi_strict_dimensions || MagickGetImageWidth (magick_wand) > size_x || MagickGetImageHeight (magick_wand) > size_y || exif->shave_amount > 0) + { + /* Shave borders if requested */ + if (exif->shave_amount > 0) + MagickShaveImage (magick_wand, exif->shave_amount, exif->shave_amount); + + /* Prepare image before resizing */ + if (thumbnail) { + if (exif->thumbnail_crop_style != CROP_STYLE_NORMAL) { + w = MagickGetImageWidth (magick_wand); + h = MagickGetImageHeight (magick_wand); + new_w = w; + new_h = h; + if (exif->thumbnail_crop_style == CROP_STYLE_SQUARED) { + new_w = MAX (w, h) * CROP_SIMPLE_SHAVE_AMOUNT / 100; + new_w = MIN (w - 2*new_w, h - 2*new_w); + new_h = new_w; + } + if (exif->thumbnail_crop_style == CROP_STYLE_FIXED) { + source_aspect = (double) w / (double) h; + target_aspect = (double) size_x / (double) size_y; + if (target_aspect >= source_aspect) { + new_w = w; + new_h = (int) ((double) w / target_aspect); + } else { + new_w = (int) ((double) h * target_aspect); + new_h = h; + } + new_w = (int) ((double) new_w * (double) (100 - CROP_SIMPLE_SHAVE_AMOUNT) / 100); + new_h = (int) ((double) new_h * (double) (100 - CROP_SIMPLE_SHAVE_AMOUNT) / 100); + } + switch (exif->thumbnail_crop_hint) { + case CROP_HINT_UNDEFINED: + case CROP_HINT_CENTER: + MagickCropImage (magick_wand, new_w, new_h, (w - new_w) / 2, (h - new_h) / 2); + break; + case CROP_HINT_LEFT: + MagickCropImage (magick_wand, new_w, new_h, 0, (h - new_h) / 2); + break; + case CROP_HINT_RIGHT: + MagickCropImage (magick_wand, new_w, new_h, w - new_w, (h - new_h) / 2); + break; + case CROP_HINT_TOP: + MagickCropImage (magick_wand, new_w, new_h, (w - new_w) / 2, 0); + break; + case CROP_HINT_BOTTOM: + MagickCropImage (magick_wand, new_w, new_h, (w - new_w) / 2, h - new_h); + break; + } + } + } + + /* Shave the source image to match exact dimensions after resize */ + if (hidpi_strict_dimensions && (! thumbnail || exif->thumbnail_crop_style == CROP_STYLE_NORMAL)) { + w = MagickGetImageWidth (magick_wand); + h = MagickGetImageHeight (magick_wand); + source_aspect = (double) w / (double) h; + target_aspect = (double) size_x / (double) size_y; + if (source_aspect != target_aspect) { + if (target_aspect >= source_aspect) { + new_w = w; + new_h = lround ((double) w / target_aspect); + } else { + new_w = lround ((double) h * target_aspect); + new_h = h; + } + MagickCropImage (magick_wand, new_w, new_h, (w - new_w) / 2, (h - new_h) / 2); + g_warn_if_fail (MagickGetImageWidth (magick_wand) == new_w); + g_warn_if_fail (MagickGetImageHeight (magick_wand) == new_h); + } + } + + if (resize_opts == NULL) { + /* Perform internal resizing */ + /* Note: MagickResizeImage() does no aspect correction, stretching the image to the required dimensions */ + if (thumbnail) { + MagickThumbnailImage (magick_wand, size_x, size_y); + } else { +#ifdef HAVE_IMAGEMAGICK_7 + MagickResizeImage (magick_wand, size_x, size_y, LanczosFilter); +#else + MagickResizeImage (magick_wand, size_x, size_y, LanczosFilter, 1.0); +#endif + } + } else { + /* Perform resizing through ImageMagick commandline parser */ + res_id = g_strdup_printf ("cgg_resize_image_%p", g_thread_self ()); + mpr_res_id = g_strdup_printf ("mpr:%s", res_id); + if (MagickWriteImage (magick_wand, mpr_res_id) == MagickFalse) { + description = MagickGetException (magick_wand, &severity); + log_error ("Error writing mpr image: %s %s %ld %s\n", GetMagickModule(), description); + MagickRelinquishMemory (description); + DestroyMagickWand (magick_wand); + g_free (res_id); + g_free (mpr_res_id); + return FALSE; + } + ClearMagickWand (magick_wand); + + cmd_args = parse_cmd_args (resize_opts, "convert", NULL, mpr_res_id, mpr_res_id, size_x, size_y); + image_info = AcquireImageInfo (); + g_assert (image_info != NULL); + exception_info = AcquireExceptionInfo (); + g_assert (exception_info != NULL); + if (MagickCommandGenesis (image_info, ConvertImageCommand, g_strv_length (cmd_args), cmd_args, NULL, exception_info) == MagickFalse) { + /* MagickCommandGenesis() should've printed verbose error message */ + DestroyImageInfo (image_info); + DestroyExceptionInfo (exception_info); + DestroyMagickWand (magick_wand); + g_free (res_id); + g_free (mpr_res_id); + return FALSE; + } + DestroyImageInfo (image_info); + DestroyExceptionInfo (exception_info); + g_strfreev (cmd_args); + + if (MagickReadImage (magick_wand, mpr_res_id) == MagickFalse) { + description = MagickGetException (magick_wand, &severity); + printf ("Error reading mpr image: %s %s %ld %s\n", GetMagickModule(), description); + MagickRelinquishMemory (description); + DestroyMagickWand (magick_wand); + g_free (res_id); + g_free (mpr_res_id); + return FALSE; + } + g_free (mpr_res_id); + } + } + + if (thumbnail) { + /* FIXME: this strips image ICC profile, should do proper conversion first */ + MagickStripImage (magick_wand); + } + + if ((int) MagickGetImageCompressionQuality (magick_wand) != quality) + MagickSetImageCompressionQuality (magick_wand, quality); + + /* Write the image and destroy it. */ + if (MagickWriteImage (magick_wand, dst) == MagickFalse) { + description = MagickGetException (magick_wand, &severity); + log_error ("Error writing image: %s %s %ld %s\n", GetMagickModule(), description); + DeleteImageRegistry (res_id); + g_free (res_id); + return FALSE; + } + + if (res_id) { + /* This is potentially dangerous operation - modifying ImageMagick's internal image registry */ + DeleteImageRegistry (res_id); + g_free (res_id); + } + magick_wand = DestroyMagickWand (magick_wand); + + return TRUE; +} + + +/* + * get_image_sizes: retrieve image dimensions + */ +void +get_image_sizes (const gchar *img, + unsigned long *width, unsigned long *height, + int *quality, + gboolean autorotate) +{ + MagickWand *magick_wand; + MagickBooleanType b; + ExceptionType severity; + gchar *description; + + *width = 0; + *height = 0; + if (quality) + *quality = -1; + + g_assert (img != NULL); + + /* Read an image. */ + magick_wand = NewMagickWand(); + if (autorotate) + b = MagickReadImage (magick_wand, img); + else + b = MagickPingImage (magick_wand, img); + if (b == MagickFalse) { + description = MagickGetException (magick_wand, &severity); + /* -- make it silent + log_error ("Error reading image info: %s %s %ld %s\n", GetMagickModule(), description); + */ + MagickRelinquishMemory(description); + return; + } + + if (autorotate) + autorotate_image (magick_wand); + + *width = MagickGetImageWidth (magick_wand); + *height = MagickGetImageHeight (magick_wand); + if (quality) + *quality = (int) MagickGetImageCompressionQuality (magick_wand); + + magick_wand = DestroyMagickWand (magick_wand); +} + + +/* + * calculate_sizes: calculate maximal image sizes within specified limits keeping aspect ratio + */ +void +calculate_sizes (const unsigned long max_width, const unsigned long max_height, + unsigned long *width, unsigned long *height) +{ + if (max_width > *width && max_height > *height) + return; + + double max_ratio = (double) max_width / (double) max_height; + double real_ratio = (double) *width / (double) *height; + + if (*width > *height && max_ratio <= real_ratio) { + *height = (unsigned long) (max_width / real_ratio); + *width = max_width; + } else { + *width = (unsigned long) (max_height * real_ratio); + *height = max_height; + } +} + +static gboolean +shift_exif_time (GExiv2Metadata *metadata, const char *key, int amount) +{ + struct tm *tt; + gchar *s; + gchar *st; + gboolean res = FALSE; + + s = gexiv2_metadata_get_tag_string (metadata, key); + if (s && strlen (s) > 0) { + tt = parse_exif_date (s); + if (tt) { + shift_time (tt, amount); + st = format_exif_time (tt, NULL); + if (st) + res = gexiv2_metadata_set_tag_string (metadata, key, st); + g_free (st); + g_free (tt); + } + } + g_free (s); + + return res; +} + +static gboolean +override_exif_time (GExiv2Metadata *metadata, const char *key, time_t datetime) +{ + struct tm tt = {0}; + gchar *s; + gchar *st; + gboolean res = FALSE; + + s = gexiv2_metadata_get_tag_string (metadata, key); + if (s && strlen (s) > 0) { + localtime_r (&datetime, &tt); + st = format_exif_time (&tt, NULL); + if (st) + res = gexiv2_metadata_set_tag_string (metadata, key, st); + g_free (st); + } + g_free (s); + + return res; +} + +/* List of tags we don't want to copy from external EXIF data since they are related to the RAW file, + * not the processed image. Note that this list is far from complete. + */ +static const gchar *keep_source_tags[] = { + "Exif.Image.ImageWidth", + "Exif.Image.ImageHeight", + "Exif.Image.ImageLength", + "Exif.Image.Orientation", + "Exif.Image.XResolution", + "Exif.Image.YResolution", + "Exif.Image.ResolutionUnit", + "Exif.Image.Compression", + "Exif.Image.BitsPerSample", + "Exif.Image.SamplesPerPixel", + "Exif.Image.JPEGTables", + "Exif.Image.JPEGProc", + "Exif.Image.JPEGInterchangeFormat", + "Exif.Image.JPEGInterchangeFormatLength", + "Exif.Image.JPEGRestartInterval", + "Exif.Image.JPEGLosslessPredictors", + "Exif.Image.JPEGPointTransforms", + "Exif.Image.JPEGQTables", + "Exif.Image.JPEGDCTables", + "Exif.Image.JPEGACTables", + "Exif.Image.YCbCrCoefficients", + "Exif.Image.YCbCrSubSampling", + "Exif.Image.YCbCrPositioning", + "Exif.Image.ReferenceBlackWhite", + "Exif.Image.PhotometricInterpretation", + "Exif.Image.PlanarConfiguration", + "Exif.Photo.ColorSpace", + "Exif.Photo.ComponentsConfiguration", + "Exif.Photo.CompressedBitsPerPixel", + "Exif.Photo.FocalPlaneXResolution", + "Exif.Photo.FocalPlaneYResolution", + "Exif.Photo.PixelXDimension", + "Exif.Photo.PixelYDimension", + "Exif.Canon.ColorSpace", +}; + +static gboolean +copy_tag (GExiv2Metadata *metadata, GExiv2Metadata *source_metadata, GExiv2Metadata *external_metadata, const gchar *tag) +{ + gchar *val; + unsigned int i; + gboolean res = FALSE; + + /* filter-out tags that should be copied neither from the source or the external metadata */ + if (g_str_has_prefix (tag, "Exif.Thumbnail.") || + g_str_equal (tag, "Exif.Canon.0x4002") || + g_str_equal (tag, "Exif.Canon.0x4005")) + return TRUE; + + /* should be copied from the source file */ + for (i = 0; i < G_N_ELEMENTS (keep_source_tags); i++) + if (g_str_equal (tag, keep_source_tags[i])) { + val = gexiv2_metadata_get_tag_string (source_metadata, tag); + if (val) + res = gexiv2_metadata_set_tag_string (metadata, tag, val); + g_free (val); + return res; + } + + val = gexiv2_metadata_get_tag_string (external_metadata, tag); + if (val) + res = gexiv2_metadata_set_tag_string (metadata, tag, val); + g_free (val); + return res; +} + +static void +copy_metadata (GExiv2Metadata *metadata, const gchar *source_img, const gchar *external_img) +{ + GExiv2Metadata *source_metadata; + GExiv2Metadata *external_metadata; + GError *error = NULL; + gchar **tags; + + source_metadata = gexiv2_metadata_new (); + if (! gexiv2_metadata_open_path (source_metadata, source_img, &error)) { + log_error ("copy_metadata: %s (ignoring...)\n", error->message); + g_error_free (error); + /* continue with empty source metadata */ + } + + external_metadata = gexiv2_metadata_new (); + if (! gexiv2_metadata_open_path (external_metadata, external_img, &error)) { + log_error ("copy_metadata: %s\n", error->message); + g_error_free (error); + g_object_unref (source_metadata); + g_object_unref (external_metadata); + return; + } + + tags = gexiv2_metadata_get_exif_tags (external_metadata); + for (gchar **i = tags; *i; i++) + copy_tag (metadata, source_metadata, external_metadata, *i); + g_strfreev (tags); + + tags = gexiv2_metadata_get_iptc_tags (external_metadata); + for (gchar **i = tags; *i; i++) + copy_tag (metadata, source_metadata, external_metadata, *i); + g_strfreev (tags); + + g_object_unref (source_metadata); + g_object_unref (external_metadata); +} + + + +/* + * modify_exif: - strip thumbnail stored in EXIF table + * - write down overriden keys + */ +void +modify_exif (const gchar *filename, ExifData *exif, gboolean strip_thumbnail, gboolean strip_xmp) +{ + GExiv2Metadata *metadata; + GError *error = NULL; + gboolean modified; + gboolean res; + + g_assert (filename != NULL); + if (! strip_thumbnail && ! strip_xmp && ! exif) + return; + + modified = FALSE; + res = FALSE; + + metadata = gexiv2_metadata_new (); + if (! gexiv2_metadata_open_path (metadata, filename, &error)) { + log_error ("modify_exif: %s\n", error->message); + g_error_free (error); + /* gexiv2 cannot operate on empty, newly constructed object, bail out */ + g_object_unref (metadata); + return; + } + + /* write down metadata from external file if supplied */ + if (exif->external_exif_data) { + gexiv2_metadata_clear (metadata); + copy_metadata (metadata, filename, exif->external_exif_data); + modified = TRUE; + } + + if (exif->override_copyright) { + if (gexiv2_metadata_set_tag_string (metadata, "Exif.Image.Copyright", exif->override_copyright)) + modified = TRUE; + if (gexiv2_metadata_set_tag_string (metadata, "Iptc.Application2.Copyright", exif->override_copyright)) + modified = TRUE; + } + + if (exif->timezone_shift != 0 && exif->override_datetime == (time_t) -1) { + if (gexiv2_metadata_has_exif (metadata)) { + res = shift_exif_time (metadata, "Exif.Photo.DateTimeOriginal", exif->timezone_shift) || res; + res = shift_exif_time (metadata, "Exif.Photo.DateTimeDigitized", exif->timezone_shift) || res; + res = shift_exif_time (metadata, "Exif.Image.DateTime", exif->timezone_shift) || res; + res = gexiv2_metadata_clear_tag (metadata, "Exif.Image.TimeZoneOffset") || res; + res = gexiv2_metadata_clear_tag (metadata, "Exif.Image.PreviewDateTime") || res; + } + if (gexiv2_metadata_has_iptc (metadata)) { + res = gexiv2_metadata_clear_tag (metadata, "Iptc.Application2.DateCreated") || res; + res = gexiv2_metadata_clear_tag (metadata, "Iptc.Application2.TimeCreated") || res; + res = gexiv2_metadata_clear_tag (metadata, "Iptc.Application2.DigitizationDate") || res; + res = gexiv2_metadata_clear_tag (metadata, "Iptc.Application2.DigitizationTime") || res; + } + modified = res || modified; + } + + if (exif->override_datetime != (time_t) -1) { + if (gexiv2_metadata_has_exif (metadata)) { + res = override_exif_time (metadata, "Exif.Photo.DateTimeOriginal", exif->override_datetime); + res = override_exif_time (metadata, "Exif.Photo.DateTimeDigitized", exif->override_datetime) || res; + res = override_exif_time (metadata, "Exif.Image.DateTime", exif->override_datetime) || res; + } + if (gexiv2_metadata_has_iptc (metadata)) { + res = gexiv2_metadata_clear_tag (metadata, "Iptc.Application2.DateCreated") || res; + res = gexiv2_metadata_clear_tag (metadata, "Iptc.Application2.TimeCreated") || res; + res = gexiv2_metadata_clear_tag (metadata, "Iptc.Application2.DigitizationDate") || res; + res = gexiv2_metadata_clear_tag (metadata, "Iptc.Application2.DigitizationTime") || res; + } + modified = res || modified; + } + + if (exif->override_aperture != -1) { + if (gexiv2_metadata_set_exif_tag_rational (metadata, "Exif.Photo.FNumber", exif->override_aperture * 1000000.0, 1000000)) + modified = TRUE; + if (gexiv2_metadata_set_exif_tag_rational (metadata, "Exif.Photo.ApertureValue", exif->override_aperture * 1000000.0, 1000000)) + modified = TRUE; + if (gexiv2_metadata_set_exif_tag_rational (metadata, "Exif.CanonSi.ApertureValue", exif->override_aperture * 1000000.0, 1000000)) + modified = TRUE; + } + + if (exif->override_focal_length != -1) { + if (gexiv2_metadata_set_exif_tag_rational (metadata, "Exif.Photo.FocalLength", exif->override_focal_length * 1000000.0, 1000000)) + modified = TRUE; + } + + if (exif->override_artist_name) { + if (gexiv2_metadata_set_tag_string (metadata, "Exif.Image.Artist", exif->override_artist_name)) + modified = TRUE; + if (gexiv2_metadata_set_tag_string (metadata, "Exif.Photo.CameraOwnerName", exif->override_artist_name)) + modified = TRUE; + if (gexiv2_metadata_set_tag_string (metadata, "Exif.Canon.OwnerName", exif->override_artist_name)) + modified = TRUE; + if (gexiv2_metadata_set_tag_string (metadata, "Iptc.Application2.Byline", exif->override_artist_name)) + modified = TRUE; + } + + if (strip_thumbnail) { + gexiv2_metadata_erase_exif_thumbnail (metadata); + modified = TRUE; + } + + if (strip_xmp && gexiv2_metadata_has_xmp (metadata)) { + gexiv2_metadata_clear_xmp (metadata); + modified = TRUE; + } + + if (modified) { + if (! gexiv2_metadata_save_file (metadata, filename, &error)) { + log_error ("modify_exif: couldn't write metadata to '%s': %s\n", filename, error->message); + g_error_free (error); + } + } + + g_object_unref (metadata); +} -- cgit v1.2.3