SVG (generic ink reader) + Stylus Labs Write (.svg / .svgz)¶
Status: read support, two layers sharing one code base
(core/src/inkterop/formats/svg/):
SvgReader(format_id "svg") — a bounded generic subset of SVG 1.1, tuned so that our own SVG writer (render/svg.py) round-trips.WriteReader(format_id "write") — Stylus Labs Write documents, whose native format is plain SVG (.svgz= the same file gzipped).
Registration order matters: both claim .svg, and reader_for returns
the first detect() hit, so WriteReader must be registered before
SvgReader. (The .svg writer slot belongs to render/svg.py's
SvgWriter, unrelated to these readers.)
Generic reader scope¶
Supported [verified against hand-made fixtures]:
| Feature | Notes |
|---|---|
<path> |
commands M/m L/l H/h V/v C/c Q/q Z/z; curves flattened with a fixed 16-segment subdivision |
<polyline>, <line> |
|
| styling | presentation attributes + inline style=: stroke, stroke-width, stroke-opacity, fill, fill-opacity, opacity, stroke-linecap, color (for currentColor) |
transform |
translate / scale / matrix / rotate flattened through a stack; skewX/skewY log-skipped |
nested <svg x y> |
treated as translate(x, y) [inferred] |
<text> |
-> ir.TextBlock |
Ignored by design: <defs>, <clipPath>, CSS <style> blocks, <use>,
gradients/patterns/masks/filters, <image>; paths containing A(rc) or
any other unsupported command are skipped whole with a logged warning.
Shapes with fill but no stroke and no data-rmi-* attributes are
skipped — in the wild they are decorations, not ink [inferred].
Units [inferred]¶
Page bounds come from the root viewBox (fallbacks: width/height,
then content extent). point_scale = (root width converted to pt) /
(viewBox width), with unit conversion pt=1, px/unitless=0.75 (CSS
96 dpi), in=72, mm=72/25.4, cm=72/2.54, pc=12.
Round-tripping our own SvgWriter output [verified by test]¶
render/svg.py embeds per-stroke data-rmi-tool, data-rmi-pressure,
data-rmi-width attributes (space-separated per-point values) plus
<g data-rmi-layer> / <g data-rmi-template> groups. The reader:
- restores the tool family from
data-rmi-tooland PRESSURE fromdata-rmi-pressure; - STROKED_CONSTANT strokes are plain stroked paths; the WIDTH channel is
data-rmi-widthre-scaled so its first value equals the renderedstroke-width(the writer's unit scale is not stored)[inferred]; - STROKED_VARIABLE strokes were tessellated into a closed filled outline
(n forward points, optional 7-point round cap fan, n reversed points,
optional fan; n = data-rmi channel length): the centerline is rebuilt
as forward/reverse midpoints, per-point WIDTH as forward/reverse
distances — geometry verified against
.rmfixtures within 0.05 pt; - all-coincident strokes became
<circle>: center replicated to the channel length; data-rmi-layergroups ->ir.Layer;data-rmi-templatecontent is skipped, only the background kind survives (pitch isn't embedded).
Per-point ALPHA and TIMESTAMP/tilt channels do not survive an SVG round-trip (the writer never embeds them).
Stylus Labs Write¶
License bright line: the Write app source (github.com/styluslabs/Write)
is AGPL and was not consulted. Facts below come from the
styluslabs/templates README (which documents the page structure), and
from the bytes of Write-produced documents: styluslabs.com serves its
website pages as Write SVGs (corpus/third-party/styluslabs-write/:
site1_page002.svg, features_page002.svg, plus template
Dot grid 25.svg; provenance in corpus/third-party/MANIFEST.toml).
Structure¶
| Fact | Confidence |
|---|---|
Multi-page: root <svg id="write-document"> holding one <svg class="write-page"> per page |
[verified] in templates README + template files; site samples are single pages whose root is the page itself |
<g class="write-content write-v3"> carries page setup: width, height, xruling, yruling, marginLeft, papercolor, rulecolor |
[verified] (README + samples; width/height absent on site samples -> fall back to the page <svg>'s width/height) |
rulecolor is alpha-first #AARRGGBB (#7F0000FF matches the ruleline's stroke-opacity="0.498"); papercolor is #RRGGBB |
[verified] |
Ruling is drawn as real elements in <g class="ruleline"> (with rect.pagerect page background) — decoration, skipped as content |
[verified] |
Pen strokes: <path class="write-stroke-pen" fill="none" stroke="#RRGGBB" stroke-width="W" stroke-linecap="round" d="M x y l dx dy ..."> (absolute moveto + relative linetos) |
[verified] |
Handwritten hyperlinks: ink wrapped in <a class="hyperref">, and those paths carry no write-stroke-* class (site1: 149 classed + 27 unclassed strokes) |
[verified] |
__comx / __comy per-stroke attrs = center-of-mass bookkeeping, ignorable |
[inferred] |
Stroke classes other than write-stroke-pen (highlighter etc.) |
[unknown] — unobserved; mapped to ToolFamily.UNKNOWN |
Units = CSS px at 96 dpi -> point_scale 0.75 |
[inferred] |
| Per-point width/pressure storage | [unknown] — observed strokes are constant-width; Write may bake dynamics into geometry |
IR mapping¶
yruling>0 & xruling>0->TemplateBackground("grid"),yrulingonly ->"lines"(pitch = yruling),xrulingonly ->"unknown"; ruling gray derived fromrulecolorluminance blended by its alpha.- Non-white
papercolor(no ruling) ->ColorBackground. xruling/yruling/marginLeft/papercolor/rulecolorraw strings are kept inpage.extra["write"].write-stroke-<kind>->NativeTool("write", kind);pen->ToolFamily.PEN, everything elseUNKNOWN.
Fixtures: core/tests/fixtures/svg/ (hand-made, CC0 — write-mini.svg
is schema-by-us, not a Write export); real Write samples are
corpus-gated tests (tests/test_svg_reader.py).