@pixldocs/canvas-renderer 0.5.95 → 0.5.97

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
@@ -415,33 +415,6 @@ function measureTextHeight(element) {
415
415
  return fallbackEstimateHeight(element);
416
416
  }
417
417
  }
418
- function getMinTextWidth(element) {
419
- if (element.type !== "text" || !element.text) {
420
- return typeof element.width === "number" ? element.width : 200;
421
- }
422
- try {
423
- const textbox = new fabric__namespace.Textbox(element.text, {
424
- width: 1e4,
425
- fontSize: element.fontSize || 16,
426
- fontFamily: element.fontFamily || "Open Sans",
427
- fontWeight: element.fontWeight || 400,
428
- fontStyle: element.fontStyle || "normal",
429
- lineHeight: element.lineHeight || 1.2,
430
- charSpacing: element.charSpacing || 0,
431
- splitByGrapheme: element.splitByGrapheme ?? element.wordWrap === "break-word",
432
- left: 0,
433
- top: 0
434
- });
435
- textbox.initDimensions();
436
- const lineWidths = textbox.__lineWidths;
437
- if (lineWidths && lineWidths.length > 0) {
438
- const maxLine = Math.max(...lineWidths);
439
- return Math.ceil(maxLine) + 2;
440
- }
441
- } catch (_) {
442
- }
443
- return typeof element.width === "number" ? element.width : 200;
444
- }
445
418
  function fallbackEstimateHeight(element) {
446
419
  if (element.type !== "text" || !element.text) {
447
420
  return element.height || 20;
@@ -2797,11 +2770,22 @@ const waitUntilFontsAvailable = async (fontFamilies, options) => {
2797
2770
  }
2798
2771
  };
2799
2772
  const clearFabricCharCache = () => {
2773
+ var _a;
2800
2774
  const fabricAny = fabric__namespace;
2775
+ if (fabricAny.cache && typeof fabricAny.cache.clearFontCache === "function") {
2776
+ fabricAny.cache.clearFontCache();
2777
+ }
2778
+ if (((_a = fabricAny.cache) == null ? void 0 : _a.charWidthsCache) instanceof Map) {
2779
+ fabricAny.cache.charWidthsCache.clear();
2780
+ }
2801
2781
  if (typeof fabricAny.charWidthsCache === "object" && fabricAny.charWidthsCache !== null) {
2802
- Object.keys(fabricAny.charWidthsCache).forEach((key) => {
2803
- delete fabricAny.charWidthsCache[key];
2804
- });
2782
+ if (fabricAny.charWidthsCache instanceof Map) {
2783
+ fabricAny.charWidthsCache.clear();
2784
+ } else {
2785
+ Object.keys(fabricAny.charWidthsCache).forEach((key) => {
2786
+ delete fabricAny.charWidthsCache[key];
2787
+ });
2788
+ }
2805
2789
  }
2806
2790
  if (typeof fabric__namespace.util !== "undefined" && typeof fabric__namespace.util.clearFabricFontCache === "function") {
2807
2791
  fabric__namespace.util.clearFabricFontCache();
@@ -5576,6 +5560,8 @@ function createText(element) {
5576
5560
  const targetScaleY = element.scaleY ?? 1;
5577
5561
  const textbox = new fabric__namespace.Textbox(text, {
5578
5562
  width: targetWidth,
5563
+ minWidth: 1,
5564
+ dynamicMinWidth: 0,
5579
5565
  scaleX: targetScaleX,
5580
5566
  scaleY: targetScaleY,
5581
5567
  fontSize,
@@ -5596,6 +5582,8 @@ function createText(element) {
5596
5582
  textbox.initDimensions();
5597
5583
  textbox.set({
5598
5584
  width: targetWidth,
5585
+ minWidth: 1,
5586
+ dynamicMinWidth: 0,
5599
5587
  scaleX: targetScaleX,
5600
5588
  scaleY: targetScaleY
5601
5589
  });
@@ -5605,6 +5593,7 @@ function createText(element) {
5605
5593
  const scaleYAfterSet = textbox.scaleY ?? 1;
5606
5594
  if (Math.abs(widthAfterSet - targetWidth) > 0.01 || Math.abs(scaleXAfterSet - targetScaleX) > 0.01 || Math.abs(scaleYAfterSet - targetScaleY) > 0.01) {
5607
5595
  textbox.width = targetWidth;
5596
+ textbox.dynamicMinWidth = 0;
5608
5597
  textbox.scaleX = targetScaleX;
5609
5598
  textbox.scaleY = targetScaleY;
5610
5599
  textbox.setCoords();
@@ -6451,6 +6440,48 @@ const PageCanvas = react.forwardRef(
6451
6440
  if (!canvas2) return false;
6452
6441
  return !!canvas2._currentTransform || !!canvas2.__isUserTransforming;
6453
6442
  }, []);
6443
+ const reflowTextboxesFromCurrentElements = react.useCallback((canvas2) => {
6444
+ const elementById = new Map(elementsRef.current.map((el) => [el.id, el]));
6445
+ let didReflow = false;
6446
+ const reflowObject = (obj) => {
6447
+ var _a;
6448
+ if (obj instanceof fabric__namespace.Group) {
6449
+ (_a = obj._objects) == null ? void 0 : _a.forEach(reflowObject);
6450
+ obj.dirty = true;
6451
+ return;
6452
+ }
6453
+ if (!(obj instanceof fabric__namespace.Textbox)) return;
6454
+ const id = getObjectId(obj);
6455
+ const element = id ? elementById.get(id) : void 0;
6456
+ if (!element) return;
6457
+ const targetWidth = Math.max(1, Number(element.width) > 0 ? Number(element.width) : Number(obj.width ?? 200));
6458
+ const overflowPolicy = element.overflowPolicy || "grow-and-push";
6459
+ const splitByGrapheme = overflowPolicy === "auto-shrink" ? false : element.splitByGrapheme ?? element.wordWrap === "break-word";
6460
+ obj.set({
6461
+ width: targetWidth,
6462
+ minWidth: 1,
6463
+ dynamicMinWidth: 0,
6464
+ text: element.text || "Text",
6465
+ fontSize: element.fontSize || 16,
6466
+ fontFamily: element.fontFamily || "Open Sans",
6467
+ fontWeight: element.fontWeight || 400,
6468
+ fontStyle: element.fontStyle || "normal",
6469
+ lineHeight: element.lineHeight || 1.2,
6470
+ charSpacing: element.charSpacing || 0,
6471
+ splitByGrapheme
6472
+ });
6473
+ obj.initDimensions();
6474
+ if (Math.abs((obj.width ?? 0) - targetWidth) > 0.01) {
6475
+ obj.width = targetWidth;
6476
+ }
6477
+ obj.dynamicMinWidth = 0;
6478
+ obj.setCoords();
6479
+ obj.dirty = true;
6480
+ didReflow = true;
6481
+ };
6482
+ canvas2.getObjects().forEach(reflowObject);
6483
+ if (didReflow) canvas2.requestRenderAll();
6484
+ }, []);
6454
6485
  react.useEffect(() => {
6455
6486
  if (!canvasElRef.current) return;
6456
6487
  const zoom2 = workspaceZoom || 1;
@@ -6531,6 +6562,8 @@ const PageCanvas = react.forwardRef(
6531
6562
  };
6532
6563
  initFonts();
6533
6564
  const persistTextDimensionsAfterFontLoad = (canvas2) => {
6565
+ reflowTextboxesFromCurrentElements(canvas2);
6566
+ if (isPreviewMode) return;
6534
6567
  const state = useEditorStore.getState();
6535
6568
  const page = state.canvas.pages.find((p) => p.id === pageId);
6536
6569
  if (!page) return;
@@ -9127,10 +9160,11 @@ const PageCanvas = react.forwardRef(
9127
9160
  text = result;
9128
9161
  }
9129
9162
  }
9130
- const minWidth = getMinTextWidth(element);
9131
- const textboxWidth = overflowPolicy === "auto-shrink" ? fixedWidth : Math.max(storedWidth, minWidth);
9163
+ const textboxWidth = fixedWidth;
9132
9164
  obj.set({
9133
9165
  width: textboxWidth,
9166
+ minWidth: 1,
9167
+ dynamicMinWidth: 0,
9134
9168
  fontSize,
9135
9169
  fontFamily: element.fontFamily || "Open Sans",
9136
9170
  fill: element.fill || "#1a1a1a",
@@ -9150,6 +9184,7 @@ const PageCanvas = react.forwardRef(
9150
9184
  if (Math.abs((obj.width ?? 0) - textboxWidth) > 0.01) {
9151
9185
  obj.width = textboxWidth;
9152
9186
  }
9187
+ obj.dynamicMinWidth = 0;
9153
9188
  obj.setCoords();
9154
9189
  obj.dirty = true;
9155
9190
  try {
@@ -12938,7 +12973,6 @@ function PixldocsPreview(props) {
12938
12973
  const [isLoading, setIsLoading] = react.useState(false);
12939
12974
  const [fontsReady, setFontsReady] = react.useState(false);
12940
12975
  const [canvasSettled, setCanvasSettled] = react.useState(false);
12941
- const [stabilizationPass, setStabilizationPass] = react.useState(0);
12942
12976
  const isResolveMode = !("config" in props && props.config);
12943
12977
  react.useEffect(() => {
12944
12978
  if (!isResolveMode) {
@@ -13009,21 +13043,16 @@ function PixldocsPreview(props) {
13009
13043
  isResolveMode ? props.themeId : void 0
13010
13044
  ]);
13011
13045
  const config = isResolveMode ? resolvedConfig : props.config;
13012
- const previewKey = react.useMemo(
13013
- () => `${pageIndex}-${stabilizationPass}`,
13014
- [pageIndex, stabilizationPass]
13015
- );
13046
+ const previewKey = react.useMemo(() => `${pageIndex}`, [pageIndex]);
13016
13047
  react.useEffect(() => {
13017
13048
  if (isResolveMode) return;
13018
13049
  if (!config) {
13019
13050
  setFontsReady(false);
13020
13051
  setCanvasSettled(false);
13021
- setStabilizationPass(0);
13022
13052
  return;
13023
13053
  }
13024
13054
  setFontsReady(false);
13025
13055
  setCanvasSettled(false);
13026
- setStabilizationPass(0);
13027
13056
  let cancelled = false;
13028
13057
  const hasAutoShrink = configHasAutoShrinkText$1(config);
13029
13058
  const waitMs = hasAutoShrink ? 4e3 : 1800;
@@ -13046,16 +13075,10 @@ function PixldocsPreview(props) {
13046
13075
  };
13047
13076
  }, [isResolveMode, config]);
13048
13077
  const handleCanvasReady = react.useCallback(() => {
13049
- if (stabilizationPass === 0) {
13050
- console.log(PREVIEW_DEBUG_PREFIX, "canvas-ready-pass", { pageIndex, stabilizationPass, action: "stabilize-again" });
13051
- setCanvasSettled(false);
13052
- setStabilizationPass(1);
13053
- return;
13054
- }
13055
- console.log(PREVIEW_DEBUG_PREFIX, "canvas-ready-pass", { pageIndex, stabilizationPass, action: "settled" });
13078
+ console.log(PREVIEW_DEBUG_PREFIX, "canvas-ready", { pageIndex, action: "settled" });
13056
13079
  setCanvasSettled(true);
13057
13080
  onReady == null ? void 0 : onReady();
13058
- }, [onReady, pageIndex, stabilizationPass]);
13081
+ }, [onReady, pageIndex]);
13059
13082
  if (isLoading) {
13060
13083
  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..." }) });
13061
13084
  }
@@ -14577,6 +14600,10 @@ function getEmbeddedJsPDFFontName(fontName, weight, isItalic = false) {
14577
14600
  function isFontAvailable(fontName) {
14578
14601
  return fontName in FONT_FILES;
14579
14602
  }
14603
+ const registeredFamilies = /* @__PURE__ */ new Set();
14604
+ function isFamilyEmbedded(family) {
14605
+ return registeredFamilies.has(family);
14606
+ }
14580
14607
  const ttfCache = /* @__PURE__ */ new Map();
14581
14608
  async function fetchTTFAsBase64(url) {
14582
14609
  const cached = ttfCache.get(url);
@@ -14649,12 +14676,133 @@ async function embedFont(pdf, fontName, weight, fontBaseUrl, isItalic = false) {
14649
14676
  } catch {
14650
14677
  }
14651
14678
  }
14679
+ registeredFamilies.add(fontName);
14652
14680
  return true;
14653
14681
  } catch (e) {
14654
14682
  console.warn(`[pdf-fonts] Failed to embed ${fontName} w${weight}:`, e);
14655
14683
  return false;
14656
14684
  }
14657
14685
  }
14686
+ const googleFontNotFound = /* @__PURE__ */ new Set();
14687
+ const fontshareNotFound = /* @__PURE__ */ new Set();
14688
+ function bytesToBase64(bytes) {
14689
+ let binary = "";
14690
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
14691
+ return btoa(binary);
14692
+ }
14693
+ async function fetchGoogleFontTTF(fontFamily, weight, isItalic = false) {
14694
+ const cacheKey = `gf:${fontFamily}:${weight}:${isItalic ? "i" : "n"}`;
14695
+ if (ttfCache.has(cacheKey)) return ttfCache.get(cacheKey);
14696
+ if (googleFontNotFound.has(fontFamily)) return null;
14697
+ try {
14698
+ const ital = isItalic ? "1" : "0";
14699
+ const cssUrl = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(
14700
+ fontFamily
14701
+ )}:ital,wght@${ital},${weight}&display=swap`;
14702
+ const cssRes = await fetch(cssUrl, {
14703
+ headers: {
14704
+ "User-Agent": "Mozilla/5.0 (Linux; U; Android 2.2; en-us) AppleWebKit/533.1 (KHTML, like Gecko)"
14705
+ }
14706
+ });
14707
+ if (!cssRes.ok) {
14708
+ if (cssRes.status === 400 || cssRes.status === 404) googleFontNotFound.add(fontFamily);
14709
+ return null;
14710
+ }
14711
+ const css = await cssRes.text();
14712
+ const urlMatch = css.match(/url\(([^)]+)\)\s+format\(['"]?truetype['"]?\)/i) || css.match(/url\(([^)]+)\)/);
14713
+ if (!urlMatch) return null;
14714
+ const ttfUrl = urlMatch[1].replace(/['"]/g, "");
14715
+ const ttfRes = await fetch(ttfUrl);
14716
+ if (!ttfRes.ok) return null;
14717
+ const buf = await ttfRes.arrayBuffer();
14718
+ const bytes = new Uint8Array(buf);
14719
+ if (!isJsPdfEmbeddableTrueType(bytes)) {
14720
+ console.warn(`[pdf-fonts] Google Fonts returned a non-TTF for ${fontFamily} (${weight}); skipping`);
14721
+ return null;
14722
+ }
14723
+ const b64 = bytesToBase64(bytes);
14724
+ ttfCache.set(cacheKey, b64);
14725
+ return b64;
14726
+ } catch (err) {
14727
+ console.warn(`[pdf-fonts] fetchGoogleFontTTF failed for ${fontFamily} (${weight}):`, err);
14728
+ return null;
14729
+ }
14730
+ }
14731
+ async function fetchFontshareTTF(fontFamily, weight, isItalic = false) {
14732
+ const cacheKey = `fs:${fontFamily}:${weight}:${isItalic ? "i" : "n"}`;
14733
+ if (ttfCache.has(cacheKey)) return ttfCache.get(cacheKey);
14734
+ if (fontshareNotFound.has(fontFamily)) return null;
14735
+ const slug = fontFamily.trim().toLowerCase().replace(/\s+/g, "-");
14736
+ try {
14737
+ const styleSuffix = isItalic ? "i" : "";
14738
+ const cssUrl = `https://api.fontshare.com/v2/css?f[]=${slug}@${weight}${styleSuffix}&display=swap`;
14739
+ const cssRes = await fetch(cssUrl);
14740
+ if (!cssRes.ok) {
14741
+ if (cssRes.status === 400 || cssRes.status === 404) fontshareNotFound.add(fontFamily);
14742
+ return null;
14743
+ }
14744
+ const css = await cssRes.text();
14745
+ const ttMatch = css.match(/url\(([^)]+)\)\s+format\(['"]?truetype['"]?\)/i);
14746
+ if (!ttMatch) {
14747
+ fontshareNotFound.add(fontFamily);
14748
+ return null;
14749
+ }
14750
+ let ttfUrl = ttMatch[1].replace(/['"]/g, "").trim();
14751
+ if (ttfUrl.startsWith("//")) ttfUrl = `https:${ttfUrl}`;
14752
+ const ttfRes = await fetch(ttfUrl);
14753
+ if (!ttfRes.ok) return null;
14754
+ const buf = await ttfRes.arrayBuffer();
14755
+ const bytes = new Uint8Array(buf);
14756
+ if (!isJsPdfEmbeddableTrueType(bytes)) return null;
14757
+ const b64 = bytesToBase64(bytes);
14758
+ ttfCache.set(cacheKey, b64);
14759
+ return b64;
14760
+ } catch (err) {
14761
+ console.warn(`[pdf-fonts] fetchFontshareTTF failed for ${fontFamily} (${weight}):`, err);
14762
+ return null;
14763
+ }
14764
+ }
14765
+ function registerJsPdfFont(pdf, fontName, resolvedWeight, isItalic, base64) {
14766
+ const label = FONT_WEIGHT_LABELS[resolvedWeight];
14767
+ const italicSuffix = isItalic ? "Italic" : "";
14768
+ const jsPdfFontName = getEmbeddedJsPDFFontName(fontName, resolvedWeight, isItalic);
14769
+ const fileName = `${getJsPDFFontName(fontName)}-${label}${italicSuffix}.ttf`;
14770
+ try {
14771
+ pdf.addFileToVFS(fileName, base64);
14772
+ pdf.addFont(fileName, jsPdfFontName, "normal");
14773
+ if (fontName !== jsPdfFontName) {
14774
+ try {
14775
+ pdf.addFont(fileName, fontName, "normal");
14776
+ } catch {
14777
+ }
14778
+ }
14779
+ registeredFamilies.add(fontName);
14780
+ return true;
14781
+ } catch (err) {
14782
+ console.warn(`[pdf-fonts] registerJsPdfFont failed for ${fontName}:`, err);
14783
+ return false;
14784
+ }
14785
+ }
14786
+ async function embedFontWithGoogleFallback(pdf, fontName, weight = 400, fontBaseUrl, isItalic = false) {
14787
+ if (FONT_FILES[fontName]) {
14788
+ const ok = await embedFont(pdf, fontName, weight, fontBaseUrl, isItalic);
14789
+ if (ok) return true;
14790
+ }
14791
+ const resolved = resolveFontWeight(weight);
14792
+ const fsB64 = await fetchFontshareTTF(fontName, resolved, isItalic);
14793
+ if (fsB64) return registerJsPdfFont(pdf, fontName, resolved, isItalic, fsB64);
14794
+ if (isItalic) {
14795
+ const fsUpright = await fetchFontshareTTF(fontName, resolved, false);
14796
+ if (fsUpright) return registerJsPdfFont(pdf, fontName, resolved, isItalic, fsUpright);
14797
+ }
14798
+ const b64 = await fetchGoogleFontTTF(fontName, resolved, isItalic);
14799
+ if (b64) return registerJsPdfFont(pdf, fontName, resolved, isItalic, b64);
14800
+ if (isItalic) {
14801
+ const uprightB64 = await fetchGoogleFontTTF(fontName, resolved, false);
14802
+ if (uprightB64) return registerJsPdfFont(pdf, fontName, resolved, isItalic, uprightB64);
14803
+ }
14804
+ return false;
14805
+ }
14658
14806
  async function embedFontsForConfig(pdf, config, fontBaseUrl) {
14659
14807
  const fontKeys = /* @__PURE__ */ new Set();
14660
14808
  const SEP = "";
@@ -14813,7 +14961,7 @@ function rewriteSvgFontsForJsPDF(svgStr) {
14813
14961
  const rawFf = resolveInheritedValue(el, "font-family");
14814
14962
  if (!rawFf) continue;
14815
14963
  const clean = (_a = rawFf.split(",")[0]) == null ? void 0 : _a.replace(/['"]/g, "").trim();
14816
- if (!isFontAvailable(clean)) continue;
14964
+ if (!isFontAvailable(clean) && !isFamilyEmbedded(clean)) continue;
14817
14965
  const weightRaw = resolveInheritedValue(el, "font-weight") || "400";
14818
14966
  const styleRaw = resolveInheritedValue(el, "font-style") || "normal";
14819
14967
  const weight = resolveWeightNum(weightRaw);
@@ -14896,14 +15044,19 @@ async function embedFontsInPdf(pdf, fontFamilies, fontBaseUrl) {
14896
15044
  fontFamilies.add(FONT_FALLBACK_SYMBOLS);
14897
15045
  fontFamilies.add(FONT_FALLBACK_DEVANAGARI);
14898
15046
  for (const family of fontFamilies) {
14899
- if (!isFontAvailable(family)) {
14900
- console.warn(`[pdf-fonts] No TTF mapping for "${family}" — will use Helvetica fallback`);
14901
- continue;
14902
- }
14903
- for (const w of weights) {
15047
+ if (isFontAvailable(family)) {
15048
+ for (const w of weights) {
15049
+ tasks.push(
15050
+ embedFont(pdf, family, w, fontBaseUrl).then((ok) => {
15051
+ if (ok) embedded.add(`${family}${w}`);
15052
+ })
15053
+ );
15054
+ }
15055
+ } else {
14904
15056
  tasks.push(
14905
- embedFont(pdf, family, w, fontBaseUrl).then((ok) => {
14906
- if (ok) embedded.add(`${family}${w}`);
15057
+ embedFontWithGoogleFallback(pdf, family, 400, fontBaseUrl, false).then((ok) => {
15058
+ if (ok) embedded.add(`${family}400`);
15059
+ else console.warn(`[pdf-fonts] No TTF (local/Google/Fontshare) for "${family}" — will use Helvetica fallback`);
14907
15060
  })
14908
15061
  );
14909
15062
  }
@@ -14919,6 +15072,7 @@ const pdfFonts = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProp
14919
15072
  FONT_FILES,
14920
15073
  FONT_WEIGHT_LABELS,
14921
15074
  embedFont,
15075
+ embedFontWithGoogleFallback,
14922
15076
  embedFontsForConfig,
14923
15077
  embedFontsInPdf,
14924
15078
  extractFontFamiliesFromSvgs,