Programmatic API
The engine every adapter calls. Use it directly when you're building a tool, a custom plugin, or a one-off script — not for in-app use, where the build-time plugins are the right surface.
When to reach for the API
The five patterns cover almost every in-app use case — the build-time plugins compute offsets once and ship flat results. Drop down to the API when you're:
- Writing a new bundler adapter (your own Webpack loader, esbuild plugin, etc.).
- Generating a static asset manifest of pre-computed offsets.
- Running the pipeline inside a Worker, Edge Function, or non-Node runtime that brings its own rasterizer.
- Investigating the pipeline interactively in a script.
Browser-safe core
The root optical-center export ships only platform-agnostic
code. Feed any RGBA buffer (canvas, OffscreenCanvas, Sharp's raw
output, ImageData) into getOpticalCenter
and apply the result to an SVG string with
transformViewBox + applyTransformToSvg.
import {
getOpticalCenter,
transformViewBox,
applyTransformToSvg,
} from 'optical-center';
// raster: any RGBA buffer of the rasterized icon
const offset = getOpticalCenter(raster);
// → { dx, dy, dxPercent, dyPercent }
const result = transformViewBox(svg, raster, offset);
// → { viewBox, breadcrumb, clipDetected }
const next = applyTransformToSvg(svg, result);
// → string with rewritten <svg> Node helpers
optical-center/node wraps the core with a rasterizer
(resvg) so you can start from an SVG string instead of
bringing your own pixel buffer. Import only when you actually need
Node-only code — pulling this into a client bundle won't work.
import { rasterizeSvg } from 'optical-center/node';
import { getOpticalCenter } from 'optical-center';
const raster = rasterizeSvg(svgString);
const offset = getOpticalCenter(raster); There is also a one-shot helper that wraps rasterize + center + transform in a single call:
import { transformViewBoxFromSvg } from 'optical-center/node';
const result = transformViewBoxFromSvg(svgString);
// → { viewBox, breadcrumb, clipDetected } End-to-end example
The flow the CLI and Vite plugin use, written out by hand. Every step is independently testable, which is why the adapters never fork the math.
import {
getOpticalCenter,
transformViewBox,
applyTransformToSvg,
} from 'optical-center';
import { rasterizeSvg } from 'optical-center/node';
const svg = '<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>';
const raster = rasterizeSvg(svg);
const offset = getOpticalCenter(raster);
const result = transformViewBox(svg, raster, offset);
const next = applyTransformToSvg(svg, result);
// next is the same string the Vite/Babel/CLI adapters would ship. Types
Every public function is fully typed. The shared interfaces:
type Offset = {
dx: number; // pixels in the rasterized image
dy: number;
dxPercent: number; // percent of the icon's viewBox width
dyPercent: number;
};
type ViewBox = {
x: number;
y: number;
w: number;
h: number;
source: 'attribute' | 'derived' | 'default';
};
type TransformResult = {
viewBox: string; // "x y w h" — drop into <svg viewBox="…">
breadcrumb: Record<string, string>; // data-* attrs for DevTools
clipDetected: boolean; // true if the shift might cut painted pixels
};