Skip to content

Text And Fonts

Wasm Recipe bundles Apache-2.0 Roboto Regular as its zero-setup default, so text, text measurement, and tables need no font upload or registration. text() and textDimensions() use 14 points when neither size nor fontSize is supplied. The two option names are aliases; { fontSize: 12 } and { size: 12 } both select 12 points. A size that is not a finite number greater than zero, including zero, a negative number, NaN, and Infinity, throws a RangeError naming the option and the value, as it does on native, because such a size draws nothing readable and measures to nonsensical metrics. Pass null, undefined, or neither option to select the default. Character-spacing measurements include retained leading and trailing whitespace and count each Unicode code point once, so non-BMP characters do not add an extra spacing interval.

import { createRecipe } from "@muhammara/wasm";

var Recipe = await createRecipe();
var recipe = new Recipe().createPage("letter");
var heading = recipe.textDimensions("Browser report", { size: 26 });

var pdfBytes = recipe
  .text("Browser report", (612 - heading.width) / 2, 60, {
    size: 26,
    color: "#0f3d5e",
  })
  .text(
    "Recipe wraps this paragraph within a fixed width and keeps all work in memory.",
    72,
    112,
    {
      size: 12,
      textBox: {
        width: 468,
        padding: 12,
        style: { fill: "#edf6ff", stroke: "#9cc5e3" },
      },
    },
  )
  .endPage()
  .endPDF();

Roboto is dynamically imported by createRecipe() and registered on the first text or measurement use in each loaded runtime. The complete regular face adds about 145 KB of font data before encoding and compression. Preserve dynamic import splitting in your bundler to keep that data out of the main JavaScript bundle. The low-level createMuhammaraWasm() API never imports it, so low-level writers require explicit font registration before drawing text.

Only Roboto's regular face is bundled. Bold and italic requests fall back to regular until matching faces are registered, and Recipe does not synthesize styles. Register fonts that cover glyphs outside Roboto; font registration does not add complex-script shaping or right-to-left layout. Native Recipe defaults to Helvetica and bundles more faces, so select the same face on both platforms when matching metrics, wrapping, or layout matters.

Custom Fonts

Use registerFont() for Uint8Array or ArrayBuffer, and registerFontAsync() for Blob or File. Register styles under one family name, then select them through text options.

import { createRecipe } from "@muhammara/wasm";

var regularFontFile = await (await fetch("/fonts/Report-Regular.ttf")).blob();
var boldFontFile = await (await fetch("/fonts/Report-Bold.ttf")).blob();
var Recipe = await createRecipe({ defaultFont: false });
await Recipe.registerFontAsync("report", regularFontFile);
await Recipe.registerFontAsync("report", boldFontFile, "bold");

var pdfBytes = new Recipe()
  .createPage("letter")
  .text("Custom heading", 72, 72, {
    font: "report",
    bold: true,
    size: 24,
  })
  .text("Custom body", 72, 112, { font: "report", size: 12 })
  .endPage()
  .endPDF();

Passing defaultFont: false avoids downloading Roboto and requires every text call to name a registered font; omitting font throws Unknown font: (none). Font family names are case-insensitive, and other unknown explicit names throw Unknown font: <name>. A registered Roboto family overrides the bundled default.

Finish and dispose active documents before unregistering fonts or calling Recipe.disposeAssets(). After bundled Roboto is removed, the next default-font text or measurement call restores it automatically.

Custom Default Font

Pass your own Uint8Array, ArrayBuffer, Blob, or File as defaultFont to skip importing Roboto and install that face as the default family:

import { createRecipe } from "@muhammara/wasm";

var fontFile = await (await fetch("/fonts/Report-Regular.ttf")).blob();
var Recipe = await createRecipe({ defaultFont: fontFile });
var pdfBytes = new Recipe()
  .createPage("letter")
  .text("Uses my default font", 72, 72)
  .endPage()
  .endPDF();

Initialization copies the supplied bytes and enforces limits.maxInputBytes. Additional styles can be registered under the default family. The configured bytes remain available to restore that family after Recipe.disposeAssets(). These choices happen in createRecipe() because text drawing and measurement are synchronous; registering a font later cannot undo the initialization-time font download. See Byte Assets for registration and lifecycle details.

Wrapping And HTML

textBox supports wrapping, alignment, padding, background, border, fixed-height clipping, and continuation callbacks. Its clip, trim, and ellipsis modes have different output semantics; ellipsis ends the line with …, as native does. html: true enables a DOM-free subset for text runs, paragraphs, simple emphasis, decoration, inline color, URL links through <a href>, and visual unordered and ordered lists through ul, ol, and li. Lists use * or one-based numeric prefixes and native-compatible nesting indentation. Inline formatting and links remain active inside each item. This is not browser HTML/CSS layout or semantic tagged-PDF output; arbitrary DOM, general CSS inheritance, and plugin handlers are unavailable.

recipe.text(
  "<ul><li>First</li><li><b>Important</b><ol><li>Nested</li></ol></li></ul>",
  36,
  72,
  { html: true, textBox: { width: 240 } },
);
import { createRecipe } from "@muhammara/wasm";

var Recipe = await createRecipe();
var remainder = "";
var pdfBytes = new Recipe()
  .createPage(300, 180)
  .text("First line fits. The remaining words are reported.", 24, 24, {
    size: 12,
    textBox: {
      width: 180,
      height: 18,
      clipIfExceedsBox: true,
      onClip: function (_recipe, result) {
        remainder = result.remainder;
      },
    },
  })
  .endPage()
  .endPDF();

console.log(remainder, pdfBytes.byteLength);

Continue Text Across Calls

Pass flow: true to build one text box from several text() calls, for example to style a single word in a sentence. The first call sets the position; later calls without coordinates continue the line where the previous run ended and wrap together inside the shared text box. A call without coordinates flows unless it passes flow: false: it continues the open flow and inherits the options of the runs before it, or starts a flow at the text cursor.

Flowed text is laid out when the flow ends, so textBox alignment, justification, and styling apply to the whole box. End it with flow: false, which adds that call's text first; text("", { flow: false }) ends it without adding any. Inside a flow, movedown() ends the current line. Because the flow is drawn when it ends, shapes or images drawn between its calls end up beneath its text, and movedown(lines, true) reports the flow's starting position until then.

var pdfBytes = new Recipe()
  .createPage("letter")
  .text("Only ", 72, 72, { flow: true, textBox: { width: 300 } })
  .text("this", { color: "#c62828", hilite: true })
  .text(" word is marked.", { color: "#000000", hilite: false })
  .text("", { flow: false })
  .endPage()
  .endPDF();

A flow that is not ended explicitly is drawn by the next text() call with coordinates, table(), or endPage().