aboutsummaryrefslogtreecommitdiffstats
path: root/nikola/image_processing.py
diff options
context:
space:
mode:
authorLibravatarUnit 193 <unit193@unit193.net>2021-02-03 19:17:50 -0500
committerLibravatarUnit 193 <unit193@unit193.net>2021-02-03 19:17:50 -0500
commit475d074fd74425efbe783fad08f97f2df0c4909f (patch)
tree2acdae53999b3c74b716efa4edb5b40311fa356a /nikola/image_processing.py
parentcd502d52787f666fff3254d7d7e7578930c813c2 (diff)
parent3a0d66f07b112b6d2bdc2b57bbf717a89a351ce6 (diff)
Update upstream source from tag 'upstream/8.1.2'
Update to upstream version '8.1.2' with Debian dir e5e966a9e6010ef70618dc9a61558fa4db35aceb
Diffstat (limited to 'nikola/image_processing.py')
-rw-r--r--nikola/image_processing.py241
1 files changed, 133 insertions, 108 deletions
diff --git a/nikola/image_processing.py b/nikola/image_processing.py
index e0096b2..04d4e64 100644
--- a/nikola/image_processing.py
+++ b/nikola/image_processing.py
@@ -26,35 +26,24 @@
"""Process images."""
-from __future__ import unicode_literals
import datetime
+import gzip
import os
-import lxml
import re
-import gzip
+import lxml
import piexif
+from PIL import ExifTags, Image
from nikola import utils
-Image = None
-try:
- from PIL import ExifTags, Image # NOQA
-except ImportError:
- try:
- 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', '.svgz', '.bmp', '.tiff']
+ image_ext_list_builtin = ['.jpg', '.png', '.jpeg', '.gif', '.svg', '.svgz', '.bmp', '.tiff', '.webp']
def _fill_exif_tag_names(self):
"""Connect EXIF tag names to numeric values."""
@@ -92,101 +81,136 @@ class ImageProcessor(object):
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)
+ def resize_image(self, src, dst=None, max_size=None, bigger_panoramas=True, preserve_exif_data=False, exif_whitelist={}, preserve_icc_profiles=False, dst_paths=None, max_sizes=None):
+ """Make a copy of the image in the requested size(s).
+
+ max_sizes should be a list of sizes, and the image would be resized to fit in a
+ square of each size (preserving aspect ratio).
+
+ dst_paths is a list of the destination paths, and should be the same length as max_sizes.
+
+ Backwards compatibility:
+
+ * If max_sizes is None, it's set to [max_size]
+ * If dst_paths is None, it's set to [dst]
+ * Either max_size or max_sizes should be set
+ * Either dst or dst_paths should be set
+ """
+ if dst_paths is None:
+ dst_paths = [dst]
+ if max_sizes is None:
+ max_sizes = [max_size]
+ if len(max_sizes) != len(dst_paths):
+ raise ValueError('resize_image called with incompatible arguments: {} / {}'.format(dst_paths, max_sizes))
+ extension = os.path.splitext(src)[1].lower()
+ if extension in {'.svg', '.svgz'}:
+ self.resize_svg(src, dst_paths, max_sizes, bigger_panoramas)
return
- im = Image.open(src)
- size = w, h = im.size
- if w > max_size or h > max_size:
- size = max_size, max_size
-
- # Panoramas get larger thumbnails because they look *awful*
- if bigger_panoramas and w > 2 * h:
- size = min(w, max_size * 4), min(w, max_size * 4)
-
- 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:
+
+ _im = Image.open(src)
+
+ # The jpg exclusion is Issue #3332
+ is_animated = hasattr(_im, 'n_frames') and _im.n_frames > 1 and extension not in {'.jpg', '.jpeg'}
+
+ exif = None
+ if "exif" in _im.info:
+ exif = piexif.load(_im.info["exif"])
# 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
-
- 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 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)
+ if "0th" in 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
+ exif = self.filter_exif(exif, exif_whitelist)
+
+ icc_profile = _im.info.get('icc_profile') if preserve_icc_profiles else None
+
+ for dst, max_size in zip(dst_paths, max_sizes):
+ if is_animated: # Animated gif, leave as-is
+ utils.copy_file(src, dst)
+ continue
+
+ im = _im.copy()
+
+ size = w, h = im.size
+ if w > max_size or h > max_size:
+ size = max_size, max_size
+ # Panoramas get larger thumbnails because they look *awful*
+ if bigger_panoramas and w > 2 * h:
+ size = min(w, max_size * 4), min(w, max_size * 4)
+ try:
+ im.thumbnail(size, Image.ANTIALIAS)
+ save_args = {}
+ if icc_profile:
+ save_args['icc_profile'] = icc_profile
+
+ 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
+ save_args['exif'] = piexif.dump(exif)
+
+ im.save(dst, **save_args)
+ except Exception as e:
+ self.logger.warning("Can't process {0}, using original "
+ "image! ({1})".format(src, e))
+ utils.copy_file(src, dst)
+
+ def resize_svg(self, src, dst_paths, max_sizes, bigger_panoramas):
+ """Make a copy of an svg at the requested sizes."""
+ # 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()
+
+ for dst, max_size in zip(dst_paths, max_sizes):
+ try:
+ 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.warning("No width/height in %s. Original exception: %s" % (src, e))
+ utils.copy_file(src, dst)
def image_date(self, src):
"""Try to figure out the date of the image."""
@@ -194,6 +218,7 @@ class ImageProcessor(object):
try:
im = Image.open(src)
exif = im._getexif()
+ im.close()
except Exception:
exif = None
if exif is not None: