diff --git a/README.rst b/README.rst index 5bd0d6e2d..25481554e 100644 --- a/README.rst +++ b/README.rst @@ -132,7 +132,7 @@ You can use the 'get_thumbnail':: See more examples in the section `Low level API examples`_ in the Documentation Using in combination with other thumbnailers --------------------------------------------- +============================================ Alternatively, you load the templatetags by {% load sorl_thumbnail %} instead of traditional {% load thumbnail %}. It's especially useful in diff --git a/docs/template.rst b/docs/template.rst index a4f820826..a840e7207 100644 --- a/docs/template.rst +++ b/docs/template.rst @@ -136,6 +136,12 @@ cropping options if you don't want to generate unnecessary thumbnails. In case you are wondering, sorl-thumbnail sorts the options so the order does not matter, same options but in different order will generate only one thumbnail. +``transform`` +^^^^^^^^^^^^^ +Transform is a boolean and controls if the image will be free transformed to the +dimensions provided. If set to true, the image will be forcibly resized to the +supplied dimensions and stretch as needed. Default value is ``False``. + ``upscale`` ^^^^^^^^^^^ Upscale is a boolean and controls if the image can be upscaled or not. For @@ -190,7 +196,6 @@ Images are not padded by default, but this can be changed by setting This is the color to use for padding the image. It defaults to ``#ffffff`` and can be globally set with the setting ``THUMBNAIL_PADDING_COLOR``. - ``options`` ^^^^^^^^^^^ Yes this option is called ``options``. This needs to be a context variable that diff --git a/sorl/thumbnail/base.py b/sorl/thumbnail/base.py index cc78895c7..fcc2285d8 100644 --- a/sorl/thumbnail/base.py +++ b/sorl/thumbnail/base.py @@ -34,13 +34,14 @@ class ThumbnailBackend(object): 'cropbox': None, 'rounded': None, 'padding': settings.THUMBNAIL_PADDING, - 'padding_color': settings.THUMBNAIL_PADDING_COLOR, + 'padding_color': settings.THUMBNAIL_PADDING_COLOR } extra_options = ( ('progressive', 'THUMBNAIL_PROGRESSIVE'), ('orientation', 'THUMBNAIL_ORIENTATION'), ('blur', 'THUMBNAIL_BLUR'), + ('transform', "THUMBNAIL_TRANSFORM") ) def file_extension(self, source): diff --git a/sorl/thumbnail/conf/defaults.py b/sorl/thumbnail/conf/defaults.py index 5320f85e2..30e197288 100644 --- a/sorl/thumbnail/conf/defaults.py +++ b/sorl/thumbnail/conf/defaults.py @@ -88,6 +88,9 @@ # Orientate the thumbnail with respect to source EXIF orientation tag THUMBNAIL_ORIENTATION = True +# Whether to apply free-transform to the image by default (breaking the aspect ratio lock) +THUMBNAIL_TRANSFORM = False + # This means sorl.thumbnail will generate and serve a generated dummy image # regardless of the thumbnail source content THUMBNAIL_DUMMY = False diff --git a/sorl/thumbnail/engines/base.py b/sorl/thumbnail/engines/base.py index fd4b81d8f..5982182a4 100644 --- a/sorl/thumbnail/engines/base.py +++ b/sorl/thumbnail/engines/base.py @@ -75,7 +75,13 @@ def scale(self, image, geometry, options): Wrapper for ``_scale`` """ upscale = options['upscale'] + transform = options.get('transform', settings.THUMBNAIL_TRANSFORM) x_image, y_image = map(float, self.get_image_size(image)) + + if transform: + image = self._scale(image, geometry[0], geometry[1]) + return image + if self.flip_dimensions(image): x_image, y_image = y_image, x_image factor = self._calculate_scaling_factor(x_image, y_image, geometry, options) @@ -84,7 +90,6 @@ def scale(self, image, geometry, options): width = toint(x_image * factor) height = toint(y_image * factor) image = self._scale(image, width, height) - return image def crop(self, image, geometry, options): diff --git a/tests/thumbnail_tests/test_alternative_resolutions.py b/tests/thumbnail_tests/test_alternative_resolutions.py index 17471aa9b..e25da61e5 100644 --- a/tests/thumbnail_tests/test_alternative_resolutions.py +++ b/tests/thumbnail_tests/test_alternative_resolutions.py @@ -45,7 +45,7 @@ def test_retina(self): # save the 2x resolution version 'save: test/cache/91/bb/91bb06cf9169e4c52132bb113f2d4c0d@2x.jpg', 'get_available_name: test/cache/91/bb/91bb06cf9169e4c52132bb113f2d4c0d@2x.jpg', - 'exists: test/cache/91/bb/91bb06cf9169e4c52132bb113f2d4c0d@2x.jpg' + 'exists: test/cache/91/bb/91bb06cf9169e4c52132bb113f2d4c0d@2x.jpg', ] self.assertEqual(self.log, actions) diff --git a/tests/thumbnail_tests/test_engines.py b/tests/thumbnail_tests/test_engines.py index ed4a02c61..b430ecaa3 100644 --- a/tests/thumbnail_tests/test_engines.py +++ b/tests/thumbnail_tests/test_engines.py @@ -411,6 +411,25 @@ def setUp(self): 'pil_engine' not in settings.THUMBNAIL_ENGINE, 'the other engines fail this test', ) + def test_PIL_freetransform(self): + th = self.BACKEND.get_thumbnail(self.portrait, '100x100', transform=True) + engine = PILEngine() + im = engine.get_image(th) + self.assertEqual(im.width, 100) + self.assertEqual(im.height, 100) + + @unittest.skipIf( + 'convert_engine' not in settings.THUMBNAIL_ENGINE, + 'the other engines fail this test', + ) + def test_convert_engine_freetransform(self): + from sorl.thumbnail.engines.convert_engine import Engine as ConvertEngine + th = self.BACKEND.get_thumbnail(self.portrait, '100x100', transform=True) + engine = ConvertEngine() + im = engine.get_image(th) + size = engine.get_image_size(im) + self.assertEqual(size, (100, 100)) + def PIL_test_portrait_crop(self): def mean_pixel(x, y): values = im.getpixel((x, y)) @@ -490,6 +509,15 @@ def mean_pixel(x, y): 'wand_engine' not in settings.THUMBNAIL_ENGINE, 'the other engines fail this test', ) + + def test_wand_engine_freetransform(self): + from sorl.thumbnail.engines.wand_engine import Engine as WandEngine + th = self.BACKEND.get_thumbnail(self.portrait, '100x100', transform=True) + engine = WandEngine() + im = engine.get_image(th) + self.assertEqual(im.width, 100) + self.assertEqual(im.height, 100) + def wand_test_cropbox(self): from sorl.thumbnail.engines.wand_engine import Engine as WandEngine th = self.BACKEND.get_thumbnail(self.portrait, '100x100', cropbox="0,50,100,150") @@ -504,6 +532,15 @@ def wand_test_cropbox(self): 'pgmagick_engine' not in settings.THUMBNAIL_ENGINE, 'the other engines fail this test', ) + + def test_pgmagick_engine_freetransform(self): + from sorl.thumbnail.engines.pgmagick_engine import Engine as PgmagickEngine + th = self.BACKEND.get_thumbnail(self.portrait, '100x100', transform=True) + engine = PgmagickEngine() + im = engine.get_image(th) + self.assertEqual(im.width(100), 100) + self.assertEqual(im.height(100), 100) + def pgmagick_test_cropbox(self): from sorl.thumbnail.engines.pgmagick_engine import Engine as PgMagickEngine th = self.BACKEND.get_thumbnail(self.portrait, '100x100', cropbox="0,50,100,150") @@ -527,7 +564,6 @@ def convert_test_cropbox(self): # If the crop went well, then it should scale to 100x100 perfectly self.assertEqual(im["size"], (100, 100)) - class DummyTestCase(unittest.TestCase): def setUp(self): self.BACKEND = get_module_class(settings.THUMBNAIL_BACKEND)()