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.1.1

Published

A modern PDF generation library with theming and vector support

Readme

@nemu-ai/pdf

A TypeScript library for generating PDFs programmatically. It builds on PDFKit and adds a structured element tree, a three-pass layout engine (measure, layout, render), composable flex and flow containers, header and footer zones, parent-child element relationships, z-index layering, theming, and inline markdown and LaTeX parsing.

Installation

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

Quick start

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

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

page.add(
  page.text({ content: "Hello, world!", style: { font_size: 24 } }),
  page.text({ content: "Generated with @nemu-ai/pdf." }),
);

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

Design overview

The library follows a three-pass pipeline:

  1. Measure - each element computes its intrinsic size using PDFKit font metrics.
  2. Layout - positions are computed top-down from the page margin and header zone height.
  3. Render - elements draw themselves to PDFKit in z-index order.

You build the element tree before calling build(). Factory methods on Page create elements; page.add() places them in the page's flow. Container elements have their own add() method for nesting children. Any element can also own overlay children created via element.text(), element.rect(), etc., which render on top of the parent positioned relative to its top-left corner.


Classes

Document

The root object. Holds page dimensions, margin, font registrations, and the list of pages.

new Document(options?: DocumentOptions)

Options

| Property | Type | Default | Description | |---|---|---|---| | page_size | PageSize | "A4" | Named page size. | | custom_dimensions | PageDimensions | - | Explicit { width, height } in points. Takes precedence over page_size. | | margin | number \| MarginValues | 72 | Page margin in points. A single number sets all four sides equally. | | parse_markdown | boolean | false | Enables inline markdown parsing globally for all text elements. |

Methods

create_page(theme?: Theme): Page Creates a new page and appends it to the document. The optional theme overrides the document's default theme for this page.

add_page(): Page Alias for create_page() with no theme argument.

set_theme(theme: Theme): void Sets the default theme applied to every page unless overridden at page creation.

load_font(name: string, path: string): Promise<void> load_font_sync(name: string, path: string): void Registers a custom TTF or OTF font under name. The name is then used in style.font_family. Custom fonts must be loaded before calling build().

await doc.load_font("NotoSans", "./fonts/NotoSans-Regular.ttf");
await doc.load_font("NotoSans-Bold", "./fonts/NotoSans-Bold.ttf");

load_image(name: string, path: string): Promise<void> load_image_sync(name: string, path: string): void Registers an image under name for use with page.image({ name }).

build(file_path: string): Promise<void> Runs the full measure-layout-render pipeline for every page and writes the PDF to file_path.

Properties

| Property | Type | Description | |---|---|---| | page_width | number | Page width in points. | | page_height | number | Page height in points. | | margin | MarginValues | Resolved margin with top, right, bottom, left fields. | | pages | Page[] | All pages in declaration order. |


Page

Represents one page. You obtain it from doc.create_page(). Do not construct it directly.

Header and footer zones

page.header_container(options?: CreateContainerOptions): HeaderContainer
page.footer_container(options?: CreateContainerOptions): FooterContainer

Both methods are idempotent - calling them more than once returns the existing zone. The header sits at the top margin; the footer sits at the bottom margin. Their heights are determined by their content at measure time, and the body content area is inset accordingly.

const header = page.header_container({ style: { padding_bottom: 12 } });
header.add(
  page.text({ content: "My report", style: { font_size: 10 } }),
);

Element factories

All factory methods return an element reference. The element is not placed on the page until you call page.add() or include it inside a container via container.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

Placing elements

page.add(...elements: BaseElement[]): this Appends elements to the page's root flow. Returns the page for chaining.

Dimension helpers

| Method | Returns | |---|---| | get_width() | Page width in points | | get_height() | Page height in points | | get_content_width() | Page width minus left and right margin | | get_content_height() | Page height minus top and bottom margin | | get_margin_left() | Left margin | | get_margin_right() | Right margin | | get_margin_top() | Top margin | | get_margin_bottom() | Bottom margin |


BaseElement

Abstract base class for all element types. You never construct this directly.

Properties

| Property | Type | Description | |---|---|---| | id | symbol | Unique identity for this element instance. | | layout_mode | LayoutMode | "flow" or "explicit". Set automatically based on whether position is provided. | | z_index | number | Draw order within the same parent. Higher values render on top. Default is 0. | | measured_size | MeasuredSize \| null | Set after the measure pass. | | computed_position | Vector \| null | Set after the layout pass. | | computed_size | Vector \| null | Set after the layout pass. |

Introspection methods (available after build())

get_position(): Vector - throws if called before build(). get_size(): Vector - throws if called before build(). get_bbox(): BoundingBox - returns { x, y, width, height }.

Child factory methods

Any element can own overlay children. These children are positioned relative to the parent element's top-left corner and rendered on top of it, sorted by z-index.

element.text(options: CreateTextOptions): TextElement element.rect(options: CreateRectOptions): RectElement element.image(options: CreateImageOptions): ImageElement element.create_container(options?: CreateContainerOptions): ContainerElement

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);

Children with position are offset from the parent's top-left corner. Children without position render at the parent's top-left corner.

box.text({
  content: "Bottom right label",
  position: { x: 200, y: 56 },
  z_index: 1,
  style: { font_size: 9 },
});

TextElement

Renders a string of text. Supports inline markdown and LaTeX when enabled.

Options: CreateTextOptions

| Property | Type | Description | |---|---|---| | content | string | The text to render. Required. | | style | StyleProperties | Visual styling. | | classname | string | Theme class name to merge. | | position | VectorLike | If set, the element uses explicit layout mode and is placed at this offset from the content origin (or parent top-left). | | width | number | Wrap width in points. Defaults to the available width from the parent. | | parse_markdown | boolean | Overrides the document-level parse_markdown flag for this element. | | z_index | number | Draw order. Default 0. |

Layout modes

When position is omitted, the element participates in flow layout and the parent places it. When position is provided, the parent positions it at that offset and does not advance the cursor past it.

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

RectElement

Draws a filled and/or stroked rectangle.

Options: CreateRectOptions

| Property | Type | Description | |---|---|---| | style | StyleProperties | General styling (padding, background color). | | shape_style | ShapeStyle | Fill color, stroke color, stroke width, border radius. | | position | VectorLike | Explicit position offset if needed. | | width | number | Width in points. Defaults to available width. | | height | number | Height in points. Defaults to 0. | | z_index | number | Draw order. Default 0. |

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

ImageElement

Embeds a registered image.

Options: CreateImageOptions

| Property | Type | Description | |---|---|---| | name | string | The name used when calling doc.load_image(). Required. | | position | VectorLike | Explicit position offset if needed. | | width | number | Rendered width in points. | | height | number | Rendered height in points. | | z_index | number | Draw order. Default 0. |

await doc.load_image("logo", "./assets/logo.png");

const img = page.image({ name: "logo", width: 120, height: 40 });
page.add(img);

ContainerElement

Groups child elements and controls how they are arranged. The two layout strategies are flow (vertical stacking) and flex (row or column with alignment).

Options: CreateContainerOptions

| Property | Type | Description | |---|---|---| | layout | ContainerLayout | Flow or flex layout configuration. Defaults to { type: "flow" }. | | style | StyleProperties | Padding, background color. | | classname | string | Theme class name to merge. | | position | VectorLike | Explicit position offset if needed. | | width | number | Width in points. Defaults to available width. | | height | number | Height in points. If omitted, height is derived from children. | | z_index | number | Draw order. Default 0. |

Methods

add(...elements: BaseElement[]): this Adds layout children to this container. Children flow or flex inside the container's padded area.

get_elements(): BaseElement[] Returns a copy of the layout children array.

Flow layout

Children stack vertically with an optional gap.

const stack = page.create_container({
  layout: { type: "flow", gap: 8 },
  style: { padding: 16, background_color: "#f7fafc" },
  width: 400,
});
stack.add(
  page.text({ content: "First item" }),
  page.text({ content: "Second item" }),
  page.text({ content: "Third item" }),
);
page.add(stack);

Flex layout

Children are distributed along a row or column.

const row = page.create_container({
  layout: {
    type: "flex",
    direction: "row",
    justify: "space-between",
    align: "center",
    gap: 12,
  },
  width: 500,
  height: 48,
});
row.add(
  page.text({ content: "**Label**" }),
  page.text({ content: "$42,000" }),
);
page.add(row);

FlexLayoutOptions fields:

| Property | Type | Default | Description | |---|---|---|---| | type | "flex" | required | | | direction | FlexDirection | required | "row" or "column". | | justify | FlexJustify | "flex-start" | Main-axis alignment. | | align | FlexAlign | "flex-start" | Cross-axis alignment. | | gap | number | 0 | Gap between children in points. |

