showroom · real code · real pdfs
Code in. Document out.
Every example is real @jasy/pdf code on the left and the exact PDF it renders on the right. Scroll it, page through it, download it - nothing is pre-baked.
invoice.ts
Invoice
A two-page commercial invoice - line-item table, totals, and a footer that paginate cleanly.
// A clean, designed invoice that flows across two pages - the line-item table breaks, its header
// repeats on page two, and the page footer repeats on both. Pure layout, standard-14 fonts.
import { Document, Page, Column, Row, Box, Padding, Text, Divider, Table } from "@jasy/pdf";
const ink = "#1b2433";
const muted = "#6b7280";
const brand = "#1450aa";
const hair = "#e6eaf2";
const paper = "#f5f8fd";
const eur = (n: number) =>
n.toLocaleString("de-DE", { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + " €";
type Item = { qty: number; name: string; price: number };
const items: Item[] = [
{ qty: 2, name: "Brand identity workshop", price: 680 },
{ qty: 1, name: "Logo design, primary and variants", price: 1450 },
{ qty: 1, name: "Visual identity guidelines", price: 920 },
{ qty: 3, name: "UI design, key screens", price: 540 },
{ qty: 1, name: "Design system in Figma", price: 1680 },
{ qty: 2, name: "Interactive prototype", price: 460 },
{ qty: 1, name: "Frontend setup (Nuxt)", price: 780 },
{ qty: 6, name: "Component implementation", price: 220 },
{ qty: 1, name: "Responsive pass", price: 640 },
{ qty: 1, name: "Accessibility audit", price: 540 },
{ qty: 2, name: "Content modelling", price: 380 },
{ qty: 1, name: "CMS integration", price: 1120 },
{ qty: 3, name: "Page templates", price: 360 },
{ qty: 1, name: "Animation polish", price: 480 },
{ qty: 1, name: "Performance tuning", price: 560 },
{ qty: 2, name: "QA and bugfixing", price: 340 },
{ qty: 1, name: "Deployment and CI", price: 420 },
{ qty: 1, name: "Documentation", price: 380 },
{ qty: 2, name: "Stakeholder review", price: 260 },
{ qty: 1, name: "Project management", price: 1200 },
];
const net = items.reduce((s, it) => s + it.qty * it.price, 0);
const vat = net * 0.19;
const gross = net + vat;
const hcell = (t: string, align: "left" | "right" = "left") =>
Text(t, { size: 8.5, bold: true, color: muted, align });
const cell = (t: string, align: "left" | "right" = "left", bold = false) =>
Text(t, { size: 10.5, color: ink, align, bold });
const rows = items.map((it) => [
cell(String(it.qty)),
cell(it.name),
cell(eur(it.price), "right"),
cell(eur(it.qty * it.price), "right", true),
]);
const detail = (label: string, value: string) =>
Row({ justify: "between", gap: 18 }, [
Text(label, { size: 10, color: muted }),
Text(value, { size: 10, color: ink, bold: true, align: "right" }),
]);
const totalRow = (label: string, value: string, o: { bold?: boolean; size?: number; color?: string } = {}) =>
Row({ justify: "between" }, [
Text(label, { size: o.size ?? 10.5, color: o.color ?? muted, bold: o.bold }),
Text(value, { size: o.size ?? 10.5, color: o.color ?? ink, bold: o.bold, align: "right" }),
]);
const footer = Column({ gap: 6 }, [
Divider({ color: hair }),
Row({ justify: "between" }, [
Text("Muster Studio GmbH · Hauptstraße 1 · 10115 Berlin", { size: 8, color: muted }),
Text("VAT DE123456789 · hello@muster.studio", { size: 8, color: muted, align: "right" }),
]),
]);
const page = Page({ gap: 0, margin: 48, footer }, [
// header band
Row({ justify: "between", align: "start" }, [
Column({ gap: 1 }, [
Text("Muster Studio", { size: 19, bold: true, color: brand }),
Text("Design & Development", { size: 10, color: muted }),
]),
Column({ gap: 1, align: "end" }, [
Text("INVOICE", { size: 26, bold: true, color: ink }),
Text("RE-2026-0142", { size: 11, color: muted }),
]),
]),
// billed-to + meta card
Padding(
{ top: 22 },
Row({ justify: "between", align: "start", gap: 40 }, [
Column({ gap: 2 }, [
Text("BILLED TO", { size: 8, bold: true, color: brand }),
Padding({ top: 2 }, Text("Beispiel Kunde AG", { size: 12, bold: true, color: ink })),
Text("Marienplatz 1", { size: 10.5, color: ink }),
Text("80331 München", { size: 10.5, color: ink }),
Text("Germany", { size: 10.5, color: muted }),
]),
Box({ width: 230, bg: paper, padding: { x: 16, y: 14 }, radius: 8 }, [
Column({ gap: 7 }, [
detail("Invoice no.", "RE-2026-0142"),
detail("Issue date", "20 Jun 2026"),
detail("Due date", "04 Jul 2026"),
detail("Reference", "PO-99213"),
]),
]),
]),
),
// line items
Padding(
{ top: 26 },
Table(
{
columns: ["auto", "1fr", 92, 92],
header: [hcell("QTY"), hcell("DESCRIPTION"), hcell("UNIT", "right"), hcell("AMOUNT", "right")],
rowGap: 9,
colGap: 14,
cellPadding: { y: 4 },
rule: hair,
},
rows,
),
),
// totals
Padding(
{ top: 18 },
Row({ justify: "end" }, [
Box({ width: 250 }, [
Column({ gap: 8 }, [
totalRow("Subtotal", eur(net)),
totalRow("VAT 19%", eur(vat)),
Divider({ color: hair, margin: { y: 2 } }),
totalRow("Total due", eur(gross), { bold: true, size: 13, color: brand }),
]),
]),
]),
),
// payment note
Padding(
{ top: 22 },
Box({ border: hair, bg: paper, padding: { x: 14, y: 12 }, radius: 8 }, [
Column({ gap: 3 }, [
Text("Payment", { size: 9, bold: true, color: brand }),
Text(
"Please transfer the total to IBAN DE02 1203 0000 0000 2020 51 within 14 days, quoting the invoice number.",
{ size: 10, color: ink },
),
]),
]),
),
]);
export default Document([page]);
zugferd-invoice.ts
ZUGFeRD e-invoice
A conformant ZUGFeRD PDF/A-3 with EN-16931 XML embedded - for humans and tax offices both.
// A ZUGFeRD / Factur-X e-invoice: a typed EN-16931 Invoice in -> a conformant PDF/A-3 with the
// CII XML embedded out. Totals + the VAT breakdown are computed by the generator, so the business
// rules that check the arithmetic hold by construction. Rendered via @jasy/zugferd (not renderToBytes);
// the harness writes both the .pdf and the .xml.
import type { Invoice } from "@jasy/zugferd";
const invoice: Invoice = {
number: "RE-2026-0142",
issueDate: "2026-06-20",
dueDate: "2026-07-04",
currency: "EUR",
buyerReference: "PO-99213",
notes: ["Thank you for your business. Conformant ZUGFeRD invoice generated with @jasy/zugferd."],
seller: {
name: "Muster Studio GmbH",
vatId: "DE123456789",
legalRegistrationId: "HRB 12345 B",
electronicAddress: "rechnung@muster-studio.de",
address: { line1: "Hauptstraße 1", postCode: "10115", city: "Berlin", country: "DE" },
contact: { name: "Erika Muster", phone: "+49 30 1234567", email: "kontakt@muster-studio.de" },
},
buyer: {
name: "Beispiel Kunde AG",
vatId: "DE987654321",
address: { line1: "Marienplatz 1", postCode: "80331", city: "München", country: "DE" },
},
lines: [
{ id: "1", name: "Brand identity workshop", quantity: 2, unit: "HUR", netUnitPrice: 680, vat: { category: "S", ratePercent: 19 } },
{ id: "2", name: "Logo design, primary and variants", quantity: 1, unit: "C62", netUnitPrice: 1450, vat: { category: "S", ratePercent: 19 } },
{ id: "3", name: "Design system in Figma", quantity: 1, unit: "C62", netUnitPrice: 1680, vat: { category: "S", ratePercent: 19 } },
{ id: "4", name: "Frontend implementation", quantity: 8, unit: "HUR", netUnitPrice: 95, vat: { category: "S", ratePercent: 19 } },
{ id: "5", name: "Deployment and CI setup", quantity: 1, unit: "C62", netUnitPrice: 420, vat: { category: "S", ratePercent: 19 } },
],
payment: {
meansCode: "58", // SEPA credit transfer
iban: "DE02120300000000202051",
bic: "BYLADEM1001",
accountName: "Muster Studio GmbH",
terms: "Zahlbar innerhalb 14 Tagen netto.",
},
};
export const zugferd = { invoice, options: { profile: "en16931" as const } };
certificate.ts
Certificate
An A4-landscape certificate - the recipient name set in an embedded TrueType script font.
// A certificate - the showcase for custom TrueType embedding. The recipient name is set in Great
// Vibes (an OFL calligraphy font, subsetted + embedded as Type0/Identity-H); everything else uses the
// standard fonts. doc.addFont(name, ttfPath) registers it; Text({ font: name }) uses it.
// The gold frame is an out-of-flow Positioned border filling the page content box (so it keeps an
// equal margin on all four sides), and the content is centered inside it.
import { Document, Page, Column, Row, Box, Padding, Positioned, Text } from "@jasy/pdf";
const ink = "#1b2433";
const muted = "#6b7280";
const brand = "#1450aa";
const gold = "#a8842c";
const rule = (w: number) => Box({ width: w, height: 1.4, bg: gold }, []);
const spaced = (s: string) => s.split("").join(String.fromCharCode(32)); // letter-spaced title
const doc = Document([
Page({ size: "A4", orientation: "landscape", margin: 46, justify: "center", align: "center" }, [
// the frame: a border filling the content box, out of flow so it never shifts the content
Positioned({ top: 0, left: 0, right: 0, bottom: 0 }, Box({ border: gold, borderWidth: 1.5, radius: 6 }, [])),
// the content, centered in the frame
Padding(
{ x: 56 },
Column({ align: "center", gap: 0 }, [
Text(spaced("CERTIFICATE"), { font: "Times-Roman", bold: true, size: 26, color: ink, align: "center" }),
Box({ height: 5 }, []),
Text(spaced("OF ACHIEVEMENT"), { font: "Times-Roman", size: 11, color: gold, align: "center" }),
Box({ height: 34 }, []),
Text("This certificate is proudly presented to", { font: "Times-Roman", size: 13, color: muted, align: "center" }),
Box({ height: 8 }, []),
Text("Florian Heuberger", { font: "Great Vibes", size: 60, color: brand, align: "center" }),
Box({ height: 8 }, []),
rule(160),
Box({ height: 26 }, []),
Text("in recognition of building jasy, a declarative PDF engine in pure TypeScript,", { font: "Times-Roman", size: 12, color: ink, align: "center" }),
Text("ZUGFeRD-conformant, with no headless browser and no Java underneath.", { font: "Times-Roman", size: 12, color: ink, align: "center" }),
Box({ height: 46 }, []),
Row({ justify: "between", align: "end" }, [
Column({ align: "center", gap: 3 }, [rule(160), Text("Erika Muster · Founder", { font: "Times-Roman", size: 9, color: muted })]),
Box({ width: 18, height: 18, radius: 9, border: gold, borderWidth: 1.5 }, []),
Column({ align: "center", gap: 3 }, [rule(160), Text("Berlin · 24 June 2026", { font: "Times-Roman", size: 9, color: muted })]),
]),
]),
),
]),
]);
doc.addFont("Great Vibes", "examples/assets/fonts/GreatVibes-Regular.ttf");
export default doc;
cover.ts
Cover page
A full-bleed cover - colour to the edge, out-of-flow positioned shapes, and overlaid display type.
// A full-bleed cover page: the background runs edge to edge (no page margin), a bold accent disc pokes
// off the corner and is cropped by overflow:hidden, and the text sits on top - all driven by the
// positioning layer (a relative Box frame + out-of-flow Positioned children).
import { Document, Page, Box, Expanded, Positioned, Column, Text, Paragraph, Divider } from "@jasy/pdf";
const navy = "#0a2348";
const navy2 = "#13315f";
const accent = "#f3dc29";
const white = "#ffffff";
const soft = "#9db8e0";
const LEAD =
"You describe a document as a tree of components and jasy lays it out and writes the PDF bytes " +
"itself, with no headless browser and no Java. Text breaks at real font metrics, images move as a " +
"whole across a page break, and the background you see here bleeds past the page margin because a " +
"Positioned child is allowed to poke right out to the paper's edge.";
export default Document([
Page({ margin: 0 }, [
Expanded(
Box({ bg: navy, relative: true, overflow: "hidden" }, [
// subtle disc for depth (bottom-left), then the bold accent disc cropped at the top-right corner
Positioned({ bottom: -170, left: -130 }, Box({ width: 380, height: 380, radius: 190, bg: navy2 }, [])),
Positioned({ top: -130, right: -120 }, Box({ width: 300, height: 300, radius: 150, bg: accent }, [])),
// content, inset from the edges
Positioned(
{ top: 220, left: 80, right: 80 },
Column({ gap: 16 }, [
Text("PURE TYPESCRIPT · ZERO DEPENDENCIES", { size: 11, bold: true, color: accent }),
Text("Documents that flow, by design.", { size: 40, bold: true, color: white }),
Divider({ color: navy2 }),
Paragraph(LEAD, { size: 13, color: soft, font: "Times-Roman" }),
]),
),
// a corner mark, anchored to the frame
Positioned({ bottom: 48, right: 56 }, Text("jasy · 01", { size: 11, bold: true, color: accent })),
]),
),
]),
]);
datasheet.ts
Datasheet
A product datasheet - stat cards, a spec table, and a bar chart drawn from primitives.
// A datasheet page built from the drawing primitives: stat cards, a bar chart drawn as rectangles on
// a baseline, and a spec strip. Shows boxes, fills, rules and color driving a data-dense layout.
import { Document, Page, Column, Row, Box, Padding, Text, Divider } from "@jasy/pdf";
const ink = "#1b2433";
const muted = "#6b7280";
const brand = "#1450aa";
const accent = "#e3b505";
const hair = "#e6eaf2";
const paper = "#f5f8fd";
const stat = (value: string, label: string, sub: string) =>
Box({ bg: paper, padding: { x: 16, y: 14 }, radius: 8 }, [
Column({ gap: 3 }, [
Text(value, { size: 25, bold: true, color: brand, lineHeight: 1.3 }),
Text(label, { size: 10.5, bold: true, color: ink, lineHeight: 1.3 }),
Text(sub, { size: 9, color: muted, lineHeight: 1.3 }),
]),
]);
// bar chart data: [label, value]; the peak gets the accent fill
const data: [string, number][] = [
["W1", 62],
["W2", 95],
["W3", 78],
["W4", 128],
["W5", 112],
["W6", 168],
["W7", 141],
["W8", 190],
];
const MAXV = 200;
const CHART_H = 168;
const BAR_W = 34;
const peak = Math.max(...data.map(([, v]) => v));
const bars = Row(
{ justify: "between", align: "end" },
data.map(([, v]) =>
Box({ width: BAR_W, height: (CHART_H * v) / MAXV, bg: v === peak ? accent : brand, radius: 3 }, []),
),
);
const labels = Row(
{ justify: "between" },
data.map(([lbl]) => Box({ width: BAR_W }, [Text(lbl, { size: 9, color: muted, align: "center" })])),
);
const spec = (k: string, v: string) =>
Row({ justify: "between", gap: 12 }, [
Text(k, { size: 9.5, color: muted, lineHeight: 1.3 }),
Text(v, { size: 9.5, color: ink, bold: true, align: "right", lineHeight: 1.3 }),
]);
const page = Page({ margin: 56, gap: 0 }, [
// header
Text("DATASHEET", { size: 10, bold: true, color: accent }),
Padding({ top: 3 }, Text("Render performance", { size: 26, bold: true, color: ink, lineHeight: 1.3 })),
Text("Pure-TypeScript PDF engine, measured on a laptop, single thread", {
size: 11.5,
color: muted,
lineHeight: 1.3,
}),
// stat cards
Padding(
{ top: 22 },
Row({ gap: 14, align: "stretch" }, [
stat("2.4s", "1,000 invoices", "rendered end to end"),
stat("0", "native deps", "no browser, no JVM"),
stat("~97%", "font subset", "embedded TrueType"),
stat("13/13", "PDF/A-3 checks", "ZUGFeRD conformant"),
]),
),
// bar chart
Padding({ top: 30 }, Text("Throughput, pages per second over eight builds", { size: 11, bold: true, color: ink })),
Padding(
{ top: 12 },
Column({ gap: 8 }, [bars, Divider({ color: ink }), labels]),
),
// spec strip
Padding(
{ top: 30 },
Box({ border: hair, padding: { x: 16, y: 14 }, radius: 8 }, [
Row({ gap: 40, align: "start" }, [
Box({ width: 200 }, [
Column({ gap: 8 }, [
spec("Page sizes", "A6 to A3, Letter, custom"),
spec("Fonts", "standard-14 + TrueType"),
spec("Output", "PDF 1.7, PDF/A-3"),
]),
]),
Box({ width: 200 }, [
Column({ gap: 8 }, [
spec("Images", "JPEG, PNG, BoxFit"),
spec("Pagination", "text, tables, images"),
spec("E-invoice", "EN 16931 CII + UBL"),
]),
]),
]),
]),
),
]);
export default Document([page]);
article.ts
Article
Flowing body copy - headings, paragraphs and inherited line-height breaking across two pages.
// A written article that flows across pages: a display title, serif body, section headings and a
// pull-quote, with a footer that repeats on every page. The point is typographic text flow - the body
// breaks at real font metrics and paginates on its own.
import { Document, Page, Column, Row, Box, Padding, Text, Paragraph, Divider } from "@jasy/pdf";
const ink = "#1b2433";
const muted = "#6b7280";
const brand = "#1450aa";
const gold = "#9c720c";
const hair = "#e6eaf2";
const LEAD =
"Every tool that makes a PDF has to answer the same question sooner or later: what happens when the " +
"content does not fit on one page. Most answer it badly. They draw until they run out of room, then " +
"start a fresh page wherever the cursor happened to land, splitting a heading from its first " +
"paragraph or slicing a table row clean in half. A document engine earns its name by answering that " +
"question well.";
const P1 =
"Laying a single page out is the easy part. You measure each element, stack them, and stop. The " +
"trouble starts at the page boundary. A paragraph has to break between lines, never between a line " +
"and its own descenders. An image has to move as a whole rather than tear across the seam. A table " +
"has to carry its header onto the next page so the reader does not lose the column meanings. Each of " +
"these is simple in isolation and unforgiving in combination, and together they are the fifteen " +
"percent of the work that takes eighty-five percent of the time.";
const P2 =
"The naive approach mutates positions as it goes, nudging elements up and down until they happen to " +
"fit. It works until two elements disagree about who moves, and then it produces the subtle, " +
"maddening bugs that only surface on the third page of a long report. The durable approach computes " +
"the fragment of content that fits, hands back the remainder, and repeats. Constraints flow down " +
"once, sizes flow up once, and nothing is nudged after the fact.";
const P3 =
"Wrapping text correctly is impossible without knowing how wide each glyph is. Browsers know because " +
"they ship a font stack and a shaping engine. A library that wants to avoid a headless browser has " +
"to bring its own metrics. jasy parses the Adobe font-metric files for the standard fonts and reads " +
"the same tables straight out of any TrueType file you embed, so the width of a line is computed " +
"from the true advance of every character rather than estimated from an average.";
const P4 =
"This is also why the same text wraps identically whether it is being measured, drawn, or split " +
"across a page. One line breaker feeds all three. When the measure and the draw disagree by even a " +
"fraction of a point, text that was sized to fit suddenly does not, and a word drops to a line of " +
"its own for no visible reason. Keeping a single source of truth is less a nicety than a requirement.";
const P5 =
"The payoff for treating flow as a first-class idea, rather than a patch bolted onto a single-page " +
"layout, is that complex documents stop being special cases. An invoice whose line items run long " +
"simply continues onto a second page with its header intact. A report with a chapter of dense prose " +
"paginates without anyone thinking about it. The author describes the shape of the document, and the " +
"engine decides where the paper ends.";
const P6 =
"None of this requires a browser, a virtual machine, or a network call. It is a tree of components, " +
"a set of real font metrics, and a fragmentation pass that knows how to say: this much fits here, " +
"the rest goes next. That is the whole trick, and getting it right is the difference between a PDF " +
"you fight with and one you forget about.";
const heading = (t: string) =>
Padding({ top: 20, bottom: 2 }, Text(t, { size: 15, bold: true, color: ink }));
const body = (t: string) =>
Paragraph(t, { font: "Times-Roman", size: 11.5, color: ink, lineHeight: 1.45 });
const footer = Column({ gap: 6 }, [
Divider({ color: hair }),
Row({ justify: "between" }, [
Text("jasy", { size: 8.5, bold: true, color: brand }),
Text("On document engineering", { size: 8.5, color: muted }),
]),
]);
const page = Page({ margin: 64, gap: 5, footer }, [
Text("ON DOCUMENT ENGINEERING", { size: 10, bold: true, color: gold }),
Padding({ top: 4 }, Text("Why pagination is the hard part", { size: 29, bold: true, color: ink })),
Text("And what it takes to get right in pure TypeScript", { size: 13, color: muted }),
Padding({ top: 12, bottom: 6 }, Divider({ color: hair })),
Paragraph(LEAD, { font: "Times-Roman", size: 13, color: ink, lineHeight: 1.5 }),
heading("The last fifteen percent"),
body(P1),
body(P2),
Padding(
{ top: 14, bottom: 14 },
Box({ borderLeft: brand, padding: { left: 18, y: 4 } }, [
Text(
"A table that breaks mid-row, a heading stranded at the foot of a page: these are the details that separate a layout engine from a drawing API.",
{ size: 16, italic: true, font: "Times-Roman", color: brand, lineHeight: 1.35 },
),
]),
),
heading("Metrics, not guesses"),
body(P3),
body(P4),
heading("Flow as a first-class idea"),
body(P5),
body(P6),
]);
export default Document([page]);
letter.ts
Letter
A business letter - body type set once on the Document so every line inherits it; muted blocks use DefaultTextStyle.
// A business letter - the showcase for inherited text styles. The Document sets the body typeface,
// size, colour and line-height ONCE; every paragraph below just inherits them (no per-Text styling).
// Blocks that need their own look (the contact line, the signature title, the confidentiality note)
// wrap a DefaultTextStyle that re-defaults only that subtree, still inheriting the font.
import {
Document,
Page,
Column,
Row,
Box,
Divider,
Spacer,
DefaultTextStyle,
Text,
} from "@jasy/pdf";
const ink = "#1f2a37";
const brand = "#1450aa";
const muted = "#6b7280";
const hair = "#dfe4ee";
const gap = (h: number) => Box({ height: h }, []);
export default Document({ font: "Times-Roman", size: 11, color: ink, lineHeight: 1.5 }, [
Page({ size: "A4", margin: 64 }, [
// letterhead: brand wordmark + a muted contact block (its own default style)
Row({ justify: "between", align: "end" }, [
Text("Muster Studio", { size: 20, bold: true, color: brand }),
DefaultTextStyle({ size: 9, color: muted }, [
Column({ align: "end", gap: 1 }, [
Text("Hauptstraße 1 · 10115 Berlin"),
Text("kontakt@muster-studio.de"),
]),
]),
]),
gap(10),
Divider({ color: hair }),
gap(26),
Row({ justify: "end" }, [Text("Berlin, 24 June 2026")]),
gap(20),
Column({ gap: 2 }, [
Text("Beispiel Kunde AG", { bold: true }),
Text("Marienplatz 1"),
Text("80331 München"),
]),
gap(24),
Text("Re: Your brand refresh - milestone two", { bold: true }),
gap(16),
Text("Dear Ms. Example,"),
gap(10),
Text(
"thank you for the trust you have placed in our studio. The first phase of your brand " +
"refresh is complete, and we are delighted with how the new identity has come together.",
),
gap(8),
Text(
"Over the coming weeks we will prepare the design system and the component library, and " +
"share a working preview with your team well ahead of the launch date. Should anything " +
"need adjusting, there is ample room in the schedule to accommodate it.",
),
gap(8),
Text("We look forward to the next milestone and remain at your disposal for any questions."),
gap(20),
Text("Sincerely,"),
gap(6),
Text("Erika Muster", { bold: true }),
DefaultTextStyle({ size: 9, color: muted }, [Text("Founder · Muster Studio")]),
Spacer(),
Divider({ color: hair }),
gap(8),
DefaultTextStyle({ size: 8.5, color: muted, lineHeight: 1.4 }, [
Text(
"This letter is confidential and intended solely for the addressee. If you have received " +
"it in error, please notify the sender and delete it.",
),
]),
]),
]);
banner.ts
Banner
An A5-landscape banner - a full-bleed image with type composited over it.
// A promo banner in a different format (A5 landscape): a raster image fills the page edge to edge
// (BoxFit cover, cropped by overflow:hidden), with the message positioned on top. Shows the image
// layer + a non-A4 format. Everything is out-of-flow (Positioned) inside an Expanded relative frame,
// so the frame fills the page cleanly - the same pattern as the cover page.
import { Document, Page, Box, Expanded, Positioned, Column, Text, Image } from "@jasy/pdf";
const white = "#ffffff";
const soft = "#cdddf5";
const accent = "#f3dc29";
// A5 landscape = 595.28 x 419.53 pt (148 x 210 mm)
export default Document([
Page({ size: "A5", orientation: "landscape", margin: 0 }, [
Expanded(
Box({ relative: true, overflow: "hidden" }, [
// full-bleed background image: sized a touch over the page and cropped to a clean bleed by
// overflow:hidden. A Positioned child is out of flow, so even oversized it never paginates.
Positioned({ top: 0, left: 0 }, Image("examples/assets/banner.png", { width: 600, height: 425, fit: "cover" })),
// message
Positioned(
{ top: 64, left: 56, right: 56 },
Column({ gap: 12 }, [
Text("PURE TYPESCRIPT · ZERO DEPENDENCIES", { size: 11, bold: true, color: accent }),
Column({ gap: 2 }, [
Text("Make beautiful PDFs", { size: 34, bold: true, color: white }),
Text("straight from components.", { size: 34, bold: true, color: white }),
]),
Text("No headless browser, no Java. Just a tree of components and real font metrics.", {
size: 13,
color: soft,
lineHeight: 1.35,
}),
]),
),
// footer marks, anchored to the frame corners
Positioned({ bottom: 36, left: 56 }, Text("@jasy/pdf", { size: 12, bold: true, color: accent })),
Positioned({ bottom: 36, right: 56 }, Text("v1.0 · MIT", { size: 11, color: soft })),
]),
),
]),
]);
label.ts
Product label
A 50x65 mm label via mm() - brand, price, and a barcode drawn from rectangles.
// A 50x65mm product label - a custom (non-A/B) page format via the mm() helper. Compact layout:
// brand, product, price, and a barcode drawn from rectangles.
import { Document, Page, Column, Row, Box, Text, Divider, Spacer, mm } from "@jasy/pdf";
const ink = "#1b2433";
const muted = "#6b7280";
const brand = "#1450aa";
const hair = "#dfe4ee";
// a barcode from bars of varying width (deterministic pattern)
const WIDTHS = [1, 2, 1, 1, 3, 1, 2, 1, 1, 2, 3, 1, 1, 2, 1, 3, 1, 1, 2, 1, 2, 1, 3, 1, 1, 2, 1];
const barcode = Row(
{ gap: 1.4, align: "end", justify: "center" },
WIDTHS.map((w) => Box({ width: w, height: 28, bg: ink }, [])),
);
export default Document([
Page({ size: mm(50, 65), margin: 10 }, [
Row({ justify: "between", align: "center" }, [
Text("MUSTER", { size: 11, bold: true, color: brand }),
Text("ROASTERS", { size: 6.5, bold: true, color: muted }),
]),
Divider({ color: hair, margin: { y: 7 } }),
Text("SINGLE ORIGIN", { size: 7, bold: true, color: muted }),
Text("Ethiopia Yirgacheffe", { size: 13, bold: true, color: ink, lineHeight: 1.15 }),
Text("Washed · floral, citrus, tea-like", { size: 7.5, color: muted, lineHeight: 1.2 }),
Spacer(),
Row({ justify: "between", align: "end" }, [
Column({ gap: 1 }, [
Text("250 g", { size: 8.5, bold: true, color: ink }),
Text("Whole bean", { size: 7, color: muted }),
]),
Text("12,90 €", { size: 17, bold: true, color: ink }),
]),
Divider({ color: hair, margin: { y: 7 } }),
Column({ gap: 4 }, [barcode, Text("4 006381 332149", { size: 6.5, color: muted, align: "center" })]),
]),
]);