Skip to main content

Overview

The PDF Export plugin enables converting AppFlowy Editor documents to PDF files. It works by first converting the document to HTML/Markdown, then rendering it as PDF using the pdf package.

Installation

The PDF encoder is included with AppFlowy Editor and requires the pdf package:
dependencies:
  appflowy_editor: ^latest
  pdf: ^3.10.0
  http: ^1.0.0  # For network images
Import the encoder:
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:pdf/widgets.dart' as pw;

Basic Usage

Converting Document to PDF

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

Future<void> exportToPdf(
  EditorState editorState,
  String outputPath,
) async {
  // Convert document to markdown
  final markdown = documentToMarkdown(editorState.document);
  
  // Create PDF encoder with font configuration
  final encoder = PdfHTMLEncoder(
    font: await loadFont(),
    fontFallback: await loadFallbackFonts(),
  );
  
  // Convert to PDF
  final pdfDocument = await encoder.convert(markdown);
  
  // Save to file
  final file = File(outputPath);
  await file.writeAsBytes(await pdfDocument.save());
}

Font Configuration

PDF rendering requires font files for proper text display:

Loading Fonts

import 'package:pdf/widgets.dart' as pw;
import 'package:flutter/services.dart' show rootBundle;

Future<pw.Font> loadFont() async {
  final fontData = await rootBundle.load('assets/fonts/Roboto-Regular.ttf');
  return pw.Font.ttf(fontData);
}

Future<List<pw.Font>> loadFallbackFonts() async {
  final fonts = <pw.Font>[];
  
  // Add fonts for different scripts/languages
  final arabicFont = await rootBundle.load('assets/fonts/NotoSans-Arabic.ttf');
  fonts.add(pw.Font.ttf(arabicFont));
  
  final cjkFont = await rootBundle.load('assets/fonts/NotoSans-CJK.ttf');
  fonts.add(pw.Font.ttf(cjkFont));
  
  return fonts;
}

Creating the Encoder

final encoder = PdfHTMLEncoder(
  font: await loadFont(),           // Primary font
  fontFallback: await loadFallbackFonts(),  // Fallback fonts for special characters
);

Supported Elements

The PDF encoder supports these document elements:

Block Elements

  • Headings: H1 through H6 with appropriate sizing
  • Paragraphs: Regular text blocks
  • Lists:
    • Bulleted lists
    • Numbered lists
    • Todo lists (with checkboxes)
  • Tables: Full table support with borders
  • Images: Network and local images
  • Block quotes: Rendered as quoted sections

Text Formatting

  • Bold: <b>, <strong>
  • Italic: <i>, <em>
  • Underline: <u>
  • Strikethrough: <del>, <s>
  • Links: Rendered in blue with underline
  • Code: Inline code with gray background
  • Colors: Text and background colors from CSS

Implementation Details

Conversion Pipeline

The PDF encoder uses a two-step process:
  1. Markdown to HTML: Convert markdown to HTML using the markdown package
  2. HTML to PDF: Parse HTML and render as PDF widgets
From /lib/src/plugins/pdf/html_to_pdf_encoder.dart:24-56:
Future<pw.Document> convert(String input) async {
  // Convert markdown to HTML
  final htmlx = md.markdownToHtml(
    input,
    blockSyntaxes: const [md.TableSyntax()],
    inlineSyntaxes: [
      md.InlineHtmlSyntax(),
      md.ImageSyntax(),
      md.StrikethroughSyntax(),
    ],
  );
  
  // Parse HTML
  final document = parse(htmlx);
  final body = document.body;
  
  // Convert to PDF widgets
  final nodes = await _parseElement(body.nodes);
  final newPdf = pw.Document();
  newPdf.addPage(
    pw.MultiPage(build: (pw.Context context) => nodes.toList()),
  );
  
  return newPdf;
}

Heading Sizes

Headings are sized according to their level (from html_to_pdf_encoder.dart:657-682):
  • H1: 32pt
  • H2: 28pt
  • H3: 20pt
  • H4: 17pt
  • H5: 14pt
  • H6: 10pt

Table Rendering

