Browse Source

Implemented a thumbnailer that generates thumbnails in the output directory.

Russell Hay 11 years ago
parent
commit
9e2b0eb064

+ 26 - 0
thumbnailer/Readme.md

@@ -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 - 0
thumbnailer/__init__.py

@@ -0,0 +1 @@
+from .thumbnailer import *

BIN
thumbnailer/test_data/expected_exact.jpg


BIN
thumbnailer/test_data/expected_height.jpg


BIN
thumbnailer/test_data/expected_square.jpg


BIN
thumbnailer/test_data/expected_width.jpg


BIN
thumbnailer/test_data/sample_image.jpg


+ 36 - 0
thumbnailer/test_thumbnails.py

@@ -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 - 0
thumbnailer/thumbnailer.py

@@ -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)