Fixed
- Contrast lint compared every text node against the slide background instead of what is actually painted behind it. Light text on a dark card was measured against the white slide and flagged as low-contrast — up to 37 false warnings on a single fixture. The check now carries an effective background through the tree walk and resolves the nearest painted ancestor. Solid fills update the comparison colour; image and gradient fills suppress the check for their subtree, since there is no meaningful ratio against a non-uniform backdrop. (#7)
- Zero-opacity fills were treated as opaque black by the contrast lint.
FlexContainer frames default to { opacity: 0, visible: true } fills, which overrode the real slide background for every descendant text node — the main source of the false warnings above. Paints with opacity === 0 are now skipped so the parent background shows through.
- A bold request could resolve onto a weight the family does not ship.
mapFontStyle and the measurement surface each encoded the same two-weight vocabulary independently and had already drifted: the clamp picked a weight the handoff then wrote as "Bold", naming a face that does not exist. Both now derive from a single NAMEABLE_WEIGHTS constant in font-normalize.mjs. (#8)
- Curved vector paths rendered as straight-line polygons wherever a node's only geometry was its
vectorNetworkBlob. The rasterizer decided line-versus-curve from the wrong field, discarding curvature while keeping the endpoints. A segment is straight if and only if all four bezier tangent components are zero. This affected stroke-only vectors — those carrying neither fillGeometry nor strokeGeometry — in both .deck and .fig, and no fixture exposed it because our own output happened to satisfy the old rule.
Changed
- Vector geometry is now written in the form the format expects. Earlier releases reflected an incomplete understanding of the
vectorNetworkBlob layout: field order was off by one word and regions carried a spurious trailing value. Coordinates happened to land correctly, so files opened and rendered, but compound shapes were not described the way they should be. Corrected from our testing, with a conformance suite added so it stays correct. Rendering is unaffected — the rasterizer reference reports are pixel-identical.
- Vector network and path-command encoding now come from
openfig-core. This package carried its own vectorNetworkBlob encoder, its own decoder, and a byte-for-byte duplicate of encodeCommandsBlob. That duplication is what allowed a wrong layout to live in one copy while the other was right — each looked self-consistent because it only ever round-tripped against itself. lib/slides/api-core.mjs now contains no raw byte manipulation at all, and decodeVnb is a thin adapter that scales coordinates and emits path commands. Verified equivalent before removal: emitted bytes identical across filled, multi-subpath and open stroked paths, and rendering unchanged at 0 of 2,073,600 pixels.
decodeVnb returns null when a blob will not parse. Previously it stopped mid-walk and emitted whatever it had read. Geometry that cannot be read in full now falls through to the caller's placeholder rather than being rendered from a partial parse.
- Requires
openfig-core ^0.4.1, up from ^0.3.8 — for the aligned encoder, the corrected sub-path grouping, and the emitRegions option that open stroked paths need.
Added
- Conformance tests for the vector network format.
test/slides/vector-network-layout.test.mjs holds the deck emitter to the layout and test/rasterizer/decode-vnb.test.mjs pins curve classification, so neither can regress silently.
npm install -g openfig-cli