justify values: "flex-start", "flex-end", "center", "space-between", "space-around", "space-evenly". align values: "flex-start", "flex-end", "center", "stretch", "baseline".


TableElement

Renders a grid with optional header row styling, cell padding, and borders.

Options: CreateTableOptions

| Property | Type | Description | |---|---|---| | columns | number \| number[] | Number of equal columns, or an array of column widths in points. Use 0 for auto-width columns. | | style | StyleProperties | Base text style applied to all cells. | | width | number | Total table width in points. Defaults to available width. | | border_color | string | Hex color for cell borders. Default "#e2e8f0". | | border_width | number | Border stroke width. Default 1. Set to 0 to hide borders. | | cell_padding | number | Inner padding for each cell in points. Default 8. | | header_style | StyleProperties | Style applied to the first row. | | cell_style | StyleProperties | Style applied to all non-header cells. | | position | VectorLike | Explicit position if needed. | | z_index | number | Draw order. Default 0. |

Methods

add_row(cells: Array<string | TableCellInput>): this Appends a row. The first row receives header_style. Each cell can be a plain string or { content: string, style: StyleProperties } for per-cell overrides.

const table = page.table({
  columns: [200, 0, 100],
  border_color: "#cbd5e0",
  border_width: 0.5,
  cell_padding: 8,
  header_style: { background_color: "#ebf4ff", font_size: 11 },
  cell_style: { font_size: 11 },
});
table.add_row(["Name", "Role", "Status"]);
table.add_row(["Alice", "Engineer", "Active"]);
table.add_row([
  { content: "Bob", style: { color: "#718096" } },
  "Designer",
  { content: "Inactive", style: { color: "#c53030" } },
]);
page.add(table);

HeaderContainer / FooterContainer

Extend ContainerElement. Use page.header_container() and page.footer_container() - do not construct directly.

Their height (zone_height) is determined during the measure pass and is used by the layout engine to push the body content area down (header) or constrain it from the bottom (footer).


Styling

The StyleProperties object controls typography and spacing. All properties are optional.

| Property | Type | Description | |---|---|---| | font_family | string | Font name. Defaults to "Helvetica". | | font_size | number | Font size in points. Default 12. | | font_weight | string | "bold" or "normal". | | font_style | string | "italic" or "normal". | | color | string | Text color as a hex string, e.g. "#1a365d". | | background_color | string | Background fill color. | | text_align | string | "left", "center", "right", "justify". | | line_height | number | Line height multiplier. Default 1.2. | | padding | number | Shorthand for all four sides. | | padding_top | number | Top padding in points. | | padding_right | number | Right padding in points. | | padding_bottom | number | Bottom padding in points. | | padding_left | number | Left padding in points. |

Padding affects both measured size and text render position. A text element with padding: 12 starts its text 12 points from its computed top-left corner.


ShapeStyle

Controls the visual appearance of RectElement.

| Property | Type | Description | |---|---|---| | fill_color | string | Fill color as a hex string. | | stroke_color | string | Stroke color. | | stroke_width | number | Stroke line width in points. | | border_radius | number | Corner radius in points. |


Theming

Themes centralize colors and element styles. You create a theme once and apply it at the document or page level.

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

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

const doc = new Document({ page_size: "A4", margin: 50 });
doc.set_theme(theme);

create_theme(name: string, config: ThemeConfig): Theme Creates and returns a Theme instance.

theme.get_color(name: string): string Returns the hex color registered under name. Throws if not found.

theme.merge_classname(style: StyleProperties, classname: string): StyleProperties Returns a new style that is the base merged with the class definition for classname.

theme.apply_to_style(style: StyleProperties, element_type: string): StyleProperties Returns a style with default element-level properties applied.


Markdown parsing

Inline markdown is supported when parse_markdown: true is set on the document or on an individual text element.

| Syntax | Result | |---|---| | **text** or __text__ | Bold | | *text* or _text_ | Italic | | ***text*** or ___text___ | Bold and italic | | ~~text~~ | Strikethrough | | $formula$ | Inline LaTeX, converted to readable text | | $$formula$$ | Block LaTeX, converted to readable text |

const doc = new Document({ parse_markdown: true });
const page = doc.create_page();

