kopia lustrzana https://github.com/wagtail/wagtail
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
rodzic
0b4302afe9
commit
912747f6ae
|
@ -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)
|
||||
|
|
|
@ -643,7 +643,7 @@ Contributors
|
|||
* Abayomi Victory
|
||||
* Victoria Poromon
|
||||
* Dokua Asiedu
|
||||
|
||||
* Darrel O'Pry
|
||||
|
||||
Translators
|
||||
===========
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
),
|
||||
),
|
||||
]
|
Ładowanie…
Reference in New Issue