/* 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 #include #include #include #include #include #include #include "jpeg-utils.h" #include "gallery-utils.h" struct ExifDataPrivate { Exiv2::Image::AutoPtr image; }; 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); /* 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 ("f/%.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), "%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), "%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); } /* * 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, ExifData *exif) { MagickWand *magick_wand; ExceptionType severity; unsigned long w, h; unsigned long new_w, new_h; double source_aspect, target_aspect; gchar *description; 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); return FALSE; } if (autorotate) autorotate_image (magick_wand); /* Don't resize if smaller than desired size */ if (MagickGetImageWidth (magick_wand) > size_x || MagickGetImageHeight (magick_wand) > size_y) { /* Process thumbnail if required */ 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; } } MagickThumbnailImage (magick_wand, size_x, size_y); /* FIXME: this strips image ICC profile, should do proper conversion first */ MagickStripImage (magick_wand); } else MagickResizeImage (magick_wand, size_x, size_y, LanczosFilter, 1.0); } 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); MagickRelinquishMemory (description); return FALSE; } 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, gboolean autorotate) { MagickWand *magick_wand; MagickBooleanType b; ExceptionType severity; gchar *description; *width = -1; *height = -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); 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; } /* * 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(); image->setMetadata (*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 (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()); } }