page.add(
  page.text({
    content: "The **Q4 revenue** grew by *32%*, with ***record margins*** in all segments. ~~Previous guidance~~ is revised.",
  }),
);

You can override the document setting per element:

page.add(
  page.text({
    content: "Raw **asterisks** not parsed.",
    parse_markdown: false,
  }),
);

LaTeX support

Math expressions inside $...$ (inline) or $$...$$ (display) are parsed and converted to readable text using a symbol table. The output is rendered in italic.

page.add(
  page.text({ content: "Pythagorean theorem: $a^2 + b^2 = c^2$" }),
  page.text({ content: "Quadratic formula: $x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}$" }),
);

Font support

Standard built-in PDF fonts (Helvetica, Times-Roman, Courier) only cover the Latin-1 character set (U+0000 to U+00FF). Operators such as \times (x), \div (div), and \pm (+/-) map to Latin-1 characters and render correctly. Greek letters (\alpha, \beta, etc.) and most math symbols fall outside Latin-1 and are rendered as ASCII text approximations (e.g. alpha, beta) unless you register a Unicode font.

To render proper Greek and math symbols, register a Unicode-capable font such as NotoSans:

await doc.load_font("NotoSans", "./fonts/NotoSans-Regular.ttf");
await doc.load_font("NotoSans-Bold", "./fonts/NotoSans-Bold.ttf");

page.add(
  page.text({
    content: "Euler's identity: $e^{i\\pi} + 1 = 0$",
    style: { font_family: "NotoSans" },
  }),
);

Z-index and layering

Every element has a z_index property. Within the same parent, elements are rendered in ascending z-index order regardless of declaration order.

Root page elements use z-index to sort the final render pass. Container children are sorted within their container. Overlay children (created via element.text(), element.rect(), etc.) are sorted separately and always render after the parent.

const base = page.rect({ width: 400, height: 80, shape_style: { fill_color: "#1a365d" }, z_index: 0 });
const top = page.rect({ shape_style: { fill_color: "#3182ce" }, position: { x: 20, y: 10 }, z_index: 2 });
const mid = page.rect({ shape_style: { fill_color: "#63b3ed" }, position: { x: 40, y: 20 }, z_index: 1 });
page.add(base, top, mid);

Note that explicit-positioned root elements are placed relative to the page content origin, not the current cursor. To make layered explicit elements relative to each other, put them inside a container:

const layer = page.create_container({ width: 400, height: 80 });
layer.add(base, top, mid);
page.add(layer);

Explicit positioning

Set position on an element to place it at a fixed offset rather than in the automatic flow. For root elements, the offset is relative to the top-left of the content area. For elements inside a container, the offset is relative to the container's inner top-left corner (after padding).

const info = page.rect({ width: 300, height: 80, shape_style: { fill_color: "#faf5ff" } });
info.text({ content: "Top left",     position: { x: 8, y: 8 },    z_index: 1, style: { font_size: 9 } });
info.text({ content: "Bottom right", position: { x: 208, y: 60 }, z_index: 1, style: { font_size: 9 } });
info.text({ content: "Center",       position: { x: 120, y: 34 }, z_index: 1, style: { font_size: 11 } });
page.add(info);

Multi-page documents

Call doc.create_page() once per page. Each page is processed independently by the layout engine.

const doc = new Document({ page_size: "A4", margin: 50 });

for (let i = 1; i <= 5; i++) {
  const page = doc.create_page();
  page.add(
    page.text({ content: `Page ${i}`, style: { font_size: 24 } }),
  );
}

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

Complete example

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

const doc = new Document({ page_size: "A4", margin: 50, parse_markdown: true });

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

const page = doc.create_page(theme);

const header = page.header_container({ style: { padding_bottom: 12 } });
header.add(
  page.text({ content: "Q4 2025 Report", style: { font_size: 9, color: theme.get_color("muted") } }),
);

const footer = page.footer_container({ style: { padding_top: 10 } });
footer.add(
  page.text({ content: "Confidential", style: { font_size: 9, color: theme.get_color("muted"), text_align: "right" } }),
);

page.add(
  page.text({ content: "Revenue Summary", style: { font_size: 22, color: theme.get_color("primary") } }),
  page.text({
    content: "**Total revenue** for Q4 was *$128,400*, representing a ***14.3% increase*** year-over-year.",
    style: { padding_top: 8 },
  }),
);

