import React, { useState } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { createGIF } from "./animated-gif"; import { getSvgMetadata, SvgWithBackground } from "./auto-sizing-svg"; import "./export-svg.css"; function getSvgDocument(svgMarkup: string): string { return [ ``, "", '', svgMarkup, ].join("\n"); } type ProgressHandler = (value: number | null) => void; type ImageExporter = ( svgEl: SVGSVGElement, onProgress: ProgressHandler ) => Promise; /** * Initiates a download on the user's browser which downloads the given * SVG element under the given filename, using the given export algorithm. */ async function exportImage( svgRef: React.RefObject, basename: string, ext: string, onProgress: ProgressHandler, exporter: ImageExporter ) { const svgEl = svgRef.current; if (!svgEl) { alert("Oops, an error occurred! Please try again later."); return; } const url = await exporter(svgEl, onProgress); const anchor = document.createElement("a"); anchor.href = url; anchor.download = `${basename}.${ext}`; document.body.append(anchor); anchor.click(); document.body.removeChild(anchor); onProgress(null); } function getCanvasContext2D( canvas: HTMLCanvasElement ): CanvasRenderingContext2D { const ctx = canvas.getContext("2d"); if (!ctx) throw new Error(`Unable to get 2D context for canvas!`); return ctx; } function getSvgUrl(svgMarkup: string): string { return `data:image/svg+xml;utf8,${encodeURIComponent( getSvgDocument(svgMarkup) )}`; } /** * Exports the given SVG as an SVG in a data URL. */ const exportSvg: ImageExporter = async (svgEl) => getSvgUrl(svgEl.outerHTML); /** * Exports the given SVG as a PNG in a data URL. */ const exportPng: ImageExporter = async (svgEl, onProgress) => { const dataURL = await exportSvg(svgEl, onProgress); return new Promise((resolve, reject) => { const canvas = document.createElement("canvas"); const img = document.createElement("img"); img.onload = () => { canvas.width = img.width; canvas.height = img.height; const ctx = getCanvasContext2D(canvas); ctx.drawImage(img, 0, 0); resolve(canvas.toDataURL()); }; img.onerror = reject; img.src = dataURL; }); }; function drawImage( canvas: HTMLCanvasElement, dataURL: string, scale: number ): Promise { return new Promise((resolve, reject) => { const img = document.createElement("img"); img.onload = () => { const scaledWidth = Math.floor(img.width * scale); const scaledHeight = Math.floor(img.height * scale); canvas.width = scaledWidth; canvas.height = scaledHeight; const ctx = getCanvasContext2D(canvas); ctx.drawImage( img, 0, 0, img.width, img.height, 0, 0, scaledWidth, scaledHeight ); resolve(); }; img.onerror = reject; img.src = dataURL; }); } /** * Exports the given SVG as a GIF in a data URL. */ async function exportGif( animate: ExportableAnimation, scale: number, svgEl: SVGSVGElement, onProgress: (value: number) => void ): Promise { const fps = animate.fps || 15; const msecPerFrame = 1000 / fps; const numFrames = Math.floor(animate.duration / msecPerFrame); const svgMeta = getSvgMetadata(svgEl); const render = (animPct: number) => ( {animate.render(animPct)} ); const gif = createGIF(); for (let i = 0; i < numFrames; i++) { onProgress(i / numFrames); const canvas = document.createElement("canvas"); const animPct = i / numFrames; const markup = renderToStaticMarkup(render(animPct)); const url = getSvgUrl(markup); await drawImage(canvas, url, scale); gif.addFrame(canvas, { delay: msecPerFrame }); } return new Promise((resolve, reject) => { gif.on("finished", function (blob) { onProgress(1); resolve(URL.createObjectURL(blob)); }); gif.render(); }); } export type AnimationRenderer = (time: number) => JSX.Element; export type ExportableAnimation = { duration: number; fps?: number; render: AnimationRenderer; }; const DEFAULT_GIF_SCALE = 0.5; export const ExportWidget: React.FC<{ svgRef: React.RefObject; animate?: ExportableAnimation | false; gifScale?: number; basename: string; }> = ({ svgRef, basename, animate, gifScale }) => { const [progress, setProgress] = useState(null); if (progress !== null) { return (

Exporting…

); } return ( <> {" "} {" "} {animate && ( )} ); };