Guide

Pagination

How ngpdf breaks content into pages - pagination units, table rows and row views taller than a page, text that breaks between lines with orphans and widows, repeated table headers, breakBefore, breakAfter, keeping a view or table together and pdf-page-number formats.

You do not place page breaks by hand. ngpdf lays out each pdf-page at its content width with unbounded height, then cuts the result into sheets of the page's size. This page explains where it cuts.

Units

ngpdf moves content between pages in whole units. A unit is never split, but a long pdf-text is cut into several units, so it can break between its lines (see Text across pages), and a table row or a row pdf-view taller than a page splits between the lines of its cells or children (see Rows taller than a page and Row views taller than a page).

Starting from the children of the pdf-page, ngpdf looks inside some elements and treats others as units:

Element Treated as
pdf-view or block pdf-link with flex-direction: column (the default) and children a wrapper: its children are looked at in turn
pdf-view or block pdf-link with flex-direction: row and flex-wrap: wrap or wrap-reverse a wrapper: each of its lines is one unit (see Wrapping rows)
pdf-view with a transform one unit, with everything inside it, so it is never cut across sheets
pdf-view with overflow: hidden, clip, scroll or auto one unit, with everything inside it, so its clip is never split
pdf-table a wrapper: each of its rows is a unit; a row taller than a page splits instead (see Rows taller than a page)
pdf-view or block pdf-link with flex-direction: row that does not wrap one unit, with everything inside it; one taller than a page splits instead (see Row views taller than a page)
column pdf-view with flex-wrap whose children went into more than one column one unit, with everything inside it
pdf-text its lines, grouped by orphans and widows; one unit if it is short, has break-inside: avoid, a fixed height, a transform or an overflow other than visible
pdf-row, pdf-image, an empty pdf-view or one that holds only absolute elements and notes one unit
an element with position: absolute never a unit; see Positioned elements
pdf-note never a unit; see Notes

Wrappers can nest to any depth, so a table inside a column pdf-view inside another column pdf-view still yields its rows as units. A table inside a cell or inside a row pdf-view is part of that unit.

When the next unit does not fit below the previous one, it moves to the top of a new sheet. The vertical space between units (margins, gaps, the padding of wrappers) is kept as it was in the layout, except that a unit moved to a new sheet starts at the top of the content area. The bottom padding and border of wrappers that close right after a unit move with that unit. An auto margin counts as the space Yoga gave it in that single layout: a box with margin-top: auto or margin-bottom: auto in a column split across sheets keeps that position, and the free space is not shared out again on each sheet.

If the page's own flex-direction is row, its whole content is one unit, unless the page wraps: then each of its lines is a unit.

Wrapping rows

A row with flex-wrap: wrap or wrap-reverse breaks between its lines. Each line is a unit with every child on it, so a line that fits on an empty sheet is never cut, and a card on it never leaves the rest of the line. Only a line taller than a sheet splits, as below.

  • A line goes to the next sheet whole when it does not fit. The row gap (or align-content space) before it is dropped there, and the line starts at the top of the content area, as a child of a column does.
  • A line taller than the content area splits across sheets like a row view, with the children on the line as its columns. A line with nothing that can break, such as a tall image or a box with a fixed height, goes on a sheet of its own and is clipped, as any unit taller than a page. On a wrapping pdf-page the warning names the line: ngpdf: a wrapped line of pdf-page is taller than a page and is cut at the bottom.
  • The row's background and border are sliced per sheet like a wrapper's, and break-inside: avoid, break-before, break-after work on it as on a column pdf-view.
  • With wrap-reverse, the lines go on the sheets top to bottom as drawn, so the first children end up last.
  • align-content is not worked out again per sheet: lines keep the places Yoga gave them in the single layout of the page, apart from the space dropped before a line that moves to a new sheet. It only matters when the row has free space on the cross axis, such as a fixed height taller than its lines.
  • A wrapping row inside a table cell or inside a row pdf-view is part of that unit and does not break.

Keep units small

A pdf-view with a transform or an overflow other than visible is never split, with everything inside it, text included. A table row or a row pdf-view that does not wrap is split only when it is taller than a page; one that fits moves to the next sheet whole. A row pdf-view with flex-wrap: wrap breaks between its lines.

Text across pages

