Browse Source

Merge pull request #770 from bnice5000/photos

Photos: Add ability to keep EXIF data on resize
Justin Mayer 8 years ago
parent
commit
c83f10964d
4 changed files with 263 additions and 111 deletions
  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.
 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
 ## 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.
 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 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.
 * 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.
 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`
 `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
 	this-one-will-be-skipped-too.jpg
 	# but-this-file-will-NOT-be-skipped.jpg
 	# but-this-file-will-NOT-be-skipped.jpg
 	this-one-will-be-also-skipped.jpg
 	this-one-will-be-also-skipped.jpg
-	
+
 
 
 Here is an example Markdown article that shows the three use cases:
 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:
 	Here are my best photos, taken with my favorite camera:
 	![]({photo}mybag/camera.jpg).
 	![]({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
 ## 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
 ## 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
 ```Python
 def photo_gallery_gen(location):
 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 -*-
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
+import datetime
 import itertools
 import itertools
+import json
 import logging
 import logging
+import multiprocessing
 import os
 import os
 import pprint
 import pprint
 import re
 import re
 import sys
 import sys
-import multiprocessing
 
 
 from pelican.generators import ArticlesGenerator
 from pelican.generators import ArticlesGenerator
 from pelican.generators import PagesGenerator
 from pelican.generators import PagesGenerator
@@ -17,15 +19,22 @@ from pelican.utils import pelican_open
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
-
 try:
 try:
-    from PIL import ExifTags
     from PIL import Image
     from PIL import Image
     from PIL import ImageDraw
     from PIL import ImageDraw
     from PIL import ImageEnhance
     from PIL import ImageEnhance
     from PIL import ImageFont
     from PIL import ImageFont
 except ImportError:
 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):
 def initialized(pelican):
@@ -37,14 +46,23 @@ def initialized(pelican):
     DEFAULT_CONFIG.setdefault('PHOTO_ARTICLE', (760, 506, 80))
     DEFAULT_CONFIG.setdefault('PHOTO_ARTICLE', (760, 506, 80))
     DEFAULT_CONFIG.setdefault('PHOTO_THUMB', (192, 144, 60))
     DEFAULT_CONFIG.setdefault('PHOTO_THUMB', (192, 144, 60))
     DEFAULT_CONFIG.setdefault('PHOTO_GALLERY_TITLE', '')
     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', 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', '')
-    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_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['queue_resize'] = {}
     DEFAULT_CONFIG['created_galleries'] = {}
     DEFAULT_CONFIG['created_galleries'] = {}
+    DEFAULT_CONFIG['plugin_dir'] = os.path.dirname(os.path.realpath(__file__))
 
 
     if pelican:
     if pelican:
         pelican.settings.setdefault('PHOTO_LIBRARY', p)
         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_ARTICLE', (760, 506, 80))
         pelican.settings.setdefault('PHOTO_THUMB', (192, 144, 60))
         pelican.settings.setdefault('PHOTO_THUMB', (192, 144, 60))
         pelican.settings.setdefault('PHOTO_GALLERY_TITLE', '')
         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', 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', '')
-        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_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):
 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)):
 def enqueue_resize(orig, resized, spec=(640, 480, 80)):
-
     if resized not in DEFAULT_CONFIG['queue_resize']:
     if resized not in DEFAULT_CONFIG['queue_resize']:
         DEFAULT_CONFIG['queue_resize'][resized] = (orig, spec)
         DEFAULT_CONFIG['queue_resize'][resized] = (orig, spec)
     elif 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
     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):
 def ReduceOpacity(im, opacity):
-    """Reduces Opacity
+    """Reduces Opacity.
 
 
     Returns an image with reduced opacity.
     Returns an image with reduced opacity.
     Taken from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/362879
     Taken from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/362879
     """
     """
-
     assert opacity >= 0 and opacity <= 1
     assert opacity >= 0 and opacity <= 1
     if isalpha(im):
     if isalpha(im):
         im = im.copy()
         im = im.copy()
@@ -113,7 +144,7 @@ def ReduceOpacity(im, opacity):
     return im
     return im
 
 
 
 
-def watermark_photo(image, watermark_text, watermark_image, watermark_image_size):
+def watermark_photo(image, settings):
 
 
     margin = [10, 10]
     margin = [10, 10]
     opacity = 0.6
     opacity = 0.6
@@ -124,22 +155,20 @@ def watermark_photo(image, watermark_text, watermark_image, watermark_image_size
     image_reducer = 8
     image_reducer = 8
     text_size = [0, 0]
     text_size = [0, 0]
     mark_size = [0, 0]
     mark_size = [0, 0]
-    font_height = 0
     text_position = [0, 0]
     text_position = [0, 0]
 
 
-    if watermark_text:
+    if settings['PHOTO_WATERMARK_TEXT']:
         font_name = 'SourceCodePro-Bold.otf'
         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)
         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]]
         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_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 = [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]])
         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
     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))
     logger.info('photos: make photo {} -> {}'.format(orig, resized))
     im = Image.open(orig)
     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)
     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):
 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'])
     pool = multiprocessing.Pool(generator.settings['PHOTO_RESIZE_JOBS'])
+    logger.debug('Debug Status: {}'.format(debug))
     for resized, what in DEFAULT_CONFIG['queue_resize'].items():
     for resized, what in DEFAULT_CONFIG['queue_resize'].items():
         resized = os.path.join(generator.output_path, resized)
         resized = os.path.join(generator.output_path, resized)
         orig, spec = what
         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.close()
     pool.join()
     pool.join()
@@ -257,7 +351,7 @@ def galleries_string_decompose(gallery_string):
     title_regex = re.compile(r'{(.+)}')
     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 = 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]
     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 = 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]
         galleries = [dict(gallery) for gallery in galleries]
         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']
                 gallery['title'] = DEFAULT_CONFIG['PHOTO_GALLERY_TITLE']
         return galleries
         return galleries
     else:
     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):
 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)))
             logger.debug('Gallery Data: '.format(pprint.pformat(content.photo_gallery)))
             DEFAULT_CONFIG['created_galleries']['gallery'] = content_gallery
             DEFAULT_CONFIG['created_galleries']['gallery'] = content_gallery
         else:
         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):
 def detect_gallery(generator, content):
@@ -346,10 +438,15 @@ def detect_gallery(generator, content):
             logger.error('photos: Gallery tag not recognized: {}'.format(gallery))
             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}'):
     if image.startswith('{photo}'):
         path = os.path.join(os.path.expanduser(generator.settings['PHOTO_LIBRARY']), image_clipper(image))
         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):
 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):
 def detect_images_and_galleries(generators):
-    """Runs generator on both pages and articles. """
+    """Runs generator on both pages and articles."""
     for generator in generators:
     for generator in generators:
         if isinstance(generator, ArticlesGenerator):
         if isinstance(generator, ArticlesGenerator):
             for article in itertools.chain(generator.articles, generator.translations, generator.drafts):
             for article in itertools.chain(generator.articles, generator.translations, generator.drafts):
@@ -400,7 +497,7 @@ def detect_images_and_galleries(generators):
 
 
 
 
 def register():
 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)
     signals.initialized.connect(initialized)
     try:
     try:
         signals.content_object_init.connect(detect_content)
         signals.content_object_init.connect(detect_content)

+ 1 - 0
photos/requirements.txt

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