Guide

Text and fonts

Write text with pdf-text, use the standard PDF fonts or your own with providePdfFonts, and learn how ngpdf falls back between fonts, wraps lines, hyphenates words, cuts text with max-lines and handles line height.

Text goes in pdf-text. It stays selectable and searchable in the PDF.

html
<pdf-page style="padding: 36; font-family: Times; font-size: 12; line-height: 1.4">
  <pdf-text style="font-size: 18; font-weight: bold">Release notes</pdf-text>
  <pdf-text>Body text set in Times-Roman.</pdf-text>
  <pdf-text style="font-style: italic; color: #555555">An italic aside.</pdf-text>
  <pdf-text style="font-family: monospace; font-size: 9">npm install &#64;ngpdf/core</pdf-text>
  <pdf-text style="text-align: center; text-decoration: line-through">Withdrawn</pdf-text>
</pdf-page>

The default text is Helvetica, 10pt, black, with a line height of 1.2, aligned left.

Fonts

By default ngpdf uses the standard fonts that every PDF reader has, so nothing is embedded. Of the 14 standard PDF fonts, the 12 text faces are available: Helvetica, Times and Courier, each in regular, bold, italic and bold italic. Symbol and ZapfDingbats are not available. For other fonts and for characters beyond WinAnsi, register your own: see Custom fonts.

You pick a face with three properties:

Property Value Result
font-family contains courier or mono Courier
contains times, or serif but not sans Times
anything else Helvetica
font-weight above 500 bold
500 or less regular
font-style italic or oblique italic
normal upright

font-weight is a number from 1 to 1000 or one of the names below, in any case. Any other value throws ngpdf: invalid value "<value>" for font-weight on <tag>, and the message lists the allowed values. font-style is normal, italic or oblique; any other value throws the same way. The standard fonts have only a regular and a bold face, so 100 to 500 is regular and 600 to 900 is bold; a registered family can have a face for every weight.

Name Weight
thin 100
extralight 200
light 300
normal 400
medium 500
semibold 600
bold 700
extrabold 800
black 900
bolder from the inherited weight: below 350 gives 400, below 550 gives 700, below 900 gives 900, and 900 or more stays
lighter from the inherited weight: below 100 stays, below 550 gives 100, below 750 gives 400, and 750 or more gives 700

When the value names no registered family, the family match ignores case and looks at the whole value, so 'Times New Roman', serif, Georgia, serif and serif all give Times, and Arial, Inter or sans-serif give Helvetica. ngpdf never throws for a family name.

The resulting font names in the PDF are Helvetica, Helvetica-Bold, Helvetica-Oblique, Helvetica-BoldOblique, Times-Roman, Times-Bold, Times-Italic, Times-BoldItalic, Courier, Courier-Bold, Courier-Oblique and Courier-BoldOblique.

Custom fonts

Register fonts once with providePdfFonts, then name them in font-family:

ts
// app.config.ts
import { providePdfFonts } from '@ngpdf/core';

export const appConfig: ApplicationConfig = {
  providers: [
    providePdfFonts([
      { family: 'Inter', src: '/fonts/Inter-Regular.woff2' },
      { family: 'Inter', src: '/fonts/Inter-Light.woff2', weight: 300 },
      { family: 'Inter', src: '/fonts/Inter-Bold.woff2', weight: 'bold' },
      { family: 'Noto Sans JP', src: '/fonts/NotoSansJP-Regular.ttf' },
    ]),
  ],
};
html
<pdf-page style="font-family: Inter, 'Noto Sans JP'">
  <pdf-text>Zoë, Łódź, Ελληνικά, Кириллица, 日本語</pdf-text>
