Skip to content

Commit 2e64298

Browse files
committed
✨(pdf) preserve image aspect ratio in PDF export
images were distorted in PDF exports; height is now computed to fix that Signed-off-by: Cyril <[email protected]>
1 parent 8dad9ea commit 2e64298

File tree

5 files changed

+58
-26
lines changed

5 files changed

+58
-26
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ and this project adheres to
2323
- 🐛(frontend) fix pdf embed to use full width #1526
2424
- 🐛(frontend) fix fallback translations with Trans #1620
2525
- 🐛(pdf) fix table cell alignment issue in exported documents #1582
26+
- 🐛(pdf) preserve image aspect ratio in PDF export #1622
2627

2728
### Security
2829

src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/imageDocx.tsx

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,10 @@ export const blockMappingImageDocx: DocsExporterDocx['mappings']['blockMapping']
3131
const svgText = await blob.text();
3232
const FALLBACK_SIZE = 536;
3333
previewWidth = previewWidth || blob.size || FALLBACK_SIZE;
34-
pngConverted = await convertSvgToPng(svgText, previewWidth);
35-
const img = new Image();
36-
img.src = pngConverted;
37-
await new Promise((resolve) => {
38-
img.onload = () => {
39-
dimensions = { width: img.width, height: img.height };
40-
resolve(null);
41-
};
42-
});
34+
35+
const result = await convertSvgToPng(svgText, previewWidth);
36+
pngConverted = result.png;
37+
dimensions = { width: result.width, height: result.height };
4338
} else {
4439
dimensions = await getImageDimensions(blob);
4540
}

src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/imageODT.tsx

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,10 @@ export const blockMappingImageODT: DocsExporterODT['mappings']['blockMapping']['
2828
const svgText = await blob.text();
2929
const FALLBACK_SIZE = 536;
3030
previewWidth = previewWidth || blob.size || FALLBACK_SIZE;
31-
pngConverted = await convertSvgToPng(svgText, previewWidth);
32-
const img = new Image();
33-
img.src = pngConverted;
34-
await new Promise((resolve) => {
35-
img.onload = () => {
36-
dimensions = { width: img.width, height: img.height };
37-
resolve(null);
38-
};
39-
});
31+
32+
const result = await convertSvgToPng(svgText, previewWidth);
33+
pngConverted = result.png;
34+
dimensions = { width: result.width, height: result.height };
4035
} else {
4136
dimensions = await getImageDimensions(blob);
4237
}

src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/imagePDF.tsx

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import { convertSvgToPng } from '../utils';
66

77
const PIXELS_PER_POINT = 0.75;
88
const FONT_SIZE = 16;
9+
const MAX_WIDTH = 600;
910

1011
export const blockMappingImagePDF: DocsExporterPDF['mappings']['blockMapping']['image'] =
1112
async (block, exporter) => {
1213
const blob = await exporter.resolveFile(block.props.url);
1314
let pngConverted: string | undefined;
14-
let width = block.props.previewWidth || undefined;
15+
let dimensions: { width: number; height: number } | undefined;
16+
let previewWidth = block.props.previewWidth || undefined;
1517

1618
if (!blob.type.includes('image')) {
1719
return <View wrap={false} />;
@@ -20,16 +22,33 @@ export const blockMappingImagePDF: DocsExporterPDF['mappings']['blockMapping']['
2022
if (blob.type.includes('svg')) {
2123
const svgText = await blob.text();
2224
const FALLBACK_SIZE = 536;
23-
width = width || blob.size || FALLBACK_SIZE;
24-
pngConverted = await convertSvgToPng(svgText, width);
25+
previewWidth = previewWidth || FALLBACK_SIZE;
26+
27+
const result = await convertSvgToPng(svgText, previewWidth);
28+
pngConverted = result.png;
29+
dimensions = { width: result.width, height: result.height };
30+
} else {
31+
dimensions = await getImageDimensions(blob);
2532
}
2633

34+
if (!dimensions) {
35+
return <View wrap={false} />;
36+
}
37+
38+
const { width, height } = dimensions;
39+
40+
// Ensure the final width never exceeds MAX_WIDTH to prevent images
41+
// from overflowing the page width in the exported document
42+
const finalWidth = Math.min(previewWidth || width, MAX_WIDTH);
43+
const finalHeight = (finalWidth / width) * height;
44+
2745
return (
2846
<View wrap={false}>
2947
<Image
3048
src={pngConverted || blob}
3149
style={{
32-
width: width ? width * PIXELS_PER_POINT : undefined,
50+
width: finalWidth * PIXELS_PER_POINT,
51+
height: finalHeight * PIXELS_PER_POINT,
3352
maxWidth: '100%',
3453
}}
3554
/>
@@ -38,6 +57,21 @@ export const blockMappingImagePDF: DocsExporterPDF['mappings']['blockMapping']['
3857
);
3958
};
4059

60+
async function getImageDimensions(blob: Blob) {
61+
if (typeof window !== 'undefined') {
62+
const url = URL.createObjectURL(blob);
63+
const img = document.createElement('img');
64+
img.src = url;
65+
66+
return new Promise<{ width: number; height: number }>((resolve) => {
67+
img.onload = () => {
68+
URL.revokeObjectURL(url);
69+
resolve({ width: img.naturalWidth, height: img.naturalHeight });
70+
};
71+
});
72+
}
73+
}
74+
4175
function caption(
4276
props: Partial<DefaultProps & { caption: string; previewWidth: number }>,
4377
) {

src/frontend/apps/impress/src/features/docs/doc-export/utils.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,21 @@ export function downloadFile(blob: Blob, filename: string) {
2020
}
2121

2222
/**
23-
* Converts an SVG string into a PNG image and returns it as a data URL.
23+
* Converts an SVG string into a PNG image and returns it as a data URL with dimensions.
2424
*
2525
* This function creates a canvas, parses the SVG, calculates the appropriate height
2626
* to preserve the aspect ratio, and renders the SVG onto the canvas using Canvg.
2727
*
2828
* @param {string} svgText - The raw SVG markup to convert.
2929
* @param {number} width - The desired width of the output PNG (height is auto-calculated to preserve aspect ratio).
30-
* @returns {Promise<string>} A Promise that resolves to a PNG image encoded as a base64 data URL.
30+
* @returns {Promise<{ png: string; width: number; height: number }>} A Promise that resolves to an object containing the PNG data URL and its dimensions.
3131
*
3232
* @throws Will throw an error if the canvas context cannot be initialized.
3333
*/
34-
export async function convertSvgToPng(svgText: string, width: number) {
34+
export async function convertSvgToPng(
35+
svgText: string,
36+
width: number,
37+
): Promise<{ png: string; width: number; height: number }> {
3538
// Create a canvas and render the SVG onto it
3639
const canvas = document.createElement('canvas');
3740
const ctx = canvas.getContext('2d', {
@@ -64,7 +67,11 @@ export async function convertSvgToPng(svgText: string, width: number) {
6467
svg.resize(width, height, true);
6568
await svg.render();
6669

67-
return canvas.toDataURL('image/png');
70+
return {
71+
png: canvas.toDataURL('image/png'),
72+
width,
73+
height: height || width,
74+
};
6875
}
6976

7077
export function docxBlockPropsToStyles(

0 commit comments

Comments
 (0)