ZetCode

Dart PDF

last modified October 18, 2026

In this article we show how to create PDF documents in Dart language.

Portable Document Format (PDF) is a versatile file format created by Adobe that gives people an easy, reliable way to present and exchange documents.

$ dart pub add pdf

To produce PDF files in Dart, we use the pdf package. It is written in pure Dart and works in command-line programs, Flutter applications, and on the web. The widgets resemble the Flutter widgets, but they are different classes; therefore, we import the library with the pw prefix.

Dart PDF simple example

The following is a simple Dart example which produces a PDF document.

main.dart
import 'dart:io';

import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;

void main() async {
  final pdf = pw.Document();

  pdf.addPage(pw.Page(
    pageFormat: PdfPageFormat.a4,
    build: (pw.Context context) {
      return pw.Center(
        child: pw.Text('An old falcon'),
      );
    },
  ));

  final file = File('simple.pdf');
  await file.writeAsBytes(await pdf.save());
}

The program creates a new PDF file and writes it to the disk. It contains centered text. The program prints nothing; the result is the simple.pdf file.

import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;

We import the required packages. The pdf.dart library contains low-level types such as PdfPageFormat and PdfColors; the widgets are in widgets.dart.

final pdf = pw.Document();

A new empty document is created.

pdf.addPage(pw.Page(
  pageFormat: PdfPageFormat.a4,
  build: (pw.Context context) {
    return pw.Center(
      child: pw.Text('An old falcon'),
    );
  },
));

A page is added with addPage. We provide the page format and the content. The content consists of a Text widget which is centered on the page with Center. A pw.Page holds exactly one page; for content that may span several pages, we use MultiPage.

final file = File('simple.pdf');
await file.writeAsBytes(await pdf.save());

The save method returns the document as bytes, which we write to a file.

Dart PDF metadata

The Document constructor accepts metadata such as the title and the author. They are shown by PDF viewers in the document properties.

main.dart
import 'dart:io';

import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;

void main() async {
  final pdf = pw.Document(
    title: 'Falcons',
    author: 'Jan Bodnar',
    subject: 'Birds of prey',
  );

  pdf.addPage(pw.Page(
    build: (context) => pw.Center(
      child: pw.Text(
        'The peregrine falcon',
        style: pw.TextStyle(
          fontSize: 28,
          fontWeight: pw.FontWeight.bold,
          color: PdfColors.indigo,
        ),
      ),
    ),
  ));

  await File('meta.pdf').writeAsBytes(await pdf.save());
}

The text is styled with TextStyle: we set its size, weight, and colour. When no pageFormat is given, the A4 format is used.

Dart PDF header

A document header is created with Header.

main.dart
import 'dart:io';

import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;

void main() async {
  const txt = '''
The Battle of Thermopylae was fought between an alliance of Greek city-states,
led by King Leonidas of Sparta, and the Persian Empire of Xerxes I over the
course of three days, during the second Persian invasion of Greece.
''';

  final pdf = pw.Document();

  pdf.addPage(pw.Page(
    pageFormat: PdfPageFormat.a4,
    build: (pw.Context context) {
      return pw.Column(children: [
        pw.Header(level: 0, text: 'The Battle of Thermopylae'),
        pw.Paragraph(text: txt),
      ]);
    },
  ));

  await File('header.pdf').writeAsBytes(await pdf.save());
}

The document contains a header and a paragraph of text.

return pw.Column(children: [
  pw.Header(level: 0, text: 'The Battle of Thermopylae'),
  pw.Paragraph(text: txt),
]);

The header and the paragraph are added to a column widget. We specify the level and text for the header and the text for the paragraph. The level 0 is the largest header.

Dart PDF bullets

A bullet is created with the Bullet widget.

main.dart
import 'dart:io';

import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;

void main() async {
  const languages = ['Dart', 'F#', 'Clojure', 'Go', 'Groovy', 'Raku', 'Python'];

  final pdf = pw.Document();

  pdf.addPage(pw.Page(
    pageFormat: PdfPageFormat.a4,
    build: (pw.Context context) {
      return pw.Column(children: [
        pw.Header(level: 1, text: 'Programming languages'),
        for (final lang in languages) pw.Bullet(text: lang),
      ]);
    },
  ));

  await File('bullets.pdf').writeAsBytes(await pdf.save());
}

We have a header and a list of programming languages. Each language is displayed as a bullet. A collection for creates the widgets from the list.

Dart PDF rectangles

A rectangle is created with Rectangle.

main.dart
import 'dart:io';

import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;

void main() async {
  final colors = [
    PdfColors.grey,
    PdfColors.amber300,
    PdfColors.green500,
    PdfColors.blue600,
  ];

  final pdf = pw.Document();

  pdf.addPage(pw.Page(
    pageFormat: PdfPageFormat.a4,
    build: (pw.Context context) {
      return pw.Row(
        mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
        children: [
          for (final color in colors)
            pw.SizedBox(
              width: 80,
              height: 80,
              child: pw.Rectangle(fillColor: color),
            ),
        ],
      );
    },
  ));

  await File('rectangles.pdf').writeAsBytes(await pdf.save());
}