A pdf-text that is a unit by the rules above, as a child of the page or of a column pdf-view, can break between two of its lines, as a paragraph does in CSS:

html
<pdf-text style="orphans: 3; widows: 2">{{ longParagraph() }}</pdf-text>
  • orphans is the fewest lines left at the bottom of a page before the break, and widows the fewest carried to the top of the next. Both are integers of at least 1, are inherited, and default to 2.
  • A text cut by max-lines splits the same way, over the lines it keeps.
  • A text with fewer than orphans + widows lines moves to the next page whole, as do texts with break-inside: avoid, a fixed height, a max-height, a transform or an overflow other than visible.
  • A text longer than a page continues over as many pages as it needs. The first orphans and last widows lines always stay together, and a page in the middle keeps at least as many lines as the larger of the two. Each page takes as many lines as fit while the rest can still be laid out that way, so with lines of very different heights, such as a very large pdf-span, a page can end early or the text can start on the next page.
  • When no layout keeps those limits, because a few lines are taller than most of a page, lines are placed one by one between the first and last groups, and a page in the middle can have fewer lines.
  • Its top padding and border stay with its first lines, the bottom ones with its last lines. A background or border is sliced like a wrapper's.
  • A pdf-text inside a row pdf-view or a table row is part of that unit. It breaks only when the row is taller than a page, as described below.

Rows taller than a page

A table row that fits on an empty page (below its repeated header, if the table has one) moves to the next page whole, as any unit. A row taller than that splits across sheets instead of being cut:

  • It starts on the current sheet when at least one line of each non-empty cell fits there, and otherwise on the next sheet.
  • A row that fits on an empty page only without its header splits only when every piece fits below the header. When one can't, because content that can't break is taller than the space under the header, the row goes whole on a sheet of its own, without the header (see Tables).
  • Each cell breaks on its own: between the lines of its texts, or between the children of a column pdf-view inside it, at the last break that fits. The next sheet carries on from there, under the repeated header.
  • orphans and widows don't apply inside a split cell: any line can end a piece.
  • Content that can't break, such as an image, a row pdf-view, a table or a box with a fixed height, moves whole to the next piece. One taller than a page is clipped, with the warning below.
  • Every piece of the row is as tall as its tallest cell piece on that sheet, so the borders of the cells line up. Cell and row backgrounds and borders are sliced like a wrapper's: no border is drawn at a cut, and the top padding stays with the first piece.
  • Every cell of a split row is drawn from the top of each piece and stretched to its height: align-items on the row, align-self on a cell, and the space that justify-content adds above a cell's content are ignored.
  • An absolute element inside a split row is drawn on the sheet where its top lands, and cut at the bottom of that sheet.

break-inside: avoid on the row keeps it one unit, clipped as below. So does a fixed height or max-height, a transform or an overflow other than visible on the row. Header rows never split.

Row views taller than a page

A pdf-view or block pdf-link with flex-direction: row that is taller than an empty sheet splits the same way as a table row, with its children as the cells. This is how a two-column layout with a long main column runs over several pages:

html
<pdf-view style="flex-direction: row; column-gap: 24">
  <pdf-view style="flex: 1">
    @for (paragraph of article(); track $index) {
      <pdf-text style="margin-bottom: 8">{{ paragraph }}</pdf-text>
    }
  </pdf-view>
  <pdf-view style="width: 150; background-color: #f2f2f2; padding: 8">
    <pdf-text>{{ sidebar() }}</pdf-text>
  </pdf-view>
</pdf-view>
  • A row view that fits on an empty sheet moves whole, as before. Only a taller one splits, so documents that already fit keep their pages.
  • It starts on the current sheet when at least one line of each non-empty child fits there, and otherwise on the next sheet.
  • Each child breaks on its own: a pdf-text between its lines, a column pdf-view between its children (and inside them, the same way). orphans and widows don't apply. Content that can't break, such as an image, a nested row pdf-view or a box with a fixed height, moves whole to the next piece, or is clipped when it is taller than a page.
  • Every piece is as tall as its tallest child piece on that sheet.
  • The row's background and border are sliced per piece, with no border drawn at a cut. A child the row stretched to its full height (the default align-items: stretch) fills every piece too, so a sidebar's background runs down each sheet. A child that is shorter, such as with align-items: flex-start, is drawn only as far as its content goes.
  • On the first sheet each child keeps its place in the row, its margins included. On the next sheets, each child's content carries on from the top of the piece.
  • An absolute element of the row is drawn on the sheet where its top lands.
  • min-presence-ahead and break-after: avoid treat a split row view as they treat a split table row: neither moves it, since it is taller than a page. A min-presence-ahead on a child counts for the whole row when the row fits a page.

