Implemented a thumbnailer that generates thumbnails in the output directory.
This commit is contained in:
26
thumbnailer/Readme.md
Normal file
26
thumbnailer/Readme.md
Normal file
@@ -0,0 +1,26 @@
|
||||
Thumbnail Creation of images
|
||||
============================
|
||||
|
||||
This plugin creates thumbnails for all of the images found under a specific directory, in various thumbnail sizes
|
||||
It requires PIL to function properly since PIL is used to resize the images, and will only rebuild a thumbnail if it
|
||||
doesn't already exists (to save processing time)
|
||||
|
||||
Installation
|
||||
-------------
|
||||
|
||||
Setup up like a normal plugin by setting PLUGIN_PATH, and adding "thumbnailer" to the PLUGINS list
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
* IMAGE_PATH is the path to the image directory. It should reside under content, and defaults to "pictures"
|
||||
* THUMBNAIL_DIR is the path to the output sub directory where the thumbnails are generated
|
||||
* THUMBNAIL_SIZES is a dictionary mapping name of size to size specifications.
|
||||
The generated filename will be originalname_thumbnailname.ext
|
||||
|
||||
Sizes can be specified using any of the following formats:
|
||||
|
||||
* wxh will resize to exactly wxh cropping as necessary to get that size
|
||||
* wx? will resize so that the width is the specified size, and the height will scale to retain aspect ratio
|
||||
* ?xh same as wx? but will height being a set size
|
||||
* s is a shorthand for wxh where w=h
|
||||
1
thumbnailer/__init__.py
Normal file
1
thumbnailer/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .thumbnailer import *
|
||||
BIN
thumbnailer/test_data/expected_exact.jpg
Normal file
BIN
thumbnailer/test_data/expected_exact.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
BIN
thumbnailer/test_data/expected_height.jpg
Normal file
BIN
thumbnailer/test_data/expected_height.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.6 KiB |
BIN
thumbnailer/test_data/expected_square.jpg
Normal file
BIN
thumbnailer/test_data/expected_square.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
BIN
thumbnailer/test_data/expected_width.jpg
Normal file
BIN
thumbnailer/test_data/expected_width.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
BIN
thumbnailer/test_data/sample_image.jpg
Normal file
BIN
thumbnailer/test_data/sample_image.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 330 KiB |
36
thumbnailer/test_thumbnails.py
Normal file
36
thumbnailer/test_thumbnails.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from thumbnailer import _resizer
|
||||
from unittest import TestCase, main
|
||||
import os.path as path
|
||||
from PIL import Image, ImageChops
|
||||
|
||||
class ThumbnailerTests(TestCase):
|
||||
|
||||
def path(self, filename):
|
||||
return path.join(self.img_path, filename)
|
||||
|
||||
def setUp(self):
|
||||
self.img_path = path.join(path.dirname(__file__), "test_data")
|
||||
self.img = Image.open(self.path("sample_image.jpg"))
|
||||
|
||||
def testSquare(self):
|
||||
r = _resizer('square', '100')
|
||||
output = r.resize(self.img)
|
||||
self.assertEqual((100, 100), output.size)
|
||||
|
||||
def testExact(self):
|
||||
r = _resizer('exact', '250x100')
|
||||
output = r.resize(self.img)
|
||||
self.assertEqual((250, 100), output.size)
|
||||
|
||||
def testWidth(self):
|
||||
r = _resizer('aspect', '250x?')
|
||||
output = r.resize(self.img)
|
||||
self.assertEqual((250, 166), output.size)
|
||||
|
||||
def testHeight(self):
|
||||
r = _resizer('aspect', '?x250')
|
||||
output = r.resize(self.img)
|
||||
self.assertEqual((375, 250), output.size)
|
||||
|
||||
if __name__=="__main__":
|
||||
main()
|
||||
137
thumbnailer/thumbnailer.py
Normal file
137
thumbnailer/thumbnailer.py
Normal file
@@ -0,0 +1,137 @@
|
||||
import os
|
||||
import os.path as path
|
||||
import re
|
||||
from pelican import signals
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
from PIL import Image, ImageOps
|
||||
enabled = True
|
||||
except ImportError:
|
||||
logging.warning("Unable to load PIL, disabling thumbnailer")
|
||||
enabled = False
|
||||
|
||||
DEFAULT_IMAGE_DIR = "pictures"
|
||||
DEFAULT_THUMBNAIL_DIR = "thumbnails"
|
||||
DEFAULT_THUMBNAIL_SIZES = {
|
||||
'thumbnail_square': '150',
|
||||
'thumbnail_wide': '150x?',
|
||||
'thumbnail_tall': '?x150',
|
||||
}
|
||||
|
||||
class _resizer(object):
|
||||
""" Resizes based on a text specification, see readme """
|
||||
|
||||
REGEX = re.compile(r'(\d+|\?)x(\d+|\?)')
|
||||
|
||||
def __init__(self, name, spec):
|
||||
self._name = name
|
||||
self._spec = spec
|
||||
|
||||
def _null_resize(self, w, h, image):
|
||||
return image
|
||||
|
||||
def _exact_resize(self, w, h, image):
|
||||
retval = ImageOps.fit(image, (w,h), Image.BICUBIC)
|
||||
return retval
|
||||
|
||||
def _aspect_resize(self, w, h, image):
|
||||
retval = image.copy()
|
||||
retval.thumbnail((w, h), Image.ANTIALIAS)
|
||||
|
||||
return retval
|
||||
|
||||
def resize(self, image):
|
||||
resizer = self._null_resize
|
||||
|
||||
# Square resize and crop
|
||||
if 'x' not in self._spec:
|
||||
resizer = self._exact_resize
|
||||
targetw = int(self._spec)
|
||||
targeth = targetw
|
||||
else:
|
||||
matches = self.REGEX.search(self._spec)
|
||||
tmpw = matches.group(1)
|
||||
tmph = matches.group(2)
|
||||
|
||||
# Full Size
|
||||
if tmpw == '?' and tmph == '?':
|
||||
targetw = image.size[0]
|
||||
targeth = image.size[1]
|
||||
resizer = self._null_resize
|
||||
|
||||
# Set Height Size
|
||||
if tmpw == '?':
|
||||
targetw = image.size[0]
|
||||
targeth = int(tmph)
|
||||
resizer = self._aspect_resize
|
||||
|
||||
# Set Width Size
|
||||
elif tmph == '?':
|
||||
targetw = int(tmpw)
|
||||
targeth = image.size[1]
|
||||
resizer = self._aspect_resize
|
||||
|
||||
# Scale and Crop
|
||||
else:
|
||||
targetw = int(tmpw)
|
||||
targeth = int(tmph)
|
||||
resizer = self._exact_resize
|
||||
|
||||
logging.debug("Using resizer {0}".format(resizer.__name__))
|
||||
return resizer(targetw, targeth, image)
|
||||
|
||||
def _adjust_filename(self, in_path):
|
||||
new_filename = path.basename(in_path)
|
||||
(basename, ext) = path.splitext(new_filename)
|
||||
basename = "{0}_{1}".format(basename, self._name)
|
||||
new_filename = "{0}{1}".format(basename, ext)
|
||||
return new_filename
|
||||
|
||||
def resize_file_to(self, in_path, out_path):
|
||||
""" Given a filename, resize and save the image per the specification into out_path
|
||||
|
||||
:param in_path: path to image file to save. Must be supposed by PIL
|
||||
:param out_path: path to the directory root for the outputted thumbnails to be stored
|
||||
:return: None
|
||||
"""
|
||||
filename = path.join(out_path, self._adjust_filename(in_path))
|
||||
if not path.exists(out_path):
|
||||
os.makedirs(out_path)
|
||||
if not path.exists(filename):
|
||||
image = Image.open(in_path)
|
||||
thumbnail = self.resize(image)
|
||||
thumbnail.save(filename)
|
||||
logger.info("Generated Thumbnail {0}".format(path.basename(filename)))
|
||||
|
||||
|
||||
def resize_thumbnails(pelican):
|
||||
""" Resize a directory tree full of images into thumbnails
|
||||
|
||||
:param pelican: The pelican instance
|
||||
:return: None
|
||||
"""
|
||||
global enabled
|
||||
if not enabled:
|
||||
return
|
||||
|
||||
in_path = path.join(pelican.settings['PATH'],
|
||||
pelican.settings.get('IMAGE_PATH', DEFAULT_IMAGE_DIR))
|
||||
out_path = path.join(pelican.settings['OUTPUT_PATH'],
|
||||
pelican.settings.get('THUMBNAIL_DIR', DEFAULT_THUMBNAIL_DIR))
|
||||
|
||||
sizes = pelican.settings.get('THUMBNAIL_SIZES', DEFAULT_THUMBNAIL_SIZES)
|
||||
resizers = dict((k, _resizer(k, v)) for k,v in sizes.items())
|
||||
logger.debug("Thumbnailer Started")
|
||||
for dirpath, _, filenames in os.walk(in_path):
|
||||
for filename in filenames:
|
||||
for name, resizer in resizers.items():
|
||||
in_filename = path.join(dirpath, filename)
|
||||
logger.debug("Processing thumbnail {0}=>{1}".format(filename, name))
|
||||
resizer.resize_file_to(in_filename, out_path)
|
||||
|
||||
|
||||
def register():
|
||||
signals.finalized.connect(resize_thumbnails)
|
||||
Reference in New Issue
Block a user