Skip to content

node-projects/layout2vector

Repository files navigation

@node-projects/layout2vector

npm version

A TypeScript (ESM) library that extracts rendered layout geometry from a live DOM — including HTML, SVG, CSS transforms, and Shadow DOM — and converts it to DXF, EMF, PDF, PNG, SVG, or HTML.

Overview

layout2vector works in three stages:

  1. DOM Extraction — Traverses the live DOM (including open Shadow DOM trees), computes stacking context order, and uses getBoxQuads() / getBoundingClientRect() for HTML geometry and SVG-native APIs (getCTM, getBBox, getTotalLength, getPointAtLength) for SVG geometry.
  2. Intermediate Representation (IR) — A flat, renderer-independent array of typed nodes (polygon, polyline, text, image) ordered by paint order, each carrying a style subset.
  3. Writers — Pluggable output backends. Built-in writers for DXF (via @tarikjabiri/dxf), EMF (Windows Enhanced Metafile), PDF (custom lightweight PDF generator), PNG (via Canvas 2D API), SVG, and HTML. Implement the Writer<T> interface to add your own.

Installation

Package: @node-projects/layout2vector

npm install @node-projects/layout2vector

Quick Start

import { extractIR, renderIR, DXFWriter, EMFWriter, PDFWriter, ImageWriter, SVGWriter, HTMLWriter } from "@node-projects/layout2vector";

// In a browser context (e.g. Playwright, Puppeteer, or a web page):
const root = document.getElementById("my-element")!;

// 1. Extract geometry from the live DOM (now async!)
const ir = await extractIR(root, {
  boxType: "border",      // "border" | "content"
  includeText: true,       // extract text node geometry
  includeInvisible: false, // skip display:none / visibility:hidden
  includeImages: true,     // enable image extraction (recommended)
});

// 2. Render to DXF
const dxfWriter = new DXFWriter({ maxY: document.documentElement.scrollHeight });
const dxfString = await renderIR(ir, dxfWriter);
// dxfString is a complete .dxf file as a string

// 3. Render to PDF
const pdfWriter = new PDFWriter(); // defaults to A4
const pdfDoc = await renderIR(ir, pdfWriter); // returns a PdfDocument
await pdfDoc.finalize();
const pdfBytes = pdfDoc.toBytes(); // Uint8Array

// 4. Render to PNG (requires Canvas-capable environment)
const imageWriter = new ImageWriter({ width: 800, height: 600 });
const imageResult = await renderIR(ir, imageWriter);
await imageResult.finalize(); // loads and draws raster images
const pngDataUrl = imageResult.toDataURL(); // data:image/png;base64,...
const pngBytes = imageResult.toBytes(); // Uint8Array
const jpegDataUrl = imageResult.toDataURL("image/jpeg", 0.92); // JPEG output

// 5. Render to SVG
const svgWriter = new SVGWriter({ width: 800, height: 600 });
const svgString = await renderIR(ir, svgWriter);
// svgString is a complete standalone SVG document

// 6. Render to HTML
const htmlWriter = new HTMLWriter({ width: 800, height: 600, customCss: ".my-class { color: red; }" });
const htmlString = await renderIR(ir, htmlWriter);
// htmlString is a complete standalone HTML document

// 7. Render to EMF (Windows Enhanced Metafile)
const emfWriter = new EMFWriter({ width: 800, height: 600 });
const emfBytes = await renderIR(ir, emfWriter); // Uint8Array → save as .emf file

API Reference

Pipeline

async extractIR(root: Element, options?: Options): Promise<IRNode[]>

Main entry point. Traverses the DOM tree under root, builds a stacking context tree, flattens to paint order, and extracts geometry from each element.

Note: extractIR is now async and must be awaited. It automatically preloads all images (including <img> and CSS background-image, even in Shadow DOM) without mutating the DOM.

Options:

Option Type Default Description
boxType "border" | "content" "border" Which CSS box to use for element quads
includeText boolean true Whether to extract text node geometry
includeImages boolean false Whether to extract <img> element content (see Image Handling)
includeInvisible boolean false Include display:none / visibility:hidden elements
zoom number 1 Scale factor applied to all extracted coordinates. Useful when the source DOM is rendered at a different zoom level
imageScale number 1 Scale factor for rasterizing embedded images. Higher values (e.g. 2) produce sharper images when zooming in on the exported file. Max pixel dimension is capped at 4096
svgToVector boolean false When true, embedded SVG images (in <img> tags and CSS background-image) are always converted to vector IR nodes (polygon, polyline, text) instead of being rasterized to bitmap image nodes. This produces resolution-independent output but may not accurately render SVGs that use fill-rule:evenodd with complex multi-subpath paths.

