From 05026951f1658a7e379a226b852ea79def92c641 Mon Sep 17 00:00:00 2001 From: jamie Date: Fri, 29 May 2020 12:43:20 +0100 Subject: [PATCH 1/5] Add config option for PIL's MAX_IMAGE_PIXELS --- doc/configuration.md | 2 ++ etc/loris2.conf | 5 +++++ loris/webapp.py | 11 ++++++++--- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/doc/configuration.md b/doc/configuration.md index 7d8621e6..c5c82856 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -54,6 +54,8 @@ Any options you add here will be passed through to the resolver you implement. F Probably safe to leave these as-is unless you care about something very specific. See the [Developer Notes](develop.md#image-transformations) for when this may not be the case. The exceptions are `kdu_expand` and `kdu_libs` in the `[transforms.jp2]` (see [Installing Dependencies](dependencies.md) step 2) or if you're not concerned about color profiles (see next). +If you are seeing `DecompressionBombError`s thrown for very large source images, try setting the `pil_max_image_pixels` property here. This error occurs for images that are larger than 2x PIL's MAX_IMAGE_PIXELS property, which is `1024 * 1024 * 1024 // 4 // 3 = 89478485` by default (and so the error is thrown for images larger than `2 * 89478485 = 178956970` pixels). + ### map_profile_to_srgb You can tell Loris to map embedded color profiles to sRGB with the following settings in your transformer: diff --git a/etc/loris2.conf b/etc/loris2.conf index ca73aa97..159a20ca 100644 --- a/etc/loris2.conf +++ b/etc/loris2.conf @@ -107,6 +107,11 @@ dither_bitonal_images = False # To enable TIFF output, add "tif" here: target_formats = ['jpg','png','gif','webp'] +# By default PIL throws a DecompressionBombError for images that are larger than +# 2x its MAX_IMAGE_PIXELS property (this limit is 2 * 89478485 = 178956970px). +# This property can be overridden by this config value, eg: +# pil_max_image_pixels = 250000000 + [[jpg]] impl = 'JPG_Transformer' diff --git a/loris/webapp.py b/loris/webapp.py index 54f2db1a..e01470ed 100755 --- a/loris/webapp.py +++ b/loris/webapp.py @@ -19,6 +19,7 @@ sys.path.append('.') from configobj import ConfigObj +from PIL import Image from werkzeug.http import parse_date, http_date from werkzeug.wrappers import ( @@ -379,13 +380,17 @@ def _load_transformers(self): tforms = self.app_configs['transforms'] source_formats = [k for k in tforms if isinstance(tforms[k], dict)] self.logger.debug('Source formats: %r', source_formats) - global_tranform_options = dict((k, v) for k, v in tforms.items() if not isinstance(v, dict)) - self.logger.debug('Global transform options: %r', global_tranform_options) + global_transform_options = dict((k, v) for k, v in tforms.items() if not isinstance(v, dict)) + self.logger.debug('Global transform options: %r', global_transform_options) + + pil_max_image_pixels = tforms.get('pil_max_image_pixels', Image.MAX_IMAGE_PIXELS) + Image.MAX_IMAGE_PIXELS = pil_max_image_pixels + self.logger.debug('PIL maximum image pixels set to: %d', pil_max_image_pixels) transformers = {} for sf in source_formats: # merge [transforms] options and [transforms][source_format]] options - config = dict(list(self.app_configs['transforms'][sf].items()) + list(global_tranform_options.items())) + config = dict(list(self.app_configs['transforms'][sf].items()) + list(global_transform_options.items())) transformers[sf] = self._load_transformer(config) return transformers From a4c389eb96b75e07aa78d44ddb8d3588daf1339c Mon Sep 17 00:00:00 2001 From: jamie Date: Fri, 29 May 2020 13:51:47 +0100 Subject: [PATCH 2/5] Add a note about DecompressionBombWarnings --- doc/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/configuration.md b/doc/configuration.md index c5c82856..9d6b6940 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -54,7 +54,7 @@ Any options you add here will be passed through to the resolver you implement. F Probably safe to leave these as-is unless you care about something very specific. See the [Developer Notes](develop.md#image-transformations) for when this may not be the case. The exceptions are `kdu_expand` and `kdu_libs` in the `[transforms.jp2]` (see [Installing Dependencies](dependencies.md) step 2) or if you're not concerned about color profiles (see next). -If you are seeing `DecompressionBombError`s thrown for very large source images, try setting the `pil_max_image_pixels` property here. This error occurs for images that are larger than 2x PIL's MAX_IMAGE_PIXELS property, which is `1024 * 1024 * 1024 // 4 // 3 = 89478485` by default (and so the error is thrown for images larger than `2 * 89478485 = 178956970` pixels). +If you are seeing `DecompressionBombError`s thrown for very large source images, try setting the `pil_max_image_pixels` property here. This error occurs for images that are larger than **2x** PIL's MAX_IMAGE_PIXELS property, which is `1024 * 1024 * 1024 // 4 // 3 = 89478485` by default (and so the error is thrown for images larger than `2 * 89478485 = 178956970` pixels). Note that PIL will still log a `DecompressionBombWarning` will still be logged if the image is bigger than **1x** the configured value. ### map_profile_to_srgb From 08a53df93a373b942721798e93d2c2055d9f951c Mon Sep 17 00:00:00 2001 From: jamie Date: Fri, 29 May 2020 14:50:28 +0100 Subject: [PATCH 3/5] Add test for pil_max_image_pixels --- tests/transforms_t.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/transforms_t.py b/tests/transforms_t.py index 4339961f..ed2e4da2 100644 --- a/tests/transforms_t.py +++ b/tests/transforms_t.py @@ -2,6 +2,7 @@ import operator import pytest +from PIL.Image import DecompressionBombError from loris import transforms from loris.loris_exception import ConfigError @@ -344,3 +345,11 @@ def test_can_transform_transparent_png_as_nontransparent_format(self): ident = 'png_with_transparency.png' request_path = '/%s/full/full/0/default.jpg' % ident self.request_image_from_client(request_path) + + def test_respects_pil_max_image_pixels(self): + config = get_debug_config('kdu') + config['transforms']['pil_max_image_pixels'] = 1 + self.build_client_from_config(config) + with pytest.raises(DecompressionBombError): + request_path = '/%s/full/300,300/0/default.jpg' % self.ident + self.request_image_from_client(request_path) From 42e75d1abdb8209408c9cab46d18551dc335ac7f Mon Sep 17 00:00:00 2001 From: jamie Date: Fri, 29 May 2020 15:10:01 +0100 Subject: [PATCH 4/5] Reset MAX_IMAGE_PIXELS after test --- tests/transforms_t.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/tests/transforms_t.py b/tests/transforms_t.py index ed2e4da2..03a2c97c 100644 --- a/tests/transforms_t.py +++ b/tests/transforms_t.py @@ -2,7 +2,7 @@ import operator import pytest -from PIL.Image import DecompressionBombError +from PIL import Image from loris import transforms from loris.loris_exception import ConfigError @@ -347,9 +347,15 @@ def test_can_transform_transparent_png_as_nontransparent_format(self): self.request_image_from_client(request_path) def test_respects_pil_max_image_pixels(self): - config = get_debug_config('kdu') - config['transforms']['pil_max_image_pixels'] = 1 - self.build_client_from_config(config) - with pytest.raises(DecompressionBombError): - request_path = '/%s/full/300,300/0/default.jpg' % self.ident - self.request_image_from_client(request_path) + default_max_pixels = Image.MAX_IMAGE_PIXELS + try: + config = get_debug_config('kdu') + config['transforms']['pil_max_image_pixels'] = 1 + self.build_client_from_config(config) + with pytest.raises(Image.DecompressionBombError): + request_path = '/%s/full/300,300/0/default.jpg' % self.ident + self.request_image_from_client(request_path) + finally: + # Because we have to mutate a value in an external library + # it must be reset at the end of this test + Image.MAX_IMAGE_PIXELS = default_max_pixels From 0a1c02094e7617ffd2ab536cc79d9f2e716abcf0 Mon Sep 17 00:00:00 2001 From: jamie Date: Fri, 29 May 2020 15:15:12 +0100 Subject: [PATCH 5/5] Allow removing MAX_IMAGE_PIXELS limit entirely --- doc/configuration.md | 2 ++ etc/loris2.conf | 3 ++- loris/webapp.py | 8 ++++++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/doc/configuration.md b/doc/configuration.md index 9d6b6940..7a350996 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -56,6 +56,8 @@ Probably safe to leave these as-is unless you care about something very specific If you are seeing `DecompressionBombError`s thrown for very large source images, try setting the `pil_max_image_pixels` property here. This error occurs for images that are larger than **2x** PIL's MAX_IMAGE_PIXELS property, which is `1024 * 1024 * 1024 // 4 // 3 = 89478485` by default (and so the error is thrown for images larger than `2 * 89478485 = 178956970` pixels). Note that PIL will still log a `DecompressionBombWarning` will still be logged if the image is bigger than **1x** the configured value. +If `pil_max_image_pixels` is set to `0`, `PIL.Image.MAX_IMAGE_PIXELS` is set to `None` and there is no limit on image size. + ### map_profile_to_srgb You can tell Loris to map embedded color profiles to sRGB with the following settings in your transformer: diff --git a/etc/loris2.conf b/etc/loris2.conf index 159a20ca..cd5d2222 100644 --- a/etc/loris2.conf +++ b/etc/loris2.conf @@ -109,7 +109,8 @@ target_formats = ['jpg','png','gif','webp'] # By default PIL throws a DecompressionBombError for images that are larger than # 2x its MAX_IMAGE_PIXELS property (this limit is 2 * 89478485 = 178956970px). -# This property can be overridden by this config value, eg: +# This property can be overridden by this config value. If set to 0, MAX_IMAGE_PIXELS +# is set to `None` and there is no limit on image size. # pil_max_image_pixels = 250000000 [[jpg]] diff --git a/loris/webapp.py b/loris/webapp.py index e01470ed..9a7d17a5 100755 --- a/loris/webapp.py +++ b/loris/webapp.py @@ -384,8 +384,12 @@ def _load_transformers(self): self.logger.debug('Global transform options: %r', global_transform_options) pil_max_image_pixels = tforms.get('pil_max_image_pixels', Image.MAX_IMAGE_PIXELS) - Image.MAX_IMAGE_PIXELS = pil_max_image_pixels - self.logger.debug('PIL maximum image pixels set to: %d', pil_max_image_pixels) + if pil_max_image_pixels != 0: + Image.MAX_IMAGE_PIXELS = pil_max_image_pixels + self.logger.debug('PIL maximum image pixels set to: %d', pil_max_image_pixels) + else: + Image.MAX_IMAGE_PIXELS = None + self.logger.debug('PIL maximum image pixels limit removed.') transformers = {} for sf in source_formats: