Skip to content

Commit

Permalink
Merge branch 'master' of github.com:getnikola/nikola
Browse files Browse the repository at this point in the history
  • Loading branch information
ralsina committed Apr 13, 2020
2 parents e119941 + 7c92c7b commit 7d19015
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 126 deletions.
2 changes: 2 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ Features
Bugfixes
--------

* Refactored scale_image causing performance increasing in image
resizing.
* Don’t force absolute links for brand/languages (Issue #3229)
* Fix RTL mirroring in base theme (``:dir()`` pseudo-class is Firefox only)
(Issue #3353)
Expand Down
215 changes: 118 additions & 97 deletions nikola/image_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,111 +81,132 @@ def filter_exif(self, exif, whitelist):

return exif or None

def resize_image(self, src, dst, max_size, bigger_panoramas=True, preserve_exif_data=False, exif_whitelist={}, preserve_icc_profiles=False):
"""Make a copy of the image in the requested size."""
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, max_size, bigger_panoramas)
self.resize_svg(src, dst_paths, max_sizes, bigger_panoramas)
return

im = Image.open(src)
_im = Image.open(src)

# The jpg exclusion is Issue #3332
if hasattr(im, 'n_frames') and im.n_frames > 1 and extension not in {'.jpg', '.jpeg'}:
# Animated gif, leave as-is
utils.copy_file(src, dst)
return
is_animated = hasattr(_im, 'n_frames') and _im.n_frames > 1 and extension not in {'.jpg', '.jpeg'}

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:
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:
icc_profile = im.info.get('icc_profile') if preserve_icc_profiles else None
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), icc_profile=icc_profile)
else:
im.save(dst, icc_profile=icc_profile)
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, 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.warning("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)
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
im.save(dst, exif=piexif.dump(exif), icc_profile=icc_profile)
else:
im.save(dst, icc_profile=icc_profile)
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."""
Expand Down
40 changes: 13 additions & 27 deletions nikola/plugins/task/galleries.py
Original file line number Diff line number Diff line change
Expand Up @@ -573,42 +573,28 @@ def create_target_images(self, img, input_path):
orig_dest_path = os.path.join(output_gallery, img_name)
yield utils.apply_filters({
'basename': self.name,
'name': thumb_path,
'name': orig_dest_path,
'file_dep': [img],
'targets': [thumb_path],
'targets': [thumb_path, orig_dest_path],
'actions': [
(self.resize_image,
(img, thumb_path, self.kw['thumbnail_size'], True, self.kw['preserve_exif_data'],
self.kw['exif_whitelist'], self.kw['preserve_icc_profiles']))
],
[img], {
'dst_paths': [thumb_path, orig_dest_path],
'max_sizes': [self.kw['thumbnail_size'], self.kw['max_image_size']],
'bigger_panoramas': True,
'preserve_exif_data': self.kw['preserve_exif_data'],
'exif_whitelist': self.kw['exif_whitelist'],
'preserve_icc_profiles': self.kw['preserve_icc_profiles']})],
'clean': True,
'uptodate': [utils.config_changed({
1: self.kw['thumbnail_size'],
2: self.kw['preserve_exif_data'],
3: self.kw['exif_whitelist'],
4: self.kw['preserve_icc_profiles'],
2: self.kw['max_image_size'],
3: self.kw['preserve_exif_data'],
4: self.kw['exif_whitelist'],
5: self.kw['preserve_icc_profiles'],
}, 'nikola.plugins.task.galleries:resize_thumb')],
}, self.kw['filters'])

yield utils.apply_filters({
'basename': self.name,
'name': orig_dest_path,
'file_dep': [img],
'targets': [orig_dest_path],
'actions': [
(self.resize_image,
(img, orig_dest_path, self.kw['max_image_size'], True, self.kw['preserve_exif_data'],
self.kw['exif_whitelist'], self.kw['preserve_icc_profiles']))
],
'clean': True,
'uptodate': [utils.config_changed({
1: self.kw['max_image_size'],
2: self.kw['preserve_exif_data'],
3: self.kw['exif_whitelist'],
4: self.kw['preserve_icc_profiles'],
}, 'nikola.plugins.task.galleries:resize_max')],
}, self.kw['filters'])

def remove_excluded_image(self, img, input_folder):
"""Remove excluded images."""
# Remove excluded images
Expand Down
11 changes: 9 additions & 2 deletions nikola/plugins/task/scale_images.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,15 @@ def process_tree(self, src, dst):

def process_image(self, src, dst, thumb):
"""Resize an image."""
self.resize_image(src, dst, self.kw['max_image_size'], True, preserve_exif_data=self.kw['preserve_exif_data'], exif_whitelist=self.kw['exif_whitelist'], preserve_icc_profiles=self.kw['preserve_icc_profiles'])
self.resize_image(src, thumb, self.kw['image_thumbnail_size'], True, preserve_exif_data=self.kw['preserve_exif_data'], exif_whitelist=self.kw['exif_whitelist'], preserve_icc_profiles=self.kw['preserve_icc_profiles'])
self.resize_image(
src,
dst_paths=[dst, thumb],
max_sizes=[self.kw['max_image_size'], self.kw['image_thumbnail_size']],
bigger_panoramas=True,
preserve_exif_data=self.kw['preserve_exif_data'],
exif_whitelist=self.kw['exif_whitelist'],
preserve_icc_profiles=self.kw['preserve_icc_profiles']
)

def gen_tasks(self):
"""Copy static files into the output folder."""
Expand Down

0 comments on commit 7d19015

Please sign in to comment.