Compare commits

...

7 Commits

Author SHA1 Message Date
Martin Gross
de1e7abc48 base/pdf: Only call get_fonts() once per generation 2024-03-08 11:23:31 +01:00
Martin Gross
5fa5810670 Invoices: Add support for event-fonts 2024-02-28 22:03:43 +01:00
Martin Gross
572fcf4751 Editor/webfonts.css: Do not create a font section for "pdf_only" 2024-02-28 21:46:33 +01:00
Martin Gross
c11f718253 isort 2024-02-28 21:31:39 +01:00
Martin Gross
7949be15b7 Review Comments: Do not run signal for every single textbox on page 2024-02-28 21:30:03 +01:00
Martin Gross
cc020f24a2 Review Comments: Check for // instead of https for web-webfonts 2024-02-28 21:21:52 +01:00
Martin Gross
f0a76a3ee0 Add register_event_fonts signal and facilities for web-embedded webfonts 2024-02-13 10:20:12 +01:00
14 changed files with 118 additions and 30 deletions

View File

@@ -84,6 +84,8 @@ convenient to you:
.. automethod:: _register_fonts .. automethod:: _register_fonts
.. automethod:: _register_event_fonts
.. automethod:: _on_first_page .. automethod:: _on_first_page
.. automethod:: _on_other_page .. automethod:: _on_other_page

View File

@@ -182,7 +182,7 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
pdfmetrics.registerFontFamily('OpenSans', normal='OpenSans', bold='OpenSansBd', pdfmetrics.registerFontFamily('OpenSans', normal='OpenSans', bold='OpenSansBd',
italic='OpenSansIt', boldItalic='OpenSansBI') italic='OpenSansIt', boldItalic='OpenSansBI')
for family, styles in get_fonts().items(): for family, styles in get_fonts(event=self.event, pdf_support_required=True).items():
if family == self.event.settings.invoice_renderer_font: if family == self.event.settings.invoice_renderer_font:
pdfmetrics.registerFont(TTFont(family, finders.find(styles['regular']['truetype']))) pdfmetrics.registerFont(TTFont(family, finders.find(styles['regular']['truetype'])))
self.font_regular = family self.font_regular = family

View File

