Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions pdf/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,74 @@ web.HTMLAnchorElement()
..click();
```

## MultiPage (automatic pagination)

`pw.MultiPage` automatically flows content across pages, creating page breaks when content doesn't fit.

### Key behaviors
- **Automatic page breaks**: Creates new pages when children don't fit the remaining space.
- **Headers/footers**: Built per page; their space is reserved before laying out content.
- **Spanning vs. inseparable widgets**:
- Most widgets are inseparable (must fit on one page or trigger a new page).
- Spanning widgets (`pw.Flex`, `pw.Partition`, `pw.Table`, `pw.Wrap`, `pw.GridView`, `pw.Column`) can split across pages.
- Use `pw.Inseparable` to control behavior. By default (`canSpan: true`) children can
span on other pages.
- **Page breaks**: `pw.NewPage()` always breaks. `pw.NewPage(freeSpace: 40)` breaks if < 40pt remain.
- **Safety**: `maxPages` (default 20) prevents runaway pagination in debug mode (not checked in release).
- `pw.Flexible` children consume remaining space on a page; spanning widgets cannot be flexible within `MultiPage`.

### Example
```dart
pdf.addPage(pw.MultiPage(
pageFormat: PdfPageFormat.a4,
header: (context) => pw.Padding(
padding: const pw.EdgeInsets.only(bottom: 8),
child: pw.Text('Document Header'),
),
footer: (context) => pw.Padding(
padding: const pw.EdgeInsets.only(top: 8),
child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.Text('Footer'),
pw.Text('Page ${context.pageNumber} of ${context.pagesCount}'),
],
),
),
build: (context) => [
pw.Text('Section 1: Introduction'),
pw.SizedBox(height: 20),

// Spanning content that can break across pages
pw.Wrap(
spacing: 8,
runSpacing: 8,
children: List.generate(50, (i) => pw.Container(
padding: const pw.EdgeInsets.all(8),
decoration: pw.BoxDecoration(
border: pw.Border.all(),
borderRadius: const pw.BorderRadius.all(pw.Radius.circular(4)),
),
child: pw.Text('Item $i'),
)),
),

pw.NewPage(), // Force page break

// Inseparable content that stays together
pw.Inseparable(
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text('• This content must stay together'),
pw.Text('• It cannot be split across pages'),
],
),
),
],
));
```

## Encryption, Digital Signature, and loading a PDF Document

Encryption using RC4-40, RC4-128, AES-128, and AES-256 is fully supported using a separate library.
Expand Down
136 changes: 57 additions & 79 deletions pdf/lib/src/widgets/multi_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,7 @@ mixin SpanningWidget on Widget {
/// Called before relayout to restore the saved state and
/// restart the layout in the same conditions
@protected
void applyContext(covariant WidgetContext context) =>
saveContext().apply(context);
void applyContext(covariant WidgetContext context) => saveContext().apply(context);
}

/// Trigger a page break if there is not enough free space.
Expand All @@ -72,13 +71,11 @@ class NewPage extends Widget {
final double? freeSpace;

@override
void layout(Context context, BoxConstraints constraints,
{bool parentUsesSize = false}) {
void layout(Context context, BoxConstraints constraints, {bool parentUsesSize = false}) {
box = PdfRect.zero;
}

bool newPageNeeded(double availableSpace) =>
(freeSpace == null) || (availableSpace < freeSpace!);
bool newPageNeeded(double availableSpace) => (freeSpace == null) || (availableSpace < freeSpace!);
}

@immutable
Expand Down Expand Up @@ -147,6 +144,27 @@ class _MultiPageInstance {
///
/// The [Wrap] [Widget] here is able to rearrange its children to span them across
/// multiple pages. But a child of [Wrap] must fit in a page, or an error will raise.
///
/// Use [Inseparable] to control page breaking. By default (`canSpan: false`),
/// widgets stay on one page. Set `canSpan: true` to allow spanning:
///
/// ```dart
/// final pdf = Document();
/// pdf.addPage(MultiPage(build: (context) {
/// return [
/// Text('Header'),
/// Inseparable(
/// canSpan: true, // Allows spanning if content is too large
/// child: Wrap(
/// children: [
/// Text('This wrap can span'),
/// Text('across multiple pages'),
/// ]
/// )
/// ),
/// ];
/// }));
/// ```
class MultiPage extends Page {
MultiPage({
PageTheme? pageTheme,
Expand Down Expand Up @@ -193,17 +211,15 @@ class MultiPage extends Page {
/// This is not checked with a Release build.
final int maxPages;

void _paintChild(
Context context, Widget child, double x, double y, double pageHeight) {
void _paintChild(Context context, Widget child, double x, double y, double pageHeight) {
if (mustRotate) {
final _margin = resolvedMargin!;
context.canvas
..saveContext()
..setTransform(
Matrix4.identity()
..rotateZ(-math.pi / 2)
..translateByDouble(x - pageHeight + _margin.top - _margin.left,
y + _margin.left - _margin.bottom, 0, 1),
..translateByDouble(x - pageHeight + _margin.top - _margin.left, y + _margin.left - _margin.bottom, 0, 1),
);
child.paint(context);
context.canvas.restoreContext();
Expand All @@ -221,30 +237,23 @@ class MultiPage extends Page {
final _margin = resolvedMargin!;
final _mustRotate = mustRotate;
final pageHeight = _mustRotate ? pageFormat.width : pageFormat.height;
final pageHeightMargin =
_mustRotate ? _margin.horizontal : _margin.vertical;
final pageHeightMargin = _mustRotate ? _margin.horizontal : _margin.vertical;
final constraints = BoxConstraints(
maxWidth: _mustRotate
? (pageFormat.height - _margin.vertical)
: (pageFormat.width - _margin.horizontal));
maxWidth: _mustRotate ? (pageFormat.height - _margin.vertical) : (pageFormat.width - _margin.horizontal));
final fullConstraints = mustRotate
? BoxConstraints(
maxWidth: pageFormat.height - _margin.vertical,
maxHeight: pageFormat.width - _margin.horizontal)
maxWidth: pageFormat.height - _margin.vertical, maxHeight: pageFormat.width - _margin.horizontal)
: BoxConstraints(
maxWidth: pageFormat.width - _margin.horizontal,
maxHeight: pageFormat.height - _margin.vertical);
maxWidth: pageFormat.width - _margin.horizontal, maxHeight: pageFormat.height - _margin.vertical);
final calculatedTheme = theme ?? document.theme ?? ThemeData.base();
Context? context;
var offsetEnd = 0.0;
double? offsetStart;
var _index = 0;
var sameCount = 0;
final baseContext =
Context(document: document.document).inheritFromAll(<Inherited>[
final baseContext = Context(document: document.document).inheritFromAll(<Inherited>[
calculatedTheme,
if (pageTheme.textDirection != null)
InheritedDirectionality(pageTheme.textDirection),
if (pageTheme.textDirection != null) InheritedDirectionality(pageTheme.textDirection),
]);
final children = _buildList(baseContext);
WidgetContext? widgetContext;
Expand All @@ -262,13 +271,10 @@ class MultiPage extends Page {
}());

// Calculate available space of the current page
final freeSpace = (offsetStart == null)
? fullConstraints.maxHeight
: offsetStart - offsetEnd;
final freeSpace = (offsetStart == null) ? fullConstraints.maxHeight : offsetStart - offsetEnd;

// Create a new page if we don't already have one
if (context == null ||
(child is NewPage) && child.newPageNeeded(freeSpace)) {
if (context == null || (child is NewPage) && child.newPageNeeded(freeSpace)) {
final pdfPage = PdfPage(
document.document,
pageFormat: pageFormat,
Expand All @@ -285,10 +291,8 @@ class MultiPage extends Page {
return true;
}());

offsetStart = pageHeight -
(_mustRotate ? pageHeightMargin - _margin.bottom : _margin.top);
offsetEnd =
_mustRotate ? pageHeightMargin - _margin.left : _margin.bottom;
offsetStart = pageHeight - (_mustRotate ? pageHeightMargin - _margin.bottom : _margin.top);
offsetEnd = _mustRotate ? pageHeightMargin - _margin.left : _margin.bottom;

_pages.add(_MultiPageInstance(
context: context,
Expand Down Expand Up @@ -340,8 +344,7 @@ class MultiPage extends Page {

// Else we crash if the widget is too big and cannot be separated
if (!canSpan) {
throw Exception(
'Widget won\'t fit into the page as its height (${child.box!.height}) '
throw Exception('Widget won\'t fit into the page as its height (${child.box!.height}) '
'exceed a page height (${pageHeight - pageHeightMargin}). '
'You probably need a SpanningWidget or use a single page layout');
}
Expand All @@ -353,8 +356,7 @@ class MultiPage extends Page {
span.applyContext(savedContext);
}

final localConstraints =
constraints.copyWith(maxHeight: offsetStart - offsetEnd);
final localConstraints = constraints.copyWith(maxHeight: offsetStart - offsetEnd);
span.layout(context, localConstraints, parentUsesSize: false);
assert(span.box != null);
widgetContext = span.saveContext();
Expand All @@ -381,8 +383,7 @@ class MultiPage extends Page {
_MultiPageWidget(
child: child,
constraints: constraints,
widgetContext:
child is SpanningWidget && canSpan ? child.cloneContext() : null,
widgetContext: child is SpanningWidget && canSpan ? child.cloneContext() : null,
),
);

Expand All @@ -398,27 +399,21 @@ class MultiPage extends Page {
final _mustRotate = mustRotate;
final pageHeight = _mustRotate ? pageFormat.width : pageFormat.height;
final pageWidth = _mustRotate ? pageFormat.height : pageFormat.width;
final pageHeightMargin =
_mustRotate ? _margin.horizontal : _margin.vertical;
final pageHeightMargin = _mustRotate ? _margin.horizontal : _margin.vertical;
final pageWidthMargin = _mustRotate ? _margin.vertical : _margin.horizontal;
final availableWidth = pageWidth - pageWidthMargin;
final isRTL = pageTheme.textDirection == TextDirection.rtl;
for (final page in _pages) {
var offsetStart = pageHeight -
(_mustRotate ? pageHeightMargin - _margin.bottom : _margin.top);
var offsetEnd =
_mustRotate ? pageHeightMargin - _margin.left : _margin.bottom;
var offsetStart = pageHeight - (_mustRotate ? pageHeightMargin - _margin.bottom : _margin.top);
var offsetEnd = _mustRotate ? pageHeightMargin - _margin.left : _margin.bottom;

if (pageTheme.buildBackground != null) {
final child = pageTheme.buildBackground!(page.context);

child.layout(page.context, page.fullConstraints, parentUsesSize: false);
assert(child.box != null);
final xPos = isRTL
? _margin.left + (availableWidth - child.box!.width)
: _margin.left;
_paintChild(
page.context, child, xPos, _margin.bottom, pageFormat.height);
final xPos = isRTL ? _margin.left + (availableWidth - child.box!.width) : _margin.left;
_paintChild(page.context, child, xPos, _margin.bottom, pageFormat.height);
}

var totalFlex = 0;
Expand All @@ -443,28 +438,20 @@ class MultiPage extends Page {

if (header != null) {
final headerWidget = header!(page.context);
headerWidget.layout(page.context, page.constraints,
parentUsesSize: false);
headerWidget.layout(page.context, page.constraints, parentUsesSize: false);
assert(headerWidget.box != null);
offsetStart -= headerWidget.box!.height;
final xPos = isRTL
? _margin.left + (availableWidth - headerWidget.box!.width)
: _margin.left;
_paintChild(page.context, headerWidget, xPos,
page.offsetStart! - headerWidget.box!.height, pageFormat.height);
final xPos = isRTL ? _margin.left + (availableWidth - headerWidget.box!.width) : _margin.left;
_paintChild(page.context, headerWidget, xPos, page.offsetStart! - headerWidget.box!.height, pageFormat.height);
}

if (footer != null) {
final footerWidget = footer!(page.context);
footerWidget.layout(page.context, page.constraints,
parentUsesSize: false);
footerWidget.layout(page.context, page.constraints, parentUsesSize: false);
assert(footerWidget.box != null);
final xPos = isRTL
? _margin.left + (availableWidth - footerWidget.box!.width)
: _margin.left;
final xPos = isRTL ? _margin.left + (availableWidth - footerWidget.box!.width) : _margin.left;
offsetEnd += footerWidget.box!.height;
_paintChild(page.context, footerWidget, xPos, _margin.bottom,
pageFormat.height);
_paintChild(page.context, footerWidget, xPos, _margin.bottom, pageFormat.height);
}

final freeSpace = math.max(0.0, offsetStart - offsetEnd - allocatedSize);
Expand Down Expand Up @@ -493,16 +480,14 @@ class MultiPage extends Page {
break;
case MainAxisAlignment.spaceBetween:
leadingSpace = 0.0;
betweenSpace =
totalChildren > 1 ? freeSpace / (totalChildren - 1) : 0.0;
betweenSpace = totalChildren > 1 ? freeSpace / (totalChildren - 1) : 0.0;
break;
case MainAxisAlignment.spaceAround:
betweenSpace = totalChildren > 0 ? freeSpace / totalChildren : 0.0;
leadingSpace = betweenSpace / 2.0;
break;
case MainAxisAlignment.spaceEvenly:
betweenSpace =
totalChildren > 0 ? freeSpace / (totalChildren + 1) : 0.0;
betweenSpace = totalChildren > 0 ? freeSpace / (totalChildren + 1) : 0.0;
leadingSpace = betweenSpace;
break;
}
Expand All @@ -514,11 +499,8 @@ class MultiPage extends Page {
final flex = child is Flexible ? child.flex : 0;
final fit = child is Flexible ? child.fit : FlexFit.loose;
if (flex > 0) {
assert(child is! SpanningWidget || child.canSpan == false,
'Cannot have a spanning widget flexible');
final maxChildExtent = child == lastFlexChild
? (freeSpace - allocatedFlexSpace)
: spacePerFlex * flex;
assert(child is! SpanningWidget || child.canSpan == false, 'Cannot have a spanning widget flexible');
final maxChildExtent = child == lastFlexChild ? (freeSpace - allocatedFlexSpace) : spacePerFlex * flex;
late double minChildExtent;
switch (fit) {
case FlexFit.tight:
Expand Down Expand Up @@ -572,8 +554,7 @@ class MultiPage extends Page {
if (child is SpanningWidget && child.canSpan) {
child.applyContext(widget.widgetContext!);
}
_paintChild(page.context, widget.child, _margin.left + x, pos,
pageFormat.height);
_paintChild(page.context, widget.child, _margin.left + x, pos, pageFormat.height);
pos -= betweenSpace;
}

Expand All @@ -582,11 +563,8 @@ class MultiPage extends Page {

child.layout(page.context, page.fullConstraints, parentUsesSize: false);
assert(child.box != null);
final xPos = isRTL
? _margin.left + (availableWidth - child.box!.width)
: _margin.left;
_paintChild(
page.context, child, xPos, _margin.bottom, pageFormat.height);
final xPos = isRTL ? _margin.left + (availableWidth - child.box!.width) : _margin.left;
_paintChild(page.context, child, xPos, _margin.bottom, pageFormat.height);
}
}
}
Expand Down