From 444c8ceb627716a1c1ff4aaf3d2deac5c8039fc6 Mon Sep 17 00:00:00 2001 From: Remilia Da Costa Faro Date: Sat, 3 May 2025 13:19:12 +0200 Subject: [PATCH] Some refactoring, and added a script to regenerate variants --- .idea/kakigoori.iml | 2 +- .idea/misc.xml | 4 +- .../commands/regenerate_variants.py | 55 ++++++++++++ .../0008_imagevariant_regenerate.py | 18 ++++ images/models.py | 83 +++++++++++-------- pyproject.toml | 1 + uv.lock | 49 +++++++++++ 7 files changed, 176 insertions(+), 36 deletions(-) create mode 100644 images/management/commands/regenerate_variants.py create mode 100644 images/migrations/0008_imagevariant_regenerate.py diff --git a/.idea/kakigoori.iml b/.idea/kakigoori.iml index e66e84a..ad740da 100644 --- a/.idea/kakigoori.iml +++ b/.idea/kakigoori.iml @@ -16,7 +16,7 @@ - + diff --git a/.idea/misc.xml b/.idea/misc.xml index b14ede6..19d9409 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,7 +3,7 @@ - + \ No newline at end of file diff --git a/images/management/commands/regenerate_variants.py b/images/management/commands/regenerate_variants.py new file mode 100644 index 0000000..4d93e9e --- /dev/null +++ b/images/management/commands/regenerate_variants.py @@ -0,0 +1,55 @@ +from django.core.management.base import BaseCommand + +from images.models import ImageVariant +from images.utils import get_b2_resource + + +class Command(BaseCommand): + def add_arguments(self, parser): + parser.add_argument("variants", nargs="+", type=str) + + def handle(self, *args, **options): + if options["variants"] and len(options["variants"]) > 0: + variants = options["variants"] + else: + variants = ImageVariant.objects.all() + + bucket = get_b2_resource() + + for variant_id in variants: + print("Regenerating variant %s" % variant_id) + + image_variant = ImageVariant.objects.filter(id=variant_id).first() + + if image_variant.is_full_size and ( + image_variant.file_type == "jpg" or image_variant.file_type == "png" + ): + print("Can't regenerate original image") + continue + + if image_variant.file_type == "webp" or image_variant.file_type == "avif": + image_variant.regenerate = True + image_variant.save() + continue + + image, file_extension = image_variant.image.create_resized_image( + image_variant.height, + image_variant.width, + image_variant.gaussian_blur, + image_variant.brightness, + ) + + if file_extension == "jpg": + content_type = "image/jpeg" + elif file_extension == "png": + content_type = "image/png" + else: + content_type = "binary/octet-stream" + + image_variant.file_type = file_extension + + bucket.upload_fileobj( + image, + image_variant.backblaze_filepath, + ExtraArgs={"ContentType": content_type}, + ) diff --git a/images/migrations/0008_imagevariant_regenerate.py b/images/migrations/0008_imagevariant_regenerate.py new file mode 100644 index 0000000..401c29a --- /dev/null +++ b/images/migrations/0008_imagevariant_regenerate.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2 on 2025-05-03 10:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("images", "0007_image_version_imagevariant_available_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="imagevariant", + name="regenerate", + field=models.BooleanField(default=False), + ), + ] diff --git a/images/models.py b/images/models.py index 47639b3..52205ef 100644 --- a/images/models.py +++ b/images/models.py @@ -58,18 +58,7 @@ class Image(models.Model): available=False, ).save() - ImageVariant( - image=variant.image, - height=variant.height, - width=variant.width, - is_full_size=variant.is_full_size, - file_type="jpegli", - gaussian_blur=variant.gaussian_blur, - brightness=variant.brightness, - available=False, - ).save() - - def create_variant(self, width, height, gaussian_blur, brightness): + def download_original_variant(self): bucket = get_b2_resource() original_image = BytesIO() @@ -85,8 +74,46 @@ class Image(models.Model): original_image, ) original_image.seek(0) + return original_image + def create_resized_image( + self, + height, + width, + gaussian_blur, + brightness, + ): + original_image = self.download_original_variant() resized_image = BytesIO() + file_extension = "jpg" + + with PILImage.open(original_image) as im: + ImageOps.exif_transpose(im, in_place=True) + im = im.filter(ImageFilter.GaussianBlur(gaussian_blur)) + enhancer = ImageEnhance.Brightness(im) + im = enhancer.enhance(brightness) + im.thumbnail( + (width, height), resample=PILImage.Resampling.LANCZOS, reducing_gap=3.0 + ) + + if im.has_transparency_data: + try: + im.save(resized_image, "PNG", quality=90) + file_extension = "png" + except OSError: + im.convert("RGB").save(resized_image, "JPEG", quality=90) + else: + try: + im.save(resized_image, "JPEG", quality=90) + except OSError: + im.convert("RGB").save(resized_image, "JPEG", quality=90) + + resized_image.seek(0) + + return resized_image, file_extension + + def create_variant(self, width, height, gaussian_blur, brightness): + bucket = get_b2_resource() image_variant = ImageVariant( image=self, @@ -99,29 +126,18 @@ class Image(models.Model): available=True, ) - content_type = "image/jpeg" + resized_image, file_extension = self.create_resized_image( + height, width, gaussian_blur, brightness + ) - with PILImage.open(original_image) as im: - ImageOps.exif_transpose(im, in_place=True) - im = im.filter(ImageFilter.GaussianBlur(gaussian_blur)) - enhancer = ImageEnhance.Brightness(im) - im = enhancer.enhance(brightness) - im.thumbnail((width, height)) + if file_extension == "jpg": + content_type = "image/jpeg" + elif file_extension == "png": + content_type = "image/png" + else: + content_type = "binary/octet-stream" - if im.has_transparency_data: - try: - im.save(resized_image, "PNG") - image_variant.file_extension = "png" - content_type = "image/png" - except OSError: - im.convert("RGB").save(resized_image, "JPEG") - else: - try: - im.save(resized_image, "JPEG") - except OSError: - im.convert("RGB").save(resized_image, "JPEG") - - resized_image.seek(0) + image_variant.file_type = file_extension bucket.upload_fileobj( resized_image, @@ -146,6 +162,7 @@ class ImageVariant(models.Model): is_full_size = models.BooleanField(default=False) file_type = models.CharField(max_length=10) available = models.BooleanField(default=False) + regenerate = models.BooleanField(default=False) @property def backblaze_filepath(self): diff --git a/pyproject.toml b/pyproject.toml index 5c84313..ec3431a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ dependencies = [ "charset-normalizer==3.3.2", "click==8.1.7", "django==5.2", + "django-stubs>=5.2.0", "gunicorn==23.0.0", "idna==3.8", "jmespath==1.0.1", diff --git a/uv.lock b/uv.lock index 1ed3ffd..abdabbe 100644 --- a/uv.lock +++ b/uv.lock @@ -146,6 +146,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/63/e0/6a5b5ea350c5bd63fe94b05e4c146c18facb51229d9dee42aa39f9fc2214/Django-5.2-py3-none-any.whl", hash = "sha256:91ceed4e3a6db5aedced65e3c8f963118ea9ba753fc620831c77074e620e7d83", size = 8301361 }, ] +[[package]] +name = "django-stubs" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "django" }, + { name = "django-stubs-ext" }, + { name = "types-pyyaml" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/79/7e7ad8b4bac545c8e608fa8db0bd061977f93035a112be78a7f3ffc6ff66/django_stubs-5.2.0.tar.gz", hash = "sha256:07e25c2d3cbff5be540227ff37719cc89f215dfaaaa5eb038a75b01bbfbb2722", size = 276297 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/01/5913ba5514337f3896c7bcbff6075808184dd303cd0fc3ecc289ec7e0c96/django_stubs-5.2.0-py3-none-any.whl", hash = "sha256:cd52da033489afc1357d6245f49e3cc57bf49015877253fb8efc6722ea3d2d2b", size = 481836 }, +] + +[[package]] +name = "django-stubs-ext" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/7a/84338605817960942c1ea9d852685923ccccd0d91ba0d49532605973491f/django_stubs_ext-5.2.0.tar.gz", hash = "sha256:00c4ae307b538f5643af761a914c3f8e4e3f25f4e7c6d7098f1906c0d8f2aac9", size = 9618 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/65/9f5ca467d84a67c0c547f10b0ece9fd9c26c5efc818a01bf6a3d306c2a0c/django_stubs_ext-5.2.0-py3-none-any.whl", hash = "sha256:b27ae0aab970af4894ba4e9b3fcd3e03421dc8731516669659ee56122d148b23", size = 9066 }, +] + [[package]] name = "gunicorn" version = "23.0.0" @@ -189,6 +218,7 @@ dependencies = [ { name = "charset-normalizer" }, { name = "click" }, { name = "django" }, + { name = "django-stubs" }, { name = "gunicorn" }, { name = "idna" }, { name = "jmespath" }, @@ -217,6 +247,7 @@ requires-dist = [ { name = "charset-normalizer", specifier = "==3.3.2" }, { name = "click", specifier = "==8.1.7" }, { name = "django", specifier = "==5.2" }, + { name = "django-stubs", specifier = ">=5.2.0" }, { name = "gunicorn", specifier = "==23.0.0" }, { name = "idna", specifier = "==3.8" }, { name = "jmespath", specifier = "==1.0.1" }, @@ -382,6 +413,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/a5/b2860373aa8de1e626b2bdfdd6df4355f0565b47e51f7d0c54fe70faf8fe/sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4", size = 44156 }, ] +[[package]] +name = "types-pyyaml" +version = "6.0.12.20250402" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/68/609eed7402f87c9874af39d35942744e39646d1ea9011765ec87b01b2a3c/types_pyyaml-6.0.12.20250402.tar.gz", hash = "sha256:d7c13c3e6d335b6af4b0122a01ff1d270aba84ab96d1a1a1063ecba3e13ec075", size = 17282 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/56/1fe61db05685fbb512c07ea9323f06ea727125951f1eb4dff110b3311da3/types_pyyaml-6.0.12.20250402-py3-none-any.whl", hash = "sha256:652348fa9e7a203d4b0d21066dfb00760d3cbd5a15ebb7cf8d33c88a49546681", size = 20329 }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 }, +] + [[package]] name = "tzdata" version = "2025.2"