@pixldocs/canvas-renderer
v0.5.210
Published
Client-side template renderer for Pixldocs — React component + imperative API for rendering templates in any web app
Downloads
15,002
Readme
@pixldocs/canvas-renderer
Client-side template renderer for Pixldocs — render templates in any web app with 1:1 visual parity with the Pixldocs editor. No server round-trip needed for previews.
Installation
npm install @pixldocs/canvas-renderer fabric react react-domPublic package — no auth token or
.npmrcneeded.
Quick Start: Build an App From a Workspace
Got a Pixldocs workspace full of published templates (e.g. social-media posts, invitations, certificates)? You can spin up a standalone app that lists and renders them with just the workspace ID + Pixldocs anon key — no per-template wiring needed.
import {
listPublishedTemplates,
PixldocsRenderer,
} from '@pixldocs/canvas-renderer';
const SUPABASE_URL = 'https://ttvtjhxjxuxdeybcnjkd.supabase.co';
const SUPABASE_ANON_KEY = 'eyJ...'; // Pixldocs publishable key (safe in browser)
const WORKSPACE_ID = 'your-workspace-uuid';
// 1. List all published templates owned by a workspace
const templates = await listPublishedTemplates({
workspaceId: WORKSPACE_ID,
supabaseUrl: SUPABASE_URL,
supabaseAnonKey: SUPABASE_ANON_KEY,
// category: 'social-media', // optional filter
});
// templates: [{ id, name, thumbnail_url, category, price, ... }, ...]
// 2. Render a chosen template (form-bound or static)
const renderer = new PixldocsRenderer({
supabaseUrl: SUPABASE_URL,
supabaseAnonKey: SUPABASE_ANON_KEY,
});
const pages = await renderer.renderFromForm({
templateId: templates[0].id,
sectionState: { /* user inputs (optional for static templates) */ },
});Two ways to source templates
| Approach | Best for | API |
|----------|----------|-----|
| By workspace | Multi-template apps (template gallery, social-media post maker, certificate maker) — pick from a curated workspace | listPublishedTemplates({ workspaceId }) |
| By form schema | Single-purpose apps where one form drives many template variants (e.g. BioMaker — one form, many biodata designs) | Resolve directly via renderFromForm({ templateId, formSchemaId, sectionState }) |
Both work with the same anon key and the same renderer — choose whichever matches your product shape.
Templates Catalog API
listPublishedTemplates(options)
Returns every published template belonging to a workspace. Uses public RLS (no auth needed beyond the anon key).
const templates = await listPublishedTemplates({
workspaceId: 'uuid',
supabaseUrl: '...',
supabaseAnonKey: '...',
category: 'social-media', // optional
limit: 200, // optional (default 200)
offset: 0, // optional pagination
});Each item: { id, name, description, category, thumbnail_url, preview_images, price, download_count, workspace_id, sort_order, created_at, updated_at }.
getPublishedTemplate({ templateId, supabaseUrl, supabaseAnonKey })
Fetch a single published template's catalog row (without the heavy config
JSON — use the renderer for that).
Two Rendering Approaches
Approach A: Live Canvas (interactive)
Renders a visible <canvas> element in your page. Best for editors, interactive tools, or when you need real-time canvas manipulation.
import { PixldocsPreview } from '@pixldocs/canvas-renderer';
<PixldocsPreview
templateId="your-template-id"
formSchemaId="your-schema-id"
sectionState={sectionState}
supabaseUrl="https://xxx.supabase.co"
supabaseAnonKey="eyJ..."
pageIndex={0}
onReady={() => console.log('Canvas ready')}
/>Pros: Real-time updates, no re-render flicker
Cons: Heavier DOM footprint, canvas stays in memory
Approach B: Hidden Canvas → Image (recommended for previews)
Renders off-screen using a hidden canvas, captures the result as a data URL, and displays it as a lightweight <img>. Best for preview panels, template pickers, and multi-page views.
import { useState, useEffect } from 'react';
import { PixldocsRenderer } from '@pixldocs/canvas-renderer';
const renderer = new PixldocsRenderer({
supabaseUrl: 'https://xxx.supabase.co',
supabaseAnonKey: 'eyJ...',
});
function TemplatePreview({ templateId, formSchemaId, sectionState, themeId }) {
const [pages, setPages] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
let cancelled = false;
async function render() {
setLoading(true);
try {
const results = await renderer.renderFromForm({
templateId,
formSchemaId,
sectionState,
themeId,
});
if (!cancelled) setPages(results);
} catch (err) {
console.error('Render failed:', err);
} finally {
if (!cancelled) setLoading(false);
}
}
render();
return () => { cancelled = true; };
}, [templateId, formSchemaId, sectionState, themeId]);
if (loading) return <div>Generating preview...</div>;
return (
<div>
{pages.map((page, i) => (
<img key={i} src={page.dataUrl} width={page.width} height={page.height} alt={`Page ${i + 1}`} />
))}
</div>
);
}Pros: Lightweight DOM (just <img> tags), canvas is disposed after capture, fast for multi-page
Cons: Not interactive, requires re-render to update
Approach C: Vector PDF Export
Renders all pages as SVGs and assembles them into a vector PDF using jsPDF + svg2pdf.js. Best for print-quality exports.
import { PixldocsRenderer } from '@pixldocs/canvas-renderer';
const renderer = new PixldocsRenderer({
supabaseUrl: 'https://xxx.supabase.co',
supabaseAnonKey: 'eyJ...',
});
// From form data (V2 sectionState)
const { blob, totalPages } = await renderer.renderPdfFromForm({
templateId: 'your-template-id',
formSchemaId: 'your-schema-id',
sectionState: { section_abc: { name: 'John' } },
title: 'My Document',
});
// Download the PDF
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'document.pdf';
a.click();
URL.revokeObjectURL(url);
// Or from a pre-resolved config
const result = await renderer.renderPdf(templateConfig, { title: 'My Doc' });Pros: Vector output (infinite zoom), small file size, print-quality
Cons: No interactivity, requires jsPDF/svg2pdf.js (bundled as dependencies)
API Reference
Form schema shape (V2 sectionState)
sectionState accepts entries for both repeatable sections and
repeatable pages. Both look the same on the wire — an array of entry
objects keyed by the section/page id:
sectionState = {
// single section
personal: { name: 'Jane' },
// repeatable section (e.g. Experience)
experience: [
{ title: 'Designer', company: 'Acme' },
{ title: 'Lead', company: 'Beta' },
],
// repeatable PAGE (e.g. Photo Page) — same shape as above, but at render
// time the bound template page is cloned once per entry.
photo_page: [
{ caption: 'Cover' },
{ caption: 'Back' },
],
}Fetch the schema from the Pixldocs form API to discover what keys/fields are
expected. The response exposes repeatable pages as a dedicated
schema.repeatablePages[] array (each entry mirrors a repeatable section:
id, label, order, templateKeyPrefix, minEntries, maxEntries,
entryFields).
const res = await fetch(
`${SUPABASE_URL}/functions/v1/form-api?form_schema_id=${SCHEMA_ID}&action=schema`,
{ headers: { apikey: SUPABASE_ANON_KEY } },
).then((r) => r.json());
res.schema.sections // single + repeatable sections
res.schema.repeatablePages // repeatable pages (clone-per-entry)PixldocsPreview (React Component)
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| config | TemplateConfig | — | Pre-resolved template config (Mode 1) |
| templateId | string | — | Template UUID (Mode 2) |
| formSchemaId | string | — | Form schema UUID (Mode 2) |
| sectionState | SectionFormState | — | V2 section state data (Mode 2) |
| themeId | string | 'default' | Theme variant ID |
| supabaseUrl | string | — | Supabase URL (Mode 2) |
| supabaseAnonKey | string | — | Supabase anon key (Mode 2) |
| pageIndex | number | 0 | Page index to render |
| zoom | number | auto-fit | Zoom/scale factor |
| imageProxyUrl | string | — | CORS proxy URL for external images |
| onReady | () => void | — | Called when rendering completes |
| onError | (error: Error) => void | — | Called on error |
PixldocsRenderer (Imperative API)
const renderer = new PixldocsRenderer({
supabaseUrl: string,
supabaseAnonKey: string,
imageProxyUrl?: string, // CORS proxy for external images
pixelRatio?: number, // default: 2
assetWaitTimeoutMs?: number, // default: 15000; use ~30000 for multi-photo PDF exports
assetWaitEarlyExitMs?: number, // default: 1500; ignored for strict PDF image-ID waits
maxImageEdgePx?: number, // auto: 2048 for user-uploaded PDF photos; 0 disables
debug?: boolean, // logs image wait/SVG completeness diagnostics
});For BioMaker-style PDFs with user-uploaded profile/gallery photos, use the live
SVG path and do not set forcePerElementPdf: true. Large data:image/*
photos are normalized before the hidden Fabric canvas mounts (default cap:
2048px edge and ~2MB encoded data URL) so svg2pdf does not corrupt/drop
multi-megapixel uploads.
renderer.renderFromForm(options)
Renders all pages from a V2 sectionState payload (same format as the server /render-from-form API).
const results = await renderer.renderFromForm({
templateId: 'uuid',
formSchemaId: 'uuid',
sectionState: { section_abc: { name: 'John' }, section_def: [...] },
themeId: 'default', // optional
format: 'png', // 'png' | 'jpeg' | 'webp'
quality: 0.92, // for jpeg/webp
scale: 1, // scale multiplier
});
// Returns: RenderResult[] — one per page
// Each: { dataUrl, width, height, pixelWidth, pixelHeight }renderer.render(config, options?)
Renders a single page from a pre-resolved template config.
const { dataUrl, width, height } = await renderer.render(templateConfig, { pageIndex: 0 });renderer.renderAllPages(config, options?)
Renders all pages from a pre-resolved config.
const pages = await renderer.renderAllPages(templateConfig);Data Resolution Utilities
Resolve template data without rendering — useful for inspecting configs or building custom pipelines.
import { resolveFromForm, resolveTemplateData } from '@pixldocs/canvas-renderer';
// V2: resolve from sectionState
const { config, totalPages } = await resolveFromForm({
templateId: 'uuid',
formSchemaId: 'uuid',
sectionState: { ... },
supabaseUrl: '...', supabaseAnonKey: '...',
});
// Legacy: resolve with flat formData
const { config } = await resolveTemplateData({
templateId: 'uuid',
formData: { name: 'Jane' },
supabaseUrl: '...', supabaseAnonKey: '...',
});Peer Dependencies
fabric^6.0.0react^18.0.0 || ^19.0.0react-dom^18.0.0 || ^19.0.0
License
UNLICENSED — Private package for Pixldocs ecosystem only.
Preview Blur (anti-screenshot)
Set previewBlur: true on any text or image element in your config to redact
it in watermarked previews — useful for protecting paid content (e.g. biodata
reference rows) from screenshot theft.
The element renders normally underneath, and a translucent frosted-glass
overlay rectangle is appended on top at the element's absolute bounds, so
layout/wrap stays identical but the content is visually obscured. The
overlay auto-tints (light glass on dark text, dark glass on light text) and
is rendered with no stroke, no shadow, and square corners so it sits flush
against neighboring content. In the live web preview the overlay is upgraded
to a CSS backdrop-filter: blur() rectangle for a real frosted-glass effect;
exported PNG/PDF use the translucent rect (the closest pure-vector
approximation).
Preview-blur is gated identically to the watermark: it only runs when
watermark === true (or, by default, when template.price > 0 and the user
hasn't paid). Clean / paid downloads always render the original content.
Live backdrop-filter overlay in <PixldocsPreview>
The React <PixldocsPreview> component automatically renders a real CSS
backdrop-filter: blur() frosted-glass overlay on top of every element
marked previewBlur: true. Bounds are auto-derived from the config
(groups respected), so no extra wiring is required.
Defaults: blur(5px) saturate(130%) with a rgba(255,255,255,0.12) tint,
square corners, no stroke, no shadow — identical to the in-app preview on
pixldocs.com.
Opt-out or tune via props:
<PixldocsPreview
config={config}
// disable entirely
frostedBlur={false}
// or tune
frostedBlurOptions={{ blurPx: 6, saturatePct: 140, background: 'rgba(255,255,255,0.15)' }}
/>The imperative PNG / PDF export paths still use the static translucent rectangle (the closest pure-vector approximation), unchanged.
// In your template config
{
id: 'reference-1-name',
type: 'text',
text: 'John Doe',
previewBlur: true, // ← redacted in preview, full in paid download
...
}No API change is required on the consumer side — the same renderFromForm,
renderPdfFromForm, etc. respect the flag automatically.
Example: per-field blur for a biodata form (use page)
On pixldocs.com's Use page for the Biodata preset, every form field renders a small 🔒 Blur in preview checkbox next to it. Toggling it redacts only that field's text in the watermarked preview — perfect for hiding reference rows, phone numbers, or addresses behind the paywall while leaving the rest of the biodata visible. Paid / clean downloads always render the original content.
Under the hood the flow is:
- Keep a
Set<string>of "blurred field ids" in component state — each id encodes the field's path (single field, repeatable entry, or nested repeatable entry), e.g.__bioPreviewBlur__:nested:family:1:siblings:2:name. - Resolve each blurred field id to the exact cloned element ids in the paginated config (a single repeatable field may map to N cloned elements, one per entry).
- Pass that set to
injectPreviewBlurasextraElementExactIds(orextraElementBaseIdsif you only know the source/base id), then render the returned config like any other.
import { injectPreviewBlur } from '@pixldocs/canvas-renderer';
// `blurredElementIds` = the resolved Set<string> of exact cloned element ids
// you built from the user's checkbox state.
const previewConfig = injectPreviewBlur(paginatedConfig, {
extraElementExactIds: blurredElementIds,
});
// Then render `previewConfig` with <PixldocsPreview config={previewConfig} />
// or `renderer.renderAllPages(previewConfig)`. The live React preview will
// additionally upgrade the static rectangles to real CSS
// `backdrop-filter: blur()` overlays automatically.This is exactly how the Biodata preset on pixldocs.com wires its per-field 🔒 checkboxes — no template authoring required, the user picks at runtime which fields stay visible in the watermarked preview.
Shortcut: pass blur field ids straight to <PixldocsPreview> (v0.5.186+)
If you're using the React <PixldocsPreview> component (rather than the
imperative renderer), you don't need to call injectPreviewBlur yourself.
Just pass the field ids directly as a prop:
import { PixldocsPreview } from '@pixldocs/canvas-renderer';
// Source / base ids straight from your form schema.
// Every cloned instance across pages + repeatable entries is auto-matched.
const blurFieldIds = ['text-reference-name', 'text-phone', 'text-address'];
<PixldocsPreview
templateId={templateId}
formSchemaId={formSchemaId}
sectionState={sectionState}
supabaseUrl={SUPABASE_URL}
supabaseAnonKey={SUPABASE_ANON_KEY}
blurFieldIds={blurFieldIds}
/>blurFieldIds: matched by base id — cloned suffixes (_1,_2_3, …) are stripped before comparison, so one entry blurs every clone of that element across all pages / repeatable entries.blurElementExactIds: optional companion prop for targeting a single specific clone (no base-id stripping).- The preview still respects any
previewBlur: trueflags already baked into the resolved config —blurFieldIdsis purely additive. - This only affects the live React preview overlay. For watermarked
downloads (PNG/PDF) you still want to run
injectPreviewBlur(...)on the resolved config before handing it to the imperative renderer, so the blur is baked into the exported pixels.
Biodata teaser previews — blur by flat form key (v0.5.187+, hardened in v0.5.189, pagination-safe in v0.5.190)
Form-driven apps (BioMaker, the pixldocs.com Use page, etc.) already
speak the flat form-key language produced by applyFormDataToConfig
— e.g.
field_<treeNodeId>_<entryIdx1>_<fieldKey>
field_<parentTree>_<pi>_field_<childTree>_<ci>_<fieldKey> // nestedInstead of reverse-engineering config.__cloneIdMap keys or the
__cN / _eN clone suffixes on canvas element ids, you can pass those
flat keys straight to <PixldocsPreview> and the package resolves them:
import {
PixldocsPreview,
buildTeaserBlurFlatKeys,
} from '@pixldocs/canvas-renderer';
// e.g. for a biodata teaser: blur every detail-row VALUE after row 3.
const blurFlatFormKeys = buildTeaserBlurFlatKeys(sectionState, schema, {
afterRow: 3,
bindings: 'value',
});
<PixldocsPreview
config={displayConfig}
pageIndex={pageIndex}
frostedBlur
blurFlatFormKeys={blurFlatFormKeys}
// Optional — defaults to { bindings: 'value' } so only `*_value` keys
// are honoured. Pass 'all' to blur labels / titles too. `pageIndex` is
// forwarded into the resolver so verification scopes to the page
// currently being rendered (recommended for stacked multi-page previews).
blurFlatFormKeyOptions={{ bindings: 'value', pageIndex }}
/>Notes:
- Matching is done on
config.__cloneIdMapand handles all alias variants (grp-…_4_value,…_4_field_detail_section_N_field_N_value, with or without the wrapperfield_prefix). As of v0.5.189 every resolved id is then verified against the actual rendered page tree (config.pages[*]) — stale or alias map entries that don't match a real element are dropped silently. - As of v0.5.190
applyContentBoundsPaginationrewrites__cloneIdMapafter pagination so flat keys for overflow rows that moved to continuation pages still resolve to ids that exist on the tree (clones inherit__sourceIdfrom the original element). If the map is still stale for any reason, the resolver also falls back to walking the page tree and matching__sourceId/__baseNodeIdagainst the raw resolved ids — the same strategy the theming pipeline uses to track paginated clones. End result: frosted blur for row N appears on whichever page row N actually renders on, even when content-bounds pagination moved it. - Pass
pageIndexinblurFlatFormKeyOptionsto scope verification to a single page — clones that landed on a different page will not contribute overlays to this page. This is the recommended pattern for stacked multi-page previews. Default is'all'(verify across every page). - Use
skipTreeVerification: trueonly if you fully trust your__cloneIdMap(e.g. you just built the config in-process and haven't mutated the tree since). - Defaults to
bindings: 'value', i.e. only flat keys whose last segment isvalueresolve. This is exactly the biodata teaser pattern: blur row values, leave labels / section titles / full-name / photo sharp. Switch to'all'if you want labels blurred too. - This only drives the live preview overlay. For the watermarked PNG/PDF download path, resolve the same keys to exact ids and bake them in (see below).
buildTeaserBlurFlatKeys(sectionState, sections, options)
Emits the flat form-keys for every repeatable-section entry after
afterRow (1-indexed). Walks nested repeatables recursively.
sectionState— the sameSectionFormStateyou'd hand toresolveFromForm/<PixldocsPreview sectionState={…}>.sections—InferredSection[]for the active form schema (you already have these frominferFormSchemaFromTemplateor from theform_schemasrow converted viafromFormDefSections).options.afterRow— keep the first N entries sharp; blur entriesN+1, N+2, ….options.bindings—'value'(default) emits only_valuekeys (the canonical biodata teaser pattern);'all'emits every entry field key.
Returned keys can be fed straight into the blurFlatFormKeys prop
above and into resolveBlurElementExactIdsFromFlatFormKeys for the
watermarked download path below.
Watermarked downloads — same resolver + injectPreviewBlur
import {
injectPreviewBlur,
resolveBlurElementExactIdsFromFlatFormKeys,
} from '@pixldocs/canvas-renderer';
const exactIds = resolveBlurElementExactIdsFromFlatFormKeys(
resolvedConfig,
blurFlatFormKeys,
// { bindings: 'value', pageIndex: 'all' } by default. For the
// download path you usually want 'all' since the export covers
// every page.
);
const exportConfig = injectPreviewBlur(resolvedConfig, {
extraElementExactIds: new Set(exactIds),
});
// Hand `exportConfig` to renderer.renderAllPages / renderPdf / …Same resolution path as the live preview — so the on-screen overlay and the downloaded PNG/PDF blur exactly the same elements.
Parity note
The pixldocs.com Use page (the in-app paid-template preview, including
the Biodata preset's 🔒 per-field checkboxes) and external consumers
(BioMaker, etc.) both call the same resolveBlurElementExactIdsFromFlatFormKeys
function — there is now exactly one resolver shared across the entire
stack. Anything that blurs correctly in one place blurs identically in
the other; visual/UX bugs introduced in one pipeline cannot diverge
silently from the other.
Reliable PDF photo embedding across browsers (v0.5.191+)
Form-driven apps that embed user-uploaded photos (biodatas, resumes,
ID cards…) historically hit a Safari/iOS bug where the downloaded PDF
rendered without the photos even though the on-screen preview was fine.
The svg2pdf fast path internally re-rasterises data:image/* sources
through a tainted offscreen canvas roundtrip, which Safari silently drops
for large camera-roll JPEGs.
v0.5.191 fixes this with two new options:
const renderer = new PixldocsRenderer({
supabaseUrl, supabaseAnonKey,
// Keep the live SVG path. Use true only as a manual escape hatch because
// the per-element/config-space path can drift from live Fabric layout.
forcePerElementPdf: 'auto', // true | false | 'auto' (default)
// Normalize data:image/* photos before mounting into Fabric. The default
// auto value is 2048 and also recompresses oversized encoded data URLs.
maxImageEdgePx: 2048, // default auto; 0 disables
});
const pdf = await renderer.renderPdfFromForm({
templateId, formSchemaId, sectionState, title,
// Per-call overrides also accepted:
// forcePerElementPdf: 'auto',
// maxImageEdgePx: 2400,
});Defaults: forcePerElementPdf: 'auto' keeps the live SVG path. Any resolved
config containing data:image/(jpeg|png|webp) sources is normalized before PDF
mounting so large camera/gallery uploads behave consistently across browsers
and server renderers.
Hosts that previously monkey-patched captureFabricCanvasSvgForPdf to
throw (the unofficial BioMaker workaround) can delete that patch after
upgrading.
