InkSight
InkSight turns a pen-plotter SVG into a structured, machine-readable report: page geometry, ink totals, per-layer stats, a density grid, and plottability warnings. It is deterministic — no rendering and no model calls, so two runs on the same file agree to the digit.
Use it
npx @endonny/inksight plot.svg # StatsReport JSON on stdout
npx @endonny/inksight plot.svg --grid 16 # finer density grid
cat plot.svg | npx @endonny/inksight # or pipe it
npx @endonny/inksight diff a.svg b.svg c.svg # variability across variants
npx @endonny/inksight check plot.svg --max-cv 1.5 # gate: exit 0 pass, 1 fail
npm i -g @endonny/inksight installs it permanently. Flags: --grid N, --pen-width MM,
--saturation-threshold C, --compact, -i/--input FILE, -h/--help. Errors go to stderr
with a nonzero exit, so the JSON on stdout is always a report or nothing.
Gate on it
check turns the report into a pass/fail decision a queue can branch on without parsing
anything. Every threshold is optional; an unset one is not checked.
inksight check plot.svg --max-cv 1.5 --max-density 0.35 --no-margin-violations --max-saturated 0
Exit 0 prints {"pass":true,"failures":[]}. Exit 1 names each breached threshold with both
sides of the comparison:
{
"pass": false,
"failures": [
{ "check": "cv", "measured": 0.76, "threshold": 0.5 },
{ "check": "inkDensity", "measured": 0.2807, "threshold": 0.1 }
]
}
Checks are cv (--max-cv), inkDensity (--max-density, a ratio), marginViolations
(--no-margin-violations) and saturatedCells (--max-saturated). --report embeds the full
StatsReport under report when you want the verdict and the numbers in one call; the default
verdict stays compact so an exit-code consumer pays nothing for it. --grid, --pen-width and
--saturation-threshold apply here too.
A file that cannot be measured is not a failed gate: it exits 1 with the error on stderr and no JSON on stdout, so a broken file can never be mistaken for a measured rejection.
There is also a browser version — drop an SVG and read the same report, entirely client-side: https://chronick.github.io/inksight/
What it measures
| field | what it says |
|---|---|
page, drawable |
Physical page in mm, and the margin-clipped drawable rect inside it. |
units |
The unit declared on width/height (mm, cm, in, px, or assumed-px when the file declares none), and the resulting mm per user unit. |
penWidthMm, penWidthSource |
Effective pen width for coverage math, and whether it came from a flag, the SVG, or the default. |
scale |
Path-space → mm factor recovered from the group transform. |
totals |
Layers, paths, vertices, segments, arcLengthMm (ink drawn), penUpTravelMm (travel between paths in file order), bounding box, bboxCoverageRatio, inkDensity. |
layers[] |
Per-layer paths, vertices, arc length, ink density, and stroke color — the pen-change plan, in numbers. |
densityGrid |
Coverage per grid cell plus max, mean, and cv, the coefficient of variation that scores spatial balance (≤0.5 even, ≥1.5 clumped). |
warnings |
Paths crossing the margin clip (they will be cut off), and cells at solid-ink saturation for this pen width. |
Every length in the report is millimetres.
The shape itself is published as a JSON Schema (draft 2020-12), so an agent can validate or generate types against it instead of inferring the shape from one example:
import schema from "@endonny/inksight/schema.json" with { type: "json" };
additionalProperties is false at every level, and a test validates a real analyzeSvg run
against the schema on every build — so a field that appears, disappears, or changes type fails
CI rather than reaching you as a surprise.
Library usage
import { analyzeSvg } from "inksight";
import { readFileSync } from "node:fs";
const report = analyzeSvg(readFileSync("plot.svg", "utf-8"), { grid: 16 });
console.log(report.totals.arcLengthMm, report.densityGrid.cv);
if (report.warnings.marginViolationPaths > 0) throw new Error("geometry escapes the margin");
inksight/report exports the presentation layer the web page uses — buildReportModel(report)
turns a StatsReport into labelled, formatted rows, a heatmap model, and warning items, with no
DOM dependency.
Scope
v1 measures polyline SVGs: absolute M/L paths under a translate(cx,cy) scale(S) group, sized
by a viewBox plus width/height. Files declaring mm, cm, in or px are converted to
millimetres; a file with no unit is read as CSS px at 96dpi and the units field reports
assumed-px, so a guess is never disguised as a measurement.
Curves (C/Q/A/…), relative commands, closepath, and nested transforms are rejected with a clear error rather than silently mis-measured. Plot-time estimation and pen-travel optimization modelling are deliberately out of scope — that is vpype-grade work.
Works with
Any generator that exports polyline SVGs in the shape above. hatch3d is one, and its exports are what the bundled example is rendered from.
License
MIT