@@ -20,7 +20,7 @@
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
# #
from collections import OrderedDict from collections import OrderedDict
from urllib.parse import urlsplit from urllib.parse import urlparse, urlsplit
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from django.conf import settings from django.conf import settings
@@ -40,6 +40,7 @@ from pretix.base.settings import global_settings_object
from pretix.multidomain.urlreverse import ( from pretix.multidomain.urlreverse import (
get_event_domain, get_organizer_domain, get_event_domain, get_organizer_domain,
) )
from pretix.presale.style import get_fonts
_supported = None _supported = None
@@ -240,6 +241,14 @@ class SecurityMiddleware(MiddlewareMixin):
) )
def process_response(self, request, resp): def process_response(self, request, resp):
def nested_dict_values(d):
for v in d.values():
if isinstance(v, dict):
yield from nested_dict_values(v)
else:
if isinstance(v, str):
yield v
url = resolve(request.path_info) url = resolve(request.path_info)
if settings.DEBUG and resp.status_code >= 400: if settings.DEBUG and resp.status_code >= 400:
@@ -259,6 +268,14 @@ class SecurityMiddleware(MiddlewareMixin):
if gs.settings.leaflet_tiles: if gs.settings.leaflet_tiles:
img_src.append(gs.settings.leaflet_tiles[:gs.settings.leaflet_tiles.index("/", 10)].replace("{s}", "*")) img_src.append(gs.settings.leaflet_tiles[:gs.settings.leaflet_tiles.index("/", 10)].replace("{s}", "*"))
font_src = set()
if hasattr(request, 'event'):
for font in get_fonts(request.event, pdf_support_required=False).values():
for path in list(nested_dict_values(font)):
font_location = urlparse(path)
if font_location.scheme and font_location.netloc:
font_src.add('{}://{}'.format(font_location.scheme, font_location.netloc))
h = { h = {
'default-src': ["{static}"], 'default-src': ["{static}"],
'script-src': ['{static}'], 'script-src': ['{static}'],
@@ -267,7 +284,7 @@ class SecurityMiddleware(MiddlewareMixin):
'style-src': ["{static}", "{media}"], 'style-src': ["{static}", "{media}"],
'connect-src': ["{dynamic}", "{media}"], 'connect-src': ["{dynamic}", "{media}"],
'img-src': ["{static}", "{media}", "data:"] + img_src, 'img-src': ["{static}", "{media}", "data:"] + img_src,
'font-src': ["{static}"], 'font-src': ["{static}"] + list(font_src),
'media-src': ["{static}", "data:"], 'media-src': ["{static}", "data:"],
# form-action is not only used to match on form actions, but also on URLs # form-action is not only used to match on form actions, but also on URLs
# form-actions redirect to. In the context of e.g. payment providers or # form-actions redirect to. In the context of e.g. payment providers or

View File

@@ -78,7 +78,7 @@ from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import Paragraph from reportlab.platypus import Paragraph
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.models import Order, OrderPosition, Question from pretix.base.models import Event, Order, OrderPosition, Question
from pretix.base.settings import PERSON_NAME_SCHEMES from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import layout_image_variables, layout_text_variables from pretix.base.signals import layout_image_variables, layout_text_variables
from pretix.base.templatetags.money import money_filter from pretix.base.templatetags.money import money_filter
@@ -738,9 +738,10 @@ class Renderer:
else: else:
self.bg_bytes = None self.bg_bytes = None
self.bg_pdf = None self.bg_pdf = None
self.event_fonts = list(get_fonts(event, pdf_support_required=True).keys()) + ['Open Sans']
@classmethod @classmethod
def _register_fonts(cls): def _register_fonts(cls, event: Event = None):
if hasattr(cls, '_fonts_registered'): if hasattr(cls, '_fonts_registered'):
return return
pdfmetrics.registerFont(TTFont('Open Sans', finders.find('fonts/OpenSans-Regular.ttf'))) pdfmetrics.registerFont(TTFont('Open Sans', finders.find('fonts/OpenSans-Regular.ttf')))
@@ -748,7 +749,7 @@ class Renderer:
pdfmetrics.registerFont(TTFont('Open Sans B', finders.find('fonts/OpenSans-Bold.ttf'))) pdfmetrics.registerFont(TTFont('Open Sans B', finders.find('fonts/OpenSans-Bold.ttf')))
pdfmetrics.registerFont(TTFont('Open Sans B I', finders.find('fonts/OpenSans-BoldItalic.ttf'))) pdfmetrics.registerFont(TTFont('Open Sans B I', finders.find('fonts/OpenSans-BoldItalic.ttf')))
for family, styles in get_fonts().items(): for family, styles in get_fonts(event, pdf_support_required=True).items():
pdfmetrics.registerFont(TTFont(family, finders.find(styles['regular']['truetype']))) pdfmetrics.registerFont(TTFont(family, finders.find(styles['regular']['truetype'])))
if 'italic' in styles: if 'italic' in styles:
pdfmetrics.registerFont(TTFont(family + ' I', finders.find(styles['italic']['truetype']))) pdfmetrics.registerFont(TTFont(family + ' I', finders.find(styles['italic']['truetype'])))
@@ -939,6 +940,12 @@ class Renderer:
if o['italic']: if o['italic']:
font += ' I' font += ' I'
# Since pdfmetrics.registerFont is global, we want to make sure that no one tries to sneak in a font, they
# should not have access to.
if font not in self.event_fonts:
logger.warning(f'Unauthorized use of font "{font}"')
font = 'Open Sans'
try: try:
ad = getAscentDescent(font, float(o['fontsize'])) ad = getAscentDescent(font, float(o['fontsize']))
except KeyError: # font not known, fall back except KeyError: # font not known, fall back

View File

@@ -89,7 +89,7 @@ def primary_font_kwargs():
choices = [('Open Sans', 'Open Sans')] choices = [('Open Sans', 'Open Sans')]
choices += sorted([ choices += sorted([
(a, {"title": a, "data": v}) for a, v in get_fonts().items() if not v.get('pdf_only', False) (a, {"title": a, "data": v}) for a, v in get_fonts(pdf_support_required=False).items()
], key=lambda a: a[0]) ], key=lambda a: a[0])
return { return {
'choices': choices, 'choices': choices,

View File

@@ -79,6 +79,7 @@ from pretix.helpers.countries import CachedCountries
from pretix.multidomain.models import KnownDomain from pretix.multidomain.models import KnownDomain
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.plugins.banktransfer.payment import BankTransfer from pretix.plugins.banktransfer.payment import BankTransfer
from pretix.presale.style import get_fonts
class EventWizardFoundationForm(forms.Form): class EventWizardFoundationForm(forms.Form):
@@ -651,6 +652,9 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
del self.fields['event_list_available_only'] del self.fields['event_list_available_only']
del self.fields['event_list_filters'] del self.fields['event_list_filters']
del self.fields['event_calendar_future_only'] del self.fields['event_calendar_future_only']
self.fields['primary_font'].choices += [
(a, {"title": a, "data": v}) for a, v in get_fonts(self.event, pdf_support_required=False).items()
]
# create "virtual" fields for better UX when editing <name>_asked and <name>_required fields # create "virtual" fields for better UX when editing <name>_asked and <name>_required fields
self.virtual_keys = [] self.virtual_keys = []
@@ -931,6 +935,9 @@ class InvoiceSettingsForm(EventSettingsValidationMixin, SettingsForm):
) )
) )
self.fields['invoice_generate'].choices = generate_choices self.fields['invoice_generate'].choices = generate_choices
self.fields['invoice_renderer_font'].choices += [
(a, a) for a in get_fonts(event, pdf_support_required=True).keys()
]
def contains_web_channel_validate(val): def contains_web_channel_validate(val):

