From 69ea5b406907a5b6ce91d652519d4e0ed5ea8976 Mon Sep 17 00:00:00 2001 From: Kara Engelhardt Date: Tue, 2 Jun 2026 15:28:11 +0200 Subject: [PATCH] Implement basic font fallback pretixprint-PR: https://github.com/pretix/libpretixprint/pull/10 --- src/pretix/base/pdf.py | 51 +++++++++++++++++++++------------ src/pretix/helpers/reportlab.py | 51 +++++++++++++++++---------------- 2 files changed, 60 insertions(+), 42 deletions(-) diff --git a/src/pretix/base/pdf.py b/src/pretix/base/pdf.py index a8cb2fc2c3..23f028c42f 100644 --- a/src/pretix/base/pdf.py +++ b/src/pretix/base/pdf.py @@ -83,7 +83,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 @@ -805,7 +806,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): @@ -1004,7 +1008,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", "
\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 = "
".join(get_display(reshaper.reshape(l)) for l in text.split("
")) + 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 @@ -1018,6 +1040,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) @@ -1046,21 +1076,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", "
\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 = "
".join(get_display(reshaper.reshape(l)) for l in text.split("
")) - except: - logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text))) p = Paragraph(text, style=style) return p, ad, lineheight diff --git a/src/pretix/helpers/reportlab.py b/src/pretix/helpers/reportlab.py index 5a39ac4a5b..68b67d1e0d 100644 --- a/src/pretix/helpers/reportlab.py +++ b/src/pretix/helpers/reportlab.py @@ -70,37 +70,40 @@ reshaper = SimpleLazyObject(lambda: ArabicReshaper(configuration={ })) +def font_supports_text(text, font_name): + if not text: + return True + font = pdfmetrics.getFont(font_name) + 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): + 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) - 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 - def register_ttf_font_if_new(name, path): from reportlab.pdfbase import pdfmetrics