A 100-page report
You’ll make a 100-page report with two embedded fonts: on every page, a heading, a paragraph, a line of Chinese and a table of 20 products. Start from it for any long document you generate from data, such as monthly statements or a catalogue.
The function draws one region’s page at a time in a loop, then returns the PDF’s bytes:
// A 100-page report with two embedded fonts. Each page has a heading, a paragraph broken into a// box, a line of Chinese, a table of products with rules between the rows, and a page number.import { Builder, type BuilderOptions, Font, PAGE_SIZES, type Rect, type Surface } from "@laminapdf/sdk";
/** The number of pages. */const REPORT_PAGES = 100;const PRODUCTS = 20;const PRICE = 12.5;const [PAGE_WIDTH] = PAGE_SIZES.A4;const MARGIN = 56;const RIGHT_EDGE = PAGE_WIDTH - MARGIN;const HEADING_AT = [MARGIN, 72] as const;const PARAGRAPH_BOX = [MARGIN, 88, RIGHT_EDGE - MARGIN, 60] as const;const CHINESE_AT = [MARGIN, 166] as const;const FOOTER_AT = [MARGIN, 800] as const;const FIRST_ROW = 200;const ROW_HEIGHT = 24;/** The units' and the revenue's columns (left edge, width), both right-aligned. */const UNITS_COLUMN = [300, 60] as const;const REVENUE_COLUMN = [400, RIGHT_EDGE - 400] as const;/** A cell's box starts this far above its row's baseline and is this tall; the rule is this far below. */const CELL_RISE = 11;const CELL_HEIGHT = 14;const RULE_DROP = 7;/** Font sizes: the heading, the paragraph and the Chinese line, the table, the page number. */const SIZES = { heading: 20, body: 11, table: 10, footer: 9 } as const;const PARAGRAPH = "Quarterly figures for every region follow. Each table lists the units shipped, the revenue " + "and the change against the previous quarter; totals are at the end of the report.";
/** * The document's bytes. Needs `init({ text: true })`. * * @param latinFont - a font for the text, such as Noto Sans. * @param chineseFont - a font with Chinese glyphs, such as Noto Sans SC. * @param options - can fix the file ID, the producer and the creation date. */export function report( latinFont: Uint8Array, chineseFont: Uint8Array, options: BuilderOptions = {},): Uint8Array<ArrayBuffer> { using b = new Builder(options); const font = Font.fromBytes(latinFont); const chinese = Font.fromBytes(chineseFont); b.font(font); b.font(chinese); for (let region = 1; region <= REPORT_PAGES; region++) { const page = b.page("A4"); const s = page.surface(); s.text(`Region ${region}: quarterly report`, { font, size: SIZES.heading, at: HEADING_AT }); s.textBox(PARAGRAPH, { font, size: SIZES.body, box: PARAGRAPH_BOX }); s.text("你好世界,这是中文字体子集测试。", { font: chinese, size: SIZES.body, at: CHINESE_AT }); products(s, font, region); s.text(`Page ${region} of ${REPORT_PAGES}`, { font, size: SIZES.footer, at: FOOTER_AT }); page.finish(); } b.metadata({ title: "Quarterly report", author: "lamina examples" }).lang("en-GB"); using doc = b.finish(); return doc.save();}
/** Made-up sales figures: the units of `product` that `region` shipped, from 0 to 499. */function unitsShipped(region: number, product: number): number { return (region * 37 + product * 11) % 500;}
/** A region's table: one row per product, with its units and revenue right-aligned. */function products(s: Surface, font: Font, region: number): void { for (let product = 0; product < PRODUCTS; product++) { const y = FIRST_ROW + ROW_HEIGHT * product; const units = unitsShipped(region, product); const size = SIZES.table; s.text(`Product ${product + 1}`, { font, size, at: [MARGIN, y] }); s.textBox(String(units), { font, size, box: cell(UNITS_COLUMN, y), align: "end" }); s.textBox(`€ ${(units * PRICE).toFixed(2)}`, { font, size, box: cell(REVENUE_COLUMN, y), align: "end" }); s.strokeColor([0.8]) .path((p) => p.moveTo(MARGIN, y + RULE_DROP).lineTo(RIGHT_EDGE, y + RULE_DROP)) .stroke({ width: 0.5 }); }}
/** The box of a cell in `column` (left edge, width) on the row at `baseline`. */function cell([left, width]: readonly [number, number], baseline: number): Rect { return [left, baseline - CELL_RISE, width, CELL_HEIGHT];}The result is 100 A4 pages, one per region. It has no reference PDF to open. Instead, Lamina PDF’s tests build and time the report in every JavaScript runtime and check its SHA-256 there, as does the Rust benchmark that builds the same bytes.
How it works
Section titled “How it works”- It needs the text module,
init({ text: true }), because it draws with embedded fonts. Font.fromBytesreads each font file, andb.fontregisters both before the first page. Each font is embedded once for the whole report, with only the glyphs its 100 pages draw.s.textdraws one line, withat[1]as its baseline, ands.textBoxbreaks the paragraph into lines that fitPARAGRAPH_BOX.- In the table, the units and the revenue are text boxes as wide as their columns, with
align: "end"to right-align them.cellworks out each box from its column and the row’s baseline. strokeColor([0.8])sets a light gray, and each rule is a path from the left margin to the right edge, stroked 0.5 points wide.metadataandlang("en-GB")set the title, author and language once for the document, andusingfrees the builder and the document when the function returns.
What to change
Section titled “What to change”- Your data: replace
unitsShippedwith your own figures, and setREPORT_PAGESandPRODUCTSto your counts. - A longer table: 20 rows end at 656 points, above the page number at 800. For more rows, lower
ROW_HEIGHTor carry the rest over to the next page. - Another script: load a font that covers it and draw its text in that font, as the Chinese line does. See Text and fonts.
- The file’s details: pass
producerandcreationDateinoptions, such ascreationDate: "D:20261104120000Z". The builder never reads the clock, so a report without a date has none.
Next: open someone else’s file, with a password and tighter limits.