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.

core.ts ts
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.

node-helpers.ts ts
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:

node-oneshot.ts ts
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.

end-to-end.ts ts
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:

types.ts ts
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
};