View File

@@ -7,7 +7,7 @@
{% block title %}{% trans "General settings" %}{% endblock %} {% block title %}{% trans "General settings" %}{% endblock %}
{% block custom_header %} {% block custom_header %}
{{ block.super }} {{ block.super }}
<link type="text/css" rel="stylesheet" href="{% url "control:pdf.css" %}"> <link type="text/css" rel="stylesheet" href="{% url "control:pdf.css" organizer=request.organizer.slug event=request.event.slug %}">
{% endblock %} {% endblock %}
{% block inside %} {% block inside %}
<h1>{% trans "General settings" %}</h1> <h1>{% trans "General settings" %}</h1>

View File

@@ -8,7 +8,7 @@
{% compress css %} {% compress css %}
<link type="text/css" rel="stylesheet" href="{% static "pretixcontrol/scss/pdfeditor.css" %}"> <link type="text/css" rel="stylesheet" href="{% static "pretixcontrol/scss/pdfeditor.css" %}">
{% endcompress %} {% endcompress %}
<link type="text/css" rel="stylesheet" href="{% url "control:pdf.css" %}"> <link type="text/css" rel="stylesheet" href="{% url "control:pdf.css" organizer=request.organizer.slug event=request.event.slug %}">
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h1> <h1>

View File

@@ -1,4 +1,5 @@
{% load static %} {% load static %}
@font-face { @font-face {
font-family: 'AND'; font-family: 'AND';
font-style: normal; font-style: normal;
@@ -14,7 +15,7 @@
{% for family, styles in fonts.items %} {% for family, styles in fonts.items %}
{% for style, formats in styles.items %} {% for style, formats in styles.items %}
{% if "sample" not in style %} {% if "sample" not in style and "pdf_only" not in style %}
@font-face { @font-face {
font-family: '{{ family }}'; font-family: '{{ family }}';
{% if style == "italic" or style == "bolditalic" %} {% if style == "italic" or style == "bolditalic" %}
@@ -27,9 +28,9 @@
{% else %} {% else %}
font-weight: normal; font-weight: normal;
{% endif %} {% endif %}
src: {% if "woff2" in formats %}url('{% static formats.woff2 %}') format('woff2'),{% endif %} src: {% if "woff2" in formats %}{% if '//' in formats.woff2 %}url('{{ formats.woff2 }}'){% else %}url('{% static formats.woff2 %}'){% endif %} format('woff2'),{% endif %}
{% if "woff" in formats %}url('{% static formats.woff %}') format('woff'),{% endif %} {% if "woff" in formats %}{% if '//' in formats.woff %}url('{{ formats.woff }}'){% else %}url('{% static formats.woff %}'){% endif %} format('woff'),{% endif %}
{% if "truetype" in formats %}url('{% static formats.truetype %}') format('truetype'){% endif %}; {% if "truetype" in formats %}{% if '//' in formats.truetype %}url('{{ formats.truetype }}'){% else %}url('{% static formats.truetype %}'){% endif %} format('truetype'){% endif %};
} }
.preload-font[data-family="{{family}}"][data-style="{{style}}"] { .preload-font[data-family="{{family}}"][data-style="{{style}}"] {
font-family: '{{ family }}', 'AND'; font-family: '{{ family }}', 'AND';

View File

@@ -262,7 +262,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs) ctx = super().get_context_data(**kwargs)
ctx['fonts'] = get_fonts() ctx['fonts'] = get_fonts(self.request.event, pdf_support_required=True)
ctx['pdf'] = self.get_current_background() ctx['pdf'] = self.get_current_background()
ctx['variables'] = self.get_variables() ctx['variables'] = self.get_variables()
ctx['images'] = self.get_images() ctx['images'] = self.get_images()
@@ -278,7 +278,7 @@ class FontsCSSView(TemplateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs) ctx = super().get_context_data(**kwargs)
ctx['fonts'] = get_fonts() ctx['fonts'] = get_fonts(self.request.event if hasattr(self.request, 'event') else None, pdf_support_required=True)
return ctx return ctx

View File

@@ -41,7 +41,7 @@ from pretix.plugins.ticketoutputpdf.models import (
TicketLayout, TicketLayoutItem, TicketLayout, TicketLayoutItem,
) )
from pretix.presale.style import ( # NOQA: legacy import from pretix.presale.style import ( # NOQA: legacy import
get_fonts, register_fonts, get_fonts, register_event_fonts, register_fonts,
) )

View File

@@ -92,7 +92,7 @@ class PdfTicketOutput(BaseTicketOutput):
return self.event._ticketoutputpdf_cache_default_layout return self.event._ticketoutputpdf_cache_default_layout
def _register_fonts(self): def _register_fonts(self):
Renderer._register_fonts() Renderer._register_fonts(self.event)
def _draw_page(self, layout: TicketLayout, op: OrderPosition, order: Order): def _draw_page(self, layout: TicketLayout, op: OrderPosition, order: Order):
buffer = BytesIO() buffer = BytesIO()

View File

@@ -264,7 +264,7 @@ class LayoutEditorView(BaseEditorView):
return static('pretixpresale/pdf/ticket_default_a4.pdf') return static('pretixpresale/pdf/ticket_default_a4.pdf')
def generate(self, op: OrderPosition, override_layout=None, override_background=None): def generate(self, op: OrderPosition, override_layout=None, override_background=None):
Renderer._register_fonts() Renderer._register_fonts(self.request.event)
buffer = BytesIO() buffer = BytesIO()
if override_background: if override_background:

View File

@@ -41,6 +41,7 @@ from pretix.base.models import Event, Event_SettingsStore, Organizer
from pretix.base.services.tasks import ( from pretix.base.services.tasks import (
TransactionAwareProfiledEventTask, TransactionAwareTask, TransactionAwareProfiledEventTask, TransactionAwareTask,
) )
from pretix.base.signals import EventPluginSignal
from pretix.celery_app import app from pretix.celery_app import app
from pretix.multidomain.urlreverse import ( from pretix.multidomain.urlreverse import (
get_event_domain, get_organizer_domain, get_event_domain, get_organizer_domain,
@@ -86,7 +87,7 @@ def compile_scss(object, file="main.scss", fonts=True):
font = object.settings.get('primary_font') font = object.settings.get('primary_font')
if font != 'Open Sans' and fonts: if font != 'Open Sans' and fonts:
sassrules.append(get_font_stylesheet(font)) sassrules.append(get_font_stylesheet(font, event=object if isinstance(object, Event) else None))
sassrules.append( sassrules.append(
'$font-family-sans-serif: "{}", "Open Sans", "OpenSans", "Helvetica Neue", Helvetica, Arial, sans-serif ' '$font-family-sans-serif: "{}", "Open Sans", "OpenSans", "Helvetica Neue", Helvetica, Arial, sans-serif '
'!default'.format( '!default'.format(
@@ -202,7 +203,8 @@ def regenerate_organizer_css(organizer_id: int, regenerate_events=True):
register_fonts = Signal() register_fonts = Signal()
""" """
Return a dictionaries of the following structure. Paths should be relative to static root. Return a dictionaries of the following structure. Paths should be relative to static root or an absolute URL. In the
latter case, the fonts won't be available for PDF-rendering.
{ {
"font name": { "font name": {
@@ -225,17 +227,69 @@ Return a dictionaries of the following structure. Paths should be relative to st
} }
""" """
register_event_fonts = EventPluginSignal()
"""
Return a dictionaries of the following structure. Paths should be relative to static root or an absolute URL. In the
latter case, the fonts won't be available for PDF-rendering.
As with all plugin signals, the ``sender`` keyword argument will contain the event.
{
"font name": {
"regular": {
"truetype": "….ttf",
"woff": "",
"woff2": ""
},
"bold": {
...
},
"italic": {
...
},
"bolditalic": {
...
},
"pdf_only": False, # if True, font is not usable on the web,
}
}
"""
def get_fonts(event: Event = None, pdf_support_required=False):
def nested_dict_values(d):
for v in d.values():
if isinstance(v, dict):
yield from nested_dict_values(v)
else:
if isinstance(v, str):
yield v
def get_fonts():
f = {} f = {}
received_fonts = {}
for recv, value in register_fonts.send(0): for recv, value in register_fonts.send(0):
f.update(value) received_fonts.update(value)
if event:
for recv, value in register_event_fonts.send(event):
received_fonts.update(value)
for font, payload in received_fonts.items():
if pdf_support_required:
if any('//' in v for v in list(nested_dict_values(payload))):
continue
f.update({font: payload})
else:
if payload.get('pdf_only', False):
continue
f.update({font: payload})
return f return f
def get_font_stylesheet(font_name): def get_font_stylesheet(font_name, event: Event = None):
stylesheet = [] stylesheet = []
font = get_fonts()[font_name] font = get_fonts(event)[font_name]
for sty, formats in font.items(): for sty, formats in font.items():
if sty == 'sample': if sty == 'sample':
continue continue
@@ -251,12 +305,12 @@ def get_font_stylesheet(font_name):
stylesheet.append("font-weight: normal;") stylesheet.append("font-weight: normal;")
srcs = [] srcs = []
if "woff2" in formats: for f in ["woff2", "woff", "truetype"]:
srcs.append("url(static('{}')) format('woff2')".format(formats['woff2'])) if f in formats:
if "woff" in formats: if formats[f].startswith('https'):
srcs.append("url(static('{}')) format('woff')".format(formats['woff'])) srcs.append(f"url('{formats[f]}') format('{f}')")
if "truetype" in formats: else:
srcs.append("url(static('{}')) format('truetype')".format(formats['truetype'])) srcs.append(f"url(static('{formats[f]}')) format('{f}')")
stylesheet.append("src: {};".format(", ".join(srcs))) stylesheet.append("src: {};".format(", ".join(srcs)))
stylesheet.append("font-display: swap;") stylesheet.append("font-display: swap;")
stylesheet.append("}") stylesheet.append("}")