npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nemu-ai/pdf

v1.5.5

Published

PDF generation for Node and Bun with a declarative document API and a low level element API, bundled variable fonts with real weights, LaTeX math, native charts, and theming.

Readme

@nemu-ai/pdf

A modern PDF generation library for Node and Bun. It ships two APIs from one package: a declarative Doc for writing documents as content blocks, and a low level Document for placing elements with full control. Both render through a single measure, layout, render engine that embeds fonts, draws vector graphics, and typesets LaTeX.

Features

  • Two APIs that share one engine: declarative Doc and imperative Document.
  • Six variable fonts bundled and auto registered, with real weights from thin to black.
  • LaTeX math typeset by MathJax into crisp vector paths.
  • Native vector charts: bar, line, area, pie, donut.
  • Tables with rich cells, notes, lists, code blocks, dividers, and images.
  • Flex and flow containers, z index layering, and explicit positioning.
  • Headers and footers as blocks, functions of page context, or raw drawing callbacks.
  • Themes, role styles, and a small Color utility.
  • Works on plain Node 18+ and Bun, in both ESM and CommonJS.

Install

npm install @nemu-ai/pdf
# or
bun add @nemu-ai/pdf

Fonts and the font instancer are bundled, so nothing else is required. The default math font for LaTeX is included; additional math fonts are optional installs.

Choosing an API

Use Doc for documents: reports, articles, invoices, anything that is content first. You describe blocks and the library handles layout, pagination, headers, and footers.

Use Document when you need exact coordinates, custom layouts, overlapping elements, or fine control over every element. Doc itself compiles down to Document, so you can mix mental models freely.


Doc: the declarative API

A Doc is a sequence of pages, and each page is a list of content blocks. Set role styles once, add content, and call build.

import { Doc } from "@nemu-ai/pdf";

const doc = new Doc({ page_size: "A4", margin: 54, padding: 10 });

doc.set_style({
  heading: { font_family: "inter", color: "#111827" },
  paragraph: { font_family: "source-serif-4", font_size: 11.5, line_height: 1.6 },
  link: { color: "#2563eb" },
});

doc.page().content(
  { type: "heading", text: "Quarterly Report", level: 1 },
  { type: "paragraph", text: "Revenue grew across every region this quarter." },
  { type: "note", variant: "info", title: "Note", text: "Figures are unaudited." },
);

await doc.build("report.pdf");

Doc methods

new Doc(options?: DocOptions)
doc.set_style(styles: RoleStyles): this
doc.set_header(header: HeaderFooter): this
doc.set_footer(footer: HeaderFooter): this
doc.load_font(name: string, path: string, variable?: boolean): this
doc.load_image(name: string, path: string): this
doc.page(config?: PageConfig): DocPage
await doc.build(file_path: string): Promise<void>

page.content(...items: Array<Block | Block[]>): this

new Doc(options) turns markdown parsing on for plain strings by default. content flattens arrays, so a component that returns Block[] can be spread inline. build writes the file and is always awaited. margin plus padding form the page content inset, and auto_paginate is on unless you set it to false.

Blocks

Every block is a tagged object. A block level style overrides the role style for that one block.

{ type: "heading", text: "Title", level: 2 }
{ type: "paragraph", text: "Body text." }
{ type: "code", text: "const x = 1;", language: "ts" }
{ type: "formula", text: "E = mc^2" }
{ type: "list", items: ["First", "Second"], ordered: true }
{ type: "image", src: "logo", width: 120, height: 40 }
{ type: "divider" }
{ type: "spacer", size: 16 }
{ type: "note", variant: "warn", title: "Careful", text: "Provisional value." }
{ type: "table", headers: ["A", "B"], rows: [["1", "2"]] }
{ type: "chart", chart: "bar", data: { labels: ["Q1"], series: [{ values: [10] }] } }
{ type: "group", children: [/* blocks kept together across page breaks */] }

group prevents its children from splitting across a page break, which is the natural way to keep a heading with its first paragraph. spacer adds vertical space, divider draws a rule, and image references a path or a name registered with load_image.

Inline content

Wherever a block takes text, it accepts a string, a single inline node, or an array mixing both. Inline nodes nest, so a link can wrap a formula and a strong run can wrap a link.

{
  type: "paragraph",
  text: [
    "See the ",
    { type: "link", href: "https://example.com", text: { type: "strong", text: "spec" } },
    " and the identity ",
    { type: "formula", text: "e^{i\\pi} + 1 = 0" },
    ".",
  ],
}

Inline node types are strong, em, strike, link with href, formula, and code. When markdown is enabled, plain strings also honor **bold**, *italic*, ~~strike~~, and $inline math$. For generated content, structured inline nodes are the reliable path.

Styling and roles

Styles are plain objects. set_style sets a default per role, and a block style merges on top. The roles are heading, paragraph, code, formula, list, link, group, divider, note, table, and chart.

doc.set_style({
  heading: { font_family: "inter", font_weight: "bold", color: "#0f172a" },
  paragraph: { font_family: "source-serif-4", font_size: 12, line_height: 1.6 },
  code: { font_family: "geist-mono", background_color: "#f6f8fa" },
  note: { color: "#1f2937" },
});

The style fields used most in documents are color, background_color, font_family, font_size, font_weight, line_height, text_align, margin, and padding. Colors accept a hex string or a Color. See Fonts and weights for font_weight.

Fonts and weights

Six variable fonts are bundled and registered automatically. Use them by name:

| Name | Style | | --- | --- | | inter | sans serif | | geist | sans serif | | geist-mono | monospace | | nunito-sans | sans serif | | roboto | sans serif | | source-serif-4 | serif |

font_weight selects a real weight by instancing the variable font. Names map to the usual axis values and numbers pass straight through.

{ type: "paragraph", text: "Heavy", style: { font_family: "inter", font_weight: "black" } }
{ type: "paragraph", text: "Light", style: { font_family: "inter", font_weight: 300 } }

Weight names: thin 100, extralight 200, light 300, regular 400, medium 500, semibold 600, bold 700, extrabold 800, black 900. Markdown **bold** and the strong inline node also produce a real bold weight. The bundled fonts are upright only, so italic renders upright. To use a custom variable font with weights, register it with doc.load_font(name, path, true).

Formulas

Block and inline formula text is LaTeX, typeset by MathJax into vector paths. The default math font is termes. Other fonts are optional installs.

import { available_formula_fonts, default_formula_font } from "@nemu-ai/pdf";
// available_formula_fonts -> ["termes", "newcm", "modern", "pagella", "stix2", "fira"]
// default_formula_font    -> "termes"

doc.set_style({ formula: { font: "termes", font_size: 13, color: "#111827" } });

doc.page().content(
  { type: "formula", text: "\\int_0^\\infty e^{-x^2}\\,dx = \\frac{\\sqrt{\\pi}}{2}" },
);

A non default font needs its package, for example npm i @mathjax/mathjax-pagella-font. A missing font logs a warning and falls back to termes.

Tables

{
  type: "table",
  headers: ["Metric", "Q3", "Q4"],
  rows: [
    ["Revenue", "1.2M", { text: { type: "strong", text: "1.6M" }, align: "right" }],
    ["Margin", "18%", { text: "24%", align: "right" }],
  ],
}

A cell is a string or { text, align }, where text is inline content. Bold, links, and inline formulas all work inside cells. columns is optional; omit it to size columns equally from the header or first row. Set it to a count for equal columns, or to an array of absolute widths in points where 0 means auto.

Charts

Charts render as native vector graphics with no image step.

{
  type: "chart",
  chart: "bar",
  title: "Revenue by quarter",
  legend: true,
  height: 220,
  data: {
    labels: ["Q1", "Q2", "Q3", "Q4"],
    series: [
      { name: "2024", values: [12, 19, 14, 23], color: "#111827" },
      { name: "2025", values: [16, 22, 20, 28], color: "#9ca3af" },
    ],
  },
}

bar, line, and area read labels and series. pie and donut read slices, where each slice is { label, value, color? }.

Headers, footers, and raw drawing