</pdf-page>
ts
interface PdfFont {
  readonly family: string;
  readonly src: string | ArrayBuffer | Uint8Array | Blob;
  readonly weight?: number | 'normal' | 'bold'; // 1 to 1000; 'normal' is 400 (default), 'bold' 700
  readonly style?: 'normal' | 'italic'; // default 'normal'
}
function providePdfFonts(fonts: readonly PdfFont[]): EnvironmentProviders;
  • Formats: TrueType (.ttf), OpenType (.otf), WOFF and WOFF2. Font collections (.ttc) are not supported.
  • Sources: src is a URL, a data URL, or the bytes. A URL is fetched like an image (same-origin credentials only), on the first render whose document names its family. A font no document names is never fetched, so a broken font file fails only the documents that use it. It is fetched once per injector and reused by later renders. A failed fetch is not kept, so the next render tries again. With renderToBuffer from @ngpdf/core/server, a font URL is read only through its fetchImage, and kept for that render only; see Server rendering. renderToBlob and PdfRenderer.toBlob fetch it as above, also on a server.
  • Embedding: each PDF embeds only the fonts its pages use, cut down to the characters it draws.
  • Matching: family is matched in any case. Each face is one weight and style; register one entry per face. A weight outside 1 to 1000 rejects the first render that uses the family with ngpdf: invalid font weight "<weight>" for "<family>".
  • Missing faces: if a family has no face for the text's weight, ngpdf picks a weight as CSS does. From 400 to 500 it tries heavier weights up to 500 first, going up, then lighter weights going down, then heavier weights above 500, so with only whole hundreds 400 tries 500 first and 500 tries 400 first. Below 400 it tries lighter weights first, then heavier; above 500, heavier first, then lighter. Among the faces of that weight it takes the text's style, or else the upright one. It warns in development mode, naming the face it used (ngpdf: font "Inter" has no medium face; using its regular face), and it never makes a bold or italic face up.
  • Where they apply: documents rendered with the injector that has the provider, or one below it. providePdfFonts in a route's providers adds fonts for that route's renders through pdfResource, pdfDownload and renderToBlob, on top of the app's. PdfRenderer is a root service, so it sees only the app's fonts. Several providePdfFonts calls in one injector add up.

The family list and fallback

font-family is read as a list. Each character is drawn in the first font of the list that has it:

  • A registered family is used for the characters its font has.
  • A standard name in the list covers WinAnsi. The names are, in any case: Helvetica, Arial, sans-serif and system-ui for Helvetica; Times, Times New Roman, Times-Roman and serif for Times; Courier, Courier New, monospace and ui-monospace for Courier.
  • Names that are neither are skipped, including web fonts such as Open Sans or IBM Plex Mono that you have not registered.
  • When the list has registered families but no standard name, a standard font is added at the end, chosen from the whole value as above. So WinAnsi characters a custom font lacks are still drawn.

With font-family: Inter, 'Noto Sans JP', Latin text is drawn in Inter, Japanese in Noto Sans JP, and a € that neither had would be drawn in Helvetica. Kerning applies within each font's run of text, not across two fonts.

A character that no font in the list has is drawn as the first font's empty glyph, usually a box. In development mode ngpdf logs one warning per render listing them:

ngpdf: characters missing from every font in their font-family, drawn as an empty box: ☃

If the list starts with a standard font, such a character becomes ?, as below.

There is no right-to-left text yet, so Arabic and Hebrew are not laid out correctly, and color emoji fonts are not supported. Emoji can be drawn as images instead: see Emoji.

Emoji

A font draws emoji in one color at best, and the standard fonts draw them as ?. To draw them in color, give ngpdf a source of emoji images with providePdfEmoji. Each emoji is then drawn as a small inline image in the text.

ts
// app.config.ts
import { providePdfEmoji } from '@ngpdf/core';

export const appConfig: ApplicationConfig = {
  providers: [providePdfEmoji('/assets/emoji/{codepoints}.png')],
};
html
<pdf-text>Shipped 👍🏽 to 🇧🇷 at 9️⃣ o'clock 🎉</pdf-text>
ts
type PdfEmojiImage = string | Uint8Array | Blob | ArrayBuffer;
type PdfEmojiSource =
  | string
  | ((emoji: string, codepoints: string) => PdfEmojiImage | null | Promise<PdfEmojiImage | null>);
