forked from CGM_Public/pretix_original
Lazy-load TTF fonts for PDFs
This commit is contained in:
@@ -126,6 +126,7 @@ dev = [
|
||||
"pytest-sugar",
|
||||
"pytest-xdist==3.8.*",
|
||||
"pytest-playwright",
|
||||
"pytest-memray",
|
||||
"pytest==9.1.*",
|
||||
"playwright",
|
||||
"responses",
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -20,9 +20,13 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
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(
|
||||
'<img src="/etc/passwd" />',
|
||||
)
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
Reference in New Issue
Block a user