A header or footer is a block, an array of blocks, a function of page context, or a raw draw callback. Functions run per page at build time.

doc.set_footer((ctx) => ({
  type: "paragraph",
  text: `Page ${ctx.page_number} of ${ctx.page_count}`,
  style: { font_size: 9, color: "#9ca3af", text_align: "right" },
}));

doc.set_header({
  height: 40,
  draw: (pdoc, area) => {
    pdoc.rect(area.x, area.y + area.height - 1, area.width, 1).fill("#e5e7eb");
  },
});

The page context is { page_number, page_count, date }. The raw draw callback receives the live pdfkit document and the zone rectangle { x, y, width, height } plus the page context. Set per page headers and footers through doc.page({ header, footer }).

Reusable components

A component is a function that returns a block or an array of blocks. Spread it into content.

const section = (title: string, body: InlineContent): Block[] => [
  { type: "heading", text: title, level: 2 },
  { type: "paragraph", text: body },
];

doc.page().content(
  ...section("Overview", "What this document covers."),
  { type: "note", variant: "info", text: "Spread arrays of blocks directly." },
);

See examples/simplified for a full multi page Doc reference document.


Document: the low level API

Document places elements directly. You create pages, build elements with the page factories, add them to the page or to containers, and call build. Elements flow top to bottom by default, or sit at explicit coordinates when given a position.

import { Document } from "@nemu-ai/pdf";

const pdf = new Document({ page_size: "A4", margin: 50 });
const page = pdf.create_page();

page.add(
  page.text({ content: "Invoice", style: { font_size: 28, font_weight: "bold" } }),
  page.text({ content: "Thank you for your business.", style: { color: "#6b7280" } }),
);

await pdf.build("invoice.pdf");

The pipeline runs in three passes. Measure computes each element's intrinsic size from font metrics. Layout assigns positions from the page margin and header zone. Render draws every element in z index order.

Document and Page

new Document(options?: DocumentOptions)
pdf.create_page(theme?: Theme): Page
pdf.add_page(): Page
pdf.set_theme(theme: Theme): void
pdf.load_font_sync(name: string, file_path: string, variable?: boolean): void
pdf.load_image_sync(name: string, file_path: string): void
await pdf.build(file_path: string): Promise<void>

DocumentOptions are page_size ("A4" | "Letter" | "Legal" | "Custom"), custom_dimensions ({ width, height } in points), margin (a number or per side values), and parse_markdown. build is the full pipeline; it is split internally into prepare_pdf and render_to so a caller can measure before rendering.

The page factories build elements but do not place them. Add what you build with page.add or a container's add.

page.text(options: CreateTextOptions): TextElement
page.rect(options: CreateRectOptions): RectElement
page.image(options: CreateImageOptions): ImageElement
page.table(options: CreateTableOptions): TableElement
page.create_container(options?: CreateContainerOptions): ContainerElement
page.header_container(options?: CreateContainerOptions): HeaderContainer
page.footer_container(options?: CreateContainerOptions): FooterContainer
page.add(...elements: BaseElement[]): this

header_container and footer_container are idempotent and return the existing zone if called again. The page also exposes get_content_width() and get_content_height() along with margin helpers.

Text, rectangles, and images

const label = page.text({
  content: "Absolute label",
  position: { x: 100, y: 200 },
  style: { font_size: 11 },
});

const card = page.rect({
  width: 400,
  height: 120,
  shape_style: { fill_color: "#1a365d", border_radius: 10 },
});

pdf.load_image_sync("logo", "./assets/logo.png");
const img = page.image({ name: "logo", width: 120, height: 40 });

page.add(label, card, img);

A position makes an element explicit and absolute. Without one it joins the flow. Rectangle colors come from style.background_color or a shape_style with fill_color, stroke_color, stroke_width, and border_radius. Images reference a name registered with load_image_sync.

Overlay children and z index

Any element can own overlay children built with element.text, element.rect, element.image, or element.create_container. Children are positioned relative to the parent's top left corner and render on top of it, sorted by z index.

