mirror of
https://github.com/pretix/pretix.git
synced 2026-10-01 19:14:43 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24de110fe9 | ||
|
|
77adaa69fe |
@@ -126,6 +126,7 @@ dev = [
|
||||
"pytest-sugar",
|
||||
"pytest-xdist==3.8.*",
|
||||
"pytest-playwright",
|
||||
"pytest-memray",
|
||||
"pytest==9.1.*",
|
||||
"playwright",
|
||||
"responses",
|
||||
|
||||
+33
-18
@@ -84,7 +84,8 @@ from pretix.base.templatetags.money import money_filter
|
||||
from pretix.base.templatetags.phone_format import phone_format
|
||||
from pretix.helpers.daterange import datetimerange
|
||||
from pretix.helpers.reportlab import (
|
||||
ThumbnailingImageReader, register_ttf_font_if_new, reshaper,
|
||||
ThumbnailingImageReader, find_font_supporting_text,
|
||||
register_ttf_font_if_new, reshaper,
|
||||
)
|
||||
from pretix.presale.style import get_fonts
|
||||
|
||||
@@ -827,7 +828,10 @@ class Renderer:
|
||||
else:
|
||||
self.bg_bytes = None
|
||||
self.bg_pdf = None
|
||||
self.event_fonts = list(get_fonts(event, pdf_support_required=True).keys()) + ['Open Sans']
|
||||
|
||||
event_fonts = get_fonts(event, pdf_support_required=True) | {'Open Sans': {"bold", "italic", "bolditalic"}}
|
||||
# sorted by font name to match ordering of libpretixprint
|
||||
self.event_fonts = dict(sorted(event_fonts.items(), key=lambda x: x[0]))
|
||||
|
||||
@classmethod
|
||||
def _register_fonts(cls, event: Event = None):
|
||||
@@ -1026,7 +1030,25 @@ class Renderer:
|
||||
)
|
||||
canvas.restoreState()
|
||||
|
||||
def _text_paragraph(self, op: OrderPosition, order: Order, o: dict, legacy_lineheight=False, override_fontsize=None):
|
||||
def _prepare_text_paragraph_text(self, op: OrderPosition, order: Order, o: dict):
|
||||
# add an almost-invisible space   after hyphens as word-wrap in ReportLab only works on space chars
|
||||
text = conditional_escape(
|
||||
self._get_text_content(op, order, o) or "",
|
||||
).replace("\n", "<br/>\n").replace("-", "- ")
|
||||
|
||||
# reportlab does not support unicode combination characters
|
||||
# It's important we do this before we use ArabicReshaper
|
||||
text = unicodedata.normalize("NFC", text)
|
||||
|
||||
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
|
||||
# to resolve all ligatures and python-bidi to switch RTL texts.
|
||||
try:
|
||||
text = "<br/>".join(get_display(reshaper.reshape(l)) for l in text.split("<br/>"))
|
||||
except:
|
||||
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
|
||||
return text
|
||||
|
||||
def _get_text_paragraph_font(self, o: dict, text: str):
|
||||
font = o['fontfamily']
|
||||
|
||||
# Since pdfmetrics.registerFont is global, we want to make sure that no one tries to sneak in a font, they
|
||||
@@ -1040,6 +1062,14 @@ class Renderer:
|
||||
if o['italic']:
|
||||
font += ' I'
|
||||
|
||||
font = find_font_supporting_text(self.event_fonts, text, font)
|
||||
|
||||
return font
|
||||
|
||||
def _text_paragraph(self, op: OrderPosition, order: Order, o: dict, legacy_lineheight=False, override_fontsize=None):
|
||||
text = self._prepare_text_paragraph_text(op, order, o)
|
||||
font = self._get_text_paragraph_font(o, text)
|
||||
|
||||
fontsize = override_fontsize if override_fontsize is not None else float(o['fontsize'])
|
||||
try:
|
||||
ad = getAscentDescent(font, fontsize)
|
||||
@@ -1068,21 +1098,6 @@ class Renderer:
|
||||
alignment=align_map[o['align']],
|
||||
splitLongWords=o.get('splitlongwords', True),
|
||||
)
|
||||
# add an almost-invisible space   after hyphens as word-wrap in ReportLab only works on space chars
|
||||
text = conditional_escape(
|
||||
self._get_text_content(op, order, o) or "",
|
||||
).replace("\n", "<br/>\n").replace("-", "- ")
|
||||
|
||||
# reportlab does not support unicode combination characters
|
||||
# It's important we do this before we use ArabicReshaper
|
||||
text = unicodedata.normalize("NFC", text)
|
||||
|
||||
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
|
||||
# to resolve all ligatures and python-bidi to switch RTL texts.
|
||||
try:
|
||||
text = "<br/>".join(get_display(reshaper.reshape(l)) for l in text.split("<br/>"))
|
||||
except:
|
||||
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
|
||||
|
||||
p = Paragraph(text, style=style) # not using AutoEscapeParagraph is safe as we escape above
|
||||
return p, ad, lineheight
|
||||
|
||||
@@ -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
|
||||
@@ -89,39 +89,50 @@ def normalize_text(text: str) -> str:
|
||||
return text
|
||||
|
||||
|
||||
def font_supports_text(text, font_name):
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
|
||||
if not text:
|
||||
return True
|
||||
font = pdfmetrics.getFont(font_name)
|
||||
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
|
||||
|
||||
|
||||
def find_font_supporting_text(fonts, text, preferred_font):
|
||||
if font_supports_text(text, preferred_font):
|
||||
return preferred_font
|
||||
for family, styles in fonts.items():
|
||||
if font_supports_text(text, family):
|
||||
if (preferred_font.endswith("It") or preferred_font.endswith(" I")) and "italic" in styles:
|
||||
return family + " I"
|
||||
if (preferred_font.endswith("Bd") or preferred_font.endswith(" B")) and "bold" in styles:
|
||||
return family + " B"
|
||||
return family
|
||||
return preferred_font
|
||||
|
||||
|
||||
class FontFallbackParagraph(Paragraph):
|
||||
def __init__(self, text, style=None, *args, **kwargs):
|
||||
if style is None:
|
||||
style = ParagraphStyle(name='paragraphImplicitDefaultStyle')
|
||||
|
||||
if not self._font_supports_text(text, style.fontName):
|
||||
newFont = self._find_font(text, style.fontName)
|
||||
if newFont:
|
||||
logger.debug(f"replacing {style.fontName} with {newFont} for {text!r}")
|
||||
style = style.clone(name=style.name + '_' + newFont, fontName=newFont)
|
||||
|
||||
supporting_font = find_font_supporting_text(get_fonts(pdf_support_required=True), text, style.fontName)
|
||||
if supporting_font != style.fontName:
|
||||
logger.debug(f"replacing {style.fontName} with {supporting_font} for {text!r}")
|
||||
style = style.clone(name=style.name + '_' + supporting_font, fontName=supporting_font)
|
||||
super().__init__(text, style, *args, **kwargs)
|
||||
|
||||
def _font_supports_text(self, text, font_name):
|
||||
if not text:
|
||||
return True
|
||||
font = pdfmetrics.getFont(font_name)
|
||||
if not isinstance(font, TTFont):
|
||||
return True
|
||||
return all(
|
||||
ord(c) in font.face.charToGlyph or not c.isprintable()
|
||||
for c in text
|
||||
)
|
||||
|
||||
def _find_font(self, text, original_font):
|
||||
for family, styles in get_fonts(pdf_support_required=True).items():
|
||||
if self._font_supports_text(text, family):
|
||||
if (original_font.endswith("It") or original_font.endswith(" I")) and "italic" in styles:
|
||||
return family + " I"
|
||||
if (original_font.endswith("Bd") or original_font.endswith(" B")) and "bold" in styles:
|
||||
return family + " B"
|
||||
return family
|
||||
|
||||
|
||||
class PlainTextParagraph(FontFallbackParagraph):
|
||||
def __init__(self, text, style=None, linebreaks=True, *args, **kwargs):
|
||||
@@ -141,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