From d0d9377ea5fc7a1b1642f9a16f2a06d375b18735 Mon Sep 17 00:00:00 2001
From: Konrad Mohrfeldt <konrad.mohrfeldt@farbdev.org>
Date: Thu, 11 Apr 2024 19:21:31 +0200
Subject: [PATCH] feat: add image render endpoint

This endpoint adds a quick way for clients to request arbitrary image
sizes based on the functionality available in the versatileimagefield
package.

If we have more advanced use cases we could either take a look at
integrating libvips [1] or delegate requests to a thumbor instance [2].

[1] https://github.com/libvips/pyvips
[2] https://github.com/thumbor/thumbor
---
 program/models.py      | 12 ++++++++++++
 program/serializers.py |  5 +++++
 program/views.py       | 40 ++++++++++++++++++++++++++++++++++++++--
 3 files changed, 55 insertions(+), 2 deletions(-)

diff --git a/program/models.py b/program/models.py
index 5217837d..fc926cbf 100644
--- a/program/models.py
+++ b/program/models.py
@@ -145,6 +145,18 @@ class Image(models.Model):
     ppoi = PPOIField()
     width = models.PositiveIntegerField(blank=True, null=True)
 
+    def render(self, width: int | None = None, height: int | None = None):
+        if width is None and height is None:
+            return self.image.url
+        elif width and height:
+            return self.image.crop[f"{width}x{height}"].url
+        aspect_ratio = self.width / self.height
+        if width is None:
+            width = int(height * aspect_ratio)
+        if height is None:
+            height = int(width / aspect_ratio)
+        return self.image.thumbnail[f"{width}x{height}"].url
+
     def save(self, *args, **kwargs):
         super().save(*args, **kwargs)
 
diff --git a/program/serializers.py b/program/serializers.py
index 97d8df0a..131303e8 100644
--- a/program/serializers.py
+++ b/program/serializers.py
@@ -342,6 +342,11 @@ class ImageSerializer(serializers.ModelSerializer):
         return instance
 
 
+class ImageRenderSerializer(serializers.Serializer):
+    width = serializers.IntegerField(required=False, min_value=1)
+    height = serializers.IntegerField(required=False, min_value=1)
+
+
 class HostSerializer(serializers.ModelSerializer):
     image_id = serializers.PrimaryKeyRelatedField(
         allow_null=True, queryset=Image.objects.all(), required=False, source="image"
diff --git a/program/views.py b/program/views.py
index 23439554..6caeb13e 100644
--- a/program/views.py
+++ b/program/views.py
@@ -25,15 +25,23 @@ from itertools import pairwise
 from textwrap import dedent
 
 from django_filters.rest_framework import DjangoFilterBackend
-from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_view
+from drf_spectacular.utils import (
+    OpenApiParameter,
+    OpenApiResponse,
+    OpenApiTypes,
+    extend_schema,
+    extend_schema_view,
+)
+from rest_framework import decorators
 from rest_framework import filters as drf_filters
 from rest_framework import mixins, permissions, status, viewsets
 from rest_framework.pagination import LimitOffsetPagination
 from rest_framework.response import Response
 
+from django.conf import settings
 from django.contrib.auth.models import User
 from django.db import IntegrityError
-from django.http import HttpResponse, JsonResponse
+from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
 from django.shortcuts import get_object_or_404
 from django.utils import timezone
 from django.utils.translation import gettext as _
@@ -61,6 +69,7 @@ from program.serializers import (
     ErrorSerializer,
     FundingCategorySerializer,
     HostSerializer,
+    ImageRenderSerializer,
     ImageSerializer,
     LanguageSerializer,
     LicenseSerializer,
@@ -347,6 +356,33 @@ class APIImageViewSet(viewsets.ModelViewSet):
 
         return Response(status=status.HTTP_204_NO_CONTENT)
 
+    @extend_schema(
+        parameters=[
+            ImageRenderSerializer,
+            OpenApiParameter(
+                name="Location",
+                type=OpenApiTypes.URI,
+                location=OpenApiParameter.HEADER,
+                description="/",
+                response=[301],
+            ),
+        ],
+        responses={301: None},
+    )
+    @decorators.action(["GET"], detail=True)
+    def render(self, *args, **kwargs):
+        image = self.get_object()
+        serializer = ImageRenderSerializer(data=self.request.GET)
+        if serializer.is_valid():
+            image_spec = serializer.validated_data
+            url = image.render(
+                width=image_spec.get("width", None),
+                height=image_spec.get("height", None),
+            )
+            return HttpResponseRedirect(settings.SITE_URL + url)
+        else:
+            return Response(status=status.HTTP_400_BAD_REQUEST)
+
 
 @extend_schema_view(
     create=extend_schema(summary="Create a new show."),
-- 
GitLab