Print Format Developer Guide
Everything you need to build production-quality PDF print formats with FrapPDFy — from images and QR codes to multi-copy labels and page numbering.
FrapPDFy renders your Jinja2 HTML template against a Frappe document, then pipes it through WeasyPrint or Playwright (Chromium) to produce a PDF. Your template is stored in the Print Format Template doctype as a single HTML string, automatically split into three regions:
| Region | Class / ID | Purpose |
|---|---|---|
| Header | .pdf-header | Repeats on every page |
| Content | .pdf-content (optional) | Main document body |
| Footer | .pdf-footer | Repeats on every page |
.pdf-content is found, everything outside .pdf-header and .pdf-footer is treated as content automatically.
A complete template skeleton showing all three regions:
<!-- HEADER — repeats on every page -->
<div class="pdf-header">
<img src="{{ frappe.utils.get_url(doc.company_logo) }}" height="40">
<h2>{{ doc.company }}</h2>
</div>
<!-- MAIN CONTENT -->
<div class="pdf-content">
<h1>{{ doc.name }}</h1>
<p>Date: {{ doc.posting_date }}</p>
<table>
<thead><tr>
<th>Item</th><th>Qty</th><th>Rate</th><th>Amount</th>
</tr></thead>
<tbody>
{% for row in doc.items %}
<tr>
<td>{{ row.item_name }}</td>
<td>{{ row.qty }}</td>
<td>{{ row.rate }}</td>
<td>{{ row.amount }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- FOOTER — repeats on every page -->
<div class="pdf-footer">
<span>{{ doc.company }} | {{ doc.company_address }}</span>
<span>Page <span class="page-number"></span> of <span class="total-pages"></span></span>
</div>
The approach differs depending on which PDF engine you use.
WeasyPrint — CSS Counters
Add these spans in your header or footer, then define the CSS:
<!-- HTML -->
<span>Page <span class="page-number"></span> of <span class="total-pages"></span></span>
/* Custom CSS field */
.page-number::before {
content: counter(page);
}
.total-pages::before {
content: counter(pages);
}
Playwright — Chromium Template Classes
Use special classes inside header/footer templates:
<span class="pageNumber"></span> <!-- current page -->
<span class="totalPages"></span> <!-- total pages -->
pageNumber / totalPages classes only work inside Playwright header/footer templates — they won't work in WeasyPrint. Use CSS counters for WeasyPrint.
These utility classes are pre-defined by the engine — do not redefine them in Custom CSS.
<!-- Force a page break AFTER this element -->
<div class="page-break"></div>
<!-- Prevent this block from splitting across pages -->
<div class="no-break">
... keep this section together on one page ...
</div>
Always convert image paths to absolute URLs — both engines fetch images over HTTP and cannot resolve relative paths.
<!-- From an Attach field on the document -->
<img src="{{ frappe.utils.get_url(doc.company_logo) }}" height="50">
<!-- From a hardcoded file path -->
<img src="{{ frappe.utils.get_url('/files/letterhead.png') }}" height="80">
doc.image_field directly (without frappe.utils.get_url()) will silently fail — the image will be blank in the PDF.
Use the web_block helper to render an e-Invoice QR code from a registered Web Template.
<!-- Render the QR code block -->
{{ web_block('e-Invoice QR', values={'e_invoice_qr_text': e_invoice_log.signed_qr_code}) }}
/* Size the QR image via Custom CSS */
.qrcode {
width: 90px;
height: 90px;
}
e_invoice_log must be available in your template context. Fetch it using frappe.get_doc("e-Invoice Log", doc.name) in a Jinja context or pass it via the calling code.
Frappe stores rich text (Long Text with editor) as HTML with Quill editor classes. Override default Quill padding to avoid unwanted spacing in your PDF.
/* Add to Custom CSS */
.ql-editor {
padding: 0;
margin: 0;
}
<!-- Render a rich text field — it's already HTML -->
<div class="ql-editor">{{ doc.terms_and_conditions }}</div>
All custom CSS goes in the Custom CSS field of the Print Format Template. Page size and orientation are set via the doctype fields — not CSS.
/* Universal reset */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Tables */
table {
table-layout: fixed;
width: 100%;
border-collapse: collapse;
}
/* Prevent table rows splitting across pages */
tr {
break-inside: avoid;
page-break-inside: avoid;
}
/* Remove Quill editor default padding */
.ql-editor {
padding: 0;
margin: 0;
}
/* Page number counters (WeasyPrint) */
.page-number::before { content: counter(page); }
.total-pages::before { content: counter(pages); }
/* QR code size */
.qrcode { width: 90px; height: 90px; }
Page Size & Orientation Fields
| Field | Accepted Values |
|---|---|
| Page Size | A4, A3, A5, Letter, Legal, Tabloid |
| Orientation | portrait, landscape |
| Margin (mm) | 10mm (all sides) or 10mm 15mm (top/bottom left/right) |
Print multiple copies (e.g. Original, Duplicate, Triplicate) in one PDF, each with different labels.
Step 1 — Set fields in Print Format Template
| Field | Value |
|---|---|
| Number of Copies | e.g. 3 |
| Copy Values | JSON array — one object per copy |
Step 2 — Copy Values JSON
[
{ ".copy-label": "Original" },
{ ".copy-label": "Duplicate" },
{ ".copy-label": "Triplicate" }
]
Step 3 — Template placeholder
<div class="pdf-header">
<span class="copy-label"></span> <!-- filled per copy -->
<h2>Tax Invoice</h2>
</div>
Selector Syntax
| Prefix | Targets |
|---|---|
.classname | All elements with that class |
#id | Element with that ID |
plain-name | Elements with that class (same as .classname) |
Choose the right engine for your template's requirements.
| Feature | WeasyPrint | Playwright |
|---|---|---|
CSS Paged Media (@page) | ✅ Full | ❌ No |
| CSS counters (page numbers) | ✅ | ❌ |
| JavaScript rendering | ❌ | ✅ |
| Flexbox / Grid support | Partial | ✅ Full |
| Background images in headers | ✅ | ✅ |
| Minimum page margin | None | ~6.35mm per side |
| Speed | Faster | Slower (browser launch) |
Use WeasyPrint for document-style reports with running headers/footers and CSS page numbers.
Use Playwright for complex modern layouts requiring full CSS/JS support.
| Problem | Cause | Fix |
|---|---|---|
| Images not showing | Relative URL | Use frappe.utils.get_url(path) |
| Header/footer missing | Wrong class name | Must be exactly pdf-header / pdf-footer |
| Page numbers not working | Wrong engine/CSS | See Page Numbers section above |
| Extra padding in rich text | Quill defaults | Add .ql-editor { padding: 0; margin: 0; } |
| Table rows split across pages | Missing CSS | Add tr { break-inside: avoid; } |
| Margins ignored (Playwright) | Engine minimum | Playwright enforces ~6.35mm minimum margin |
| QR code too large/small | Default size | Override with .qrcode { width: 90px; } |
| Content cut off | Header/footer too tall | Increase top/bottom margin in Print Format settings |
A cheat-sheet for everyday use: