Skip to content
Lamina PDF
Unreleased

These docs describe changes that aren’t released yet. For the newest release, see the released docs.

Multilingual text

You’ll put five scripts on one page: Latin, Arabic, Devanagari, Hebrew and Chinese, each as a single line and as a paragraph wrapped in a box. Start from it whenever your text goes beyond Latin, or mixes left-to-right and right-to-left writing.

The function takes one font file per script, draws each script’s line and paragraph, and returns the PDF’s bytes. In TypeScript it needs the text module, init({ text: true }). Both languages write the same bytes:

packages/sdk/examples/multilingual.ts
// Five scripts on one page, each as a single line and as a wrapped paragraph. The scripts are
// Latin, Arabic and Hebrew (right to left), Devanagari and simplified Chinese; each is shaped and
// bidi-resolved, and each paragraph wraps in a text box. Each script needs a font that covers it,
// such as the Noto fonts (OFL).
import { Builder, type BuilderOptions, type Direction, Font } from "@laminapdf/sdk";
/** Each script's line, its paragraph and the paragraph's direction, in the order of the fonts. */
export const SCRIPTS: readonly (readonly [line: string, paragraph: string, direction: Direction])[] = [
[
"Latin: the office staff fill the affine waffle flask",
"A paragraph breaks at spaces and fits the width of its box, line by line.",
"ltr",
],
["مرحبا بالعالم السلام عليكم", "مرحبا بالعالم السلام عليكم مرحبا بالعالم السلام عليكم", "rtl"],
["नमस्ते दुनिया। हिन्दी पाठ का परीक्षण", "नमस्ते दुनिया। हिन्दी पाठ का परीक्षण नमस्ते दुनिया।", "ltr"],
["שלום עולם וברוכים הבאים", "שלום עולם וברוכים הבאים שלום עולם וברוכים הבאים", "rtl"],
["你好世界,这是中文字体子集测试。", "你好世界,这是中文字体子集测试。你好世界,这是中文。", "ltr"],
];
const MARGIN = 56;
const FIRST_BASELINE = 80;
/** The distance from one script's line to the next one's. */
const SCRIPT_HEIGHT = 140;
/** The paragraph's box: this far below the line, this wide and this tall. */
const PARAGRAPH_DROP = 14;
const PARAGRAPH_WIDTH = 220;
const PARAGRAPH_HEIGHT = 100;
/**
* The document's bytes. Needs `init({ text: true })`.
*
* @param fonts - one font file per script, in the order of {@link SCRIPTS}.
* @param options - can fix the file ID, the producer and the creation date.
*/
export function multilingual(fonts: readonly Uint8Array[], options: BuilderOptions = {}): Uint8Array<ArrayBuffer> {
if (fonts.length !== SCRIPTS.length) throw new Error(`${SCRIPTS.length} fonts, one per script, are needed`);
using b = new Builder(options);
const loaded = fonts.map((bytes) => Font.fromBytes(bytes));
for (const font of loaded) b.font(font);
const page = b.page("A4");
const s = page.surface();
SCRIPTS.forEach(([line, paragraph, direction], row) => {
const font = loaded[row];
if (font === undefined) return;
const y = FIRST_BASELINE + SCRIPT_HEIGHT * row;
s.text(line, { font, size: 16, at: [MARGIN, y] });
// A narrow box breaks the paragraph into lines; right-to-left paragraphs start at the right.
const box = [MARGIN, y + PARAGRAPH_DROP, PARAGRAPH_WIDTH, PARAGRAPH_HEIGHT] as const;
s.textBox(paragraph, { font, size: 12, box, direction, align: "start" });
});
page.finish();
using doc = b.finish();
return doc.save();
}

The result is one A4 page, with each script set in its own Noto font: open the reference PDF.

  • Font.fromBytes (font_bytes in Rust) loads each font, which must cover its script. The builder embeds only the glyphs the page draws.
  • Lamina PDF shapes the text with each font, so Arabic letters join, Devanagari letters combine into clusters and the Latin line forms ligatures.
  • textBox breaks each paragraph into lines that fit its 220-point-wide box. It breaks where Unicode’s line-breaking rules allow: between words, and between Chinese characters.
  • direction: "rtl" (Direction::RightToLeft) makes the Arabic and Hebrew paragraphs right to left, so align: "start" starts their lines at the box’s right edge.
  • Each font is embedded with its ToUnicode mapping, so text copied or extracted from the PDF equals the strings you passed in, ligatures included.
  • Your own scripts: change SCRIPTS and pass one font per row, in the same order. A character the font lacks fails with missing-glyph.
  • The paragraph’s box: PARAGRAPH_WIDTH and PARAGRAPH_HEIGHT set its size. Lines that don’t fit are left out, unless you pass overflow: "error" (Overflow::Error) to fail with text-overflow instead.
  • Alignment and spacing: align also takes "end", "left", "right" or "center", and lineHeight (with_line_height) sets the distance between baselines, 1.2 times the size by default.
  • Ligatures and other OpenType features: features: ["liga=0"] on text or textBox turns the ligatures off. In Rust, call features on the run or with_feature on the text box’s options. See Text and fonts.

Next: images and vectors, with shapes, colour, transparency and two images.