From ed956f5d5da170801b8ee23903d97597bc423a57 Mon Sep 17 00:00:00 2001 From: Remilia Da Costa Faro Date: Mon, 24 Mar 2025 16:59:38 -0400 Subject: [PATCH] Really? I forgot to push that? --- .idea/misc.xml | 4 +- ...version_imagevariant_available_and_more.py | 76 +++++++++++++ images/models.py | 107 ++++++++++++------ images/views.py | 105 ++++++++--------- manage.py | 0 5 files changed, 204 insertions(+), 88 deletions(-) create mode 100644 images/migrations/0007_image_version_imagevariant_available_and_more.py mode change 100755 => 100644 manage.py diff --git a/.idea/misc.xml b/.idea/misc.xml index 67c7da3..b14ede6 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,7 +3,7 @@ - + \ No newline at end of file diff --git a/images/migrations/0007_image_version_imagevariant_available_and_more.py b/images/migrations/0007_image_version_imagevariant_available_and_more.py new file mode 100644 index 0000000..8d84649 --- /dev/null +++ b/images/migrations/0007_image_version_imagevariant_available_and_more.py @@ -0,0 +1,76 @@ +# Generated by Django 5.1.1 on 2024-11-10 15:11 + +import uuid +from django.db import migrations, models + + +def fill_mymodel_uuid(apps, schema_editor): + db_alias = schema_editor.connection.alias + ImageVariant = apps.get_model("images", "imagevariant") + for obj in ImageVariant.objects.using(db_alias).all(): + obj.uuid = uuid.uuid4() + obj.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ("images", "0006_imagevarianttask"), + ] + + operations = [ + migrations.AddField( + model_name="image", + name="version", + field=models.IntegerField(default=2), + ), + migrations.AddField( + model_name="imagevariant", + name="available", + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="imagevariant", + name="brightness", + field=models.FloatField(default=1), + ), + migrations.AddField( + model_name="imagevariant", + name="gaussian_blur", + field=models.FloatField(default=0), + ), + migrations.AddField( + model_name="imagevarianttask", + name="brightness", + field=models.FloatField(default=1), + ), + migrations.AddField( + model_name="imagevarianttask", + name="gaussian_blur", + field=models.FloatField(default=0), + ), + migrations.AddField( + model_name="imagevariant", + name="uuid", + field=models.UUIDField(null=True), + ), + migrations.RunPython(fill_mymodel_uuid, migrations.RunPython.noop), + migrations.AlterField( + model_name="imagevariant", + name="uuid", + field=models.UUIDField( + default=uuid.uuid4, serialize=False, editable=False, unique=True + ), + ), + migrations.RemoveField("imagevariant", "id"), + migrations.RenameField( + model_name="imagevariant", old_name="uuid", new_name="id" + ), + migrations.AlterField( + model_name="imagevariant", + name="id", + field=models.UUIDField( + primary_key=True, default=uuid.uuid4, serialize=False, editable=False + ), + ), + ] diff --git a/images/models.py b/images/models.py index 35508f9..bda38ad 100644 --- a/images/models.py +++ b/images/models.py @@ -1,11 +1,12 @@ import uuid from io import BytesIO +from botocore.compat import file_type from django.db import models from django.utils import timezone from images.utils import get_b2_resource -from PIL import Image as PILImage, ImageOps +from PIL import Image as PILImage, ImageOps, ImageEnhance, ImageFilter class Image(models.Model): @@ -21,6 +22,7 @@ class Image(models.Model): model_version = models.IntegerField(default=1) height = models.IntegerField(default=0) width = models.IntegerField(default=0) + version = models.IntegerField(default=2) @property def thumbnail_size(self): @@ -33,56 +35,81 @@ class Image(models.Model): def backblaze_filepath(self): return f"{self.id.hex[:2]}/{self.id.hex[2:4]}/{self.id.hex}" - def create_variant_tasks(self, width, height, original_file_type): - ImageVariantTask( - image=self, - height=height, - width=width, - original_file_type=original_file_type, + def create_variant_tasks(self, variant): + ImageVariant( + image=variant.image, + height=variant.height, + width=variant.width, + is_full_size=variant.is_full_size, file_type="avif", + gaussian_blur=variant.gaussian_blur, + brightness=variant.brightness, + available=False, ).save() - ImageVariantTask( - image=self, - height=height, - width=width, - original_file_type=original_file_type, + ImageVariant( + image=variant.image, + height=variant.height, + width=variant.width, + is_full_size=variant.is_full_size, file_type="webp", + gaussian_blur=variant.gaussian_blur, + brightness=variant.brightness, + available=False, ).save() - ImageVariantTask( - image=self, - height=height, - width=width, - original_file_type=original_file_type, + 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): + def create_variant(self, width, height, gaussian_blur, brightness): bucket = get_b2_resource() original_image = BytesIO() original_variant = self.imagevariant_set.filter( - is_full_size=True, file_type__in=["jpg", "png"] + is_full_size=True, + file_type__in=["jpg", "png"], + gaussian_blur=0, + brightness=1, ).first() + bucket.download_fileobj( - f"{self.backblaze_filepath}/{original_variant.width}-{original_variant.height}/image.{original_variant.file_type}", + original_variant.backblaze_filepath, original_image, ) original_image.seek(0) resized_image = BytesIO() - file_extension = "jpg" + image_variant = ImageVariant( + image=self, + height=height, + width=width, + is_full_size=False, + file_type="jpg", + gaussian_blur=gaussian_blur, + brightness=brightness, + available=True, + ) 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 im.has_transparency_data: try: im.save(resized_image, "PNG") - file_extension = "png" + image_variant.file_extension = "png" except OSError: im.convert("RGB").save(resized_image, "JPEG") else: @@ -93,32 +120,40 @@ class Image(models.Model): resized_image.seek(0) - bucket.upload_fileobj( - resized_image, - f"{self.backblaze_filepath}/{width}-{height}/image.{file_extension}", - ) - - image_variant = ImageVariant( - image=self, - height=height, - width=width, - is_full_size=False, - file_type=file_extension, - ) + bucket.upload_fileobj(resized_image, image_variant.backblaze_filepath) image_variant.save() - self.create_variant_tasks(width, height, file_extension) + self.create_variant_tasks(image_variant) return image_variant class ImageVariant(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) image = models.ForeignKey(Image, on_delete=models.CASCADE) height = models.IntegerField() width = models.IntegerField() + gaussian_blur = models.FloatField(default=0) + brightness = models.FloatField(default=1) is_full_size = models.BooleanField(default=False) file_type = models.CharField(max_length=10) + available = models.BooleanField(default=False) + + @property + def backblaze_filepath(self): + return f"{self.id.hex[:2]}/{self.id.hex[2:4]}/{self.id.hex}.{self.file_type}" + + @property + def parent_variant_for_optimized_versions(self): + return ImageVariant.objects.filter( + image_id=self.image_id, + height=self.height, + width=self.width, + gaussian_blur=self.gaussian_blur, + brightness=self.brightness, + file_type__in=["jpg", "png"], + ).first() class ImageVariantTask(models.Model): @@ -127,6 +162,8 @@ class ImageVariantTask(models.Model): image = models.ForeignKey(Image, on_delete=models.CASCADE) height = models.IntegerField() width = models.IntegerField() + gaussian_blur = models.FloatField(default=0) + brightness = models.FloatField(default=1) original_file_type = models.CharField(max_length=10) file_type = models.CharField(max_length=10) diff --git a/images/views.py b/images/views.py index 6b85dc1..2423ce3 100644 --- a/images/views.py +++ b/images/views.py @@ -9,6 +9,7 @@ from django.http import ( HttpResponseNotFound, ) from django.shortcuts import redirect, render +from django.template.defaultfilters import first from django.views.decorators.csrf import csrf_exempt from images.decorators import ( @@ -16,7 +17,7 @@ from images.decorators import ( can_upload_variant, can_upload_image, ) -from images.models import Image, ImageVariant, ImageVariantTask +from images.models import Image, ImageVariant from images.utils import get_b2_resource JpegImagePlugin._getmp = lambda x: None @@ -44,13 +45,16 @@ def upload(request): elif im.format == "PNG": file_extension = "png" else: - return {"created": False, "error": "Uploaded file should be JPEG or PNG"} + return JsonResponse( + {"created": False, "error": "Uploaded file should be JPEG or PNG"}, + status=400, + ) file.seek(0) same_md5_image = Image.objects.filter(original_md5=file_md5_hash).first() if same_md5_image: - return {"created": False, "id": same_md5_image.id} + return JsonResponse({"created": False, "id": same_md5_image.id}) image = Image( original_name=file.name, @@ -59,44 +63,46 @@ def upload(request): height=height, width=width, model_version=2, + version=3, ) image.save() - bucket.upload_fileobj( - file, f"{image.backblaze_filepath}/{width}-{height}/image.{file_extension}" - ) - - ImageVariant.objects.get_or_create( + variant, _ = ImageVariant.objects.get_or_create( image=image, height=height, width=width, file_type=file_extension, is_full_size=True, + available=True, ) - image.create_variant_tasks(image.width, image.height, file_extension) + bucket.upload_fileobj(file, variant.backblaze_filepath) + + image.create_variant_tasks(variant) image.uploaded = True image.save() - return JsonResponse({"created": True, "id": image.id}) + return JsonResponse({"created": True, "id": image.id}, status=201) @can_upload_variant def image_type_optimization_needed(request, image_type): - tasks = ImageVariantTask.objects.filter(file_type=image_type).all() + variants = ImageVariant.objects.filter( + image__version=3, file_type=image_type, available=False + ).all()[:100] return JsonResponse( { "variants": [ { - "image_id": task.image.id, - "task_id": task.id, - "height": task.height, - "width": task.width, - "file_type": task.original_file_type, + "original_variant_id": variant.parent_variant_for_optimized_versions.id, + "original_variant_file_type": variant.parent_variant_for_optimized_versions.file_type, + "variant_id": variant.id, + "height": variant.height, + "width": variant.width, } - for task in tasks + for variant in variants ] } ) @@ -105,47 +111,36 @@ def image_type_optimization_needed(request, image_type): @csrf_exempt @can_upload_variant def upload_variant(request): - if "task_id" not in request.POST: + if "variant_id" not in request.POST: return HttpResponseBadRequest() - task = ImageVariantTask.objects.filter(id=request.POST["task_id"]).first() + variant = ImageVariant.objects.filter(id=request.POST["variant_id"]).first() - if not task: + if not variant: return HttpResponseNotFound() - image = task.image - height = task.height - width = task.width - file_type = task.file_type file = request.FILES["file"] - if file_type == "jpegli": - file_name = "jpegli.jpg" - else: - file_name = "image." + file_type - - upload_path = f"{image.backblaze_filepath}/{width}-{height}/{file_name}" - bucket = get_b2_resource() - bucket.upload_fileobj(file, upload_path) + bucket.upload_fileobj(file, variant.backblaze_filepath) - ImageVariant.objects.get_or_create( - image=image, - height=height, - width=width, - file_type=file_type, - is_full_size=(height == image.height and width == image.width), - ) - - task.delete() + variant.available = True + variant.save() return JsonResponse({"status": "ok"}) def image_with_size(request, image, width, height, image_type): + gaussian_blur = float(request.GET.get("gaussian_blur", 0)) + brightness = float(request.GET.get("brightness", 1)) + image_variants = ImageVariant.objects.filter( - image=image, height=height, width=width + image=image, + height=height, + width=width, + gaussian_blur=gaussian_blur, + brightness=brightness, ) if image_type != "auto": if image_type == "original": @@ -153,16 +148,21 @@ def image_with_size(request, image, width, height, image_type): else: image_variants = image_variants.filter(file_type=image_type) + if image.version == 3: + image_variants = image_variants.filter(available=True) + variants = image_variants.all() if not variants: if image_type != "auto" and image_type != "original": return JsonResponse({"error": "Image version not available"}, status=404) else: - image_variant = image.create_variant(width, height) + image_variant = image.create_variant( + width, height, gaussian_blur, brightness + ) return redirect( - f"{settings.S3_PUBLIC_BASE_PATH}/{image.backblaze_filepath}/{width}-{height}/image.{image_variant.file_type}" + f"{settings.S3_PUBLIC_BASE_PATH}/{image_variant.backblaze_filepath}" ) if image_type == "auto": @@ -195,14 +195,17 @@ def image_with_size(request, image, width, height, image_type): variant = variant[0] - if file_type == "jpegli": - file_name = "jpegli.jpg" - else: - file_name = "image." + file_type + if image.version == 2: + if file_type == "jpegli": + file_name = "jpegli.jpg" + else: + file_name = "image." + file_type - return redirect( - f"{settings.S3_PUBLIC_BASE_PATH}/{image.backblaze_filepath}/{variant.width}-{variant.height}/{file_name}" - ) + return redirect( + f"{settings.S3_PUBLIC_BASE_PATH}/{image.backblaze_filepath}/{variant.width}-{variant.height}/{file_name}" + ) + + return redirect(f"{settings.S3_PUBLIC_BASE_PATH}/{variant.backblaze_filepath}") return HttpResponseNotFound() diff --git a/manage.py b/manage.py old mode 100755 new mode 100644