We create a row of SizedBox widgets. In the boxes, we place rectangle widgets. The spaceBetween alignment distributes the free space between the boxes, which replaces the Spacer widgets.

pw.SizedBox(
  width: 80,
  height: 80,
  child: pw.Rectangle(fillColor: color),
),

For each rectangle, we define a fill colour.

Dart PDF footer

In the next example, we add a footer to our pages.

main.dart
import 'dart:io';

import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;

void main() async {
  final pdf = pw.Document();
  final title = pw.LoremText();

  pdf.addPage(
    pw.MultiPage(
      footer: _buildFooter,
      build: (context) => [
        pw.Header(level: 1, text: title.sentence(1)),
        for (final n in [50, 70, 150, 250, 350, 450]) ...[
          pw.SizedBox(height: 20),
          pw.Lorem(length: n),
        ],
      ],
    ),
  );

  await File('footer.pdf').writeAsBytes(await pdf.save());
}

pw.Widget _buildFooter(pw.Context context) {
  return pw.Row(
    mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
    crossAxisAlignment: pw.CrossAxisAlignment.end,
    children: [
      pw.Text(
        'Page ${context.pageNumber}/${context.pagesCount}',
        style: const pw.TextStyle(
          fontSize: 12,
          color: PdfColors.blue700,
        ),
      ),
    ],
  );
}

The footer consists of a row that contains the current and the total page numbers.

final title = pw.LoremText();

The lorem classes provide some filler text for the document. This is a common practice in desktop publishing when testing.

pdf.addPage(
  pw.MultiPage(
    footer: _buildFooter,
    build: (context) => [
      ...
    ],
  ),
);

We have a multi-page document. The build function returns a list of widgets, which the MultiPage flows across as many pages as needed. The creation of the footer is delegated to the _buildFooter function.

pw.SizedBox(height: 20),
pw.Lorem(length: n),

We add some empty space with SizedBox. The Lorem widget adds n words of Latin filler text.

pw.Text(
  'Page ${context.pageNumber}/${context.pagesCount}',
  style: const pw.TextStyle(
    fontSize: 12,
    color: PdfColors.blue700,
  ),
),

The footer is a row with a Text widget. It contains the current page number and the count of all pages. Also, we style the text: we define its size and colour.

Dart PDF table

A table is created with TableHelper.fromTextArray. It is a complex helper with many options. (In older versions of the package, the same functionality was provided by the Table.fromTextArray constructor, which is now deprecated.)

main.dart
import 'dart:io';

import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;

const users = [
  ('John Doe', 'gardener'),
  ('Roger Roe', 'driver'),
  ('Joe Smith', 'teacher'),
];

void main() async {
  final pdf = pw.Document();

  pdf.addPage(pw.Page(
    pageFormat: PdfPageFormat.a4,
    build: genTable,
  ));

  await File('table.pdf').writeAsBytes(await pdf.save());
}

pw.Widget genTable(pw.Context ctx) {
  return pw.TableHelper.fromTextArray(
    border: null,
    cellAlignment: pw.Alignment.centerLeft,
    headerDecoration: const pw.BoxDecoration(
      borderRadius: pw.BorderRadius.all(pw.Radius.circular(1)),
      color: PdfColors.blue500,
    ),
    headerHeight: 25,
    cellHeight: 40,
    headerStyle: pw.TextStyle(
      color: PdfColors.white,
      fontSize: 10,
      fontWeight: pw.FontWeight.bold,
    ),
    cellStyle: const pw.TextStyle(
      color: PdfColors.blueGrey800,
      fontSize: 10,
    ),
    rowDecoration: const pw.BoxDecoration(
      border: pw.Border(
        bottom: pw.BorderSide(
          color: PdfColors.blueGrey900,
          width: .5,
        ),
      ),
    ),
    headers: ['Name', 'Occupation'],
    data: [
      for (final (name, occupation) in users) [name, occupation],
    ],
  );
}

We display users in a table. The users are stored in a list of records.

const users = [
  ('John Doe', 'gardener'),
  ('Roger Roe', 'driver'),
  ('Joe Smith', 'teacher'),
];

The data source for the table is a list of records. This replaces a dedicated class with an index-based accessor, which was needed in the older version of this example.

headerHeight: 25,
cellHeight: 40,
cellAlignment: pw.Alignment.centerLeft,

We have options that define the look of the table. Here we define the header and cell heights and the alignment of the cells.

rowDecoration: const pw.BoxDecoration(
  border: pw.Border(
    bottom: pw.BorderSide(
      color: PdfColors.blueGrey900,
      width: .5,
    ),
  ),
),

We define table bottom borders. We set the border colour and size.

headers: ['Name', 'Occupation'],
data: [
  for (final (name, occupation) in users) [name, occupation],
],

We set the headers for the table and fill it with data. Each row is a list of strings; the record pattern in the for loop extracts the two fields.

Source

Dart pdf library - documentation

In this article we have shown how to generate PDF documents in Dart.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Dart tutorials.