From 933bf01dabe0c2383534e7b61b73e21d9540e870 Mon Sep 17 00:00:00 2001 From: Simon Date: Thu, 28 Feb 2013 00:06:30 +0100 Subject: [PATCH 1/4] Use save_image from pilkit to avoid reinventing the wheel. --- requirements.txt | 1 + setup.py | 2 +- sigal/gallery.py | 4 ++-- sigal/image.py | 46 +++++++--------------------------------------- 4 files changed, 11 insertions(+), 42 deletions(-) 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..ca4a6bf 100644 --- a/sigal/gallery.py +++ b/sigal/gallery.py @@ -244,14 +244,14 @@ def process_image(filepath, outpath, settings): if settings['keep_orig']: img.save(join(outpath, settings['orig_dir'], filename), - **settings['jpg_options']) + options=settings['jpg_options']) img.resize(settings['img_size']) if settings['copyright']: img.add_copyright(settings['copyright']) - img.save(outname, **settings['jpg_options']) + img.save(outname, options=settings['jpg_options']) if settings['make_thumbs']: thumb_name = join(outpath, get_thumb(settings, filename)) diff --git a/sigal/image.py b/sigal/image.py index 70ebd4f..486bb68 100644 --- a/sigal/image.py +++ b/sigal/image.py @@ -24,10 +24,9 @@ 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 +from PIL import ImageDraw, ImageOps +from pilkit.utils import save_image # EXIF specs Orientation constant EXIF_ORIENTATION_TAG = 274 @@ -47,10 +46,7 @@ class Image(object): self.filename = filename self.imgname = os.path.split(filename)[1] self.logger = logging.getLogger(__name__) - - with open(filename, 'rb') as fp: - self.img = PILImage.open(fp) - self.img.load() + self.img = PILImage.open(filename) # Try to read exif metadata. This can fail if: # - the image does not have exif (png files) -> AttributeError @@ -68,25 +64,14 @@ class Image(object): if rotation: self.img = self.img.rotate(rotation) - def save(self, filename, **kwargs): + def save(self, filename, format='JPEG', options=None, autoconvert=True): """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 + Pass a dict with PIL options (quality, optimize, progressive). """ - 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 + save_image(self.img, filename, format, options=options, + autoconvert=autoconvert) def resize(self, size): """Resize the image. @@ -128,20 +113,3 @@ class Image(object): self.img.thumbnail(box, PILImage.ANTIALIAS) self.img.save(filename, quality=quality) - - -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 __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) - - def __exit__(self, *args, **kwargs): - os.dup2(self.old, self.stderr_fd) - os.close(self.null_fd) - os.close(self.old) From 692a0362377cf11f3601a5e093c27ec60bc12ad2 Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 1 Mar 2013 00:59:04 +0100 Subject: [PATCH 2/4] Move to pilkit. Replace the custom code in the image module by pilkit generators. --- sigal/gallery.py | 36 +++++++-------- sigal/image.py | 108 +++++++++++++++----------------------------- sigal/writer.py | 11 +++-- tests/test_image.py | 29 +++--------- 4 files changed, 66 insertions(+), 118 deletions(-) diff --git a/sigal/gallery.py b/sigal/gallery.py index ca4a6bf..4db8bcc 100644 --- a/sigal/gallery.py +++ b/sigal/gallery.py @@ -33,7 +33,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 +211,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 +238,27 @@ 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), - options=settings['jpg_options']) + # TODO + # if settings['keep_orig']: + # img.save(join(outpath, settings['orig_dir'], filename), + # options=settings['jpg_options']) - img.resize(settings['img_size']) - - if settings['copyright']: - img.add_copyright(settings['copyright']) - - img.save(outname, options=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 +267,8 @@ def get_metadata(path): - title - thumbnail image - description - """ + """ descfile = join(path, DESCRIPTION_FILE) if not os.path.isfile(descfile): @@ -279,11 +280,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 486bb68..3e9a67d 100644 --- a/sigal/image.py +++ b/sigal/image.py @@ -20,96 +20,60 @@ # 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 from PIL import Image as PILImage from PIL import ImageDraw, ImageOps +from pilkit.processors import ProcessorPipeline, Transpose, ResizeToFill from pilkit.utils import save_image -# EXIF specs Orientation constant -EXIF_ORIENTATION_TAG = 274 +def generate_image(source, outname, size, format, options=None, + autoconvert=True, copyright_text=''): + """Image processor, rotate and resize the image. -class Image(object): - """Image container - - 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__) - self.img = PILImage.open(filename) + logger = logging.getLogger(__name__) + img = PILImage.open(source) + original_format = img.format - # 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 processors + processors = [ + Transpose(), # use exif to rotate the img + 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, format='JPEG', options=None, autoconvert=True): - """Save the image. + 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) - Pass a dict with PIL options (quality, optimize, progressive). - """ - save_image(self.img, filename, format, options=options, - autoconvert=autoconvert) +def generate_thumbnail(source, outname, box, format, fit=True, options=None): + "Create a thumbnail image" - def resize(self, size): - """Resize the image. + logger = logging.getLogger(__name__) + img = PILImage.open(source) + original_format = img.format - - 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. + if fit: + img = ImageOps.fit(img, box, PILImage.ANTIALIAS) + else: + img.thumbnail(box, PILImage.ANTIALIAS) - :param size: tuple with the (with, height) to resize + 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) - """ - if self.img.size[0] > self.img.size[1]: - newsize = size - else: - newsize = (size[1], size[0]) +def add_copyright(img, text): + "Add a copyright to the image" - 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) + 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) From 493675a1bfddedabeefd1dc0cb5abe93ee1921e7 Mon Sep 17 00:00:00 2001 From: Simon Conseil Date: Sat, 20 Apr 2013 00:57:42 +0200 Subject: [PATCH 3/4] Copy the original image if needed. --- sigal/gallery.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/sigal/gallery.py b/sigal/gallery.py index 4db8bcc..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 @@ -247,10 +248,8 @@ def process_image(filepath, outpath, settings): else: options = {} - # TODO - # if settings['keep_orig']: - # img.save(join(outpath, settings['orig_dir'], filename), - # options=settings['jpg_options']) + if settings['keep_orig']: + shutil.copy(filepath, join(outpath, settings['orig_dir'], filename)) generate_image(filepath, outname, settings['img_size'], None, options=options, copyright_text=settings['copyright']) From 13207e3fc4cea79ab1711a98b8e98971d1349498 Mon Sep 17 00:00:00 2001 From: Simon Conseil Date: Sun, 21 Apr 2013 00:53:10 +0200 Subject: [PATCH 4/4] Handle exception when PIL fails to read EXIF metadata --- sigal/image.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/sigal/image.py b/sigal/image.py index 3e9a67d..8f0938b 100644 --- a/sigal/image.py +++ b/sigal/image.py @@ -40,11 +40,15 @@ def generate_image(source, outname, size, format, options=None, img = PILImage.open(source) original_format = img.format - # Run the processors - processors = [ - Transpose(), # use exif to rotate the img - ResizeToFill(*size) - ] + # Rotate the img, and catch IOError when PIL fails to read EXIF + try: + processor = Transpose() + img = processor.process(img) + except IOError: + pass + + # Run the other processors + processors = [ResizeToFill(*size)] img = ProcessorPipeline(processors).process(img) if copyright_text: