@pixldocs/canvas-renderer 0.3.19 → 0.3.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -184,15 +184,38 @@ export declare class PixldocsRenderer {
184
184
  * This is the primary external API for the package.
185
185
  */
186
186
  renderFromForm(options: RenderFromFormOptions): Promise<RenderResult[]>;
187
+ /**
188
+ * Render a page and capture the Fabric canvas SVG output (vector, not raster).
189
+ * This is the key building block for client-side vector PDF export.
190
+ */
191
+ renderPageSvg(templateConfig: TemplateConfig, pageIndex?: number): Promise<SvgRenderResult>;
192
+ /**
193
+ * Render all pages and return SVG strings for each.
194
+ */
195
+ renderAllPageSvgs(templateConfig: TemplateConfig): Promise<SvgRenderResult[]>;
196
+ /**
197
+ * Resolve from V2 sectionState and return SVGs for all pages (for server vector PDF).
198
+ */
199
+ renderSvgsFromForm(options: Omit<RenderFromFormOptions, 'format' | 'quality' | 'scale' | 'pixelRatio'>): Promise<SvgRenderResult[]>;
187
200
  /**
188
201
  * Convenience: fetch by ID with simple flat data and render.
189
202
  */
190
203
  renderById(templateId: string, formData?: Record<string, any>, options?: RenderOptions): Promise<RenderResult>;
204
+ /**
205
+ * Convenience: fetch by ID with flat data and render ALL pages.
206
+ */
207
+ renderAllById(templateId: string, formData?: Record<string, any>, options?: Omit<RenderOptions, 'pageIndex'>): Promise<RenderResult[]>;
191
208
  private getExpectedImageCount;
192
209
  private waitForCanvasImages;
193
210
  private getNormalizedGradientStops;
194
211
  private paintPageBackground;
195
212
  private renderPageViaPreviewCanvas;
213
+ private captureSvgViaPreviewCanvas;
214
+ /**
215
+ * Find the Fabric.Canvas instance that belongs to a given container element,
216
+ * using the global __fabricCanvasRegistry (set by PageCanvas).
217
+ */
218
+ private getFabricCanvasFromContainer;
196
219
  }
197
220
 
198
221
  export declare interface RendererConfig {
@@ -291,6 +314,19 @@ export { SmartElementProps }
291
314
 
292
315
  export { SmartElementType }
293
316
 
317
+ export declare interface SvgRenderResult {
318
+ /** Raw SVG string from Fabric's toSVG() — clean, no viewport transforms */
319
+ svg: string;
320
+ /** Page width in CSS pixels */
321
+ width: number;
322
+ /** Page height in CSS pixels */
323
+ height: number;
324
+ /** Background color of the page */
325
+ backgroundColor: string;
326
+ /** Background gradient (if any) */
327
+ backgroundGradient?: any;
328
+ }
329
+
294
330
  export declare interface TemplateConfig {
295
331
  version?: string;
296
332
  name?: string;
package/dist/index.js CHANGED
@@ -2643,6 +2643,17 @@ function isPrivateUrl(url) {
2643
2643
  }
2644
2644
  }
2645
2645
  function toPublicStorageUrl(url) {
2646
+ try {
2647
+ const parsed = new URL(url);
2648
+ const origin = parsed.origin;
2649
+ const signedMatch = parsed.pathname.match(/\/storage\/v1\/object\/sign\/([^?]+)/);
2650
+ if (signedMatch) {
2651
+ return `${origin}/storage/v1/object/public/${signedMatch[1]}`;
2652
+ }
2653
+ if (parsed.pathname.includes("/storage/v1/object/public/")) return url;
2654
+ } catch {
2655
+ return null;
2656
+ }
2646
2657
  return null;
2647
2658
  }
2648
2659
  function getProxiedImageUrl(imageUrl) {
@@ -2652,7 +2663,7 @@ function getProxiedImageUrl(imageUrl) {
2652
2663
  console.warn("[image-proxy] Skipping private URL:", imageUrl.substring(0, 80));
2653
2664
  return "";
2654
2665
  }
2655
- const publicUrl = toPublicStorageUrl();
2666
+ const publicUrl = toPublicStorageUrl(imageUrl);
2656
2667
  if (publicUrl) return publicUrl;
2657
2668
  return `${API_URL}/image-proxy?url=${encodeURIComponent(imageUrl)}`;
2658
2669
  }
@@ -10683,6 +10694,58 @@ class PixldocsRenderer {
10683
10694
  }
10684
10695
  return this.renderAllPages(configToRender, renderOpts);
10685
10696
  }
