feat: use Willow instead of Pillow for images.

Override all Django code calling Pillow, so that we can
more easily implement SVG support when it lands in
Willow.
pull/9486/head
Darrel O'Pry 2022-08-09 23:07:27 -04:00 zatwierdzone przez Dan Braghis
rodzic 0b4302afe9
commit 912747f6ae
Nie znaleziono w bazie danych klucza dla tego podpisu
7 zmienionych plików z 230 dodań i 20 usunięć

Wyświetl plik

@ -6,6 +6,7 @@ Changelog
* Add custom permissions section to permissions documentation page (Dan Hayden)
* Remove unsquashed `testapp` migrations (Matt Westcott)
* Switch to using Willow instead of Pillow for images (Darrel O'Pry)
* Fix: Make sure workflow timeline icons are visible in high-contrast mode (Loveth Omokaro)
* Fix: Ensure authentication forms (login, password reset) have a visible border in Windows high-contrast mode (Loveth Omokaro)
* Fix: Ensure visual consistency between buttons and links as buttons in Windows high-contrast mode (Albina Starykova)

Wyświetl plik

@ -643,7 +643,7 @@ Contributors
* Abayomi Victory
* Victoria Poromon
* Dokua Asiedu
* Darrel O'Pry
Translators
===========

Wyświetl plik

@ -16,6 +16,7 @@ depth: 1
* Add custom permissions section to permissions documentation page (Dan Hayden)
* Wagtail's documentation (v2.9 to v2.13) has been updated on [Dash user contributions](https://github.com/Kapeli/Dash-User-Contributions) for [Dash](https://kapeli.com/dash) or [Zeal](https://zealdocs.org/) offline docs applications (Damilola Oladele)
* Switch to using [Willow](https://github.com/wagtail/Willow/) instead of Pillow for images (Darrel O'Pry)
### Bug fixes
@ -25,3 +26,8 @@ depth: 1
## Upgrade considerations
### Wagtail-specific image field (`WagtailImageField`)
The `AbstractImage` and `AbstractRendition` models use a Wagtail-specific `WagtailImageField` which extends Django's `ImageField`
to use [Willow](https://github.com/wagtail/Willow/) for image file handling. This will generate a new migration if you
are using a [custom image model](custom_image_model)

Wyświetl plik

@ -1,9 +1,11 @@
import os
from io import BytesIO
import willow
from django.conf import settings
from django.core.exceptions import ValidationError
from django.forms.fields import ImageField
from django.core.validators import FileExtensionValidator
from django.forms.fields import FileField, ImageField
from django.template.defaultfilters import filesizeformat
from django.utils.translation import gettext_lazy as _
@ -12,6 +14,8 @@ SUPPORTED_FORMATS_TEXT = _("GIF, JPEG, PNG, WEBP")
class WagtailImageField(ImageField):
default_validators = [FileExtensionValidator(ALLOWED_EXTENSIONS)]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -43,7 +47,9 @@ class WagtailImageField(ImageField):
% SUPPORTED_FORMATS_TEXT
)
self.error_messages["invalid_image_known_format"] = _("Not a valid %s image.")
self.error_messages["invalid_image_known_format"] = _(
"Not a valid .%s image. The extension does not match the file format (%s)"
)
self.error_messages["file_too_large"] = (
_("This file is too big (%%s). Maximum filesize %s.") % max_upload_size_text
@ -68,19 +74,15 @@ class WagtailImageField(ImageField):
code="invalid_image_extension",
)
image_format = extension.upper()
if image_format == "JPG":
image_format = "JPEG"
internal_image_format = f.image.format.upper()
if internal_image_format == "MPO":
internal_image_format = "JPEG"
if extension == "jpg":
extension = "jpeg"
# Check that the internal format matches the extension
# It is possible to upload PSD files if their extension is set to jpg, png or gif. This should catch them out
if internal_image_format != image_format:
if extension != f.image.format_name:
raise ValidationError(
self.error_messages["invalid_image_known_format"] % (image_format,),
self.error_messages["invalid_image_known_format"]
% (extension, f.image.format_name),
code="invalid_image_known_format",
)
@ -102,9 +104,8 @@ class WagtailImageField(ImageField):
return
# Check the pixel size
image = willow.Image.open(f)
width, height = image.get_size()
frames = image.get_frame_count()
width, height = f.image.get_size()
frames = f.image.get_frame_count()
num_pixels = width * height * frames
if num_pixels > self.max_image_pixels:
@ -114,7 +115,40 @@ class WagtailImageField(ImageField):
)
def to_python(self, data):
f = super().to_python(data)
"""
Check that the file-upload field data contains a valid image (GIF, JPG,
PNG, etc. -- whatever Willow supports). Overridden from ImageField to use
Willow instead of Pillow as the image library in order to enable SVG support.
"""
f = FileField.to_python(self, data)
if f is None:
return None
# We need to get a file object for Pillow. We might have a path or we might
# have to read the data into memory.
if hasattr(data, "temporary_file_path"):
file = data.temporary_file_path()
else:
if hasattr(data, "read"):
file = BytesIO(data.read())
else:
file = BytesIO(data["content"])
try:
# Annotate the python representation of the FileField with the image
# property so subclasses can reuse it for their own validation
f.image = willow.Image.open(file)
f.content_type = image_format_name_to_content_type(f.image.format_name)
except Exception as exc:
# Willow doesn't recognize it as an image.
raise ValidationError(
self.error_messages["invalid_image"],
code="invalid_image",
) from exc
if hasattr(f, "seek") and callable(f.seek):
f.seek(0)
if f is not None:
self.check_image_file_size(f)
@ -122,3 +156,27 @@ class WagtailImageField(ImageField):
self.check_image_pixel_size(f)
return f
def image_format_name_to_content_type(image_format_name):
"""
Convert a Willow image format name to a content type.
TODO: Replace once https://github.com/wagtail/Willow/pull/102 and
a new Willow release is out
"""
if image_format_name == "svg":
return "image/svg+xml"
elif image_format_name == "jpeg":
return "image/jpeg"
elif image_format_name == "png":
return "image/png"
elif image_format_name == "gif":
return "image/gif"
elif image_format_name == "bmp":
return "image/bmp"
elif image_format_name == "tiff":
return "image/tiff"
elif image_format_name == "webp":
return "image/webp"
else:
raise ValueError("Unknown image format name")

Wyświetl plik

@ -0,0 +1,33 @@
# Generated by Django 4.0.7 on 2022-08-10 16:26
from django.db import migrations
import wagtail.images.models
class Migration(migrations.Migration):
dependencies = [
("wagtailimages", "0024_index_image_file_hash"),
]
operations = [
migrations.AlterField(
model_name="image",
name="file",
field=wagtail.images.models.WagtailImageField(
height_field="height",
upload_to=wagtail.images.models.get_upload_to,
verbose_name="file",
width_field="width",
),
),
migrations.AlterField(
model_name="rendition",
name="file",
field=wagtail.images.models.WagtailImageField(
height_field="height",
upload_to=wagtail.images.models.get_rendition_upload_to,
width_field="width",
),
),
]

Wyświetl plik

@ -7,6 +7,7 @@ from contextlib import contextmanager
from io import BytesIO
from typing import Union
import willow
from django.apps import apps
from django.conf import settings
from django.core import checks
@ -21,7 +22,6 @@ from django.utils.module_loading import import_string
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from taggit.managers import TaggableManager
from willow.image import Image as WillowImage
from wagtail import hooks
from wagtail.coreutils import string_to_ascii
@ -184,12 +184,51 @@ class ImageFileMixin:
@contextmanager
def get_willow_image(self):
with self.open_file() as image_file:
yield WillowImage.open(image_file)
yield willow.Image.open(image_file)
class WagtailImageFieldFile(models.fields.files.ImageFieldFile):
"""
Override the ImageFieldFile in order to use Willow instead
of Pillow.
"""
def _get_image_dimensions(self):
"""
override _get_image_dimensions to call our own get_image_dimensions.
"""
if not hasattr(self, "_dimensions_cache"):
self._dimensions_cache = self.get_image_dimensions()
return self._dimensions_cache
def get_image_dimensions(self):
"""
The upstream ImageFieldFile calls a local function get_image_dimensions. In this implementation we've made get_image_dimensions
a method to make it easier to override for Wagtail developers in the future.
"""
close = self.closed
try:
self.open()
image = willow.Image.open(self)
return image.get_size()
finally:
if close:
self.close()
class WagtailImageField(models.ImageField):
"""
Override the attr_class on the Django ImageField Model to inject our ImageFieldFile
with Willow support.
"""
attr_class = WagtailImageFieldFile
class AbstractImage(ImageFileMixin, CollectionMember, index.Indexed, models.Model):
title = models.CharField(max_length=255, verbose_name=_("title"))
file = models.ImageField(
""" Use local ImageField with Willow support. """
file = WagtailImageField(
verbose_name=_("file"),
upload_to=get_upload_to,
width_field="width",
@ -743,7 +782,8 @@ class Filter:
class AbstractRendition(ImageFileMixin, models.Model):
filter_spec = models.CharField(max_length=255, db_index=True)
file = models.ImageField(
""" Use local ImageField with Willow support. """
file = WagtailImageField(
upload_to=get_rendition_upload_to,
storage=get_rendition_storage,
width_field="width",

Wyświetl plik

@ -0,0 +1,72 @@
# Generated by Django 4.0.7 on 2022-10-19 00:20
from django.db import migrations
import wagtail.images.models
class Migration(migrations.Migration):
dependencies = [
("tests", "0009_alter_eventpage_options"),
]
operations = [
migrations.AlterField(
model_name="customimage",
name="file",
field=wagtail.images.models.WagtailImageField(
height_field="height",
upload_to=wagtail.images.models.get_upload_to,
verbose_name="file",
width_field="width",
),
),
migrations.AlterField(
model_name="customimagefilepath",
name="file",
field=wagtail.images.models.WagtailImageField(
height_field="height",
upload_to=wagtail.images.models.get_upload_to,
verbose_name="file",
width_field="width",
),
),
migrations.AlterField(
model_name="customimagewithauthor",
name="file",
field=wagtail.images.models.WagtailImageField(
height_field="height",
upload_to=wagtail.images.models.get_upload_to,
verbose_name="file",
width_field="width",
),
),
migrations.AlterField(
model_name="customrendition",
name="file",
field=wagtail.images.models.WagtailImageField(
height_field="height",
upload_to=wagtail.images.models.get_rendition_upload_to,
width_field="width",
),
),
migrations.AlterField(
model_name="customrenditionwithauthor",
name="file",
field=wagtail.images.models.WagtailImageField(
height_field="height",
upload_to=wagtail.images.models.get_rendition_upload_to,
width_field="width",
),
),
migrations.AlterField(
model_name="customrestaurantimage",
name="file",
field=wagtail.images.models.WagtailImageField(
height_field="height",
upload_to=wagtail.images.models.get_upload_to,
verbose_name="file",
width_field="width",
),
),
]