diff --git a/requirements.txt b/requirements.txt index 45cc767..18306a3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ clint Jinja2 Markdown Pillow +pilkit pytest diff --git a/setup.py b/setup.py index f3c6157..319d78c 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ import os from setuptools import setup, find_packages -requires = ['argh', 'clint', 'jinja2', 'Markdown'] +requires = ['argh', 'clint', 'jinja2', 'Markdown', 'pilkit'] try: from PIL import Image, ImageOps # NOQA diff --git a/sigal/gallery.py b/sigal/gallery.py index 62f93ac..2c8da59 100644 --- a/sigal/gallery.py +++ b/sigal/gallery.py @@ -26,6 +26,7 @@ import codecs import logging import markdown import os +import shutil import sys from clint.textui import progress, colored @@ -33,7 +34,7 @@ from multiprocessing import Pool from os.path import join from PIL import Image as PILImage -from .image import Image +from .image import generate_image, generate_thumbnail from .settings import get_thumb from .writer import Writer @@ -211,7 +212,6 @@ class Gallery(object): for f in img_iterator: filename = os.path.split(f)[1] outname = join(outpath, filename) - self.logger.info(filename) if os.path.isfile(outname) and not self.force: self.logger.info("%s exists - skipping", filename) @@ -239,25 +239,25 @@ def process_image(filepath, outpath, settings): filename = os.path.split(filepath)[1] outname = join(outpath, filename) + ext = os.path.splitext(filename) - img = Image(filepath) + if ext in ['.jpg', '.jpeg', '.JPG', '.JPEG']: + options = settings['jpg_options'] + elif ext == '.png': + options = {'optimize': True} + else: + options = {} if settings['keep_orig']: - img.save(join(outpath, settings['orig_dir'], filename), - **settings['jpg_options']) + shutil.copy(filepath, join(outpath, settings['orig_dir'], filename)) - img.resize(settings['img_size']) - - if settings['copyright']: - img.add_copyright(settings['copyright']) - - img.save(outname, **settings['jpg_options']) + generate_image(filepath, outname, settings['img_size'], None, + options=options, copyright_text=settings['copyright']) if settings['make_thumbs']: thumb_name = join(outpath, get_thumb(settings, filename)) - img.thumbnail(thumb_name, settings['thumb_size'], - fit=settings['thumb_fit'], - quality=settings['jpg_options']['quality']) + generate_thumbnail(outname, thumb_name, settings['thumb_size'], None, + fit=settings['thumb_fit'], options=options) def get_metadata(path): @@ -266,8 +266,8 @@ def get_metadata(path): - title - thumbnail image - description - """ + """ descfile = join(path, DESCRIPTION_FILE) if not os.path.isfile(descfile): @@ -279,11 +279,10 @@ def get_metadata(path): 'thumbnail': '' } else: - md = markdown.Markdown(extensions=['meta']) - with codecs.open(descfile, "r", "utf-8") as f: text = f.read() + md = markdown.Markdown(extensions=['meta']) html = md.convert(text) meta = { diff --git a/sigal/image.py b/sigal/image.py index 70ebd4f..8f0938b 100644 --- a/sigal/image.py +++ b/sigal/image.py @@ -20,128 +20,64 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. -from __future__ import division - import logging -import os -import sys -from exceptions import IOError from PIL import Image as PILImage -from PIL import ImageDraw, ImageOps, ImageFile - -# EXIF specs Orientation constant -EXIF_ORIENTATION_TAG = 274 +from PIL import ImageDraw, ImageOps +from pilkit.processors import ProcessorPipeline, Transpose, ResizeToFill +from pilkit.utils import save_image -class Image(object): - """Image container +def generate_image(source, outname, size, format, options=None, + autoconvert=True, copyright_text=''): + """Image processor, rotate and resize the image. - Prepare images: resize images, and create thumbnails with some options - (squared thumbs, ...). - - :param filename: path to an image + :param source: path to an image + :param options: dict with PIL options (quality, optimize, progressive) """ - def __init__(self, filename): - self.filename = filename - self.imgname = os.path.split(filename)[1] - self.logger = logging.getLogger(__name__) + logger = logging.getLogger(__name__) + img = PILImage.open(source) + original_format = img.format - with open(filename, 'rb') as fp: - self.img = PILImage.open(fp) - self.img.load() + # Rotate the img, and catch IOError when PIL fails to read EXIF + try: + processor = Transpose() + img = processor.process(img) + except IOError: + pass - # Try to read exif metadata. This can fail if: - # - the image does not have exif (png files) -> AttributeError - # - PIL fail to read exif -> IOError - try: - exif = self.img._getexif() - except (IOError, AttributeError): - exif = False + # Run the other processors + processors = [ResizeToFill(*size)] + img = ProcessorPipeline(processors).process(img) - if exif: - # http://www.impulseadventure.com/photo/exif-orientation.html - orientation = exif.get(EXIF_ORIENTATION_TAG) - rotate_map = {3: 180, 6: -90, 8: 90} - rotation = rotate_map.get(orientation) - if rotation: - self.img = self.img.rotate(rotation) + if copyright_text: + add_copyright(img, copyright_text) - def save(self, filename, **kwargs): - """Save the image. - - Pass a dict with PIL options (quality, optimize, progressive). PIL can - have problems saving large JPEGs if MAXBLOCK isn't big enough, So if - we have a problem saving, we temporarily increase it. See - http://github.com/jdriscoll/django-imagekit/issues/91 - - """ - try: - with quiet(): - self.img.save(filename, "JPEG", **kwargs) - except IOError: - old_maxblock = ImageFile.MAXBLOCK - ImageFile.MAXBLOCK = self.img.size[0] * self.img.size[1] - try: - self.img.save(filename, "JPEG", **kwargs) - finally: - ImageFile.MAXBLOCK = old_maxblock - - def resize(self, size): - """Resize the image. - - - check if the image format is portrait or landscape and adjust `size`. - - compute the width and height ratio, and keep the min to resize the - image inside the `size` box without distorting it. - - :param size: tuple with the (with, height) to resize - - """ - - if self.img.size[0] > self.img.size[1]: - newsize = size - else: - newsize = (size[1], size[0]) - - wratio = newsize[0] / self.img.size[0] - hratio = newsize[1] / self.img.size[1] - ratio = min(wratio, hratio) - newsize = (int(ratio * self.img.size[0]), - int(ratio * self.img.size[1])) - - if ratio < 1: - self.img = self.img.resize(newsize, PILImage.ANTIALIAS) - - def add_copyright(self, text): - "Add a copyright to the image" - - draw = ImageDraw.Draw(self.img) - draw.text((5, self.img.size[1] - 15), '\xa9 ' + text) - - def thumbnail(self, filename, box, fit=True, quality=90): - "Create a thumbnail image" - - if fit: - self.img = ImageOps.fit(self.img, box, PILImage.ANTIALIAS) - else: - self.img.thumbnail(box, PILImage.ANTIALIAS) - - self.img.save(filename, quality=quality) + format = format or img.format or original_format or 'JPEG' + logger.debug('Save resized image to {0} ({1})'.format(outname, format)) + save_image(img, outname, format, options=options, autoconvert=autoconvert) -class quiet(object): - """A context manager for suppressing the stderr activity of PIL's C - libraries. Based on http://stackoverflow.com/a/978264/155370 +def generate_thumbnail(source, outname, box, format, fit=True, options=None): + "Create a thumbnail image" - """ - def __enter__(self): - self.stderr_fd = sys.__stderr__.fileno() - self.null_fd = os.open(os.devnull, os.O_RDWR) - self.old = os.dup(self.stderr_fd) - os.dup2(self.null_fd, self.stderr_fd) + logger = logging.getLogger(__name__) + img = PILImage.open(source) + original_format = img.format - def __exit__(self, *args, **kwargs): - os.dup2(self.old, self.stderr_fd) - os.close(self.null_fd) - os.close(self.old) + if fit: + img = ImageOps.fit(img, box, PILImage.ANTIALIAS) + else: + img.thumbnail(box, PILImage.ANTIALIAS) + + format = format or img.format or original_format or 'JPEG' + logger.debug('Save thumnail image to {0} ({1})'.format(outname, format)) + save_image(img, outname, format, options=options, autoconvert=True) + + +def add_copyright(img, text): + "Add a copyright to the image" + + draw = ImageDraw.Draw(img) + draw.text((5, img.size[1] - 15), '\xa9 ' + text) diff --git a/sigal/writer.py b/sigal/writer.py index 582747d..54d5c2c 100644 --- a/sigal/writer.py +++ b/sigal/writer.py @@ -33,7 +33,7 @@ from distutils.dir_util import copy_tree from jinja2 import Environment, FileSystemLoader, ChoiceLoader, PrefixLoader from jinja2.exceptions import TemplateNotFound -from .image import Image +from .image import generate_thumbnail from .settings import get_thumb from .pkgmeta import __url__ as sigal_link @@ -147,10 +147,11 @@ class Writer(object): # generate the thumbnail if it is missing (if # settings['make_thumbs'] is False) if not os.path.exists(thumb_path): - img = Image(os.path.join(self.output_dir, dpath, alb_thumb)) - img.thumbnail(thumb_path, self.settings['thumb_size'], - fit=self.settings['thumb_fit'], - quality=self.settings['jpg_quality']) + source = os.path.join(self.output_dir, dpath, alb_thumb) + generate_thumbnail( + source, thumb_path, self.settings['thumb_size'], + fit=self.settings['thumb_fit'], + quality=self.settings['jpg_options']['quality']) ctx['albums'].append({ 'url': d + '/' + self.url_ext, diff --git a/tests/test_image.py b/tests/test_image.py index 17194cd..9529bb6 100644 --- a/tests/test_image.py +++ b/tests/test_image.py @@ -1,35 +1,18 @@ # -*- coding:utf-8 -*- import os -from tempfile import mkdtemp -from shutil import rmtree -try: - import unittest2 as unittest -except ImportError: - import unittest # NOQA - -from sigal.image import Image +from sigal.image import generate_image, generate_thumbnail CURRENT_DIR = os.path.dirname(__file__) TEST_IMAGE = 'exo20101028-b-full.jpg' -class TestImage(unittest.TestCase): +def test_image(tmpdir): "Test the Image class." - def setUp(self): - self.temp_path = mkdtemp() - self.srcfile = os.path.join(CURRENT_DIR, 'sample', 'dir2', TEST_IMAGE) - self.dstfile = os.path.join(self.temp_path, TEST_IMAGE) - self.img = Image(self.srcfile) + srcfile = os.path.join(CURRENT_DIR, 'sample', 'dir2', TEST_IMAGE) + dstfile = str(tmpdir.join(TEST_IMAGE)) - def tearDown(self): - rmtree(self.temp_path) - - def test_imgname(self): - self.assertEqual(self.img.imgname, TEST_IMAGE) - - def test_save(self): - self.img.save(self.dstfile) - self.assertTrue(os.path.isfile(self.dstfile)) + generate_thumbnail(srcfile, dstfile, (200, 150)) + assert os.path.isfile(dstfile)