function providePdfEmoji(source: PdfEmojiSource): EnvironmentProviders;
  • URL template: a string source is a URL in which {codepoints} is replaced by the emoji's code points in lower-case hex, joined by -, without U+FE0F. 👍🏽 becomes 1f44d-1f3fd and #️⃣ becomes 23-20e3. Emoji image sets name their files their own way: Twemoji keeps U+FE0F in some names, and Noto Emoji uses emoji_u, _ and at least four hex digits. To match one exactly, use a function source.
  • Function: a function source gets the emoji and the same code points, and returns a PNG or JPEG as a URL, a data URL or the bytes, or a promise of one. It returns null to draw that emoji as text.
  • No images by default: ngpdf ships no emoji images and fetches nothing unless you provide a source. Without one, an emoji is text like any other character: a custom font in the font-family list that has the glyph draws it, and one that no font in the list has follows the fallback, ? when the list starts with a standard font and the first font's empty glyph otherwise.
  • Where it applies: documents rendered with the injector that has the provider, or one below it, as with custom fonts. A provider in a child injector replaces the parent's.

A function that matches Noto Emoji's file names (emoji_u1f44d_1f3fd.png, emoji_u00a9.png), for a copy of its png/72 folder:

ts
providePdfEmoji((_emoji, codepoints) => {
  const name = codepoints
    .split('-')
    .map((hex) => hex.padStart(4, '0'))
    .join('_');
  return `/assets/noto/emoji_u${name}.png`;
});

That folder has no flags: the Noto Emoji repository keeps them apart, in third_party/region-flags/png, named by region code (BR.png) or, for England, Scotland and Wales, by subdivision (GB-SCT.png). Copy those too and send each regional-indicator pair and each 🏴 tag sequence there:

ts
const SUBDIVISIONS = new Set(['gbeng', 'gbsct', 'gbwls']);

providePdfEmoji((_emoji, codepoints) => {
  const points = codepoints.split('-').map((hex) => parseInt(hex, 16));
  if (points.length === 2 && points.every((point) => point >= 0x1f1e6 && point <= 0x1f1ff)) {
    const region = String.fromCharCode(...points.map((point) => point - 0x1f1e6 + 65));
    return `/assets/flags/${region}.png`;
  }
  // 🏴 followed by tag letters and a cancel tag, such as 🏴󠁧󠁢󠁳󠁣󠁴󠁿 (gbsct): GB-SCT.png.
  if (points[0] === 0x1f3f4 && points.at(-1) === 0xe007f) {
    const tag = String.fromCharCode(...points.slice(1, -1).map((point) => point - 0xe0000));
    // Only the files Noto has: any other tag, which could hold "/" or "..", is drawn as text.
    return SUBDIVISIONS.has(tag) ? `/assets/flags/GB-${tag.slice(2).toUpperCase()}.png` : null;
  }
  return `/assets/noto/emoji_u${points.map((point) => point.toString(16).padStart(4, '0')).join('_')}.png`;
});

What counts as an emoji

A character as you see it (a grapheme cluster) is an emoji when:

  • it contains U+FE0F (the emoji variation selector);
  • or it is a ZWJ sequence that starts with a pictographic character (Extended_Pictographic), such as 👨‍👩‍👧‍👦 or ⛹‍♀;
  • or it is a keycap: a digit, # or * followed by U+20E3, with or without U+FE0F, such as #️⃣ or 1⃣;
  • or it is a modifier sequence: a character that takes a skin tone (Emoji_Modifier_Base) followed by one, such as ✌🏽;
  • or it starts with a character that shows as an emoji by default, which covers 😀, a skin tone such as 👍🏽, flags such as 🇧🇷 and tag sequences such as 🏴󠁧󠁢󠁳󠁣󠁴󠁿.

