Really? I forgot to push that?

This commit is contained in:
2025-03-24 16:59:38 -04:00
parent 30d9655d82
commit ed956f5d5d
5 changed files with 204 additions and 88 deletions

4
.idea/misc.xml generated
View File

@@ -3,7 +3,7 @@
<component name="Black"> <component name="Black">
<option name="enabledOnReformat" value="true" /> <option name="enabledOnReformat" value="true" />
<option name="enabledOnSave" value="true" /> <option name="enabledOnSave" value="true" />
<option name="sdkName" value="Python 3.12 (kakigoori)" /> <option name="sdkName" value="Python 3.13 (kakigoori)" />
</component> </component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (kakigoori)" project-jdk-type="Python SDK" /> <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 (kakigoori)" project-jdk-type="Python SDK" />
</project> </project>

View File

@@ -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
),
),
]

View File

@@ -1,11 +1,12 @@
import uuid import uuid
from io import BytesIO from io import BytesIO
from botocore.compat import file_type
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
from images.utils import get_b2_resource 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): class Image(models.Model):
@@ -21,6 +22,7 @@ class Image(models.Model):
model_version = models.IntegerField(default=1) model_version = models.IntegerField(default=1)
height = models.IntegerField(default=0) height = models.IntegerField(default=0)
width = models.IntegerField(default=0) width = models.IntegerField(default=0)
version = models.IntegerField(default=2)
@property @property
def thumbnail_size(self): def thumbnail_size(self):
@@ -33,56 +35,81 @@ class Image(models.Model):
def backblaze_filepath(self): def backblaze_filepath(self):
return f"{self.id.hex[:2]}/{self.id.hex[2:4]}/{self.id.hex}" return f"{self.id.hex[:2]}/{self.id.hex[2:4]}/{self.id.hex}"
def create_variant_tasks(self, width, height, original_file_type): def create_variant_tasks(self, variant):
ImageVariantTask( ImageVariant(
image=self, image=variant.image,
height=height, height=variant.height,
width=width, width=variant.width,
original_file_type=original_file_type, is_full_size=variant.is_full_size,
file_type="avif", file_type="avif",
gaussian_blur=variant.gaussian_blur,
brightness=variant.brightness,
available=False,
).save() ).save()
ImageVariantTask( ImageVariant(
image=self, image=variant.image,
height=height, height=variant.height,
width=width, width=variant.width,
original_file_type=original_file_type, is_full_size=variant.is_full_size,
file_type="webp", file_type="webp",
gaussian_blur=variant.gaussian_blur,
brightness=variant.brightness,
available=False,
).save() ).save()
ImageVariantTask( ImageVariant(
image=self, image=variant.image,
height=height, height=variant.height,
width=width, width=variant.width,
original_file_type=original_file_type, is_full_size=variant.is_full_size,
file_type="jpegli", file_type="jpegli",
gaussian_blur=variant.gaussian_blur,
brightness=variant.brightness,
available=False,
).save() ).save()
def create_variant(self, width, height): def create_variant(self, width, height, gaussian_blur, brightness):
bucket = get_b2_resource() bucket = get_b2_resource()
original_image = BytesIO() original_image = BytesIO()
original_variant = self.imagevariant_set.filter( 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() ).first()
bucket.download_fileobj( 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,
) )
original_image.seek(0) original_image.seek(0)
resized_image = BytesIO() 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: with PILImage.open(original_image) as im:
ImageOps.exif_transpose(im, in_place=True) 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)) im.thumbnail((width, height))
if im.has_transparency_data: if im.has_transparency_data:
try: try:
im.save(resized_image, "PNG") im.save(resized_image, "PNG")
file_extension = "png" image_variant.file_extension = "png"
except OSError: except OSError:
im.convert("RGB").save(resized_image, "JPEG") im.convert("RGB").save(resized_image, "JPEG")
else: else:
@@ -93,32 +120,40 @@ class Image(models.Model):
resized_image.seek(0) resized_image.seek(0)
bucket.upload_fileobj( bucket.upload_fileobj(resized_image, image_variant.backblaze_filepath)
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,
)
image_variant.save() image_variant.save()
self.create_variant_tasks(width, height, file_extension) self.create_variant_tasks(image_variant)
return image_variant return image_variant
class ImageVariant(models.Model): class ImageVariant(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
image = models.ForeignKey(Image, on_delete=models.CASCADE) image = models.ForeignKey(Image, on_delete=models.CASCADE)
height = models.IntegerField() height = models.IntegerField()
width = models.IntegerField() width = models.IntegerField()
gaussian_blur = models.FloatField(default=0)
brightness = models.FloatField(default=1)
is_full_size = models.BooleanField(default=False) is_full_size = models.BooleanField(default=False)
file_type = models.CharField(max_length=10) 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): class ImageVariantTask(models.Model):
@@ -127,6 +162,8 @@ class ImageVariantTask(models.Model):
image = models.ForeignKey(Image, on_delete=models.CASCADE) image = models.ForeignKey(Image, on_delete=models.CASCADE)
height = models.IntegerField() height = models.IntegerField()
width = models.IntegerField() width = models.IntegerField()
gaussian_blur = models.FloatField(default=0)
brightness = models.FloatField(default=1)
original_file_type = models.CharField(max_length=10) original_file_type = models.CharField(max_length=10)
file_type = models.CharField(max_length=10) file_type = models.CharField(max_length=10)

