← Back to FrapPDFy Print Format Guide GitHub →

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.

📋 Overview

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:

RegionClass / IDPurpose
Header.pdf-headerRepeats on every page
Content.pdf-content (optional)Main document body
Footer.pdf-footerRepeats on every page
ℹ️ If no .pdf-content is found, everything outside .pdf-header and .pdf-footer is treated as content automatically.

🏗️ HTML Structure

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>


🔢 Page Numbers

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 -->
⚠️ Chromium's pageNumber / totalPages classes only work inside Playwright header/footer templates — they won't work in WeasyPrint. Use CSS counters for WeasyPrint.

↕️ Page Breaks

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>

🖼️ Images

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">
⚠️ Using doc.image_field directly (without frappe.utils.get_url()) will silently fail — the image will be blank in the PDF.

QR Codes

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.

📝 Markdown / Rich Text Fields

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>

🎨 CSS Tips & Overrides

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

FieldAccepted Values
Page SizeA4, A3, A5, Letter, Legal, Tabloid
Orientationportrait, landscape
Margin (mm)10mm (all sides) or 10mm 15mm (top/bottom left/right)

📑 Multi-Copy & Copy Labels

Print multiple copies (e.g. Original, Duplicate, Triplicate) in one PDF, each with different labels.

Step 1 — Set fields in Print Format Template

FieldValue
Number of Copiese.g. 3
Copy ValuesJSON 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

PrefixTargets
.classnameAll elements with that class
#idElement with that ID
plain-nameElements with that class (same as .classname)

⚙️ WeasyPrint vs Playwright

Choose the right engine for your template's requirements.

FeatureWeasyPrintPlaywright
CSS Paged Media (@page)✅ Full❌ No
CSS counters (page numbers)
JavaScript rendering
Flexbox / Grid supportPartial✅ Full
Background images in headers
Minimum page marginNone~6.35mm per side
SpeedFasterSlower (browser launch)
💡
Recommendation:
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.

🚧 Common Pitfalls
ProblemCauseFix
Images not showingRelative URLUse frappe.utils.get_url(path)
Header/footer missingWrong class nameMust be exactly pdf-header / pdf-footer
Page numbers not workingWrong engine/CSSSee Page Numbers section above
Extra padding in rich textQuill defaultsAdd .ql-editor { padding: 0; margin: 0; }
Table rows split across pagesMissing CSSAdd tr { break-inside: avoid; }
Margins ignored (Playwright)Engine minimumPlaywright enforces ~6.35mm minimum margin
QR code too large/smallDefault sizeOverride with .qrcode { width: 90px; }
Content cut offHeader/footer too tallIncrease top/bottom margin in Print Format settings

Quick Reference

A cheat-sheet for everyday use:

Images frappe.utils.get_url(doc.field)
Page nos. .page-number::before { content: counter(page); }
Header <div class="pdf-header">...</div>
Footer <div class="pdf-footer">...</div>
Content <div class="pdf-content">...</div>
QR code {{ web_block('e-Invoice QR', values={...}) }}
QR size .qrcode { width: 90px; }
Rich text .ql-editor { padding: 0; margin: 0; }
Page break <div class="page-break"></div>
No break <div class="no-break">...</div>
Copy label <span class="copy-label"></span>
Total pages .total-pages::before { content: counter(pages); }