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 +530 -157
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +531 -158
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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 =
|
|
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
|
|
11859
|
-
|
|
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 =
|
|
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
|
-
|
|
11901
|
-
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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 {
|
|
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:
|
|
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
|
|
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
|
|
12749
|
-
let
|
|
12750
|
-
//
|
|
12751
|
-
if (
|
|
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
|
-
|
|
12761
|
-
|
|
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
|
|
12872
|
-
if (!
|
|
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
|
-
|
|
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:
|
|
12895
|
-
height:
|
|
12896
|
-
scaleX
|
|
12897
|
-
scaleY
|
|
12898
|
-
src:
|
|
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:
|
|
12981
|
-
height:
|
|
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
|
|
13050
|
-
if (result)
|
|
13051
|
-
|
|
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
|
-
|
|
13076
|
-
if (
|
|
13077
|
-
|
|
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
|
-
|
|
13285
|
-
const
|
|
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:
|
|
13292
|
-
height:
|
|
13293
|
-
scaleX
|
|
13294
|
-
scaleY
|
|
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
|
-
|
|
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:
|
|
14310
|
-
height:
|
|
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
|
-
|
|
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:
|
|
14368
|
-
height:
|
|
14369
|
-
scaleX
|
|
14370
|
-
scaleY
|
|
14740
|
+
width: actualWidth,
|
|
14741
|
+
height: actualHeight,
|
|
14742
|
+
scaleX,
|
|
14743
|
+
scaleY,
|
|
14371
14744
|
src: base64,
|
|
14372
14745
|
};
|
|
14373
14746
|
if (opacity < 1)
|