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.
layout2vector works in three stages:
- 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. - Intermediate Representation (IR) — A flat, renderer-independent array of typed nodes (
polygon,polyline,text,image) ordered by paint order, each carrying a style subset. - 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 theWriter<T>interface to add your own.
Package: @node-projects/layout2vector
npm install @node-projects/layout2vectorimport { 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 fileMain 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.
Passes each IR node through the writer in paint order. Returns a Promise for the writer's end() result. You must await the result.
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
LWPOLYLINEentities - Polylines →
LWPOLYLINEentities (open or closed) - Filled SVG shapes →
HATCHentities withSOLIDpattern (closed polylines with fill color) - Text →
TEXTentities - Rounded rectangles →
LWPOLYLINEwith arc-approximated corners - Colors →
trueColorfrom CSSbackgroundColor/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 →
IMAGEentities referencing external files. AccessdxfWriter.imageFiles(aMap<string, string>of path → data URL) afterend()to save the referenced image files alongside the DXF
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_POLYGON16entities with GDI brush/pen - Polylines →
EMR_POLYLINE16entities (open or closed) - Rounded rectangles →
EMR_ROUNDRECTentities - Text →
EMR_EXTTEXTOUTWentities withEMR_EXTCREATEFONTINDIRECTWfont selection - Images →
EMR_STRETCHDIBITSentities with 24-bit DIB pixel data - Colors → GDI COLORREF (
0x00BBGGRRvalues) - Transparent elements are skipped
- Output is a valid AC1015-format EMF file readable by GDI-enabled applications (Word, Visio, AutoCAD, etc.)
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, andPNGWriterOptionsare still exported as aliases for backward compatibility.
- Polygons → Canvas filled/stroked paths
- Polylines → Canvas path operations (open or closed)
- Rounded rectangles → Canvas
arcTopaths - Gradients → Canvas
createLinearGradient/createRadialGradient - Text → Canvas
fillTextwith CSS font string - Opacity → Canvas
globalAlpha - Transparent elements are skipped
- Raster images → drawn via async
finalize()step usingImageelement 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".
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>withrx/ryattributes - Gradients → SVG
<linearGradient>/<radialGradient>definitions withuserSpaceOnUseunits - 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
opacityattribute - Stroke dash arrays → SVG
stroke-dasharrayattribute - 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.
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 (seehtmlWriter.imageFilesafterend()){ type: "css" }: Images are rendered as CSSbackground-imageon<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-imageon<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.
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 forradial-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)
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.
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.
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"; }
}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 };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.
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 |
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";- Element box quads via
getBoxQuads()(withgetBoundingClientRectfallback) - Text node geometry via
Range.getClientRects() - Border box and content box modes
- 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:noneandvisibility:hiddenSVG elements are correctly skipped
- 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)
- Traverses open/declarative shadow roots (
element.shadowRoot) - Declarative shadow DOM (
<template shadowrootmode="open">) supported
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 CSSbackground-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,.svgURLs): automatically converted to vector geometry (polygons, polylines, text) — no rasterization. - Raster images (PNG, JPEG, GIF, WebP, data URLs, remote URLs): extracted as
imageIR 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
IMAGEentities referencing external files; SVG shapes as native DXF entities withHATCHsolid 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.
- Parses
rgb(),rgba(), hex (#rgb,#rrggbb,#rrggbbaa) - Alpha-aware: fully transparent colors (
rgba(0,0,0,0)) are skipped, not rendered as black - CSS
transparentandnonevalues handled correctly
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);# 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:demosThe 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.
┌──────────────┐ ┌──────────┐ ┌────────────┐
│ 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 │──> ...
└────────────┘
MIT