MCPcopy Index your code
hub / github.com/cosmiciron/vmprint

github.com/cosmiciron/vmprint @draft2final-v1.0.1

Chat with this repo
repository ↗ · DeepWiki ↗ · release draft2final-v1.0.1 ↗ · + Follow
3,210 symbols 10,315 edges 145 files 37 documented · 1%
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

# VMPrint :: A deterministic, zero-DOM typesetting engine/VM in pure TypeScript.

VMPrint is not another HTML-to-PDF wrapper. Instead, it is the crucial missing primitive you might need if you want to build one on your own -- from scratch.

If you want to build a custom edge-rendering pipeline, a better document generator, or an entirely new word processor without resorting to contenteditable hacks, hidden iframes, or sloppy DOM overlays -- VMPrint provides the low-level mathematical infrastructure to make it happen.

It is a compact, zero-dependency layout Virtual Machine that completely bypasses the browser's HTML/CSS box model. Using interval-arithmetic and a custom morphable-box architecture, it calculates complex print layouts natively. It handles multi-column text wrapping, cross-gutter floats, strict baseline grids, and multilingual line-breaking directly from a semantic JSON AST, outputting a flat array of absolute X/Y coordinates.

Because it operates purely on math and carries zero DOM dependencies, it runs identically everywhere: Cloudflare Workers, Lambda, Bun, Deno, or directly in the browser. It provides the missing primitives for true programmatic document layout.

To see what this engine can build, view the beautifully typeset PDF version of this README. It was compiled directly from this Markdown source file using draft2final, a fully-featured manuscript and screenplay compiler built entirely on top of the VMPrint API.


VMPrint manifesto

The above image -- including annotations, measurement guides, legends, and script direction markers -- are all rendered entirely within VMPrint. The source documents can be found in the repository under /documents/readme-assets/.

Features at a Glance

  • Deterministic Layout Engine: Generates bit-for-bit identical layout across operating systems and runtimes. No more layout drift.
  • Zero Environment Dependencies: The core engine requires no headless browser, DOM, or Node.js built-ins. Runs seamlessly in browsers, Node.js, and edge environments like Cloudflare Workers and AWS Lambda.
  • True Glyph-Based Measurement: Reads intrinsic OpenType advance widths and kerning pairs from font files. Layout relies on absolute typographic math, not browser approximations.
  • Fast Performance: Renders complex, multi-page layouts in milliseconds. Global caches for glyph metrics and text segmentation ensure high throughput for batch pipelines.
  • Multi-Column & Mixed Layouts: Native support for DTP-style multi-column story regions. Seamlessly mix single-column headers, three-column articles, and pull-quotes on the same page. Floating obstacles naturally shape text across multiple column boundaries.
  • Advanced Pagination & Features: Floating elements, drop caps, widow/orphan control, and cross-page continuation markers.
  • Header and Footer Regions: Top-level header / footer document regions with page-selector support (default, firstPage, odd, even). Per-page suppression via element pageOverrides. Region content automatically clipped to margin bounds. See Header and Footer Architecture for full specification.
  • Complex Table Support: First-class handling for tables that span multiple pages, including smart row splitting, colspan, rowspan, and automatically repeated headers.
  • Publishing-Grade Typography: Grapheme-accurate line breaking using Intl.Segmenter, language-aware hyphenation, and mixed-script text runs with perfect baseline alignment.
  • JSON-Based Layout Pipeline: Layout output is a serializable object tree. Pre-compile layouts into JSON to rapidly distribute identical layouts that render instantly at runtime, snapshot them for CI regression testing, or inspect exact sub-point glyph measurements.
  • Pluggable Architecture: Swappable font managers and rendering backends (PDF provided). Easily extensible to SVG, Canvas, or custom contexts.
  • Markdown Transmutation: Standalone transmuters (@vmprint/transmuter-mkd-mkd, @vmprint/transmuter-mkd-academic, @vmprint/transmuter-mkd-literature, @vmprint/transmuter-mkd-manuscript, @vmprint/transmuter-mkd-screenplay) convert Markdown to VMPrint's DocumentInput AST — usable in browser, Node.js, or edge environments without touching the layout engine. Decouples source format from layout pipeline.
  • Source-to-PDF/AST (draft2final): Thin transmuter-first orchestration that auto-selects transmuters (via CLI/frontmatter), loads config/theme defaults, and outputs either PDF or AST JSON.

VMPrint manifesto

Baselines, shaping, and directionality remain stable across mixed-language content. The above image -- including annotations, measurement guides, legends, and script direction markers -- are all rendered within VMPrint. The source documents can be found in the repository under /documents/readme-assets/.

Background

In the 1980s and 90s, serious software thought seriously about pages. TeX got hyphenation and justification right. Display PostScript gave NeXT workstations a real imaging model — every application had access to typographically precise, device-independent layout at the OS level. Desktop publishing software understood widows, orphans, and the subtle difference between a line break and a paragraph break.

