Flutter and Dart have no HTML engine, so "HTML to PDF" in a Flutter app means one of three things: a package that translates HTML into PDF drawing calls, the phone's own WebView, or Chrome on a server. We tested the pure-Dart option, htmltopdfwidgets 2.2.0, and one package:http call to an API on Dart 3.13, with the same invoice, and read the source of the printing package's WebView converter. Every Dart snippet below was run as shown.
| Option | Pick it when |
|---|---|
| htmltopdfwidgets Pure Dart, runs on every platform | Your HTML is simple (headings, paragraphs, tables, inline styles) and you want offline generation with no server. |
| Printing.convertHtml The OS WebView, Android, iOS and macOS only | You only ship to those three platforms and the HTML has no JavaScript you depend on. |
| An API (headless Chrome) One HTTP call from your backend | The PDF must look like the page in a browser: grid, SVG, JavaScript, page breaks, web fonts. Same result on every platform, including web. |
Skip these: flutter_html_to_pdf last shipped in September 2021 (0.7.0, Android and iOS only). The pdf package on its own builds PDFs from widgets, not from HTML; it's what htmltopdfwidgets outputs to.
Save this as invoice.html next to pubspec.yaml. It has a flex header with a dark background, a table with light bottom borders and euro prices:
<!doctype html>
<html><head><meta charset="utf-8"><style>
@page { size: A4; margin: 20mm 15mm; }
body { font-family: sans-serif; }
.top { display: flex; justify-content: space-between; align-items: center;
background: #1e3a8a; color: #fff; padding: 12px; }
table { width: 100%; border-collapse: collapse; }
td, th { border-bottom: 1px solid #ddd; padding: 6px; text-align: left; }
</style></head><body>
<div class="top"><h1>Invoice #1042</h1><p>Due: 2026-11-01</p></div>
<table><tr><th>Item</th><th>Price</th></tr>
<tr><td>Consulting (3h)</td><td>€450.00</td></tr>
<tr><td>Hosting, Oct.</td><td>€19.00</td></tr></table>
</body></html>
dart pub add htmltopdfwidgets (or flutter pub add). It parses the HTML, lays it out itself and returns widgets for the pdf package. bin/widgets.dart:
import 'dart:io';
import 'package:htmltopdfwidgets/htmltopdfwidgets.dart';
Future<void> main() async {
final html = await File('invoice.html').readAsString();
final widgets = await HTMLToPdf().convert(html);
final pdf = Document();
pdf.addPage(MultiPage(pageFormat: PdfPageFormat.a4, build: (context) => widgets));
await File('invoice.pdf').writeAsBytes(await pdf.save());
}
Result: a 1-page A4 PDF. The flex header came out right, blue bar and all, which is better than most HTML-to-PDF packages outside a browser. What went wrong:
pdftotext read the prices as "450.00" and "19.00": the default font is the PDF's built-in Helvetica, which has no glyph for € (or any non-Latin text). Fix below.Fix for the missing characters: hand it a Unicode TTF to fall back on. We used DejaVu Sans; in a Flutter app, add the font to your assets and load it with rootBundle.load('assets/DejaVuSans.ttf') instead of File:
import 'dart:io';
import 'package:htmltopdfwidgets/htmltopdfwidgets.dart';
Future<void> main() async {
final html = await File('invoice.html').readAsString();
// The built-in Helvetica has no € glyph: give it a Unicode TTF to fall back on.
final bytes = await File('DejaVuSans.ttf').readAsBytes();
final unicode = Font.ttf(bytes.buffer.asByteData());
final widgets = await HTMLToPdf().convert(html, fontFallback: [unicode]);
final pdf = Document();
pdf.addPage(MultiPage(pageFormat: PdfPageFormat.a4, build: (context) => widgets));
await File('invoice.pdf').writeAsBytes(await pdf.save());
}
With the fallback, the text layer reads "€450.00" and DejaVu Sans is embedded.
We then fed it a second page with the things real templates use. Each line is what the PDF showed:
display: grid came out as two stacked full-width boxes..r { text-align: right; color: #c00 } turned the text red but left it aligned left.<script> stayed "(filled by JS)". Charts drawn by Chart.js or similar won't appear either.<svg><rect> was dropped (0 green pixels on the page).page-break-before: always was ignored: 1 page instead of 2.None of this is a bug you can configure away: the package is its own layout engine, and it covers a subset of CSS. If you write the HTML yourself and keep it to that subset, it works offline on every platform. If the HTML comes from a web app, a template designer or a CMS, it will drift.
The printing package has Printing.convertHtml(html: ..., format: ...), which loads the HTML into the platform's WebView and prints it. We couldn't run it here (it needs a Flutter app on a device), so this is from the printing 5.15.1 source: it's implemented for Android, iOS and macOS; the web version throws UnimplementedError, and the Windows and Linux plugins don't implement it. It needs Flutter, so it can't run in a plain Dart server either. The output also depends on the WebView version on each user's device.
Shotmint renders the HTML in Chrome on its servers and returns the PDF bytes, so the result is the same on Android, iOS, web and desktop. dart pub add http, then lib/html_to_pdf.dart:
import 'dart:convert';
import 'dart:typed_data';
import 'package:http/http.dart' as http;
/// Renders [html] to PDF bytes in headless Chrome on Shotmint's servers.
Future<Uint8List> htmlToPdf(String html, {required String apiKey}) async {
final res = await http
.post(
Uri.parse('https://shotmint.creasework.com/v1/pdf'),
headers: {'X-API-Key': apiKey, 'Content-Type': 'application/json'},
body: jsonEncode({
'html': html, // or 'url': 'https://example.com'
'prefer_css_page_size': true, // use @page size/margins from the CSS
'page_numbers': true, // "1 / 2" footer on every page
}),
)
.timeout(const Duration(seconds: 60));
if (res.statusCode != 200) {
throw Exception('PDF failed: ${res.statusCode} ${res.body}');
}
return res.bodyBytes;
}
And a command-line test, bin/api.dart (the import uses the package name from pubspec.yaml, ours was h2p), run with SHOTMINT_KEY=... dart run bin/api.dart:
import 'dart:io';
import 'package:h2p/html_to_pdf.dart';
Future<void> main() async {
final html = await File('invoice.html').readAsString();
final pdf = await htmlToPdf(html, apiKey: Platform.environment['SHOTMINT_KEY']!);
await File('invoice.pdf').writeAsBytes(pdf);
print('saved invoice.pdf (${pdf.length} bytes)');
}
Result: a 1-page A4 PDF in 1.7 s including Dart's start, with the blue header, the light row lines as written, €450.00 in the text layer and "1 / 1" in the footer. The second test page came out as 2 pages, with the grid side by side, the right-aligned line on the right, "TOTAL: 469.00" filled in by the script and the green SVG drawn.
Don't put the API key in the app. Anything shipped in an APK or IPA can be extracted. Call htmlToPdf() from your backend (a Dart server with shelf or dart_frog, a Cloud Function, or any other server) and have the app download the PDF from there. In the app, the printing package can then preview, print or share the bytes. We didn't run this part (no Flutter SDK on our test box), but checked both signatures in printing 5.15.1:
import 'dart:typed_data';
import 'package:printing/printing.dart';
// bytes: the PDF your backend returned (it calls htmlToPdf() above).
Future<void> showPdf(Uint8List bytes) async {
await Printing.layoutPdf(onLayout: (_) async => bytes); // preview + print dialog
}
Future<void> sharePdf(Uint8List bytes) async {
await Printing.sharePdf(bytes: bytes, filename: 'invoice.pdf'); // share sheet
}
Printing.convertHtml.pdftotext and by rendering it (pdftoppm -r 50 -png out.pdf p): a dropped character can hide in a quick look at a small preview.Related: HTML invoices to PDF, CSS page breaks, custom fonts in PDFs, PDFs with charts, jsPDF HTML to PDF.
One PDF = one credit. €5 buys 2,000 renders, credits never expire and there's no subscription. The free key gives 25 renders a month.
No signup, no card. Get a key in one click, then buy credits only if you need more (from €5 for 2,000, never expire). From a script, an AI agent, Make or Zapier, POST /v1/keys/free (no body) returns a key as JSON (openapi.json, no-code recipes).
All guides: guides · integrations · comparisons