← All @molecule/* packages · App templates
@molecule/api-canvas-renderUtility · canvas-render · API (Node) · v1.0.1 · Apache-2.0
Server-side canvas rendering — render a canvas-document JSON into PNG/SVG/PDF buffers.
npm install @molecule/api-canvas-render@molecule/api-canvas-render is a utility package for the API (Node) side (canvas-render).
import { renderCanvasDocument } from '@molecule/api-canvas-render'
const result = await renderCanvasDocument(
{
width: 800,
height: 600,
background: '#ffffff',
layers: [
{ kind: 'rect', x: 20, y: 20, width: 200, height: 80, fill: '#3b82f6', radius: 8 },
{ kind: 'text', x: 40, y: 70, text: 'Hello', fontSize: 32, fill: '#ffffff' },
],
},
{ format: 'png', dpi: 2 },
)
// result.buffer is a PNG/JPEG/WebP Buffer — write it to disk
// (fs.writeFile) or stream it in an HTTP response.
console.log(result.extension, result.buffer.byteLength)Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Server-side canvas rendering for molecule.dev. Takes a
CanvasDocument (width / height / background / layered shapes & text)
and returns a Buffer for PNG, SVG, or PDF.
Wraps @napi-rs/canvas for the PNG raster path; SVG and PDF are emitted
as pure strings (no native dependency required for those formats).
import { renderCanvasDocument } from '@molecule/api-canvas-render'
const result = await renderCanvasDocument(
{
width: 800,
height: 600,
background: '#ffffff',
layers: [
{ kind: 'rect', x: 20, y: 20, width: 200, height: 80, fill: '#3b82f6', radius: 8 },
{ kind: 'text', x: 40, y: 70, text: 'Hello', fontSize: 32, fill: '#ffffff' },
],
},
{ format: 'png', dpi: 2 },
)
// result.buffer is a PNG/JPEG/WebP Buffer — write it to disk
// (fs.writeFile) or stream it in an HTTP response.
console.log(result.extension, result.buffer.byteLength)
// HTTP handler — Express adapter
import { createCanvasRenderHandler } from '@molecule/api-canvas-render'
const handle = createCanvasRenderHandler()
router.post('/canvas/render', async (req, res, next) => {
try {
await handle(
{ body: req.body },
{
setHeader: (n, v) => res.setHeader(n, v),
setStatus: (s) => {
res.status(s)
},
sendBuffer: (b) => {
res.end(b)
},
sendJson: (j) => {
res.json(j)
},
},
)
} catch (err) {
next(err)
}
})
utility
npm install @molecule/api-canvas-render @napi-rs/canvas
Canvas2DContextSubset of the HTML5 2D canvas API used by the renderer. Real
@napi-rs/canvas contexts satisfy this; tests can satisfy it with
spy-backed objects.
interface Canvas2DContext {
fillStyle: string
strokeStyle: string
lineWidth: number
globalAlpha: number
font: string
textAlign: 'left' | 'center' | 'right' | 'start' | 'end'
textBaseline: 'top' | 'middle' | 'alphabetic' | 'bottom' | 'hanging' | 'ideographic'
save(): void
restore(): void
translate(x: number, y: number): void
rotate(angle: number): void
scale(x: number, y: number): void
beginPath(): void
closePath(): void
rect(x: number, y: number, w: number, h: number): void
moveTo(x: number, y: number): void
lineTo(x: number, y: number): void
ellipse(
x: number,
y: number,
rx: number,
ry: number,
rotation: number,
start: number,
end: number,
): void
fill(): void
stroke(): void
fillText(text: string, x: number, y: number): void
strokeText(text: string, x: number, y: number): void
fillRect(x: number, y: number, w: number, h: number): void
drawImage(image: any, x: number, y: number, w: number, h: number): void
loadImage?: (source: any) => Promise<any>
}
CanvasDocumentTop-level canvas document consumed by {@link renderCanvasDocument}.
interface CanvasDocument {
/** Document width in user-space units (CSS pixels at DPI 1). */
width: number
/** Document height in user-space units. */
height: number
/** Optional background color (CSS string). Omit for transparent. */
background?: string
/** Layers, drawn in array order (later = on top). */
layers: Layer[]
}
CanvasLikeThe minimal canvas surface used by the renderer.
interface CanvasLike {
getContext(kind: '2d'): Canvas2DContext
toBuffer(mimeType: 'image/png'): Buffer
}
CanvasModuleThe narrow slice of @napi-rs/canvas we depend on. Declaring it here keeps
the test seam minimal — a mock just needs createCanvas(w, h) returning
{ getContext, toBuffer }.
interface CanvasModule {
createCanvas: (width: number, height: number) => CanvasLike
}
CanvasRenderRequestMinimal request shape consumed by {@link createCanvasRenderHandler}.
interface CanvasRenderRequest {
/** Parsed JSON body — `{ doc, options }` envelope. */
body: unknown
}
CanvasRenderResponseMinimal response shape consumed by {@link createCanvasRenderHandler}.
interface CanvasRenderResponse {
/** Set a single response header. */
setHeader: (name: string, value: string) => void
/** Set the HTTP status code. */
setStatus: (status: number) => void
/** Write a binary buffer body and end the response. */
sendBuffer: (buffer: Buffer) => void
/** Write a JSON body and end the response. */
sendJson: (body: unknown) => void
}
CreateCanvasRenderHandlerOptionsOptions for {@link createCanvasRenderHandler}.
interface CreateCanvasRenderHandlerOptions {
/**
* Optional pre-flight validator. Throw to reject the request; the thrown
* error's `.message` becomes the JSON `error` field with HTTP 400.
*/
validate?: (doc: CanvasDocument, options: RenderOptions) => void | Promise<void>
/** Default suggested filename (without extension). Defaults to `'canvas'`. */
filename?: string
}
EllipseLayerAxis-aligned ellipse described by its bounding-box corner + size.
interface EllipseLayer extends ShapeStyle, Transform {
kind: 'ellipse'
x: number
y: number
width: number
height: number
}
GroupLayerGroup layer — applies its Transform to every child, then renders them
in array order (later = on top).
interface GroupLayer extends Transform {
kind: 'group'
children: Layer[]
}
ImageLayerImage element. Source is one of:
src — an http(s): URL (raster) — only honoured by PNG output.data — a data: URI (e.g. data:image/png;base64,...).buffer — a raw Buffer of PNG/JPEG bytes (encoded to data URI).Exactly one of src, data, buffer must be provided.
interface ImageLayer extends Transform {
kind: 'image'
x: number
y: number
width: number
height: number
src?: string
data?: string
buffer?: Buffer
/** MIME type when supplying `buffer`. Defaults to `image/png`. */
mimeType?: string
}
LineLayerSingle straight line segment.
interface LineLayer extends ShapeStyle, Transform {
kind: 'line'
x1: number
y1: number
x2: number
y2: number
}
PathLayerArbitrary path described by an SVG-style command string (M/L/C/Q/Z…).
interface PathLayer extends ShapeStyle, Transform {
kind: 'path'
/** SVG path data (`"M0,0 L10,10 Z"`). */
d: string
}
RectLayerAxis-aligned rectangle, optionally with rounded corners.
interface RectLayer extends ShapeStyle, Transform {
kind: 'rect'
/** Top-left X. */
x: number
/** Top-left Y. */
y: number
width: number
height: number
/** Optional uniform corner radius. */
radius?: number
}
RenderOptionsOptions accepted by {@link renderCanvasDocument}.
interface RenderOptions {
/** Output format. */
format: CanvasRenderFormat
/**
* Override the rendered width (user-space units). When set, the document is
* scaled uniformly to fit. Defaults to `document.width`.
*/
width?: number
/**
* Override the rendered height. Defaults to `document.height`.
*/
height?: number
/**
* Pixels-per-user-space-unit for raster output (PNG). Defaults to 1.
* For "retina" PNGs, pass `2`.
*/
dpi?: number
}
RenderResultResult of {@link renderCanvasDocument}.
interface RenderResult {
buffer: Buffer
/** MIME type of the buffer — `image/png`, `image/svg+xml`, `application/pdf`. */
contentType: string
/** Suggested filename extension (no leading dot). */
extension: 'png' | 'svg' | 'pdf'
}
ShapeStyleStyle fields shared by drawable layers. All colors are CSS-style strings
(e.g. "#1f2937", "rgba(0,0,0,0.5)", "transparent").
interface ShapeStyle {
/** Fill color. Omit for no fill. */
fill?: string
/** Stroke color. Omit for no stroke. */
stroke?: string
/** Stroke width in user-space units. Defaults to 1 when `stroke` is set. */
strokeWidth?: number
}
TextLayerA run of text rendered at a baseline anchor.
interface TextLayer extends Transform {
kind: 'text'
x: number
y: number
text: string
/** Font family (e.g. `"Inter"`, `"Arial"`). Defaults to `"sans-serif"`. */
fontFamily?: string
/** Font size in user-space units. Defaults to 16. */
fontSize?: number
/** Font weight (`"normal"`, `"bold"`, `100`..`900`). Defaults to `"normal"`. */
fontWeight?: string | number
/** Italic style. Defaults to `false`. */
italic?: boolean
/** Fill color. Defaults to `"#000000"`. */
fill?: string
/** Stroke color. Omit for no stroke. */
stroke?: string
/** Stroke width. Defaults to 1 when `stroke` is set. */
strokeWidth?: number
/** Horizontal alignment relative to `(x, y)`. Defaults to `"left"`. */
align?: 'left' | 'center' | 'right'
/** Vertical alignment / baseline. Defaults to `"alphabetic"`. */
baseline?: 'top' | 'middle' | 'alphabetic' | 'bottom'
}
Transform2D affine transform applied to a layer (and its children, when the layer
is a {@link GroupLayer}). Values are in the document's user-space
coordinate system. Order is translate(x, y) → rotate(rotation) → scale(sx, sy).
interface Transform {
/** Translate X (user-space units). Defaults to 0. */
x?: number
/** Translate Y (user-space units). Defaults to 0. */
y?: number
/** Rotation around the layer's local origin, in degrees. Defaults to 0. */
rotation?: number
/** Horizontal scale factor. Defaults to 1. */
scaleX?: number
/** Vertical scale factor. Defaults to 1. */
scaleY?: number
/** Opacity, 0..1. Defaults to 1. */
opacity?: number
}
CanvasRenderFormatSupported output formats. Driven by the format option on
{@link RenderOptions}.
type CanvasRenderFormat = 'png' | 'svg' | 'pdf'
LayerDiscriminated union of every layer kind.
type Layer = RectLayer | EllipseLayer | LineLayer | PathLayer | TextLayer | ImageLayer | GroupLayer
createCanvasRenderHandler(handlerOptions)Build a (req, res) => Promise<void> handler that invokes
{@link renderCanvasDocument} and streams the resulting buffer back.
function createCanvasRenderHandler(
handlerOptions?: CreateCanvasRenderHandlerOptions,
): (req: CanvasRenderRequest, res: CanvasRenderResponse) => Promise<void>
handlerOptions — Optional pre-flight validator + filename default.Returns: An async handler accepting { body } and a response shim.
loadCanvasModule()Load @napi-rs/canvas dynamically. Cached after first resolution. Tests
use {@link setCanvasModule} to inject a mock instead of requiring
vi.mock-style global rewrites.
function loadCanvasModule(): Promise<CanvasModule>
Returns: The resolved canvas module.
renderCanvasDocument(doc, options)Render a {@link CanvasDocument} into a {@link RenderResult}. The output
format is chosen by options.format.
function renderCanvasDocument(doc: CanvasDocument, options: RenderOptions): Promise<RenderResult>
doc — The canvas document.options — Format + sizing options.Returns: Buffer + content-type + extension.
renderPdf(doc, options)Render a {@link CanvasDocument} as a Buffer containing a single-page PDF.
function renderPdf(doc: CanvasDocument, options: RenderOptions): Buffer<ArrayBufferLike>
doc — Document to render.options — Output sizing options. dpi is ignored.Returns: A PDF buffer (starts with the %PDF- header).
renderPng(doc, options)Render a {@link CanvasDocument} as a PNG Buffer.
function renderPng(doc: CanvasDocument, options: RenderOptions): Promise<Buffer<ArrayBufferLike>>
doc — Document to render.options — Output sizing / DPI options.Returns: A PNG buffer (starts with the standard 89 50 4E 47 signature).
renderSvg(doc, options)Render a {@link CanvasDocument} as an SVG Buffer (UTF-8).
function renderSvg(doc: CanvasDocument, options: RenderOptions): Buffer<ArrayBufferLike>
doc — Document to render.options — Output sizing options. dpi is ignored (SVG is vector).Returns: A Buffer containing the SVG XML body.
setCanvasModule(mod)Override the canvas module used by {@link renderPng}. Pass undefined to
clear the override and revert to the real @napi-rs/canvas.
function setCanvasModule(mod: CanvasModule | undefined): void
mod — Module shim, or undefined to clear.PDF_CONTENT_TYPEconst PDF_CONTENT_TYPE: 'application/pdf'
PNG_CONTENT_TYPEconst PNG_CONTENT_TYPE: 'image/png'
SVG_CONTENT_TYPEconst SVG_CONTENT_TYPE: 'image/svg+xml'
@napi-rs/canvasThe renderer is a pure function of its inputs — no fetch, no global state,
no implicit locale handling. Locale text is the caller's responsibility:
run user-visible strings through t() before populating the
CanvasDocument. This package never displays text on its own.
Resource intensity: a high-DPI PNG of a complex document will allocate a sizeable raster. For flagship apps with unknown user input, gate the handler behind a queue / rate limiter rather than calling it inline on the hot request path.
PNG output loads the @napi-rs/canvas native addon (prebuilt binaries for
common Linux/macOS/Windows targets — nothing to apt-install for the addon
itself). Text layers, however, rasterize with the fonts installed on the
HOST: a minimal container image with no fonts renders text as empty
rectangles. Install a font package in the runtime image (e.g. fontconfig
plus a sans-serif family) when PNG output includes text, or prefer
SVG/PDF output, which embeds font-family names for the viewer to resolve.