const row = page.create_container({
  layout: { type: "flex", direction: "row", justify: "space-between", gap: 16 },
  style: { background_color: "#ebf8ff", padding: 16 },
  width: page.get_content_width(),
});
row.add(
  page.text({ content: "**Revenue**\n$128,400" }),
  page.text({ content: "**Growth**\n+14.3%" }),
  page.text({ content: "**Margin**\n38.2%" }),
);
page.add(row);

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(["Bob Chen", "Designer", "Active"]);
page.add(table);

const card = page.rect({
  width: page.get_content_width(),
  height: 80,
  shape_style: { fill_color: "#276749", border_radius: 10 },
});
card.text({
  content: "**Key takeaway** - Q4 performance exceeded projections in all segments.",
  style: { color: "#ffffff", padding: 16 },
  z_index: 1,
});
page.add(card);

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

Type reference

type PageSize = "A4" | "Letter" | "Legal";

interface PageDimensions {
  width: number;
  height: number;
}

interface DocumentOptions {
  page_size?: PageSize;
  custom_dimensions?: PageDimensions;
  margin?: number | MarginValues;
  parse_markdown?: boolean;
}

interface MarginValues {
  top: number;
  right: number;
  bottom: number;
  left: number;
}

interface StyleProperties {
  font_family?: string;
  font_size?: number;
  font_weight?: string;
  font_style?: string;
  color?: string;
  background_color?: string;
  text_align?: string;
  line_height?: number;
  padding?: number;
  padding_top?: number;
  padding_right?: number;
  padding_bottom?: number;
  padding_left?: number;
}

interface ShapeStyle {
  fill_color?: string;
  stroke_color?: string;
  stroke_width?: number;
  border_radius?: number;
}

type LayoutMode = "flow" | "explicit";

interface MeasuredSize {
  width: number;
  height: number;
}

interface BoundingBox {
  x: number;
  y: number;
  width: number;
  height: number;
}

type FlexDirection = "row" | "column";
type FlexJustify = "flex-start" | "flex-end" | "center" | "space-between" | "space-around" | "space-evenly";
type FlexAlign = "flex-start" | "flex-end" | "center" | "stretch" | "baseline";

interface FlexLayoutOptions {
  type: "flex";
  direction: FlexDirection;
  justify?: FlexJustify;
  align?: FlexAlign;
  gap?: number;
}

interface FlowLayoutOptions {
  type: "flow";
  gap?: number;
}

type ContainerLayout = FlexLayoutOptions | FlowLayoutOptions;

interface CreateTextOptions {
  content: string;
  style?: StyleProperties;
  classname?: string;
  position?: VectorLike;
  width?: number;
  parse_markdown?: boolean;
  z_index?: number;
}

interface CreateRectOptions {
  style?: StyleProperties;
  shape_style?: ShapeStyle;
  position?: VectorLike;
  width?: number;
  height?: number;
  z_index?: number;
}

interface CreateImageOptions {
  name: string;
  position?: VectorLike;
  width?: number;
  height?: number;
  z_index?: number;
}

interface CreateContainerOptions {
  layout?: ContainerLayout;
  style?: StyleProperties;
  classname?: string;
  position?: VectorLike;
  width?: number;
  height?: number;
  z_index?: number;
}

interface TableCellInput {
  content: string;
  style?: StyleProperties;
}

interface CreateTableOptions {
  columns: number | number[];
  style?: StyleProperties;
  position?: VectorLike;
  width?: number;
  border_color?: string;
  border_width?: number;
  cell_padding?: number;
  header_style?: StyleProperties;
  cell_style?: StyleProperties;
  z_index?: number;
}

Vector

Vector is the spatial primitive used throughout the library for positions and sizes.

import { Vector, vector } from "@nemu-ai/pdf";

const v = vector(100, 200);
v.x; // 100
v.y; // 200
v.copy(); // new Vector(100, 200)

vector(x: number, y: number): Vector is a convenience constructor equivalent to new Vector(x, y).

Elements expose get_position() and get_size() after build(), both returning Vector.

await doc.build("output.pdf");
console.log(element.get_position()); // Vector { x: 50, y: 82 }
console.log(element.get_bbox());     // { x: 50, y: 82, width: 300, height: 24 }

Standard 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 doc = new Document({
  custom_dimensions: { width: 400, height: 600 },
});