QuickHtmlPdf
A fast, high-performance Flutter Web package for converting HTML templates with dynamic data into PDFs using JavaScript interoperability.
Features
- Instant Download Mode: Uses native browser print for near-instant PDF generation (~50ms)
- Bytes Mode: Returns PDF as
Uint8Listfor further processing (upload, store, etc.) - Template Engine: Support for
{{placeholders}}, loops, and raw HTML insertion - Print CSS: Optimized CSS for accurate pagination and table handling
- Large Documents: Chunked rendering for 200+ page documents
- Header/Footer: Custom header and footer support for each page
- Multiple Formats: A4, Letter, Legal with portrait/landscape orientation
Installation
Add to your pubspec.yaml:
dependencies:
quick_html_pdf: ^1.0.0
Required: Add JS Libraries (for Bytes Mode)
If you plan to use PdfOutput.bytes, add these scripts to your web/index.html:
<head>
<!-- ... other head content ... -->
<!-- Required for bytes mode only -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
</head>
Note: These libraries are NOT required for PdfOutput.download mode, which uses the browser's native print dialog.
Quick Start
Basic Usage
import 'package:quick_html_pdf/quick_html_pdf.dart';
// Generate PDF and trigger download
await QuickHtmlPdf.generate(
htmlTemplate: '<h1>Hello {{name}}</h1>',
data: {'name': 'World'},
options: PdfOptions(
output: PdfOutput.download,
filename: 'hello.pdf',
),
);
Get PDF as Bytes
final bytes = await QuickHtmlPdf.generate(
htmlTemplate: '<h1>Hello {{name}}</h1>',
data: {'name': 'World'},
options: PdfOptions(output: PdfOutput.bytes),
);
if (bytes != null) {
print('PDF size: ${bytes.length} bytes');
// Upload, store, or process the bytes
// Or trigger download manually:
QuickHtmlPdf.downloadBytes(bytes: bytes, filename: 'hello.pdf');
}
Template Syntax
Simple Interpolation
// Template
'<p>Hello {{name}}, you have {{count}} messages.</p>'
// Data
{'name': 'John', 'count': 5}
// Output
'<p>Hello John, you have 5 messages.</p>'
Nested Objects
// Template
'<p>{{user.name}} works at {{user.company.name}}</p>'
// Data
{
'user': {
'name': 'Alice',
'company': {'name': 'Acme Corp'}
}
}
Loops with {{#each}}
// Template
'''
<table>
<tr><th>Item</th><th>Price</th></tr>
{{#each items}}
<tr>
<td>{{this.name}}</td>
<td>{{this.price}}</td>
</tr>
{{/each}}
</table>
'''
// Data
{
'items': [
{'name': 'Widget', 'price': '\$10'},
{'name': 'Gadget', 'price': '\$20'},
]
}
Loop Variables
{{this}}- Current item value{{this.field}}- Field of current item{{@index}}- Zero-based loop index{{@index1}}- One-based loop index{{@first}}- True if first iteration{{@last}}- True if last iteration
Raw HTML (Unescaped)
// Template - triple braces for raw HTML
'<div>{{{htmlContent}}}</div>'
// Data
{'htmlContent': '<strong>Bold</strong>'}
// Output (HTML is not escaped)
'<div><strong>Bold</strong></div>'
HTML Escaping
Regular {{}} automatically escapes HTML entities:
// Template
'<p>{{userInput}}</p>'
// Data
{'userInput': '<script>alert("xss")</script>'}
// Output (safely escaped)
'<p><script>alert("xss")</script></p>'
Configuration Options
PdfOptions(
// Page format (default: A4)
pageFormat: PdfPageFormat.a4, // or .letter, .legal
// Orientation (default: portrait)
orientation: PdfOrientation.portrait, // or .landscape
// Margins in millimeters
margins: PdfMargins(
topMm: 20,
rightMm: 15,
bottomMm: 20,
leftMm: 15,
),
// Output mode
output: PdfOutput.download, // or .bytes
// Filename for download
filename: 'document.pdf',
// Optional header HTML (appears on each page)
headerHtml: '<div>Company Name</div>',
// Optional footer HTML (appears on each page)
footerHtml: '<div>Page footer</div>',
// Debug logging
debug: false,
// Advanced: Scale for canvas rendering (bytes mode only)
scale: 1.5, // Higher = better quality, slower
// Advanced: JPEG quality (bytes mode only)
imageQuality: 0.92,
// Advanced: Resource loading timeout
resourceTimeoutMs: 10000,
)
Sample Templates
Simple Invoice (Single Page)
const invoiceTemplate = '''
<div style="font-family: Arial, sans-serif; padding: 40px;">
<div style="display: flex; justify-content: space-between;">
<div>
<h1 style="margin: 0;">INVOICE</h1>
<p>{{company.name}}</p>
</div>
<div style="text-align: right;">
<p><strong>Invoice #:</strong> {{invoiceNumber}}</p>
<p><strong>Date:</strong> {{date}}</p>
</div>
</div>
<div style="margin: 30px 0;">
<h3>Bill To:</h3>
<p>{{customer.name}}<br>{{customer.address}}</p>
</div>
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="background: #f5f5f5;">
<th style="padding: 12px; text-align: left; border-bottom: 2px solid #ddd;">Item</th>
<th style="padding: 12px; text-align: right; border-bottom: 2px solid #ddd;">Qty</th>
<th style="padding: 12px; text-align: right; border-bottom: 2px solid #ddd;">Price</th>
<th style="padding: 12px; text-align: right; border-bottom: 2px solid #ddd;">Total</th>
</tr>
</thead>
<tbody>
{{#each items}}
<tr>
<td style="padding: 12px; border-bottom: 1px solid #eee;">{{this.name}}</td>
<td style="padding: 12px; text-align: right; border-bottom: 1px solid #eee;">{{this.qty}}</td>
<td style="padding: 12px; text-align: right; border-bottom: 1px solid #eee;">{{this.price}}</td>
<td style="padding: 12px; text-align: right; border-bottom: 1px solid #eee;">{{this.total}}</td>
</tr>
{{/each}}
</tbody>
</table>
<div style="text-align: right; margin-top: 20px;">
<p style="font-size: 20px;"><strong>Total: {{grandTotal}}</strong></p>
</div>
</div>
''';
// Generate invoice
await QuickHtmlPdf.generate(
htmlTemplate: invoiceTemplate,
data: {
'company': {'name': 'Acme Corp'},
'invoiceNumber': 'INV-001',
'date': 'January 8, 2024',
'customer': {
'name': 'John Smith',
'address': '123 Main St, City, ST 12345',
},
'items': [
{'name': 'Widget', 'qty': 2, 'price': '\$50.00', 'total': '\$100.00'},
{'name': 'Gadget', 'qty': 1, 'price': '\$75.00', 'total': '\$75.00'},
],
'grandTotal': '\$175.00',
},
options: PdfOptions(
output: PdfOutput.download,
filename: 'invoice.pdf',
),
);
Large Report (Multi-Page with Table)
const reportTemplate = '''
<div style="font-family: Arial, sans-serif;">
<h1 style="text-align: center;">{{title}}</h1>
<p style="text-align: center; color: #666;">Generated: {{date}}</p>
<table style="width: 100%; border-collapse: collapse; font-size: 11px;">
<thead>
<tr style="background: #1e40af; color: white;">
<th style="padding: 8px;">#</th>
<th style="padding: 8px;">Date</th>
<th style="padding: 8px;">Customer</th>
<th style="padding: 8px;">Product</th>
<th style="padding: 8px; text-align: right;">Amount</th>
</tr>
</thead>
<tbody>
{{#each rows}}
<tr style="{{this.rowStyle}}">
<td style="padding: 6px; border-bottom: 1px solid #eee;">{{@index1}}</td>
<td style="padding: 6px; border-bottom: 1px solid #eee;">{{this.date}}</td>
<td style="padding: 6px; border-bottom: 1px solid #eee;">{{this.customer}}</td>
<td style="padding: 6px; border-bottom: 1px solid #eee;">{{this.product}}</td>
<td style="padding: 6px; border-bottom: 1px solid #eee; text-align: right;">{{this.amount}}</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
''';
// Generate 1000 rows of data
final rows = List.generate(1000, (i) => {
'date': '2024-01-${(i % 28 + 1).toString().padLeft(2, '0')}',
'customer': 'Customer ${i + 1}',
'product': 'Product ${(i % 10) + 1}',
'amount': '\$${(i * 10 + 99).toStringAsFixed(2)}',
'rowStyle': i.isOdd ? 'background: #f9f9f9;' : '',
});
await QuickHtmlPdf.generate(
htmlTemplate: reportTemplate,
data: {
'title': 'Sales Report 2024',
'date': 'January 8, 2024',
'rows': rows,
},
options: PdfOptions(
output: PdfOutput.download,
filename: 'sales-report.pdf',
headerHtml: '<div><strong>Sales Report</strong></div>',
footerHtml: '<div style="color: #666;">Confidential</div>',
),
);
Performance Tips for Large PDFs
Use Download Mode When Possible
PdfOutput.download is significantly faster than PdfOutput.bytes:
| Document Size | Download Mode | Bytes Mode |
|---|---|---|
| 10 pages | ~50ms | ~500ms |
| 100 pages | ~50ms | ~3s |
| 300 pages | ~50ms | ~8s |
Download mode leverages the browser's native PDF engine, while bytes mode must render each page as a canvas.
Optimize Table Structure
<!-- Good: thead repeats on each page -->
<table>
<thead>
<tr>
<th>Header</th>
</tr>
</thead>
<tbody>
{{#each rows}}
<tr>
<td>{{this.value}}</td>
</tr>
{{/each}}
</tbody>
</table>
Use Page Breaks Strategically
<!-- Force page break after a section -->
<div class="page-break"></div>
<!-- Keep content together -->
<div class="no-break">
<h2>Section Title</h2>
<p>This content stays together</p>
</div>
Reduce Image Quality for Bytes Mode
PdfOptions(
output: PdfOutput.bytes,
scale: 1.0, // Lower scale = faster (default 1.5)
imageQuality: 0.8, // Lower quality = smaller file
)
Pre-render Template for Multiple Outputs
// Render template once
final renderedHtml = TemplateEngine.render(template, data);
// Compose full HTML
final fullHtml = HtmlComposer.compose(renderedHtml, options);
// Generate multiple PDFs from same rendered content
// (Avoid re-rendering template each time)
Limitations
-
Web Only: This package only works on Flutter Web. It throws
UnsupportedErroron mobile and desktop platforms. -
External Images: Images from external URLs may not render correctly due to CORS restrictions. Use base64-encoded images or self-hosted images when possible.
-
Custom Fonts: Web fonts must be loaded before PDF generation. The package waits for
document.fonts.ready, but ensure fonts are properly linked in your HTML. -
Complex CSS: Some advanced CSS features (flexbox with wrapping, CSS grid) may not paginate perfectly. Test your templates with large datasets.
-
JavaScript Libraries: Bytes mode requires html2canvas and jsPDF libraries to be loaded in
index.html.
Platform Support
| Platform | Supported |
|---|---|
| Web | ✅ |
| Android | ❌ |
| iOS | ❌ |
| macOS | ❌ |
| Windows | ❌ |
| Linux | ❌ |
For native platform PDF generation, consider using:
Error Handling
try {
await QuickHtmlPdf.generate(
htmlTemplate: template,
data: data,
options: PdfOptions(output: PdfOutput.bytes),
);
} on UnsupportedError catch (e) {
// Platform not supported
print('Web only: $e');
} on TemplateException catch (e) {
// Template syntax error
print('Template error: ${e.message}');
} on PdfGenerationException catch (e) {
// PDF generation failed
print('PDF error: ${e.message} (phase: ${e.phase})');
}
Debug Mode
Enable debug logging to see timing information:
await QuickHtmlPdf.generate(
htmlTemplate: template,
data: data,
options: PdfOptions(debug: true),
);
// Console output:
// [QuickHtmlPdf] Template rendered in 5ms
// [QuickHtmlPdf] Rendered HTML size: 125.3 KB
// [QuickHtmlPdf] HTML composed in 2ms
// [QuickHtmlPdf] Full HTML size: 128.7 KB
// [QuickHtmlPdf:Bytes] Page size: 210mm x 297mm
// [QuickHtmlPdf:Bytes] Rendering 42 pages...
// [QuickHtmlPdf:Bytes] Rendered 10/42 pages
// ...
// [QuickHtmlPdf] ===== Generation Complete =====
// [QuickHtmlPdf] Total time: 3250ms
// [QuickHtmlPdf] Output: 2.4 MB
License
MIT License - see LICENSE for details.
Libraries
- quick_html_pdf
- QuickHtmlPdf - Fast HTML to PDF conversion for Flutter Web.