question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

`max` filter breaks animated gifs

See original GitHub issue

Issue Summary

Uploading a gif (this one in particular): blow-mind-mind-blown breaks wagtail /admin/images route.

The error is fun, this line https://github.com/emcconville/wand/blob/4fe2c6ba9cb0d4105361cea6e9e9e83116080473/wand/image.py#L1501 gets two different values, 421and 420, raising an error in _abs function.

I am not sure whether this comes from Wagtail or Wand, and I would like some help to track this down. Fix : There is a fix for this issue : replacing max by fill here helps get rid of it https://github.com/wagtail/wagtail/blob/main/wagtail/images/templates/wagtailimages/images/results_image.html#L18

Steps to Reproduce

  1. Add Wand to your wagtail project to support animated gifs
  2. In wagtail admin, go to http://localhost:8000/admin/images/multiple/add/ and upload the gif
  3. Go to http://localhost:8000/admin/images/
  4. Enjoy 🍿

I have confirmed that this issue can be reproduced as described on a fresh Wagtail project: yes, on https://github.com/wagtail/docker-wagtail-develop I added Wand as a dependency, and the error occurs.

Traceback below if it can be of any help
Environment:


Request Method: GET
Request URL: http://localhost:8000/admin/images/

Django Version: 3.2.10
Python Version: 3.8.12
Installed Applications:
['bakerydemo.base',
 'bakerydemo.blog',
 'bakerydemo.breads',
 'bakerydemo.locations',
 'bakerydemo.search',
 'wagtail.contrib.search_promotions',
 'wagtail.contrib.forms',
 'wagtail.contrib.redirects',
 'wagtail.embeds',
 'wagtail.sites',
 'wagtail.users',
 'wagtail.snippets',
 'wagtail.documents',
 'wagtail.images',
 'wagtail.search',
 'wagtail.admin',
 'wagtail.api.v2',
 'wagtail.contrib.modeladmin',
 'wagtail.contrib.routable_page',
 'wagtail.core',
 'rest_framework',
 'modelcluster',
 'taggit',
 'wagtailfontawesome',
 'debug_toolbar',
 'django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'django.contrib.sitemaps']
Installed Middleware:
['debug_toolbar.middleware.DebugToolbarMiddleware',
 'django.middleware.security.SecurityMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware',
 'wagtail.contrib.redirects.middleware.RedirectMiddleware']


Template error:
In template /code/wagtail/wagtail/images/templates/wagtailimages/images/results_image.html, error at line 18
   421 > 420
   8 :  This behaviour caused a confusing error on the images listing view where it
   9 :  would go blank if one of the images was invalid.
   10 : 
   11 :  Separating the image rendering code into this file allows us to limit Django's
   12 :  crash/blanking behaviour to a single image so the listing can still be used when
   13 :  the issue occurs.
   14 : {% endcomment %}
   15 : 
   16 : {% load wagtailimages_tags %}
   17 : 
   18 : <div class="image"> {% image image max-165x165 class="show-transparency" alt="" %} </div>
   19 : 

Traceback (most recent call last):
  File "/code/wagtail/wagtail/images/models.py", line 307, in get_rendition
    rendition = self.renditions.get(
  File "/usr/local/lib/python3.8/site-packages/django/db/models/manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/usr/local/lib/python3.8/site-packages/django/db/models/query.py", line 435, in get
    raise self.model.DoesNotExist(

During handling of the above exception (Rendition matching query does not exist.), another exception occurred:
  File "/usr/local/lib/python3.8/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/usr/local/lib/python3.8/site-packages/django/core/handlers/base.py", line 204, in _get_response
    response = response.render()
  File "/usr/local/lib/python3.8/site-packages/django/template/response.py", line 105, in render
    self.content = self.rendered_content
  File "/usr/local/lib/python3.8/site-packages/django/template/response.py", line 83, in rendered_content
    return template.render(context, self._request)
  File "/usr/local/lib/python3.8/site-packages/django/template/backends/django.py", line 61, in render
    return self.template.render(context)
  File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 170, in render
    return self._render(context)
  File "/usr/local/lib/python3.8/site-packages/django/test/utils.py", line 100, in instrumented_test_render
    return self.nodelist.render(context)
  File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 938, in render
    bit = node.render_annotated(context)
  File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 905, in render_annotated
    return self.render(context)
  File "/usr/local/lib/python3.8/site-packages/django/template/loader_tags.py", line 150, in render
    return compiled_parent._render(context)
  File "/usr/local/lib/python3.8/site-packages/django/test/utils.py", line 100, in instrumented_test_render
    return self.nodelist.render(context)
  File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 938, in render
    bit = node.render_annotated(context)
  File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 905, in render_annotated
    return self.render(context)
  File "/usr/local/lib/python3.8/site-packages/django/template/loader_tags.py", line 150, in render
    return compiled_parent._render(context)
  File "/usr/local/lib/python3.8/site-packages/django/test/utils.py", line 100, in instrumented_test_render
    return self.nodelist.render(context)
  File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 938, in render
    bit = node.render_annotated(context)
  File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 905, in render_annotated
    return self.render(context)
  File "/usr/local/lib/python3.8/site-packages/django/template/loader_tags.py", line 150, in render
    return compiled_parent._render(context)
  File "/usr/local/lib/python3.8/site-packages/django/test/utils.py", line 100, in instrumented_test_render
    return self.nodelist.render(context)
  File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 938, in render
    bit = node.render_annotated(context)
  File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 905, in render_annotated
    return self.render(context)
  File "/usr/local/lib/python3.8/site-packages/django/template/loader_tags.py", line 62, in render
    result = block.nodelist.render(context)
  File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 938, in render
    bit = node.render_annotated(context)
  File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 905, in render_annotated
    return self.render(context)
  File "/usr/local/lib/python3.8/site-packages/django/template/loader_tags.py", line 62, in render
    result = block.nodelist.render(context)
  File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 938, in render
    bit = node.render_annotated(context)
  File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 905, in render_annotated
    return self.render(context)
  File "/usr/local/lib/python3.8/site-packages/django/template/loader_tags.py", line 195, in render
    return template.render(context)
  File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 172, in render
    return self._render(context)
  File "/usr/local/lib/python3.8/site-packages/django/test/utils.py", line 100, in instrumented_test_render
    return self.nodelist.render(context)
  File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 938, in render
    bit = node.render_annotated(context)
  File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 905, in render_annotated
    return self.render(context)
  File "/usr/local/lib/python3.8/site-packages/django/template/defaulttags.py", line 312, in render
    return nodelist.render(context)
  File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 938, in render
    bit = node.render_annotated(context)
  File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 905, in render_annotated
    return self.render(context)
  File "/usr/local/lib/python3.8/site-packages/django/template/defaulttags.py", line 211, in render
    nodelist.append(node.render_annotated(context))
  File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 905, in render_annotated
    return self.render(context)
  File "/usr/local/lib/python3.8/site-packages/django/template/loader_tags.py", line 195, in render
    return template.render(context)
  File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 172, in render
    return self._render(context)
  File "/usr/local/lib/python3.8/site-packages/django/test/utils.py", line 100, in instrumented_test_render
    return self.nodelist.render(context)
  File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 938, in render
    bit = node.render_annotated(context)
  File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 905, in render_annotated
    return self.render(context)
  File "/code/wagtail/wagtail/images/templatetags/wagtailimages_tags.py", line 107, in render
    rendition = get_rendition_or_not_found(image, self.filter)
  File "/code/wagtail/wagtail/images/shortcuts.py", line 13, in get_rendition_or_not_found
    return image.get_rendition(specs)
  File "/code/wagtail/wagtail/images/models.py", line 321, in get_rendition
    generated_image = filter.run(self, BytesIO())
  File "/code/wagtail/wagtail/images/models.py", line 476, in run
    willow = willow.crop(transform.get_rect().round())
  File "/usr/local/lib/python3.8/site-packages/willow/plugins/wand.py", line 87, in crop
    clone.image.crop(left=rect[0], top=rect[1], right=rect[2], bottom=rect[3])
  File "/usr/local/lib/python3.8/site-packages/wand/image.py", line 1089, in wrapped
    result = function(self, *args, **kwargs)
  File "/usr/local/lib/python3.8/site-packages/wand/image.py", line 1098, in wrapped
    result = function(self, *args, **kwargs)
  File "/usr/local/lib/python3.8/site-packages/wand/image.py", line 4490, in crop
    bottom = abs_(bottom, self.height)
  File "/usr/local/lib/python3.8/site-packages/wand/image.py", line 4472, in abs_
    raise ValueError(repr(n) + ' > ' + repr(m))

Exception Type: ValueError at /admin/images/
Exception Value: 421 > 420

Technical details

  • Python version: 3.8.12
  • Django version: 3.2.10
  • Wagtail version: 2.15.1

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Reactions:1
  • Comments:12 (6 by maintainers)

github_iconTop GitHub Comments

2reactions
gasmancommented, Feb 25, 2022

Have now released Willow 1.4.1 which fixes this - will open a new lower-priority ticket for addressing the underlying rounding error issue.

2reactions
gasmancommented, Feb 21, 2022

OK, this turns out to be quite hairy…

  • The max-165x165 filter is translated to a MinMaxOperation instance
  • MinMaxOperation.run calculates the appropriate final size for the image, with both dimensions rounded to integers, and passes this to ImageTransform.resize
  • ImageTransform.resize reverses the calculation MinMaxOperation just did to get an X and Y scale factor, which is how ImageTransform represents scale operations internally. (Note: even though this resize operation is supposed to preserve aspect ratio, the integer rounding in the previous step means that the X and Y factors are liable to end up slightly different. I think this detail is a red herring though…)
  • The Filter object managing this whole process then calls willow.crop(transform.get_rect().round()).
  • transform.get_rect() calculates the crop rectangle to apply to the original image, by multiplying the target size by the scale factors. Since the max filter doesn’t do any cropping, this should just return the original image size. However, this unnecessary division and multiplication causes floating point inaccuracies to creep in, and the result is in some cases infinitesimally larger. Taking the original 640x420 image from this issue as input, the resulting rectangle is Rect(left: -0.0, top: -0.0, right: 640.0, bottom: 420.00000000000006)
  • Rect.round() rounds the dimensions up - presumably to avoid zero-sized images - resulting in Rect(left: 0, top: 0, right: 640, bottom: 421)
  • Finally, this is passed to Willow to perform a crop operation on the image. (There’s no check to see if a crop is actually needed, so even if we did get the calculations right, this would be totally redundant…) Pillow is happy to handle a crop rectangle larger than the original image, but Wand (which is used for animated gifs) fails.
Read more comments on GitHub >

github_iconTop Results From Across the Web

Split Animated GIF Images - VEED.IO
Our free online GIF splitter tool lets you split your animated GIF images. ... VEED's GIF splitter lets you break your GIFs into...
Read more >
Animated GIFs in Email: Examples & How To's [Guide] - Litmus
An email marketers guide to animated GIFs in email! Learn about the benefits, examples, how to create them, and email client support.
Read more >
How to create GIFs with FFmpeg - Medium
High-quality animation on any platform for free · Preparation · Convert video to GIF · Convert series of images to GIF · Define...
Read more >
Create a GIF with R - VP Nagraj
With R you can turn a collection of images into an animated GIF. That can be useful for animating plots or for converting...
Read more >
How to Add Animated GIFs to Your Emails - Mailchimp
Here's how to make the most out of animated GIFs in your email ... Many search engines have advanced filters so that only...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found