summaryrefslogtreecommitdiff
path: root/src/jpeg-utils.c
diff options
context:
space:
mode:
authorTomas Bzatek <tbzatek@users.sourceforge.net>2019-05-13 22:16:28 +0200
committerTomas Bzatek <tbzatek@users.sourceforge.net>2019-05-13 22:16:28 +0200
commit0c3b218886342e44275b087c41faf3b6a2b7f664 (patch)
tree35191c66c8f20e6f9b245d81cd79398a435370d9 /src/jpeg-utils.c
parent86438b558aaeb758d8770144c9e2fe70fdc4ee8a (diff)
downloadcataract-0c3b218886342e44275b087c41faf3b6a2b7f664.tar.xz
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.
Diffstat (limited to 'src/jpeg-utils.c')
-rw-r--r--src/jpeg-utils.c992
1 files changed, 992 insertions, 0 deletions
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 <tbzatek@users.sourceforge.net>
+ *
+ * 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 <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <time.h>
+
+#include <gexiv2/gexiv2.h>
+
+#ifdef HAVE_IMAGEMAGICK_7
+# include <MagickWand/MagickWand.h>
+#else
+# include <wand/MagickWand.h>
+#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 ("&#402;/%.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 &deg;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);
+}