10697
+ /**
10698
+ * Render a page and capture the Fabric canvas SVG output (vector, not raster).
10699
+ * This is the key building block for client-side vector PDF export.
10700
+ */
10701
+ async renderPageSvg(templateConfig, pageIndex = 0) {
10702
+ const page = templateConfig.pages[pageIndex];
10703
+ if (!page) {
10704
+ throw new Error(`Page index ${pageIndex} not found (template has ${templateConfig.pages.length} pages)`);
10705
+ }
10706
+ await ensureFontsForResolvedConfig(templateConfig);
10707
+ const { setPackageApiUrl: setPackageApiUrl2 } = await Promise.resolve().then(() => appApi);
10708
+ setPackageApiUrl2(this.config.imageProxyUrl);
10709
+ const canvasWidth = templateConfig.canvas.width;
10710
+ const canvasHeight = templateConfig.canvas.height;
10711
+ return this.captureSvgViaPreviewCanvas(templateConfig, pageIndex, canvasWidth, canvasHeight);
10712
+ }
10713
+ /**
10714
+ * Render all pages and return SVG strings for each.
10715
+ */
10716
+ async renderAllPageSvgs(templateConfig) {
10717
+ await ensureFontsForResolvedConfig(templateConfig);
10718
+ const { setPackageApiUrl: setPackageApiUrl2 } = await Promise.resolve().then(() => appApi);
10719
+ setPackageApiUrl2(this.config.imageProxyUrl);
10720
+ const results = [];
10721
+ for (let i = 0; i < templateConfig.pages.length; i++) {
10722
+ const canvasWidth = templateConfig.canvas.width;
10723
+ const canvasHeight = templateConfig.canvas.height;
10724
+ results.push(await this.captureSvgViaPreviewCanvas(templateConfig, i, canvasWidth, canvasHeight));
10725
+ }
10726
+ return results;
10727
+ }
10728
+ /**
10729
+ * Resolve from V2 sectionState and return SVGs for all pages (for server vector PDF).
10730
+ */
10731
+ async renderSvgsFromForm(options) {
10732
+ const { templateId, formSchemaId, sectionState, themeId, watermark } = options;
10733
+ const resolved = await resolveFromForm({
10734
+ templateId,
10735
+ formSchemaId,
10736
+ sectionState,
10737
+ themeId,
10738
+ supabaseUrl: this.config.supabaseUrl,
10739
+ supabaseAnonKey: this.config.supabaseAnonKey
10740
+ });
10741
+ const shouldWatermark = watermark ?? resolved.price > 0;
10742
+ let configToRender = resolved.config;
10743
+ if (shouldWatermark) {
10744
+ const { injectWatermark } = await import("./canvasWatermark-CM85x4k7.js");
10745
+ configToRender = injectWatermark(configToRender);
10746
+ }
10747
+ return this.renderAllPageSvgs(configToRender);
10748
+ }
10686
10749
  /**
10687
10750
  * Convenience: fetch by ID with simple flat data and render.
10688
10751
  */
@@ -10695,6 +10758,18 @@ class PixldocsRenderer {
10695
10758
  });
10696
10759
  return this.render(resolved.config, options);
10697
10760
  }
10761
+ /**
10762
+ * Convenience: fetch by ID with flat data and render ALL pages.
10763
+ */
10764
+ async renderAllById(templateId, formData, options) {
10765
+ const resolved = await resolveTemplateData({
10766
+ templateId,
10767
+ formData,
10768
+ supabaseUrl: this.config.supabaseUrl,
10769
+ supabaseAnonKey: this.config.supabaseAnonKey
10770
+ });
10771
+ return this.renderAllPages(resolved.config, options);
10772
+ }
10698
10773
  // ─── Internal: render a page using the full PreviewCanvas engine ───
