@urvis/pdf
v0.1.3
Published
Vector-preserving PDF export for urvis scenes. No external PDF library; deflate compression uses `CompressionStream("deflate")` only.
Readme
@urvis/pdf
Vector-preserving PDF export for urvis scenes. No external PDF library; deflate
compression uses CompressionStream("deflate") only.
toPDF(pages, opts)
import { toPDF } from "@urvis/pdf";
const bytes = await toPDF([sceneRoot], { pageSize: "a4", dpi: 96 });- One physical page per input scene root.
pageSize: aPAPER_SIZESkey ("a4","a5","letter","legal","tabloid") or an explicit{ width, height }PhysicalLengthpair.MediaBoxis[0, 0, toPt(width), toPt(height)].dpi(default 96): scene pixels per inch. Each page opens with a globals 0 0 -s 0 Hpt cmflip (s = 72/dpi), so all geometry is emitted in the scene's own pixel coordinates, y-down — the scene maps straight through.
What is preserved as vectors
- Shapes —
rect → re;circle/ellipse → four kappa cubic arcs;line/polyline/polygon;path → m/l/c/h(quadratics elevated to cubics, elliptic arcs flattened to cubics). - Fills / strokes — solid colors via
rg/RG(+f/f*/S/B), any CSS color gamut-mapped into sRGB. Dashes viad; caps/joins viaJ/j. - Opacity & blend — folded group opacity and
blendModebecome deduplicated/ExtGStateentries (ca/CA/BM). - Linear & radial gradients — emitted as PDF shadings (ShadingType 2 / 3)
with a Type 3 stitching function over one Type 2 exponential segment per stop
pair.
objectBoundingBoxcoordinates are resolved against the node's local bounds. The gradient clips its geometry (W n) and paints withsh.
Images
Raster nodes are embedded as image XObjects (raw RGB FlateDecode stream plus an
8-bit gray /SMask from the alpha channel, drawn with Do) only when the
node's src is a raw-RGBA data URI:
data:image/x-urvis-rgba;base64,<uint32 LE width><uint32 LE height><RGBA bytes>Use encodeRawRgba(width, height, rgba) to produce one. This is the only image
form embeddable without a host PNG/JPEG decoder — the core image geometry
carries a src string, not pixels. Nodes whose src is a normal URL or a
data:image/png / image/jpeg URI are skipped (a host that has decoded
pixels should re-emit them through the raw-RGBA form). Decoding standard image
formats is out of scope for this package.
Text
Text nodes embed subset TrueType fonts and draw shaped glyphs:
import { toPDF } from "@urvis/pdf";
import { FontStore, HarfBuzzShaper } from "@urvis/text";
const fonts = new FontStore();
fonts.register(fontBytes);
const shaper = await HarfBuzzShaper.load(fonts);
const bytes = await toPDF([sceneRoot], { pageSize: "a4", dpi: 96, fonts, shaper });- Each
FontHandleused across all pages issubsetFonted once into a self-contained TrueType, embedded as aType0font whose descendant is aCIDFontType2(/Encoding /Identity-H,/CIDToGIDMap /Identity, so a CID is the subset glyph id)./Wwidths are the glyphs' advances in 1000-per-em units;/DWis 1000./FontFile2is the Flate-compressed subset (with/Length1). - A
/ToUnicodeCMap reverses the original font's cmap per used glyph; ligatures (no cmap preimage) get their source text from the export-time shaping alignment, so copy/extract recovers the original characters (e.g. thefiligature →f,i). - Content per line:
BT,/F<n> <sizePx> Tf,Tm = [1 0 0 -1 x baseline](the negated vertical scale un-flips the y-up glyphs against the page's y-down flip;baseline = block y + ascent), then aTJof 2-byte big-endian CIDs with kerning deviations from natural advances as thousandths-of-em adjustments. opts.fontsandopts.shaperare required for text nodes; otherwisePdfTextNeedsFontsError(naming the node id) is thrown. A glyph run shaped byCanvasShaper(font.handle === null, no glyph outlines) throwsPdfNeedsGlyphDataError— re-export with aHarfBuzzShaper.
Known gaps / fallbacks
- Conic & freeform gradients and tiled
Patternpaints have no analytic PDF form. They currently fall back to a solid fill of the first stop / first point color (patterns fall back to black). Full rasterization into an image XObject is not yet implemented. - Text: one shaped run per text node (no automatic line breaking), left-to-right
fills only; capHeight/StemV in the
/FontDescriptorare heuristic (ascent-derived and 80 respectively). - Clip rects / masks / filters on nodes are not yet emitted.