View File

@@ -9,6 +9,7 @@ from django.http import (
HttpResponseNotFound, HttpResponseNotFound,
) )
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.template.defaultfilters import first
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from images.decorators import ( from images.decorators import (
@@ -16,7 +17,7 @@ from images.decorators import (
can_upload_variant, can_upload_variant,
can_upload_image, can_upload_image,
) )
from images.models import Image, ImageVariant, ImageVariantTask from images.models import Image, ImageVariant
from images.utils import get_b2_resource from images.utils import get_b2_resource
JpegImagePlugin._getmp = lambda x: None JpegImagePlugin._getmp = lambda x: None
@@ -44,13 +45,16 @@ def upload(request):
elif im.format == "PNG": elif im.format == "PNG":
file_extension = "png" file_extension = "png"
else: 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) file.seek(0)
same_md5_image = Image.objects.filter(original_md5=file_md5_hash).first() same_md5_image = Image.objects.filter(original_md5=file_md5_hash).first()
if same_md5_image: if same_md5_image:
return {"created": False, "id": same_md5_image.id} return JsonResponse({"created": False, "id": same_md5_image.id})
image = Image( image = Image(
original_name=file.name, original_name=file.name,
@@ -59,44 +63,46 @@ def upload(request):
height=height, height=height,
width=width, width=width,
model_version=2, model_version=2,
version=3,
) )
image.save() image.save()
bucket.upload_fileobj( variant, _ = ImageVariant.objects.get_or_create(
file, f"{image.backblaze_filepath}/{width}-{height}/image.{file_extension}"
)
ImageVariant.objects.get_or_create(
image=image, image=image,
height=height, height=height,
width=width, width=width,
file_type=file_extension, file_type=file_extension,
is_full_size=True, 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.uploaded = True
image.save() image.save()
return JsonResponse({"created": True, "id": image.id}) return JsonResponse({"created": True, "id": image.id}, status=201)
@can_upload_variant @can_upload_variant
def image_type_optimization_needed(request, image_type): 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( return JsonResponse(
{ {
"variants": [ "variants": [
{ {
"image_id": task.image.id, "original_variant_id": variant.parent_variant_for_optimized_versions.id,
"task_id": task.id, "original_variant_file_type": variant.parent_variant_for_optimized_versions.file_type,
"height": task.height, "variant_id": variant.id,
"width": task.width, "height": variant.height,
"file_type": task.original_file_type, "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 @csrf_exempt
@can_upload_variant @can_upload_variant
def upload_variant(request): def upload_variant(request):
if "task_id" not in request.POST: if "variant_id" not in request.POST:
return HttpResponseBadRequest() 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() return HttpResponseNotFound()
image = task.image
height = task.height
width = task.width
file_type = task.file_type
file = request.FILES["file"] 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 = get_b2_resource()
bucket.upload_fileobj(file, upload_path) bucket.upload_fileobj(file, variant.backblaze_filepath)
ImageVariant.objects.get_or_create( variant.available = True
image=image, variant.save()
height=height,
width=width,
file_type=file_type,
is_full_size=(height == image.height and width == image.width),
)
task.delete()
return JsonResponse({"status": "ok"}) return JsonResponse({"status": "ok"})
def image_with_size(request, image, width, height, image_type): 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_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 != "auto":
if image_type == "original": if image_type == "original":
@@ -153,16 +148,21 @@ def image_with_size(request, image, width, height, image_type):
else: else:
image_variants = image_variants.filter(file_type=image_type) image_variants = image_variants.filter(file_type=image_type)
if image.version == 3:
image_variants = image_variants.filter(available=True)
variants = image_variants.all() variants = image_variants.all()
if not variants: if not variants:
if image_type != "auto" and image_type != "original": if image_type != "auto" and image_type != "original":
return JsonResponse({"error": "Image version not available"}, status=404) return JsonResponse({"error": "Image version not available"}, status=404)
else: else:
image_variant = image.create_variant(width, height) image_variant = image.create_variant(
width, height, gaussian_blur, brightness
)
return redirect( 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": if image_type == "auto":
@@ -195,14 +195,17 @@ def image_with_size(request, image, width, height, image_type):
variant = variant[0] variant = variant[0]
if file_type == "jpegli": if image.version == 2:
file_name = "jpegli.jpg" if file_type == "jpegli":
else: file_name = "jpegli.jpg"
file_name = "image." + file_type else:
file_name = "image." + file_type
return redirect( return redirect(
f"{settings.S3_PUBLIC_BASE_PATH}/{image.backblaze_filepath}/{variant.width}-{variant.height}/{file_name}" 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() return HttpResponseNotFound()

0
manage.py Executable file → Normal file
View File