@pixldocs/canvas-renderer 0.5.18 → 0.5.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
@@ -11863,6 +11863,83 @@ async function ensureFontsForResolvedConfig(config) {
11863
11863
  });
11864
11864
  }
11865
11865
  }
11866
+ const TEXT_TYPES = /* @__PURE__ */ new Set(["textbox", "text", "i-text"]);
11867
+ function getTextLines(obj) {
11868
+ const lines = Array.isArray(obj.textLines) && obj.textLines.length ? obj.textLines : String(obj.text || "").split("\n");
11869
+ return lines.map((line) => Array.isArray(line) ? line.join("") : String(line ?? ""));
11870
+ }
11871
+ function measureLineWidth(obj, line) {
11872
+ if (typeof document === "undefined") return 0;
11873
+ const canvas = document.createElement("canvas");
11874
+ const ctx = canvas.getContext("2d");
11875
+ if (!ctx) return 0;
11876
+ const fontStyle = obj.fontStyle || "normal";
11877
+ const fontWeight = obj.fontWeight || 400;
11878
+ const fontSize = obj.fontSize || 16;
11879
+ const fontFamily = obj.fontFamily || "sans-serif";
11880
+ ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`;
11881
+ const charSpacing = Number(obj.charSpacing || 0) / 1e3 * fontSize;
11882
+ return ctx.measureText(line).width + Math.max(0, line.length - 1) * charSpacing;
11883
+ }
11884
+ function getFabricCanvasFromContainer(container) {
11885
+ const registry2 = window.__fabricCanvasRegistry;
11886
+ if (registry2 instanceof Map) {
11887
+ for (const entry of registry2.values()) {
11888
+ const canvas = (entry == null ? void 0 : entry.canvas) || entry;
11889
+ if (!canvas || typeof canvas.toSVG !== "function") continue;
11890
+ const el = canvas.lowerCanvasEl || canvas.upperCanvasEl;
11891
+ if (el && container.contains(el)) return canvas;
11892
+ }
11893
+ }
11894
+ return null;
11895
+ }
11896
+ function stabilizeFabricTextObjects(fabricInstance) {
11897
+ var _a, _b, _c;
11898
+ if (!(fabricInstance == null ? void 0 : fabricInstance.getObjects)) return;
11899
+ clearFabricCharCache();
11900
+ clearMeasurementCache();
11901
+ const walk = (obj) => {
11902
+ var _a2, _b2, _c2, _d;
11903
+ if (!obj) return;
11904
+ const children = Array.isArray(obj._objects) ? obj._objects : Array.isArray(obj.objects) ? obj.objects : [];
11905
+ if (children.length) children.forEach(walk);
11906
+ const isTextObject = typeof obj.text === "string" && typeof obj.initDimensions === "function" && (TEXT_TYPES.has(obj.type) || obj.isEditing !== void 0);
11907
+ if (!isTextObject) return;
11908
+ const saved = { width: obj.width, scaleX: obj.scaleX, scaleY: obj.scaleY };
11909
+ const reset = () => {
11910
+ var _a3;
11911
+ (_a3 = obj._clearCache) == null ? void 0 : _a3.call(obj);
11912
+ obj.__charBounds = [];
11913
+ obj.__lineWidths = [];
11914
+ obj.__lineHeights = [];
11915
+ obj.__graphemeLines = [];
11916
+ obj._textLines = [];
11917
+ obj.textLines = [];
11918
+ obj._styleMap = null;
11919
+ obj.styleMap = null;
11920
+ obj.dirty = true;
11921
+ };
11922
+ reset();
11923
+ obj.initDimensions();
11924
+ if (saved.width != null) {
11925
+ (_a2 = obj.set) == null ? void 0 : _a2.call(obj, { width: saved.width, scaleX: saved.scaleX, scaleY: saved.scaleY });
11926
+ reset();
11927
+ obj.initDimensions();
11928
+ (_b2 = obj.set) == null ? void 0 : _b2.call(obj, { width: saved.width, scaleX: saved.scaleX, scaleY: saved.scaleY });
11929
+ }
11930
+ if (obj.underline || obj.linethrough) {
11931
+ const lineWidths = Array.isArray(obj.__lineWidths) ? obj.__lineWidths : [];
11932
+ obj.__lineWidths = getTextLines(obj).map((line, index) => Math.max(lineWidths[index] || 0, measureLineWidth(obj, line)));
11933
+ }
11934
+ obj.dirty = true;
11935
+ (_c2 = obj._clearCache) == null ? void 0 : _c2.call(obj);
11936
+ (_d = obj.setCoords) == null ? void 0 : _d.call(obj);
11937
+ };
11938
+ fabricInstance.getObjects().forEach(walk);
11939
+ (_a = fabricInstance.calcOffset) == null ? void 0 : _a.call(fabricInstance);
11940
+ (_b = fabricInstance.renderAll) == null ? void 0 : _b.call(fabricInstance);
11941
+ (_c = fabricInstance.requestRenderAll) == null ? void 0 : _c.call(fabricInstance);
11942
+ }
11866
11943
  function PixldocsPreview(props) {
11867
11944
  const {
11868
11945
  pageIndex = 0,
@@ -11959,6 +12036,13 @@ function PixldocsPreview(props) {
11959
12036
  () => `${pageIndex}-${fontsReadyVersion}-${stabilizationPass}`,
11960
12037
  [pageIndex, fontsReadyVersion, stabilizationPass]
11961
12038
  );
12039
+ const previewWrapRef = react.useCallback((node) => {
12040
+ if (!node || !config) return;
12041
+ requestAnimationFrame(() => {
12042
+ const fabricCanvas = getFabricCanvasFromContainer(node);
12043
+ if (fabricCanvas) stabilizeFabricTextObjects(fabricCanvas);
12044
+ });
12045
+ }, [config, previewKey]);
11962
12046
  react.useEffect(() => {
11963
12047
  if (isResolveMode) return;
11964
12048
  if (!config) {
@@ -11989,7 +12073,7 @@ function PixldocsPreview(props) {
11989
12073
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style: { ...style, display: "flex", alignItems: "center", justifyContent: "center", minHeight: 200 }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { color: "#888", fontSize: 14 }, children: "Loading preview..." }) });
11990
12074
  }
11991
12075
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: { ...style, position: "relative" }, children: [
11992
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { visibility: canvasSettled ? "visible" : "hidden" }, children: /* @__PURE__ */ jsxRuntime.jsx(
12076
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: previewWrapRef, style: { visibility: canvasSettled ? "visible" : "hidden" }, children: /* @__PURE__ */ jsxRuntime.jsx(
11993
12077
  PreviewCanvas,
11994
12078
  {
11995
12079
  config,
@@ -12004,6 +12088,66 @@ function PixldocsPreview(props) {
12004
12088
  !canvasSettled && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", minHeight: 200 }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { color: "#888", fontSize: 14 }, children: "Loading preview..." }) })
12005
12089
  ] });
12006
12090
  }
12091
+ const inlinedAssetCache = /* @__PURE__ */ new Map();
12092
+ function shouldInlineImageUrl(url) {
12093
+ if (!url || url.startsWith("data:")) return false;
12094
+ if (url.startsWith("blob:")) return true;
12095
+ if (url.startsWith("/") && !url.startsWith("//")) return true;
12096
+ try {
12097
+ const parsed = new URL(url, window.location.href);
12098
+ const current = new URL(window.location.href);
12099
+ const host = parsed.hostname.toLowerCase();
12100
+ return parsed.origin === current.origin || host === "localhost" || host === "127.0.0.1" || host === "0.0.0.0" || host.endsWith(".local") || /^(10\.|192\.168\.|169\.254\.)/.test(host);
12101
+ } catch {
12102
+ return false;
12103
+ }
12104
+ }
12105
+ async function imageUrlToDataUrl(url) {
12106
+ if (inlinedAssetCache.has(url)) return inlinedAssetCache.get(url) ?? null;
12107
+ try {
12108
+ const response = await fetch(url);
12109
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
12110
+ const blob = await response.blob();
12111
+ if (blob.type.includes("text/html")) throw new Error("Expected image but got text/html");
12112
+ const dataUrl = await new Promise((resolve, reject) => {
12113
+ const reader = new FileReader();
12114
+ reader.onloadend = () => resolve(String(reader.result || ""));
12115
+ reader.onerror = reject;
12116
+ reader.readAsDataURL(blob);
12117
+ });
12118
+ inlinedAssetCache.set(url, dataUrl);
12119
+ return dataUrl;
12120
+ } catch (error) {
12121
+ console.warn("[@pixldocs/canvas-renderer] Failed to inline image asset:", url, error);
12122
+ inlinedAssetCache.set(url, null);
12123
+ return null;
12124
+ }
12125
+ }
12126
+ async function inlineNodeAssets(node) {
12127
+ if (node.type === "image") {
12128
+ const url = typeof node.src === "string" && node.src.trim() ? node.src.trim() : typeof node.imageUrl === "string" ? node.imageUrl.trim() : "";
12129
+ if (shouldInlineImageUrl(url)) {
12130
+ const dataUrl = await imageUrlToDataUrl(url);
12131
+ if (dataUrl) {
12132
+ node.src = dataUrl;
12133
+ node.imageUrl = dataUrl;
12134
+ }
12135
+ }
12136
+ }
12137
+ if (Array.isArray(node.children)) {
12138
+ await Promise.all(node.children.map(inlineNodeAssets));
12139
+ }
12140
+ }
12141
+ async function inlineBrowserReachableImageAssets(config) {
12142
+ if (typeof window === "undefined" || typeof fetch === "undefined" || typeof FileReader === "undefined") {
12143
+ return config;
12144
+ }
12145
+ const cloned = JSON.parse(JSON.stringify(config));
12146
+ await Promise.all(
12147
+ (cloned.pages || []).flatMap((page) => (page.children || []).map(inlineNodeAssets))
12148
+ );
12149
+ return cloned;
12150
+ }
12007
12151
  class PixldocsRenderer {
12008
12152
  constructor(config) {
12009
12153
  __publicField(this, "config");
@@ -12014,21 +12158,22 @@ class PixldocsRenderer {
12014
12158
  * Mounts a hidden PreviewCanvas component and captures the Fabric canvas output.
12015
12159
  */
12016
12160
  async render(templateConfig, options = {}) {
12161
+ const renderConfig = await inlineBrowserReachableImageAssets(templateConfig);
12017
12162
  const pageIndex = options.pageIndex ?? 0;
12018
12163
  const format = options.format ?? "png";
12019
12164
  const quality = options.quality ?? 0.92;
12020
12165
  const pixelRatio = options.pixelRatio ?? this.config.pixelRatio ?? 2;
12021
- const canvasWidth = templateConfig.canvas.width;
12022
- const canvasHeight = templateConfig.canvas.height;
12023
- const page = templateConfig.pages[pageIndex];
12166
+ const canvasWidth = renderConfig.canvas.width;
12167
+ const canvasHeight = renderConfig.canvas.height;
12168
+ const page = renderConfig.pages[pageIndex];
12024
12169
  if (!page) {
12025
- throw new Error(`Page index ${pageIndex} not found (template has ${templateConfig.pages.length} pages)`);
12170
+ throw new Error(`Page index ${pageIndex} not found (template has ${renderConfig.pages.length} pages)`);
12026
12171
  }
12027
- await ensureFontsForResolvedConfig(templateConfig);
12172
+ await ensureFontsForResolvedConfig(renderConfig);
12028
12173
  const { setPackageApiUrl: setPackageApiUrl2 } = await Promise.resolve().then(() => appApi);
12029
12174
  setPackageApiUrl2(this.config.imageProxyUrl);
12030
12175
  const dataUrl = await this.renderPageViaPreviewCanvas(
12031
- templateConfig,
12176
+ renderConfig,
12032
12177
  pageIndex,
12033
12178
  pixelRatio,
12034
12179
  format,
@@ -12080,16 +12225,17 @@ class PixldocsRenderer {
12080
12225
  * This is the key building block for client-side vector PDF export.
12081
12226
  */
12082
12227
  async renderPageSvg(templateConfig, pageIndex = 0) {
12083
- const page = templateConfig.pages[pageIndex];
12228
+ const renderConfig = await inlineBrowserReachableImageAssets(templateConfig);
12229
+ const page = renderConfig.pages[pageIndex];
12084
12230
  if (!page) {
12085
- throw new Error(`Page index ${pageIndex} not found (template has ${templateConfig.pages.length} pages)`);
12231
+ throw new Error(`Page index ${pageIndex} not found (template has ${renderConfig.pages.length} pages)`);
12086
12232
  }
12087
- await ensureFontsForResolvedConfig(templateConfig);
12233
+ await ensureFontsForResolvedConfig(renderConfig);
12088
12234
  const { setPackageApiUrl: setPackageApiUrl2 } = await Promise.resolve().then(() => appApi);
12089
12235
  setPackageApiUrl2(this.config.imageProxyUrl);
12090
- const canvasWidth = templateConfig.canvas.width;
12091
- const canvasHeight = templateConfig.canvas.height;
12092
- return this.captureSvgViaPreviewCanvas(templateConfig, pageIndex, canvasWidth, canvasHeight);
12236
+ const canvasWidth = renderConfig.canvas.width;
12237
+ const canvasHeight = renderConfig.canvas.height;
12238
+ return this.captureSvgViaPreviewCanvas(renderConfig, pageIndex, canvasWidth, canvasHeight);
12093
12239
  }
12094
12240
  /**
12095
12241
  * Render all pages and return SVG strings for each.
@@ -12437,7 +12583,7 @@ class PixldocsRenderer {
12437
12583
  cleanup();
12438
12584
  reject(new Error("Render timeout (30s)"));
12439
12585
  }, 3e4);
12440
- let stabilizationPass = 0;
12586
+ let finished = false;
12441
12587
  const cleanup = () => {
12442
12588
  clearTimeout(timeout);
12443
12589
  try {
@@ -12447,22 +12593,12 @@ class PixldocsRenderer {
12447
12593
  container.remove();
12448
12594
  };
12449
12595
  const onReady = () => {
12450
- if (stabilizationPass === 0) {
12451
- stabilizationPass = 1;
12452
- root.render(
12453
- react.createElement(PreviewCanvas2, {
12454
- config,
12455
- pageIndex,
12456
- zoom: pixelRatio,
12457
- absoluteZoom: true,
12458
- skipFontReadyWait: true,
12459
- onReady
12460
- })
12461
- );
12462
- return;
12463
- }
12596
+ if (finished) return;
12597
+ finished = true;
12464
12598
  this.waitForCanvasScene(container, config, pageIndex).then(async () => {
12465
12599
  try {
12600
+ await this.waitForStableTextMetrics(container, config);
12601
+ await this.waitForCanvasScene(container, config, pageIndex, 2500, 50);
12466
12602
  const fabricInstance = this.getFabricCanvasFromContainer(container);
12467
12603
  const expectedImageCount = this.getExpectedImageCount(config, pageIndex);
12468
12604
  await this.waitForCanvasImages(container, expectedImageCount);
@@ -12533,7 +12669,7 @@ class PixldocsRenderer {
12533
12669
  cleanup();
12534
12670
  reject(new Error("SVG render timeout (30s)"));
12535
12671
  }, 3e4);
12536
- let stabilizationPass = 0;
12672
+ let finished = false;
12537
12673
  const cleanup = () => {
12538
12674
  clearTimeout(timeout);
12539
12675
  try {
@@ -12543,23 +12679,13 @@ class PixldocsRenderer {
12543
12679
  container.remove();
12544
12680
  };
12545
12681
  const onReady = () => {
12546
- if (stabilizationPass === 0) {
12547
- stabilizationPass = 1;
12548
- root.render(
12549
- react.createElement(PreviewCanvas2, {
12550
- config,
12551
- pageIndex,
12552
- zoom: 1,
12553
- absoluteZoom: true,
12554
- skipFontReadyWait: true,
12555
- onReady
12556
- })
12557
- );
12558
- return;
12559
- }
12682
+ if (finished) return;
12683
+ finished = true;
12560
12684
  this.waitForCanvasScene(container, config, pageIndex).then(async () => {
12561
12685
  var _a, _b;
12562
12686
  try {
12687
+ await this.waitForStableTextMetrics(container, config);
12688
+ await this.waitForCanvasScene(container, config, pageIndex, 2500, 50);
12563
12689
  const fabricInstance = this.getFabricCanvasFromContainer(container);
12564
12690
  if (!fabricInstance) {
12565
12691
  cleanup();
@@ -12679,16 +12805,7 @@ class PixldocsRenderer {
12679
12805
  * using the global __fabricCanvasRegistry (set by PageCanvas).
12680
12806
  */
12681
12807
  getFabricCanvasFromContainer(container) {
12682
- const registry2 = window.__fabricCanvasRegistry;
12683
- if (registry2 instanceof Map) {
12684
- for (const entry of registry2.values()) {
12685
- const canvas = (entry == null ? void 0 : entry.canvas) || entry;
12686
- if (!canvas || typeof canvas.toSVG !== "function") continue;
12687
- const el = canvas.lowerCanvasEl || canvas.upperCanvasEl;
12688
- if (el && container.contains(el)) return canvas;
12689
- }
12690
- }
12691
- return null;
12808
+ return getFabricCanvasFromContainer(container);
12692
12809
  }
12693
12810
  async waitForStableTextMetrics(container, config) {
12694
12811
  if (typeof document !== "undefined") {
@@ -12702,59 +12819,7 @@ class PixldocsRenderer {
12702
12819
  clearFabricCharCache();
12703
12820
  clearMeasurementCache();
12704
12821
  };
12705
- const reflowTextboxes = () => {
12706
- var _a, _b, _c;
12707
- const walk = (obj) => {
12708
- var _a2, _b2, _c2, _d;
12709
- if (!obj) return;
12710
- const children = Array.isArray(obj._objects) ? obj._objects : Array.isArray(obj.objects) ? obj.objects : [];
12711
- if (children.length) children.forEach(walk);
12712
- const isTextObject = typeof obj.text === "string" && typeof obj.initDimensions === "function" && (obj.type === "textbox" || obj.type === "text" || obj.type === "i-text" || obj.isEditing !== void 0);
12713
- if (isTextObject) {
12714
- const saved = {
12715
- width: obj.width,
12716
- scaleX: obj.scaleX,
12717
- scaleY: obj.scaleY
12718
- };
12719
- const resetTextboxLayoutInternals = () => {
12720
- var _a3;
12721
- (_a3 = obj._clearCache) == null ? void 0 : _a3.call(obj);
12722
- obj.__charBounds = [];
12723
- obj.__lineWidths = [];
12724
- obj.__lineHeights = [];
12725
- obj.__graphemeLines = [];
12726
- obj._textLines = [];
12727
- obj.textLines = [];
12728
- obj._styleMap = null;
12729
- obj.styleMap = null;
12730
- obj.dirty = true;
12731
- };
12732
- resetTextboxLayoutInternals();
12733
- obj.initDimensions();
12734
- if (saved.width != null) {
12735
- (_a2 = obj.set) == null ? void 0 : _a2.call(obj, {
12736
- width: saved.width,
12737
- scaleX: saved.scaleX,
12738
- scaleY: saved.scaleY
12739
- });
12740
- resetTextboxLayoutInternals();
12741
- obj.initDimensions();
12742
- }
12743
- (_b2 = obj.set) == null ? void 0 : _b2.call(obj, {
12744
- width: saved.width,
12745
- scaleX: saved.scaleX,
12746
- scaleY: saved.scaleY,
12747
- dirty: true
12748
- });
12749
- (_c2 = obj._clearCache) == null ? void 0 : _c2.call(obj);
12750
- (_d = obj.setCoords) == null ? void 0 : _d.call(obj);
12751
- }
12752
- };
12753
- fabricInstance.getObjects().forEach(walk);
12754
- (_a = fabricInstance.calcOffset) == null ? void 0 : _a.call(fabricInstance);
12755
- (_b = fabricInstance.renderAll) == null ? void 0 : _b.call(fabricInstance);
12756
- (_c = fabricInstance.requestRenderAll) == null ? void 0 : _c.call(fabricInstance);
12757
- };
12822
+ const reflowTextboxes = () => stabilizeFabricTextObjects(fabricInstance);
12758
12823
  clearTextMetricCaches();
12759
12824
  await waitForPaint();
12760
12825
  reflowTextboxes();
@@ -14510,6 +14575,12 @@ function convertTextDecorationsToLines(svg) {
14510
14575
  } else {
14511
14576
  textWidth = content.length * fontSize * 0.6;
14512
14577
  }
14578
+ if (typeof tspan.getComputedTextLength === "function") {
14579
+ try {
14580
+ textWidth = Math.max(textWidth, tspan.getComputedTextLength());
14581
+ } catch {
14582
+ }
14583
+ }
14513
14584
  const underlineY = y + fontSize * 0.15;
14514
14585
  const thickness = Math.max(0.5, fontSize * 0.066667);
14515
14586
  const line = doc.createElementNS("http://www.w3.org/2000/svg", "line");