break-inside: avoid, a fixed height or max-height, a transform or an overflow other than visible on the row view keep it one unit, clipped with the warning below. There is no separate switch: break-inside: avoid keeps a view whole, and break-inside: auto (the default) lets it split.

Which views split

View Fits on an empty sheet Taller than an empty sheet
column pdf-view breaks between its children breaks between its children
column pdf-view with break-inside: avoid moves whole breaks between its children
row pdf-view moves whole splits, each child on its own
row pdf-view with break-inside: avoid, a fixed height or a max-height moves whole one unit, clipped
row pdf-view with flex-wrap: wrap breaks between its lines breaks between its lines; a line taller than a sheet splits like a row view
pdf-view with a transform or an overflow other than visible moves whole one unit, clipped
column pdf-view with flex-wrap whose children went into more than one column moves whole one unit, clipped

Units taller than a page

A unit taller than the content area goes on a sheet of its own and is clipped at the bottom of the content area. In development mode ngpdf logs a warning such as:

ngpdf: pdf-view is taller than a page and is cut at the bottom

Text lines that would cross the cut are not drawn, and images, backgrounds and side borders are cut at it. A pdf-text that can break between lines, and a table row or row pdf-view whose content can, continue on the next page instead. Only a group of lines that is itself taller than the content area, such as one line in a very large pdf-span, is still clipped.

Wrapper backgrounds and borders

A column pdf-view or a pdf-table with a background or a border that runs across several sheets is sliced: each sheet draws the part that holds its units. The bottom border is not drawn at the cut, and the top border is not drawn on the continued part, so the box reads as one box across pages.

A block pdf-link is sliced the same way: each sheet gets one clickable area over the part of the link on it.

Repeated table headers

A pdf-row with the header attribute repeats at the top of every sheet its table runs onto. See Tables for the cases where it is not repeated.

breakBefore

breakBefore on a pdf-view or pdf-text starts a new sheet before that element. On any element but a pdf-span, the style break-before: page does the same. Inside a unit, a break starts the new sheet before that whole unit, since a unit is never split: break-before: page on a pdf-cell in a table row, or breakBefore on a pdf-text inside a row pdf-view.

html
@for (chapter of chapters(); track chapter.title; let first = $first) {
  <pdf-view [breakBefore]="!first">
    <pdf-text style="font-size: 18; font-weight: bold; margin-bottom: 8">
      {{ chapter.title }}
    </pdf-text>
    @for (paragraph of chapter.paragraphs; track $index) {
      <pdf-text style="margin-bottom: 6">{{ paragraph }}</pdf-text>
    }
  </pdf-view>
}

On a wrapper, the break happens before its first unit. On a repeated table header row, it happens before the table's first row, once (see Tables). A break on a sheet that has no units yet is ignored, so you never get a blank page.

breakAfter

breakAfter on a pdf-view or pdf-text, or the style break-after: page on any element but a pdf-span, starts a new sheet after that element. It works like breakBefore turned around:

  • On a wrapper, the next sheet starts after its last unit. A split pdf-text breaks after its last line, never between its lines.
  • Inside a unit, the break comes after the whole unit: break-after: page on a pdf-cell breaks after its row.
  • When nothing follows on that pdf-page, it does nothing, so the last element never adds a blank sheet.
  • break-after: avoid or avoid-page keeps the element with what follows instead; see Keeping a heading with what follows. auto does nothing, and any other value throws.

Keeping a view or table on one sheet

break-inside: avoid on a column pdf-view or a pdf-table keeps it on one sheet when it can:

html
<pdf-view style="break-inside: avoid">
  <pdf-text style="font-weight: bold">Totals</pdf-text>
  <pdf-table [columns]="['1fr', 80]">…</pdf-table>
