@pixldocs/canvas-renderer 0.3.18 → 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.cjs CHANGED
@@ -2662,6 +2662,17 @@ function isPrivateUrl(url) {
2662
2662
  }
2663
2663
  }
2664
2664
  function toPublicStorageUrl(url) {
2665
+ try {
2666
+ const parsed = new URL(url);
2667
+ const origin = parsed.origin;
2668
+ const signedMatch = parsed.pathname.match(/\/storage\/v1\/object\/sign\/([^?]+)/);
2669
+ if (signedMatch) {
2670
+ return `${origin}/storage/v1/object/public/${signedMatch[1]}`;
2671
+ }
2672
+ if (parsed.pathname.includes("/storage/v1/object/public/")) return url;
2673
+ } catch {
2674
+ return null;
2675
+ }
2665
2676
  return null;
2666
2677
  }
2667
2678
  function getProxiedImageUrl(imageUrl) {
@@ -2671,7 +2682,7 @@ function getProxiedImageUrl(imageUrl) {
2671
2682
  console.warn("[image-proxy] Skipping private URL:", imageUrl.substring(0, 80));
2672
2683
  return "";
2673
2684
  }
2674
- const publicUrl = toPublicStorageUrl();
2685
+ const publicUrl = toPublicStorageUrl(imageUrl);
2675
2686
  if (publicUrl) return publicUrl;
2676
2687
  return `${API_URL}/image-proxy?url=${encodeURIComponent(imageUrl)}`;
2677
2688
  }
@@ -10702,6 +10713,58 @@ class PixldocsRenderer {
10702
10713
  }
10703
10714
  return this.renderAllPages(configToRender, renderOpts);
10704
10715
  }
10716
+ /**
10717
+ * Render a page and capture the Fabric canvas SVG output (vector, not raster).
10718
+ * This is the key building block for client-side vector PDF export.
10719
+ */
10720
+ async renderPageSvg(templateConfig, pageIndex = 0) {
10721
+ const page = templateConfig.pages[pageIndex];
10722
+ if (!page) {
10723
+ throw new Error(`Page index ${pageIndex} not found (template has ${templateConfig.pages.length} pages)`);
10724
+ }
10725
+ await ensureFontsForResolvedConfig(templateConfig);
10726
+ const { setPackageApiUrl: setPackageApiUrl2 } = await Promise.resolve().then(() => appApi);
10727
+ setPackageApiUrl2(this.config.imageProxyUrl);
10728
+ const canvasWidth = templateConfig.canvas.width;
10729
+ const canvasHeight = templateConfig.canvas.height;
10730
+ return this.captureSvgViaPreviewCanvas(templateConfig, pageIndex, canvasWidth, canvasHeight);
10731
+ }
10732
+ /**
10733
+ * Render all pages and return SVG strings for each.
10734
+ */
10735
+ async renderAllPageSvgs(templateConfig) {
10736
+ await ensureFontsForResolvedConfig(templateConfig);
10737
+ const { setPackageApiUrl: setPackageApiUrl2 } = await Promise.resolve().then(() => appApi);
10738
+ setPackageApiUrl2(this.config.imageProxyUrl);
10739
+ const results = [];
10740
+ for (let i = 0; i < templateConfig.pages.length; i++) {
10741
+ const canvasWidth = templateConfig.canvas.width;
10742
+ const canvasHeight = templateConfig.canvas.height;
10743
+ results.push(await this.captureSvgViaPreviewCanvas(templateConfig, i, canvasWidth, canvasHeight));
10744
+ }
10745
+ return results;
10746
+ }
10747
+ /**
10748
+ * Resolve from V2 sectionState and return SVGs for all pages (for server vector PDF).
10749
+ */
10750
+ async renderSvgsFromForm(options) {
10751
+ const { templateId, formSchemaId, sectionState, themeId, watermark } = options;
10752
+ const resolved = await resolveFromForm({
10753
+ templateId,
10754
+ formSchemaId,
10755
+ sectionState,
10756
+ themeId,
10757
+ supabaseUrl: this.config.supabaseUrl,
10758
+ supabaseAnonKey: this.config.supabaseAnonKey
10759
+ });
10760
+ const shouldWatermark = watermark ?? resolved.price > 0;
10761
+ let configToRender = resolved.config;
10762
+ if (shouldWatermark) {
10763
+ const { injectWatermark } = await Promise.resolve().then(() => require("./canvasWatermark-DAZIQ_IR.cjs"));
10764
+ configToRender = injectWatermark(configToRender);
10765
+ }
10766
+ return this.renderAllPageSvgs(configToRender);
10767
+ }
10705
10768
  /**
10706
10769
  * Convenience: fetch by ID with simple flat data and render.
10707
10770
  */
