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. --- configure.ac | 48 +-- src/Makefile.am | 6 +- src/jpeg-utils.c | 992 ++++++++++++++++++++++++++++++++++++++++++++++ src/jpeg-utils.cpp | 1105 ---------------------------------------------------- src/jpeg-utils.h | 15 +- 5 files changed, 1014 insertions(+), 1152 deletions(-) create mode 100644 src/jpeg-utils.c delete mode 100644 src/jpeg-utils.cpp diff --git a/configure.ac b/configure.ac index adeac72..e99d6be 100644 --- a/configure.ac +++ b/configure.ac @@ -12,14 +12,10 @@ AM_INIT_AUTOMAKE([1.11.1 no-dist-gzip dist-xz tar-ustar]) AM_CONFIG_HEADER(config.h) GLIB_REQUIRED=2.16.0 -EXIV2_REQUIRED=0.17 AC_C_CONST AC_SEARCH_LIBS([strerror],[cposix]) AC_PROG_CC -AC_PROG_CPP -AC_PROG_CXX -AC_PROG_CXXCPP AC_PROG_INSTALL AC_PROG_LN_S AC_PROG_MAKE_SET @@ -66,39 +62,6 @@ dnl ************************************************** PKG_CHECK_MODULES(LIBXML2, libxml-2.0) -dnl ************************************************** -dnl *** Check for EXIV2 version *** -dnl ************************************************** -PKG_CHECK_MODULES(EXIV2, exiv2 >= $EXIV2_REQUIRED) -AC_MSG_CHECKING(for EXIV2 - version >= $EXIV2_REQUIRED) -exiv2_version=`$PKG_CONFIG --modversion exiv2` -AC_MSG_RESULT(yes (version $exiv2_version)) - -dnl Check for new exiv2 thumbnailing API -AC_DEFUN([EXIV2_HAVE_NEW_THUMBNAILING_API], -[AC_CACHE_CHECK(for new Exiv2::ExifThumb API, -ac_cv_exiv2_have_new_exifthumb, -[AC_LANG_PUSH([C++]) - AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[#include -#include - -void test () { - Exiv2::Image::AutoPtr image = Exiv2::ImageFactory::open(""); - Exiv2::ExifData &exifData = image->exifData(); - Exiv2::ExifThumb exifThumb(image->exifData()); - exifThumb.erase(); -} -]], [[return 0;]])],[ac_cv_exiv2_have_new_exifthumb=yes],[ac_cv_exiv2_have_new_exifthumb=no]) - AC_LANG_POP([]) -]) -if test "$ac_cv_exiv2_have_new_exifthumb" = yes; then - AC_DEFINE(HAVE_EXIFTHUMB,1,[new Exiv2::ExifThumb API]) -fi -]) - -EXIV2_HAVE_NEW_THUMBNAILING_API - - dnl ************************************************** dnl *** Check for ImageMagick *** dnl ************************************************** @@ -114,6 +77,17 @@ PKG_CHECK_EXISTS([MagickWand >= 7], [Define to 1 if ImageMagick 7 is available])) +dnl ************************************************** +dnl *** Check for gexiv2 library *** +dnl ************************************************** +PKG_CHECK_MODULES(GEXIV2, gexiv2) + +dnl Check for gexiv2 version +AC_MSG_CHECKING(for gexiv2) +gexiv2_version=`$PKG_CONFIG --modversion gexiv2` +AC_MSG_RESULT(yes (version $gexiv2_version)) + + AC_CONFIG_FILES([ diff --git a/src/Makefile.am b/src/Makefile.am index ec7b3b1..fbf8672 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -9,7 +9,7 @@ AM_CPPFLAGS = \ $(DISABLE_DEPRECATED_CFLAGS) \ $(LIBXML2_CFLAGS) \ $(MAGICKWAND_CFLAGS) \ - $(EXIV2_CFLAGS) \ + $(GEXIV2_CFLAGS) \ -DG_LOG_DOMAIN=\"cgg\" bin_PROGRAMS = \ @@ -34,7 +34,7 @@ cgg_SOURCES = \ items.h \ job-manager.h \ job-manager.c \ - jpeg-utils.cpp \ + jpeg-utils.c \ jpeg-utils.h \ properties-table.c \ properties-table.h \ @@ -52,7 +52,7 @@ cgg_LDADD = \ $(GLIB_LIBS) \ $(LIBXML2_LIBS) \ $(MAGICKWAND_LIBS) \ - $(EXIV2_LIBS) \ + $(GEXIV2_LIBS) \ -lm EXTRA_DIST = cgg-dirgen 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); +} diff --git a/src/jpeg-utils.cpp b/src/jpeg-utils.cpp deleted file mode 100644 index 2e214e4..0000000 --- a/src/jpeg-utils.cpp +++ /dev/null @@ -1,1105 +0,0 @@ -/* 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. - */ -#include "config.h" - -#include -#include -#include - -#include -#include - -#ifdef HAVE_IMAGEMAGICK_7 -# include -#else -# include -#endif - -#include - -#include "jpeg-utils.h" -#include "gallery-utils.h" - - -struct ExifDataPrivate { - Exiv2::Image::AutoPtr image; -}; - - -/* - * Thread-safe ImageMagick and exiv2 libraries initialization and cleanup - */ -void -init_jpeg_utils (void) -{ - MagickWandGenesis(); - /* http://dev.exiv2.org/projects/exiv2/wiki/Thread_safety */ - /* https://bugs.kde.org/show_bug.cgi?id=166424 */ - Exiv2::XmpParser::initialize(); -} - -void -destroy_jpeg_utils (void) -{ - Exiv2::XmpParser::terminate(); - 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 gchar * -format_exif_time (struct tm *tm) -{ - char conv[1024]; - - memset (&conv, 0, sizeof(conv)); - if (strftime (&conv[0], sizeof(conv), "%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; - - g_assert (filename != NULL); - data = exif_data_new_empty (); - - try { - data->priv->image = Exiv2::ImageFactory::open (filename); - g_assert (data->priv->image.get() != 0); - data->priv->image->readMetadata(); - } - catch (Exiv2::AnyError& e) - { - log_error ("read_exif: Caught Exiv2 exception: '%s'\n", e.what()); - 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); - /* FIXME: free data->priv->image */ - 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; -} - - -/* Returns newly allocated string */ -/* FIXME: for some reason exiv2 returns const strings with unknown lifetime, can't really trust the library */ -#define exiv2_str_val(data,key) g_strdup(data[key].toString().c_str()) - -static gboolean -iptc_has_key (Exiv2::IptcData iptcData, const char *key) -{ - Exiv2::IptcData::const_iterator md; - - if (! iptcData.empty()) { - md = iptcData.findKey(Exiv2::IptcKey(std::string(key))); - if (md != iptcData.end() && iptcData[key].count() > 0) - return TRUE; - } - return FALSE; -} - - -/* - * Retrieves value of the specified key or NULL if the key does not exist. - * The key argument belongs to Exiv2 namespace - see http://exiv2.org/tags.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); - - try { - if (g_strcmp0 (key, JPEG_COMMENT) == 0) { - return g_strdup (exif->priv->image->comment().c_str()); - } - - if (g_str_has_prefix (key, "Exif.")) { - Exiv2::ExifData &exifData = exif->priv->image->exifData(); - if (! exifData.empty()) { - return exiv2_str_val (exifData, key); - } - } - - if (g_str_has_prefix (key, "Iptc.")) { - Exiv2::IptcData &iptcData = exif->priv->image->iptcData(); - if (iptc_has_key (iptcData, key)) { - return exiv2_str_val (iptcData, key); - } - } - - return NULL; - } - catch (...) { - return NULL; - } -} - -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); - - try - { - if (g_str_has_prefix (key, "Exif.")) { - Exiv2::ExifData &exifData = exif->priv->image->exifData(); - if (exifData.empty()) - return NULL; - - if (g_str_equal (key, EXIF_APERTURE)) { - double val = -1; - if (exif->override_aperture != -1) - val = exif->override_aperture; - else { - if (exifData["Exif.Photo.FNumber"].count() > 0) - val = exifData["Exif.Photo.FNumber"].toFloat(); - else - if (exifData["Exif.Photo.ApertureValue"].count() > 0) - val = exifData["Exif.Photo.ApertureValue"].toFloat(); - } - if (val >= 0) - return g_strdup_printf ("ƒ/%.1f", val); - } - - 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 = NULL; - try { - val = exiv2_str_val (exifData, "Exif.Photo.DateTimeOriginal"); - } catch (...) { } - if (! val || strlen (val) == 0) - try { - val = exiv2_str_val (exifData, "Exif.Photo.DateTimeDigitized"); - } catch (...) { } - if (! val || strlen (val) == 0) - try { - /* usually a modification date */ - val = exiv2_str_val (exifData, "Exif.Image.DateTime"); - } catch (...) { } - - if (val && strlen (val) > 0) { - struct tm *tt = NULL; - char conv[1024]; - gchar *res = NULL; - - memset (&conv, 0, sizeof (conv)); - - if (exif->override_datetime != (time_t) -1) { - tt = (struct tm *) g_malloc0 (sizeof (struct tm)); - localtime_r (&exif->override_datetime, tt); - if (strftime (&conv[0], sizeof (conv), exif->datetime_format ? exif->datetime_format : "%c", tt)) - res = g_strdup (&conv[0]); - } - - if (! res) { - tt = parse_exif_date (val); - if (tt) { - shift_time (tt, exif->timezone_shift); - if (strftime (&conv[0], sizeof (conv), exif->datetime_format ? exif->datetime_format : "%c", tt)) - res = g_strdup (&conv[0]); - } - } - - g_free (tt); - if (res) - return res; - } - g_free (val); - } - - if (g_str_equal (key, EXIF_EXPOSURE)) { - float val = exifData["Exif.Photo.ExposureTime"].toFloat(); - if (val > 0) { - 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)) { - long int val = exifData["Exif.Photo.Flash"].toLong(); - if (val > 0 && (val & 1) == 1) - return g_strdup ("Flash fired"); - else - return g_strdup ("--"); - } - - if (g_str_equal (key, EXIF_FOCAL_LENGTH)) { - double val; - val = exif->override_focal_length != -1 ? exif->override_focal_length : exifData["Exif.Photo.FocalLength"].toFloat(); - if (val >= 0) - return g_strdup_printf ("%.0f mm", val); - } - - if (g_str_equal (key, EXIF_ISO)) { - long int val = exifData["Exif.Photo.ISOSpeedRatings"].toLong(); - if (val > 0) - return g_strdup_printf ("%ld", val); - } - - if (g_str_equal (key, EXIF_CANON_CAMERA_TEMP)) { - if (exifData["Exif.CanonSi.0x000c"].count() > 0) { - int long val = exifData["Exif.CanonSi.0x000c"].toLong(); - if (val > 0) - return g_strdup_printf ("%ld °C", val - 128); - } - } - } - - if (g_str_has_prefix (key, "Iptc.")) { - Exiv2::IptcData &iptcData = exif->priv->image->iptcData(); - if (iptcData.empty()) - return NULL; - } - } - catch (...) { - return NULL; - } - - return get_exif_data (exif, key); -} - -/* - * Returns TRUE if the image contains the key specified - */ -gboolean -exif_has_key (ExifData *exif, const gchar *key) -{ - const gchar *rkey; - - g_return_val_if_fail (exif != NULL, FALSE); - g_return_val_if_fail (key != NULL, FALSE); - - rkey = get_real_key_name (key); - - try { - if (g_strcmp0 (rkey, JPEG_COMMENT) == 0) { - return (! exif->priv->image->comment().empty()); - } - - if (g_str_has_prefix (rkey, "Exif.")) { - Exiv2::ExifData &exifData = exif->priv->image->exifData(); - if (exifData.empty() || exifData[rkey].count() <= 0) - return FALSE; - - /* Special case for some keys */ - if (g_str_equal (key, EXIF_CANON_CAMERA_TEMP)) { - int long val = exifData["Exif.CanonSi.0x000c"].toLong(); - return val > 0; - } - - return TRUE; - } - - if (g_str_has_prefix (rkey, "Iptc.")) { - Exiv2::IptcData &iptcData = exif->priv->image->iptcData(); - return iptc_has_key (iptcData, rkey); - } - - return FALSE; - } - catch (...) { - return FALSE; - } -} - - -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 (Exiv2::ExifData& exifData, const char *key, int amount) -{ - struct tm *tt; - gchar *s; - gchar *st; - gboolean res; - - res = FALSE; - try { - if (exifData[key].count() > 0) { - s = exiv2_str_val (exifData, key); - - if (s && strlen (s) > 0) { - tt = parse_exif_date (s); - if (tt) { - shift_time (tt, amount); - st = format_exif_time (tt); - if (st) - exifData[key] = st; - g_free (st); - g_free (tt); - res = TRUE; - } - } - g_free (s); - } - } catch (...) { } - - return res; -} - -static gboolean -shift_iptc_time (Exiv2::IptcData &iptcData, const char *date_key, const char *time_key, int amount) -{ - struct tm tt = {0}; - long int orig_time; - gboolean res; - Exiv2::DateValue dval; - Exiv2::TimeValue tval; - - res = FALSE; - if (iptc_has_key (iptcData, date_key) || iptc_has_key (iptcData, time_key)) { - orig_time = (iptc_has_key (iptcData, date_key) ? iptcData[date_key].toLong() : 0) + - (iptc_has_key (iptcData, time_key) ? iptcData[time_key].toLong() : 0); - if (orig_time > 0) { - orig_time += amount * 60; - localtime_r (&orig_time, &tt); - mktime (&tt); - if (tt.tm_isdst) - shift_time (&tt, -60); - - dval = Exiv2::DateValue(tt.tm_year + 1900, tt.tm_mon + 1, tt.tm_mday); - tval = Exiv2::TimeValue(tt.tm_hour, tt.tm_min, tt.tm_sec, 0, 0); - iptcData[date_key].setValue(&dval); - iptcData[time_key].setValue(&tval); - res = TRUE; - } - } - - return res; -} - -static gboolean -override_exif_time (Exiv2::ExifData &exifData, const char *key, time_t datetime) -{ - struct tm tt = {0}; - gchar *st; - gboolean res; - - res = FALSE; - try { - if (exifData[key].count() > 0) { - localtime_r (&datetime, &tt); - st = format_exif_time (&tt); - if (st) - exifData[key] = st; - g_free (st); - res = TRUE; - } - } catch (...) { } - - return res; -} - -static gboolean -override_iptc_time (Exiv2::IptcData &iptcData, const char *date_key, const char *time_key, time_t datetime) -{ - struct tm tt = {0}; - Exiv2::DateValue dval; - Exiv2::TimeValue tval; - - if (iptc_has_key (iptcData, date_key) || iptc_has_key (iptcData, time_key)) { - localtime_r (&datetime, &tt); - - dval = Exiv2::DateValue(tt.tm_year + 1900, tt.tm_mon + 1, tt.tm_mday); - tval = Exiv2::TimeValue(tt.tm_hour, tt.tm_min, tt.tm_sec, 0, 0); - iptcData[date_key].setValue(&dval); - iptcData[time_key].setValue(&tval); - - return TRUE; - } - - return FALSE; -} - - -/* 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 not by far complete. - */ -static const gchar * image_size_tags[] = { - "Exif.Image.ImageWidth", - "Exif.Image.ImageHeight", - "Exif.Image.ImageLength", - "Exif.Image.Orientation", - "Exif.Photo.PixelXDimension", - "Exif.Photo.PixelYDimension", - "Exif.Image.XResolution", - "Exif.Image.YResolution", - "Exif.Image.ResolutionUnit", - "Exif.Image.Compression", - "Exif.Image.BitsPerSample", - "Exif.Image.SamplesPerPixel", - "Exif.Photo.ComponentsConfiguration", - "Exif.Photo.CompressedBitsPerPixel", - "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", -}; - -static gboolean -is_image_size_tag (const gchar *s) -{ - unsigned int i; - - for (i = 0; i < G_N_ELEMENTS (image_size_tags); i++) - if (g_str_equal (s, image_size_tags[i])) - return TRUE; - return FALSE; -} - -static void -copy_metadata (Exiv2::Image::AutoPtr &img, Exiv2::Image::AutoPtr &external_img) -{ - Exiv2::ExifData exifData; - Exiv2::ExifData &img_exifData = img->exifData(); - Exiv2::ExifData &ext_exifData = external_img->exifData(); - - /* First copy metadata from the external image excluding size tags */ - Exiv2::ExifData::const_iterator end = ext_exifData.end(); - for (Exiv2::ExifData::const_iterator i = ext_exifData.begin(); i != end; ++i) { - gchar *s = g_strdup (i->key().c_str()); - if (! is_image_size_tag (s)) - exifData[s] = ext_exifData[s]; - g_free (s); - } - - /* Copy selected size tags from the processed image */ - end = img_exifData.end(); - for (Exiv2::ExifData::const_iterator i = img_exifData.begin(); i != end; ++i) { - gchar *s = g_strdup (i->key().c_str()); - if (is_image_size_tag (s)) - exifData[s] = img_exifData[s]; - g_free (s); - } - - img->setMetadata (*external_img); - img->setExifData (exifData); -} - - - -/* - * 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) -{ - gboolean modified; - gboolean res; - - g_assert (filename != NULL); - - if (! strip_thumbnail && exif == NULL) - return; - - modified = FALSE; - try { - Exiv2::Image::AutoPtr image = Exiv2::ImageFactory::open (filename); - g_assert (image.get() != 0); - - image->readMetadata(); - - /* Write down metadata from external file if supplied */ - if (exif && exif->external_exif_data) { - Exiv2::Image::AutoPtr ext_image = Exiv2::ImageFactory::open (exif->external_exif_data); - if (ext_image.get() != 0) { - ext_image->clearMetadata(); - ext_image->readMetadata(); - copy_metadata (image, ext_image); - modified = TRUE; - } - } - - Exiv2::ExifData &exifData = image->exifData(); - Exiv2::IptcData &iptcData = image->iptcData(); - - if (exif) { - if (exif->override_copyright) { - if (! exifData.empty()) { - exifData["Exif.Image.Copyright"] = exif->override_copyright; - modified = TRUE; - } - if (! iptcData.empty()) { - iptcData["Iptc.Application2.Copyright"] = exif->override_copyright; - modified = TRUE; - } - } - - if (exif->timezone_shift != 0 && exif->override_datetime == (time_t) -1) { - if (! exifData.empty()) { - res = shift_exif_time (exifData, "Exif.Photo.DateTimeOriginal", exif->timezone_shift); - res = shift_exif_time (exifData, "Exif.Photo.DateTimeDigitized", exif->timezone_shift) || res; - res = shift_exif_time (exifData, "Exif.Image.DateTime", exif->timezone_shift) || res; - modified = TRUE; - } - if (! iptcData.empty()) { - res = shift_iptc_time (iptcData, "Iptc.Application2.DateCreated", "Iptc.Application2.TimeCreated", exif->timezone_shift); - res = shift_iptc_time (iptcData, "Iptc.Application2.DigitizationDate", "Iptc.Application2.DigitizationTime", exif->timezone_shift) || res; - if (res) - modified = TRUE; - } - } - - if (exif->override_datetime != (time_t) -1 && !exifData.empty()) { - if (! exifData.empty()) { - res = override_exif_time (exifData, "Exif.Photo.DateTimeOriginal", exif->override_datetime); - res = override_exif_time (exifData, "Exif.Photo.DateTimeDigitized", exif->override_datetime) || res; - res = override_exif_time (exifData, "Exif.Image.DateTime", exif->override_datetime) || res; - modified = TRUE; - } - if (! iptcData.empty()) { - res = override_iptc_time (iptcData, "Iptc.Application2.DateCreated", "Iptc.Application2.TimeCreated", exif->override_datetime); - res = override_iptc_time (iptcData, "Iptc.Application2.DigitizationDate", "Iptc.Application2.DigitizationTime", exif->override_datetime) || res; - if (res) - modified = TRUE; - } - } - - if (exif->override_aperture != -1) { - if (! exifData.empty()) { - exifData["Exif.Photo.FNumber"] = Exiv2::floatToRationalCast (exif->override_aperture); - if (exifData["Exif.Photo.ApertureValue"].count() > 0) - exifData["Exif.Photo.ApertureValue"] = Exiv2::floatToRationalCast (exif->override_aperture); - modified = TRUE; - } - } - - if (exif->override_focal_length != -1) { - if (! exifData.empty()) { - exifData["Exif.Photo.FocalLength"] = Exiv2::floatToRationalCast (exif->override_focal_length); - modified = TRUE; - } - } - - if (exif->override_artist_name) { - if (! exifData.empty()) { - exifData["Exif.Image.Artist"] = exif->override_artist_name; - if (exifData["Exif.Photo.CameraOwnerName"].count() >= 1) - exifData["Exif.Photo.CameraOwnerName"] = exif->override_artist_name; - if (exifData["Exif.Canon.OwnerName"].count() >= 1) - exifData["Exif.Canon.OwnerName"] = exif->override_artist_name; - modified = TRUE; - } - } - } - - - if (strip_thumbnail && ! exifData.empty()) { -#ifdef HAVE_EXIFTHUMB - Exiv2::ExifThumb exifThumb(image->exifData()); - std::string thumbExt = exifThumb.extension(); -#else - std::string thumbExt = exifData.thumbnailExtension(); -#endif - - if (! thumbExt.empty()) { -#ifdef HAVE_EXIFTHUMB - exifThumb.erase(); -#else - exifData.eraseThumbnail(); -#endif - modified = TRUE; - } - } - - if (strip_xmp) { - if (! image->xmpData().empty()) { - image->clearXmpData (); - modified = TRUE; - } - if (! image->xmpPacket().empty()) { - image->clearXmpPacket (); - modified = TRUE; - } - } - - if (modified) - image->writeMetadata(); - } - catch (Exiv2::AnyError& e) - { - log_error ("modify_exif: Caught Exiv2 exception: '%s'\n", e.what()); - } -} diff --git a/src/jpeg-utils.h b/src/jpeg-utils.h index 767320a..b73576b 100644 --- a/src/jpeg-utils.h +++ b/src/jpeg-utils.h @@ -28,6 +28,7 @@ G_BEGIN_DECLS #define CROP_SIMPLE_SHAVE_AMOUNT 0 /* percent */ /* EXIF data known keys */ +/* Exiv2 Tag Reference can be found at http://exiv2.org/metadata.html */ #define EXIF_APERTURE "Exif.Photo.FNumber" #define EXIF_CAMERA_MODEL "Exif.Image.Model" #define EXIF_DATETIME "Exif.Photo.DateTimeOriginal" @@ -99,12 +100,12 @@ gboolean exif_has_key (ExifData *exif, const gchar *key); */ 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); + int quality, + gboolean thumbnail, + gboolean autorotate, + gboolean hidpi_strict_dimensions, + ExifData *exif, + gchar *resize_opts); /* * get_image_sizes: retrieve image dimensions @@ -118,7 +119,7 @@ void get_image_sizes (const gchar *img, * 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); + unsigned long *width, unsigned long *height); /* * modify_exif: - strip thumbnail stored in EXIF table -- cgit v1.2.3