diff --git a/docs/changelog.rst b/docs/changelog.rst new file mode 100644 index 0000000..faab49f --- /dev/null +++ b/docs/changelog.rst @@ -0,0 +1,28 @@ +Changelog +========= + +v1.0.2 +------ + +- Added this changelog. + +- Enhanced extension detection, format detection, and conversion between the + two. This eliminates the reliance on an image being loaded into memory + beforehand in order to detect said image's extension. + +- Fixed a regression from the 0.4.x series in which ImageKit was unable to + convert a PNG file in ``P`` or "palette" mode to JPEG. + +v1.0.1 +------ + +- Minor fixes related to the rendering of ``README.rst`` as a reStructured + text file. + +- Fixed the included admin template not being found when ImageKit was and + the packaging of the included admin templates. + +v1.0 +---- + +- Initial release of the *new* field-based ImageKit API. diff --git a/docs/conf.py b/docs/conf.py index 4005268..b8fd9cb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,9 +48,9 @@ copyright = u'2011, Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett & # built documents. # # The short X.Y version. -version = '1.0.1' +version = '1.0.2' # The full version, including alpha/beta/rc tags. -release = '1.0.1' +release = '1.0.2' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/index.rst b/docs/index.rst index 01916fd..5e78c02 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -22,6 +22,7 @@ Digging Deeper .. toctree:: apireference + changelog Indices and tables diff --git a/imagekit/__init__.py b/imagekit/__init__.py index 93b5287..ef5b0cb 100644 --- a/imagekit/__init__.py +++ b/imagekit/__init__.py @@ -1,4 +1,4 @@ __title__ = 'django-imagekit' __author__ = 'Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett, Matthew Tretter, Eric Eldredge' -__version__ = (1, 0, 1, 'final', 0) +__version__ = (1, 0, 2, 'final', 0) __license__ = 'BSD' diff --git a/imagekit/models.py b/imagekit/models.py index 435d58b..e5ed1bb 100755 --- a/imagekit/models.py +++ b/imagekit/models.py @@ -10,7 +10,9 @@ from django.db.models.signals import post_save, post_delete from django.utils.encoding import force_unicode, smart_str from imagekit.lib import Image, ImageFile -from imagekit.utils import img_to_fobj, get_spec_files, open_image +from imagekit.utils import img_to_fobj, get_spec_files, open_image, \ + format_to_extension, extension_to_format, UnknownFormatError, \ + UnknownExtensionError from imagekit.processors import ProcessorPipeline # Modify image file buffer size. @@ -95,24 +97,26 @@ class ImageSpec(_ImageSpecMixin): dispatch_uid='%s.delete' % uid) -def get_registered_extensions(): - Image.preinit() - return Image.EXTENSION - - def _get_suggested_extension(name, format): - if format: - # Try to look up an extension by the format. - extensions = [k for k, v in get_registered_extensions().iteritems() \ - if v == format.upper()] - else: - extensions = [] original_extension = os.path.splitext(name)[1] - if not extensions or original_extension.lower() in extensions: - # If the original extension matches the format, use it. + try: + suggested_extension = format_to_extension(format) + except UnknownFormatError: extension = original_extension else: - extension = extensions[0] + if suggested_extension.lower() == original_extension.lower(): + extension = original_extension + else: + try: + original_format = extension_to_format(original_extension) + except UnknownExtensionError: + extension = suggested_extension + else: + # If the formats match, give precedence to the original extension. + if format.lower() == original_format.lower(): + extension = original_extension + else: + extension = suggested_extension return extension @@ -129,7 +133,10 @@ class _ImageSpecFileMixin(object): # The extension is explicit, so assume they want the matching format. extension = os.path.splitext(filename)[1].lower() # Try to guess the format from the extension. - format = get_registered_extensions().get(extension) + try: + format = extension_to_format(extension) + except UnknownExtensionError: + pass format = format or img.format or original_format or 'JPEG' if format != 'JPEG': diff --git a/imagekit/utils.py b/imagekit/utils.py index 637433e..b6ca548 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -10,10 +10,11 @@ def img_to_fobj(img, format, **kwargs): tmp = tempfile.TemporaryFile() # Preserve transparency if the image is in Pallette (P) mode. - if img.mode == 'P': + transparency_formats = ('PNG', 'GIF', ) + if img.mode == 'P' and format in transparency_formats: kwargs['transparency'] = len(img.split()[-1].getcolors()) else: - img.convert('RGB') + img = img.convert('RGB') img.save(tmp, format, **kwargs) tmp.seek(0) @@ -49,3 +50,82 @@ def _wrap_copy(f): pass return img return copy + + +class UnknownExtensionError(Exception): + pass + + +class UnknownFormatError(Exception): + pass + + +_pil_init = 0 + + +def _preinit_pil(): + """Loads the standard PIL file format drivers. Returns True if ``preinit()`` + was called (and there's a potential that more drivers were loaded) or False + if there is no possibility that new drivers were loaded. + + """ + global _pil_init + if _pil_init < 1: + Image.preinit() + _pil_init = 1 + return True + return False + + +def _init_pil(): + """Loads all PIL file format drivers. Returns True if ``init()`` was called + (and there's a potential that more drivers were loaded) or False if there is + no possibility that new drivers were loaded. + + """ + global _pil_init + _preinit_pil() + if _pil_init < 2: + Image.init() + _pil_init = 2 + return True + return False + + +def _extension_to_format(extension): + return Image.EXTENSION.get(extension.lower()) + + +def _format_to_extension(format): + for k, v in Image.EXTENSION.iteritems(): + if v == format.upper(): + return k + return None + + +def extension_to_format(extension): + """Returns the format that corresponds to the provided extension. + + """ + format = _extension_to_format(extension) + if not format and _preinit_pil(): + format = _extension_to_format(extension) + if not format and _init_pil(): + format = _extension_to_format(extension) + if not format: + raise UnknownExtensionError(extension) + return format + + +def format_to_extension(format): + """Returns the first extension that matches the provided format. + + """ + extension = _format_to_extension(format) + if not extension and _preinit_pil(): + extension = _format_to_extension(format) + if not extension and _init_pil(): + extension = _format_to_extension(format) + if not extension: + raise UnknownFormatError(format) + return extension diff --git a/tests/core/tests.py b/tests/core/tests.py index dd83fce..36d7ae7 100644 --- a/tests/core/tests.py +++ b/tests/core/tests.py @@ -5,6 +5,7 @@ from django.core.files.base import ContentFile from django.db import models from django.test import TestCase +from imagekit import utils from imagekit.lib import Image from imagekit.models import ImageSpec from imagekit.processors import Adjust @@ -18,7 +19,6 @@ class Photo(models.Model): class IKTest(TestCase): - """ Base TestCase class. """ def generate_image(self): tmp = tempfile.TemporaryFile() Image.new('RGB', (800, 600)).save(tmp, 'JPEG') @@ -53,3 +53,19 @@ class IKTest(TestCase): def test_thumbnail_source_file(self): self.assertEqual( self.photo.thumbnail.source_file, self.photo.original_image) + + +class IKUtilsTest(TestCase): + def test_extension_to_format(self): + self.assertEqual(utils.extension_to_format('.jpeg'), 'JPEG') + self.assertEqual(utils.extension_to_format('.rgba'), 'SGI') + + with self.assertRaises(utils.UnknownExtensionError): + utils.extension_to_format('.txt') + + def test_format_to_extension_no_init(self): + self.assertEqual(utils.format_to_extension('PNG'), '.png') + self.assertEqual(utils.format_to_extension('ICO'), '.ico') + + with self.assertRaises(utils.UnknownFormatError): + utils.format_to_extension('TXT')