@pixldocs/canvas-renderer 0.3.19 → 0.3.20

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,6 +184,19 @@ 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
  */
@@ -193,6 +206,12 @@ export declare class PixldocsRenderer {
193
206
  private getNormalizedGradientStops;
194
207
  private paintPageBackground;
195
208
  private renderPageViaPreviewCanvas;
209
+ private captureSvgViaPreviewCanvas;
210
+ /**
211
+ * Find the Fabric.Canvas instance that belongs to a given container element,
212
+ * using the global __fabricCanvasRegistry (set by PageCanvas).
213
+ */
214
+ private getFabricCanvasFromContainer;
196
215
  }
197
216
 
198
217
  export declare interface RendererConfig {
@@ -291,6 +310,19 @@ export { SmartElementProps }
291
310
 
292
311
  export { SmartElementType }
293
312
 
313
+ export declare interface SvgRenderResult {
314
+ /** Raw SVG string from Fabric's toSVG() — clean, no viewport transforms */
315
+ svg: string;
316
+ /** Page width in CSS pixels */
317
+ width: number;
318
+ /** Page height in CSS pixels */
319
+ height: number;
320
+ /** Background color of the page */
321
+ backgroundColor: string;
322
+ /** Background gradient (if any) */
323
+ backgroundGradient?: any;
324
+ }
325
+
294
326
  export declare interface TemplateConfig {
295
327
  version?: string;
296
328
  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
  */
@@ -10718,6 +10781,7 @@ class PixldocsRenderer {
10718
10781
  return new Promise((resolve) => {
10719
10782
  const start = Date.now();
10720
10783
  let stableFrames = 0;
10784
+ let lastSummary = "";
10721
10785
  const isRenderableImage = (value) => value instanceof HTMLImageElement && value.complete && value.naturalWidth > 0 && value.naturalHeight > 0;
10722
10786
  const collectRenderableImages = (obj, seen) => {
10723
10787
  if (!obj || typeof obj !== "object") return;
@@ -10747,6 +10811,25 @@ class PixldocsRenderer {
10747
10811
  return null;
10748
10812
  };
10749
10813
  const settle = () => requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
10814
+ const getImageDebugInfo = (obj, bucket) => {
10815
+ if (!obj || typeof obj !== "object") return;
10816
+ const candidates = [obj._element, obj._originalElement, obj._filteredEl, obj._cacheCanvasEl];
10817
+ for (const candidate of candidates) {
10818
+ if (candidate instanceof HTMLImageElement) {
10819
+ bucket.push({
10820
+ id: obj.__docuforgeId || obj.id || "unknown",
10821
+ src: (candidate.currentSrc || candidate.src || "").slice(0, 240),
10822
+ complete: candidate.complete,
10823
+ naturalWidth: candidate.naturalWidth,
10824
+ naturalHeight: candidate.naturalHeight
10825
+ });
10826
+ }
10827
+ }
10828
+ const nested = Array.isArray(obj._objects) ? obj._objects : Array.isArray(obj.objects) ? obj.objects : [];
10829
+ for (const child of nested) {
10830
+ getImageDebugInfo(child, bucket);
10831
+ }
10832
+ };
10750
10833
  const check = () => {
10751
10834
  const elapsed = Date.now() - start;
10752
10835
  const domImages = Array.from(container.querySelectorAll("img"));
@@ -10756,12 +10839,18 @@ class PixldocsRenderer {
10756
10839
  const renderableImages = /* @__PURE__ */ new Set();
10757
10840
  const fabricReady = fabricObjects.every((obj) => collectRenderableImages(obj, renderableImages) !== false);
10758
10841
  const actualImageCount = Math.max(domImages.length, renderableImages.size);
10759
- !!fabricCanvas && !!(fabricCanvas.lowerCanvasEl || fabricCanvas.upperCanvasEl);
10842
+ const canvasReady = !!fabricCanvas && !!(fabricCanvas.lowerCanvasEl || fabricCanvas.upperCanvasEl);
10760
10843
  const hasExpectedAssets = expectedImageCount === 0 ? true : actualImageCount >= expectedImageCount;
10761
10844
  const ready = allDomLoaded && fabricReady && hasExpectedAssets;
10845
+ const summary = `expected=${expectedImageCount} actual=${actualImageCount} dom=${domImages.length} fabricReady=${fabricReady} domReady=${allDomLoaded} canvasReady=${canvasReady}`;
10846
+ if (summary !== lastSummary) {
10847
+ lastSummary = summary;
10848
+ console.log(`[canvas-renderer][asset-wait] ${summary}`);
10849
+ }
10762
10850
  if (ready) {
10763
10851
  stableFrames += 1;
10764
10852
  if (stableFrames >= 2) {
10853
+ console.log(`[canvas-renderer][asset-wait] ready after ${elapsed}ms (${summary})`);
10765
10854
  settle();
10766
10855
  return;
10767
10856
  }
@@ -10769,6 +10858,20 @@ class PixldocsRenderer {
10769
10858
  stableFrames = 0;
10770
10859
  }
10771
10860
  if (elapsed >= maxWaitMs) {
10861
+ const fabricImageDebug = [];
10862
+ for (const obj of fabricObjects) {
10863
+ getImageDebugInfo(obj, fabricImageDebug);
10864
+ }
10865
+ const domImageDebug = domImages.map((img, index) => ({
10866
+ index,
10867
+ src: (img.currentSrc || img.src || "").slice(0, 240),
10868
+ complete: img.complete,
10869
+ naturalWidth: img.naturalWidth,
10870
+ naturalHeight: img.naturalHeight
10871
+ }));
10872
+ console.warn(`[canvas-renderer][asset-wait-timeout] elapsed=${elapsed}ms ${summary}`);
10873
+ console.warn("[canvas-renderer][asset-wait-timeout][dom-images]", domImageDebug);
10874
+ console.warn("[canvas-renderer][asset-wait-timeout][fabric-images]", fabricImageDebug);
10772
10875
  settle();
10773
10876
  return;
10774
10877
  }
@@ -10922,6 +11025,93 @@ class PixldocsRenderer {
10922
11025
  );
10923
11026
  });
10924
11027
  }
11028
+ // ─── Internal: capture SVG from a rendered Fabric canvas ───
11029
+ captureSvgViaPreviewCanvas(config, pageIndex, canvasWidth, canvasHeight) {
11030
+ return new Promise(async (resolve, reject) => {
11031
+ const { PreviewCanvas: PreviewCanvas2 } = await Promise.resolve().then(() => PreviewCanvas$1);
11032
+ const container = document.createElement("div");
11033
+ container.style.cssText = `
11034
+ position: fixed; left: -99999px; top: -99999px;
11035
+ width: ${canvasWidth}px; height: ${canvasHeight}px;
11036
+ overflow: hidden; pointer-events: none; opacity: 0;
11037
+ `;
11038
+ document.body.appendChild(container);
11039
+ const timeout = setTimeout(() => {
11040
+ cleanup();
11041
+ reject(new Error("SVG render timeout (30s)"));
11042
+ }, 3e4);
11043
+ const cleanup = () => {
11044
+ clearTimeout(timeout);
11045
+ try {
11046
+ root.unmount();
11047
+ } catch {
11048
+ }
11049
+ container.remove();
11050
+ };
11051
+ const onReady = () => {
11052
+ const expectedImageCount = this.getExpectedImageCount(config, pageIndex);
11053
+ this.waitForCanvasImages(container, expectedImageCount).then(() => {
11054
+ var _a, _b;
11055
+ try {
11056
+ const fabricInstance = this.getFabricCanvasFromContainer(container);
11057
+ if (!fabricInstance) {
11058
+ cleanup();
11059
+ reject(new Error("No Fabric canvas instance found for SVG capture"));
11060
+ return;
11061
+ }
11062
+ const prevVPT = fabricInstance.viewportTransform ? [...fabricInstance.viewportTransform] : void 0;
11063
+ const prevSvgVPT = fabricInstance.svgViewportTransformation;
11064
+ fabricInstance.viewportTransform = [1, 0, 0, 1, 0, 0];
11065
+ fabricInstance.svgViewportTransformation = false;
11066
+ const svgString = fabricInstance.toSVG();
11067
+ if (prevVPT) fabricInstance.viewportTransform = prevVPT;
11068
+ fabricInstance.svgViewportTransformation = prevSvgVPT;
11069
+ const page = config.pages[pageIndex];
11070
+ const backgroundColor = ((_a = page == null ? void 0 : page.settings) == null ? void 0 : _a.backgroundColor) || "#ffffff";
11071
+ const backgroundGradient = (_b = page == null ? void 0 : page.settings) == null ? void 0 : _b.backgroundGradient;
11072
+ cleanup();
11073
+ resolve({
11074
+ svg: svgString,
11075
+ width: canvasWidth,
11076
+ height: canvasHeight,
11077
+ backgroundColor,
11078
+ backgroundGradient
11079
+ });
11080
+ } catch (err) {
11081
+ cleanup();
11082
+ reject(err);
11083
+ }
11084
+ });
11085
+ };
11086
+ const root = createRoot(container);
11087
+ root.render(
11088
+ createElement(PreviewCanvas2, {
11089
+ config,
11090
+ pageIndex,
11091
+ zoom: 1,
11092
+ // 1:1 — no scaling for SVG capture
11093
+ absoluteZoom: true,
11094
+ onReady
11095
+ })
11096
+ );
11097
+ });
11098
+ }
11099
+ /**
11100
+ * Find the Fabric.Canvas instance that belongs to a given container element,
11101
+ * using the global __fabricCanvasRegistry (set by PageCanvas).
11102
+ */
11103
+ getFabricCanvasFromContainer(container) {
11104
+ const registry2 = window.__fabricCanvasRegistry;
11105
+ if (registry2 instanceof Map) {
11106
+ for (const entry of registry2.values()) {
11107
+ const canvas = (entry == null ? void 0 : entry.canvas) || entry;
11108
+ if (!canvas || typeof canvas.toSVG !== "function") continue;
11109
+ const el = canvas.lowerCanvasEl || canvas.upperCanvasEl;
11110
+ if (el && container.contains(el)) return canvas;
11111
+ }
11112
+ }
11113
+ return null;
11114
+ }
10925
11115
  }
10926
11116
  function collectImageUrls(config) {
10927
11117
  const urls = [];