</pdf-view>
  • If it does not fit in the space left on the sheet but fits on a whole sheet, it moves to the next sheet in one piece. A table's header is drawn once, above its first row.
  • If it is taller than a sheet, it splits as it would without avoid: it is not clipped and there is no warning. A table's header repeats as usual.
  • Wrappers that close with it count: the bottom padding and border of a view that ends with it must fit too.
  • When kept views nest, the outermost one that fits on a sheet is kept. If the outer one is too tall, a view inside it can still be kept on its own.
  • A pdf-view or pdf-table inside a row is already part of that row's unit, so avoid changes nothing there. On a pdf-text, avoid keeps the text as one unit, as described in Text across pages.

Keeping a heading with what follows

break-after: avoid (or avoid-page) keeps an element on the same sheet as what comes after it, so a heading does not end a sheet alone, except in the cases listed below:

html
<pdf-text style="font-size: 14; font-weight: bold; break-after: avoid">Totals</pdf-text>
<pdf-text>The figures below cover the whole year…</pdf-text>
  • When the unit after it does not fit in the space left, the element moves to the next sheet with it.
  • When that unit is a pdf-text split across sheets, only its first lines need to fit: the first orphans lines. When it is a table, its first row and its header.
  • On a column pdf-view, it applies to the view's last unit, and on a split pdf-text to its last lines. Inside a unit, such as a cell in a row, it applies to the whole row.
  • Elements in a row that all have avoid, such as a heading and a subheading, move together with the unit after the last one.
  • When the whole group would not fit on an empty sheet, avoid does nothing and the sheet breaks as usual, so it never pushes a sheet's worth of content forward.
  • A page break asked for between them wins, and an element already at the top of a sheet stays there.
  • The last lines of a pdf-text that began on an earlier sheet stay where the text's own breaks put them, so orphans and widows still hold. What follows moves alone.
  • A view or table with break-inside: avoid after it counts with its whole height.
  • It is a style only; there is no input for it, and it is not inherited.

Leaving room after an element

min-presence-ahead asks for room below an element. When the element would end a sheet with less than that much space left under it, it moves to the next sheet, so a section title never sits at the very bottom with its first paragraph on the next sheet:

html
<pdf-text style="font-size: 14; font-weight: bold; min-presence-ahead: 80pt">Totals</pdf-text>
<pdf-text>The figures below cover the whole year…</pdf-text>
  • It takes 0 (the default, which turns it off) or a length in any unit ngpdf reads. A percentage, a keyword or a negative length throws. It is a style only, and it is not inherited.
  • The space left is measured to the bottom of the content area, above the page number and any fixed footer, which keep their own space.
  • An element already at the top of a sheet stays there. So does the last element of a pdf-page, and one a page break follows (its own break-after: page or breakAfter, or a break-before: page or breakBefore on what comes next), since nothing follows it on that sheet.
  • It works on any element that is a unit, such as a pdf-text, a pdf-image or a row in a table, and on a column pdf-view, a pdf-text split into line groups or a pdf-table, which then move whole. A value on a cell counts for its whole row. A row that moves gets its table's header repeated above it, as usual.
  • It applies only when the element fits whole in what is left of the sheet. One that does not moves or splits as it would without it, and the last part of an element split across sheets stays where it lands.
  • A header row's own value is ignored, whether the header repeats or not.
  • It works on its own next to break-after: avoid. When an element moves because of it, the elements before it joined to it by break-after: avoid move with it. A joined group that would not fit on an empty sheet is left where it is, as break-after: avoid leaves it.

Positioned elements

An element with position: absolute is never a unit. It never moves, splits or pushes other units, and page breaks asked for on it or inside it do nothing.

  • When its containing block is one unit (a row pdf-view, or a view with a transform or an overflow other than visible), it is drawn with that unit, on the same sheet.
  • When its containing block is split across sheets (a column pdf-view, a pdf-table or the page itself), it goes on the sheet where its own top lands, moved like the units around it. One whose top falls in the space a page break skips goes to the top of the next sheet. One whose top falls below the last unit's sheet goes on a new sheet, at its top, and the others below that sheet go on the new one too, moved with it, until one's top falls below it and starts the next. It is never drawn above the top of the content area.
  • It is never split: one taller than what is left of its sheet is cut at the bottom, with a dev-mode warning.
  • Against the page, bottom and percentages of the height are measured on the page's whole content, all sheets together, not on one sheet.
  • Inside a fixed view it is drawn with the view on every sheet, and the view is its containing block.
  • A position: relative element is placed like any other: pagination ignores its offsets, which only move it when it is drawn. A relative column pdf-view that is split moves each of its pieces by its offsets.

