diff --git a/CHANGELOG.md b/CHANGELOG.md
index b22ba50734..8ab285789d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,7 @@ and this project adheres to
- 🐛(frontend) fix pdf embed to use full width #1526
- 🐛(frontend) fix fallback translations with Trans #1620
- 🐛(pdf) fix table cell alignment issue in exported documents #1582
+- 🐛(pdf) preserve image aspect ratio in PDF export #1622
### Security
diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/imageDocx.tsx b/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/imageDocx.tsx
index 868ba0cf5d..3eb26bb251 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/imageDocx.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/imageDocx.tsx
@@ -31,15 +31,10 @@ export const blockMappingImageDocx: DocsExporterDocx['mappings']['blockMapping']
const svgText = await blob.text();
const FALLBACK_SIZE = 536;
previewWidth = previewWidth || blob.size || FALLBACK_SIZE;
- pngConverted = await convertSvgToPng(svgText, previewWidth);
- const img = new Image();
- img.src = pngConverted;
- await new Promise((resolve) => {
- img.onload = () => {
- dimensions = { width: img.width, height: img.height };
- resolve(null);
- };
- });
+
+ const result = await convertSvgToPng(svgText, previewWidth);
+ pngConverted = result.png;
+ dimensions = { width: result.width, height: result.height };
} else {
dimensions = await getImageDimensions(blob);
}
diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/imageODT.tsx b/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/imageODT.tsx
index 22f1197216..5e4676fb8a 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/imageODT.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/imageODT.tsx
@@ -28,15 +28,10 @@ export const blockMappingImageODT: DocsExporterODT['mappings']['blockMapping']['
const svgText = await blob.text();
const FALLBACK_SIZE = 536;
previewWidth = previewWidth || blob.size || FALLBACK_SIZE;
- pngConverted = await convertSvgToPng(svgText, previewWidth);
- const img = new Image();
- img.src = pngConverted;
- await new Promise((resolve) => {
- img.onload = () => {
- dimensions = { width: img.width, height: img.height };
- resolve(null);
- };
- });
+
+ const result = await convertSvgToPng(svgText, previewWidth);
+ pngConverted = result.png;
+ dimensions = { width: result.width, height: result.height };
} else {
dimensions = await getImageDimensions(blob);
}
diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/imagePDF.tsx b/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/imagePDF.tsx
index cad4fd69eb..1a3513cbfd 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/imagePDF.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/imagePDF.tsx
@@ -6,12 +6,14 @@ import { convertSvgToPng } from '../utils';
const PIXELS_PER_POINT = 0.75;
const FONT_SIZE = 16;
+const MAX_WIDTH = 600;
export const blockMappingImagePDF: DocsExporterPDF['mappings']['blockMapping']['image'] =
async (block, exporter) => {
const blob = await exporter.resolveFile(block.props.url);
let pngConverted: string | undefined;
- let width = block.props.previewWidth || undefined;
+ let dimensions: { width: number; height: number } | undefined;
+ let previewWidth = block.props.previewWidth || undefined;
if (!blob.type.includes('image')) {
return ;
@@ -20,16 +22,33 @@ export const blockMappingImagePDF: DocsExporterPDF['mappings']['blockMapping']['
if (blob.type.includes('svg')) {
const svgText = await blob.text();
const FALLBACK_SIZE = 536;
- width = width || blob.size || FALLBACK_SIZE;
- pngConverted = await convertSvgToPng(svgText, width);
+ previewWidth = previewWidth || FALLBACK_SIZE;
+
+ const result = await convertSvgToPng(svgText, previewWidth);
+ pngConverted = result.png;
+ dimensions = { width: result.width, height: result.height };
+ } else {
+ dimensions = await getImageDimensions(blob);
}
+ if (!dimensions) {
+ return ;
+ }
+
+ const { width, height } = dimensions;
+
+ // Ensure the final width never exceeds MAX_WIDTH to prevent images
+ // from overflowing the page width in the exported document
+ const finalWidth = Math.min(previewWidth || width, MAX_WIDTH);
+ const finalHeight = (finalWidth / width) * height;
+
return (
@@ -38,6 +57,21 @@ export const blockMappingImagePDF: DocsExporterPDF['mappings']['blockMapping']['
);
};
+async function getImageDimensions(blob: Blob) {
+ if (typeof window !== 'undefined') {
+ const url = URL.createObjectURL(blob);
+ const img = document.createElement('img');
+ img.src = url;
+
+ return new Promise<{ width: number; height: number }>((resolve) => {
+ img.onload = () => {
+ URL.revokeObjectURL(url);
+ resolve({ width: img.naturalWidth, height: img.naturalHeight });
+ };
+ });
+ }
+}
+
function caption(
props: Partial,
) {
diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/utils.ts b/src/frontend/apps/impress/src/features/docs/doc-export/utils.ts
index 01c1739e68..505efbb2a2 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-export/utils.ts
+++ b/src/frontend/apps/impress/src/features/docs/doc-export/utils.ts
@@ -20,18 +20,21 @@ export function downloadFile(blob: Blob, filename: string) {
}
/**
- * Converts an SVG string into a PNG image and returns it as a data URL.
+ * Converts an SVG string into a PNG image and returns it as a data URL with dimensions.
*
* This function creates a canvas, parses the SVG, calculates the appropriate height
* to preserve the aspect ratio, and renders the SVG onto the canvas using Canvg.
*
* @param {string} svgText - The raw SVG markup to convert.
* @param {number} width - The desired width of the output PNG (height is auto-calculated to preserve aspect ratio).
- * @returns {Promise} A Promise that resolves to a PNG image encoded as a base64 data URL.
+ * @returns {Promise<{ png: string; width: number; height: number }>} A Promise that resolves to an object containing the PNG data URL and its dimensions.
*
* @throws Will throw an error if the canvas context cannot be initialized.
*/
-export async function convertSvgToPng(svgText: string, width: number) {
+export async function convertSvgToPng(
+ svgText: string,
+ width: number,
+): Promise<{ png: string; width: number; height: number }> {
// Create a canvas and render the SVG onto it
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', {
@@ -64,7 +67,11 @@ export async function convertSvgToPng(svgText: string, width: number) {
svg.resize(width, height, true);
await svg.render();
- return canvas.toDataURL('image/png');
+ return {
+ png: canvas.toDataURL('image/png'),
+ width,
+ height: height || width,
+ };
}
export function docxBlockPropsToStyles(