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