Notes

A pdf-note takes no space and is never a unit of its own. A note in the flow never adds a sheet: it goes on a sheet that other content already needs. A page with nothing but notes still gets its one sheet, as an empty page does.

  • It goes on the sheet of the content right after it, at the top of where that content starts. When that content moves to the next sheet, or a breakBefore or breakAfter sends it there, the note goes with it.
  • When it is the last child of its parent, it goes on the sheet of the content before it, at its bottom.
  • Inside a unit, such as a row pdf-view, it is drawn with that unit.
  • Inside a table row split across sheets, it goes on the piece where its point falls: with the content after it at a break, or on the last piece of its cell when it is that cell's last child. A note right after a split row goes on the row's last piece, at its bottom.
  • Inside a fixed view it is written on every sheet.
  • With position: absolute it follows the rules of positioned elements instead, so one placed below the sheet of the last content goes on a new sheet.

Fixed headers and footers

Give a pdf-view that is a direct child of pdf-page the fixed input to draw it on every sheet:

html
<pdf-page style="padding: 36">
  <pdf-view fixed="top" style="flex-direction: row; margin-bottom: 12">
    <pdf-text style="flex: 1; font-weight: bold">Stock count</pdf-text>
    <pdf-text>Warehouse 7</pdf-text>
  </pdf-view>
  …
  <pdf-view fixed="bottom" style="margin-top: 8">
    <pdf-text style="font-size: 8">Made-up data</pdf-text>
  </pdf-view>
  <pdf-page-number />
</pdf-page>
  • A fixed view is laid out once and drawn the same on every sheet that pdf-page produces. It is taken out of the flow wherever you write it among the page's children.
  • A fixed view is as wide as its width (a percentage is of the content width), or else the content width less its margins. Left and right auto margins place it as in CSS, so width: 200; margin: 0 auto centres it. Top and bottom auto margins on a fixed view count as 0.
  • fixed="top" views stack at the top of the content area, and fixed="bottom" views at the bottom, in template order, with their margins. The flow gets the space between them on every sheet.
  • Bottom views sit above the pdf-page-number, which keeps the very bottom.
  • A fixed view is never a pagination unit: it is not split, moved or clipped, and page breaks on it or inside it (breakBefore, breakAfter, break-before, break-after) do nothing.
  • A fixed view can have a transform, such as a header turned along the page edge. It is drawn transformed on every sheet, and the flow keeps the space of its untransformed box.
  • Fixed views that leave no room for content fail the render with ngpdf: fixed views leave no room for content on pdf-page.
  • A fixed view nested deeper than a direct child of pdf-page is laid out as an ordinary view where it is, and logs ngpdf: a fixed pdf-view must be a direct child of pdf-page in development mode: inside the flow it stays in the flow, and inside a fixed view it repeats with that view.
  • The content of a fixed view is the same on every sheet, except a pdf-page-number inside it, which shows each sheet's number (see Page numbers), and a pdfPerSheet template (see Content that changes per sheet).

Content that changes per sheet

Put an ng-template with the pdfPerSheet directive inside a fixed view to render different content on each sheet, such as "Continued on next page" on every sheet but the last:

html
<pdf-view fixed="bottom" style="height: 20; flex-direction: row">
  <ng-template pdfPerSheet let-sheet let-total="total">
    <pdf-text style="flex: 1">Sheet {{ sheet }} of {{ total }}</pdf-text>
    @if (sheet < total) {
      <pdf-text>Continued on next page</pdf-text>
    } @else {
      <pdf-text>End of report</pdf-text>
    }
  </ng-template>
</pdf-view>

pdfPerSheet comes with PDF_ELEMENTS, so there is nothing else to import. The template gets these values:

