diff --git a/src/pretix/helpers/templatetags/thumb.py b/src/pretix/helpers/templatetags/thumb.py index d3895a5570..c6d6e05077 100644 --- a/src/pretix/helpers/templatetags/thumb.py +++ b/src/pretix/helpers/templatetags/thumb.py @@ -22,10 +22,11 @@ import logging from django import template +from django.core.cache import cache from django.core.files.storage import default_storage from pretix import settings -from pretix.helpers.thumb import get_thumbnail +from pretix.helpers.thumb import get_srcset_sizes, get_thumbnail register = template.Library() logger = logging.getLogger(__name__) @@ -46,3 +47,30 @@ def thumb(source, arg): # default_storage.url works for all files in NanoCDNStorage. For others, this may return an invalid URL. # But for a fallback, this can probably be accepted. return source.url if hasattr(source, 'url') else default_storage.url(str(source)) + + +@register.filter +def thumbset(source, arg): + cache_key = f"thumbset:{source}:{arg}" + cached_thumbset = cache.get(cache_key) + if cached_thumbset is not None: + return cached_thumbset + formats = list(set().union( + settings.PILLOW_FORMATS_IMAGE, + settings.PILLOW_FORMATS_QUESTIONS_FAVICON, + settings.PILLOW_FORMATS_QUESTIONS_IMAGE + )) + + srcs = [] + if not cached_thumbset: + for thumbsize, factor in get_srcset_sizes(arg): + try: + t = get_thumbnail(source, thumbsize, formats=formats, only_if_resized=True) + if t: + srcs.append(f"{t.thumb.url} {factor}") + except: + logger.exception(f'Failed to create thumbnail of {source} at {thumbsize}') + + srcset = ", ".join(srcs) + cache.set(cache_key, srcset, timeout=3600) + return srcset diff --git a/src/pretix/helpers/thumb.py b/src/pretix/helpers/thumb.py index 5ebf9d70b0..6e8f79a780 100644 --- a/src/pretix/helpers/thumb.py +++ b/src/pretix/helpers/thumb.py @@ -96,6 +96,23 @@ def get_minsize(size): return (min_width, min_height) +def get_srcset_sizes(size): + w, h = size.split("x") + for m in (2, 3): + if w.endswith("_"): + new_w = f"{int(w.rstrip("_")) * m}_" + else: + new_w = f"{int(w) * m}" + if h.endswith("_"): + new_h = f"{int(h.rstrip("_")) * m}_" + elif h.endswith("^"): + new_h = f"{int(h.rstrip("^")) * m}^" + else: + new_h = f"{int(h) * m}" + + yield f"{new_w}x{new_h}", f"{m}x" + + def get_sizes(size, imgsize): crop = False if size.endswith('^'): @@ -141,6 +158,7 @@ def get_sizes(size, imgsize): def resize_image(image, size): # before we calc thumbnail, we need to check and apply EXIF-orientation image = ImageOps.exif_transpose(image) + old_size = image.size new_size, crop = get_sizes(size, image.size) image = image.resize(new_size, resample=Resampling.LANCZOS) @@ -161,11 +179,12 @@ def resize_image(image, size): new_y = (image.height - new_height) // 2 image = image.crop((new_x, new_y, new_x + new_width, new_y + new_height)) + new_size = image.size - return image + return image, new_size != old_size -def create_thumbnail(source, size, formats=None): +def create_thumbnail(source, size, formats=None, only_if_resized=False): source_name = str(source) # HACK: this ensures that the file is opened in binary mode, which is not guaranteed otherwise, esp. for @@ -181,10 +200,17 @@ def create_thumbnail(source, size, formats=None): raise ThumbnailError('Could not load image') frames = [] + any_resized = False durations = [] for f in ImageSequence.Iterator(image): durations.append(f.info.get("duration", 1000)) - frames.append(resize_image(f, size)) + img, resized = resize_image(f, size) + any_resized = any_resized or resized + frames.append(img) + + if not any_resized and only_if_resized: + return + image_out = frames[0] save_kwargs = {} source_ext = os.path.splitext(source_name)[1].lower() @@ -223,10 +249,10 @@ def create_thumbnail(source, size, formats=None): return t -def get_thumbnail(source, size, formats=None): +def get_thumbnail(source, size, formats=None, only_if_resized=False): # Assumes files are immutable try: source_name = str(source) return Thumbnail.objects.get(source=source_name, size=size) except Thumbnail.DoesNotExist: - return create_thumbnail(source, size, formats=formats) + return create_thumbnail(source, size, formats=formats, only_if_resized=only_if_resized) diff --git a/src/pretix/presale/templates/pretixpresale/event/base.html b/src/pretix/presale/templates/pretixpresale/event/base.html index 8cc81165bb..4ffca17b9d 100644 --- a/src/pretix/presale/templates/pretixpresale/event/base.html +++ b/src/pretix/presale/templates/pretixpresale/event/base.html @@ -85,12 +85,14 @@ {% if event_logo and event_logo_image_large %} - + {% elif event_logo %} - + {% else %}

