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.py116
1 files changed, 85 insertions, 31 deletions
diff --git a/nikola/image_processing.py b/nikola/image_processing.py
index b6f8215..e0096b2 100644
--- a/nikola/image_processing.py
+++ b/nikola/image_processing.py
@@ -33,32 +33,72 @@ 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):
"""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 filter_exif(self, exif, whitelist):
+ """Filter EXIF data as described in the documentation."""
+ # Scenario 1: keep everything
+ if whitelist == {'*': '*'}:
+ return exif
- def resize_image(self, src, dst, max_size, bigger_panoramas=True):
+ # 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 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
@@ -66,30 +106,44 @@ 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):
@@ -98,10 +152,10 @@ class ImageProcessor(object):
# Resize svg based on viewport hacking.
# note that this can also lead to enlarged svgs
if src.endswith('.svgz'):
- with gzip.GzipFile(src) as op:
+ with gzip.GzipFile(src, 'rb') as op:
xml = op.read()
else:
- with open(src) as op:
+ with open(src, 'rb') as op:
xml = op.read()
tree = lxml.etree.XML(xml)
width = tree.attrib['width']
@@ -125,9 +179,9 @@ class ImageProcessor(object):
tree.attrib.pop("height")
tree.attrib['viewport'] = "0 0 %ipx %ipx" % (w, h)
if dst.endswith('.svgz'):
- op = gzip.GzipFile(dst, 'w')
+ op = gzip.GzipFile(dst, 'wb')
else:
- op = open(dst, 'w')
+ op = open(dst, 'wb')
op.write(lxml.etree.tostring(tree))
op.close()
except (KeyError, AttributeError) as e: