mirror of
https://github.com/pretix/pretix.git
synced 2026-05-06 15:24:02 +00:00
PDF layout: Allow to show photos from questions (#1919)
This commit is contained in:
@@ -12,12 +12,10 @@ from django.utils.formats import date_format, localize
|
||||
from django.utils.translation import (
|
||||
get_language, gettext, gettext_lazy, pgettext,
|
||||
)
|
||||
from PIL.Image import BICUBIC
|
||||
from reportlab.lib import pagesizes
|
||||
from reportlab.lib.enums import TA_LEFT, TA_RIGHT
|
||||
from reportlab.lib.styles import ParagraphStyle, StyleSheet1
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.lib.utils import ImageReader
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.pdfmetrics import stringWidth
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
@@ -31,6 +29,7 @@ from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models import Event, Invoice, Order
|
||||
from pretix.base.signals import register_invoice_renderers
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.helpers.reportlab import ThumbnailingImageReader
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -221,26 +220,6 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
|
||||
return 'invoice.pdf', 'application/pdf', buffer.read()
|
||||
|
||||
|
||||
class ThumbnailingImageReader(ImageReader):
|
||||
def resize(self, width, height, dpi):
|
||||
if width is None:
|
||||
width = height * self._image.size[0] / self._image.size[1]
|
||||
if height is None:
|
||||
height = width * self._image.size[1] / self._image.size[0]
|
||||
self._image.thumbnail(
|
||||
size=(int(width * dpi / 72), int(height * dpi / 72)),
|
||||
resample=BICUBIC
|
||||
)
|
||||
self._data = None
|
||||
return width, height
|
||||
|
||||
def _jpeg_fh(self):
|
||||
# Bypass a reportlab-internal optimization that falls back to the original
|
||||
# file handle if the file is a JPEG, and therefore does not respect the
|
||||
# (smaller) size of the modified image.
|
||||
return None
|
||||
|
||||
|
||||
class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
identifier = 'classic'
|
||||
verbose_name = pgettext('invoice', 'Classic renderer (pretix 1.0)')
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import copy
|
||||
import hashlib
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
@@ -35,9 +36,9 @@ from reportlab.platypus import Paragraph
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.invoice import ThumbnailingImageReader
|
||||
from pretix.base.models import Order, OrderPosition
|
||||
from pretix.base.models import Order, OrderPosition, Question
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.base.signals import 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.phone_format import phone_format
|
||||
from pretix.presale.style import get_fonts
|
||||
@@ -339,6 +340,47 @@ DEFAULT_VARIABLES = OrderedDict((
|
||||
"evaluate": lambda op, order, ev: str(op.seat.seat_number if op.seat else "")
|
||||
}),
|
||||
))
|
||||
DEFAULT_IMAGES = OrderedDict([])
|
||||
|
||||
|
||||
@receiver(layout_image_variables, dispatch_uid="pretix_base_layout_image_variables_questions")
|
||||
def images_from_questions(sender, *args, **kwargs):
|
||||
def get_answer(op, order, event, question_id, etag):
|
||||
a = None
|
||||
if op.addon_to:
|
||||
if 'answers' in getattr(op.addon_to, '_prefetched_objects_cache', {}):
|
||||
try:
|
||||
a = [a for a in op.addon_to.answers.all() if a.question_id == question_id][0]
|
||||
except IndexError:
|
||||
pass
|
||||
else:
|
||||
a = op.addon_to.answers.filter(question_id=question_id).first()
|
||||
|
||||
if 'answers' in getattr(op, '_prefetched_objects_cache', {}):
|
||||
try:
|
||||
a = [a for a in op.answers.all() if a.question_id == question_id][0]
|
||||
except IndexError:
|
||||
pass
|
||||
else:
|
||||
a = op.answers.filter(question_id=question_id).first()
|
||||
|
||||
if not a or not a.file or not any(a.file.name.lower().endswith(e) for e in (".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tif", ".tiff")):
|
||||
return None
|
||||
else:
|
||||
if etag:
|
||||
return hashlib.sha1(a.file.name.encode()).hexdigest()
|
||||
return a.file
|
||||
|
||||
d = {}
|
||||
for q in sender.questions.all():
|
||||
if q.type != Question.TYPE_FILE:
|
||||
continue
|
||||
d['question_{}'.format(q.identifier)] = {
|
||||
'label': _('Question: {question}').format(question=q.question),
|
||||
'evaluate': partial(get_answer, question_id=q.pk, etag=False),
|
||||
'etag': partial(get_answer, question_id=q.pk, etag=True),
|
||||
}
|
||||
return d
|
||||
|
||||
|
||||
@receiver(layout_text_variables, dispatch_uid="pretix_base_layout_text_variables_questions")
|
||||
@@ -369,6 +411,8 @@ def variables_from_questions(sender, *args, **kwargs):
|
||||
|
||||
d = {}
|
||||
for q in sender.questions.all():
|
||||
if q.type == Question.TYPE_FILE:
|
||||
continue
|
||||
d['question_{}'.format(q.pk)] = {
|
||||
'label': _('Question: {question}').format(question=q.question),
|
||||
'editor_sample': _('<Answer: {question}>').format(question=q.question),
|
||||
@@ -387,6 +431,15 @@ def _get_ia_name_part(key, op, order, ev):
|
||||
return order.invoice_address.name_parts.get(key, '') if getattr(order, 'invoice_address', None) else ''
|
||||
|
||||
|
||||
def get_images(event):
|
||||
v = copy.copy(DEFAULT_IMAGES)
|
||||
|
||||
for recv, res in layout_image_variables.send(sender=event):
|
||||
v.update(res)
|
||||
|
||||
return v
|
||||
|
||||
|
||||
def get_variables(event):
|
||||
v = copy.copy(DEFAULT_VARIABLES)
|
||||
|
||||
@@ -427,6 +480,7 @@ class Renderer:
|
||||
self.layout = layout
|
||||
self.background_file = background_file
|
||||
self.variables = get_variables(event)
|
||||
self.images = get_images(event)
|
||||
self.event = event
|
||||
if self.background_file:
|
||||
self.bg_bytes = self.background_file.read()
|
||||
@@ -514,6 +568,47 @@ class Renderer:
|
||||
return '(error)'
|
||||
return ''
|
||||
|
||||
def _draw_imagearea(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
|
||||
ev = self._get_ev(op, order)
|
||||
if not o['content'] or o['content'] not in self.images:
|
||||
image_file = None
|
||||
else:
|
||||
try:
|
||||
image_file = self.images[o['content']]['evaluate'](op, order, ev)
|
||||
except:
|
||||
logger.exception('Failed to process variable.')
|
||||
image_file = None
|
||||
|
||||
if image_file:
|
||||
ir = ThumbnailingImageReader(image_file)
|
||||
try:
|
||||
ir.resize(float(o['width']) * mm, float(o['height']) * mm, 300)
|
||||
except:
|
||||
logger.exception("Can not resize image")
|
||||
pass
|
||||
canvas.drawImage(
|
||||
image=ir,
|
||||
x=float(o['left']) * mm,
|
||||
y=float(o['bottom']) * mm,
|
||||
width=float(o['width']) * mm,
|
||||
height=float(o['height']) * mm,
|
||||
preserveAspectRatio=True,
|
||||
anchor='c', # centered in frame
|
||||
mask='auto'
|
||||
)
|
||||
else:
|
||||
canvas.saveState()
|
||||
canvas.setFillColorRGB(.8, .8, .8, alpha=1)
|
||||
canvas.rect(
|
||||
x=float(o['left']) * mm,
|
||||
y=float(o['bottom']) * mm,
|
||||
width=float(o['width']) * mm,
|
||||
height=float(o['height']) * mm,
|
||||
stroke=0,
|
||||
fill=1,
|
||||
)
|
||||
canvas.restoreState()
|
||||
|
||||
def _draw_textarea(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
|
||||
font = o['fontfamily']
|
||||
if o['bold']:
|
||||
@@ -572,6 +667,8 @@ class Renderer:
|
||||
for o in self.layout:
|
||||
if o['type'] == "barcodearea":
|
||||
self._draw_barcodearea(canvas, op, o)
|
||||
elif o['type'] == "imagearea":
|
||||
self._draw_imagearea(canvas, op, order, o)
|
||||
elif o['type'] == "textarea":
|
||||
self._draw_textarea(canvas, op, order, o)
|
||||
elif o['type'] == "poweredby":
|
||||
|
||||
@@ -613,6 +613,7 @@ If the email is associated with a specific user, e.g. a notification email, the
|
||||
well, otherwise it will be ``None``.
|
||||
"""
|
||||
|
||||
|
||||
layout_text_variables = EventPluginSignal()
|
||||
"""
|
||||
This signal is sent out to collect variables that can be used to display text in ticket-related PDF layouts.
|
||||
@@ -627,11 +628,35 @@ dictionaries as values that contain keys like in the following example::
|
||||
}
|
||||
}
|
||||
|
||||
The evaluate member will be called with the order position, order and event as arguments. The event might
|
||||
The ``evaluate`` member will be called with the order position, order and event as arguments. The event might
|
||||
also be a subevent, if applicable.
|
||||
"""
|
||||
|
||||
|
||||
layout_image_variables = EventPluginSignal()
|
||||
"""
|
||||
This signal is sent out to collect variables that can be used to display dynamic images in ticket-related PDF layouts.
|
||||
Receivers are expected to return a dictionary with globally unique identifiers as keys and more
|
||||
dictionaries as values that contain keys like in the following example::
|
||||
|
||||
return {
|
||||
"profile": {
|
||||
"label": _("Profile picture"),
|
||||
"evaluate": lambda orderposition, order, event: ContentFile(b"some-image-data"),
|
||||
"etag": lambda orderposition, order, event: hash(b"some-image-data")
|
||||
}
|
||||
}
|
||||
|
||||
The ``evaluate`` member will be called with the order position, order and event as arguments. The event might
|
||||
also be a subevent, if applicable. The return value of ``evaluate`` should be an instance of Django's ``File``
|
||||
class and point to a valid JPEG or PNG file. If no image is available, ``evaluate`` should return ``None``.
|
||||
|
||||
The ``etag`` member will be called with the same arguments as ``evaluate`` but should return a ``str`` value
|
||||
uniquely identifying the version of the file. This can be a hash of the file, but can also be something else.
|
||||
If no image is available, ``etag`` should return ``None``. In some cases, this can speed up the implementation.
|
||||
"""
|
||||
|
||||
|
||||
timeline_events = EventPluginSignal()
|
||||
"""
|
||||
This signal is sent out to collect events for the time line shown on event dashboards. You are passed
|
||||
|
||||
Reference in New Issue
Block a user