Note: If svgToVector is true, all embedded SVG images are vectorized, even if they use fill-rule:evenodd. This produces resolution-independent output, but may not exactly match browser rendering for complex SVGs with multiple subpaths and evenodd fill rules. By default (svgToVector: false), such SVGs are rasterized to ensure visual fidelity.

async renderIR<T>(nodes: IRNode[], writer: Writer<T>): Promise<T>

Passes each IR node through the writer in paint order. Returns a Promise for the writer's end() result. You must await the result.

Writers

DXFWriter

type DXFWriterOptions = {
  maxY?: number;  // Y-axis flip height (default: 1000)
  zoom?: number;
};
new DXFWriter(options?: DXFWriterOptions)

Produces a DXF string via @tarikjabiri/dxf. The maxY parameter (default 1000) is used to flip the Y axis (browser Y-down → DXF Y-up).

  • Polygons → closed LWPOLYLINE entities
  • Polylines → LWPOLYLINE entities (open or closed)
  • Filled SVG shapes → HATCH entities with SOLID pattern (closed polylines with fill color)
  • Text → TEXT entities
  • Rounded rectangles → LWPOLYLINE with arc-approximated corners
  • Colors → trueColor from CSS backgroundColor / color / SVG fill/stroke
  • Transparent elements (rgba alpha=0, transparent) are skipped
  • SVG images in <img> tags → converted to native DXF vector entities
  • Raster images → IMAGE entities referencing external files. Access dxfWriter.imageFiles (a Map<string, string> of path → data URL) after end() to save the referenced image files alongside the DXF

EMFWriter

type EMFWriterOptions = {
  width: number;
  height: number;
  zoom?: number;
};
new EMFWriter(options: EMFWriterOptions)

Produces a Uint8Array containing a binary Enhanced Metafile (EMF) file (Windows GDI format). Width and height define the viewport in CSS pixels.

  • Polygons → EMR_POLYGON16 entities with GDI brush/pen
  • Polylines → EMR_POLYLINE16 entities (open or closed)
  • Rounded rectangles → EMR_ROUNDRECT entities
  • Text → EMR_EXTTEXTOUTW entities with EMR_EXTCREATEFONTINDIRECTW font selection
  • Images → EMR_STRETCHDIBITS entities with 24-bit DIB pixel data
  • Colors → GDI COLORREF (0x00BBGGRR values)
  • Transparent elements are skipped
  • Output is a valid AC1015-format EMF file readable by GDI-enabled applications (Word, Visio, AutoCAD, etc.)

ImageWriter (formerly PNGWriter)

type ImageWriterOptions = {
  width: number;
  height: number;
  scale?: number;
  zoom?: number;
};
new ImageWriter(options: ImageWriterOptions)

Produces an ImageResult via the Canvas 2D API. Width and height are in CSS pixels. The optional scale parameter (default 1) acts as a device pixel ratio multiplier for higher resolution output (e.g. scale: 2 produces a 2× image).

Output format is configurable: call result.toDataURL(mimeType, quality) or result.toBytes(mimeType, quality) with "image/png" (default), "image/jpeg", or "image/webp".

Requires a Canvas-capable environment (browser document.createElement('canvas')). When using Playwright or Puppeteer, run the writer inside page.evaluate().

Note: PNGWriter, PNGResult, and PNGWriterOptions are still exported as aliases for backward compatibility.

  • Polygons → Canvas filled/stroked paths
  • Polylines → Canvas path operations (open or closed)
  • Rounded rectangles → Canvas arcTo paths
  • Gradients → Canvas createLinearGradient / createRadialGradient
  • Text → Canvas fillText with CSS font string
  • Opacity → Canvas globalAlpha
  • Transparent elements are skipped
  • Raster images → drawn via async finalize() step using Image element loading

After await renderIR(), call await result.finalize() to draw any queued raster images, then use result.toDataURL(mimeType?, quality?) for a data URL string or result.toBytes(mimeType?, quality?) for a Uint8Array. Supported MIME types: "image/png" (default), "image/jpeg", "image/webp".

SVGWriter

type SVGWriterOptions = {
  width: number;
  height: number;
  zoom?: number;
};
new SVGWriter(options: SVGWriterOptions)