Tables are fully supported with:
  • Row and column structure
  • Cell borders
  • Nested content within cells
  • Formatted text in cells
// From html_to_pdf_encoder.dart:203-252
Future<Iterable<pw.Widget>> _parseRawTableData(dom.Element element) async {
  List<pw.TableRow> tableRows = [];
  
  for (dom.Element row in element.querySelectorAll('tr')) {
    List<pw.Widget> rowData = [];
    for (final dom.Element cell in row.children) {
      // Parse cell content including nested formatting
      // ...
    }
    tableRows.add(pw.TableRow(children: rowData));
  }
  
  return [
    pw.Table(
      children: tableRows,
      border: pw.TableBorder.all(color: pdf.PdfColors.black),
    ),
  ];
}

Image Handling

Images are fetched and embedded:
// From html_to_pdf_encoder.dart:438-457
Future<pw.Widget> _parseImageElement(dom.Element element) async {
  final src = element.attributes['src'];
  try {
    if (src != null) {
      if (src.startsWith('https')) {
        final networkImage = await _fetchImage(src);
        return pw.Image(pw.MemoryImage(networkImage));
      } else {
        File localImage = File(src);
        return pw.Image(pw.MemoryImage(await localImage.readAsBytes()));
      }
    }
  } catch (e) {
    return pw.Text(e.toString());
  }
}

CSS Style Support

The encoder parses inline CSS styles:
  • font-weight: Bold text
  • font-style: Italic text
  • text-decoration: Underline and strikethrough
  • background-color: Background color
  • color: Text color
  • text-align: Text alignment (left, center, right, justify)

Advanced Features

Todo Lists

Todo lists are rendered with checkboxes:
// From html_to_pdf_encoder.dart:403-425
if (type == TodoListBlockKeys.type) {
  bool condition = element.text.contains('[x]');
  return pw.Row(
    children: [
      pw.Checkbox(
        width: 10,
        height: 10,
        name: element.text.substring(3, 6),
        value: condition,
      ),
      pw.Text(strippedString, style: textStyle),
    ],
  );
}

Multi-page Documents

Large documents automatically span multiple pages:
newPdf.addPage(
  pw.MultiPage(build: (pw.Context context) => nodes.toList()),
);

Complete Example

import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:flutter/services.dart';
import 'dart:io';

Future<void> exportEditorToPdf(
  EditorState editorState,
  String outputPath,
) async {
  // Load fonts
  final fontData = await rootBundle.load('assets/fonts/Roboto-Regular.ttf');
  final font = pw.Font.ttf(fontData);
  
  final boldFontData = await rootBundle.load('assets/fonts/Roboto-Bold.ttf');
  final boldFont = pw.Font.ttf(boldFontData);
  
  // Convert to markdown
  final markdown = documentToMarkdown(editorState.document);
  
  // Create PDF encoder
  final encoder = PdfHTMLEncoder(
    font: font,
    fontFallback: [boldFont],
  );
  
  // Convert to PDF
  final pdfDocument = await encoder.convert(markdown);
  
  // Save to file
  final file = File(outputPath);
  await file.writeAsBytes(await pdfDocument.save());
  
  print('PDF exported to: $outputPath');
}

Limitations

  • Only network images (HTTPS) and local file paths are supported
  • Complex CSS layouts are not fully supported
  • Font must be explicitly loaded (no system fonts)
  • Interactive elements cannot be embedded
  • Some advanced markdown features may not render perfectly

Performance Considerations

  • Large documents may take time to convert
  • Network images are fetched during conversion (blocking)
  • Font files increase app size
  • Consider showing a loading indicator during export

Troubleshooting

Missing Characters

If characters appear as boxes:
  • Add appropriate fallback fonts for the language/script
  • Ensure font files support the required character set

Image Loading Failures

If images don’t appear:
  • Verify network connectivity for HTTPS images
  • Check file paths for local images
  • Ensure proper error handling

Large File Size

To reduce PDF size:
  • Compress images before export
  • Use efficient font subsets
  • Consider reducing image resolution

See Also

Build docs developers (and LLMs) love