Skip to content

Commit fe94f1c

Browse files
authored
Fix #144 -- Improve DRM serialization performance by filtering source and ratio (#148)
1 parent 62988c1 commit fe94f1c

File tree

3 files changed

+116
-14
lines changed

3 files changed

+116
-14
lines changed

README.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -246,9 +246,21 @@ class PictureSerializer(serializers.Serializer):
246246
picture = PictureField()
247247
```
248248

249-
You may provide optional GET parameters to the serializer, to specify the aspect
250-
ratio and breakpoints you want to include in the response. The parameters are
251-
prefixed with the `fieldname_` to avoid conflicts with other fields.
249+
The response can be restricted to a single aspect ratio and image source, by
250+
providing the `aspect_ratio` and `image_source` arguments to the field.
251+
252+
```python
253+
from rest_framework import serializers
254+
from pictures.contrib.rest_framework import PictureField
255+
256+
class PictureSerializer(serializers.Serializer):
257+
picture = PictureField(aspect_ratio="16/9", image_source="WEBP")
258+
```
259+
260+
You also may provide optional GET parameters to the serializer,
261+
to specify the aspect ratio and breakpoints you want to include in the response.
262+
The parameters are prefixed with the `fieldname_`
263+
to avoid conflicts with other fields.
252264

253265
```bash
254266
curl http://localhost:8000/api/path/?picture_ratio=16%2F9&picture_m=6&picture_l=4

pictures/contrib/rest_framework.py

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,52 @@ def default(obj):
1919
class PictureField(serializers.ReadOnlyField):
2020
"""Read-only field for all aspect ratios and sizes of the image."""
2121

22+
def __init__(self, aspect_ratio=None, image_source=None, **kwargs):
23+
self.aspect_ratio = aspect_ratio
24+
self.image_source = image_source
25+
super().__init__(**kwargs)
26+
2227
def to_representation(self, obj: PictureFieldFile):
2328
if not obj:
2429
return None
25-
payload = {
30+
31+
base_payload = {
2632
"url": obj.url,
2733
"width": obj.width,
2834
"height": obj.height,
29-
"ratios": {
30-
ratio: {
31-
"sources": {
32-
f"image/{file_type.lower()}": sizes
33-
for file_type, sizes in sources.items()
34-
},
35-
}
36-
for ratio, sources in obj.aspect_ratios.items()
37-
},
3835
}
36+
37+
# if aspect_ratio is set, only return that aspect ratio to reduce payload size
38+
if self.aspect_ratio and self.image_source:
39+
try:
40+
sizes = obj.aspect_ratios[self.aspect_ratio][self.image_source]
41+
except KeyError as e:
42+
raise ValueError(
43+
f"Invalid ratio {self.aspect_ratio} or image source {self.image_source}. Choices are: {', '.join(filter(None, obj.aspect_ratios.keys()))}"
44+
) from e
45+
payload = {
46+
**base_payload,
47+
"ratios": {
48+
self.aspect_ratio: {
49+
"sources": {f"image/{self.image_source.lower()}": sizes}
50+
}
51+
},
52+
}
53+
else:
54+
payload = {
55+
**base_payload,
56+
"ratios": {
57+
ratio: {
58+
"sources": {
59+
f"image/{file_type.lower()}": sizes
60+
for file_type, sizes in sources.items()
61+
},
62+
}
63+
for ratio, sources in obj.aspect_ratios.items()
64+
},
65+
}
66+
67+
# if the request has query parameters, filter the payload
3968
try:
4069
query_params: QueryDict = self.context["request"].GET
4170
except KeyError:

tests/contrib/test_rest_framework.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,23 @@
1212

1313
class ProfileSerializer(serializers.ModelSerializer):
1414
image = rest_framework.PictureField(source="picture")
15+
image_mobile = rest_framework.PictureField(
16+
source="picture", aspect_ratio="3/2", image_source="WEBP"
17+
)
18+
19+
class Meta:
20+
model = models.Profile
21+
fields = ["image", "image_mobile"]
22+
23+
24+
class ProfileSerializerWithInvalidData(serializers.ModelSerializer):
25+
image_invalid = rest_framework.PictureField(
26+
source="picture", aspect_ratio="21/11", image_source="GIF"
27+
)
1528

1629
class Meta:
1730
model = models.Profile
18-
fields = ["image"]
31+
fields = ["image_invalid"]
1932

2033

2134
def test_default(settings):
@@ -264,3 +277,51 @@ def test_to_representation__with_false_str_container(
264277
with pytest.raises(ValueError) as e:
265278
serializer.data["image"]
266279
assert str(e.value) == "Container width is not a number: not_a_number"
280+
281+
@pytest.mark.django_db
282+
def test_to_representation__with_prefiltered_aspect_ratio_and_source(
283+
self, image_upload_file, settings
284+
):
285+
settings.PICTURES["USE_PLACEHOLDERS"] = False
286+
287+
profile = models.Profile.objects.create(picture=image_upload_file)
288+
serializer = ProfileSerializer(profile)
289+
290+
assert serializer.data["image_mobile"] == {
291+
"url": "/media/testapp/profile/image.png",
292+
"width": 800,
293+
"height": 800,
294+
"ratios": {
295+
"3/2": {
296+
"sources": {
297+
"image/webp": {
298+
"800": "/media/testapp/profile/image/3_2/800w.webp",
299+
"100": "/media/testapp/profile/image/3_2/100w.webp",
300+
"200": "/media/testapp/profile/image/3_2/200w.webp",
301+
"300": "/media/testapp/profile/image/3_2/300w.webp",
302+
"400": "/media/testapp/profile/image/3_2/400w.webp",
303+
"500": "/media/testapp/profile/image/3_2/500w.webp",
304+
"600": "/media/testapp/profile/image/3_2/600w.webp",
305+
"700": "/media/testapp/profile/image/3_2/700w.webp",
306+
}
307+
}
308+
}
309+
},
310+
}
311+
312+
@pytest.mark.django_db
313+
def test_to_representation__with_prefiltered_aspect_ratio_and_source__raise_value_error(
314+
self, image_upload_file, settings
315+
):
316+
settings.PICTURES["USE_PLACEHOLDERS"] = False
317+
318+
profile = models.Profile.objects.create(picture=image_upload_file)
319+
320+
serializer = ProfileSerializerWithInvalidData(profile)
321+
with pytest.raises(ValueError) as e:
322+
serializer.data["image_invalid"]
323+
324+
assert (
325+
str(e.value)
326+
== "Invalid ratio 21/11 or image source GIF. Choices are: 1/1, 3/2, 16/9"
327+
)

0 commit comments

Comments
 (0)