goatdee-canvas 0.0.4 → 0.0.5

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
@@ -3124,6 +3124,34 @@ function waitForImage(img, timeout = 5000) {
3124
3124
  };
3125
3125
  });
3126
3126
  }
3127
+ /**
3128
+ * 加载图片并获取其自然尺寸
3129
+ * @param src - 图片 URL
3130
+ * @param timeout - 超时时间(毫秒)
3131
+ * @returns 图片的自然宽度和高度,加载失败返回 null
3132
+ */
3133
+ async function loadImageDimensions(src, timeout = 5000) {
3134
+ return new Promise((resolve) => {
3135
+ const img = new Image();
3136
+ const timeoutId = setTimeout(() => {
3137
+ resolve(null);
3138
+ }, timeout);
3139
+ img.onload = () => {
3140
+ clearTimeout(timeoutId);
3141
+ if (img.naturalWidth > 0 && img.naturalHeight > 0) {
3142
+ resolve({ width: img.naturalWidth, height: img.naturalHeight });
3143
+ }
3144
+ else {
3145
+ resolve(null);
3146
+ }
3147
+ };
3148
+ img.onerror = () => {
3149
+ clearTimeout(timeoutId);
3150
+ resolve(null);
3151
+ };
3152
+ img.src = src;
3153
+ });
3154
+ }
3127
3155
  /**
3128
3156
  * 等待容器内所有图片加载完成
3129
3157
  * @param container - 容器元素
@@ -11813,7 +11841,7 @@ function shouldConvertAsBackground(styles, cssVariables) {
11813
11841
  * @param styles 元素样式(可选)
11814
11842
  * @returns base64 data URL 或 null
11815
11843
  */
