Skip to content

Commit

Permalink
Implement conversion/downsizing of downloaded images
Browse files Browse the repository at this point in the history
  • Loading branch information
ababic committed Jul 5, 2024
1 parent 1363272 commit ad20982
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 2 deletions.
109 changes: 107 additions & 2 deletions src/wagtail_bynder/models.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
import io
import logging
import math
import os

from dataclasses import dataclass
from datetime import datetime
from mimetypes import guess_type
from tempfile import NamedTemporaryFile
from typing import Any

from django.conf import settings
from django.core.files.uploadedfile import UploadedFile
from django.core.files.uploadedfile import InMemoryUploadedFile, UploadedFile
from django.db import models
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from wagtail.admin.panels import FieldPanel, MultiFieldPanel
from wagtail.documents.models import AbstractDocument, Document
from wagtail.images.models import AbstractImage, Image
from wagtail.images.models import (
IMAGE_FORMAT_EXTENSIONS,
AbstractImage,
Filter,
Image,
)
from wagtail.models import Collection, CollectionMember
from wagtail.search import index

Expand All @@ -24,6 +33,15 @@
logger = logging.getLogger("wagtail.images")


@dataclass(frozen=True)
class ConvertedImageDetails:
width: int
height: int
file_size: int
image_format: str
mime_type: str


class BynderAssetMixin(models.Model):
# Fields relevant to the Bynder integration only
bynder_id = models.CharField(
Expand Down Expand Up @@ -266,6 +284,93 @@ def update_file(self, asset_data: dict[str, Any]) -> None:
def download_file(self, source_url: str) -> UploadedFile:
return utils.download_image(source_url)

def process_downloaded_file(
self,
file: UploadedFile,
asset_data: dict[str, Any] | None = None,
) -> UploadedFile:
"""
Overrides ``BynderAssetWithFileMixin.process_downloaded_file()`` to
pass the downloaded image to ``convert_downloaded_image()`` before using it as
a value for this object's ``file`` field.
"""

# Write to filesystem to avoid using memory for the same image
tmp = NamedTemporaryFile(mode="w+b", dir=settings.FILE_UPLOAD_TEMP_DIR)
details = self.convert_downloaded_image(file, tmp)

# The original file is now redundant and can be deleted, making
# more memory available
del file.file

# Load the converted image into memory to speed up the additional
# reads and writes performed by Wagtail
new_file = io.BytesIO()
tmp.seek(0)
with open(tmp.name, "rb") as source:
for line in source:
new_file.write(line)

name_minus_extension, _ = os.path.splitext(file.name)
new_extension = IMAGE_FORMAT_EXTENSIONS[details.image_format]

# Return replacement InMemoryUploadedFile
return InMemoryUploadedFile(
new_file,
field_name="file",
name=f"{name_minus_extension}{new_extension}",
content_type=details.mime_type,
size=details.file_size,
charset=None,
)

def convert_downloaded_image(
self, source_file, target_file
) -> ConvertedImageDetails:
"""
Handles the conversion of the supplied ``file`` into something
``process_downloaded_file()`` can use to successfully assemble a
new ``InMemoryUploadedFile``.
``target_file`` must be a writable file-like object, and is where the
new file contents is written to.
The return value is a ``ConvertedImageDetails`` object, which allows
``process_downloaded_file()`` to determine the height, width,
format, mime-type and file size of the newly generated image without
having to perform any more file operations.
"""
original_width, original_height = self.width, self.height

# Filter.run() expects the object's width and height to reflect
# the image we're formatting, so we update them temporarily
self.width, self.height = utils.get_image_dimensions(source_file)

# Retreieve maximum height and width from settings
max_width = int(getattr(settings, "BYNDER_IMAGE_MAX_WIDTH", 3500))
max_height = int(getattr(settings, "BYNDER_IMAGE_MAX_HEIGHT", 3500))

try:
# Use wagtail built-ins to resize/reformat the image
willow_image = Filter(f"max-{max_width}x{max_height}").run(
self,
target_file,
source_file,
)
finally:
# Always restore original field values
self.width, self.height = original_width, original_height

# Gather up all of the useful data about the new image
final_width, final_height = willow_image.get_size()
return ConvertedImageDetails(
final_width,
final_height,
target_file.tell(),
willow_image.format_name,
willow_image.mime_type,
)

def set_focal_area_from_focus_point(
self, x: int, y: int, original_height: int, original_width: int
) -> None:
Expand Down
7 changes: 7 additions & 0 deletions src/wagtail_bynder/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
from asgiref.local import Local
from bynder_sdk import BynderClient
from django.conf import settings
from django.core.files import File
from django.core.files.uploadedfile import InMemoryUploadedFile
from django.template.defaultfilters import filesizeformat
from wagtail.models import Collection
from willow import Image

from .exceptions import BynderAssetFileTooLarge

Expand Down Expand Up @@ -59,6 +61,11 @@ def download_image(url: str) -> InMemoryUploadedFile:
return download_file(url, max_filesize, max_filesize_setting_name)


def get_image_dimensions(file: File) -> tuple[int, int]:
willow_image = Image.open(file)
return willow_image.get_size()


def filename_from_url(url: str) -> str:
return os.path.basename(url)

Expand Down

0 comments on commit ad20982

Please sign in to comment.