Then the web happened. Mostly great. But somewhere along the way, "generate a PDF" became either "run a headless Chromium instance" or "write your own pagination loop against a low-level drawing API." Neither of these is good. The thinking that went into document composition — the kind that made TeX and PostScript genuinely good — largely disappeared from the toolkit of the working developer.

VMPrint is an attempt to recover some of what was lost.

The Core Idea

VMPrint works in two stages, and keeping them separate is the whole point.

Stage 1 — Layout. You give it a document: structured JSON, or source text via draft2final (through transmuters producing DocumentInput). It measures glyphs, wraps lines, handles hyphenation, paginates tables, controls orphans and widows, places floats. It produces a Page[] stream — an array of pages, each containing a flat list of absolutely-positioned boxes.

Stage 2 — Rendering. A renderer walks those boxes and paints them to a context. Today that context is a PDF. Tomorrow it could be canvas, SVG, or a test spy.

The Page[] stream is the thing that makes this different. It's serializable JSON. You can diff it between versions. You can snapshot it for regression tests. You can inspect it to understand why something ended up where it did. Layout bugs become reproducible. This is not how PDF generation usually works.

Layout is based on real font metrics. VMPrint loads actual font files, reads glyph advance widths and kerning data, and measures text the way a typesetting system does — not the way a browser estimates it from computed styles. There is no CSS box model underneath. Same font files, same input, same config: identical output, down to the sub-point position of every glyph.

const engine = new LayoutEngine(config, runtime);
await engine.waitForFonts();

// pages is a plain Page[] — inspect it, snapshot it, diff it
const pages = engine.paginate(document.elements);

const renderer = new Renderer(config, false, runtime);
await renderer.render(pages, context);

Identical Output, Everywhere

The core engine has no dependency on Node.js, the browser, or any specific JavaScript runtime. It doesn't call fs. It doesn't touch the DOM. It doesn't assume Buffer exists. Font loading and rendering are injected through well-defined interfaces — the engine itself is pure, environment-agnostic JavaScript.

In practice: run VMPrint in a browser extension, a Cloudflare Worker, a Lambda, and a Node.js server. The layout output is identical. Same page breaks. Same line wraps. Same glyph positions. The rendering context changes; the layout does not.

This is not a promise about "should work in theory." It's an architectural constraint that was enforced from the beginning.

Why Not Just Use...

Headless Chrome / Puppeteer: Works great until it doesn't. Cold starts are slow. Output drifts across browser versions. Edge runtimes typically can't run it at all. You're maintaining a Chromium dependency to produce text in a box — and Chromium is ~170 MB on disk. VMPrint's full dependency tree, including the font engine that makes real glyph measurement possible, is ~2 MiB packed and ~8.7 MiB unpacked.

PDFKit / pdf-lib / react-pdf: You're writing pagination. "If this paragraph doesn't fit, cut here, carry the rest to the next page" — by hand, for every element type, including tables that span pages and headings that must stay with what follows them.

LaTeX: Genuinely excellent at what it does. Also requires a TeX installation, a 1970s input format, and an afternoon of fighting package conflicts.

VMPrint handles the pagination. You describe your document. It figures out where things break.

How It Started

I'm a film director. I hated writing in screenplay software, so I started writing in plain text. Then I wrote a book in Markdown and wanted industry-standard manuscript output — and found no tool I trusted to get there without pain.

Low-level PDF libraries made me implement my own pagination. Headless browser pipelines were heavy and unpredictable. So I took a detour and built a layout engine first.

The manuscript is still waiting. The engine shipped instead.

Getting Started

Prerequisites: Node.js 18+, npm 9+

git clone https://github.com/cosmiciron/vmprint.git
cd vmprint
npm install

Render a JSON document to PDF:

npm run dev --prefix cli -- --input engine/tests/fixtures/regression/00-all-capabilities.json --output out.pdf

Source-to-PDF (screenplay transmuter):

npm run dev --prefix draft2final -- samples/draft2final/source/screenplay/screenplay-sample.md --as screenplay --out screenplay.pdf

Full API Example

import fs from 'fs';
import { LayoutEngine, Renderer, toLayoutConfig, createEngineRuntime } from '@vmprint/engine';
import { PdfContext } from '@vmprint/context-pdf';
import { LocalFontManager } from '@vmprint/local-fonts';

const runtime = createEngineRuntime({ fontManager: new LocalFontManager() });
const config = toLayoutConfig(documentInput);
const engine = new LayoutEngine(config, runtime);

await engine.waitForFonts();
const pages = engine.paginate(documentInput.elements);

const context = new PdfContext({
  size: [612, 792],
  margins: { top: 0, right: 0, bottom: 0, left: 0 },
  autoFirstPage: false,
  bufferPages: false
});

