diff --git a/src/components/ImageExportDialog.tsx b/src/components/ImageExportDialog.tsx index c109162e4..181fa5afc 100644 --- a/src/components/ImageExportDialog.tsx +++ b/src/components/ImageExportDialog.tsx @@ -186,7 +186,11 @@ const ImageExportModal = ({ appState, files, exportPadding: DEFAULT_EXPORT_PADDING, - maxWidthOrHeight, + maxWidthOrHeight: !( + exportBackgroundImage !== "solid" && exportWithBackground + ) + ? maxWidthOrHeight + : undefined, }) .then((canvas) => { setRenderError(null); diff --git a/src/scene/export.ts b/src/scene/export.ts index 76e070084..aeb20b479 100644 --- a/src/scene/export.ts +++ b/src/scene/export.ts @@ -2,8 +2,12 @@ import rough from "roughjs/bin/rough"; 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 { + distance, + expandToAspectRatio, + isOnlyExportingSingleFrame, +} from "../utils"; +import { AppState, BinaryFiles, Dimensions } from "../types"; import { DEFAULT_EXPORT_PADDING, FANCY_BACKGROUND_IMAGES, @@ -58,7 +62,11 @@ export const exportToCanvas = async ( ? exportPadding : getFancyBackgroundPadding(exportPadding); - const [minX, minY, width, height] = getCanvasSize(elements, padding); + const [minX, minY, width, height] = !exportWithFancyBackground + ? getCanvasSize(elements, padding) + : getCanvasSize(elements, padding, { + aspectRatio: { width: 16, height: 9 }, + }); const { canvas, scale = 1 } = createCanvas(width, height); @@ -285,6 +293,7 @@ export const exportToSvg = async ( const getCanvasSize = ( elements: readonly NonDeletedExcalidrawElement[], exportPadding: number, + opts?: { aspectRatio: Dimensions }, ): [number, number, number, number] => { // we should decide if we are exporting the whole canvas // if so, we are not clipping elements in the frame @@ -311,11 +320,20 @@ const getCanvasSize = ( ); } + const padding = onlyExportingSingleFrame ? 0 : exportPadding * 2; + const [minX, minY, maxX, maxY] = getCommonBounds(elements); - const width = - distance(minX, maxX) + (onlyExportingSingleFrame ? 0 : exportPadding * 2); - const height = - distance(minY, maxY) + (onlyExportingSingleFrame ? 0 : exportPadding * 2); + const width = distance(minX, maxX) + padding; + const height = distance(minY, maxY) + padding; + + if (opts?.aspectRatio) { + const expandedDimensions = expandToAspectRatio( + { width, height }, + opts.aspectRatio, + ); + + return [minX, minY, expandedDimensions.width, expandedDimensions.height]; + } return [minX, minY, width, height]; }; diff --git a/src/utils.ts b/src/utils.ts index 226725f49..f111a9414 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -16,7 +16,7 @@ import { FontString, NonDeletedExcalidrawElement, } from "./element/types"; -import { AppState, DataURL, LastActiveTool, Zoom } from "./types"; +import { AppState, DataURL, Dimensions, LastActiveTool, Zoom } from "./types"; import { unstable_batchedUpdates } from "react-dom"; import { SHAPES } from "./shapes"; import { isEraserActive, isHandToolActive } from "./appState"; @@ -1007,3 +1007,48 @@ export const isRenderThrottlingEnabled = (() => { export const defaultExportScale = EXPORT_SCALES.includes(devicePixelRatio) ? devicePixelRatio : 1; + +/** + * Expands dimensions to fit into a specified aspect ratio without cropping. + * The resulting dimensions are rounded up to the nearest integer. + * + * @param dimensions - The original dimensions. + * @param aspectRatio - The aspect ratio to fit the dimensions into. + * + * @return The expanded dimensions. + * + * @example + * ```typescript + * const originalDimensions = { width: 800, height: 600 }; + * const targetAspectRatio = { width: 16, height: 9 }; + * const expandedDimensions = expandToAspectRatio(originalDimensions, targetAspectRatio); + * // Output will be { width: 1067, height: 600 } + * ``` + */ +export const expandToAspectRatio = ( + dimensions: Dimensions, + aspectRatio: Dimensions, +): Dimensions => { + const originalWidth = dimensions.width; + const originalHeight = dimensions.height; + + const originalAspectRatio = originalWidth / originalHeight; + const targetAspectRatio = aspectRatio.width / aspectRatio.height; + + let newWidth = Math.round(originalWidth); + let newHeight = Math.round(originalHeight); + + // Expand by width + if (originalAspectRatio > targetAspectRatio) { + newWidth = Math.round(originalHeight * targetAspectRatio); + } + // Expand by height + else { + newHeight = Math.round(originalWidth / targetAspectRatio); + } + + return { + width: newWidth, + height: newHeight, + }; +};