import {namespaces as d3Namespaces} from "d3-selection";import {document, window} from "../../module/browser";import {getCssRules, isFunction, mergeObj, toArray} from "../../module/util";
type TExportOption = TSize & { preserveAspectRatio: boolean, preserveFontStyle: boolean, mimeType: string};
type TSize = {x?: number, y?: number, width: number, height: number};
type TTextGlyph = { [key: string]: TSize & { fill: string, fontFamily: string, fontSize: string, textAnchor: string, transform: string }};
const b64EncodeUnicode = (str: string): string => window.btoa?.( encodeURIComponent(str) .replace(/%([0-9A-F]{2})/g, (match, p: number | string): string => String.fromCharCode(Number(`0x${p}`))) );
function nodeToSvgDataUrl(node, option: TExportOption, orgSize: TSize) { const {width, height} = option || orgSize; const serializer = new XMLSerializer(); const clone = node.cloneNode(true); const cssText = getCssRules(toArray(document.styleSheets)) .filter((r: CSSStyleRule) => r.cssText) .map((r: CSSStyleRule) => r.cssText);
clone.setAttribute("xmlns", d3Namespaces.xhtml);
clone.style.margin = "0"; clone.style.padding = "0";
if (option.preserveFontStyle) { clone.querySelectorAll("text").forEach(t => { t.innerHTML = ""; }); }
const nodeXml = serializer.serializeToString(clone);
const style = document.createElement("style");
style.appendChild(document.createTextNode(cssText.join("\n")));
const styleXml = serializer.serializeToString(style);
const dataStr = `<svg xmlns="${d3Namespaces.svg}" width="${width}" height="${height}" viewBox="0 0 ${orgSize.width} ${orgSize.height}" preserveAspectRatio="${option?.preserveAspectRatio === false ? "none" : "xMinYMid meet"}"> <foreignObject width="100%" height="100%"> ${styleXml} ${nodeXml.replace(/(url\()[^#]+/g, "$1")} </foreignObject></svg>`;
return `data:image/svg+xml;base64,${b64EncodeUnicode(dataStr)}`;}
function getCoords(elem, svgOffset): TSize { const {top, left} = svgOffset; const {x, y} = elem.getBBox(); const {a, b, c, d, e, f} = elem.getScreenCTM(); const {width, height} = elem.getBoundingClientRect();
return { x: (a * x) + (c * y) + e - left, y: (b * x) + (d * y) + f - top + (height - Math.round(height / 4)), width, height };}
function getGlyph(svg: SVGElement): TTextGlyph[] { const {left, top} = svg.getBoundingClientRect(); const filterFn = t => t.textContent || t.childElementCount; const glyph: TTextGlyph[] = [];
toArray(svg.querySelectorAll("text")) .filter(filterFn) .forEach((t: SVGTextElement) => { const getStyleFn = (ts: SVGTextElement): TTextGlyph => { const {fill, fontFamily, fontSize, textAnchor, transform} = window.getComputedStyle( ts ); const {x, y, width, height} = getCoords(ts, {left, top});
return { [ts.textContent as string]: { x, y, width, height, fill, fontFamily, fontSize, textAnchor, transform } }; };
if (t.childElementCount > 1) { const text: TTextGlyph[] = [];
toArray(t.querySelectorAll("tspan")) .filter(filterFn) .forEach((ts: SVGTSpanElement) => { glyph.push(getStyleFn(ts)); });
return text; } else { glyph.push(getStyleFn(t)); } });
return glyph;}
function renderText(ctx, glyph): void { glyph.forEach(g => { Object.keys(g).forEach(key => { const {x, y, width, height, fill, fontFamily, fontSize, transform} = g[key];
ctx.save();
ctx.font = `${fontSize} ${fontFamily}`; ctx.fillStyle = fill;
if (transform === "none") { ctx.fillText(key, x, y); } else { const args = transform .replace(/(matrix|\(|\))/g, "") .split(",");
if (args.splice(4).every(v => +v === 0)) { args.push(x + width - (width / 4)); args.push(y - height + (height / 3)); } else { args.push(x); args.push(y); }
ctx.transform(...args); ctx.fillText(key, 0, 0); }
ctx.restore(); }); });}
export default { export(option?: TExportOption, callback?: (dataUrl: string) => void): string { const $$ = this.internal; const {state, $el: {chart, svg}} = $$; const {width, height} = state.current; const opt = mergeObj({ width, height, preserveAspectRatio: true, preserveFontStyle: false, mimeType: "image/png" }, option) as TExportOption;
const svgDataUrl = nodeToSvgDataUrl(chart.node(), opt, {width, height}); const glyph = opt.preserveFontStyle ? getGlyph(svg.node()) : [];
if (callback && isFunction(callback)) { const img = new Image();
img.crossOrigin = "Anonymous"; img.onload = () => { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d");
canvas.width = opt.width || width; canvas.height = opt.height || height; ctx.drawImage(img, 0, 0);
if (glyph.length) { renderText(ctx, glyph);
glyph.length = 0; }
callback.bind(this)(canvas.toDataURL(opt.mimeType)); };
img.src = svgDataUrl; }
return svgDataUrl; }};