Compare commits

..

12 Commits

Author SHA1 Message Date
Ryan Di
913921631b fix contain dim change 2025-01-07 22:55:08 +08:00
Ryan Di
ec39094933 simplify code 2025-01-07 19:23:26 +08:00
Ryan Di
ef20c8b9fa tests for the updated api 2024-12-12 15:39:48 +08:00
Ryan Di
1c55160e3f debug both in app 2024-12-10 19:00:58 +08:00
Ryan Di
e49db1dd3c align svg exports with canvas exports 2024-12-10 19:00:30 +08:00
Ryan Di
cd8bfb06b5 fix padding and refactor 2024-12-10 18:37:53 +08:00
Ryan Di
7762b925ae debug preview fix 2024-12-05 16:02:04 +08:00
Ryan Di
113dfc0023 remove cover from fit options 2024-12-05 15:13:58 +08:00
Ryan Di
96b5cfc35d fix data param 2024-12-04 14:33:45 +08:00
Ryan Di
9da26fb7e0 unify exports in a single place 2024-12-04 14:31:15 +08:00
dwelle
392b362118 DEBUG 2024-11-26 12:12:40 +01:00
dwelle
0e02366dee wip 2024-11-26 12:12:40 +01:00
16 changed files with 941 additions and 379 deletions

View File

@@ -7,7 +7,7 @@
<h4 align="center">
<a href="https://excalidraw.com">Excalidraw Editor</a> |
<a href="https://plus.excalidraw.com/blog">Blog</a> |
<a href="https://blog.excalidraw.com">Blog</a> |
<a href="https://docs.excalidraw.com">Documentation</a> |
<a href="https://plus.excalidraw.com">Excalidraw+</a>
</h4>

View File

@@ -66,7 +66,7 @@ const config = {
label: "Docs",
},
{
to: "https://plus.excalidraw.com/blog",
to: "https://blog.excalidraw.com",
label: "Blog",
position: "left",
},
@@ -111,7 +111,7 @@ const config = {
items: [
{
label: "Blog",
to: "https://plus.excalidraw.com/blog",
to: "https://blog.excalidraw.com",
},
{
label: "GitHub",

View File

@@ -26,6 +26,7 @@ import {
StoreAction,
reconcileElements,
exportToCanvas,
exportToSvg,
} from "../packages/excalidraw";
import {
exportToBlob,
@@ -133,7 +134,8 @@ import DebugCanvas, {
import { AIComponents } from "./components/AI";
import { ExcalidrawPlusIframeExport } from "./ExcalidrawPlusIframeExport";
import { fileSave } from "../packages/excalidraw/data/filesystem";
import type { ExportToCanvasConfig } from "../packages/excalidraw/scene/export";
import { type ExportSceneConfig } from "../packages/excalidraw/scene/export";
import { round } from "../packages/math";
polyfill();
@@ -615,18 +617,13 @@ const ExcalidrawWrapper = () => {
}, [excalidrawAPI]);
const canvasPreviewContainerRef = useRef<HTMLDivElement>(null);
const svgPreviewContainerRef = useRef<HTMLDivElement>(null);
const [config, setConfig] = useState<ExportToCanvasConfig>(
JSON.parse(localStorage.getItem("_exportConfig") || "null") || {
width: 300,
height: 100,
padding: 2,
scale: 1,
position: "none",
fit: "contain",
canvasBackgroundColor: "yellow",
},
);
const [config, setConfig] = useState<ExportSceneConfig>({
scale: 1,
position: "center",
fit: "contain",
});
useEffect(() => {
localStorage.setItem("_exportConfig", JSON.stringify(config));
@@ -641,90 +638,83 @@ const ExcalidrawWrapper = () => {
collabAPI.syncElements(elements);
}
{
const frame = elements.find(
(el) => el.strokeStyle === "dashed" && !el.isDeleted,
);
const nonDeletedElements = getNonDeletedElements(elements);
exportToCanvas({
data: {
elements: getNonDeletedElements(elements).filter(
(x) => x.id !== frame?.id,
),
// .concat(
// restoreElements(
// [
// // @ts-ignore
// {
// type: "rectangle",
// width: appState.width / zoom,
// height: appState.height / zoom,
// x: -appState.scrollX,
// y: -appState.scrollY,
// fillStyle: "solid",
// strokeColor: "transparent",
// backgroundColor: "rgba(0,0,0,0.05)",
// roundness: { type: ROUNDNESS.ADAPTIVE_RADIUS, value: 40 },
// },
// ],
// null,
// ),
// ),
appState,
files,
},
config: {
// // light yellow
// // canvasBackgroundColor: "#fff9c4",
// // width,
// // maxWidthOrHeight: 120,
// // scale: 0.01,
// // scale: 2,
// // origin: "content",
// // fit: "cover",
// // scale: 2,
// // x: 0,
// // y: 0,
// padding: 20,
const frame = nonDeletedElements.find(
(el) => el.strokeStyle === "dashed" && el.type === "rectangle",
);
// ...config,
exportToCanvas({
data: {
elements: nonDeletedElements.filter((x) => x.id !== frame?.id),
// .concat(
// restoreElements(
// [
// // @ts-ignore
// {
// type: "rectangle",
// width: appState.width / zoom,
// height: appState.height / zoom,
// x: -appState.scrollX,
// y: -appState.scrollY,
// fillStyle: "solid",
// strokeColor: "transparent",
// backgroundColor: "rgba(0,0,0,0.05)",
// roundness: { type: ROUNDNESS.ADAPTIVE_RADIUS, value: 40 },
// },
// ],
// null,
// ),
// ),
appState,
files,
},
config: {
...(frame
? {
...config,
width: frame.width,
height: frame.height,
x: frame.x,
y: frame.y,
}
: config),
},
}).then((canvas) => {
if (canvasPreviewContainerRef.current) {
canvasPreviewContainerRef.current.replaceChildren(canvas);
document.querySelector(
".canvas_dims",
)!.innerHTML = `${canvas.width}x${canvas.height} (canvas)`;
}
});
// width: config.width,
// height: config.height,
// maxWidthOrHeight: config.maxWidthOrHeight,
// widthOrHeight: config.widthOrHeight,
// padding: config.padding,
...(frame
? {
...config,
width: frame.width,
height: frame.height,
x: frame.x,
y: frame.y,
}
: config),
// // height: 140,
// // x: -appState.scrollX,
// // y: -appState.scrollY,
// // height: 150,
// // height: appState.height,
// // scale,
// // zoom: { value: appState.zoom.value },
// // getDimensions(width,height) {
// // setCanvasSize({ width, height })
// // return {width: 300, height: 150}
// // }
},
}).then((canvas) => {
if (canvasPreviewContainerRef.current) {
canvasPreviewContainerRef.current.replaceChildren(canvas);
document.querySelector(
".dims",
)!.innerHTML = `${canvas.width}x${canvas.height}`;
// canvas.style.width = "100%";
}
});
}
exportToSvg({
data: {
elements: nonDeletedElements.filter((x) => x.id !== frame?.id),
appState,
files,
},
config: {
...(frame
? {
...config,
width: frame.width,
height: frame.height,
x: frame.x,
y: frame.y,
}
: config),
},
}).then((svg) => {
if (svgPreviewContainerRef.current) {
svgPreviewContainerRef.current.replaceChildren(svg);
document.querySelector(".svg_dims")!.innerHTML = `${round(
parseFloat(svg.getAttribute("width") ?? ""),
0,
)}x${round(parseFloat(svg.getAttribute("height") ?? ""), 0)} (svg)`;
}
});
// this check is redundant, but since this is a hot path, it's best
// not to evaludate the nested expression every time
@@ -1270,7 +1260,6 @@ const ExcalidrawWrapper = () => {
>
<option value="none">none</option>
<option value="contain">contain</option>
<option value="cover">cover</option>
</select>
</label>
<label>
@@ -1431,7 +1420,7 @@ const ExcalidrawWrapper = () => {
</label>
</div>
</div>
<div className="dims">0x0</div>
<div className="canvas_dims">0x0</div>
<div
ref={canvasPreviewContainerRef}
onClick={() => {
@@ -1457,6 +1446,32 @@ const ExcalidrawWrapper = () => {
backgroundColor: "pink",
}}
/>
<div className="svg_dims">0x0</div>
<div
ref={svgPreviewContainerRef}
onClick={() => {
exportToBlob({
data: {
elements: excalidrawAPI!.getSceneElements(),
files: excalidrawAPI?.getFiles() || null,
},
config,
}).then((blob) => {
fileSave(blob, {
name: "xx",
extension: "png",
description: "xxx",
});
});
}}
style={{
borderRadius: 12,
border: "1px solid #777",
overflow: "hidden",
padding: 10,
backgroundColor: "pink",
}}
/>
</div>
</div>
);

View File

@@ -8,7 +8,7 @@ export const EncryptedIcon = () => {
return (
<a
className="encrypted-icon tooltip"
href="https://plus.excalidraw.com/blog/end-to-end-encryption/"
href="https://blog.excalidraw.com/end-to-end-encryption/"
target="_blank"
rel="noopener noreferrer"
aria-label={t("encrypted.link")}

View File

@@ -54,8 +54,6 @@
content="https://excalidraw.com/og-image-3.png"
/>
<link rel="canonical" href="https://excalidraw.com" />
<!------------------------------------------------------------------------->
<!-- to minimize white flash on load when user has dark mode enabled -->
<script>

View File

@@ -50,9 +50,6 @@ const ChartPreviewBtn = (props: {
},
files: null,
},
config: {
skipInliningFonts: true,
},
});
svg.querySelector(".style-fonts")?.remove();
previewNode.replaceChildren();

View File

@@ -140,9 +140,6 @@ const SingleLibraryItem = ({
},
files: null,
},
config: {
skipInliningFonts: true,
},
});
node.innerHTML = svg.outerHTML;
})();

View File

@@ -309,6 +309,7 @@ export const MAX_DECIMALS_FOR_SVG_EXPORT = 2;
export const EXPORT_SCALES = [1, 2, 3];
export const DEFAULT_EXPORT_PADDING = 10; // px
export const DEFAULT_SMALLEST_EXPORT_SIZE = 20; // px
export const DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT = 1440;

View File

@@ -121,13 +121,12 @@ export const exportAsImage = async ({
exportBackground: cfg.exportBackground,
exportWithDarkMode: data.appState.exportWithDarkMode,
viewBackgroundColor: data.appState.viewBackgroundColor,
exportPadding: cfg.padding,
exportScale: data.appState.exportScale,
exportEmbedScene: data.appState.exportEmbedScene && type === "svg",
},
files: data.files,
},
config: { exportingFrame: cfg.exportingFrame },
config: { exportingFrame: cfg.exportingFrame, padding: cfg.padding },
});
if (type === "svg") {
return fileSave(

View File

@@ -19,10 +19,6 @@ const exportLibraryItemToSvg = async (elements: LibraryItem["elements"]) => {
},
files: null,
},
config: {
renderEmbeddables: false,
skipInliningFonts: true,
},
});
};

