Skip to content

Commit b4f23ab

Browse files
committed
Fix - useSvgExport composable - Avoid cropping foreignObject contents
1 parent ea7075e commit b4f23ab

File tree

1 file changed

+55
-17
lines changed

1 file changed

+55
-17
lines changed

src/useSvgExport.js

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -894,7 +894,6 @@ async function processForeignObjects(svgRoot, { mode = 'raster' } = {}) {
894894

895895
const ta = (cs.getPropertyValue('text-align') || parent?.ta || 'start').trim().toLowerCase();
896896

897-
// paddings
898897
let padT = parseFloat(cs.getPropertyValue('padding-top')) || 0;
899898
let padR = parseFloat(cs.getPropertyValue('padding-right')) || 0;
900899
let padB = parseFloat(cs.getPropertyValue('padding-bottom')) || 0;
@@ -905,7 +904,6 @@ async function processForeignObjects(svgRoot, { mode = 'raster' } = {}) {
905904
padT = t; padR = r; padB = b; padL = l;
906905
}
907906

908-
// borders / box-sizing
909907
const bL = parseFloat(cs.getPropertyValue('border-left-width')) || 0;
910908
const bR = parseFloat(cs.getPropertyValue('border-right-width')) || 0;
911909
const box = cs.getPropertyValue('box-sizing') || 'content-box';
@@ -958,7 +956,6 @@ async function processForeignObjects(svgRoot, { mode = 'raster' } = {}) {
958956
const lcaEl = lcaPath[lcaPath.length - 1] || htmlRoot;
959957
const lcaStyle = readTextStyle(lcaEl);
960958

961-
// accumulated padding
962959
let padLsum = 0, padRsum = 0, padTsum = 0;
963960
for (const el of lcaPath) {
964961
const st = readTextStyle(el);
@@ -968,21 +965,15 @@ async function processForeignObjects(svgRoot, { mode = 'raster' } = {}) {
968965
}
969966

970967
const widths = [Math.max(1, wFO - padLsum - padRsum)];
971-
972-
// For every ancestor in the LCA path, try to get the content width
973968
for (const el of lcaPath) {
974969
try {
975970
const cs = CACHE_CSS(el);
976-
977-
// Preferred: clientWidth (padding included, border excluded)
978971
if (el.clientWidth && el.clientWidth > 0) {
979972
const pL = parseFloat(cs.paddingLeft) || 0;
980973
const pR = parseFloat(cs.paddingRight) || 0;
981974
const cw = Math.max(1, el.clientWidth - pL - pR);
982975
widths.push(cw);
983976
}
984-
985-
// Fallback: computed width
986977
const wStr = cs.width;
987978
const wNum = parseFloat(wStr);
988979
if (Number.isFinite(wNum) && wNum > 0) {
@@ -1018,7 +1009,7 @@ async function processForeignObjects(svgRoot, { mode = 'raster' } = {}) {
10181009

10191010
const textEl = document.createElementNS(SVG_NS, 'text');
10201011
textEl.setAttribute('x', String(baseX));
1021-
textEl.setAttribute('y', String(yFO + padTsum)); // keep first line at the top of the content box
1012+
textEl.setAttribute('y', String(yFO + padTsum));
10221013
textEl.setAttribute('text-anchor', anchor);
10231014
textEl.setAttribute('dominant-baseline', 'text-before-edge');
10241015
textEl.setAttribute('xml:space', 'preserve');
@@ -1156,6 +1147,39 @@ async function processForeignObjects(svgRoot, { mode = 'raster' } = {}) {
11561147
}
11571148
}
11581149

1150+
const measureHtmlNaturalSize = (htmlEl) => {
1151+
try {
1152+
const wrapper = document.createElement('div');
1153+
wrapper.style.position = 'absolute';
1154+
wrapper.style.left = '-99999px';
1155+
wrapper.style.top = '-99999px';
1156+
wrapper.style.visibility = 'hidden';
1157+
wrapper.style.pointerEvents = 'none';
1158+
wrapper.style.width = 'auto';
1159+
wrapper.style.height = 'auto';
1160+
1161+
const clone = htmlEl.cloneNode(true);
1162+
1163+
clone.style.width = 'auto';
1164+
clone.style.height = 'auto';
1165+
clone.style.display = 'inline-block';
1166+
clone.style.maxWidth = 'none';
1167+
clone.style.maxHeight = 'none';
1168+
clone.style.boxSizing = 'content-box';
1169+
1170+
document.body.appendChild(wrapper);
1171+
wrapper.appendChild(clone);
1172+
1173+
const rect = clone.getBoundingClientRect();
1174+
const w = Math.ceil(rect.width || clone.scrollWidth || 0);
1175+
const h = Math.ceil(rect.height || clone.scrollHeight || 0);
1176+
1177+
wrapper.remove();
1178+
return { w: Math.max(1, w), h: Math.max(1, h) };
1179+
} catch {
1180+
return { w: 0, h: 0 };
1181+
}
1182+
};
11591183

11601184
// Text to svg
11611185
for (const fo of list) {
@@ -1193,10 +1217,21 @@ async function processForeignObjects(svgRoot, { mode = 'raster' } = {}) {
11931217

11941218
await new Promise(r => requestAnimationFrame(r));
11951219
const bb = fo.getBBox();
1196-
const w = Math.max(1, Math.ceil(bb.width));
1197-
const h = Math.max(1, Math.ceil(bb.height));
1220+
let w = Math.max(1, Math.ceil(bb.width));
1221+
let h = Math.max(1, Math.ceil(bb.height));
1222+
1223+
// If the html subtree is bigger than FO use the bigger natural size
1224+
const contentRoot = fo.firstElementChild;
1225+
if (contentRoot) {
1226+
const natural = measureHtmlNaturalSize(contentRoot);
1227+
const attrW = parseFloat(fo.getAttribute('width') || '0') || 0;
1228+
const attrH = parseFloat(fo.getAttribute('height') || '0') || 0;
1229+
1230+
w = Math.max(w, attrW, natural.w);
1231+
h = Math.max(h, attrH, natural.h);
1232+
}
11981233

1199-
if (!w || !h) {
1234+
if (!(w > 0 && h > 0)) {
12001235
skipped += 1;
12011236
continue;
12021237
}
@@ -1209,8 +1244,10 @@ async function processForeignObjects(svgRoot, { mode = 'raster' } = {}) {
12091244
tempSvg.setAttribute('viewBox', `0 0 ${w} ${h}`);
12101245

12111246
const foClone = fo.cloneNode(true);
1212-
foClone.setAttribute('x', '0'); foClone.setAttribute('y', '0');
1213-
foClone.setAttribute('width', String(w)); foClone.setAttribute('height', String(h));
1247+
foClone.setAttribute('x', '0');
1248+
foClone.setAttribute('y', '0');
1249+
foClone.setAttribute('width', String(w));
1250+
foClone.setAttribute('height', String(h));
12141251
tempSvg.appendChild(foClone);
12151252

12161253
const xml = new XMLSerializer().serializeToString(tempSvg);
@@ -1239,8 +1276,8 @@ async function processForeignObjects(svgRoot, { mode = 'raster' } = {}) {
12391276
image.setAttribute('href', dataUrl);
12401277
image.setAttribute('x', fo.getAttribute('x') || String(bb.x));
12411278
image.setAttribute('y', fo.getAttribute('y') || String(bb.y));
1242-
image.setAttribute('width', fo.getAttribute('width') || String(bb.width));
1243-
image.setAttribute('height', fo.getAttribute('height') || String(bb.height));
1279+
image.setAttribute('width', String(w));
1280+
image.setAttribute('height', String(h));
12441281

12451282
fo.parentNode.replaceChild(image, fo);
12461283
rasterized += 1;
@@ -1252,3 +1289,4 @@ async function processForeignObjects(svgRoot, { mode = 'raster' } = {}) {
12521289

12531290
return { converted, rasterized, skipped, errors };
12541291
}
1292+

0 commit comments

Comments
 (0)