diff --git a/docs/topics/images.rst b/docs/topics/images.rst index 2eeb9dff22..bf4b30a561 100644 --- a/docs/topics/images.rst +++ b/docs/topics/images.rst @@ -253,6 +253,25 @@ For example, to make the tag always convert the image to a JPEG, use ``format-jp You may also use ``format-png`` or ``format-gif``. +.. _image_background_color + +Background color +---------------- + +The PNG and GIF image formats both support transparency, but if you want to +convert images to JPEG format, the transparency will need to be replaced with a +solid background color. + +By default, Wagtail will set the background to white. But if a white background +doesn't fit your design, you can specify a color using the ``bgcolor`` filter. + +This filter takes a single argument, which is a CSS 3 or 6 digit hex code +representing the color you would like to use: + +.. code-block:: html+Django + + {# Set the image backgrounds to black #} + {% image page.photo width-400 bgcolor-000 format-jpeg %} .. _jpeg_image_quality: diff --git a/setup.py b/setup.py index 37a0c79701..87f4e1da89 100755 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ install_requires = [ "beautifulsoup4>=4.5.1,<5.0", "html5lib>=0.999,<1", "Unidecode>=0.04.14,<1.0", - "Willow>=1.0,<1.1", + "Willow>=1.1,<1.2", "requests>=2.11.1,<3.0", ] diff --git a/wagtail/images/image_operations.py b/wagtail/images/image_operations.py index f7e7d615f9..5cd49e0615 100644 --- a/wagtail/images/image_operations.py +++ b/wagtail/images/image_operations.py @@ -2,6 +2,7 @@ import inspect from wagtail.images.exceptions import InvalidFilterSpecError from wagtail.images.rect import Rect +from wagtail.images.utils import parse_color_string class Operation: @@ -236,3 +237,11 @@ class FormatOperation(Operation): def run(self, willow, image, env): env['output-format'] = self.format + + +class BackgroundColorOperation(Operation): + def construct(self, color_string): + self.color = parse_color_string(color_string) + + def run(self, willow, image, env): + return willow.set_background_color_rgb(self.color) diff --git a/wagtail/images/models.py b/wagtail/images/models.py index f4eaeaf53c..79571e2304 100644 --- a/wagtail/images/models.py +++ b/wagtail/images/models.py @@ -395,6 +395,10 @@ class Filter: else: quality = 85 + # If the image has an alpha channel, give it a white background + if willow.has_alpha(): + willow = willow.set_background_color_rgb((255, 255, 255)) + return willow.save_as_jpeg(output, quality=quality, progressive=True, optimize=True) elif output_format == 'png': return willow.save_as_png(output) diff --git a/wagtail/images/tests/test_image_operations.py b/wagtail/images/tests/test_image_operations.py index 368c8001fe..da131b69f1 100644 --- a/wagtail/images/tests/test_image_operations.py +++ b/wagtail/images/tests/test_image_operations.py @@ -544,3 +544,44 @@ class TestJPEGQualityFilter(TestCase): fil.run(image, f) save.assert_called_with(f, 'JPEG', quality=40, optimize=True, progressive=True) + + +class TestBackgroundColorFilter(TestCase): + def test_original_has_alpha(self): + # Checks that the test image we're using has alpha + fil = Filter(spec='width-400') + image = Image.objects.create( + title="Test image", + file=get_test_image_file(), + ) + out = fil.run(image, BytesIO()) + + self.assertTrue(out.has_alpha()) + + def test_3_digit_hex(self): + fil = Filter(spec='width-400|bgcolor-fff') + image = Image.objects.create( + title="Test image", + file=get_test_image_file(), + ) + out = fil.run(image, BytesIO()) + + self.assertFalse(out.has_alpha()) + + def test_6_digit_hex(self): + fil = Filter(spec='width-400|bgcolor-ffffff') + image = Image.objects.create( + title="Test image", + file=get_test_image_file(), + ) + out = fil.run(image, BytesIO()) + + self.assertFalse(out.has_alpha()) + + def test_invalid(self): + fil = Filter(spec='width-400|bgcolor-foo') + image = Image.objects.create( + title="Test image", + file=get_test_image_file(), + ) + self.assertRaises(ValueError, fil.run, image, BytesIO()) diff --git a/wagtail/images/tests/utils.py b/wagtail/images/tests/utils.py index fd29d3f8b3..885d010019 100644 --- a/wagtail/images/tests/utils.py +++ b/wagtail/images/tests/utils.py @@ -10,7 +10,7 @@ Image = get_image_model() def get_test_image_file(filename='test.png', colour='white', size=(640, 480)): f = BytesIO() - image = PIL.Image.new('RGB', size, colour) + image = PIL.Image.new('RGBA', size, colour) image.save(f, 'PNG') return ImageFile(f, name=filename) diff --git a/wagtail/images/utils.py b/wagtail/images/utils.py index 3b0416e8a4..69db64f74f 100644 --- a/wagtail/images/utils.py +++ b/wagtail/images/utils.py @@ -37,3 +37,26 @@ def get_fill_filter_spec_migrations(app_name, rendition_model_name): Rendition.objects.using(db_alias).filter(filter_spec=filter_spec).update(filter=filter) return (fill_filter_spec_forward, fill_filter_spec_reverse) + + +def parse_color_string(color_string): + """ + Parses a string a user typed into a tuple of 3 integers representing the + red, green and blue channels respectively. + + May raise a ValueError if the string cannot be parsed. + + The colour string must be a CSS 3 or 6 digit hex code without the '#' prefix. + """ + if len(color_string) == 3: + r = int(color_string[0], 16) * 17 + g = int(color_string[1], 16) * 17 + b = int(color_string[2], 16) * 17 + elif len(color_string) == 6: + r = int(color_string[0:2], 16) + g = int(color_string[2:4], 16) + b = int(color_string[4:6], 16) + else: + ValueError('Color string must be either 3 or 6 hexadecimal digits long') + + return r, g, b diff --git a/wagtail/images/wagtail_hooks.py b/wagtail/images/wagtail_hooks.py index d1329af3cc..3ffc9847a8 100644 --- a/wagtail/images/wagtail_hooks.py +++ b/wagtail/images/wagtail_hooks.py @@ -86,6 +86,7 @@ def register_image_operations(): ('height', image_operations.WidthHeightOperation), ('jpegquality', image_operations.JPEGQualityOperation), ('format', image_operations.FormatOperation), + ('bgcolor', image_operations.BackgroundColorOperation), ]