diff options
Diffstat (limited to 'nikola/image_processing.py')
| -rw-r--r-- | nikola/image_processing.py | 241 |
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: |