Produces a standalone SVG document string. Width and height define the viewport in CSS pixels.

  • Polygons → SVG <rect> (axis-aligned with border-radius) or <path> elements
  • Polylines → SVG <path> elements (open or closed)
  • Rounded rectangles → <rect> with rx/ry attributes
  • Gradients → SVG <linearGradient> / <radialGradient> definitions with userSpaceOnUse units
  • Text → SVG <text> elements with font properties, rotation, and decoration
  • Text shadow → SVG <filter> with <feDropShadow>
  • Box shadow → SVG <filter> with <feDropShadow> (outer) or clipped inset filter
  • Opacity → SVG opacity attribute
  • Stroke dash arrays → SVG stroke-dasharray attribute
  • Images → SVG <image> elements with embedded data URLs
  • Transparent elements are skipped

The output is a self-contained SVG with all gradients, filters, and images embedded inline.

Duplicate <clipPath> definitions are deduplicated (shared by reference). Duplicate raster images are embedded once as a <symbol> and referenced via <use> elements to reduce output size.

HTMLWriter

type HTMLImageMode =
  | { type: "inline" }
  | { type: "external"; basePath: string }
  | { type: "css" };

type HTMLWriterOptions = {
  width: number;
  height: number;
  imageMode?: HTMLImageMode;
  zoom?: number;
  customCss?: string;
};

new HTMLWriter(options: HTMLWriterOptions)

Produces a standalone HTML document string. Width and height define the container dimensions in CSS pixels. The imageMode parameter controls how images are rendered:

  • { type: "inline" } (default): Images are rendered as <img src="data:..."> elements (each image in-place)
  • { type: "external", basePath }: Images are rendered as <img src="[basePath]/imageN.png"> with external file references (see htmlWriter.imageFiles after end())
  • { type: "css" }: Images are rendered as CSS background-image on <div> elements, deduplicated into shared CSS classes

The optional zoom parameter (default 1) multiplies all coordinates and dimensions (useful for high-DPI output or scaling).

  • Axis-aligned polygons → absolutely positioned <div> elements with CSS backgrounds, borders, and border-radius
  • Non-axis-aligned polygons → inline SVG <path> elements
  • Polylines → inline SVG <path> elements
  • Text → <span> elements (axis-aligned) or SVG <text> elements (rotated)
  • Gradients → CSS background-image on <div> elements
  • Box shadow → CSS box-shadow
  • Opacity → CSS opacity
  • Images → as above, depending on imageMode
  • Transparent elements are skipped

The output is a self-contained HTML document with all elements absolutely positioned to match the original layout. When imageMode.type === "css", duplicate images are deduplicated into shared CSS classes. When imageMode.type === "external", call htmlWriter.imageFiles after end() to get a Map<string, string> of image file names to data URLs for saving alongside the HTML.

PDFWriter

type PDFWriterOptions = {
  pageWidth?: number;
  pageHeight?: number;
  customFonts?: Map<string, Uint8Array>;
  defaultFont?: Uint8Array;
  zoom?: number;
};
new PDFWriter(options?: PDFWriterOptions)

Produces a PdfDocument. Page dimensions default to A4 (210×297 mm). Coordinates are converted from px to pt (×0.75). Call await doc.finalize() then doc.toBytes() to get the final PDF as a Uint8Array.

The optional defaultFont parameter accepts a TTF file as Uint8Array. When provided, any text containing characters outside the standard WinAnsiEncoding range (e.g. emoji, CJK, math symbols like ⚖) will automatically use this font with full Unicode support via CID/Type0 embedding.

  • Polygons → closed paths with fill/stroke operators (f, S, B)
  • Polylines → paths with fill/stroke operators
  • Rounded rectangles → Bézier-approximated rounded rect paths
  • Gradients → PDF shading objects (axial for linear-gradient, radial for radial-gradient)
  • Text → PDF text operators with standard font mapping (Helvetica, Times, Courier families)
  • Fill/stroke mode automatically determined from style (fill only, stroke only, or both)
  • Opacity → PDF ExtGState transparency
  • Transparent elements are skipped
  • SVG images in <img> tags → converted to native PDF vector paths
  • Raster images → embedded as JPEG XObject images via DCTDecode
  • Custom TrueType fonts → embedded as CIDFontType2 (Unicode) or simple TrueType (symbol fonts)
Embedding Custom Fonts

To use fonts beyond the standard PDF fonts (Helvetica, Times, Courier, Symbol, ZapfDingbats), pass a Map<string, Uint8Array> of font family name → TTF file data:

import { PDFWriter, parseTTF } from "@node-projects/layout2vector";
import * as fs from "node:fs";

// Load TTF files
const customFonts = new Map<string, Uint8Array>();
customFonts.set("Wingdings", new Uint8Array(fs.readFileSync("wingding.ttf")));
customFonts.set("MyFont", new Uint8Array(fs.readFileSync("myfont.ttf")));

// Create writer with custom fonts
const pdfWriter = new PDFWriter(210, 297, customFonts);
const pdfDoc = await renderIR(ir, pdfWriter);
await pdfDoc.finalize();
const pdfBytes = pdfDoc.toBytes();

The font family name in the map must match the CSS font-family used in the HTML. The library includes a minimal TrueType parser (parseTTF) that extracts the metrics needed for PDF embedding (glyph widths, ascent/descent, cmap tables). Both Unicode fonts and symbol fonts (like Wingdings) are supported.

IR Node Filtering

Invisible polygons and polylines (no fill, no stroke, no border, no boxShadow, no gradient) are automatically filtered out before rendering. This reduces output size and eliminates empty elements in HTML, SVG, PNG, PDF, and DXF outputs.

Custom Writers

Implement the Writer<T> interface:

import type { Writer, Quad, Point, Style } from "@node-projects/layout2vector";

class MyWriter implements Writer<string> {
  begin(): void { /* init */ }
  drawPolygon(points: Quad, style: Style): void { /* ... */ }
  drawPolyline(points: Point[], closed: boolean, style: Style): void { /* ... */ }
  drawText(quad: Quad, text: string, style: Style): void { /* ... */ }
  drawImage?(quad: Quad, dataUrl: string, width: number, height: number, style: Style): void { /* ... */ }
  end(): string { return "result"; }
}

Types

IRNode

A discriminated union:

type IRNode =
  | { type: "polygon"; points: Quad; style: Style; zIndex: number }
  | { type: "text"; quad: Quad; text: string; style: Style; zIndex: number }
  | { type: "polyline"; points: Point[]; closed: boolean; style: Style; zIndex: number }
  | { type: "image"; quad: Quad; dataUrl: string; width: number; height: number; style: Style; zIndex: number };

Quad

A 4-point tuple: [topLeft, topRight, bottomRight, bottomLeft], where each point is { x: number; y: number }.

For untransformed elements quads are axis-aligned rectangles. For CSS-transformed or SVG-transformed elements, quads reflect the actual rendered corners.

Style

A subset of CSS computed styles relevant to rendering:

Property Type Description
fill string? Background color / SVG fill
stroke string? Border color / SVG stroke
strokeWidth string? Border width / SVG stroke-width
fontSize string? Font size (e.g. "16px")
fontFamily string? Font family
fontWeight string? Font weight (e.g. "400", "bold")
fontStyle string? normal / italic
color string? CSS text color
opacity number? Element opacity
borderRadius string? CSS border-radius
borderTopColor, etc. string? Individual border colors
borderTopWidth, etc. string? Individual border widths
backgroundImage string? CSS background-image (gradients and url())
boxShadow string? CSS box-shadow
transform string? CSS transform

Advanced: Traversal Utilities

These are exported for advanced use cases (custom pipelines, analysis):

import {
  traverseDOM,        // Build stacking context tree
  flattenStackingOrder, // Flatten to paint order
  extractStyle,        // Extract Style from CSSStyleDeclaration
  isVisible,           // Check element visibility
  createsStackingContext, // Check if element creates stacking context
  isSVGElement,        // Check SVG namespace
  isSVGRoot,           // Check if <svg> root
  isImageElement,        // Check if <img> element
  extractImageGeometry,  // Extract image data from <img>
  hasBackgroundImage,    // Check if style has background-image url()
  extractBackgroundImage, // Extract background-image as IR nodes
} from "@node-projects/layout2vector";

Features

HTML Geometry

  • Element box quads via getBoxQuads() (with getBoundingClientRect fallback)
  • Text node geometry via Range.getClientRects()
  • Border box and content box modes

SVG Geometry

  • All shape types: rect, circle, ellipse, line, polyline, polygon, path, text
  • <use> elements: shadow DOM content is traversed for full vector extraction
  • Path sampling via getTotalLength() / getPointAtLength() (64 sample points)
  • Circle/ellipse approximation (32 segments)
  • Transform extraction via getCTM()
  • display:none and visibility:hidden SVG elements are correctly skipped