View File

@@ -14,7 +14,6 @@ import { arrayToMap, distance, getFontString, toBrandedType } from "../utils";
import type { AppState, BinaryFiles } from "../types";
import {
COLOR_WHITE,
DEFAULT_EXPORT_PADDING,
DEFAULT_ZOOM_VALUE,
FRAME_STYLE,
FONT_FAMILY,
@@ -22,6 +21,7 @@ import {
THEME,
THEME_FILTER,
MIME_TYPES,
DEFAULT_SMALLEST_EXPORT_SIZE,
} from "../constants";
import { getDefaultAppState } from "../appState";
import { serializeAsJSON } from "../data/json";
@@ -29,14 +29,14 @@ import {
getInitializedImageElements,
updateImageCache,
} from "../element/image";
import { restore, restoreAppState } from "../data/restore";
import { restoreAppState } from "../data/restore";
import {
getElementsOverlappingFrame,
getFrameLikeElements,
getFrameLikeTitle,
getRootElements,
} from "../frame";
import { getNonDeletedElements, newTextElement } from "../element";
import { newTextElement } from "../element";
import { type Mutable } from "../utility-types";
import { newElementWith } from "../element/mutateElement";
import { isFrameLikeElement } from "../element/typeChecks";
@@ -164,13 +164,13 @@ type ExportToCanvasAppState = Partial<
Omit<AppState, "offsetTop" | "offsetLeft">
>;
export type ExportToCanvasData = {
export type ExportSceneData = {
elements: readonly NonDeletedExcalidrawElement[];
appState?: ExportToCanvasAppState;
files: BinaryFiles | null;
};
export type ExportToCanvasConfig = {
export type ExportSceneConfig = {
theme?: Theme;
/**
* Canvas background. Valid values are:
@@ -197,8 +197,6 @@ export type ExportToCanvasConfig = {
* order to maintain the aspect ratio. It is recommended to set `position`
* to `center` when using `fit=contain`.
*
* When `fit` is set to `cover`, padding is disabled (set to 0).
*
* When `fit` is set to `none` and either `width` or `height` or
* `maxWidthOrHeight` is set, padding is simply adding to the bounding box
* and the content may overflow the canvas, thus right or bottom padding
@@ -279,8 +277,6 @@ export type ExportToCanvasConfig = {
*
* - `none` - no scaling.
* - `contain` - scale to fit the frame. Includes `padding`.
* - `cover` - scale to fill the frame while maintaining aspect ratio. If
* content overflows, it will be cropped.
*
* If `maxWidthOrHeight` or `widthOrHeight` is set, `fit` is ignored.
*
@@ -288,7 +284,7 @@ export type ExportToCanvasConfig = {
* `widthOrHeight` is specified in which case `none` is the default (can be
* changed). If `x` or `y` are specified, `none` is forced.
*/
fit?: "none" | "contain" | "cover";
fit?: "none" | "contain";
/**
* When either `x` or `y` are not specified, indicates how the canvas should
* be aligned on the respective axis.
@@ -339,21 +335,16 @@ export type ExportToCanvasConfig = {
loadFonts?: () => Promise<void>;
};
/**
* This API is usually used as a precursor to searializing to Blob or PNG,
* but can also be used to create a canvas for other purposes.
*/
export const exportToCanvas = async ({
const configExportDimension = async ({
data,
config,
}: {
data: ExportToCanvasData;
config?: ExportToCanvasConfig;
data: ExportSceneData;
config?: ExportSceneConfig;
}) => {
// clone
const cfg = Object.assign({}, config);
const { files } = data;
const { exportingFrame } = cfg;
const elements = data.elements;
@@ -393,19 +384,6 @@ export const exportToCanvas = async ({
? "contain"
: "none");
const containPadding = cfg.fit === "contain";
if (cfg.x != null || cfg.x != null) {
cfg.fit = "none";
}
if (cfg.fit === "cover") {
if (cfg.padding && !import.meta.env.PROD) {
console.warn("`padding` is ignored when `fit` is set to `cover`");
}
cfg.padding = 0;
}
cfg.padding = cfg.padding ?? 0;
cfg.scale = cfg.scale ?? 1;
@@ -443,7 +421,7 @@ export const exportToCanvas = async ({
// make the canvas fit into the frame (e.g. for `cfg.fit` set to `contain`).
// If `cfg.scale` is set, we multiply the resulting canvasScale by it to
// scale the output further.
let canvasScale = 1;
let exportScale = 1;
const origCanvasSize = getCanvasSize(
exportingFrame ? [exportingFrame] : getRootElements(elementsForRender),
@@ -457,36 +435,47 @@ export const exportToCanvas = async ({
// variables for target bounding box
let [x, y, width, height] = origCanvasSize;
if (cfg.width != null) {
width = cfg.width;
x = cfg.x ?? x;
y = cfg.y ?? y;
width = cfg.width ?? width;
height = cfg.height ?? height;
if (cfg.padding && containPadding) {
width -= cfg.padding * 2;
}
if (cfg.fit === "contain" || cfg.widthOrHeight || cfg.maxWidthOrHeight) {
cfg.padding =
cfg.padding && cfg.padding > 0
? Math.min(
cfg.padding,
(width - DEFAULT_SMALLEST_EXPORT_SIZE) / 2,
(height - DEFAULT_SMALLEST_EXPORT_SIZE) / 2,
)
: 0;
if (cfg.height) {
height = cfg.height;
if (cfg.padding && containPadding) {
height -= cfg.padding * 2;
}
} else {
// if height not specified, scale the original height to match the new
// width while maintaining aspect ratio
height *= width / origWidth;
}
} else if (cfg.height != null) {
height = cfg.height;
if (cfg.getDimensions != null) {
const ret = cfg.getDimensions(width, height);
if (cfg.padding && containPadding) {
height -= cfg.padding * 2;
width = ret.width;
height = ret.height;
cfg.padding = Math.min(
cfg.padding,
(width - DEFAULT_SMALLEST_EXPORT_SIZE) / 2,
(height - DEFAULT_SMALLEST_EXPORT_SIZE) / 2,
);
} else if (cfg.widthOrHeight != null) {
cfg.padding = Math.min(
cfg.padding,
(cfg.widthOrHeight - DEFAULT_SMALLEST_EXPORT_SIZE) / 2,
);
} else if (cfg.maxWidthOrHeight != null) {
cfg.padding = Math.min(
cfg.padding,
(cfg.maxWidthOrHeight - DEFAULT_SMALLEST_EXPORT_SIZE) / 2,
);
}
// width not specified, so scale the original width to match the new
// height while maintaining aspect ratio
width *= height / origHeight;
}
if (cfg.maxWidthOrHeight != null || cfg.widthOrHeight != null) {
if (containPadding && cfg.padding) {
if (cfg.padding) {
if (cfg.maxWidthOrHeight != null) {
cfg.maxWidthOrHeight -= cfg.padding * 2;
} else if (cfg.widthOrHeight != null) {
@@ -498,50 +487,27 @@ export const exportToCanvas = async ({
if (cfg.widthOrHeight != null) {
// calculate by how much do we need to scale the canvas to fit into the
// target dimension (e.g. target: max 50px, actual: 70x100px => scale: 0.5)
canvasScale = cfg.widthOrHeight / max;
exportScale = cfg.widthOrHeight / max;
} else if (cfg.maxWidthOrHeight != null) {
canvasScale = cfg.maxWidthOrHeight < max ? cfg.maxWidthOrHeight / max : 1;
exportScale = cfg.maxWidthOrHeight < max ? cfg.maxWidthOrHeight / max : 1;
}
width *= canvasScale;
height *= canvasScale;
width *= exportScale;
height *= exportScale;
} else if (cfg.getDimensions) {
const ret = cfg.getDimensions(width, height);
width = ret.width;
height = ret.height;
cfg.scale = ret.scale ?? cfg.scale;
} else if (
containPadding &&
cfg.padding &&
cfg.width == null &&
cfg.height == null
) {
const whRatio = width / height;
} else if (cfg.fit === "contain") {
width -= cfg.padding * 2;
height -= (cfg.padding * 2) / whRatio;
}
height -= cfg.padding * 2;
if (
(cfg.fit === "contain" && !cfg.maxWidthOrHeight) ||
(containPadding && cfg.padding)
) {
if (cfg.fit === "contain") {
const wRatio = width / origWidth;
const hRatio = height / origHeight;
// scale the orig canvas to fit in the target frame
canvasScale = Math.min(wRatio, hRatio);
} else {
const wRatio = (width - cfg.padding * 2) / width;
const hRatio = (height - cfg.padding * 2) / height;
canvasScale = Math.min(wRatio, hRatio);
}
} else if (cfg.fit === "cover") {
const wRatio = width / origWidth;
const hRatio = height / origHeight;
// scale the orig canvas to fill the the target frame
// (opposite of "contain")
canvasScale = Math.max(wRatio, hRatio);
// scale the orig canvas to fit in the target region
exportScale = Math.min(wRatio, hRatio);
}
x = cfg.x ?? origX;
@@ -563,38 +529,83 @@ export const exportToCanvas = async ({
// aspect ratio dimensions.
if (cfg.position === "center") {
x -=
width / canvasScale / 2 -
width / exportScale / 2 -
(cfg.x == null ? origWidth : width + cfg.padding * 2) / 2;
y -=
height / canvasScale / 2 -
height / exportScale / 2 -
(cfg.y == null ? origHeight : height + cfg.padding * 2) / 2;
}
// rescale padding based on current canvasScale factor so that the resulting
// padding is kept the same as supplied by user (with the exception of
// `cfg.scale` being set, which also scales the padding)
const normalizedPadding = cfg.padding / exportScale;
// scale the whole frame by cfg.scale (on top of whatever canvasScale we
// calculated above)
exportScale *= cfg.scale;
width *= cfg.scale;
height *= cfg.scale;
const exportWidth = width + cfg.padding * 2 * cfg.scale;
const exportHeight = height + cfg.padding * 2 * cfg.scale;
return {
config: cfg,
normalizedPadding,
contentWidth: width,
contentHeight: height,
exportWidth,
exportHeight,
exportScale,
x,
y,
elementsForRender,
appState,
frameRendering,
};
};
/**
* This API is usually used as a precursor to searializing to Blob or PNG,
* but can also be used to create a canvas for other purposes.
*/
export const exportToCanvas = async ({
data,
config,
}: {
data: ExportSceneData;
config?: ExportSceneConfig;
}) => {
const {
config: cfg,
normalizedPadding,
contentWidth: width,
contentHeight: height,
exportWidth,
exportHeight,
exportScale,
x,
y,
elementsForRender,
appState,
frameRendering,
} = await configExportDimension({ data, config });
const canvas = cfg.createCanvas
? cfg.createCanvas()
: document.createElement("canvas");
// rescale padding based on current canvasScale factor so that the resulting
// padding is kept the same as supplied by user (with the exception of
// `cfg.scale` being set, which also scales the padding)
const normalizedPadding = cfg.padding / canvasScale;
// scale the whole frame by cfg.scale (on top of whatever canvasScale we
// calculated above)
canvasScale *= cfg.scale;
width *= cfg.scale;
height *= cfg.scale;
canvas.width = width + cfg.padding * 2 * cfg.scale;
canvas.height = height + cfg.padding * 2 * cfg.scale;
canvas.width = exportWidth;
canvas.height = exportHeight;
const { imageCache } = await updateImageCache({
imageCache: new Map(),
fileIds: getInitializedImageElements(elementsForRender).map(
(element) => element.fileId,
),
files: files || {},
files: data.files || {},
});
renderStaticScene({
@@ -604,7 +615,7 @@ export const exportToCanvas = async ({
arrayToMap(elementsForRender),
),
allElementsMap: toBrandedType<NonDeletedSceneElementsMap>(
arrayToMap(syncInvalidIndices(elements)),
arrayToMap(syncInvalidIndices(data.elements)),
),
visibleElements: elementsForRender,
appState: {
@@ -621,7 +632,7 @@ export const exportToCanvas = async ({
shouldCacheIgnoreZoom: false,
theme: cfg.theme || THEME.LIGHT,
},
scale: canvasScale,
scale: exportScale,
renderConfig: {
canvasBackgroundColor:
cfg.canvasBackgroundColor === false
@@ -644,7 +655,7 @@ export const exportToCanvas = async ({
};
type ExportToSvgConfig = Pick<
ExportToCanvasConfig,
ExportSceneConfig,
"canvasBackgroundColor" | "padding" | "theme" | "exportingFrame"
> & {
/**
@@ -659,108 +670,75 @@ export const exportToSvg = async ({
data,
config,
}: {
data: {
elements: readonly NonDeletedExcalidrawElement[];
appState: {
exportBackground: boolean;
exportScale?: number;
viewBackgroundColor: string;
exportWithDarkMode?: boolean;
exportEmbedScene?: boolean;
frameRendering?: AppState["frameRendering"];
gridModeEnabled?: boolean;
};
files: BinaryFiles | null;
};
config?: ExportToSvgConfig;
}): Promise<SVGSVGElement> => {
// clone
const cfg = Object.assign({}, config);
cfg.exportingFrame = cfg.exportingFrame ?? null;
const { elements: restoredElements } = restore(
{ ...data, files: data.files || {} },
null,
null,
);
const elements = getNonDeletedElements(restoredElements);
const frameRendering = getFrameRenderingConfig(
cfg?.exportingFrame ?? null,
data.appState.frameRendering ?? null,
);
let {
exportWithDarkMode = false,
viewBackgroundColor,
exportScale = 1,
exportEmbedScene,
} = data.appState;
let padding = cfg.padding ?? 0;
const elementsForRender = prepareElementsForRender({
elements,
exportingFrame: cfg.exportingFrame,
exportWithDarkMode,
data: ExportSceneData;
config?: ExportSceneConfig;
}) => {
const {
config: cfg,
normalizedPadding,
exportWidth,
exportHeight,
exportScale,
x,
y,
elementsForRender,
appState,
frameRendering,
});
} = await configExportDimension({ data, config });
if (cfg.exportingFrame) {
padding = 0;
const offsetX = -(x - normalizedPadding);
const offsetY = -(y - normalizedPadding);
const { elements } = data;
// initialize SVG root
const svgRoot = document.createElementNS(SVG_NS, "svg");
svgRoot.setAttribute("version", "1.1");
svgRoot.setAttribute("xmlns", SVG_NS);
svgRoot.setAttribute(
"viewBox",
`0 0 ${exportWidth / exportScale} ${exportHeight / exportScale}`,
);
svgRoot.setAttribute("width", `${exportWidth}`);
svgRoot.setAttribute("height", `${exportHeight}`);
if (cfg.theme === THEME.DARK) {
svgRoot.setAttribute("filter", THEME_FILTER);
}
const fontFaces = cfg.loadFonts
? await Fonts.generateFontFaceDeclarations(elements)
: [];
const delimiter = "\n "; // 6 spaces
let metadata = "";
// we need to serialize the "original" elements before we put them through
// the tempScene hack which duplicates and regenerates ids
if (exportEmbedScene) {
if (appState.exportEmbedScene) {
try {
metadata = (await import("../data/image")).encodeSvgMetadata({
// when embedding scene, we want to embed the origionally supplied
// elements which don't contain the temp frame labels.
// But it also requires that the exportToSvg is being supplied with
// only the elements that we're exporting, and no extra.
text: serializeAsJSON(
elements,
data.appState,
data.files || {},
"local",
),
text: serializeAsJSON(elements, appState, data.files || {}, "local"),
});
} catch (error: any) {
console.error(error);
}
}
let [minX, minY, width, height] = getCanvasSize(
cfg.exportingFrame
? [cfg.exportingFrame]
: getRootElements(elementsForRender),
);
width += padding * 2;
height += padding * 2;
// initialize SVG root
const svgRoot = document.createElementNS(SVG_NS, "svg");
svgRoot.setAttribute("version", "1.1");
svgRoot.setAttribute("xmlns", SVG_NS);
svgRoot.setAttribute("viewBox", `0 0 ${width} ${height}`);
svgRoot.setAttribute("width", `${width * exportScale}`);
svgRoot.setAttribute("height", `${height * exportScale}`);
if (exportWithDarkMode) {
svgRoot.setAttribute("filter", THEME_FILTER);
let exportContentClipPath = "";
if (cfg.width != null && cfg.height != null) {
exportContentClipPath = `<clipPath id="content">
<rect x="${offsetX}" y="${offsetY}" width="${exportWidth}" height="${exportWidth}"></rect>
</clipPath>`;
}
const offsetX = -minX + padding;
const offsetY = -minY + padding;
const frameElements = getFrameLikeElements(elements);
let exportingFrameClipPath = "";
const elementsMap = arrayToMap(elements);
const frameElements = getFrameLikeElements(elements);
for (const frame of frameElements) {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(frame, elementsMap);
const cx = (x2 - x1) / 2 - (frame.x - x1);
@@ -782,36 +760,33 @@ export const exportToSvg = async ({
</clipPath>`;
}
const fontFaces = !cfg?.skipInliningFonts
? await Fonts.generateFontFaceDeclarations(elements)
: [];
const delimiter = "\n "; // 6 spaces
svgRoot.innerHTML = `
${SVG_EXPORT_TAG}
${metadata}
<defs>
<style class="style-fonts">${delimiter}${fontFaces.join(delimiter)}
</style>
<style class="style-fonts">${delimiter}${fontFaces.join(delimiter)}</style>
${exportContentClipPath}
${exportingFrameClipPath}
</defs>
`;
// render background rect
if (data.appState.exportBackground && viewBackgroundColor) {
if (appState.exportBackground && appState.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);
rect.setAttribute("width", `${exportWidth / exportScale}`);
rect.setAttribute("height", `${exportHeight / exportScale}`);
rect.setAttribute(
"fill",
cfg.canvasBackgroundColor || appState.viewBackgroundColor,
);
svgRoot.appendChild(rect);
}
const rsvg = rough.svg(svgRoot);
const renderEmbeddables = cfg.renderEmbeddables ?? false;
// const renderEmbeddables = appState.embe ?? false;
renderSceneToSvg(
elementsForRender,
@@ -823,18 +798,18 @@ export const exportToSvg = async ({
offsetX,
offsetY,
isExporting: true,
exportWithDarkMode,
renderEmbeddables,
exportWithDarkMode: cfg.theme === THEME.DARK,
renderEmbeddables: false,
frameRendering,
canvasBackgroundColor: viewBackgroundColor,
embedsValidationStatus: renderEmbeddables
canvasBackgroundColor: appState.viewBackgroundColor,
embedsValidationStatus: false
? new Map(
elementsForRender
.filter((element) => isFrameLikeElement(element))
.map((element) => [element.id, true]),
)
: new Map(),
reuseImages: cfg?.reuseImages ?? true,
reuseImages: true,
},
);
@@ -854,7 +829,7 @@ export const getCanvasSize = (
export { MIME_TYPES };
type ExportToBlobConfig = ExportToCanvasConfig & {
type ExportToBlobConfig = ExportSceneConfig & {
mimeType?: string;
quality?: number;
};
@@ -863,7 +838,7 @@ export const exportToBlob = async ({
data,
config,
}: {
data: ExportToCanvasData;
data: ExportSceneData;
config?: ExportToBlobConfig;
}): Promise<Blob> => {
let { mimeType = MIME_TYPES.png, quality } = config || {};
@@ -928,7 +903,7 @@ export const exportToClipboard = async ({
data,
config,
}: {
data: ExportToCanvasData;
data: ExportSceneData;
} & (
| { type: "png"; config?: ExportToBlobConfig }
| { type: "svg"; config?: ExportToSvgConfig }

View File

@@ -6,7 +6,7 @@ exports[`Test <MermaidToExcalidraw/> > should open mermaid popup when active too
B --&gt; C{Let me think}
C --&gt;|One| D[Laptop]
C --&gt;|Two| E[iPhone]
C --&gt;|Three| F[Car]</textarea><div class="ttd-dialog-panel-button-container invisible" style="display: flex; align-items: center;"><button type="button" class="excalidraw-button ttd-dialog-panel-button"><div class=""></div></button></div></div><div class="ttd-dialog-panel"><div class="ttd-dialog-panel__header"><label>Preview</label></div><div class="ttd-dialog-output-wrapper"><div style="opacity: 1;" class="ttd-dialog-output-canvas-container"><canvas width="9" height="0" dir="ltr"></canvas></div></div><div class="ttd-dialog-panel-button-container" style="display: flex; align-items: center;"><button type="button" class="excalidraw-button ttd-dialog-panel-button"><div class="">Insert<span><svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 20 20" class="" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><g stroke-width="1.25"><path d="M4.16602 10H15.8327"></path><path d="M12.5 13.3333L15.8333 10"></path><path d="M12.5 6.66666L15.8333 9.99999"></path></g></svg></span></div></button><div class="ttd-dialog-submit-shortcut"><div class="ttd-dialog-submit-shortcut__key">Ctrl</div><div class="ttd-dialog-submit-shortcut__key">Enter</div></div></div></div></div></div></div></div></div></div></div>"
C --&gt;|Three| F[Car]</textarea><div class="ttd-dialog-panel-button-container invisible" style="display: flex; align-items: center;"><button type="button" class="excalidraw-button ttd-dialog-panel-button"><div class=""></div></button></div></div><div class="ttd-dialog-panel"><div class="ttd-dialog-panel__header"><label>Preview</label></div><div class="ttd-dialog-output-wrapper"><div style="opacity: 1;" class="ttd-dialog-output-canvas-container"><canvas width="300" height="0" dir="ltr"></canvas></div></div><div class="ttd-dialog-panel-button-container" style="display: flex; align-items: center;"><button type="button" class="excalidraw-button ttd-dialog-panel-button"><div class="">Insert<span><svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 20 20" class="" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><g stroke-width="1.25"><path d="M4.16602 10H15.8327"></path><path d="M12.5 13.3333L15.8333 10"></path><path d="M12.5 6.66666L15.8333 9.99999"></path></g></svg></span></div></button><div class="ttd-dialog-submit-shortcut"><div class="ttd-dialog-submit-shortcut__key">Ctrl</div><div class="ttd-dialog-submit-shortcut__key">Enter</div></div></div></div></div></div></div></div></div></div></div>"
`;
exports[`Test <MermaidToExcalidraw/> > should show error in preview when mermaid library throws error 1`] = `

View File

@@ -6,8 +6,8 @@ exports[`export > exporting svg containing transformed images > svg export outpu
<defs>
<style class="style-fonts">
</style>
</style>
</defs>
<clipPath id="image-clipPath-id1" data-id="id1"><rect width="100" height="100" rx="25" ry="25"></rect></clipPath><g transform="translate(20.710678118654755 20.710678118654755) rotate(315 50 50)" clip-path="url(#image-clipPath-id1)" data-id="id1"><use href="#image-file_A" width="100" height="100" opacity="1"></use></g><clipPath id="image-clipPath-id2" data-id="id2"><rect width="50" height="50" rx="12.5" ry="12.5"></rect></clipPath><g transform="translate(120.71067811865476 20.710678118654755) rotate(45 25 25)" clip-path="url(#image-clipPath-id2)" data-id="id2"><use href="#image-file_A" width="50" height="50" opacity="1" transform="translate(25 25) scale(-1 1) translate(-25 -25)"></use></g><clipPath id="image-clipPath-id3" data-id="id3"><rect width="100" height="100" rx="25" ry="25"></rect></clipPath><g transform="translate(20.710678118654755 120.71067811865476) rotate(45 50 50)" clip-path="url(#image-clipPath-id3)" data-id="id3"><use href="#image-file_A" width="100" height="100" opacity="1" transform="translate(50 50) scale(1 -1) translate(-50 -50)"></use></g><clipPath id="image-clipPath-id4" data-id="id4"><rect width="50" height="50" rx="12.5" ry="12.5"></rect></clipPath><g transform="translate(120.71067811865476 120.71067811865476) rotate(315 25 25)" clip-path="url(#image-clipPath-id4)" data-id="id4"><use href="#image-file_A" width="50" height="50" opacity="1" transform="translate(25 25) scale(-1 -1) translate(-25 -25)"></use></g></svg>"

View File

@@ -24,7 +24,7 @@ exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu it
</button>
<a
class="dropdown-menu-item dropdown-menu-item-base"
href="https://plus.excalidraw.com/blog"
href="blog.excalidaw.com"
rel="noreferrer"
target="_blank"
>

File diff suppressed because one or more lines are too long

View File

@@ -15,6 +15,7 @@ import { FONT_FAMILY, FRAME_STYLE } from "../../constants";
import { prepareElementsForExport } from "../../data";
import { diagramFactory } from "../fixtures/diagramFixture";
import { vi } from "vitest";
import { isCloseTo } from "../../../math";
const DEFAULT_OPTIONS = {
exportBackground: false,
@@ -117,9 +118,7 @@ describe("exportToSvg", () => {
},
});
expect(svgElement.getAttribute("filter")).toMatchInlineSnapshot(
`"_themeFilter_1883f3"`,
);
expect(svgElement.getAttribute("filter")).toMatchInlineSnapshot(`null`);
});
it("with exportPadding", async () => {
@@ -149,10 +148,12 @@ describe("exportToSvg", () => {
elements: ELEMENTS,
appState: {
...DEFAULT_OPTIONS,
exportScale: SCALE,
},
files: null,
},
config: {
scale: SCALE,
},
});
expect(svgElement).toHaveAttribute(
@@ -606,3 +607,622 @@ describe("exportToBlob", async () => {
});
});
});
describe("updated API", () => {
// set up
// a random set of elements
const ELEMENT_HEIGHT = 100;
const ELEMENT_WIDTH = 100;
const POSITION = 1000;
const getRandomPos = () => {
const randomNum = () =>
Math.round((Math.random() < 0.5 ? 1 : -1) * Math.random() * POSITION);
return { x: randomNum(), y: randomNum() };
};
const ELEMENTS = [
{
...diamondFixture,
height: ELEMENT_HEIGHT,
width: ELEMENT_WIDTH,
index: "a0",
...getRandomPos(),
},
{
...ellipseFixture,
height: ELEMENT_HEIGHT,
width: ELEMENT_WIDTH,
index: "a1",
...getRandomPos(),
},
{
...textFixture,
height: ELEMENT_HEIGHT,
width: ELEMENT_WIDTH,
index: "a2",
...getRandomPos(),
},
{
...textFixture,
fontFamily: FONT_FAMILY.Nunito, // test embedding external font
height: ELEMENT_HEIGHT,
width: ELEMENT_WIDTH,
index: "a3",
...getRandomPos(),
},
] as NonDeletedExcalidrawElement[];
// entire canvas
describe("exporting the entire canvas", () => {
const [, , canvasWidth, canvasHeight] = exportUtils.getCanvasSize(ELEMENTS);
it("fit = none, no padding", async () => {
const svgElement = await exportUtils.exportToSvg({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config: {
fit: "none",
},
});
const canvas = await exportUtils.exportToCanvas({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
});
expect(svgElement.getAttribute("width")).toBeCloseTo(canvas.width);
expect(svgElement.getAttribute("height")).toBeCloseTo(canvas.height);
expect(canvas.width).toBeCloseTo(canvasWidth, 1);
expect(canvas.height).toBeCloseTo(canvasHeight, 1);
});
it("fit = contain, no padding", async () => {
const config: exportUtils.ExportSceneConfig = {
fit: "none",
};
const svgElement = await exportUtils.exportToSvg({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config,
});
const canvas = await exportUtils.exportToCanvas({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config: {
fit: "contain",
},
});
expect(svgElement.getAttribute("width")).toBeCloseTo(canvas.width);
expect(svgElement.getAttribute("height")).toBeCloseTo(canvas.height);
expect(isCloseTo(canvas.width, canvasWidth, 1)).toBe(true);
expect(isCloseTo(canvas.height, canvasHeight, 1)).toBe(true);
});
it("fit = none, with padding", async () => {
const PADDING = Math.round(Math.random() * 100);
const config: exportUtils.ExportSceneConfig = {
fit: "none",
padding: PADDING,
};
const svgElement = await exportUtils.exportToSvg({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config,
});
const canvas = await exportUtils.exportToCanvas({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config,
});
expect(svgElement.getAttribute("width")).toBeCloseTo(canvas.width);
expect(svgElement.getAttribute("height")).toBeCloseTo(canvas.height);
expect(canvas.width).toBeCloseTo(canvasWidth + PADDING * 2);
expect(canvas.height).toBeCloseTo(canvasHeight + PADDING * 2);
});
it("fit = contain, with padding", async () => {
const PADDING = Math.round(Math.random() * 100);
const config: exportUtils.ExportSceneConfig = {
fit: "contain",
padding: PADDING,
};
const svgElement = await exportUtils.exportToSvg({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config,
});
const canvas = await exportUtils.exportToCanvas({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config,
});
expect(
isCloseTo(
parseFloat(svgElement.getAttribute("width") ?? ""),
canvas.width,
1,
),
).toBe(true);
expect(
isCloseTo(
parseFloat(svgElement.getAttribute("height") ?? ""),
canvas.height,
1,
),
).toBe(true);
});
});
// specified dimensions (w x h)
describe("exporting with specified dimensions", () => {
const dimension = {
width: 200,
height: 200,
};
it("fit = none, no padding", async () => {
const config: exportUtils.ExportSceneConfig = {
fit: "none",
...dimension,
};
const svgElement = await exportUtils.exportToSvg({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config,
});
const canvas = await exportUtils.exportToCanvas({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config,
});
expect(svgElement.getAttribute("width")).toBeCloseTo(canvas.width);
expect(svgElement.getAttribute("height")).toBeCloseTo(canvas.height);
expect(canvas.width).toBeCloseTo(dimension.width, 1);
expect(canvas.height).toBeCloseTo(dimension.height, 1);
});
it("fit = contain, no padding", async () => {
const config: exportUtils.ExportSceneConfig = {
fit: "contain",
...dimension,
};
const svgElement = await exportUtils.exportToSvg({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config,
});
const canvas = await exportUtils.exportToCanvas({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config,
});
expect(svgElement.getAttribute("width")).toBeCloseTo(canvas.width);
expect(svgElement.getAttribute("height")).toBeCloseTo(canvas.height);
expect(canvas.width).toBeCloseTo(dimension.width, 1);
expect(canvas.height).toBeCloseTo(dimension.height, 1);
});
it("fit = none, with padding", async () => {
const PADDING = Math.round(Math.random() * 100);
const config: exportUtils.ExportSceneConfig = {
fit: "none",
padding: PADDING,
...dimension,
};
const svgElement = await exportUtils.exportToSvg({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config,
});
const canvas = await exportUtils.exportToCanvas({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config,
});
expect(svgElement.getAttribute("width")).toBeCloseTo(canvas.width);
expect(svgElement.getAttribute("height")).toBeCloseTo(canvas.height);
expect(canvas.width).toBeCloseTo(dimension.width + PADDING * 2, 1);
expect(canvas.height).toBeCloseTo(dimension.height + PADDING * 2, 1);
});
it("fit = contain, with padding", async () => {
const PADDING = Math.round(Math.random() * 100);
const config: exportUtils.ExportSceneConfig = {
fit: "contain",
padding: PADDING,
...dimension,
};
const svgElement = await exportUtils.exportToSvg({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config,
});
const canvas = await exportUtils.exportToCanvas({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config,
});
expect(svgElement.getAttribute("width")).toBeCloseTo(canvas.width);
expect(svgElement.getAttribute("height")).toBeCloseTo(canvas.height);
expect(isCloseTo(canvas.width, dimension.width, 1)).toBe(true);
expect(isCloseTo(canvas.height, dimension.height, 1)).toBe(true);
});
});
// specified maxWH
describe("exporting with specified maxWidthOrHeight", () => {
const maxWH = 200;
it("fit = none, no padding", async () => {
const config: exportUtils.ExportSceneConfig = {
fit: "none",
maxWidthOrHeight: maxWH,
};
const svgElement = await exportUtils.exportToSvg({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config,
});
const canvas = await exportUtils.exportToCanvas({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config,
});
expect(
isCloseTo(
parseFloat(svgElement.getAttribute("width") ?? ""),
canvas.width,
1,
),
).toBe(true);
expect(
isCloseTo(
parseFloat(svgElement.getAttribute("height") ?? ""),
canvas.height,
1,
),
).toBe(true);
expect(canvas.width).toBeLessThanOrEqual(maxWH);
expect(canvas.height).toBeLessThanOrEqual(maxWH);
});
it("fit = contain, no padding", async () => {
const config: exportUtils.ExportSceneConfig = {
fit: "contain",
maxWidthOrHeight: maxWH,
};
const svgElement = await exportUtils.exportToSvg({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config,
});
const canvas = await exportUtils.exportToCanvas({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config,
});
expect(
isCloseTo(
parseFloat(svgElement.getAttribute("width") ?? ""),
canvas.width,
1,
),
).toBe(true);
expect(
isCloseTo(
parseFloat(svgElement.getAttribute("height") ?? ""),
canvas.height,
1,
),
).toBe(true);
expect(canvas.width).toBeLessThanOrEqual(maxWH);
expect(canvas.height).toBeLessThanOrEqual(maxWH);
});
it("fit = none, with padding", async () => {
const PADDING = Math.round(Math.random() * 100);
const config: exportUtils.ExportSceneConfig = {
fit: "none",
padding: PADDING,
maxWidthOrHeight: maxWH,
};
const svgElement = await exportUtils.exportToSvg({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config,
});
const canvas = await exportUtils.exportToCanvas({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config,
});
expect(
isCloseTo(
parseFloat(svgElement.getAttribute("width") ?? ""),
canvas.width,
1,
),
).toBe(true);
expect(
isCloseTo(
parseFloat(svgElement.getAttribute("height") ?? ""),
canvas.height,
1,
),
).toBe(true);
expect(canvas.width).toBeLessThanOrEqual(maxWH);
expect(canvas.height).toBeLessThanOrEqual(maxWH);
});
it("fit = contain, with padding", async () => {
const PADDING = Math.round(Math.random() * 100);
const config: exportUtils.ExportSceneConfig = {
fit: "contain",
padding: PADDING,
maxWidthOrHeight: maxWH,
};
const svgElement = await exportUtils.exportToSvg({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config,
});
const canvas = await exportUtils.exportToCanvas({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config,
});
expect(
isCloseTo(
parseFloat(svgElement.getAttribute("width") ?? ""),
canvas.width,
1,
),
).toBe(true);
expect(
isCloseTo(
parseFloat(svgElement.getAttribute("height") ?? ""),
canvas.height,
1,
),
).toBe(true);
expect(canvas.width).toBeLessThanOrEqual(maxWH);
expect(canvas.height).toBeLessThanOrEqual(maxWH);
});
});
// specified widthOrHeight
describe("exporting with specified widthOrHeight", () => {
const widthOrHeight = 200;
it("fit = none, no padding", async () => {
const config: exportUtils.ExportSceneConfig = {
fit: "none",
widthOrHeight,
};
const svgElement = await exportUtils.exportToSvg({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config,
});
const canvas = await exportUtils.exportToCanvas({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config,
});
expect(
isCloseTo(
parseFloat(svgElement.getAttribute("width") ?? ""),
canvas.width,
1,
),
).toBe(true);
expect(
isCloseTo(
parseFloat(svgElement.getAttribute("height") ?? ""),
canvas.height,
1,
),
).toBe(true);
expect(
isCloseTo(canvas.width, widthOrHeight, 1) ||
isCloseTo(canvas.height, widthOrHeight, 1),
).toBe(true);
});
it("fit = contain, no padding", async () => {
const config: exportUtils.ExportSceneConfig = {
fit: "contain",
widthOrHeight,
};
const svgElement = await exportUtils.exportToSvg({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config,
});
const canvas = await exportUtils.exportToCanvas({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config,
});
expect(
isCloseTo(
parseFloat(svgElement.getAttribute("width") ?? ""),
canvas.width,
1,
),
).toBe(true);
expect(
isCloseTo(
parseFloat(svgElement.getAttribute("height") ?? ""),
canvas.height,
1,
),
).toBe(true);
expect(
isCloseTo(canvas.width, widthOrHeight, 1) ||
isCloseTo(canvas.height, widthOrHeight, 1),
).toBe(true);
});
it("fit = none, with padding", async () => {
const PADDING = Math.round(Math.random() * 100);
const config: exportUtils.ExportSceneConfig = {
fit: "none",
padding: PADDING,
widthOrHeight,
};
const svgElement = await exportUtils.exportToSvg({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config,
});
const canvas = await exportUtils.exportToCanvas({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config,
});
expect(
isCloseTo(
parseFloat(svgElement.getAttribute("width") ?? ""),
canvas.width,
1,
),
).toBe(true);
expect(
isCloseTo(
parseFloat(svgElement.getAttribute("height") ?? ""),
canvas.height,
1,
),
).toBe(true);
expect(
isCloseTo(canvas.width, widthOrHeight, 1) ||
isCloseTo(canvas.height, widthOrHeight, 1),
).toBe(true);
});
it("fit = contain, with padding", async () => {
const PADDING = Math.round(Math.random() * 100);
const config: exportUtils.ExportSceneConfig = {
fit: "contain",
padding: PADDING,
widthOrHeight,
};
const svgElement = await exportUtils.exportToSvg({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config,
});
const canvas = await exportUtils.exportToCanvas({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config,
});
expect(
isCloseTo(
parseFloat(svgElement.getAttribute("width") ?? ""),
canvas.width,
1,
),
).toBe(true);
expect(
isCloseTo(
parseFloat(svgElement.getAttribute("height") ?? ""),
canvas.height,
1,
),
).toBe(true);
expect(
isCloseTo(canvas.width, widthOrHeight, 1) ||
isCloseTo(canvas.height, widthOrHeight, 1),
).toBe(true);
});
});
// specified position
describe("exporting with specified position", () => {
const [, , canvasWidth, canvasHeight] = exportUtils.getCanvasSize(ELEMENTS);
const position = { x: 100, y: 100 };
it("fit = none, no padding", async () => {
const config: exportUtils.ExportSceneConfig = {
fit: "none",
...position,
};
const svgElement = await exportUtils.exportToSvg({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config,
});
const canvas = await exportUtils.exportToCanvas({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config,
});
expect(
isCloseTo(
parseFloat(svgElement.getAttribute("width") ?? ""),
canvas.width,
1,
),
).toBe(true);
expect(
isCloseTo(
parseFloat(svgElement.getAttribute("height") ?? ""),
canvas.height,
1,
),
).toBe(true);
expect(canvas.width).toBeCloseTo(canvasWidth, 1);
expect(canvas.height).toBeCloseTo(canvasHeight, 1);
});
it("fit = none, with padding", async () => {
const PADDING = Math.round(Math.random() * 100);
const config: exportUtils.ExportSceneConfig = {
fit: "none",
padding: PADDING,
...position,
};
const svgElement = await exportUtils.exportToSvg({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config,
});
const canvas = await exportUtils.exportToCanvas({
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
config,
});
expect(svgElement.getAttribute("width")).toBeCloseTo(canvas.width);
expect(svgElement.getAttribute("height")).toBeCloseTo(canvas.height);
expect(canvas.width).toBeCloseTo(canvasWidth + PADDING * 2);
expect(canvas.height).toBeCloseTo(canvasHeight + PADDING * 2);
});
});
});