// Wire the context output to a Node.js write stream.
// The caller owns I/O; the context owns rendering.
const fileStream = fs.createWriteStream('output.pdf');
context.pipe({
  write(chunk) { fileStream.write(chunk); },
  end()        { fileStream.end(); },
  waitForFinish() {
    return new Promise((resolve, reject) => {
      fileStream.once('finish', resolve);
      fileStream.once('error',  reject);
    });
  }
});

const renderer = new Renderer(config, false, runtime);
await renderer.render(pages, context);

To produce a PDF with no embedded fonts — using only the 14 standard PDF fonts that every viewer guarantees — swap in StandardFontManager:

import { StandardFontManager } from '@vmprint/standard-fonts';

const runtime = createEngineRuntime({ fontManager: new StandardFontManager() });

The rest of the pipeline is identical. The engine detects the sentinel buffers that StandardFontManager returns and bypasses fontkit in favor of built-in AFM metrics. The PDF output carries font name references only — no binary font data. See font-managers/ for details.

What It Can Do

Pagination & Layout - Desktop Publishing (DTP) style multi-column story regions with adjustable gutters - Seamless mixed-layout pages (e.g., full-width headers flowing directly into 3-column articles) - keepWithNext, pageBreakBefore, orphan and widow controls - Tables that span pages: colspan, rowspan, row splitting, repeated header rows - Drop caps and continuation markers when content splits across pages - Story zones with text wrapping around complex floating obstacles (even spanning across columns) - Inline images and rich objects on text baselines

Typography and Multilingual

Most libraries treat international text as an optional concern — get ASCII layout working first, bolt on Unicode support later. VMPrint's text pipeline was built correctly from the start, because the alternative produces subtly wrong output for most of the world's writing systems.

  • Text segmentation uses Intl.Segmenter for grapheme-accurate line breaking. A grapheme cluster spanning multiple Unicode code points is always treated as a single unit.
  • CJK text breaks correctly between characters, without needing spaces.
  • Indic scripts are measured and broken as grapheme units, not codepoints.
  • Language-aware hyphenation applies per text segment, so a document mixing English and French body text hyphenates each according to its own rules.
  • Mixed-script runs — Latin with embedded CJK, inline code within prose — share the same baseline and are measured correctly across font boundaries.
  • Two justification modes: space-based (standard for Latin) and inter-character (standard for CJK and some print conventions).

VMPrint manifesto

VMPrint manifesto

VMPrint manifesto

Multilingual Rendering. The images above — including all annotations, measurement guides, legends, and script direction markers — are rendered entirely by VMPrint. Source document can be found in the r

Extension points exported contracts — how you extend this code

PackagerUnit (Interface)
(no doc) [14 implementers]
engine/src/engine/layout/packagers/packager-types.ts
VmprintOutputStream (Interface)
(no doc) [6 implementers]
contracts/src/context.ts
FormatContext (Interface)
(no doc) [2 implementers]
markdown-core/src/context.ts
Window (Interface)
(no doc)
docs/examples/ast-to-pdf/src/pipeline.ts
NormalizedEmbeddedImage (Interface)
(no doc)
engine/src/engine/image-data.ts
Context (Interface)
(no doc) [8 implementers]
contracts/src/context.ts
ElementStyle (Interface)
(no doc)
markdown-core/src/types.ts
Window (Interface)
(no doc)
docs/examples/ast-to-pdf/src/entries/vmprint-standard-fonts-browser.ts

Core symbols most depended-on inside this repo

push
called by 1070
docs/examples/mkd-to-ast/assets/vmprint-transmuter.js
slice
called by 352
docs/examples/mkd-to-ast/assets/vmprint-transmuter.js
opacity
called by 289
contracts/src/context.ts
restore
called by 246
contracts/src/context.ts
save
called by 243
contracts/src/context.ts
consume
called by 227
docs/examples/mkd-to-ast/assets/vmprint-transmuter.js
stroke
called by 204
contracts/src/context.ts
lineTo
called by 188
contracts/src/context.ts

Shape

Function 1,996
Method 1,055
Class 100
Interface 59

Languages

TypeScript100%

Modules by API surface

docs/examples/ast-to-pdf/assets/vmprint-context-pdf-lite.js904 symbols
docs/examples/mkd-to-ast/assets/vmprint-transmuter.js687 symbols
docs/examples/ast-to-pdf/assets/vmprint-engine.js384 symbols
engine/tests/flat-pipeline.spec.ts76 symbols
engine/tests/harness/engine-harness.ts47 symbols
engine/src/engine/layout/packagers/story-packager.ts39 symbols
contexts/pdf-lite/src/index.ts39 symbols
engine/src/engine/layout/text-processor.ts38 symbols
contracts/src/context.ts38 symbols
engine/src/engine/layout/layout-core.ts36 symbols
engine/src/engine/document.ts36 symbols
markdown-core/src/context.ts33 symbols

For agents

$ claude mcp add vmprint \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact