diff --git a/pyproject.toml b/pyproject.toml
index 9f062bd19..ac78302ca 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -126,6 +126,7 @@ dev = [
"pytest-sugar",
"pytest-xdist==3.8.*",
"pytest-playwright",
+ "pytest-memray",
"pytest==9.1.*",
"playwright",
"responses",
diff --git a/src/pretix/helpers/reportlab.py b/src/pretix/helpers/reportlab.py
index 200f78fce..6462291eb 100644
--- a/src/pretix/helpers/reportlab.py
+++ b/src/pretix/helpers/reportlab.py
@@ -26,13 +26,13 @@ import unicodedata
from arabic_reshaper import ArabicReshaper
from bidi import get_display
from django.conf import settings
-from django.utils.functional import SimpleLazyObject
+from django.core.cache import cache
+from django.utils.functional import SimpleLazyObject, cached_property
from django.utils.html import escape
from PIL import Image
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.utils import ImageReader
from reportlab.pdfbase import pdfmetrics
-from reportlab.pdfbase.ttfonts import TTFont
from reportlab.platypus import Paragraph
from pretix.presale.style import get_fonts
@@ -90,15 +90,23 @@ def normalize_text(text: str) -> str:
def font_supports_text(text, font_name):
+ from reportlab.pdfbase.ttfonts import TTFont
+
if not text:
return True
font = pdfmetrics.getFont(font_name)
- if not isinstance(font, TTFont):
+ if isinstance(font, TTFont):
+ return all(
+ ord(c) in font.face.charToGlyph or not c.isprintable()
+ for c in text
+ )
+ elif isinstance(font, LazyTTFont):
+ return all(
+ ord(c) in font.face.supported_glyphs or not c.isprintable()
+ for c in text
+ )
+ else: # unsupported
return True
- return all(
- ord(c) in font.face.charToGlyph or not c.isprintable()
- for c in text
- )
def find_font_supporting_text(fonts, text, preferred_font):
@@ -144,9 +152,64 @@ class PlainTextParagraph(FontFallbackParagraph):
super().__init__(text, style, *args, **kwargs)
+class LazyTTFontFace:
+ def __init__(self, *args, **kwargs):
+ self.__args = args
+ self.__kwargs = kwargs
+ self.__casted = None
+
+ def __cast(self):
+ if self.__casted is None:
+ from reportlab.pdfbase.ttfonts import TTFontFace
+ print("casting TTFontFace", self.__args)
+ self.__casted = TTFontFace(*self.__args, **self.__kwargs)
+ return self.__casted
+
+ def __getattr__(self, item):
+ print("attr", item)
+ return getattr(self.__cast(), item)
+
+ @cached_property
+ def supported_glyphs(self):
+ if self.__casted is not None:
+ return set(self.__casted.charToGlyph.keys())
+ return cache.get_or_set(
+ key=f'font_ttf_glyphs:{self.__args[0]}',
+ default=lambda: set(self.__cast().charToGlyph.keys())
+ )
+
+ @property
+ def name(self):
+ if self.__casted is not None:
+ return self.__casted.name
+ return cache.get_or_set(
+ key=f'font_ttf_name:{self.__args[0]}',
+ default=lambda: self.__cast().name
+ )
+
+
+class LazyTTFont:
+ _multiByte = 1 # noqa
+ _dynamicFont = 1 # noqa
+
+ def __init__(self, name, filename, validate=0, subfontIndex=0): # noqa
+ self.fontName = name
+ self.__file = filename
+ self.__casted = None
+ self.face = LazyTTFontFace(filename, validate=validate, subfontIndex=subfontIndex)
+
+ def __cast(self):
+ if self.__casted is None:
+ from reportlab.pdfbase.ttfonts import TTFont
+ self.__casted = TTFont(self.fontName, self.__file)
+ return self.__casted
+
+ def __getattr__(self, item):
+ return getattr(self.__cast(), item)
+
+
def register_ttf_font_if_new(name, path):
from reportlab.pdfbase import pdfmetrics
- from reportlab.pdfbase.ttfonts import TTFont
if name not in pdfmetrics.getRegisteredFontNames():
- pdfmetrics.registerFont(TTFont(name, path))
+ pdfmetrics.registerFont(LazyTTFont(name, path))
diff --git a/src/tests/helpers/test_reportlab.py b/src/tests/helpers/test_reportlab.py
index 354e2cbd1..9a277f50b 100644
--- a/src/tests/helpers/test_reportlab.py
+++ b/src/tests/helpers/test_reportlab.py
@@ -20,9 +20,13 @@
# .
#
import pytest
+from django.contrib.staticfiles import finders
from django.core.exceptions import SuspiciousFileOperation
+from reportlab.pdfbase import pdfmetrics
from reportlab.platypus import Paragraph
+from pretix.helpers.reportlab import LazyTTFont
+
def test_http_access_disabled(monkeypatch):
def guard(*args, **kwargs):
@@ -48,3 +52,14 @@ def test_file_access_disabled_direct(monkeypatch):
Paragraph(
'
',
)
+
+
+@pytest.mark.limit_memory("50KB") # loading Open Sans allocates >300KB
+def test_lazy_font_loading_does_not_load_immediately(monkeypatch):
+ @property
+ def name_prop(self):
+ return 'CachedName'
+
+ monkeypatch.setattr('pretix.helpers.reportlab.LazyTTFontFace.name', name_prop)
+ font = LazyTTFont('Open Sans', finders.find('fonts/OpenSans-Regular.ttf'))
+ pdfmetrics.registerFont(font)