Parcourir la source

Working on copying the exif information to the file.

Added exif copy. Refactored jpeg rotate. Both now rely on piexif.

Added tools to work with exif of the photos
Added tool to keep exif if the original input photo was a jpg.
Added tool to remove gps data.
Added debug option to disable multiprocess
Added tool to add copyright and artist information to exif.
Cleaned up README.txt.

Removed some debug messages and variables.

Fixed minor variable refactoring error.

Added method to deal with alpha channel backgrounds in png images.

Added option to remove watermark from thumbs.
Brian Levin il y a 7 ans
Parent
commit
e2d77d0585
4 fichiers modifiés avec 263 ajouts et 111 suppressions
  1. 61 37
      photos/README.md
  2. 30 0
      photos/licenses.json
  3. 171 74
      photos/photos.py
  4. 1 0
      photos/requirements.txt

+ 61 - 37
photos/README.md

@@ -2,6 +2,63 @@
 
 Use Photos to add a photo or a gallery of photos to an article, or to include photos in the body text. Photos are kept separately, as an organized library of high resolution photos, and resized as needed.
 
+## How to install and configure
+
+The plug-in requires `Pillow`: the Python Imaging Library and optionally `Piexif`, whose installation are outside the scope of this document.
+
+The plug-in resizes the referred photos, and generates thumbnails for galleries and associated photos, based on the following configuration and default values:
+
+`PHOTO_LIBRARY = "~/Pictures"`
+:	Absolute path to the folder where the original photos are kept, organized in sub-folders.
+
+`PHOTO_GALLERY = (1024, 768, 80)`
+:	For photos in galleries, maximum width and height, plus JPEG quality as a percentage. This would typically be the size of the photo displayed when the reader clicks a thumbnail.
+
+`PHOTO_ARTICLE = (760, 506, 80)`
+:	For photos associated with articles, maximum width, height, and quality. The maximum size would typically depend on the needs of the theme. 760px is suitable for the theme `notmyidea`.
+
+`PHOTO_THUMB = (192, 144, 60)`
+:	For thumbnails, maximum width, height, and quality.
+
+`PHOTO_RESIZE_JOBS = 5`
+: Number of parallel resize jobs to be run. Defaults to 1.
+
+`PHOTO_WATERMARK = True`
+: Adds a watermark to all photos in articles and pages. Defaults to using your site name.
+
+`PHOTO_WATERMARK_TEXT' = SITENAME`
+: Allow the user to change the watermark text or remove it completely. By default it uses [SourceCodePro-Bold](http://www.adobe.com/products/type/font-information/source-code-pro-readme.html) as the font.
+
+`PHOTO_WATERMARK_IMG = ''`
+: Allows the user to add an image in addition to or as the only watermark. Set the variable to the location.
+
+**The following features require the piexif library**
+`PHOTO_EXIF_KEEP = True`
+: Keeps the exif of the input photo.
+
+`PHOTO_EXIF_REMOVE_GPS = True`
+: Removes any GPS information from the files exif data.
+
+`PHOTO_EXIF_COPYRIGHT = 'COPYRIGHT'`
+: Attaches an author and a license to the file. Choices include:
+	- `COPYRIGHT`: Copyright
+	- `CC0`: Public Domain
+	- `CC-BY-NC-ND`: Creative Commons Attribution-NonCommercial-NoDerivatives
+	- `CC-BY-NC-SA`: Creative Commons Attribution-NonCommercial-ShareAlike
+	- `CC-BY`: Creative Commons Attribution
+	- `CC-BY-SA`: Creative Commons Attribution-ShareAlike
+	- `CC-BY-NC`: Creative Commons Attribution-NonCommercial
+	- `CC-BY-ND`: Creative Commons Attribution-NoDerivatives
+
+`PHOTO_EXIF_COPYRIGHT_AUTHOR = 'Your Name Here'`
+: Adds an author name to the photo's exif and copyright statement. Defaults to `AUTHOR` value from the `pelicanconf.py`
+
+The plug-in automatically resizes the photos and publishes them to the following output folder:
+
+    ./output/photos
+
+**WARNING:** The plug-in can take hours to resize 40,000 photos, therefore, photos and thumbnails are only generated once. Clean the output folders to regenerate the resized photos again.
+
 ## How to use
 
 Maintain an organized library of high resolution photos somewhere on disk, using folders to group related images. The default path `~/Pictures` is convenient for Mac OS X users.
@@ -11,6 +68,7 @@ Maintain an organized library of high resolution photos somewhere on disk, using
 * To use an image in the body of the text, just use the syntax `{photo}folder/image.jpg` instead of the usual `{filename}/images/image.jpg`.
 * To associate an image with an article, add the metadata field `image: {photo}folder/image.jpg` to an article. Use associated images to improve navigation. For compatibility, the syntax `image: {filename}/images/image.jpg` is also accepted.
 
+### Exif, Captions, and Blacklists
 Folders of photos may optionally have three text files, where each line describes one photo. You can use the `#` to comment out a line. Generating these optional files is left as an exercise for the reader (but consider using Phil Harvey's [exiftool](http://www.sno.phy.queensu.ca/~phil/exiftool/)). See below for one method of extracting exif data.
 
 `exif.txt`
@@ -34,7 +92,7 @@ Folders of photos may optionally have three text files, where each line describe
 	this-one-will-be-skipped-too.jpg
 	# but-this-file-will-NOT-be-skipped.jpg
 	this-one-will-be-also-skipped.jpg
-	
+
 
 Here is an example Markdown article that shows the three use cases:
 
@@ -45,41 +103,7 @@ Here is an example Markdown article that shows the three use cases:
 	Here are my best photos, taken with my favorite camera:
 	![]({photo}mybag/camera.jpg).
 
-## How to install and configure
-
-The plug-in requires Pillow, the Python Imaging Library, whose installation is outside the scope of this document.
-
-The plug-in resizes the referred photos, and generates thumbnails for galleries and associated photos, based on the following configuration and default values:
-
-`PHOTO_LIBRARY = "~/Pictures"`
-:	Absolute path to the folder where the original photos are kept, organized in sub-folders.
-
-`PHOTO_GALLERY = (1024, 768, 80)`
-:	For photos in galleries, maximum width and height, plus JPEG quality as a percentage. This would typically be the size of the photo displayed when the reader clicks a thumbnail.
-
-`PHOTO_ARTICLE = ( 760, 506, 80)`
-:	For photos associated with articles, maximum width, height, and quality. The maximum size would typically depend on the needs of the theme. 760px is suitable for the theme `notmyidea`.
-
-`PHOTO_THUMB = (192, 144, 60)`
-:	For thumbnails, maximum width, height, and quality.
-
-`PHOTO_RESIZE_JOBS = 5`
-: Number of parallel resize jobs to be run. Defaults to 1.
-
-`PHOTO_WATERMARK = True`
-: Adds a watermark to all photos in articles and pages. Defaults to using your site name.
-
-`PHOTO_WATERMARK_TEXT' = SITENAME`
-: Allow the user to change the watermark text or remove it completely. By default it uses [SourceCodePro-Bold](http://www.adobe.com/products/type/font-information/source-code-pro-readme.html) as the font.
-
-`PHOTO_WATERMARK_IMG = ''`
-: Allows the user to add an image in addition to or as the only watermark. Set the variable to the location.
-
-The plug-in automatically resizes the photos and publishes them to the following output folder:
-
-    ./output/photos
-
-**WARNING:** The plug-in can take hours to resize 40,000 photos, therefore, photos and thumbnails are only generated once. Clean the output folders to regenerate the resized photos again.
+The default behavior of the Photos plugin removes the exif information from the file. If you would like to keep the exif information, you can install the `piexif` library for python and add the following settings to keep some or all of the exif information. This feature is not a replacement for the `exif.txt` feature but in addition to that feature. This feature currently only works with jpeg input files.
 
 ## How to change the Jinja templates
 
@@ -221,7 +245,7 @@ If you are using bootstrap, the following code is an example of how one could cr
 
 ## Exiftool example
 
-You can add the following stanza to your fab file if you are using fabric to generate the appropriate text files for your galleries. You need to set the location of `Exiftool` control files.
+You can add the following stanza to your fab file if you are using `fabric` to generate the appropriate text files for your galleries. You need to set the location of `Exiftool` control files.
 
 ```Python
 def photo_gallery_gen(location):

+ 30 - 0
photos/licenses.json

@@ -0,0 +1,30 @@
+{
+	"CC-BY-NC-ND": {
+		"URL": "https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode",
+		"Text": "Copyleft license, {Author} {Year}. Content licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License, except where indicated otherwise. See {URL} for more information."
+	},
+	"CC-BY-NC-SA": {
+		"URL": "https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode",
+		"Text": "Copyleft license, {Author} {Year}. Content licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License, except where indicated otherwise. See {URL} for more information."
+	},
+	"CC-BY": {
+		"URL": "https://creativecommons.org/licenses/by/4.0/legalcode",
+		"Text": "Copyleft license, {Author} {Year}. Content licensed under a Creative Commons Attribution 4.0 International License, except where indicated otherwise. See {URL} for more information."
+	},
+	"CC-BY-SA": {
+		"URL": "https://creativecommons.org/licenses/by-sa/4.0/legalcode",
+		"Text": "Copyleft license, {Author} {Year}. Content licensed under a Creative Commons Attribution-ShareAlike 4.0 International License, except where indicated otherwise. See {URL} for more information."
+	},
+	"CC0": {
+		"URL": "https://creativecommons.org/publicdomain/zero/1.0/",
+		"Text": "CC0 Copyleft license, {Author} {Year}. To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide. This software is distributed without any warranty. See {URL} for more information."
+	},
+	"CC-BY-NC": {
+		"URL": "https://creativecommons.org/licenses/by-nc/4.0/legalcode",
+		"Text": "Copyleft license, {Author} {Year}. Content licensed under a Creative Commons Attribution-NonCommercial 4.0 International License, except where indicated otherwise. See {URL} for more information."
+	},
+	"CC-BY-ND": {
+		"URL": "https://creativecommons.org/licenses/by-nd/4.0/legalcode",
+		"Text": "Copyleft license, {Author} {Year}. Content licensed under a Creative Commons Attribution-NoDerivatives 4.0 International License, except where indicated otherwise. See {URL} for more information."
+	}
+}

+ 171 - 74
photos/photos.py

@@ -1,13 +1,15 @@
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 
+import datetime
 import itertools
+import json
 import logging
+import multiprocessing
 import os
 import pprint
 import re
 import sys
-import multiprocessing
 
 from pelican.generators import ArticlesGenerator
 from pelican.generators import PagesGenerator
@@ -17,15 +19,22 @@ from pelican.utils import pelican_open
 
 logger = logging.getLogger(__name__)
 
-
 try:
-    from PIL import ExifTags
     from PIL import Image
     from PIL import ImageDraw
     from PIL import ImageEnhance
     from PIL import ImageFont
 except ImportError:
-    raise Exception('PIL/Pillow not found')
+    logger.error('PIL/Pillow not found')
+
+try:
+    import piexif
+except ImportError:
+    ispiexif = False
+    logger.warning('piexif not found! Cannot use exif manipulation features')
+else:
+    ispiexif = True
+    logger.debug('piexif found.')
 
 
 def initialized(pelican):
@@ -37,14 +46,23 @@ def initialized(pelican):
     DEFAULT_CONFIG.setdefault('PHOTO_ARTICLE', (760, 506, 80))
     DEFAULT_CONFIG.setdefault('PHOTO_THUMB', (192, 144, 60))
     DEFAULT_CONFIG.setdefault('PHOTO_GALLERY_TITLE', '')
-    DEFAULT_CONFIG.setdefault('PHOTO_WATERMARK', 'False')
+    DEFAULT_CONFIG.setdefault('PHOTO_ALPHA_BACKGROUND_COLOR', (255, 255, 255))
+    DEFAULT_CONFIG.setdefault('PHOTO_WATERMARK', False)
+    DEFAULT_CONFIG.setdefault('PHOTO_WATERMARK_THUMB', False)
     DEFAULT_CONFIG.setdefault('PHOTO_WATERMARK_TEXT', DEFAULT_CONFIG['SITENAME'])
+    DEFAULT_CONFIG.setdefault('PHOTO_WATERMARK_TEXT_COLOR', (255, 255, 255))
     DEFAULT_CONFIG.setdefault('PHOTO_WATERMARK_IMG', '')
-    DEFAULT_CONFIG.setdefault('PHOTO_WATERMARK_IMG_SIZE', (64, 64))
+    DEFAULT_CONFIG.setdefault('PHOTO_WATERMARK_IMG_SIZE', False)
     DEFAULT_CONFIG.setdefault('PHOTO_RESIZE_JOBS', 1)
+    DEFAULT_CONFIG.setdefault('PHOTO_EXIF_KEEP', False)
+    DEFAULT_CONFIG.setdefault('PHOTO_EXIF_REMOVE_GPS', False)
+    DEFAULT_CONFIG.setdefault('PHOTO_EXIF_AUTOROTATE', True)
+    DEFAULT_CONFIG.setdefault('PHOTO_EXIF_COPYRIGHT', False)
+    DEFAULT_CONFIG.setdefault('PHOTO_EXIF_COPYRIGHT_AUTHOR', DEFAULT_CONFIG['SITENAME'])
 
     DEFAULT_CONFIG['queue_resize'] = {}
     DEFAULT_CONFIG['created_galleries'] = {}
+    DEFAULT_CONFIG['plugin_dir'] = os.path.dirname(os.path.realpath(__file__))
 
     if pelican:
         pelican.settings.setdefault('PHOTO_LIBRARY', p)
@@ -52,11 +70,19 @@ def initialized(pelican):
         pelican.settings.setdefault('PHOTO_ARTICLE', (760, 506, 80))
         pelican.settings.setdefault('PHOTO_THUMB', (192, 144, 60))
         pelican.settings.setdefault('PHOTO_GALLERY_TITLE', '')
-        pelican.settings.setdefault('PHOTO_WATERMARK', 'False')
+        pelican.settings.setdefault('PHOTO_ALPHA_BACKGROUND_COLOR', (255, 255, 255))
+        pelican.settings.setdefault('PHOTO_WATERMARK', False)
+        pelican.settings.setdefault('PHOTO_WATERMARK_THUMB', False)
         pelican.settings.setdefault('PHOTO_WATERMARK_TEXT', pelican.settings['SITENAME'])
+        pelican.settings.setdefault('PHOTO_WATERMARK_TEXT_COLOR', (255, 255, 255))
         pelican.settings.setdefault('PHOTO_WATERMARK_IMG', '')
-        pelican.settings.setdefault('PHOTO_WATERMARK_IMG_SIZE', (64, 64))
+        pelican.settings.setdefault('PHOTO_WATERMARK_IMG_SIZE', False)
         pelican.settings.setdefault('PHOTO_RESIZE_JOBS', 1)
+        pelican.settings.setdefault('PHOTO_EXIF_KEEP', False)
+        pelican.settings.setdefault('PHOTO_EXIF_REMOVE_GPS', False)
+        pelican.settings.setdefault('PHOTO_EXIF_AUTOROTATE', True)
+        pelican.settings.setdefault('PHOTO_EXIF_COPYRIGHT', False)
+        pelican.settings.setdefault('PHOTO_EXIF_COPYRIGHT_AUTHOR', pelican.settings['AUTHOR'])
 
 
 def read_notes(filename, msg=None):
@@ -83,7 +109,6 @@ def read_notes(filename, msg=None):
 
 
 def enqueue_resize(orig, resized, spec=(640, 480, 80)):
-
     if resized not in DEFAULT_CONFIG['queue_resize']:
         DEFAULT_CONFIG['queue_resize'][resized] = (orig, spec)
     elif DEFAULT_CONFIG['queue_resize'][resized] != (orig, spec):
@@ -94,13 +119,19 @@ def isalpha(img):
     return True if img.mode in ('RGBA', 'LA') or (img.mode == 'P' and 'transparency' in img.info) else False
 
 
+def remove_alpha(img, bg_color):
+    background = Image.new("RGB", img.size, bg_color)
+    background.paste(img, mask=img.split()[3])  # 3 is the alpha channel
+
+    return background
+
+
 def ReduceOpacity(im, opacity):
-    """Reduces Opacity
+    """Reduces Opacity.
 
     Returns an image with reduced opacity.
     Taken from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/362879
     """
-
     assert opacity >= 0 and opacity <= 1
     if isalpha(im):
         im = im.copy()
@@ -113,7 +144,7 @@ def ReduceOpacity(im, opacity):
     return im
 
 
-def watermark_photo(image, watermark_text, watermark_image, watermark_image_size):
+def watermark_photo(image, settings):
 
     margin = [10, 10]
     opacity = 0.6
@@ -124,22 +155,20 @@ def watermark_photo(image, watermark_text, watermark_image, watermark_image_size
     image_reducer = 8
     text_size = [0, 0]
     mark_size = [0, 0]
-    font_height = 0
     text_position = [0, 0]
 
-    if watermark_text:
+    if settings['PHOTO_WATERMARK_TEXT']:
         font_name = 'SourceCodePro-Bold.otf'
-
-        plugin_dir = os.path.dirname(os.path.realpath(__file__))
-        default_font = os.path.join(plugin_dir, font_name)
+        default_font = os.path.join(DEFAULT_CONFIG['plugin_dir'], font_name)
         font = ImageFont.FreeTypeFont(default_font, watermark_layer.size[0] // text_reducer)
-        text_size = draw_watermark.textsize(watermark_text, font)
+        text_size = draw_watermark.textsize(settings['PHOTO_WATERMARK_TEXT'], font)
         text_position = [image.size[i] - text_size[i] - margin[i] for i in [0, 1]]
-        draw_watermark.text(text_position, watermark_text, (255, 255, 255), font=font)
+        draw_watermark.text(text_position, settings['PHOTO_WATERMARK_TEXT'], settings['PHOTO_WATERMARK_TEXT_COLOR'], font=font)
 
-    if watermark_image:
-        mark_image = Image.open(watermark_image)
-        mark_image_size = [ watermark_layer.size[0] // image_reducer for size in mark_size]
+    if settings['PHOTO_WATERMARK_IMG']:
+        mark_image = Image.open(settings['PHOTO_WATERMARK_IMG'])
+        mark_image_size = [watermark_layer.size[0] // image_reducer for size in mark_size]
+        mark_image_size = settings['PHOTO_WATERMARK_IMG_SIZE'] if settings['PHOTO_WATERMARK_IMG_SIZE'] else mark_image_size
         mark_image.thumbnail(mark_image_size, Image.ANTIALIAS)
         mark_position = [watermark_layer.size[i] - mark_image.size[i] - margin[i] for i in [0, 1]]
         mark_position = tuple([mark_position[0] - (text_size[0] // 2) + (mark_image_size[0] // 2), mark_position[1] - text_size[1]])
@@ -155,56 +184,121 @@ def watermark_photo(image, watermark_text, watermark_image, watermark_image_size
     return image
 
 
-def resize_worker(orig, resized, spec, wm, wm_text, wm_img, wm_img_size):
+def rotate_image(img, exif_dict):
+
+    if "exif" in img.info and piexif.ImageIFD.Orientation in exif_dict["0th"]:
+        orientation = exif_dict["0th"].pop(piexif.ImageIFD.Orientation)
+        if orientation == 2:
+            img = img.transpose(Image.FLIP_LEFT_RIGHT)
+        elif orientation == 3:
+            img = img.rotate(180)
+        elif orientation == 4:
+            img = img.rotate(180).transpose(Image.FLIP_LEFT_RIGHT)
+        elif orientation == 5:
+            img = img.rotate(-90).transpose(Image.FLIP_LEFT_RIGHT)
+        elif orientation == 6:
+            img = img.rotate(-90)
+        elif orientation == 7:
+            img = img.rotate(90).transpose(Image.FLIP_LEFT_RIGHT)
+        elif orientation == 8:
+            img = img.rotate(90)
+
+    return (img, exif_dict)
+
+
+def build_license(license, author):
+
+    year = datetime.datetime.now().year
+    license_file = os.path.join(DEFAULT_CONFIG['plugin_dir'], 'licenses.json')
+
+    with open(license_file) as data_file:
+        licenses = json.load(data_file)
+
+    if any(license in k for k in licenses):
+        return licenses[license]['Text'].format(Author=author, Year=year, URL=licenses[license]['URL'])
+    else:
+        return 'Copyright {Year} {Author}, All Rights Reserved'.format(Author=author, Year=year)
+
+
+def manipulate_exif(img, settings):
+
+    try:
+        exif = piexif.load(img.info['exif'])
+    except Exception:
+        logger.debug('EXIF information not found')
+        exif = {}
+
+    if settings['PHOTO_EXIF_AUTOROTATE']:
+        img, exif = rotate_image(img, exif)
+
+    if settings['PHOTO_EXIF_REMOVE_GPS']:
+        exif.pop('GPS')
+
+    if settings['PHOTO_EXIF_COPYRIGHT']:
+
+        # We want to be minimally destructive to any preset exif author or copyright information.
+        # If there is copyright or author information prefer that over everything else.
+        if not exif['0th'].get(piexif.ImageIFD.Artist):
+            exif['0th'][piexif.ImageIFD.Artist] = settings['PHOTO_EXIF_COPYRIGHT_AUTHOR']
+            author = settings['PHOTO_EXIF_COPYRIGHT_AUTHOR']
+
+        if not exif['0th'].get(piexif.ImageIFD.Copyright):
+            license = build_license(settings['PHOTO_EXIF_COPYRIGHT'], author)
+            exif['0th'][piexif.ImageIFD.Copyright] = license
+
+    return (img, piexif.dump(exif))
+
+
+def resize_worker(orig, resized, spec, settings):
+
     logger.info('photos: make photo {} -> {}'.format(orig, resized))
     im = Image.open(orig)
-    try:
-        exif = im._getexif()
-    except:
-        exif = None
 
-    icc_profile = im.info.get("icc_profile", None)
+    if ispiexif and settings['PHOTO_EXIF_KEEP'] and im.format == 'JPEG':  # Only works with JPEG exif for sure.
+        im, exif_copy = manipulate_exif(im, settings)
+    else:
+        exif_copy = b''
 
-    if exif:
-        for tag, value in exif.items():
-            decoded = ExifTags.TAGS.get(tag, tag)
-            if decoded == 'Orientation':
-                if value == 3:
-                    im = im.rotate(180)
-                elif value == 6:
-                    im = im.rotate(270)
-                elif value == 8:
-                    im = im.rotate(90)
-                break
+    icc_profile = im.info.get("icc_profile", None)
     im.thumbnail((spec[0], spec[1]), Image.ANTIALIAS)
-    try:
-        os.makedirs(os.path.split(resized)[0])
-    except:
-        pass
+    directory = os.path.split(resized)[0]
+
+    if isalpha(im):
+        im = remove_alpha(im, settings['PHOTO_ALPHA_BACKGROUND_COLOR'])
+
+    if not os.path.exists(directory):
+        try:
+            os.makedirs(directory)
+        except Exception:
+            logger.exception('Could not create {}'.format(directory))
+    else:
+        logger.debug('Directory already exists at {}'.format(os.path.split(resized)[0]))
 
-    if wm:
-        im = watermark_photo(im, wm_text, wm_img, wm_img_size)
+    if settings['PHOTO_WATERMARK']:
+        isthumb = True if spec == settings['PHOTO_THUMB'] else False
+        if not isthumb or (isthumb and settings['PHOTO_WATERMARK_THUMB']):
+            im = watermark_photo(im, settings)
 
-    im.save(resized, 'JPEG', quality=spec[2], icc_profile=icc_profile)
+    im.save(resized, 'JPEG', quality=spec[2], icc_profile=icc_profile, exif=exif_copy)
 
 
 def resize_photos(generator, writer):
-    logger.info('photos: {} photo resizes to consider.'.format(len(DEFAULT_CONFIG['queue_resize'].items())))
+    if generator.settings['PHOTO_RESIZE_JOBS'] == -1:
+        debug = True
+        generator.settings['PHOTO_RESIZE_JOBS'] = 1
+    else:
+        debug = False
+
     pool = multiprocessing.Pool(generator.settings['PHOTO_RESIZE_JOBS'])
+    logger.debug('Debug Status: {}'.format(debug))
     for resized, what in DEFAULT_CONFIG['queue_resize'].items():
         resized = os.path.join(generator.output_path, resized)
         orig, spec = what
-        if (not os.path.isfile(resized) or
-                os.path.getmtime(orig) > os.path.getmtime(resized)):
-            pool.apply_async(resize_worker, args=(
-                orig,
-                resized,
-                spec,
-                generator.settings['PHOTO_WATERMARK'],
-                generator.settings['PHOTO_WATERMARK_TEXT'],
-                generator.settings['PHOTO_WATERMARK_IMG'],
-                generator.settings['PHOTO_WATERMARK_IMG_SIZE']
-            ))
+        if (not os.path.isfile(resized) or os.path.getmtime(orig) > os.path.getmtime(resized)):
+            if debug:
+                resize_worker(orig, resized, spec, generator.settings)
+            else:
+                pool.apply_async(resize_worker, (orig, resized, spec, generator.settings))
 
     pool.close()
     pool.join()
@@ -257,7 +351,7 @@ def galleries_string_decompose(gallery_string):
     title_regex = re.compile(r'{(.+)}')
     galleries = map(unicode.strip if sys.version_info.major == 2 else str.strip, filter(None, splitter_regex.split(gallery_string)))
     galleries = [gallery[1:] if gallery.startswith('/') else gallery for gallery in galleries]
-    if len(galleries) % 2 == 0 and u' ' not in galleries:
+    if len(galleries) % 2 == 0 and ' ' not in galleries:
         galleries = zip(zip(['type'] * len(galleries[0::2]), galleries[0::2]), zip(['location'] * len(galleries[0::2]), galleries[1::2]))
         galleries = [dict(gallery) for gallery in galleries]
         for gallery in galleries:
@@ -269,8 +363,7 @@ def galleries_string_decompose(gallery_string):
                 gallery['title'] = DEFAULT_CONFIG['PHOTO_GALLERY_TITLE']
         return galleries
     else:
-        logger.critical('Unexpected gallery location format! \n{}'.format(pprint.pformat(galleries)))
-        raise Exception
+        logger.error('Unexpected gallery location format! \n{}'.format(pprint.pformat(galleries)))
 
 
 def process_gallery(generator, content, location):
@@ -333,8 +426,7 @@ def process_gallery(generator, content, location):
             logger.debug('Gallery Data: '.format(pprint.pformat(content.photo_gallery)))
             DEFAULT_CONFIG['created_galleries']['gallery'] = content_gallery
         else:
-            logger.critical('photos: Gallery does not exist: {} at {}'.format(gallery['location'], dir_gallery))
-            raise Exception
+            logger.error('photos: Gallery does not exist: {} at {}'.format(gallery['location'], dir_gallery))
 
 
 def detect_gallery(generator, content):
@@ -346,10 +438,15 @@ def detect_gallery(generator, content):
             logger.error('photos: Gallery tag not recognized: {}'.format(gallery))
 
 
-def process_image(generator, content, image):
+def image_clipper(x):
+    return x[8:] if x[8] == '/' else x[7:]
 
-    image_clipper = lambda x: x[8:] if x[8] == '/' else x[7:]
-    file_clipper = lambda x: x[11:] if x[10] == '/' else x[10:]
+
+def file_clipper(x):
+    return x[11:] if x[10] == '/' else x[10:]
+
+
+def process_image(generator, content, image):
 
     if image.startswith('{photo}'):
         path = os.path.join(os.path.expanduser(generator.settings['PHOTO_LIBRARY']), image_clipper(image))
@@ -378,16 +475,16 @@ def process_image(generator, content, image):
 
 
 def detect_image(generator, content):
-        image = content.metadata.get('image', None)
-        if image:
-            if image.startswith('{photo}') or image.startswith('{filename}'):
-                process_image(generator, content, image)
-            else:
-                logger.error('photos: Image tag not recognized: {}'.format(image))
+    image = content.metadata.get('image', None)
+    if image:
+        if image.startswith('{photo}') or image.startswith('{filename}'):
+            process_image(generator, content, image)
+        else:
+            logger.error('photos: Image tag not recognized: {}'.format(image))
 
 
 def detect_images_and_galleries(generators):
-    """Runs generator on both pages and articles. """
+    """Runs generator on both pages and articles."""
     for generator in generators:
         if isinstance(generator, ArticlesGenerator):
             for article in itertools.chain(generator.articles, generator.drafts):
@@ -400,7 +497,7 @@ def detect_images_and_galleries(generators):
 
 
 def register():
-    """Uses the new style of registration based on GitHub Pelican issue #314. """
+    """Uses the new style of registration based on GitHub Pelican issue #314."""
     signals.initialized.connect(initialized)
     try:
         signals.content_object_init.connect(detect_content)

+ 1 - 0
photos/requirements.txt

@@ -1 +1,2 @@
 Pillow
+piexif>=1.0.5