@unmade/text-renderer
v1.1.0
Published
Pure function text renderer for SVG output
Readme
@unmade/text-renderer
Pure function SVG text renderer. No side effects, all dependencies injected explicitly. Web worker compatible.
Public API
import { getRenderedText } from '@unmade/text-renderer';
const svg = await getRenderedText({
text: 'HELLO',
fontUrl: 'https://example.com/font.ttf',
physicalSize: [40, 'mm'],
boxDimensions: {
width: 500,
height: 200,
physicalWidth: 100,
physicalHeight: 40,
physicalUnits: 'mm',
},
spacing: { outlineWidth: 0, letterSpacing: 0, letterSpacingOutline: 0 },
baseline: 'flat', // or 'curved', or { type: 'custom', path: '...' }
horizontalAlignment: 'centre', // 'left' | 'centre' | 'right' | 'distributed'
verticalAlignment: 'centre', // 'top' | 'centre' | 'bottom'
fillColour: { hex: '#000000' },
});Import paths
import { getRenderedText } from '@unmade/text-renderer'; // Browser
import { getRenderedText } from '@unmade/text-renderer/node'; // Node.js
import { getRenderedText } from '@unmade/text-renderer/worker'; // Web WorkerESM-only
This package is ESM-only — require('@unmade/text-renderer') will not work. This is a hard constraint: harfbuzzjs (the font shaping engine) uses top-level await for WASM initialisation, which Rollup cannot emit as CJS.
Node.js / Lambda: Node 18+ supports ESM natively, so this is not a blocker. When integrating from a CJS codebase, use dynamic import:
const { getRenderedText } = await import('@unmade/text-renderer/node');CE Lambda integration: When wiring text-renderer into the Configuration Engine Lambda, use the /node ESM entry (@unmade/text-renderer/node) from an ESM Lambda handler, or use the await import() pattern above from an existing CJS handler. The ce-adapter sub-package (@unmade/text-renderer/ce-adapter) retains a CJS build and can be require()'d normally.
Configuration Engine adapter
The ce-adapter sub-package bridges CE (Configuration Engine) editor state to renderer options:
import { extractTextPlacements, mapStateToRendererOptions } from '@unmade/text-renderer/ce-adapter';
const placements = extractTextPlacements(editor); // LoadedTextPlacement[]
const options = mapStateToRendererOptions(placements[0]); // GetRenderedTextOptions
const svg = await getRenderedText(options);extractTextPlacements reads an editor instance and returns one LoadedTextPlacement per text placement, resolving fonts, colours, and position data from the CE state.
mapStateToRendererOptions converts a LoadedTextPlacement into the GetRenderedTextOptions shape that getRenderedText accepts — including alignment normalisation (CE uses American English, TR uses British English).
Comparison system
The packages/tr-comparison package provides a pipeline for validating TR output against the CE's own rendering for real production designs.
How it works
- A design URL is POSTed to the comparison Lambda
- The Lambda loads the design via the CE factory, extracts all text placements, and renders each one with both CE and TR
- Both outputs are rasterised to PNG and diffed pixel-by-pixel with pixelmatch
- Results (match score, images, state) are stored in S3 and indexed in DynamoDB
- A browser comparison is also run via Puppeteer against the deployed demo app
- The comparison dashboard at
packages/tr-comparison/srcdisplays all results
Stored S3 artefacts
For each placement comparison:
comparisons/{designUrlHash}/{timestamp}/{placementId}/
ce.png # Config Engine render
tr.png # Text Renderer render
diff.png # Pixel difference heatmap
state.json # LoadedTextPlacement (CE state passed to the adapter)
renderer-options.json # GetRenderedTextOptions (mapped TR input)Regression tests
Regression tests live in __tests__/regression.test.ts and use fixture pairs in __tests__/fixtures/regression/:
| File | Contents |
|------|----------|
| {name}.json | Fixture metadata + LoadedTextPlacement |
| {name}.png | Reference PNG (expected TR render) |
The test pipeline for each fixture:
LoadedTextPlacement
→ mapStateToRendererOptions()
→ getRenderedText()
→ SVG → PNG (resvg-js)
→ pixelmatch diff against {name}.png
→ fail if diff > 1%Adding a fixture
- Run a comparison in the dashboard against a design URL
- Select the record and click Save as fixture in the detail panel
- Enter a descriptive name (e.g.
curved-jersey-40mm) — two files download - Place both files in
__tests__/fixtures/regression/ - Run
npm testto confirm the fixture passes, then commit
Updating fixtures after an intentional change
UPDATE_FIXTURES=true npm testThis re-renders all fixtures and overwrites the stored PNGs. Review the diffs, then commit.
Regression tests require network access to fetch fonts from their CDN URLs.