diff --git a/src/pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html b/src/pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html index 8906379acf..ef4b9f9bd6 100644 --- a/src/pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html +++ b/src/pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html @@ -48,7 +48,8 @@ {# Yes, double-escape to prevent XSS in lightbox #} data-lightbox="{{ item.id }}"> {{ item.name }} + srcset="{{ item.picture|thumbset:'60x60^' }}" + alt="{{ item.name }}"/> {% endif %}
@@ -239,7 +240,8 @@ {# Yes, double-escape to prevent XSS in lightbox #} data-lightbox="{{ item.id }}"> {{ item.name }} + srcset="{{ item.picture|thumbset:'60x60^' }}" + alt="{{ item.name }}"/> {% endif %}
diff --git a/src/pretix/presale/templates/pretixpresale/event/fragment_product_list.html b/src/pretix/presale/templates/pretixpresale/event/fragment_product_list.html index 9768683601..6d7e0574f1 100644 --- a/src/pretix/presale/templates/pretixpresale/event/fragment_product_list.html +++ b/src/pretix/presale/templates/pretixpresale/event/fragment_product_list.html @@ -39,6 +39,7 @@ data-lightbox="{{ item.id }}" aria-label="{% blocktrans trimmed with item=item.name %}Show full-size image of {{ item }}{% endblocktrans %}"> {{ item.name }} {% endif %} @@ -258,6 +259,7 @@ data-lightbox="{{ item.id }}" aria-label="{% blocktrans trimmed with item=item.name %}Show full-size image of {{ item }}{% endblocktrans %}"> {{ item.name }} {% endif %} diff --git a/src/pretix/presale/templates/pretixpresale/event/voucher.html b/src/pretix/presale/templates/pretixpresale/event/voucher.html index 317f2be7f9..9cd0fb5b64 100644 --- a/src/pretix/presale/templates/pretixpresale/event/voucher.html +++ b/src/pretix/presale/templates/pretixpresale/event/voucher.html @@ -93,7 +93,8 @@ data-lightbox="{{ item.id }}" aria-label="{% blocktrans trimmed with item=item.name %}Show full-size image of {{ item }}{% endblocktrans %}"> {{ item.name }} + srcset="{{ item.picture|thumbset:'60x60^' }}" + alt="{{ item.name }}"/> {% endif %}
@@ -274,6 +275,7 @@ data-lightbox="{{ item.id }}" aria-label="{% blocktrans trimmed with item=item.name %}Show full-size image of {{ item }}{% endblocktrans %}"> {{ item.name }} {% endif %} diff --git a/src/pretix/presale/templates/pretixpresale/organizers/base.html b/src/pretix/presale/templates/pretixpresale/organizers/base.html index 88798b85fe..6869616319 100644 --- a/src/pretix/presale/templates/pretixpresale/organizers/base.html +++ b/src/pretix/presale/templates/pretixpresale/organizers/base.html @@ -53,13 +53,15 @@ {% endif %} {% if organizer_logo and organizer.settings.organizer_logo_image_large %} - {% elif organizer_logo %} - + {% else %}

{{ organizer.name }}

diff --git a/src/tests/helpers/test_thumb.py b/src/tests/helpers/test_thumb.py index 9757b1d055..acb4832154 100644 --- a/src/tests/helpers/test_thumb.py +++ b/src/tests/helpers/test_thumb.py @@ -26,85 +26,98 @@ from pretix.helpers.thumb import resize_image def test_no_resize(): img = Image.new('RGB', (40, 20)) - img = resize_image(img, "100x100") + img, resized = resize_image(img, "100x100") width, height = img.size + assert not resized assert width == 40 assert height == 20 img = Image.new('RGB', (40, 20)) - img = resize_image(img, "100x100^") + img, resized = resize_image(img, "100x100^") width, height = img.size + assert not resized assert width == 40 assert height == 20 def test_resize(): img = Image.new('RGB', (40, 20)) - img = resize_image(img, "10x10") + img, resized = resize_image(img, "10x10") width, height = img.size + assert resized assert width == 10 assert height == 5 img = Image.new('RGB', (40, 20)) - img = resize_image(img, "100x10") + img, resized = resize_image(img, "100x10") width, height = img.size + assert resized assert width == 20 assert height == 10 img = Image.new('RGB', (40, 20)) - img = resize_image(img, "10x100") + img, resized = resize_image(img, "10x100") width, height = img.size + assert resized assert width == 10 assert height == 5 def test_crop(): img = Image.new('RGB', (40, 20)) - img = resize_image(img, "10x10^") + img, resized = resize_image(img, "10x10^") width, height = img.size + assert resized assert width == 10 assert height == 10 def test_exactsize(): img = Image.new('RGB', (6912, 3456)) - img = resize_image(img, "600_x5000") + img, resized = resize_image(img, "600_x5000") width, height = img.size + assert resized assert width == 600 assert height == 300 img = Image.new('RGB', (60, 20)) - img = resize_image(img, "10_x10") + img, resized = resize_image(img, "10_x10") width, height = img.size + assert resized assert width == 10 assert height == 3 img = Image.new('RGB', (10, 20)) - img = resize_image(img, "10_x10") + img, resized = resize_image(img, "10_x10") width, height = img.size + assert resized assert width == 10 assert height == 10 img = Image.new('RGB', (60, 20)) - img = resize_image(img, "10x10_") + img, resized = resize_image(img, "10x10_") width, height = img.size + assert resized assert width == 10 assert height == 10 img = Image.new('RGB', (20, 60)) - img = resize_image(img, "10x10_") + img, resized = resize_image(img, "10x10_") width, height = img.size + assert resized assert width == 3 assert height == 10 img = Image.new('RGB', (20, 60)) - img = resize_image(img, "10_x10_") + img, resized = resize_image(img, "10_x10_") width, height = img.size + assert resized assert width == 10 assert height == 10 img = Image.new('RGB', (20, 60)) - img = resize_image(img, "100_x100_") + img, resized = resize_image(img, "100_x100_") width, height = img.size + assert resized assert width == 100 assert height == 100