From 0ccfe9568f0c013798bc9b97dd4cfc47ea5e9b22 Mon Sep 17 00:00:00 2001
From: Fidel Ramos <f@fidelramos.net>
Date: Tue, 10 Mar 2020 23:15:41 +0000
Subject: [PATCH] Fix image resizing failing on slim images

Image operations sometimes calculate a target width or height of zero, which
make Willow raise a ValueError.

If an user uploads one such image it's possible to break the whole Wagtail
image manager/picker/uploader for all users.

The fix is to use a minimum of 1 pixel for either the target height or the
width. The image might lose some aspect ratio, but it's better than an
exception.
---
 CHANGELOG.txt                                 |  1 +
 docs/releases/2.9.rst                         |  1 +
 wagtail/images/image_operations.py            | 12 ++++++++++
 wagtail/images/tests/test_image_operations.py | 24 +++++++++++++++++++
 4 files changed, 38 insertions(+)

diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index 489712a1ba..f8aa20d6d6 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -30,6 +30,7 @@ Changelog
  * Fix: `{% image ... as var %}` now clears the context variable when passed None as an image (Maylon Pedroso)
  * Fix: `refresh_index` method on Elasticsearch no longer fails (Lars van de Kerkhof)
  * Fix: Document tags no longer fail to update when replacing the document file at the same time (Matt Westcott)
+ * Fix: Prevent error from very tall / wide images being resized to 0 pixels (Fidel Ramos)
 
 
 2.8 (03.02.2020)
diff --git a/docs/releases/2.9.rst b/docs/releases/2.9.rst
index b05d344480..c32ab8d287 100644
--- a/docs/releases/2.9.rst
+++ b/docs/releases/2.9.rst
@@ -48,6 +48,7 @@ Bug fixes
  * ``{% image ... as var %}`` now clears the context variable when passed None as an image (Maylon Pedroso)
  * ``refresh_index`` method on Elasticsearch no longer fails (Lars van de Kerkhof)
  * Document tags no longer fail to update when replacing the document file at the same time (Matt Westcott)
+ * Prevent error from very tall / wide images being resized to 0 pixels (Fidel Ramos)
 
 
 Upgrade considerations
diff --git a/wagtail/images/image_operations.py b/wagtail/images/image_operations.py
index 9eb955888a..1996b8cd77 100644
--- a/wagtail/images/image_operations.py
+++ b/wagtail/images/image_operations.py
@@ -182,6 +182,10 @@ class MinMaxOperation(Operation):
             # Unknown method
             return
 
+        # prevent zero width or height, it causes a ValueError on willow.resize
+        width = width if width > 0 else 1
+        height = height if height > 0 else 1
+
         return willow.resize((width, height))
 
 
@@ -214,6 +218,10 @@ class WidthHeightOperation(Operation):
             # Unknown method
             return
 
+        # prevent zero width or height, it causes a ValueError on willow.resize
+        width = width if width > 0 else 1
+        height = height if height > 0 else 1
+
         return willow.resize((width, height))
 
 
@@ -228,6 +236,10 @@ class ScaleOperation(Operation):
         width = int(image_width * scale)
         height = int(image_height * scale)
 
+        # prevent zero width or height, it causes a ValueError on willow.resize
+        width = width if width > 0 else 1
+        height = height if height > 0 else 1
+
         return willow.resize((width, height))
 
 
diff --git a/wagtail/images/tests/test_image_operations.py b/wagtail/images/tests/test_image_operations.py
index ac82747073..01694e31d3 100644
--- a/wagtail/images/tests/test_image_operations.py
+++ b/wagtail/images/tests/test_image_operations.py
@@ -361,6 +361,14 @@ class TestMinMaxOperation(ImageOperationTestCase):
         ('max-800x600', dict(width=1000, height=1000), [
             ('resize', ((600, 600), ), {}),
         ]),
+        # Resize doesn't try to set zero height
+        ('max-400x400', dict(width=1000, height=1), [
+            ('resize', ((400, 1), ), {}),
+        ]),
+        # Resize doesn't try to set zero width
+        ('max-400x400', dict(width=1, height=1000), [
+            ('resize', ((1, 400), ), {}),
+        ]),
     ]
 
 
@@ -391,6 +399,14 @@ class TestWidthHeightOperation(ImageOperationTestCase):
         ('height-400', dict(width=1000, height=500), [
             ('resize', ((800, 400), ), {}),
         ]),
+        # Resize doesn't try to set zero height
+        ('width-400', dict(width=1000, height=1), [
+            ('resize', ((400, 1), ), {}),
+        ]),
+        # Resize doesn't try to set zero width
+        ('height-400', dict(width=1, height=800), [
+            ('resize', ((1, 400), ), {}),
+        ]),
     ]
 
 
@@ -425,6 +441,14 @@ class TestScaleOperation(ImageOperationTestCase):
         ('scale-83.0322', dict(width=1000, height=500), [
             ('resize', ((int(1000 * 0.830322), int(500 * 0.830322)), ), {}),
         ]),
+        # Resize doesn't try to set zero height
+        ('scale-50', dict(width=1000, height=1), [
+            ('resize', ((500, 1), ), {}),
+        ]),
+        # Resize doesn't try to set zero width
+        ('scale-50', dict(width=1, height=500), [
+            ('resize', ((1, 250), ), {}),
+        ]),
     ]