Skip to content

Commit 122317e

Browse files
committed
add toSvg()
1 parent 500333d commit 122317e

2 files changed

Lines changed: 78 additions & 71 deletions

File tree

src/api/snapdom.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { captureDOM } from '../core/capture';
33
import { extendIconFonts } from '../modules/iconFonts.js';
44
import { createContext } from '../core/context';
5-
import { toImg } from '../exporters/toImg.js';
5+
import { toImg, toSvg} from '../exporters/toImg.js';
66
import { toCanvas } from '../exporters/toCanvas.js';
77
import { toBlob } from '../exporters/toBlob.js';
88
import { rasterize } from '../modules/rasterize.js';
@@ -19,9 +19,11 @@ let _safariWarmup = false;
1919
* @param {HTMLElement} element - The DOM element to capture.
2020
* @param {object} userOptions - Options for rendering/exporting.
2121
* @returns {Promise<object>} Object with exporter methods:
22+
* @deprecated toImg()
2223
* - url: The raw data URL
2324
* - toRaw(): Gets raw data URL
24-
* - toImg(): Converts to HTMLImageElement
25+
* - toImg(): Converts to Svg format
26+
* - toSvg(): Converts to Svg format
2527
* - toCanvas(): Converts to HTMLCanvasElement
2628
* - toBlob(): Converts to Blob
2729
* - toPng(): Converts to PNG format
@@ -52,7 +54,7 @@ export async function snapdom(element, userOptions) {
5254
if (!context.snap) {
5355
context.snap = {
5456
toPng: (el, opts) => snapdom.toPng(el, opts),
55-
toImg: (el, opts) => snapdom.toImg(el, opts),
57+
toSvg: (el, opts) => snapdom.toSvg(el, opts),
5658
};
5759
}
5860

@@ -87,6 +89,7 @@ snapdom.capture = async (el, context, _token) => {
8789
url,
8890
toRaw: () => url,
8991
toImg: (opts) => toImg(url, ensureContext(opts)),
92+
toSvg: (opts) => toSvg(url, ensureContext(opts)),
9093
toCanvas: (opts) => toCanvas(url, ensureContext(opts)),
9194
toBlob: (opts) => toBlob(url, ensureContext(opts)),
9295
toPng: withFormat('png'),
@@ -111,6 +114,8 @@ snapdom.toRaw = (el, options) => snapdom(el, options).then(result => result.toRa
111114
* @returns {Promise<HTMLImageElement>} Loaded image element.
112115
*/
113116
snapdom.toImg = (el, options) => snapdom(el, options).then(result => result.toImg());
117+
snapdom.toSvg = (el, options) => snapdom(el, options).then(result => result.toSvg());
118+
114119

115120
/**
116121
* Returns a Canvas element from a captured element.

src/core/capture.js

Lines changed: 70 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -120,56 +120,6 @@ export async function captureDOM(element, options) {
120120
};
121121
}
122122

123-
function parseBoxShadow(cs) {
124-
const v = cs.boxShadow || "";
125-
if (!v || v === "none") return { top: 0, right: 0, bottom: 0, left: 0 };
126-
const parts = v.split(/\),(?=(?:[^()]*\([^()]*\))*[^()]*$)/).map((s) => s.trim());
127-
let t = 0, r = 0, b2 = 0, l = 0;
128-
for (const part of parts) {
129-
const nums = part.match(/-?\d+(\.\d+)?px/g)?.map((n) => parseFloat(n)) || [];
130-
if (nums.length < 2) continue;
131-
const [ox2, oy2, blur = 0, spread = 0] = nums;
132-
const extX = Math.abs(ox2) + blur + spread;
133-
const extY = Math.abs(oy2) + blur + spread;
134-
r = Math.max(r, extX + Math.max(ox2, 0));
135-
l = Math.max(l, extX + Math.max(-ox2, 0));
136-
b2 = Math.max(b2, extY + Math.max(oy2, 0));
137-
t = Math.max(t, extY + Math.max(-oy2, 0));
138-
}
139-
return { top: Math.ceil(t), right: Math.ceil(r), bottom: Math.ceil(b2), left: Math.ceil(l) };
140-
}
141-
function parseFilterBlur(cs) {
142-
const m = (cs.filter || "").match(/blur\(\s*([0-9.]+)px\s*\)/);
143-
const b2 = m ? Math.ceil(parseFloat(m[1]) || 0) : 0;
144-
return { top: b2, right: b2, bottom: b2, left: b2 };
145-
}
146-
function parseOutline(cs) {
147-
if ((cs.outlineStyle || "none") === "none") return { top: 0, right: 0, bottom: 0, left: 0 };
148-
const w2 = Math.ceil(parseFloat(cs.outlineWidth || "0") || 0);
149-
return { top: w2, right: w2, bottom: w2, left: w2 };
150-
}
151-
function bboxWithOriginFull(w2, h2, M, ox2, oy2) {
152-
const a2 = M.a, b2 = M.b, c2 = M.c, d2 = M.d, e2 = M.e || 0, f2 = M.f || 0;
153-
function pt(x, y) {
154-
let X = x - ox2, Y = y - oy2;
155-
let X2 = a2 * X + c2 * Y, Y2 = b2 * X + d2 * Y;
156-
X2 += ox2 + e2;
157-
Y2 += oy2 + f2;
158-
return [X2, Y2];
159-
}
160-
const P = [pt(0, 0), pt(w2, 0), pt(0, h2), pt(w2, h2)];
161-
let minX2 = Infinity, minY2 = Infinity, maxX2 = -Infinity, maxY2 = -Infinity;
162-
for (const [X, Y] of P) {
163-
if (X < minX2) minX2 = X;
164-
if (Y < minY2) minY2 = Y;
165-
if (X > maxX2) maxX2 = X;
166-
if (Y > maxY2) maxY2 = Y;
167-
}
168-
return { minX: minX2, minY: minY2, maxX: maxX2, maxY: maxY2, width: maxX2 - minX2, height: maxY2 - minY2 };
169-
}
170-
function hasTFBBox(el) {
171-
return hasBBoxAffectingTransform(el);
172-
}
173123
const rect = element.getBoundingClientRect();
174124
const w0 = Math.max(1, Math.ceil(element.offsetWidth || parseFloat(csEl.width) || rect.width || 1));
175125
const h0 = Math.max(1, Math.ceil(element.offsetHeight || parseFloat(csEl.height) || rect.height || 1));
@@ -180,7 +130,7 @@ export async function captureDOM(element, options) {
180130
const optW = coerceNum(options.width);
181131
const optH = coerceNum(options.height);
182132
let w = w0, h = h0;
183-
133+
184134
const hasW = Number.isFinite(optW);
185135
const hasH = Number.isFinite(optH);
186136
const aspect0 = h0 > 0 ? w0 / h0 : 1;
@@ -235,7 +185,7 @@ export async function captureDOM(element, options) {
235185
minY -= bleed.top;
236186
maxX += bleed.right;
237187
maxY += bleed.bottom;
238-
188+
239189
const vbW0 = Math.max(1, Math.ceil(maxX - minX));
240190
const vbH0 = Math.max(1, Math.ceil(maxY - minY));
241191
const outW = Math.max(1, Math.round(vbW0 * (hasW || hasH ? w / w0 : 1)));
@@ -269,26 +219,26 @@ export async function captureDOM(element, options) {
269219
const vbH = vbH0 + pad * 2;
270220

271221

272-
const wantsSize = hasW || hasH;
222+
const wantsSize = hasW || hasH;
273223

274-
// Guardar todo en un bloque meta
275-
options.meta = {
276-
w0, // ancho natural del elemento
277-
h0, // alto natural
278-
vbW, // ancho del viewBox
279-
vbH, // alto del viewBox
280-
targetW: w, // ancho deseado según options.width
281-
targetH: h // alto deseado según options.height
282-
};
224+
// Guardar todo en un bloque meta
225+
options.meta = {
226+
w0, // ancho natural del elemento
227+
h0, // alto natural
228+
vbW, // ancho del viewBox
229+
vbH, // alto del viewBox
230+
targetW: w, // ancho deseado según options.width
231+
targetH: h // alto deseado según options.height
232+
};
283233

284-
// SVG header: si es Safari + width/height => mantener natural
285-
const svgOutW = (isSafari() && wantsSize) ? vbW : (outW + pad*2);
286-
const svgOutH = (isSafari() && wantsSize) ? vbH : (outH + pad*2);
234+
// SVG header: si es Safari + width/height => mantener natural
235+
const svgOutW = (isSafari() && wantsSize) ? vbW : (outW + pad * 2);
236+
const svgOutH = (isSafari() && wantsSize) ? vbH : (outH + pad * 2);
287237

288-
const svgHeader =
289-
`<svg xmlns="${svgNS}" width="${svgOutW}" height="${svgOutH}" viewBox="0 0 ${vbW} ${vbH}">`;
238+
const svgHeader =
239+
`<svg xmlns="${svgNS}" width="${svgOutW}" height="${svgOutH}" viewBox="0 0 ${vbW} ${vbH}">`;
290240

291-
// const svgHeader = `<svg xmlns="${svgNS}" width="${outW + pad * 2}" height="${outH + pad * 2}" viewBox="0 0 ${vbW} ${vbH}">`;
241+
// const svgHeader = `<svg xmlns="${svgNS}" width="${outW + pad * 2}" height="${outH + pad * 2}" viewBox="0 0 ${vbW} ${vbH}">`;
292242
const svgFooter = "</svg>";
293243
svgString = svgHeader + foString + svgFooter;
294244
dataURL = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}`;
@@ -299,6 +249,58 @@ const svgHeader =
299249
if (sandbox && sandbox.style.position === "absolute") sandbox.remove();
300250
return dataURL;
301251
}
252+
253+
function parseBoxShadow(cs) {
254+
const v = cs.boxShadow || "";
255+
if (!v || v === "none") return { top: 0, right: 0, bottom: 0, left: 0 };
256+
const parts = v.split(/\),(?=(?:[^()]*\([^()]*\))*[^()]*$)/).map((s) => s.trim());
257+
let t = 0, r = 0, b2 = 0, l = 0;
258+
for (const part of parts) {
259+
const nums = part.match(/-?\d+(\.\d+)?px/g)?.map((n) => parseFloat(n)) || [];
260+
if (nums.length < 2) continue;
261+
const [ox2, oy2, blur = 0, spread = 0] = nums;
262+
const extX = Math.abs(ox2) + blur + spread;
263+
const extY = Math.abs(oy2) + blur + spread;
264+
r = Math.max(r, extX + Math.max(ox2, 0));
265+
l = Math.max(l, extX + Math.max(-ox2, 0));
266+
b2 = Math.max(b2, extY + Math.max(oy2, 0));
267+
t = Math.max(t, extY + Math.max(-oy2, 0));
268+
}
269+
return { top: Math.ceil(t), right: Math.ceil(r), bottom: Math.ceil(b2), left: Math.ceil(l) };
270+
}
271+
function parseFilterBlur(cs) {
272+
const m = (cs.filter || "").match(/blur\(\s*([0-9.]+)px\s*\)/);
273+
const b2 = m ? Math.ceil(parseFloat(m[1]) || 0) : 0;
274+
return { top: b2, right: b2, bottom: b2, left: b2 };
275+
}
276+
function parseOutline(cs) {
277+
if ((cs.outlineStyle || "none") === "none") return { top: 0, right: 0, bottom: 0, left: 0 };
278+
const w2 = Math.ceil(parseFloat(cs.outlineWidth || "0") || 0);
279+
return { top: w2, right: w2, bottom: w2, left: w2 };
280+
}
281+
function bboxWithOriginFull(w2, h2, M, ox2, oy2) {
282+
const a2 = M.a, b2 = M.b, c2 = M.c, d2 = M.d, e2 = M.e || 0, f2 = M.f || 0;
283+
function pt(x, y) {
284+
let X = x - ox2, Y = y - oy2;
285+
let X2 = a2 * X + c2 * Y, Y2 = b2 * X + d2 * Y;
286+
X2 += ox2 + e2;
287+
Y2 += oy2 + f2;
288+
return [X2, Y2];
289+
}
290+
const P = [pt(0, 0), pt(w2, 0), pt(0, h2), pt(w2, h2)];
291+
let minX2 = Infinity, minY2 = Infinity, maxX2 = -Infinity, maxY2 = -Infinity;
292+
for (const [X, Y] of P) {
293+
if (X < minX2) minX2 = X;
294+
if (Y < minY2) minY2 = Y;
295+
if (X > maxX2) maxX2 = X;
296+
if (Y > maxY2) maxY2 = Y;
297+
}
298+
return { minX: minX2, minY: minY2, maxX: maxX2, maxY: maxY2, width: maxX2 - minX2, height: maxY2 - minY2 };
299+
}
300+
function hasTFBBox(el) {
301+
return hasBBoxAffectingTransform(el);
302+
}
303+
302304
function matrixFromComputed(el) {
303305
const tr = getComputedStyle(el).transform;
304306
if (!tr || tr === "none") return new DOMMatrix();

0 commit comments

Comments
 (0)