11816
- async function renderSVGToBase64(element, width, height, styles, scale = 2) {
11844
+ async function renderSVGToBase64(element, width, height, styles, scale = 5) {
11817
11845
  var _a, _b, _c;
11818
11846
  if (typeof document === "undefined" || typeof window === "undefined") {
11819
11847
  return null;
@@ -11855,8 +11883,10 @@ async function renderSVGToBase64(element, width, height, styles, scale = 2) {
11855
11883
  // 创建 data URL
11856
11884
  const dataUrl = `data:image/svg+xml,${encodedSVG}`;
11857
11885
  // 使用 canvas 将 SVG 渲染为 PNG
11858
- const pngDataUrl = await convertSVGToPNG(dataUrl, width, height, win, scale);
11859
- return pngDataUrl || dataUrl;
11886
+ const result = await convertSVGToPNG(dataUrl, width, height, win, scale);
11887
+ if (!result)
11888
+ return null;
11889
+ return result;
11860
11890
  }
11861
11891
  catch (e) {
11862
11892
  console.warn("Failed to render SVG to base64:", e);
@@ -11889,7 +11919,7 @@ function resolveCurrentColor(node, color) {
11889
11919
  * 用于处理有复杂滤镜效果的 SVG
11890
11920
  * @param win 元素所在 document 的 window(元素在 iframe 内时传入 iframe.contentWindow)
11891
11921
  */
11892
- async function convertSVGToPNG(svgDataUrl, width, height, win, scale = 2) {
11922
+ async function convertSVGToPNG(svgDataUrl, width, height, win, scale = 5) {
11893
11923
  return new Promise((resolve) => {
11894
11924
  var _a, _b;
11895
11925
  const img = new Image();
@@ -11897,8 +11927,10 @@ async function convertSVGToPNG(svgDataUrl, width, height, win, scale = 2) {
11897
11927
  img.onload = () => {
11898
11928
  try {
11899
11929
  const canvas = document.createElement("canvas");
11900
- canvas.width = width * scale * dpr;
11901
- canvas.height = height * scale * dpr;
11930
+ const actualWidth = width * scale * dpr;
11931
+ const actualHeight = height * scale * dpr;
11932
+ canvas.width = actualWidth;
11933
+ canvas.height = actualHeight;
11902
11934
  const ctx = canvas.getContext("2d");
11903
11935
  if (!ctx) {
11904
11936
  resolve(null);
@@ -11906,7 +11938,13 @@ async function convertSVGToPNG(svgDataUrl, width, height, win, scale = 2) {
11906
11938
  }
11907
11939
  ctx.scale(scale * dpr, scale * dpr);
11908
11940
  ctx.drawImage(img, 0, 0, width, height);
11909
- resolve(canvas.toDataURL("image/png"));
11941
+ resolve({
11942
+ base64: canvas.toDataURL("image/png"),
11943
+ actualWidth,
11944
+ actualHeight,
11945
+ logicalWidth: width,
11946
+ logicalHeight: height,
11947
+ });
11910
11948
  }
11911
11949
  catch (e) {
11912
11950
  console.warn("Failed to convert SVG to PNG:", e);
@@ -11970,6 +12008,7 @@ async function renderElementWithHtml2Canvas(element, styles, rect) {
11970
12008
  const blurRadius = blurMatch ? parseInt(blurMatch[1], 10) : 0;
11971
12009
  // blur 扩散范围约为 3 倍半径(3σ 原则)
11972
12010
  const padding = blurRadius * 3;
12011
+ const dpr = win.devicePixelRatio || 1;
11973
12012
  // 创建临时容器(使用元素所在 document,以便 iframe 内元素在正确上下文中渲染)
11974
12013
  const tempContainer = doc.createElement("div");
11975
12014
  tempContainer.style.position = "fixed";
@@ -11993,7 +12032,7 @@ async function renderElementWithHtml2Canvas(element, styles, rect) {
11993
12032
  // 使用 html2canvas 截图
11994
12033
  const canvas = await html2canvasExports(tempContainer, {
11995
12034
  backgroundColor: null,
11996
- scale: win.devicePixelRatio || 1,
12035
+ scale: dpr,
11997
12036
  logging: false,
11998
12037
  useCORS: true,
11999
12038
  allowTaint: true,
@@ -12005,6 +12044,10 @@ async function renderElementWithHtml2Canvas(element, styles, rect) {
12005
12044
  return {
12006
12045
  base64: canvas.toDataURL("image/png"),
12007
12046
  padding: padding,
12047
+ actualWidth: totalWidth * dpr,
12048
+ actualHeight: totalHeight * dpr,
12049
+ logicalWidth: totalWidth,
12050
+ logicalHeight: totalHeight,
12008
12051
  };
12009
12052
  }
12010
12053
  catch (e) {
@@ -12069,7 +12112,16 @@ async function renderElementWithFilterToBase64(element, width, height, styles) {
12069
12112
  const filterStr = styles.filter;
12070
12113
  // 没有 filter 或 filter:none,走常规 html2canvas 路径
12071
12114
  if (!filterStr || filterStr === 'none') {
12072
- return renderElementWithHtml2Canvas(element, styles, { width, height });
12115
+ const result = await renderElementWithHtml2Canvas(element, styles, { width, height });
12116
+ if (!result)
12117
+ return null;
12118
+ return {
12119
+ ...result,
12120
+ actualWidth: width,
12121
+ actualHeight: height,
12122
+ logicalWidth: width,
12123
+ logicalHeight: height,
12124
+ };
12073
12125
  }
12074
12126
  // html2canvas 完全不支持任何 CSS filter 函数
12075
12127
  //(blur / brightness / contrast / saturate / hue-rotate /
@@ -12126,6 +12178,10 @@ async function renderElementWithFilterToBase64(element, width, height, styles) {
12126
12178
  return {
12127
12179
  base64: finalCanvas.toDataURL('image/png'),
12128
12180
  padding,
12181
+ actualWidth: totalW * dpr,
12182
+ actualHeight: totalH * dpr,
12183
+ logicalWidth: totalW,
12184
+ logicalHeight: totalH,
12129
12185
  };
12130
12186
  }
12131
12187
  catch (e) {
@@ -12480,9 +12536,10 @@ async function renderPseudoElementWithHtml2Canvas(element, pseudoElement, rect,
12480
12536
  }
12481
12537
  }
12482
12538
  // 使用 html2canvas 截图
12539
+ const dpr = win.devicePixelRatio || 1;
12483
12540
  const canvas = await html2canvasExports(tempContainer, {
12484
12541
  backgroundColor: null,
12485
- scale: win.devicePixelRatio || 1,
12542
+ scale: dpr,
12486
12543
  logging: false,
12487
12544
  useCORS: true,
12488
12545
  allowTaint: true,
@@ -12495,6 +12552,10 @@ async function renderPseudoElementWithHtml2Canvas(element, pseudoElement, rect,
12495
12552
  return {
12496
12553
  base64: canvas.toDataURL("image/png"),
12497
12554
  padding: padding,
12555
+ actualWidth: totalWidth * dpr,
12556
+ actualHeight: totalHeight * dpr,
12557
+ logicalWidth: totalWidth,
12558
+ logicalHeight: totalHeight,
12498
12559
  };
12499
12560
  }
12500
12561
  catch (e) {
@@ -12550,6 +12611,7 @@ async function renderMarkerWithHtml2Canvas(element, rect, markerStyles, elementS
12550
12611
  try {
12551
12612
  const totalWidth = Math.ceil(rect.width);
12552
12613
  const totalHeight = Math.ceil(rect.height);
12614
+ const dpr = win.devicePixelRatio || 1;
12553
12615
  const tempContainer = doc.createElement('div');
12554
12616
  tempContainer.style.position = 'fixed';
12555
12617
  tempContainer.style.left = '-9999px';
@@ -12581,7 +12643,7 @@ async function renderMarkerWithHtml2Canvas(element, rect, markerStyles, elementS
12581
12643
  }
12582
12644
  const canvas = await html2canvasExports(tempContainer, {
12583
12645
  backgroundColor: null,
12584
- scale: win.devicePixelRatio || 1,
12646
+ scale: dpr,
12585
12647
  logging: false,
12586
12648
  useCORS: true,
12587
12649
  allowTaint: true,
@@ -12589,13 +12651,200 @@ async function renderMarkerWithHtml2Canvas(element, rect, markerStyles, elementS
12589
12651
  height: totalHeight,
12590
12652
  });
12591
12653
  doc.body.removeChild(tempContainer);
12592
- return { base64: canvas.toDataURL('image/png'), padding: 0 };
12654
+ return {
12655
+ base64: canvas.toDataURL('image/png'),
12656
+ padding: 0,
12657
+ actualWidth: totalWidth * dpr,
12658
+ actualHeight: totalHeight * dpr,
12659
+ logicalWidth: totalWidth,
12660
+ logicalHeight: totalHeight,
12661
+ };
12593
12662
  }
12594
12663
  catch (e) {
12595
12664
  console.error('[html2canvas] Error rendering ::marker:', e);
12596
12665
  return null;
12597
12666
  }
12598
12667
  }
12668
+ /**
12669
+ * 解析 repeating-linear-gradient 参数
12670
+ * 例如:repeating-linear-gradient(45deg, #000, #000 2px, transparent 2px, transparent 6px)
12671
+ * 或:repeating-linear-gradient(45deg, rgb(0,0,0) 0px, rgb(0,0,0) 2px, rgba(0,0,0,0) 2px, rgba(0,0,0,0) 6px)
12672
+ */
12673
+ function parseRepeatingLinearGradient(gradientStr) {
12674
+ // 匹配整个 repeating-linear-gradient(...),使用递归括号匹配
12675
+ let startIdx = gradientStr.indexOf('repeating-linear-gradient(');
12676
+ if (startIdx === -1) {
12677
+ return null;
12678
+ }
12679
+ startIdx += 'repeating-linear-gradient('.length;
12680
+ let parenDepth = 1;
12681
+ let endIdx = startIdx;
12682
+ // 找到匹配的右括号
12683
+ while (endIdx < gradientStr.length && parenDepth > 0) {
12684
+ if (gradientStr[endIdx] === '(')
12685
+ parenDepth++;
12686
+ if (gradientStr[endIdx] === ')')
12687
+ parenDepth--;
12688
+ if (parenDepth > 0)
12689
+ endIdx++;
12690
+ }
12691
+ const content = gradientStr.substring(startIdx, endIdx);
12692
+ // 智能分割:考虑括号嵌套(如 rgb(), rgba())
12693
+ const parts = [];
12694
+ let current = '';
12695
+ parenDepth = 0;
12696
+ for (let i = 0; i < content.length; i++) {
12697
+ const char = content[i];
12698
+ if (char === '(')
12699
+ parenDepth++;
12700
+ if (char === ')')
12701
+ parenDepth--;
12702
+ if (char === ',' && parenDepth === 0) {
12703
+ if (current.trim()) {
12704
+ parts.push(current.trim());
12705
+ }
12706
+ current = '';
12707
+ }
12708
+ else {
12709
+ current += char;
12710
+ }
12711
+ }
12712
+ if (current.trim()) {
12713
+ parts.push(current.trim());
12714
+ }
12715
+ // 解析角度
12716
+ let angle = 180; // 默认向下
12717
+ let colorStopStart = 0;
12718
+ if (parts.length > 0 && parts[0].includes('deg')) {
12719
+ const angleMatch = parts[0].match(/([-\d.]+)deg/);
12720
+ if (angleMatch) {
12721
+ angle = parseFloat(angleMatch[1]);
12722
+ colorStopStart = 1;
12723
+ }
12724
+ }
12725
+ // 解析颜色停止点
12726
+ const colorStops = [];
12727
+ let lastPosition = 0;
12728
+ for (let i = colorStopStart; i < parts.length; i++) {
12729
+ const part = parts[i].trim();
12730
+ // 尝试匹配 "color position" 格式
12731
+ // 支持: "rgb(0,0,0) 2px", "#000 2px", "transparent 2px", "rgba(0,0,0,0) 6px"
12732
+ const withPosMatch = part.match(/^(.+?)\s+([\d.]+)(px|%|em|rem)?$/);
12733
+ if (withPosMatch) {
12734
+ const color = withPosMatch[1].trim();
12735
+ const position = parseFloat(withPosMatch[2]);
12736
+ colorStops.push({ color, position });
12737
+ lastPosition = position;
12738
+ }
12739
+ else {
12740
+ // 没有位置信息,使用上一个位置
12741
+ const color = part.trim();
12742
+ colorStops.push({ color, position: lastPosition });
12743
+ }
12744
+ }
12745
+ if (colorStops.length < 2) {
12746
+ return null;
12747
+ }
12748
+ return { angle, colorStops };
12749
+ }
12750
+ /**
12751
+ * 使用 Canvas 2D 手动渲染 repeating-linear-gradient
12752
+ * html2canvas 不支持 repeating-linear-gradient,需要手动绘制
12753
+ */
12754
+ function renderRepeatingLinearGradientToCanvas(canvas, width, height, gradientStr, dpr) {
12755
+ const parsed = parseRepeatingLinearGradient(gradientStr);
12756
+ if (!parsed) {
12757
+ return false;
12758
+ }
12759
+ const { angle, colorStops } = parsed;
12760
+ if (colorStops.length < 2) {
12761
+ return false;
12762
+ }
12763
+ const ctx = canvas.getContext('2d');
12764
+ if (!ctx) {
12765
+ return false;
12766
+ }
12767
+ canvas.width = width;
12768
+ canvas.height = height;
12769
+ // 计算渐变的重复周期(最后一个颜色停止点的位置)
12770
+ const repeatLength = colorStops[colorStops.length - 1].position;
12771
+ if (repeatLength <= 0) {
12772
+ return false;
12773
+ }
12774
+ // 将角度转换为弧度
12775
+ const angleRad = (angle - 90) * Math.PI / 180;
12776
+ // 计算渐变方向向量
12777
+ const dx = Math.cos(angleRad);
12778
+ const dy = Math.sin(angleRad);
12779
+ // 计算需要覆盖整个画布的渐变长度
12780
+ const diagonal = Math.sqrt(width * width + height * height);
12781
+ const gradientLength = diagonal * 2; // 放大确保覆盖
12782
+ // 计算渐变起点和终点(从画布中心延伸)
12783
+ const centerX = width / 2;
12784
+ const centerY = height / 2;
12785
+ const x0 = centerX - dx * gradientLength / 2;
12786
+ const y0 = centerY - dy * gradientLength / 2;
12787
+ const x1 = centerX + dx * gradientLength / 2;
12788
+ const y1 = centerY + dy * gradientLength / 2;
12789
+ // 创建线性渐变
12790
+ const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
12791
+ // 计算需要重复多少次才能覆盖整个渐变长度
12792
+ const repeatCount = Math.ceil(gradientLength / repeatLength) + 2;
12793
+ // 添加重复的颜色停止点
12794
+ for (let i = 0; i < repeatCount; i++) {
12795
+ const offset = i * repeatLength;
12796
+ for (const stop of colorStops) {
12797
+ const absolutePosition = offset + stop.position;
12798
+ const normalizedPosition = absolutePosition / gradientLength;
12799
+ if (normalizedPosition >= 0 && normalizedPosition <= 1) {
12800
+ gradient.addColorStop(normalizedPosition, stop.color);
12801
+ }
12802
+ }
12803
+ }
12804
+ // 填充渐变
12805
+ ctx.fillStyle = gradient;
12806
+ ctx.fillRect(0, 0, width, height);
12807
+ return true;
12808
+ }
12809
+ /**
12810
+ * 使用 Canvas 2D 渲染 repeating-linear-gradient 为 base64 图片
12811
+ * html2canvas 不支持 repeating-linear-gradient,需要手动绘制
12812
+ * @returns 返回渲染结果,如果失败返回 null
12813
+ */
12814
+ async function renderRepeatingLinearGradientToBase64(element, width, height) {
12815
+ const doc = element.ownerDocument;
12816
+ const win = doc === null || doc === void 0 ? void 0 : doc.defaultView;
12817
+ if (!doc || !win) {
12818
+ return null;
12819
+ }
12820
+ try {
12821
+ const dpr = win.devicePixelRatio || 1;
12822
+ // 获取元素的背景样式
12823
+ const styles = win.getComputedStyle(element);
12824
+ const bgImage = styles.backgroundImage;
12825
+ if (!bgImage || !bgImage.includes('repeating-linear-gradient')) {
12826
+ return null;
12827
+ }
12828
+ // 使用 Canvas 2D 手动渲染
12829
+ const canvas = doc.createElement('canvas');
12830
+ const success = renderRepeatingLinearGradientToCanvas(canvas, width, height, bgImage, dpr);
12831
+ if (!success) {
12832
+ return null;
12833
+ }
12834
+ const base64 = canvas.toDataURL('image/png');
12835
+ return {
12836
+ base64: base64,
12837
+ actualWidth: width,
12838
+ actualHeight: height,
12839
+ logicalWidth: width,
12840
+ logicalHeight: height,
12841
+ };
12842
+ }
12843
+ catch (e) {
12844
+ console.error('[renderRepeatingLinearGradientToBase64] 错误:', e);
12845
+ return null;
12846
+ }
12847
+ }
12599
12848
  /**
12600
12849
  * 将元素的背景渲染为 base64 图片
12601
12850
  * 使用 html2canvas 完美复刻复杂背景效果
@@ -12608,6 +12857,7 @@ async function renderBackgroundToBase64(element, width, height) {
12608
12857
  return null;
12609
12858
  }
12610
12859
  try {
12860
+ const dpr = win.devicePixelRatio || 1;
12611
12861
  // 创建临时容器(使用元素所在 document,以便 iframe 内元素在正确上下文中渲染)
12612
12862
  const tempContainer = doc.createElement("div");
12613
12863
  tempContainer.style.position = "fixed";
@@ -12630,7 +12880,7 @@ async function renderBackgroundToBase64(element, width, height) {
12630
12880
  // 使用 html2canvas 截图
12631
12881
  const canvas = await html2canvasExports(tempContainer, {
12632
12882
  backgroundColor: null,
12633
- scale: win.devicePixelRatio || 1,
12883
+ scale: dpr,
12634
12884
  logging: false,
12635
12885
  useCORS: true,
12636
12886
  allowTaint: true,
@@ -12639,7 +12889,13 @@ async function renderBackgroundToBase64(element, width, height) {
12639
12889
  });
12640
12890
  // 清理
12641
12891
  doc.body.removeChild(tempContainer);
12642
- return canvas.toDataURL("image/png");
12892
+ return {
12893
+ base64: canvas.toDataURL("image/png"),
12894
+ actualWidth: width * dpr,
12895
+ actualHeight: height * dpr,
12896
+ logicalWidth: width,
12897
+ logicalHeight: height,
12898
+ };
12643
12899
  }
12644
12900
  catch (e) {
12645
12901
  console.warn("Failed to render background to base64:", e);
@@ -12744,34 +13000,36 @@ function convertImageElement(element, context) {
12744
13000
  ? resolveVariable(styles.boxShadow, cssVariables)
12745
13001
  : null;
12746
13002
  const shadow = parseShadow(shadowStr);
12747
- // 计算图片尺寸
12748
- let width = rect.width;
12749
- let height = rect.height;
12750
- // 如果高度很小(可能图片未加载),尝试根据自然尺寸计算
12751
- if (height < 10 && element.naturalWidth > 0 && element.naturalHeight > 0) {
12752
- const aspectRatio = element.naturalHeight / element.naturalWidth;
12753
- height = width * aspectRatio;
12754
- }
12755
- // 如果仍然没有高度,尝试从 HTML 属性获取
12756
- if (height < 10) {
13003
+ // 获取图片的像素尺寸(裁剪区域)
13004
+ let naturalWidth = element.naturalWidth;
13005
+ let naturalHeight = element.naturalHeight;
13006
+ // 如果图片未加载,尝试从 HTML 属性获取
13007
+ if (naturalWidth === 0 || naturalHeight === 0) {
12757
13008
  const attrWidth = element.getAttribute("width");
12758
13009
  const attrHeight = element.getAttribute("height");
12759
13010
  if (attrWidth && attrHeight) {
12760
- const w = parseFloat(attrWidth);
12761
- const h = parseFloat(attrHeight);
12762
- if (w > 0 && h > 0) {
12763
- height = (width / w) * h;
12764
- }
13011
+ naturalWidth = parseFloat(attrWidth);
13012
+ naturalHeight = parseFloat(attrHeight);
12765
13013
  }
12766
13014
  }
13015
+ // 如果仍然无法获取像素尺寸,回退到显示尺寸
13016
+ if (naturalWidth === 0 || naturalHeight === 0) {
13017
+ naturalWidth = rect.width;
13018
+ naturalHeight = rect.height;
13019
+ }
13020
+ // 计算缩放比例:显示尺寸 / 像素尺寸
13021
+ const scaleX = naturalWidth > 0 ? rect.width / naturalWidth : 1;
13022
+ const scaleY = naturalHeight > 0 ? rect.height / naturalHeight : 1;
12767
13023
  const layer = {
12768
13024
  type: "image",
12769
13025
  name: prompt,
12770
13026
  id: uuid(),
12771
13027
  left,
12772
13028
  top,
12773
- width,
12774
- height,
13029
+ width: naturalWidth,
13030
+ height: naturalHeight,
13031
+ scaleX,
13032
+ scaleY,
12775
13033
  src,
12776
13034
  };
12777
13035
  if (borderRadius > 0) {
@@ -12867,10 +13125,11 @@ async function convertSVGElement(element, context) {
12867
13125
  // 计算相对位置
12868
13126
  const left = rect.left - containerRect.left;
12869
13127
  const top = rect.top - containerRect.top;
12870
- // 获取 SVG 的 data URL
12871
- const svgDataUrl = await renderSVGToBase64(element, rect.width, rect.height, styles);
12872
- if (!svgDataUrl)
13128
+ // 获取 SVG 的 data URL(使用 scale=10 提升清晰度)
13129
+ const result = await renderSVGToBase64(element, rect.width, rect.height, styles, 10);
13130
+ if (!result)
12873
13131
  return null;
13132
+ const { base64, actualWidth, actualHeight, logicalWidth, logicalHeight } = result;
12874
13133
  // 解析圆角
12875
13134
  const borderRadiusStr = styles
12876
13135
  ? resolveVariable(styles.borderRadius, cssVariables)
@@ -12885,17 +13144,19 @@ async function convertSVGElement(element, context) {
12885
13144
  ? resolveVariable(styles.boxShadow, cssVariables)
12886
13145
  : null;
12887
13146
  const shadow = parseShadow(shadowStr);
12888
- const scale = 10;
13147
+ // 使用实际像素尺寸作为 width/height,通过 scale 控制显示尺寸
13148
+ const scaleX = logicalWidth / actualWidth;
13149
+ const scaleY = logicalHeight / actualHeight;
12889
13150
  const layer = {
12890
13151
  type: "image",
12891
13152
  id: uuid(),
12892
13153
  left,
12893
13154
  top,
12894
- width: rect.width * scale,
12895
- height: rect.height * scale,
12896
- scaleX: 1 / scale,
12897
- scaleY: 1 / scale,
12898
- src: svgDataUrl,
13155
+ width: actualWidth,
13156
+ height: actualHeight,
13157
+ scaleX,
13158
+ scaleY,
13159
+ src: base64,
12899
13160
  };
12900
13161
  if (borderRadius > 0) {
12901
13162
  layer.rx = borderRadius;
@@ -12972,21 +13233,20 @@ async function convertPseudoElements(element, context) {
12972
13233
  if (angle === undefined) {
12973
13234
  angle = getElementRotation(element, null);
12974
13235
  }
13236
+ // 使用实际像素尺寸作为 width/height,通过 scale 控制显示尺寸
13237
+ const scaleX = useScale ? logicalWidth / actualWidth : 1;
13238
+ const scaleY = useScale ? logicalHeight / actualHeight : 1;
12975
13239
  const imageLayer = {
12976
13240
  type: "image",
12977
13241
  id: uuid(),
12978
13242
  left,
12979
13243
  top,
12980
- width: layerWidth + padding * 2,
12981
- height: layerHeight + padding * 2,
13244
+ width: actualWidth,
13245
+ height: actualHeight,
13246
+ scaleX,
13247
+ scaleY,
12982
13248
  src: base64,
12983
13249
  };
12984
- if (useScale) {
12985
- const w = layerWidth + padding * 2;
12986
- const h = layerHeight + padding * 2;
12987
- imageLayer.scaleX = logicalWidth / w;
12988
- imageLayer.scaleY = logicalHeight / h;
12989
- }
12990
13250
  if (opacity < 1) {
12991
13251
  imageLayer.opacity = opacity;
12992
13252
  }
@@ -12997,6 +13257,165 @@ async function convertPseudoElements(element, context) {
12997
13257
  }
12998
13258
  return layers;
12999
13259
  }
13260
+ /**
13261
+ * 处理带有 filter 效果的背景元素
13262
+ */
13263
+ async function handleFilterBackground(element, left, top, width, height, styles) {
13264
+ const result = await renderElementWithFilterToBase64(element, width, height, styles);
13265
+ if (!result)
13266
+ return null;
13267
+ const { base64, padding, actualWidth, actualHeight, logicalWidth, logicalHeight } = result;
13268
+ // 解析透明度(考虑父元素链的透明度累积)
13269
+ const opacity = getElementOpacity(element);
13270
+ // 解析旋转角度(考虑父元素链的 transform: rotate)
13271
+ const angle = getElementRotation(element, styles);
13272
+ // 使用实际像素尺寸作为 width/height,通过 scale 控制显示尺寸
13273
+ const scaleX = logicalWidth / actualWidth;
13274
+ const scaleY = logicalHeight / actualHeight;
13275
+ const imageLayer = {
13276
+ type: "image",
13277
+ id: uuid(),
13278
+ left: left - padding,
13279
+ top: top - padding,
13280
+ width: actualWidth,
13281
+ height: actualHeight,
13282
+ scaleX,
13283
+ scaleY,
13284
+ src: base64,
13285
+ };
13286
+ if (opacity < 1) {
13287
+ imageLayer.opacity = opacity;
13288
+ }
13289
+ if (angle !== undefined) {
13290
+ imageLayer.angle = angle;
13291
+ }
13292
+ return imageLayer;
13293
+ }
13294
+ /**
13295
+ * 处理 repeating-linear-gradient 背景
13296
+ */
13297
+ async function handleRepeatingLinearGradientBackground(element, left, top, width, height, borderRadius, isCircle, styles, cssVariables) {
13298
+ const result = await renderRepeatingLinearGradientToBase64(element, width, height);
13299
+ if (!result)
13300
+ return null;
13301
+ const { base64, actualWidth, actualHeight, logicalWidth, logicalHeight } = result;
13302
+ const shadowStr = resolveVariable(styles.boxShadow, cssVariables);
13303
+ const shadow = parseShadow(shadowStr);
13304
+ // 解析透明度(考虑父元素链的透明度累积)
13305
+ const opacity = getElementOpacity(element);
13306
+ // 解析旋转角度(考虑父元素链的 transform: rotate)
13307
+ const angle = getElementRotation(element, styles);
13308
+ // 使用实际像素尺寸作为 width/height,通过 scale 控制显示尺寸
13309
+ const scaleX = logicalWidth / actualWidth;
13310
+ const scaleY = logicalHeight / actualHeight;
13311
+ const imageLayer = {
13312
+ type: "image",
13313
+ id: uuid(),
13314
+ left,
13315
+ top,
13316
+ width: actualWidth,
13317
+ height: actualHeight,
13318
+ scaleX,
13319
+ scaleY,
13320
+ src: base64,
13321
+ };
13322
+ if (borderRadius > 0 && !isCircle) {
13323
+ imageLayer.rx = borderRadius;
13324
+ imageLayer.ry = borderRadius;
13325
+ }
13326
+ if (shadow)
13327
+ imageLayer.shadow = shadow;
13328
+ if (angle !== undefined)
13329
+ imageLayer.angle = angle;
13330
+ if (opacity < 1)
13331
+ imageLayer.opacity = opacity;
13332
+ return imageLayer;
13333
+ }
13334
+ /**
13335
+ * 处理复杂背景(url、gradient 等)
13336
+ */
13337
+ async function handleComplexBackground(element, left, top, width, height, borderRadius, isCircle, bgImage, bgSize, angle, styles, cssVariables) {
13338
+ // 检查是否是简单的 url() 背景图片
13339
+ if (bgImage && bgImage.includes("url(")) {
13340
+ // 提取 URL
13341
+ const urlMatch = bgImage.match(/url\(["']?([^"')]+)["']?\)/);
13342
+ if (urlMatch && urlMatch[1]) {
13343
+ const imageUrl = urlMatch[1];
13344
+ const shadowStr = resolveVariable(styles.boxShadow, cssVariables);
13345
+ const shadow = parseShadow(shadowStr);
13346
+ // 解析透明度(考虑父元素链的透明度累积)
13347
+ const opacity = getElementOpacity(element);
13348
+ // 尝试加载图片获取自然尺寸
13349
+ const dimensions = await loadImageDimensions(imageUrl);
13350
+ let naturalWidth = width;
13351
+ let naturalHeight = height;
13352
+ let scaleX = 1;
13353
+ let scaleY = 1;
13354
+ if (dimensions) {
13355
+ naturalWidth = dimensions.width;
13356
+ naturalHeight = dimensions.height;
13357
+ scaleX = width / naturalWidth;
13358
+ scaleY = height / naturalHeight;
13359
+ }
13360
+ const imageLayer = {
13361
+ type: "image",
13362
+ id: uuid(),
13363
+ left,
13364
+ top,
13365
+ width: naturalWidth,
13366
+ height: naturalHeight,
13367
+ scaleX,
13368
+ scaleY,
13369
+ src: imageUrl,
13370
+ };
13371
+ if (borderRadius > 0 && !isCircle) {
13372
+ imageLayer.rx = borderRadius;
13373
+ imageLayer.ry = borderRadius;
13374
+ }
13375
+ if (shadow)
13376
+ imageLayer.shadow = shadow;
13377
+ if (angle !== undefined)
13378
+ imageLayer.angle = angle;
13379
+ if (opacity < 1)
13380
+ imageLayer.opacity = opacity;
13381
+ return imageLayer;
13382
+ }
13383
+ }
13384
+ // 对于复杂背景(gradient 等),使用 html2canvas 渲染
13385
+ const result = await renderBackgroundToBase64(element, width, height);
13386
+ if (!result)
13387
+ return null;
13388
+ const { base64, actualWidth, actualHeight, logicalWidth, logicalHeight } = result;
13389
+ const shadowStr = resolveVariable(styles.boxShadow, cssVariables);
13390
+ const shadow = parseShadow(shadowStr);
13391
+ // 解析透明度(考虑父元素链的透明度累积)
13392
+ const opacity = getElementOpacity(element);
13393
+ // 使用实际像素尺寸作为 width/height,通过 scale 控制显示尺寸
13394
+ const scaleX = logicalWidth / actualWidth;
13395
+ const scaleY = logicalHeight / actualHeight;
13396
+ const imageLayer = {
13397
+ type: "image",
13398
+ id: uuid(),
13399
+ left,
13400
+ top,
13401
+ width: actualWidth,
13402
+ height: actualHeight,
13403
+ scaleX,
13404
+ scaleY,
13405
+ src: base64,
13406
+ };
13407
+ if (borderRadius > 0 && !isCircle) {
13408
+ imageLayer.rx = borderRadius;
13409
+ imageLayer.ry = borderRadius;
13410
+ }
13411
+ if (shadow)
13412
+ imageLayer.shadow = shadow;
13413
+ if (angle !== undefined)
13414
+ imageLayer.angle = angle;
13415
+ if (opacity < 1)
13416
+ imageLayer.opacity = opacity;
13417
+ return imageLayer;
13418
+ }
13000
13419
  /**
13001
13420
  * 转换背景元素
13002
13421
  */
@@ -13045,93 +13464,23 @@ async function convertBackgroundElement(element, context) {
13045
13464
  borderRadius >= Math.min(width, height) / 2);
13046
13465
  const filterStr = styles.filter;
13047
13466
  const hasFilter = filterStr && filterStr !== "none";
13467
+ // 处理 filter 效果
13048
13468
  if (hasFilter) {
13049
- const result = await renderElementWithFilterToBase64(element, width, height, styles);
13050
- if (result) {
13051
- const { base64, padding } = result;
13052
- // 解析透明度(考虑父元素链的透明度累积)
13053
- const opacity = getElementOpacity(element);
13054
- // 解析旋转角度(考虑父元素链的 transform: rotate)
13055
- const angle = getElementRotation(element, styles);
13056
- const imageLayer = {
13057
- type: "image",
13058
- id: uuid(),
13059
- left: left - padding,
13060
- top: top - padding,
13061
- width: width + padding * 2,
13062
- height: height + padding * 2,
13063
- src: base64,
13064
- };
13065
- if (opacity < 1) {
13066
- imageLayer.opacity = opacity;
13067
- }
13068
- if (angle !== undefined) {
13069
- imageLayer.angle = angle;
13070
- }
13071
- return imageLayer;
13072
- }
13469
+ const result = await handleFilterBackground(element, left, top, width, height, styles);
13470
+ if (result)
13471
+ return result;
13073
13472
  }
13473
+ // 处理 repeating-linear-gradient(html2canvas 不支持,需要手动渲染)
13474
+ if (bgImage && bgImage.includes('repeating-linear-gradient')) {
13475
+ const result = await handleRepeatingLinearGradientBackground(element, left, top, width, height, borderRadius, isCircle, styles, cssVariables);
13476
+ if (result)
13477
+ return result;
13478
+ }
13479
+ // 处理其他复杂背景
13074
13480
  if (isComplexBackground(bgImage, bgSize)) {
13075
- // 检查是否是简单的 url() 背景图片
13076
- if (bgImage && bgImage.includes("url(")) {
13077
- // 提取 URL
13078
- const urlMatch = bgImage.match(/url\(["']?([^"')]+)["']?\)/);
13079
- if (urlMatch && urlMatch[1]) {
13080
- const imageUrl = urlMatch[1];
13081
- const shadowStr = resolveVariable(styles.boxShadow, cssVariables);
13082
- const shadow = parseShadow(shadowStr);
13083
- // 解析透明度(考虑父元素链的透明度累积)
13084
- const opacity = getElementOpacity(element);
13085
- const imageLayer = {
13086
- type: "image",
13087
- id: uuid(),
13088
- left,
13089
- top,
13090
- width,
13091
- height,
13092
- src: imageUrl,
13093
- };
13094
- if (borderRadius > 0 && !isCircle) {
13095
- imageLayer.rx = borderRadius;
13096
- imageLayer.ry = borderRadius;
13097
- }
13098
- if (shadow)
13099
- imageLayer.shadow = shadow;
13100
- if (angle !== undefined)
13101
- imageLayer.angle = angle;
13102
- if (opacity < 1)
13103
- imageLayer.opacity = opacity;
13104
- return imageLayer;
13105
- }
13106
- }
13107
- // 对于复杂背景(gradient 等),使用 html2canvas 渲染
13108
- const base64 = await renderBackgroundToBase64(element, width, height);
13109
- if (base64) {
13110
- const shadowStr = resolveVariable(styles.boxShadow, cssVariables);
13111
- const shadow = parseShadow(shadowStr);
13112
- // 解析透明度(考虑父元素链的透明度累积)
13113
- const opacity = getElementOpacity(element);
13114
- const imageLayer = {
13115
- type: "image",
13116
- id: uuid(),
13117
- left,
13118
- top,
13119
- width,
13120
- height,
13121
- src: base64,
13122
- };
13123
- if (borderRadius > 0 && !isCircle) {
13124
- imageLayer.rx = borderRadius;
13125
- imageLayer.ry = borderRadius;
13126
- }
13127
- if (shadow)
13128
- imageLayer.shadow = shadow;
13129
- if (angle !== undefined)
13130
- imageLayer.angle = angle;
13131
- if (opacity < 1)
13132
- imageLayer.opacity = opacity;
13133
- return imageLayer;
13134
- }
13481
+ const result = await handleComplexBackground(element, left, top, width, height, borderRadius, isCircle, bgImage, bgSize, angle, styles, cssVariables);
13482
+ if (result)
13483
+ return result;
13135
13484
  }
13136
13485
  let fill;
13137
13486
  if (bgImage && bgImage !== "none") {
@@ -13277,21 +13626,21 @@ async function convertSingleBorders(element, context) {
13277
13626
  height: rect.height,
13278
13627
  });
13279
13628
  if (result) {
13280
- const { base64, padding } = result;
13281
- const scale = 2;
13629
+ const { base64, padding, actualWidth, actualHeight, logicalWidth, logicalHeight } = result;
13282
13630
  const left = rect.left - containerRect.left - padding;
13283
13631
  const top = rect.top - containerRect.top - padding;
13284
- const width = rect.width + padding * 2;
13285
- const height = rect.height + padding * 2;
13632
+ // 使用实际像素尺寸作为 width/height,通过 scale 控制显示尺寸
13633
+ const scaleX = logicalWidth / actualWidth;
13634
+ const scaleY = logicalHeight / actualHeight;
13286
13635
  const imageLayer = {
13287
13636
  type: "image",
13288
13637
  id: uuid(),
13289
13638
  left,
13290
13639
  top,
13291
- width: width * scale,
13292
- height: height * scale,
13293
- scaleX: 1 / scale,
13294
- scaleY: 1 / scale,
13640
+ width: actualWidth,
13641
+ height: actualHeight,
13642
+ scaleX,
13643
+ scaleY,
13295
13644
  src: base64,
13296
13645
  };
13297
13646
  if (opacity < 1)
@@ -13369,6 +13718,10 @@ async function convertSingleBorders(element, context) {
13369
13718
  * HTML to EditorJSON Converter - Text Element Converters
13370
13719
  * 新的文本元素转换逻辑
13371
13720
  */
13721
+ /**
13722
+ * 标记文本元素已处理的属性名
13723
+ */
13724
+ const TEXT_PROCESSED_ATTR = 'data-text-processed';
13372
13725
  /**
13373
13726
  * 获取元素中文本节点的实际边界框
13374
13727
  * 用于处理包含非文本子元素的文本元素(如 <h2><div>图标</div>文本</h2>)
@@ -13449,6 +13802,15 @@ function getDirectTextNodes(element) {
13449
13802
  });
13450
13803
  return directTextNodes;
13451
13804
  }
13805
+ /**
13806
+ * 深度标记元素及其所有后代为已处理(递归)
13807
+ */
13808
+ function deepMarkAsProcessed(element) {
13809
+ element.setAttribute(TEXT_PROCESSED_ATTR, 'true');
13810
+ for (const child of Array.from(element.children)) {
13811
+ deepMarkAsProcessed(child);
13812
+ }
13813
+ }
13452
13814
  /**
13453
13815
  * 判断元素是否为行内元素(通过 display 属性)
13454
13816
  */
@@ -13490,6 +13852,10 @@ function isDecorationOnlyInlineBlock(child) {
13490
13852
  function canProcessAsWholeText(element) {
13491
13853
  const childElements = Array.from(element.children);
13492
13854
  const directTextNodes = getDirectTextNodes(element);
13855
+ // 规则0: 如果有子元素已经被处理过,不能作为整体处理
13856
+ if (childElements.some(child => child.hasAttribute(TEXT_PROCESSED_ATTR))) {
13857
+ return false;
13858
+ }
13493
13859
  // 规则1: 没有子元素,只有直接文本,且不包含 emoji
13494
13860
  if (childElements.length === 0 && directTextNodes.length > 0 && !directTextNodes.some(node => containsEmojiText(node.textContent || ""))) {
13495
13861
  return true;
@@ -13562,7 +13928,9 @@ async function convertChildElements(element, context, layers) {
13562
13928
  layers.push(...pseudoLayers);
13563
13929
  }
13564
13930
  // 递归处理子元素的子元素
13565
- await convertChildElements(child, context, layers);
13931
+ if (child.children.length > 0) {
13932
+ await convertChildElements(child, context, layers);
13933
+ }
13566
13934
  }
13567
13935
  }
13568
13936
  /**
@@ -13735,10 +14103,15 @@ async function processTextNodeWithEmoji(textNode, parentElement, context, layers
13735
14103
  */
13736
14104
  async function convertTextElements(element, context, layers) {
13737
14105
  var _a;
14106
+ // 检查是否已经处理过(避免重复处理)
14107
+ if (element.hasAttribute(TEXT_PROCESSED_ATTR)) {
14108
+ return true;
14109
+ }
13738
14110
  // 1. 如果是图标字体(如 Material Icons),则转换为 ImageLayer
13739
14111
  const iconLayer = await convertIconFontToImage(element, context);
13740
14112
  if (iconLayer) {
13741
14113
  layers.push(iconLayer);
14114
+ deepMarkAsProcessed(element);
13742
14115
  return true;
13743
14116
  }
13744
14117
  // 2. 作为整体转为 textLayer
@@ -13749,6 +14122,7 @@ async function convertTextElements(element, context, layers) {
13749
14122
  if (textLayer) {
13750
14123
  layers.push(textLayer);
13751
14124
  }
14125
+ deepMarkAsProcessed(element);
13752
14126
  return true;
13753
14127
  }
13754
14128
  // 3. 无法作为整体处理时,将每个直接文本节点转为独立的 textLayer
@@ -14301,21 +14675,20 @@ async function convertSinglePseudoElement(element, pseudoType, ctx) {
14301
14675
  if (angle === undefined) {
14302
14676
  angle = getElementRotation(element, null);
14303
14677
  }
14678
+ // 使用实际像素尺寸作为 width/height,通过 scale 控制显示尺寸
14679
+ const scaleX = useScale ? logicalWidth / actualWidth : 1;
14680
+ const scaleY = useScale ? logicalHeight / actualHeight : 1;
14304
14681
  const imageLayer = {
14305
14682
  type: "image",
14306
14683
  id: uuid(),
14307
14684
  left,
14308
14685
  top,
14309
- width: layerWidth + padding * 2,
14310
- height: layerHeight + padding * 2,
14686
+ width: actualWidth,
14687
+ height: actualHeight,
14688
+ scaleX,
14689
+ scaleY,
14311
14690
  src: base64,
14312
14691
  };
14313
- if (useScale) {
14314
- const w = layerWidth + padding * 2;
14315
- const h = layerHeight + padding * 2;
14316
- imageLayer.scaleX = logicalWidth / w;
14317
- imageLayer.scaleY = logicalHeight / h;
14318
- }
14319
14692
  if (opacity < 1)
14320
14693
  imageLayer.opacity = opacity;
14321
14694
  if (angle !== undefined)
@@ -14348,26 +14721,26 @@ async function convertSingleMarker(element, ctx) {
14348
14721
  const result = await renderMarkerWithHtml2Canvas(element, markerRect, markerStyles, elementStyles);
14349
14722
  if (!result)
14350
14723
  return null;
14351
- const { base64, padding } = result;
14724
+ const { base64, padding, actualWidth, actualHeight, logicalWidth, logicalHeight } = result;
14352
14725
  const { containerRect } = ctx;
14353
14726
  const left = markerRect.left - containerRect.left - padding;
14354
14727
  const top = markerRect.top - containerRect.top - padding;
14355
- const width = markerRect.width + padding * 2;
14356
- const height = markerRect.height + padding * 2;
14357
14728
  const markerOpacity = parseFloat(markerStyles.opacity || "1") || 1;
14358
14729
  const parentOpacity = getElementOpacity(element);
14359
14730
  const opacity = markerOpacity * parentOpacity;
14360
14731
  const angle = getElementRotation(element, null);
14361
- const scale = 5;
14732
+ // 使用实际像素尺寸作为 width/height,通过 scale 控制显示尺寸
14733
+ const scaleX = logicalWidth / actualWidth;
14734
+ const scaleY = logicalHeight / actualHeight;
14362
14735
  const markerLayer = {
14363
14736
  type: "image",
14364
14737
  id: uuid(),
14365
14738
  left,
14366
14739
  top,
14367
- width: width * scale,
14368
- height: height * scale,
14369
- scaleX: 1 / scale,
14370
- scaleY: 1 / scale,
14740
+ width: actualWidth,
14741
+ height: actualHeight,
14742
+ scaleX,
14743
+ scaleY,
14371
14744
  src: base64,
14372
14745
  };
14373
14746
  if (opacity < 1)