CSS Support

  • Stacking contexts: z-index, opacity, transform, filter, perspective, mix-blend-mode, will-change, contain:paint, isolation:isolate
  • Border-radius (rendered as rounded rectangles in PDF, arc-approximated polylines in DXF)
  • Background colors, border colors, font properties
  • CSS transforms reflected in quad geometry (via getBoxQuads)

Shadow DOM

  • Traverses open/declarative shadow roots (element.shadowRoot)
  • Declarative shadow DOM (<template shadowrootmode="open">) supported

Image Handling

Enable with includeImages: true. All image preloading and embedding is now automatic and handled internally by extractIR (no need to call any preload function). The DOM is not mutated during preloading.

  • All images (including <img> and CSS background-image: url(), even in Shadow DOM) are embedded as data URLs in all output formats (DXF, PDF, PNG, SVG, HTML).
  • SVG images (data:image/svg+xml, .svg URLs): automatically converted to vector geometry (polygons, polylines, text) — no rasterization.
  • Raster images (PNG, JPEG, GIF, WebP, data URLs, remote URLs): extracted as image IR nodes with embedded data URL.
  • CSS background-image: url(): SVG URLs are vector-converted; raster URLs are extracted as image nodes.
  • Data URLs: all data: schemes are supported (base64, URL-encoded, UTF-8).
  • Remote URLs: images are rasterized via canvas; cross-origin images fall back to the original src.
  • DXF output: raster images as IMAGE entities referencing external files; SVG shapes as native DXF entities with HATCH solid fills.
  • PDF output: JPEG images are embedded natively via DCTDecode; other formats are converted to JPEG automatically.
  • Caching: identical images (same source URL and dimensions) are rasterized only once per extraction run, improving performance when the same image appears on multiple elements.
  • Shadow DOM: background images and images in shadow roots are now fully supported and embedded.

Color Handling

  • Parses rgb(), rgba(), hex (#rgb, #rrggbb, #rrggbbaa)
  • Alpha-aware: fully transparent colors (rgba(0,0,0,0)) are skipped, not rendered as black
  • CSS transparent and none values handled correctly

Browser Requirements

This library runs in the browser (it needs a live DOM). Use it via:

  • Playwright / Puppeteer — inject the library into pages for headless conversion
  • Web page — import and call directly in a web app
  • Electron — use in renderer processes

The getBoxQuads() API is not supported in all browsers. For Chrome/Chromium, use the get-box-quads-polyfill:

import { addPolyfill } from "get-box-quads-polyfill";
addPolyfill(window);

Development

# Install dependencies
npm install

# Build
npm run build

# Run all tests (requires Playwright + Chromium)
npm test

# Run specific test suites
npm run test:unit
npm run test:integration
npm run test:ui
npm run test:demos

Demo Conversion

The test:demos suite loads HTML demo files from tests/demos/, extracts IR in a real Chromium browser, and writes .dxf, .pdf, .png, .svg, .html, and .emf files to tests/output/.

For GitHub-friendly browsing of the generated HTML, PDF, and preview screenshots, see tests/output/README.md.

Demo files cover: borders, gradients, transforms, SVG shapes, declarative shadow DOM, stacking contexts, typography, flexbox/grid layouts, and a comprehensive combined example.

Architecture

┌──────────────┐     ┌──────────┐     ┌────────────┐
│  Live DOM    │────>│    IR    │────>│  DXFWriter │──> .dxf
│  (browser)   │     │ IRNode[] │     └────────────┘
│              │     │          │     ┌────────────┐
│  HTML + SVG  │     │ polygon  │────>│  EMFWriter │──> .emf
│  + Shadow DOM│     │ polyline │     └────────────┘
│  + Transforms│     │ text     │     ┌────────────┐
└──────────────┘     │ image    │────>│  PDFWriter │──> .pdf
                                      └────────────┘
                                      ┌────────────┐
                                 ────>│  PNGWriter │──> .png
                                      └────────────┘
                                      ┌────────────┐
                                 ────>│  SVGWriter │──> .svg
                                      └────────────┘
                                      ┌────────────┐
                                 ────>│ HTMLWriter │──> .html
                                      └────────────┘
                                      ┌────────────┐
                                 ────>│  Custom    │──> ...
                                      └────────────┘

License

MIT

About

A converter for DOM to DXF, PDF, PNG & SVG & back to HTML

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Sponsor this project

  •  

Packages

 
 
 

Contributors