©, ®, ™, the digits and other characters that show as text by default stay text on their own. They become emoji with U+FE0F after them, in a keycap (the digits, # and *), or, for a pictographic character, in a ZWJ sequence or with a skin tone, as above. A cluster whose second character is U+FE0E (the text variation selector) always stays text.

Layout

  • Each emoji is a square 1 em on each side, at the font-size of its text. The square's bottom sits on the font's descent and its top is 1 em above that, so it lines up with the text around it. An image that is not square is fitted inside the square, centered, without stretching, so a wide one ends above the descent. A writer whose font metrics give no descent puts the square's bottom on the baseline instead; the default pdfkit writer always gives one.
  • It takes the text's letter-spacing, and lines break around it as around any character, never inside it.
  • An underline or a line-through runs under or through it like text. Its color does not apply to the image.
  • max-lines, text-align: justify and text-transform work as they do for text. An ellipsis after an emoji is drawn in the font of the text around it.
  • The text of pdf-page-number is always drawn as text.

Loading

  • Each distinct emoji is loaded once per render, together with the document's images and before layout. Six loads start at a time, and the next one starts when one of them ends or runs out of time. A load that runs out of time is no longer waited for, but a source function or a server fetchImage still running is not stopped, so more than 6 can still be in progress. The PDF embeds each image once, however often the emoji appears.
  • URLs are fetched like images, with same-origin credentials only. With renderToBuffer on the server, they are read only through its fetchImage, like images (see Server rendering). An emoji whose URL is refused is drawn as text.
  • A render loads at most 500 distinct emoji. The rest are drawn as text, with one warning in development mode: ngpdf: more than 500 distinct emoji; the rest are drawn as text.
  • An emoji whose image fails to load, is not a PNG or JPEG, is a PNG cut off after its header, is null, has no width or height, or takes more than 10 seconds from the start of its load is drawn as text. A fetch that runs out of time is aborted. The 10 seconds cannot interrupt a source function while it runs synchronously: they only cut off waiting for its promise or for a fetch. Emoji often come from user data, so a missing one does not fail the render. In development mode ngpdf logs one warning per render listing them: ngpdf: emoji without an image, drawn as text: 🦤.
  • A JPEG is checked only up to its frame header, which gives its size, because pdfkit embeds JPEG data as it is. A JPEG cut off after that header is embedded, and how it shows depends on the PDF viewer.

Limits

  • An emoji image is not text. Copying, searching and screen readers skip it. Giving it real text needs a tagged PDF, which ngpdf does not write yet.
  • Only raster images (PNG and JPEG) work. Color font formats (COLR, sbix and CBDT) are not read.

Characters: WinAnsi

The standard fonts use the WinAnsi encoding (Windows-1252). Text in a custom font is not limited to it. It covers ASCII, the Latin-1 accented letters (á, ç, ñ, ü and so on) and a few more, such as €, “ ”, ‘ ’, –, —, …, • and ™.

An accented letter written as a base letter plus a combining mark, such as e followed by U+0301 (common in macOS file names), is drawn as the composed letter é when it has one. In a font-family list, a font that lacks the letter as written but has its composed form draws that form; a custom font that has the combining mark draws the text as written.

In a standard font, any other character is drawn as ?. That includes emoji (unless providePdfEmoji gives them images), Greek, Cyrillic, CJK, arrows and check marks. In development mode ngpdf logs one warning per render that lists every replaced character:

ngpdf: characters outside WinAnsi replaced with "?": ✓→

Some characters are handled first, in every font, so they are never drawn as ? or as a missing glyph:

  • The Unicode spaces that locale data puts in pipe output, such as the thin space (U+2009) and the narrow no-break space (U+202F), and the other spaces from U+2000 to U+200A and U+205F, are drawn as a normal space. The narrow no-break space and the figure space (U+2007) become a no-break space (U+00A0), so 3:00 PM from date: 'short' still does not break between the time and PM.
  • Vertical tab, form feed, next line (U+0085), and the line and paragraph separators (U+2028, U+2029) are drawn as a normal space, so they collapse and trim like any other whitespace.
  • The ideographic space (U+3000) and the Ogham space mark (U+1680) are kept for a custom font that has them, and drawn as a normal space when no font in the list has them and the list starts with a standard font. They are trimmed at the ends of the text but never collapsed.
  • A zero-width space (U+200B) and U+FEFF (a byte order mark or zero-width no-break space) are removed. ngpdf does not break lines at them, so they are never drawn.
  • A soft hyphen (U+00AD) is never drawn itself. It marks a place where a word may break, with a hyphen drawn at the end of the line; see Hyphenation.

The same applies to the text of pdf-page-number.

Wrapping

  • By default, whitespace collapses as in CSS white-space: normal: runs of spaces, tabs and line breaks become one space, and the ends are trimmed. A line break in your data does not start a new line unless you set white-space.
  • Lines wrap at Unicode line-break opportunities (UAX #14), so they break after spaces and hyphens, and not inside words.
  • A word wider than its box is broken at the box edge, between characters as you see them (grapheme clusters), so an accent stays with its letter.
  • Widths include the font's kerning, and the ligatures of a custom font.
  • Words break with a hyphen at soft hyphens, and with hyphens: auto where your hyphenation function allows. See Hyphenation.
  • There is no right-to-left text.

White space

white-space sets which spaces and line breaks of the text are kept, and whether lines wrap at the box edge. It is inherited, and a pdf-span can set it. The values mean what they mean in CSS:

Value Spaces and tabs Line breaks Wraps
normal (default) collapse collapse yes
nowrap collapse collapse no
pre kept kept no
pre-line collapse kept yes
pre-wrap kept kept yes

An address or a note typed with line breaks keeps them with pre-line:

html
<pdf-text style="white-space: pre-line">{{ customer.address }}</pdf-text>
  • A line break is LF, CRLF or CR. Where line breaks are kept, each one starts a new line, and two in a row leave an empty line. A line break at the very end of the text adds no line.
  • Under pre-line, the spaces around a line break are dropped, and the ends of the text are trimmed. Under pre and pre-wrap, nothing is trimmed, so put the binding right against the tags: <pdf-text>{{ text }}</pdf-text>.
  • Under pre and pre-wrap, a tab moves to the next tab stop. Stops are every 8 space widths of the tab's font, counted from the start of the line, as with CSS's default tab-size. A tab is not underlined. There is no tab-size property.
  • Under pre-wrap, spaces and tabs at the end of a line hang past it and are not drawn. Under pre, they count in the line's width.
  • Text that does not wrap (nowrap, pre) and is wider than its box is drawn past the edge, or clipped with overflow: hidden. A word in it is never cut at the box edge or hyphenated.
  • A nowrap or pre pdf-span inside a wrapping paragraph does not break inside.
  • A kept line break ends the line before it, so text-align: justify leaves that line at the start, as it does the last line. max-lines counts lines across kept line breaks.

A long pdf-text breaks between lines at a page break, keeping orphans lines (default 2) before the break and widows lines (default 2) after it. break-inside: avoid keeps it whole. See Text across pages.

Hyphenation

hyphens sets where a word may break with a hyphen at the end of the line. It is inherited, and a pdf-span can set it for its own text.

Value Where words break
manual (default) at soft hyphens (U+00AD, &shy; in a template)
none nowhere: soft hyphens are ignored, and still not drawn
auto at soft hyphens, and where the function from providePdfHyphenation allows
html
<pdf-text style="width: 90pt">Pay the extra&shy;ordinary invoice today.</pdf-text>
  • A word is hyphenated only when it does not fit on what is left of the line. The line then takes as many pieces of it as fit, plus the hyphen. A word that does not fit even an empty line is cut at the box edge only when no piece fits.
  • The hyphen is drawn in the font of the text it ends: ‐ (U+2010) when the font has it, - otherwise. The standard fonts have no U+2010, so they draw -. It counts in the line's width, is underlined or struck through with the text, and is copied with it.
  • A soft hyphen where the line does not break draws nothing and takes no width.
  • Justified text, max-lines, letter-spacing and text-transform work on hyphenated lines. A line cut by max-lines ends with …, not a hyphen.

Automatic hyphenation

ngpdf ships no hyphenation patterns, because they are large and differ by language. For hyphens: auto, give it a function that cuts a word into the pieces it may break between:

ts
type PdfHyphenate = (word: string, language: string | undefined) => readonly string[];
function providePdfHyphenation(hyphenate: PdfHyphenate): EnvironmentProviders;
  • language is the language of the pdf-document, so one function can serve several languages.
  • The pieces must join back to the word: 'hyphenation' gives ['hy', 'phen', 'ation']. A function that throws, or returns anything else, means that word is not hyphenated automatically, with one warning in development mode. Like any word too long for its box, it can still break at the box edge.
  • Only words of 5 letters or more are hyphenated, with at least 2 letters on each side of the break, as browsers do by default. Punctuation around a word stays outside it.
  • text-transform applies first, so the function gets the transformed word.
  • A word that has soft hyphens breaks only at them.
  • The function is called only for words that do not fit their line, at most once per word and render.
  • Without the provider, auto breaks at soft hyphens only, and warns once per render in development mode.
  • Like providePdfFonts, it applies to documents rendered with its injector or one below it, so a route or a child injector can have its own. pdfResource, pdfDownload and renderToBlob see a route's provider. PdfRenderer is a root service, so it sees only the app's: to use a route's function, pass the route's injector to renderToBlob or downloadPdf, as below.

A pattern library, such as the hyphen package from npm, can back the function. hyphen ships no type declarations, so a TypeScript app also installs @types/hyphen:

sh
npm install hyphen
npm install --save-dev @types/hyphen

Load it lazily, so its patterns stay out of your initial bundle and load only when a document needs them:

ts
// pdf-hyphenation.ts
import { providePdfHyphenation } from '@ngpdf/core';

let hyphenateEn: ((text: string) => string) | undefined;

/** Loads the English patterns once; await it before rendering a hyphenated document. */
export async function loadHyphenation(): Promise<void> {
  hyphenateEn ??= (await import('hyphen/en')).hyphenateSync;
}

// hyphenateSync puts a soft hyphen at each break, so splitting on it gives the pieces.
export const pdfHyphenation = providePdfHyphenation((word) =>
  hyphenateEn ? hyphenateEn(word).split('\u00ad') : [word],
);
ts
// app.config.ts: providers: [pdfHyphenation]
// in the component that renders:
await loadHyphenation();
await this.pdf.download(ReportDocument, { fileName: 'report.pdf' });

With pdfHyphenation in a route's providers instead, render with that route's injector, since PdfRenderer would not see it:

ts
// in a component of that route
private readonly injector = inject(EnvironmentInjector);

async save(): Promise<void> {
  await loadHyphenation();
  await downloadPdf(ReportDocument, { injector: this.injector, fileName: 'report.pdf' });
}

Cutting text to a number of lines

max-lines keeps at most that many lines of a pdf-text. When text was cut, the last kept line ends with an ellipsis: characters come off its end until the line and … fit the box. The ellipsis is drawn in the style of the last character kept on that line. When that character's font has no …, the ellipsis comes from the first font in the font-family list that has one, like any other character, keeping the size and color. If no font in the list has …, it is ... in the first font that has .. The standard fonts have both, and a list with registered families always holds one (see the fallback), so in practice the ellipsis is …. In a box too narrow for the ellipsis itself, the line is left empty. With text-align: justify, the cut line is not stretched, like a paragraph's last line.

html
<pdf-cell><pdf-text style="max-lines: 2">{{ product.description }}</pdf-text></pdf-cell>

The box is as tall as the kept lines. max-lines: none, the default, keeps every line. The property is not inherited, so set it on the pdf-text itself.

Cutting text to its height

text-overflow: ellipsis on a pdf-text with a fixed height or max-height keeps the lines that fit fully in its content box (the height less padding and border). When text was cut, the last kept line ends with …, the same way as with max-lines.

html
<pdf-text style="height: 40; text-overflow: ellipsis">{{ product.description }}</pdf-text>
  • With max-lines as well, the smaller number of lines wins.
  • The box keeps its height; only the lines drawn in it change.
  • Without a fixed height or max-height on the pdf-text itself, ellipsis does nothing. The height of a parent with overflow: hidden does not count.
  • When not even the first line fits, it is still drawn and sticks out of the box, ending with … when more text follows it. CSS would clip it; ngpdf keeps a line that can be read. A text of one line has nothing cut, so it gets no ….
  • clip, the default, keeps every line, and lines past the height are drawn past the box unless overflow: hidden clips them. The property is not inherited.
  • A text with a fixed height or max-height is never split across pages, so a cut text stays on one page.

Line height

Value Meaning
1.4 1.4 × the font size. A bare number is always a multiplier, so 14 means 14 × the font size.
140% 1.4 × the font size
14pt, 5mm, 0.2in a fixed line height
normal keeps the inherited line height

Write a length with its unit, such as line-height: 14pt, when you mean points. A length is divided by the element's own font size, and the resulting ratio is inherited.

Each line is font-size × line-height high, using the largest font size on the line, so a block of text in one size is number of lines × font-size × line-height high. As in CSS, the space the line height adds to the text (the leading) is split in half: one half above the text, one half below. With line-height: 3 a 10pt text sits in the middle of a 30pt line. A line height smaller than the font's height gives a negative half, so the text rises and can reach outside its line, as it does in a browser. Underlines and strike-throughs follow the text; a link's clickable area is the whole line.

In a line with runs of several sizes, each run's leading is split around that run, and the run that reaches highest above the baseline sets where the baseline goes. The line stays as tall as its largest font size times line-height. CSS can make such a line a little taller when the runs are in fonts with different proportions above and below the baseline; ngpdf keeps the height, so the lower part of such a run can reach that little bit past the line.

Alignment and decoration

  • text-align: left, center, right or justify. It aligns each line inside the pdf-text box. In a row, a pdf-text is only as wide as its text, so give it a width or flex: 1 when you want alignment to show. Inside a column, the box takes the full width.
  • justify widens the gaps between words so that each line reaches both edges of the box. The last line of the paragraph stays at the left, as in CSS. So does a line with a single word, and a word wider than the box. A gap is a run of spaces or no-break spaces, and only gaps are widened: the space between letters stays as the font and letter-spacing set it. A paragraph split across pages keeps its lines justified up to its real last line.
  • text-decoration: none, underline or line-through. One value only.
  • letter-spacing: a length added after each character, the last one on a line included, as in browsers. It can be negative; normal is 0. Wrapping, alignment, justified text and max-lines all use the spaced width. A letter and its combining marks count as one character. As in CSS, spaced text drops the optional ligatures of a custom font, such as fi. Text extractors such as pdftotext guess spaces from the gaps between letters, so spacing above about a tenth of the font size can come out as N O R T H when the text is copied.
  • text-transform: none, uppercase, lowercase or capitalize. The text is transformed before it is measured, so lines break where the transformed text fits. Case changes don't depend on a locale, so ß becomes SS. capitalize uppercases the first letter of each word, a word being what follows the start of the paragraph or a space, across spans; punctuation before the letter is skipped, and a word that starts with a digit is left as it is. The page number is never transformed.
  • color and opacity apply to the text.

Unlike CSS, all text styles, text-decoration included, are inherited by child elements.

Styled runs: pdf-span

Wrap part of a text in a pdf-span to draw it in other text styles:

html
<pdf-text style="font-size: 11pt">
  Invoice <pdf-span style="font-weight: bold">#{{ number() }}</pdf-span> is
  <pdf-span style="color: #b00020">{{ days() }} days overdue</pdf-span>.
</pdf-text>
  • A span inherits the text styles around it and can change color, font-family, font-size, font-weight, font-style, text-decoration, letter-spacing, text-transform, hyphens and white-space, and move its text up or down with vertical-align (below). Spans nest.
  • The paragraph wraps as one text: a span boundary is not a place to break, and a span wraps over lines like the rest of the text. Kerning is not applied across two spans.
  • Runs of different sizes share a baseline, and a line is as tall as its largest font size times the paragraph's line-height. The baseline goes where the half-leading of the run that reaches highest puts it. A line grows when a raised or lowered run sticks out of it (see Superscript and subscript).
  • line-height and text-align belong to the pdf-text. Other properties on a span, such as padding or a background, have no effect and get a warning in development mode.
  • A span is drawn in the fonts its own font-family names, with the same fallback as any text.
  • @if, @for and components work inside a pdf-text as anywhere else.

A pdf-text inside a pdf-text is drawn in the outer text's style, with a warning in development mode: use pdf-span instead. Elements other than pdf-span, pdf-link and text inside a pdf-text are not laid out.

Superscript and subscript

vertical-align on a pdf-span raises or lowers its text, for units, formulas and footnote markers:

html
<pdf-text>
  42 m<pdf-span style="vertical-align: super">2</pdf-span> of floor, sealed against
  H<pdf-span style="vertical-align: sub">2</pdf-span>O<pdf-span style="vertical-align: super">1</pdf-span>
</pdf-text>
  • super raises the span's baseline by 0.33 times the font size around it, and sub lowers it by 0.2 times. Both draw the span at 70% of the font size around it, unless the span sets its own font-size.
  • A length raises the text by that much, and a negative one lowers it: vertical-align: 3pt. A percentage is of the span's line height, its font size times the paragraph's line-height. Neither changes the size.
  • baseline is the default. Like CSS, vertical-align is not inherited, but a span inside a raised span sits on that span's baseline, so its own vertical-align adds to the move.
  • A moved run keeps the box every run has, its font size times line-height split around it by half-leading, and that box moves with it. The line grows where the moved box sticks out above the line's baseline or below its bottom, as in CSS. In 10pt Helvetica at the default line-height: 1.2, a super span makes its line about 0.7pt taller and a sub span about 1pt.
  • The underline or strike of a moved span moves with its text. So does the clickable area of a link around it, cut to the line so it never reaches into the lines above or below.
  • vertical-align works only on a pdf-span. On any other element it has no effect, with a warning in development mode (ngpdf: vertical-align has no effect on <tag>). middle, top, text-top and the other CSS keywords throw.

pdf-link makes text or a box open a URL when clicked in a PDF viewer:

html
<pdf-text>
  Pay it at
  <pdf-link [href]="payUrl" style="color: #1a56db; text-decoration: underline">example.com/pay</pdf-link>.
</pdf-text>

<pdf-link href="https://example.com/invoices/1024" style="padding: 8; border: 1 solid #1a56db">
  <pdf-text>Open the invoice</pdf-text>
</pdf-link>
  • Inside a pdf-text, a link is inline, like a pdf-span: it takes the same text styles, and each piece of a line it covers is clickable. A link that wraps over two lines is two clickable areas.
  • Anywhere a pdf-view can go, a link is a column pdf-view, and its border box is the clickable area.
  • A link changes nothing visually: it has no default color or underline. Style it with color and text-decoration.
  • Only absolute http:, https: and mailto: URLs are written, in the normalized form the browser's URL gives them (HTTP://Example.COM becomes http://example.com/). Any other scheme (javascript:, file:, data: and so on), a relative URL or one that does not parse writes no link: the content is still drawn, with a warning in development mode naming the URL. Links often come from data, so a bad one does not fail the render.
  • Links do not nest: a pdf-link inside a pdf-link fails the render with ngpdf: pdf-link inside a pdf-link, as nested links are invalid in HTML.
  • A clickable area is an upright rectangle. Inside a transform, it is the smallest rectangle around the transformed area. It is cut at the bottom of the page's content area, but not by an overflow clip: PDF viewers cannot clip a link area, so a link inside a clipped box stays clickable over its whole box.
  • A block link split across sheets is clickable on each sheet over its part. See Pagination.

Give an element an anchor name, and a pdf-link with href="#name" jumps to it, as a table of contents does:

html
<pdf-text>See <pdf-link href="#prices" style="color: #1a56db">the price list</pdf-link>.</pdf-text>

<pdf-table anchor="prices" [columns]="['1fr', 80]">…</pdf-table>
  • pdf-view, pdf-text, pdf-image, pdf-table, pdf-row and pdf-cell take anchor.
  • The link jumps to the sheet where the element's first piece lands, with the top left of that piece at the top left of the window, at the reader's zoom. For a text split across sheets that is its first lines; for an element in a fixed view, the first sheet.
  • Names are trimmed, and an empty name is ignored. When two elements have the same name, the first one in the template keeps it, with a warning in development mode: ngpdf: duplicate anchor "<name>".
  • A link to a name that no element has writes no link: the content is still drawn, with a warning in development mode.
  • In the PDF, each anchor a link uses is a named destination, so a viewer can also open the file at it, as in report.pdf#nameddest=prices. An anchor no link or bookmark uses writes nothing.

Bare text

Text written directly in an element other than pdf-text is laid out as an anonymous pdf-text with its parent's text styles:

html
<pdf-cell style="font-weight: bold">{{ total() }}</pdf-cell>
<!-- is laid out as -->
<pdf-cell style="font-weight: bold"><pdf-text>{{ total() }}</pdf-text></pdf-cell>

Each run of text between elements becomes one anonymous box. Runs that are only whitespace are dropped. Text inside pdf-image and pdf-page-number, and text directly in pdf-document, is ignored.