aboutsummaryrefslogtreecommitdiffstats
path: root/nikola/image_processing.py
diff options
context:
space:
mode:
Diffstat (limited to 'nikola/image_processing.py')
-rw-r--r--nikola/image_processing.py162
1 files changed, 131 insertions, 31 deletions
diff --git a/nikola/image_processing.py b/nikola/image_processing.py
index 0ba139f..e0096b2 100644
--- a/nikola/image_processing.py
+++ b/nikola/image_processing.py
@@ -29,34 +29,76 @@
from __future__ import unicode_literals
import datetime
import os
+import lxml
+import re
+import gzip
+
+import piexif
from nikola import utils
Image = None
try:
- from PIL import Image, ExifTags # NOQA
+ from PIL import ExifTags, Image # NOQA
except ImportError:
try:
- import Image as _Image
import ExifTags
+ import Image as _Image
Image = _Image
except ImportError:
pass
+EXIF_TAG_NAMES = {}
-class ImageProcessor(object):
+class ImageProcessor(object):
"""Apply image operations."""
- image_ext_list_builtin = ['.jpg', '.png', '.jpeg', '.gif', '.svg', '.bmp', '.tiff']
+ image_ext_list_builtin = ['.jpg', '.png', '.jpeg', '.gif', '.svg', '.svgz', '.bmp', '.tiff']
+
+ def _fill_exif_tag_names(self):
+ """Connect EXIF tag names to numeric values."""
+ if not EXIF_TAG_NAMES:
+ for ifd in piexif.TAGS:
+ for tag, data in piexif.TAGS[ifd].items():
+ EXIF_TAG_NAMES[tag] = data['name']
- def resize_image(self, src, dst, max_size, bigger_panoramas=True):
+ def filter_exif(self, exif, whitelist):
+ """Filter EXIF data as described in the documentation."""
+ # Scenario 1: keep everything
+ if whitelist == {'*': '*'}:
+ return exif
+
+ # Scenario 2: keep nothing
+ if whitelist == {}:
+ return None
+
+ # Scenario 3: keep some
+ self._fill_exif_tag_names()
+ exif = exif.copy() # Don't modify in-place, it's rude
+ for k in list(exif.keys()):
+ if type(exif[k]) != dict:
+ pass # At least thumbnails have no fields
+ elif k not in whitelist:
+ exif.pop(k) # Not whitelisted, remove
+ elif k in whitelist and whitelist[k] == '*':
+ # Fully whitelisted, keep all
+ pass
+ else:
+ # Partially whitelisted
+ for tag in list(exif[k].keys()):
+ if EXIF_TAG_NAMES[tag] not in whitelist[k]:
+ exif[k].pop(tag)
+
+ return exif or None
+
+ def resize_image(self, src, dst, max_size, bigger_panoramas=True, preserve_exif_data=False, exif_whitelist={}):
"""Make a copy of the image in the requested size."""
- if not Image:
- utils.copy_file(src, dst)
+ if not Image or os.path.splitext(src)[1] in ['.svg', '.svgz']:
+ self.resize_svg(src, dst, max_size, bigger_panoramas)
return
im = Image.open(src)
- w, h = im.size
+ size = w, h = im.size
if w > max_size or h > max_size:
size = max_size, max_size
@@ -64,30 +106,86 @@ class ImageProcessor(object):
if bigger_panoramas and w > 2 * h:
size = min(w, max_size * 4), min(w, max_size * 4)
- try:
- exif = im._getexif()
- except Exception:
- exif = None
- if exif is not None:
- for tag, value in list(exif.items()):
- decoded = ExifTags.TAGS.get(tag, tag)
+ try:
+ exif = piexif.load(im.info["exif"])
+ except KeyError:
+ exif = None
+ # Inside this if, we can manipulate exif as much as
+ # we want/need and it will be preserved if required
+ if exif is not None:
+ # Rotate according to EXIF
+ value = exif['0th'].get(piexif.ImageIFD.Orientation, 1)
+ if value in (3, 4):
+ im = im.transpose(Image.ROTATE_180)
+ elif value in (5, 6):
+ im = im.transpose(Image.ROTATE_270)
+ elif value in (7, 8):
+ im = im.transpose(Image.ROTATE_90)
+ if value in (2, 4, 5, 7):
+ im = im.transpose(Image.FLIP_LEFT_RIGHT)
+ exif['0th'][piexif.ImageIFD.Orientation] = 1
- 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
- try:
- im.thumbnail(size, Image.ANTIALIAS)
+ try:
+ im.thumbnail(size, Image.ANTIALIAS)
+ if exif is not None and preserve_exif_data:
+ # Put right size in EXIF data
+ w, h = im.size
+ if '0th' in exif:
+ exif["0th"][piexif.ImageIFD.ImageWidth] = w
+ exif["0th"][piexif.ImageIFD.ImageLength] = h
+ if 'Exif' in exif:
+ exif["Exif"][piexif.ExifIFD.PixelXDimension] = w
+ exif["Exif"][piexif.ExifIFD.PixelYDimension] = h
+ # Filter EXIF data as required
+ exif = self.filter_exif(exif, exif_whitelist)
+ im.save(dst, exif=piexif.dump(exif))
+ else:
im.save(dst)
- except Exception as e:
- self.logger.warn("Can't thumbnail {0}, using original "
- "image as thumbnail ({1})".format(src, e))
- utils.copy_file(src, dst)
- else: # Image is small
+ except Exception as e:
+ self.logger.warn("Can't process {0}, using original "
+ "image! ({1})".format(src, e))
+ utils.copy_file(src, dst)
+
+ def resize_svg(self, src, dst, max_size, bigger_panoramas):
+ """Make a copy of an svg at the requested size."""
+ try:
+ # Resize svg based on viewport hacking.
+ # note that this can also lead to enlarged svgs
+ if src.endswith('.svgz'):
+ with gzip.GzipFile(src, 'rb') as op:
+ xml = op.read()
+ else:
+ with open(src, 'rb') as op:
+ xml = op.read()
+ tree = lxml.etree.XML(xml)
+ width = tree.attrib['width']
+ height = tree.attrib['height']
+ w = int(re.search("[0-9]+", width).group(0))
+ h = int(re.search("[0-9]+", height).group(0))
+ # calculate new size preserving aspect ratio.
+ ratio = float(w) / h
+ # Panoramas get larger thumbnails because they look *awful*
+ if bigger_panoramas and w > 2 * h:
+ max_size = max_size * 4
+ if w > h:
+ w = max_size
+ h = max_size / ratio
+ else:
+ w = max_size * ratio
+ h = max_size
+ w = int(w)
+ h = int(h)
+ tree.attrib.pop("width")
+ tree.attrib.pop("height")
+ tree.attrib['viewport'] = "0 0 %ipx %ipx" % (w, h)
+ if dst.endswith('.svgz'):
+ op = gzip.GzipFile(dst, 'wb')
+ else:
+ op = open(dst, 'wb')
+ op.write(lxml.etree.tostring(tree))
+ op.close()
+ except (KeyError, AttributeError) as e:
+ self.logger.warn("No width/height in %s. Original exception: %s" % (src, e))
utils.copy_file(src, dst)
def image_date(self, src):
@@ -103,8 +201,10 @@ class ImageProcessor(object):
decoded = ExifTags.TAGS.get(tag, tag)
if decoded in ('DateTimeOriginal', 'DateTimeDigitized'):
try:
+ if isinstance(value, tuple):
+ value = value[0]
self.dates[src] = datetime.datetime.strptime(
- value, r'%Y:%m:%d %H:%M:%S')
+ value, '%Y:%m:%d %H:%M:%S')
break
except ValueError: # Invalid EXIF date.
pass