fix: image cropping svg + compat mode (#8710)

Co-authored-by: Ryan Di <ryan.weihao.di@gmail.com>
pull/8725/head
David Luzar 3 months ago committed by GitHub
parent 96ed8a4331
commit f9815b8b4f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -7,7 +7,7 @@ import {
SVG_NS,
} from "../constants";
import { normalizeLink, toValidURL } from "../data/url";
import { getElementAbsoluteCoords } from "../element";
import { getElementAbsoluteCoords, hashString } from "../element";
import {
createPlaceholderEmbeddableLabel,
getEmbedLink,
@ -411,7 +411,25 @@ const renderElementToSvg = (
const fileData =
isInitializedImageElement(element) && files[element.fileId];
if (fileData) {
const symbolId = `image-${fileData.id}`;
const { reuseImages = true } = renderConfig;
let symbolId = `image-${fileData.id}`;
let uncroppedWidth = element.width;
let uncroppedHeight = element.height;
if (element.crop) {
({ width: uncroppedWidth, height: uncroppedHeight } =
getUncroppedWidthAndHeight(element));
symbolId = `image-crop-${fileData.id}-${hashString(
`${uncroppedWidth}x${uncroppedHeight}`,
)}`;
}
if (!reuseImages) {
symbolId = `image-${element.id}`;
}
let symbol = svgRoot.querySelector(`#${symbolId}`);
if (!symbol) {
symbol = svgRoot.ownerDocument!.createElementNS(SVG_NS, "symbol");
@ -421,18 +439,7 @@ const renderElementToSvg = (
image.setAttribute("href", fileData.dataURL);
image.setAttribute("preserveAspectRatio", "none");
if (element.crop) {
const { width: uncroppedWidth, height: uncroppedHeight } =
getUncroppedWidthAndHeight(element);
symbol.setAttribute(
"viewBox",
`${
element.crop.x / (element.crop.naturalWidth / uncroppedWidth)
} ${
element.crop.y / (element.crop.naturalHeight / uncroppedHeight)
} ${width} ${height}`,
);
if (element.crop || !reuseImages) {
image.setAttribute("width", `${uncroppedWidth}`);
image.setAttribute("height", `${uncroppedHeight}`);
} else {
@ -456,8 +463,23 @@ const renderElementToSvg = (
use.setAttribute("filter", IMAGE_INVERT_FILTER);
}
use.setAttribute("width", `${width}`);
use.setAttribute("height", `${height}`);
let normalizedCropX = 0;
let normalizedCropY = 0;
if (element.crop) {
const { width: uncroppedWidth, height: uncroppedHeight } =
getUncroppedWidthAndHeight(element);
normalizedCropX =
element.crop.x / (element.crop.naturalWidth / uncroppedWidth);
normalizedCropY =
element.crop.y / (element.crop.naturalHeight / uncroppedHeight);
}
const adjustedCenterX = cx + normalizedCropX;
const adjustedCenterY = cy + normalizedCropY;
use.setAttribute("width", `${width + normalizedCropX}`);
use.setAttribute("height", `${height + normalizedCropY}`);
use.setAttribute("opacity", `${opacity}`);
// We first apply `scale` transforms (horizontal/vertical mirroring)
@ -467,21 +489,43 @@ const renderElementToSvg = (
// the transformations correctly (the transform-origin was not being
// applied correctly).
if (element.scale[0] !== 1 || element.scale[1] !== 1) {
const translateX = element.scale[0] !== 1 ? -width : 0;
const translateY = element.scale[1] !== 1 ? -height : 0;
use.setAttribute(
"transform",
`scale(${element.scale[0]}, ${element.scale[1]}) translate(${translateX} ${translateY})`,
`translate(${adjustedCenterX} ${adjustedCenterY}) scale(${
element.scale[0]
} ${
element.scale[1]
}) translate(${-adjustedCenterX} ${-adjustedCenterY})`,
);
}
const g = svgRoot.ownerDocument!.createElementNS(SVG_NS, "g");
if (element.crop) {
const mask = svgRoot.ownerDocument!.createElementNS(SVG_NS, "mask");
mask.setAttribute("id", `mask-image-crop-${element.id}`);
mask.setAttribute("fill", "#fff");
const maskRect = svgRoot.ownerDocument!.createElementNS(
SVG_NS,
"rect",
);
maskRect.setAttribute("x", `${normalizedCropX}`);
maskRect.setAttribute("y", `${normalizedCropY}`);
maskRect.setAttribute("width", `${width}`);
maskRect.setAttribute("height", `${height}`);
mask.appendChild(maskRect);
root.appendChild(mask);
g.setAttribute("mask", `url(#${mask.id})`);
}
g.appendChild(use);
g.setAttribute(
"transform",
`translate(${offsetX || 0} ${
offsetY || 0
}) rotate(${degree} ${cx} ${cy})`,
`translate(${offsetX - normalizedCropX} ${
offsetY - normalizedCropY
}) rotate(${degree} ${adjustedCenterX} ${adjustedCenterY})`,
);
if (element.roundness) {

@ -284,6 +284,7 @@ export const exportToSvg = async (
renderEmbeddables?: boolean;
exportingFrame?: ExcalidrawFrameLikeElement | null;
skipInliningFonts?: true;
reuseImages?: boolean;
},
): Promise<SVGSVGElement> => {
const frameRendering = getFrameRenderingConfig(
@ -425,6 +426,7 @@ export const exportToSvg = async (
.map((element) => [element.id, true]),
)
: new Map(),
reuseImages: opts?.reuseImages ?? true,
},
);

@ -46,6 +46,13 @@ export type SVGRenderConfig = {
frameRendering: AppState["frameRendering"];
canvasBackgroundColor: AppState["viewBackgroundColor"];
embedsValidationStatus: EmbedsValidationStatus;
/**
* whether to attempt to reuse images as much as possible through symbols
* (reduces SVG size, but may be incompoatible with some SVG renderers)
*
* @default true
*/
reuseImages: boolean;
};
export type InteractiveCanvasRenderConfig = {

@ -10,5 +10,5 @@ exports[`export > exporting svg containing transformed images > svg export outpu
</style>
</defs>
<clipPath id="image-clipPath-id1" data-id="id1"><rect width="100" height="100" rx="25" ry="25"></rect></clipPath><g transform="translate(30.710678118654755 30.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(130.71067811865476 30.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="scale(-1, 1) translate(-50 0)"></use></g><clipPath id="image-clipPath-id3" data-id="id3"><rect width="100" height="100" rx="25" ry="25"></rect></clipPath><g transform="translate(30.710678118654755 130.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="scale(1, -1) translate(0 -100)"></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(130.71067811865476 130.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="scale(-1, -1) translate(-50 -50)"></use></g></svg>"
<clipPath id="image-clipPath-id1" data-id="id1"><rect width="100" height="100" rx="25" ry="25"></rect></clipPath><g transform="translate(30.710678118654755 30.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(130.71067811865476 30.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(30.710678118654755 130.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(130.71067811865476 130.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>"
`;

@ -167,10 +167,12 @@ export const exportToSvg = async ({
renderEmbeddables,
exportingFrame,
skipInliningFonts,
reuseImages,
}: Omit<ExportOpts, "getDimensions"> & {
exportPadding?: number;
renderEmbeddables?: boolean;
skipInliningFonts?: true;
reuseImages?: boolean;
}): Promise<SVGSVGElement> => {
const { elements: restoredElements, appState: restoredAppState } = restore(
{ elements, appState },
@ -187,6 +189,7 @@ export const exportToSvg = async ({
exportingFrame,
renderEmbeddables,
skipInliningFonts,
reuseImages,
});
};

Loading…
Cancel
Save