@@ -10737,6 +10800,7 @@ class PixldocsRenderer {
10737
10800
  return new Promise((resolve) => {
10738
10801
  const start = Date.now();
10739
10802
  let stableFrames = 0;
10803
+ let lastSummary = "";
10740
10804
  const isRenderableImage = (value) => value instanceof HTMLImageElement && value.complete && value.naturalWidth > 0 && value.naturalHeight > 0;
10741
10805
  const collectRenderableImages = (obj, seen) => {
10742
10806
  if (!obj || typeof obj !== "object") return;
@@ -10766,6 +10830,25 @@ class PixldocsRenderer {
10766
10830
  return null;
10767
10831
  };
10768
10832
  const settle = () => requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
10833
+ const getImageDebugInfo = (obj, bucket) => {
10834
+ if (!obj || typeof obj !== "object") return;
10835
+ const candidates = [obj._element, obj._originalElement, obj._filteredEl, obj._cacheCanvasEl];
10836
+ for (const candidate of candidates) {
10837
+ if (candidate instanceof HTMLImageElement) {
10838
+ bucket.push({
10839
+ id: obj.__docuforgeId || obj.id || "unknown",
10840
+ src: (candidate.currentSrc || candidate.src || "").slice(0, 240),
10841
+ complete: candidate.complete,
10842
+ naturalWidth: candidate.naturalWidth,
10843
+ naturalHeight: candidate.naturalHeight
10844
+ });
10845
+ }
10846
+ }
10847
+ const nested = Array.isArray(obj._objects) ? obj._objects : Array.isArray(obj.objects) ? obj.objects : [];
10848
+ for (const child of nested) {
10849
+ getImageDebugInfo(child, bucket);
10850
+ }
10851
+ };
10769
10852
  const check = () => {
10770
10853
  const elapsed = Date.now() - start;
10771
10854
  const domImages = Array.from(container.querySelectorAll("img"));
@@ -10775,12 +10858,18 @@ class PixldocsRenderer {
10775
10858
  const renderableImages = /* @__PURE__ */ new Set();
10776
10859
  const fabricReady = fabricObjects.every((obj) => collectRenderableImages(obj, renderableImages) !== false);
10777
10860
  const actualImageCount = Math.max(domImages.length, renderableImages.size);
10778
- !!fabricCanvas && !!(fabricCanvas.lowerCanvasEl || fabricCanvas.upperCanvasEl);
10861
+ const canvasReady = !!fabricCanvas && !!(fabricCanvas.lowerCanvasEl || fabricCanvas.upperCanvasEl);
10779
10862
  const hasExpectedAssets = expectedImageCount === 0 ? true : actualImageCount >= expectedImageCount;
10780
10863
  const ready = allDomLoaded && fabricReady && hasExpectedAssets;
10864
+ const summary = `expected=${expectedImageCount} actual=${actualImageCount} dom=${domImages.length} fabricReady=${fabricReady} domReady=${allDomLoaded} canvasReady=${canvasReady}`;
10865
+ if (summary !== lastSummary) {
10866
+ lastSummary = summary;
10867
+ console.log(`[canvas-renderer][asset-wait] ${summary}`);
10868
+ }
10781
10869
  if (ready) {
10782
10870
  stableFrames += 1;
10783
10871
  if (stableFrames >= 2) {
10872
+ console.log(`[canvas-renderer][asset-wait] ready after ${elapsed}ms (${summary})`);
10784
10873
  settle();
10785
10874
  return;
10786
10875
  }
@@ -10788,6 +10877,20 @@ class PixldocsRenderer {
10788
10877
  stableFrames = 0;
10789
10878
  }
10790
10879
  if (elapsed >= maxWaitMs) {
10880
+ const fabricImageDebug = [];
10881
+ for (const obj of fabricObjects) {
10882
+ getImageDebugInfo(obj, fabricImageDebug);
10883
+ }
10884
+ const domImageDebug = domImages.map((img, index) => ({
10885
+ index,
10886
+ src: (img.currentSrc || img.src || "").slice(0, 240),
10887
+ complete: img.complete,
10888
+ naturalWidth: img.naturalWidth,
10889
+ naturalHeight: img.naturalHeight
10890
+ }));
10891
+ console.warn(`[canvas-renderer][asset-wait-timeout] elapsed=${elapsed}ms ${summary}`);
10892
+ console.warn("[canvas-renderer][asset-wait-timeout][dom-images]", domImageDebug);
10893
+ console.warn("[canvas-renderer][asset-wait-timeout][fabric-images]", fabricImageDebug);
10791
10894
  settle();
10792
10895
  return;
10793
10896
  }
@@ -10941,15 +11044,181 @@ class PixldocsRenderer {
10941
11044
  );
10942
11045
  });
10943
11046
  }
11047
+ // ─── Internal: capture SVG from a rendered Fabric canvas ───
11048
+ captureSvgViaPreviewCanvas(config, pageIndex, canvasWidth, canvasHeight) {
11049
+ return new Promise(async (resolve, reject) => {
11050
+ const { PreviewCanvas: PreviewCanvas2 } = await Promise.resolve().then(() => PreviewCanvas$1);
11051
+ const container = document.createElement("div");
11052
+ container.style.cssText = `
11053
+ position: fixed; left: -99999px; top: -99999px;
11054
+ width: ${canvasWidth}px; height: ${canvasHeight}px;
11055
+ overflow: hidden; pointer-events: none; opacity: 0;
11056
+ `;
11057
+ document.body.appendChild(container);
11058
+ const timeout = setTimeout(() => {
11059
+ cleanup();
11060
+ reject(new Error("SVG render timeout (30s)"));
11061
+ }, 3e4);
11062
+ const cleanup = () => {
11063
+ clearTimeout(timeout);
11064
+ try {
11065
+ root.unmount();
11066
+ } catch {
11067
+ }
11068
+ container.remove();
11069
+ };
11070
+ const onReady = () => {
11071
+ const expectedImageCount = this.getExpectedImageCount(config, pageIndex);
11072
+ this.waitForCanvasImages(container, expectedImageCount).then(() => {
11073
+ var _a, _b;
11074
+ try {
11075
+ const fabricInstance = this.getFabricCanvasFromContainer(container);
11076
+ if (!fabricInstance) {
11077
+ cleanup();
11078
+ reject(new Error("No Fabric canvas instance found for SVG capture"));
11079
+ return;
11080
+ }
11081
+ const prevVPT = fabricInstance.viewportTransform ? [...fabricInstance.viewportTransform] : void 0;
11082
+ const prevSvgVPT = fabricInstance.svgViewportTransformation;
11083
+ fabricInstance.viewportTransform = [1, 0, 0, 1, 0, 0];
11084
+ fabricInstance.svgViewportTransformation = false;
11085
+ const svgString = fabricInstance.toSVG();
11086
+ if (prevVPT) fabricInstance.viewportTransform = prevVPT;
11087
+ fabricInstance.svgViewportTransformation = prevSvgVPT;
11088
+ const page = config.pages[pageIndex];
11089
+ const backgroundColor = ((_a = page == null ? void 0 : page.settings) == null ? void 0 : _a.backgroundColor) || "#ffffff";
11090
+ const backgroundGradient = (_b = page == null ? void 0 : page.settings) == null ? void 0 : _b.backgroundGradient;
11091
+ cleanup();
11092
+ resolve({
11093
+ svg: svgString,
11094
+ width: canvasWidth,
11095
+ height: canvasHeight,
11096
+ backgroundColor,
11097
+ backgroundGradient
11098
+ });
11099
+ } catch (err) {
11100
+ cleanup();
11101
+ reject(err);
11102
+ }
11103
+ });
11104
+ };
11105
+ const root = client.createRoot(container);
11106
+ root.render(
11107
+ react.createElement(PreviewCanvas2, {
11108
+ config,
11109
+ pageIndex,
11110
+ zoom: 1,
11111
+ // 1:1 — no scaling for SVG capture
11112
+ absoluteZoom: true,
11113
+ onReady
11114
+ })
11115
+ );
11116
+ });
11117
+ }
11118
+ /**
11119
+ * Find the Fabric.Canvas instance that belongs to a given container element,
11120
+ * using the global __fabricCanvasRegistry (set by PageCanvas).
11121
+ */
11122
+ getFabricCanvasFromContainer(container) {
11123
+ const registry2 = window.__fabricCanvasRegistry;
11124
+ if (registry2 instanceof Map) {
11125
+ for (const entry of registry2.values()) {
11126
+ const canvas = (entry == null ? void 0 : entry.canvas) || entry;
11127
+ if (!canvas || typeof canvas.toSVG !== "function") continue;
11128
+ const el = canvas.lowerCanvasEl || canvas.upperCanvasEl;
11129
+ if (el && container.contains(el)) return canvas;
11130
+ }
11131
+ }
11132
+ return null;
11133
+ }
11134
+ }
11135
+ function collectImageUrls(config) {
11136
+ const urls = [];
11137
+ const walk = (nodes) => {
11138
+ for (const node of nodes) {
11139
+ if (!node || node.visible === false) continue;
11140
+ const src = typeof node.src === "string" ? node.src.trim() : "";
11141
+ const imageUrl = typeof node.imageUrl === "string" ? node.imageUrl.trim() : "";
11142
+ if (node.type === "image") {
11143
+ const url = src || imageUrl;
11144
+ if (url) urls.push(url);
11145
+ }
11146
+ if (Array.isArray(node.children) && node.children.length > 0) {
11147
+ walk(node.children);
11148
+ }
11149
+ }
11150
+ };
11151
+ for (const page of config.pages || []) {
11152
+ walk(page.children || []);
11153
+ }
11154
+ return urls;
11155
+ }
11156
+ function normalizeAssetUrl(rawUrl, imageProxyUrl) {
11157
+ if (!rawUrl) return null;
11158
+ if (rawUrl.startsWith("data:") || rawUrl.startsWith("blob:")) return null;
11159
+ try {
11160
+ const h = new URL(rawUrl).hostname.toLowerCase();
11161
+ if (h === "localhost" || h === "127.0.0.1" || h === "0.0.0.0" || h.endsWith(".local") || /^(10\.|192\.168\.|169\.254\.)/.test(h)) {
11162
+ return null;
11163
+ }
11164
+ } catch {
11165
+ return null;
11166
+ }
11167
+ const supabaseUrl = typeof globalThis.__VITE_SUPABASE_URL === "string" ? globalThis.__VITE_SUPABASE_URL : "";
11168
+ if (supabaseUrl && rawUrl.includes(supabaseUrl)) {
11169
+ const signedMatch = rawUrl.match(/\/storage\/v1\/object\/sign\/([^?]+)/);
11170
+ if (signedMatch) return `${supabaseUrl}/storage/v1/object/public/${signedMatch[1]}`;
11171
+ if (rawUrl.includes("/storage/v1/object/public/")) return rawUrl;
11172
+ }
11173
+ const proxyBase = imageProxyUrl ? imageProxyUrl.replace(/\/image-proxy(?:\?.*)?$/, "") : API_URL;
11174
+ if (proxyBase) {
11175
+ return `${proxyBase}/image-proxy?url=${encodeURIComponent(rawUrl)}`;
11176
+ }
11177
+ return rawUrl;
11178
+ }
11179
+ const CONCURRENCY = 6;
11180
+ async function prefetchUrls(urls, signal) {
11181
+ const unique = [...new Set(urls)];
11182
+ if (unique.length === 0) return;
11183
+ let i = 0;
11184
+ const next = async () => {
11185
+ while (i < unique.length) {
11186
+ if (signal == null ? void 0 : signal.aborted) return;
11187
+ const url = unique[i++];
11188
+ try {
11189
+ await fetch(url, { signal, mode: "cors", credentials: "omit" });
11190
+ } catch {
11191
+ }
11192
+ }
11193
+ };
11194
+ const workers = Array.from({ length: Math.min(CONCURRENCY, unique.length) }, () => next());
11195
+ await Promise.all(workers);
11196
+ }
11197
+ async function warmResolvedTemplateForPreview(config, options) {
11198
+ const { signal, imageProxyUrl } = options ?? {};
11199
+ await ensureFontsForResolvedConfig(config);
11200
+ if (signal == null ? void 0 : signal.aborted) return;
11201
+ const rawUrls = collectImageUrls(config);
11202
+ const resolvedUrls = rawUrls.map((u) => normalizeAssetUrl(u, imageProxyUrl)).filter((u) => u !== null);
11203
+ await prefetchUrls(resolvedUrls, signal);
11204
+ }
11205
+ async function warmTemplateFromForm(options) {
11206
+ const { signal, imageProxyUrl, ...resolveOpts } = options;
11207
+ const resolved = await resolveFromForm(resolveOpts);
11208
+ if (signal == null ? void 0 : signal.aborted) return;
11209
+ await warmResolvedTemplateForPreview(resolved.config, { signal, imageProxyUrl });
10944
11210
  }
10945
11211
  exports.PixldocsPreview = PixldocsPreview;
10946
11212
  exports.PixldocsRenderer = PixldocsRenderer;
10947
11213
  exports.applyThemeToConfig = applyThemeToConfig;
10948
11214
  exports.collectFontDescriptorsFromConfig = collectFontDescriptorsFromConfig;
10949
11215
  exports.collectFontsFromConfig = collectFontsFromConfig;
11216
+ exports.collectImageUrls = collectImageUrls;
10950
11217
  exports.ensureFontsForResolvedConfig = ensureFontsForResolvedConfig;
10951
11218
  exports.loadGoogleFontCSS = loadGoogleFontCSS;
10952
11219
  exports.normalizeFontFamily = normalizeFontFamily;
10953
11220
  exports.resolveFromForm = resolveFromForm;
10954
11221
  exports.resolveTemplateData = resolveTemplateData;
11222
+ exports.warmResolvedTemplateForPreview = warmResolvedTemplateForPreview;
11223
+ exports.warmTemplateFromForm = warmTemplateFromForm;
10955
11224
  //# sourceMappingURL=index.cjs.map