const box = page.rect({
  width: 300,
  height: 80,
  shape_style: { fill_color: "#2b6cb0", border_radius: 8 },
});
box.text({
  content: "Rendered on top of the box.",
  style: { color: "#ffffff", padding: 12 },
  z_index: 1,
});
page.add(box);

Within the same parent, elements render in ascending z index order regardless of declaration order.

Containers and layout

A container groups children with a flow or flex layout.

const row = page.create_container({
  layout: { type: "flex", direction: "row", justify: "space-between", align: "center", gap: 12 },
  width: page.get_content_width(),
});
row.add(
  page.text({ content: "Left" }),
  page.text({ content: "Right" }),
);
page.add(row);

Flow stacks children vertically with an optional gap. Flex distributes them along a row or column. justify is one of flex-start, flex-end, center, space-between, space-around, space-evenly. align is one of flex-start, flex-end, center, stretch, baseline. Containers also accept style, width, height, position, and classname.

Tables

const table = page.table({
  columns: [180, 0, 100],
  border_width: 0.5,
  cell_padding: 8,
  header_style: { background_color: "#ebf4ff" },
  width: page.get_content_width(),
});
table.add_row(["Name", "Role", "Status"]);
table.add_row(["Alice Nguyen", "Lead Engineer", "Active"]);
table.add_row([
  { content: "Bob Chen", style: { color: "#718096" } },
  "Designer",
  "Active",
]);
page.add(table);

columns is an equal count or an array of widths, where 0 means auto width. Each cell is a string or { content, style }. Borders are controlled by border_color, border_width, and cell_padding.

Themes

A theme centralizes named colors and applies default styles by element type and classname.

import { Document, create_theme } from "@nemu-ai/pdf";

const theme = create_theme("brand", {
  colors: { primary: "#1a365d", muted: "#718096", accent: "#3182ce" },
});

const pdf = new Document({ page_size: "A4", margin: 50 });
pdf.set_theme(theme);
const page = pdf.create_page(theme);
theme.get_color("primary");

See examples/showcase for a full multi page Document built with a theme.


Color

Color builds a color from one input form and converts to any other. Construct it as a call or with new; both work, and you cannot mix input forms.

import { Color } from "@nemu-ai/pdf";

const a = Color({ hex: "#2563eb" });
const b = new Color({ rgb: [37, 99, 235] });
const c = Color({ hsl: [217, 91, 60] });

a.to_hex();    // "#2563eb"
a.to_rgba();   // "rgba(37, 99, 235, 1)"
a.alpha(0.5);  // a Color at 50% opacity
a.lighten(0.1);
a.darken(0.1);
a.mix(b, 0.5);
a.is_dark();   // true

Input forms are { hex }, { rgb }, { rgba }, { hsl }, and { hsla }. A Color is accepted anywhere a color is, so any color or background_color field takes a hex string or a Color.


Reference

Page sizes

| Name | Width (pt) | Height (pt) | | --- | --- | --- | | A4 | 595.28 | 841.89 | | Letter | 612 | 792 | | Legal | 612 | 1008 |

Use custom_dimensions for any other size.

const pdf = new Document({ custom_dimensions: { width: 400, height: 600 } });

Vector

Vector is the spatial primitive for positions and sizes. vector(x, y) is a convenience constructor. After build, elements expose get_position(), get_size(), and get_bbox().

import { vector } from "@nemu-ai/pdf";
const v = vector(100, 200); // v.x === 100, v.y === 200

Types

All of the following are exported as types: Block, Inline, InlineContent, DocOptions, PageConfig, PageContext, HeaderFooter, RawDraw, DrawArea, RoleStyles, StyleRole, TableCell, TableRow, ChartKind, ChartData, ChartSeries, ChartSlice, StyleProperties, DocumentOptions, PageSize, PageDimensions, MarginValues, ColorLike, ColorInput, ColorValue, ContainerLayout, FlexLayoutOptions, FlowLayoutOptions, CreateTextOptions, CreateRectOptions, CreateImageOptions, CreateTableOptions, and CreateContainerOptions.

For an API map aimed at language models, see llms.txt.

License

MIT. Bundled fonts are licensed under the SIL Open Font License; see dist/fonts/OFL.txt.