10699
10774
  getExpectedImageCount(config, pageIndex) {
10700
10775
  const page = config.pages[pageIndex];
@@ -10718,6 +10793,7 @@ class PixldocsRenderer {
10718
10793
  return new Promise((resolve) => {
10719
10794
  const start = Date.now();
10720
10795
  let stableFrames = 0;
10796
+ let lastSummary = "";
10721
10797
  const isRenderableImage = (value) => value instanceof HTMLImageElement && value.complete && value.naturalWidth > 0 && value.naturalHeight > 0;
10722
10798
  const collectRenderableImages = (obj, seen) => {
10723
10799
  if (!obj || typeof obj !== "object") return;
@@ -10747,6 +10823,25 @@ class PixldocsRenderer {
10747
10823
  return null;
10748
10824
  };
10749
10825
  const settle = () => requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
10826
+ const getImageDebugInfo = (obj, bucket) => {
10827
+ if (!obj || typeof obj !== "object") return;
10828
+ const candidates = [obj._element, obj._originalElement, obj._filteredEl, obj._cacheCanvasEl];
10829
+ for (const candidate of candidates) {
10830
+ if (candidate instanceof HTMLImageElement) {
10831
+ bucket.push({
10832
+ id: obj.__docuforgeId || obj.id || "unknown",
10833
+ src: (candidate.currentSrc || candidate.src || "").slice(0, 240),
10834
+ complete: candidate.complete,
10835
+ naturalWidth: candidate.naturalWidth,
10836
+ naturalHeight: candidate.naturalHeight
10837
+ });
10838
+ }
10839
+ }
10840
+ const nested = Array.isArray(obj._objects) ? obj._objects : Array.isArray(obj.objects) ? obj.objects : [];
10841
+ for (const child of nested) {
10842
+ getImageDebugInfo(child, bucket);
10843
+ }
10844
+ };
10750
10845
  const check = () => {
10751
10846
  const elapsed = Date.now() - start;
10752
10847
  const domImages = Array.from(container.querySelectorAll("img"));
@@ -10756,12 +10851,18 @@ class PixldocsRenderer {
10756
10851
  const renderableImages = /* @__PURE__ */ new Set();
10757
10852
  const fabricReady = fabricObjects.every((obj) => collectRenderableImages(obj, renderableImages) !== false);
10758
10853
  const actualImageCount = Math.max(domImages.length, renderableImages.size);
10759
- !!fabricCanvas && !!(fabricCanvas.lowerCanvasEl || fabricCanvas.upperCanvasEl);
10854
+ const canvasReady = !!fabricCanvas && !!(fabricCanvas.lowerCanvasEl || fabricCanvas.upperCanvasEl);
10760
10855
  const hasExpectedAssets = expectedImageCount === 0 ? true : actualImageCount >= expectedImageCount;
10761
10856
  const ready = allDomLoaded && fabricReady && hasExpectedAssets;
10857
+ const summary = `expected=${expectedImageCount} actual=${actualImageCount} dom=${domImages.length} fabricReady=${fabricReady} domReady=${allDomLoaded} canvasReady=${canvasReady}`;
10858
+ if (summary !== lastSummary) {
10859
+ lastSummary = summary;
10860
+ console.log(`[canvas-renderer][asset-wait] ${summary}`);
10861
+ }
10762
10862
  if (ready) {
10763
10863
  stableFrames += 1;
10764
10864
  if (stableFrames >= 2) {
10865
+ console.log(`[canvas-renderer][asset-wait] ready after ${elapsed}ms (${summary})`);
10765
10866
  settle();
10766
10867
  return;
10767
10868
  }
@@ -10769,6 +10870,20 @@ class PixldocsRenderer {
10769
10870
  stableFrames = 0;
10770
10871
  }
10771
10872
  if (elapsed >= maxWaitMs) {
10873
+ const fabricImageDebug = [];
10874
+ for (const obj of fabricObjects) {
10875
+ getImageDebugInfo(obj, fabricImageDebug);
10876
+ }
10877
+ const domImageDebug = domImages.map((img, index) => ({
10878
+ index,
10879
+ src: (img.currentSrc || img.src || "").slice(0, 240),
10880
+ complete: img.complete,
10881
+ naturalWidth: img.naturalWidth,
10882
+ naturalHeight: img.naturalHeight
10883
+ }));
10884
+ console.warn(`[canvas-renderer][asset-wait-timeout] elapsed=${elapsed}ms ${summary}`);
10885
+ console.warn("[canvas-renderer][asset-wait-timeout][dom-images]", domImageDebug);
10886
+ console.warn("[canvas-renderer][asset-wait-timeout][fabric-images]", fabricImageDebug);
10772
10887
  settle();
10773
10888
  return;
10774
10889
  }
@@ -10922,6 +11037,93 @@ class PixldocsRenderer {
10922
11037
  );
10923
11038
  });
10924
11039
  }
11040
+ // ─── Internal: capture SVG from a rendered Fabric canvas ───
11041
+ captureSvgViaPreviewCanvas(config, pageIndex, canvasWidth, canvasHeight) {
11042
+ return new Promise(async (resolve, reject) => {
11043
+ const { PreviewCanvas: PreviewCanvas2 } = await Promise.resolve().then(() => PreviewCanvas$1);
11044
+ const container = document.createElement("div");
11045
+ container.style.cssText = `
11046
+ position: fixed; left: -99999px; top: -99999px;
11047
+ width: ${canvasWidth}px; height: ${canvasHeight}px;
11048
+ overflow: hidden; pointer-events: none; opacity: 0;
11049
+ `;
11050
+ document.body.appendChild(container);
11051
+ const timeout = setTimeout(() => {
11052
+ cleanup();
11053
+ reject(new Error("SVG render timeout (30s)"));
11054
+ }, 3e4);
11055
+ const cleanup = () => {
11056
+ clearTimeout(timeout);
11057
+ try {
11058
+ root.unmount();
11059
+ } catch {
11060
+ }
11061
+ container.remove();
11062
+ };
11063
+ const onReady = () => {
11064
+ const expectedImageCount = this.getExpectedImageCount(config, pageIndex);
11065
+ this.waitForCanvasImages(container, expectedImageCount).then(() => {
11066
+ var _a, _b;
11067
+ try {
11068
+ const fabricInstance = this.getFabricCanvasFromContainer(container);
11069
+ if (!fabricInstance) {
11070
+ cleanup();
11071
+ reject(new Error("No Fabric canvas instance found for SVG capture"));
11072
+ return;
11073
+ }
11074
+ const prevVPT = fabricInstance.viewportTransform ? [...fabricInstance.viewportTransform] : void 0;
11075
+ const prevSvgVPT = fabricInstance.svgViewportTransformation;
11076
+ fabricInstance.viewportTransform = [1, 0, 0, 1, 0, 0];
11077
+ fabricInstance.svgViewportTransformation = false;
11078
+ const svgString = fabricInstance.toSVG();
11079
+ if (prevVPT) fabricInstance.viewportTransform = prevVPT;
11080
+ fabricInstance.svgViewportTransformation = prevSvgVPT;
11081
+ const page = config.pages[pageIndex];
11082
+ const backgroundColor = ((_a = page == null ? void 0 : page.settings) == null ? void 0 : _a.backgroundColor) || "#ffffff";
11083
+ const backgroundGradient = (_b = page == null ? void 0 : page.settings) == null ? void 0 : _b.backgroundGradient;
11084
+ cleanup();
11085
+ resolve({
11086
+ svg: svgString,
11087
+ width: canvasWidth,
11088
+ height: canvasHeight,
11089
+ backgroundColor,
11090
+ backgroundGradient
11091
+ });
11092
+ } catch (err) {
11093
+ cleanup();
11094
+ reject(err);
11095
+ }
11096
+ });
11097
+ };
11098
+ const root = createRoot(container);
11099
+ root.render(
11100
+ createElement(PreviewCanvas2, {
11101
+ config,
11102
+ pageIndex,
11103
+ zoom: 1,
11104
+ // 1:1 — no scaling for SVG capture
11105
+ absoluteZoom: true,
11106
+ onReady
11107
+ })
11108
+ );
11109
+ });
11110
+ }
11111
+ /**
11112
+ * Find the Fabric.Canvas instance that belongs to a given container element,
11113
+ * using the global __fabricCanvasRegistry (set by PageCanvas).
11114
+ */
11115
+ getFabricCanvasFromContainer(container) {
11116
+ const registry2 = window.__fabricCanvasRegistry;
11117
+ if (registry2 instanceof Map) {
11118
+ for (const entry of registry2.values()) {
11119
+ const canvas = (entry == null ? void 0 : entry.canvas) || entry;
11120
+ if (!canvas || typeof canvas.toSVG !== "function") continue;
11121
+ const el = canvas.lowerCanvasEl || canvas.upperCanvasEl;
11122
+ if (el && container.contains(el)) return canvas;
11123
+ }
11124
+ }
11125
+ return null;
11126
+ }
10925
11127
  }
10926
11128
  function collectImageUrls(config) {
10927
11129
  const urls = [];