Merge pull request #43 from RussTheAerialist/master

Implement a thumbnail-generation plugin.
This commit is contained in:
Alexis Metaireau
2013-07-10 15:56:15 -07:00
9 changed files with 240 additions and 0 deletions

26
thumbnailer/Readme.md Normal file
View 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
View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

View 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()

177
thumbnailer/thumbnailer.py Normal file
View File

@@ -0,0 +1,177 @@
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',
}
DEFAULT_TEMPLATE = """<a href="{url}" rel="shadowbox" title="{filename}"><img src="{thumbnail}" alt="{filename}"></a>"""
DEFAULT_GALLERY_THUMB = "thumbnail_square"
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 get_thumbnail_name(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.get_thumbnail_name(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 = _image_path(pelican)
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 _image_path(pelican):
return path.join(pelican.settings['PATH'],
pelican.settings.get("IMAGE_PATH", DEFAULT_IMAGE_DIR))
def expand_gallery(generator, metadata):
""" Expand a gallery tag to include all of the files in a specific directory under IMAGE_PATH
:param pelican: The pelican instance
:return: None
"""
if "gallery" not in metadata or metadata['gallery'] is None:
import pprint
pprint.pprint(metadata)
return # If no gallery specified, we do nothing
lines = [ ]
base_path = _image_path(generator)
in_path = path.join(base_path, metadata['gallery'])
template = generator.settings.get('GALLERY_TEMPLATE', DEFAULT_TEMPLATE)
thumbnail_name = generator.settings.get("GALLERY_THUMBNAIL", DEFAULT_GALLERY_THUMB)
thumbnail_prefix = generator.settings.get("")
resizer = _resizer(thumbnail_name, '?x?')
for dirpath, _, filenames in os.walk(in_path):
for filename in filenames:
url = path.join(dirpath, filename).replace(base_path, "")[1:]
url = path.join('/static', generator.settings.get('IMAGE_PATH', DEFAULT_IMAGE_DIR), url).replace('\\', '/')
logger.debug("GALLERY: {0}".format(url))
thumbnail = resizer.get_thumbnail_name(filename)
thumbnail = path.join('/', generator.settings.get('THUMBNAIL_DIR', DEFAULT_THUMBNAIL_DIR), thumbnail).replace('\\', '/')
lines.append(template.format(
filename=filename,
url=url,
thumbnail=thumbnail,
))
metadata['gallery_content'] = "\n".join(lines)
def register():
signals.finalized.connect(resize_thumbnails)
signals.article_generate_context.connect(expand_gallery)