-
Notifications
You must be signed in to change notification settings - Fork 0
Design Document: PDF Ticket Generation
Reference: https://xhtml2pdf.readthedocs.io/en/latest/
SocialPass allows users to generate tickets if they are able to prove ownership of certain required digital assets (such as NFTs) set by the event organizers. Another useful feature after proving ownership and generating the ticket is saving the ticket as a PDF file. This page is meant to document the required interactions in order to allow users to accomplish the above stated goal.
In order to create a PDF representation of the ticket, a few steps must be followed. These steps are:
- Create a Ticket template using Django templates;
- Select, Populate and Render the template with the desired context
- Convert the rendered template to PDF using XHTML2PDF
- Use the PDF file to send it to the user through email or in a API endpoint
The above steps are documented in detail in the following sections:
First of all, we need to create a template that will be used to generate the PDF. Since we are using Django, this template can follow the Django template syntax. For example:
<!DOCTYPE html>
<html>
<head>
<style type="text/css">
@page {
size: a4 portrait;
@frame header_frame { /* Static Frame */
-pdf-frame-content: header_content;
left: 50pt; width: 512pt; top: 50pt; height: 40pt;
}
@frame content_frame { /* Content Frame */
left: 50pt; width: 512pt; top: 90pt; height: 632pt;
}
@frame footer_frame { /* Another static Frame */
-pdf-frame-content: footer_content;
left: 50pt; width: 512pt; top: 772pt; height: 20pt;
}
}
.ticket {
background: red;
}
</style>
</head>
<body>
<!-- Content for Static Frame 'header_frame' -->
<div id="header_content">Social Pass</div>
<!-- Content for Static Frame 'footer_frame' -->
<div id="footer_content">(c) - page <pdf:pagenumber>
of <pdf:pagecount>
</div>
<!-- HTML Content -->
{{ ticket.data }}
</body>
</html>
As you could see in the template example, in order to bridge the differences between HTML and PDFs, xhtml2pdf makes use of the concept of Pages and Frames. Pages define the size, orientation and margins of pages. Frames are rectangular regions with in each page.
@page syntax and values:
Syntax: @page { size: <type> <orientation>; }
Where <type> is one of:
a0 .. a6
b0 .. b6
elevenseventeen
legal
letter
And <orientation> is one of:
landscape
portrait
Defaults to:
size: a4 portrait;
Pages properties:
background-image
size
margin, margin-bottom, margin-left, margin-right, margin-top
@frame syntax and values:
Syntax: @frame <frame_name> { <property>: <value>; }
where <frame_name> is the name of the frame
where <property> is one of:
bottom, top, height
left, right, width
margin, margin-bottom, margin-left, margin-right, margin-top
where <value> is a number in pt;
This is an example of page layout defined in the template: (more examples can be found in documentation)
+-page------------------+
| +-header_frame------+ |
| | Lyrics-R-Us | |
| +-------------------+ |
| +-content_frame-----+ |
| | To PDF or not to | |
| | PDF | |
| | | |
| | | |
| +-------------------+ |
| +-footer_frame------+ |
| | (c) - page 1 of 1 | |
| +-------------------+ |
+-----------------------+
About styling, there is a point that must be taken into account. The xhtml2pdf library does not support all standard CSS properties.
According to the documentation, the library supports the following default properties:
background-color
border-bottom-color, border-bottom-style, border-bottom-width
border-left-color, border-left-style, border-left-width
border-right-color, border-right-style, border-right-width
border-top-color, border-top-style, border-top-width
colordisplay
font-family, font-size, font-style, font-weight
height
line-height, list-style-type
margin-bottom, margin-left, margin-right, margin-top
padding-bottom, padding-left, padding-right, padding-top
page-break-after, page-break-before
size
text-align, text-decoration, text-indent
vertical-align
white-space
width
zoom
In addition to these, xhtml2pdf adds the following vendor-specific properties:
-pdf-frame-border
-pdf-frame-break
-pdf-frame-content
-pdf-keep-with-next
-pdf-next-page
-pdf-outline
-pdf-outline-level
-pdf-outline-open
-pdf-page-break
In order to select and populate the template with the desired context, we need to get the template using the Django template system.
from django.template.loader import get_template
template = get_template('event_discovery/ticket.html')
After that, as an optional step, just create a context dictionary that will be used to populate the template.
context = {'data': 'this is your template context data'}
Finally, we can render the template with the context.
html = template.render(context)
In this step we need to convert the rendered template to PDF using the xhtml2pdf library.
First of all, we need to create a BytesIO object that will be used to store the PDF.
from io import BytesIO
pdfFile = BytesIO()
After that, we can create the PDF object using the xhtml2pdf lib and storing in the BytesIO object.
But first, to allow URL references to be resolved using Django’s STATIC_URL and MEDIA_URL settings, xhtml2pdf allows users to specify a link_callback parameter to point to a function that converts relative URLs to absolute system paths.
import os
from django.contrib.staticfiles import finders
from django.conf import settings
def link_callback(uri, rel):
"""
Convert HTML URIs to absolute system paths so xhtml2pdf can access those
resources
"""
result = finders.find(uri)
if result:
if not isinstance(result, (list, tuple)):
result = [result]
result = list(os.path.realpath(path) for path in result)
path = result[0]
else:
sUrl = settings.STATIC_URL # Typically /static/
sRoot = settings.STATIC_ROOT # Typically /home/userX/project_static/
mUrl = settings.MEDIA_URL # Typically /media/
mRoot = settings.MEDIA_ROOT # Typically /home/userX/project_static/media/
if uri.startswith(mUrl):
path = os.path.join(mRoot, uri.replace(mUrl, ""))
elif uri.startswith(sUrl):
path = os.path.join(sRoot, uri.replace(sUrl, ""))
else:
return uri
# make sure that file exists
if not os.path.isfile(path):
raise Exception(
'media URI must start with %s or %s' % (sUrl, mUrl)
)
return path
After everything is set up, we can install xhtml2pdf lib, create the PDF object using the xhtml2pdf and storing in the BytesIO object. Note that we must pass link_callback function as a parameter.
# install xhtml2pdf
pip install xhtml2pdf
from xhtml2pdf import pisa
creation_status = pisa.CreatePDF(html, dest=pdfFile, link_callback=link_callback)
In this example pisa_status should store the status of the PDF creation. We can check the status of the PDF creation using the creation_status.err
value.
if creation_status.err:
# do something
Te last step is to use the PDF file to send it to the user through email or in a API endpoint.
If we want to make it available in a API endpoint, we can return a HTTPResponse with the PDF file. Setting it's content type to application/pdf
.
response = HttpResponse(pdfFile.getvalue(), content_type='application/pdf')
That will render the PDF file in the browser.
(optional) If we want to force the download of the PDF, we can set the content disposition to attachment
.
response['Content-Disposition'] = 'attachment; filename="ticket.pdf"'
In order to send the PDF file to the user through email, we need to create a email message. In Django, to send a PDF as attachment, we need to create a EmailMultiAlternatives
message and then attach the PDF file to it using the attach_alternative
method.
from django.core.mail import EmailMultiAlternatives
subject, from_email, to = 'hello', '[email protected]', '[email protected]'
text_content = 'This is an important message.'
html_content = '<p>This is an <strong>important</strong> message.</p>'
msg = EmailMultiAlternatives(subject, text_content, from_email, [to])
msg.attach_alternative(pdfFile.getvalue(), "application/pdf")
msg.send()
More details about sending an email with a attachment can be found in Django documentation.
There are other solutions to achieve the same result. This library create a PDF from a HTML file without the use of a external package.
For example, wkhtmltopdf is an alternative to xhtml2pdf. It has a package to integrate directly to django called django-wkhtmltopdf. But in order to get it to work, as a requirement, we need to install the wkhtmltopdf static binary which should be installed in the OS.
Django, in its documentation, recommends using another library for creating PDFs, reportlab. But in this approach, we need to construct the PDF like in a Canvas, adding elements to it. xhtml2pdf was built on top ot the reportlab.