From e8cc787edcd4804ebfb314e9c2ecd124e7321198 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arno=C5=A1t=20Pleskot?= Date: Fri, 11 Aug 2023 12:00:14 +0200 Subject: [PATCH] feat: fancyBackgrounds in svg --- src/data/index.ts | 1 + src/element/image.ts | 21 +++++++++++++++ src/scene/export.ts | 52 +++++++++++++++++++++++++++--------- src/scene/fancyBackground.ts | 48 ++++++++++++++++++++++++++++++--- 4 files changed, 106 insertions(+), 16 deletions(-) diff --git a/src/data/index.ts b/src/data/index.ts index 20ba75ebe..545971da2 100644 --- a/src/data/index.ts +++ b/src/data/index.ts @@ -47,6 +47,7 @@ export const exportCanvas = async ( exportPadding, exportScale: appState.exportScale, exportEmbedScene: appState.exportEmbedScene && type === "svg", + fancyBackgroundImageUrl: appState.fancyBackgroundImageUrl, }, files, ); diff --git a/src/element/image.ts b/src/element/image.ts index bd9bcd627..bb7b20799 100644 --- a/src/element/image.ts +++ b/src/element/image.ts @@ -123,3 +123,24 @@ export const normalizeSVG = async (SVGString: string) => { return svg.outerHTML; } }; + +export const loadSVGElement = (filePath: string) => { + return new Promise((resolve, reject) => { + fetch(filePath) + .then((response) => response.text()) + .then((svgString) => { + const parser = new DOMParser(); + const svgDoc = parser.parseFromString(svgString, "image/svg+xml"); + const svgElement = svgDoc.documentElement; + + if (svgElement instanceof SVGSVGElement) { + resolve(svgElement); + } else { + reject(new Error("Parsed element is not an SVGSVGElement")); + } + }) + .catch((error) => { + reject(error); + }); + }); +}; diff --git a/src/scene/export.ts b/src/scene/export.ts index 82f198cfd..35f68ae6e 100644 --- a/src/scene/export.ts +++ b/src/scene/export.ts @@ -3,7 +3,7 @@ import { NonDeletedExcalidrawElement } from "../element/types"; import { getCommonBounds, getElementAbsoluteCoords } from "../element/bounds"; import { renderSceneToSvg, renderStaticScene } from "../renderer/renderScene"; import { distance, isOnlyExportingSingleFrame } from "../utils"; -import { AppState, BinaryFiles } from "../types"; +import { AppState, BinaryFiles, DataURL } from "../types"; import { DEFAULT_EXPORT_PADDING, FANCY_BG_BORDER_RADIUS, @@ -19,7 +19,10 @@ import { updateImageCache, } from "../element/image"; import Scene from "./Scene"; -import { applyFancyBackground } from "./fancyBackground"; +import { + applyFancyBackgroundOnCanvas, + applyFancyBackgroundOnSvg, +} from "./fancyBackground"; export const SVG_EXPORT_TAG = ``; @@ -93,7 +96,7 @@ export const exportToCanvas = async ( }; if (exportWithFancyBackground) { - await applyFancyBackground({ + await applyFancyBackgroundOnCanvas({ canvas, fancyBackgroundImageUrl: appState.fancyBackgroundImageUrl!, backgroundColor: viewBackgroundColor, @@ -136,6 +139,7 @@ export const exportToSvg = async ( exportWithDarkMode?: boolean; exportEmbedScene?: boolean; renderFrame?: boolean; + fancyBackgroundImageUrl: DataURL | null; }, files: BinaryFiles | null, opts?: { @@ -148,7 +152,18 @@ export const exportToSvg = async ( viewBackgroundColor, exportScale = 1, exportEmbedScene, + exportBackground, } = appState; + + const exportWithFancyBackground = + exportBackground && + !!appState.fancyBackgroundImageUrl && + elements.length > 0; + + const padding = !exportWithFancyBackground + ? exportPadding + : (exportPadding + FANCY_BG_PADDING + FANCY_BG_BORDER_RADIUS) * exportScale; + let metadata = ""; if (exportEmbedScene) { try { @@ -163,7 +178,7 @@ export const exportToSvg = async ( console.error(error); } } - const [minX, minY, width, height] = getCanvasSize(elements, exportPadding); + const [minX, minY, width, height] = getCanvasSize(elements, padding); // initialize SVG root const svgRoot = document.createElementNS(SVG_NS, "svg"); @@ -198,8 +213,8 @@ export const exportToSvg = async ( const onlyExportingSingleFrame = isOnlyExportingSingleFrame(elements); - const offsetX = -minX + (onlyExportingSingleFrame ? 0 : exportPadding); - const offsetY = -minY + (onlyExportingSingleFrame ? 0 : exportPadding); + const offsetX = -minX + (onlyExportingSingleFrame ? 0 : padding); + const offsetY = -minY + (onlyExportingSingleFrame ? 0 : padding); const exportingFrame = isExportingWholeCanvas || !onlyExportingSingleFrame @@ -243,13 +258,24 @@ export const exportToSvg = async ( // render background rect if (appState.exportBackground && viewBackgroundColor) { - const rect = svgRoot.ownerDocument!.createElementNS(SVG_NS, "rect"); - rect.setAttribute("x", "0"); - rect.setAttribute("y", "0"); - rect.setAttribute("width", `${width}`); - rect.setAttribute("height", `${height}`); - rect.setAttribute("fill", viewBackgroundColor); - svgRoot.appendChild(rect); + if (appState.fancyBackgroundImageUrl) { + await applyFancyBackgroundOnSvg({ + svgRoot, + fancyBackgroundImageUrl: + `${appState.fancyBackgroundImageUrl}` as DataURL, + backgroundColor: viewBackgroundColor, + dimensions: { w: width, h: height }, + exportScale, + }); + } else { + const rect = svgRoot.ownerDocument!.createElementNS(SVG_NS, "rect"); + rect.setAttribute("x", "0"); + rect.setAttribute("y", "0"); + rect.setAttribute("width", `${width}`); + rect.setAttribute("height", `${height}`); + rect.setAttribute("fill", viewBackgroundColor); + svgRoot.appendChild(rect); + } } const rsvg = rough.svg(svgRoot); diff --git a/src/scene/fancyBackground.ts b/src/scene/fancyBackground.ts index 696702cdb..2906580e4 100644 --- a/src/scene/fancyBackground.ts +++ b/src/scene/fancyBackground.ts @@ -1,5 +1,5 @@ -import { FANCY_BG_BORDER_RADIUS, FANCY_BG_PADDING } from "../constants"; -import { loadHTMLImageElement } from "../element/image"; +import { FANCY_BG_BORDER_RADIUS, FANCY_BG_PADDING, SVG_NS } from "../constants"; +import { loadHTMLImageElement, loadSVGElement } from "../element/image"; import { roundRect } from "../renderer/roundRect"; import { AppState, DataURL } from "../types"; @@ -121,7 +121,7 @@ const addContentBackground = ( }); }; -export const applyFancyBackground = async ({ +export const applyFancyBackgroundOnCanvas = async ({ canvas, fancyBackgroundImageUrl, backgroundColor, @@ -144,3 +144,45 @@ export const applyFancyBackground = async ({ addContentBackground(context, canvasDimensions, backgroundColor, exportScale); }; + +export const applyFancyBackgroundOnSvg = async ({ + svgRoot, + fancyBackgroundImageUrl, + backgroundColor, + dimensions, + exportScale, +}: { + svgRoot: SVGSVGElement; + fancyBackgroundImageUrl: DataURL; + backgroundColor: string; + dimensions: Dimensions; + exportScale: AppState["exportScale"]; +}) => { + const fancyBackgroundImage = await loadSVGElement( + `${fancyBackgroundImageUrl}`, + ); + + fancyBackgroundImage.setAttribute("x", "0"); + fancyBackgroundImage.setAttribute("y", "0"); + fancyBackgroundImage.setAttribute("width", `${dimensions.w}`); + fancyBackgroundImage.setAttribute("height", `${dimensions.h}`); + fancyBackgroundImage.setAttribute("preserveAspectRatio", "none"); + + svgRoot.appendChild(fancyBackgroundImage); + + const rect = svgRoot.ownerDocument!.createElementNS(SVG_NS, "rect"); + rect.setAttribute("x", (FANCY_BG_PADDING * exportScale).toString()); + rect.setAttribute("y", (FANCY_BG_PADDING * exportScale).toString()); + rect.setAttribute( + "width", + `${dimensions.w - FANCY_BG_PADDING * 2 * exportScale}`, + ); + rect.setAttribute( + "height", + `${dimensions.h - FANCY_BG_PADDING * 2 * exportScale}`, + ); + rect.setAttribute("rx", (FANCY_BG_PADDING * exportScale).toString()); + rect.setAttribute("ry", (FANCY_BG_PADDING * exportScale).toString()); + rect.setAttribute("fill", backgroundColor); + svgRoot.appendChild(rect); +};