Name Type Value
$implicit, sheet number The sheet's number, from 1, across all pdf-page elements
total number The number of sheets in the whole document
first boolean Whether it is the first sheet of the document
last boolean Whether it is the last sheet of the document
page number Which pdf-page the sheet comes from, from 1, in template order
  • The template renders like the rest of the document: pipes, inputs, services and your own components work in it. Change detection runs on the template alone, once per sheet. Each template destroys its previous sheet's content before it renders the next one, and its last one when the document is destroyed, so a long document keeps at most one sheet's content per template in memory.
  • The rest of the fixed view's content stays the same on every sheet. The template's content is drawn where the ng-template sits in the view.
  • A pdf-page-number inside the template shows the same numbers as sheet and total.
  • A pdf-image in the template loads like any other, under the same maxImageBytes and server fetchImage. Each sheet loads only the sources no earlier sheet loaded, so an image over the cap that only a later sheet shows still rejects the render.
  • pdfPerSheet works only inside a fixed view (at any depth inside it). Anywhere else, the render fails with ngpdf: pdfPerSheet must be inside a fixed pdf-view. Content in the flow that changed per sheet could move a page break, which would change the number of sheets and so the content again.
  • An anchor or bookmark inside the template is ignored, since its element would be on every sheet. Put it on the fixed view itself.

Height

The fixed view reserves its height before the sheets are known. To measure it, the template is rendered once as if the document had one sheet: sheet and total are 1, and first and last are true. The flow gets the space that leaves, as with any fixed view.

On each sheet, the template's content is laid out in that reserved height. Content taller than the reservation is cut at the bottom of the view, with one development-mode warning per render: ngpdf: pdfPerSheet content is taller on sheet <n> than on the first; set a height on the fixed view.

When the content's height varies from sheet to sheet, give the fixed view a height that fits the tallest version, as in the example above.

Page numbers

A pdf-page-number shows the sheet's number. Put it directly inside a pdf-page to reserve the bottom of every sheet for it:

html
<pdf-page style="padding: 48">
  …
  <pdf-page-number format="Page {page} of {total}" style="font-size: 8; color: #777777" />
</pdf-page>
  • format replaces {page} with the sheet's number and {total} with the number of sheets in the whole document, across all pdf-page elements. Each token can appear any number of times. The default format is {page} / {total}.
  • It sits at the bottom of the content area, inside the page padding, across the content width, on every sheet that pdf-page produces. It is taken out of the flow wherever you write it among the page's children, so break-before and break-after on it do nothing.
  • It reserves its height on every sheet: one line of its font (font-size × line-height) plus its own vertical padding and borders, or its height if you set one. Content and bottom fixed views stop above it. Its text sits in the middle of that line, as in a pdf-text.
  • It is right-aligned unless it, or an element above it, sets text-align.
  • Text styles apply: font-size, color, font-weight and the rest. Other position styles are ignored.
  • The text is drawn on one line and does not wrap. Content you put inside the element is ignored.
  • Only the first pdf-page-number among the direct children of a pdf-page reserves the bottom; other direct children are ignored. Page numbers placed anywhere else are drawn too.

Anywhere else

A pdf-page-number that is not a direct child of pdf-page is laid out where you write it, like a one-line pdf-text, and shows the numbers of the sheet it is drawn on. In a fixed header, that is every sheet:

html
<pdf-view fixed="top" style="flex-direction: row">
  <pdf-text style="flex: 1; font-weight: bold">Stock count</pdf-text>
  <pdf-page-number format="Sheet {page} of {total}" />
</pdf-view>
  • The layout is done once for all sheets, so its box is measured with every {page} and {total} taken as three digits. The drawn text is aligned inside that box by text-align, so a number that grows from 9 to 10 does not move what is next to it.
  • In the flow, it shows the number of the sheet its unit lands on. {total} is always the number of sheets in the whole document.
  • Inside a pdf-text it is flattened like any other element there; put it next to the text instead, for example in a row pdf-view.

Pages with an auto height

A page with an auto height ([size]="[226, 'auto']", see Pages that grow with their content) is cut the same way, on a sheet 14400pt tall. Each sheet then ends below its content: bottom fixed views and the page number move up to sit right after it. A page break you ask for (break-before, break-after) starts a sheet sized to its own content. As in CSS, a bottom margin just before such a break is dropped; the padding and border of the wrappers that end there are kept. Content taller than 14400pt breaks onto further sheets, with one warning.

Warnings

All of these warnings are printed with console.warn at the end of the render, once each, and only in development mode (when Angular's ngDevMode is on). Production builds print nothing.