@frank/ad-template-renderer (0.2.0)
Installation
@frank:registry=npm install @frank/ad-template-renderer@0.2.0"@frank/ad-template-renderer": "0.2.0"About this package
@frank/ad-template-renderer
The canonical deterministic renderer for final ad templates, ported from
packages/ad-template-renderer in the Blockwise repo; its only divergence is
the narrow font-cache identity fix described below. It renders an
AdTemplate from @frank/ad-template into feed (1080x1350) and story
(1080x1920) PNGs, fails closed when authored text cannot fit its box, and
produces a render audit receipt (pixel gates, perceptual duplicate-image
checks, text diagnostics) for the tool-run quality gate.
Public API (src/index.ts)
renderPlacement(input, placement),renderBoth(input)- render one or both placements.RenderInputcarriestemplate,imageValues,textValues,colourMap, optionalfontValues(font file -> bytes).TextPreflightError/TEXT_PREFLIGHT_ERROR_CODE- thrown when text cannot fit at the readability floor.auditTemplateArtifact(input)- full audit;AuditArtifactInputcarriesartifactBytes,assetsDir, optionalsourceBytes,runId,iteration.- Subpath
@frank/ad-template-renderer/text-layout:segmentGraphemes,measureTrackedTextWidth,wrapText,prepareTextLayout. - Subpath
@frank/ad-template-renderer/layer-geometry(browser-safe, no canvas):resolveGeometry,effectiveTextFontSize,imageMaskRadiusand the Fabric helpers. The renderer uses the same functions, so an editor that imports them places layers exactly where the server renders them. - Bins:
ad-template-render(CLI render),ad-template-audit(CLI audit).
Config
No environment reads. Fonts the template declares by bare file name resolve
to fonts/adbuilder/ inside this package; callers override by passing
fontValues bytes. Asset files resolve against the caller-provided
assetsDir.
Usage
import { renderBoth } from "@frank/ad-template-renderer";
const [feed, story] = await renderBoth({ template, imageValues, textValues, colourMap });
Colour resolution
The colour map a render receives is partial by contract: exactly the
document's persisted resolvedColourMap plus any explicit colourOverrides
(overrides win), and nothing else. Template semanticColours are never merged
in. A role the document does not persist (and no override supplies) stays
omitted, and the canonical per-layer fallback applies: text layers render
#000000, plate layers #FFFFFF. Apps that want template defaults must
resolve them into document.resolvedColourMap before enqueuing the request;
existing persisted empty, partial and full maps keep their original canonical
render behavior.
Shared tool execution boundary
validateTemplateRenderInput(input) parses unknown input.
executeTemplateRender(input, ports, signal?) implements the shared layered
render tool using the existing renderPlacement engine. It validates all
references before reading bytes, preserves separate placement crop overrides,
computes both PNGs before writes and checks the abort signal around each port
operation. Rejected port operations retain their original error unless the
lease was aborted. Native Vue Fabric documents receive
CREATIVE_NATIVE_EDITOR_UNSUPPORTED; native edits must use their existing
export/save adapter, never a silent layered fallback.
The ports are readAsset(ref, signal?) -> Promise<Buffer> and
writeArtifact({ scope, objectKey, mimeType, bytes }, signal?) -> Promise<ScopedAssetRef>. The result is { scope, requestId, feed, story, outputs: { feed: { objectKey, mimeType }, story: { objectKey, mimeType } } }.
Stored refs are validated for full shape, both scope components and PNG MIME.
import { executeTemplateRender } from "@frank/ad-template-renderer";
const result = await executeTemplateRender(payload, {
readAsset: readAuthorizedImmutableAsset,
writeArtifact: storeProjectArtifact,
}, context.signal); // Existing @frank/jobs handler lease signal.
Adapters must authorize project access independently, verify the job workspace fence, and preserve immutable asset versions. Store create-only artifacts; on retry, an existing object is success only after checking its content hash. Publish result pointers only after both outputs persist. Computing both first prevents deterministic render errors from writing half a pair; storage writes are not universally atomic, so a failed write can leave an unpublished orphan.
Imported font aliases include a SHA-256 of their bytes. Different versions under one filename, concurrent projects, and bundled fonts therefore retain their own glyphs in the process-wide font registry. Bundled font aliases remain unchanged. One registration is retained per distinct imported content identity; worker process lifetime still bounds registry memory.
App adapter proposals and survivor
- Blockwise
src/lib/adbuilder/save-ad.ts: replace layered render input/asset assembly with an authorized scoped request and this job handler. Preserve its revision-save and content-verified upload transaction behavior. Native saves insrc/lib/adbuilder/vue-native-save.tsand the pinned Vue Fabric editor stay on their existing validated export path. - Frank
apps/frank/lib/runs/tools.ts: retain Hermes template authoring. A validated generated pack can enqueuecreateTemplateRenderJob(request); use this same worker boundary for project previews/results.
The duplicate candidate is app-specific layered render glue; the survivor is this boundary plus the canonical renderer and shared geometry helpers. Fixture tests prove direct-engine PNG parity with real image/text/font inputs, different feed/story crops, and equal pixels through two differently scoped adapter fakes. No app glue or engine was deleted; remove it only after real consumer integration retains these checks and product save semantics. UI, customer permissions, pricing and publishing remain app-owned.
Dependencies
Dependencies
| ID | Version |
|---|---|
| @frank/ad-template | 0.2.0 |
| @napi-rs/canvas | ^1.0.3 |
Development dependencies
| ID | Version |
|---|---|
| @types/node | ^22.0.0 |
| fabric | ^7.4.0 |