✨(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 <c.gromoff@gmail.com>
This commit is contained in:
@@ -23,6 +23,7 @@ and this project adheres to
|
|||||||
- 🐛(frontend) fix pdf embed to use full width #1526
|
- 🐛(frontend) fix pdf embed to use full width #1526
|
||||||
- 🐛(frontend) fix fallback translations with Trans #1620
|
- 🐛(frontend) fix fallback translations with Trans #1620
|
||||||
- 🐛(pdf) fix table cell alignment issue in exported documents #1582
|
- 🐛(pdf) fix table cell alignment issue in exported documents #1582
|
||||||
|
- 🐛(pdf) preserve image aspect ratio in PDF export #1622
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
|
|||||||
@@ -31,15 +31,10 @@ export const blockMappingImageDocx: DocsExporterDocx['mappings']['blockMapping']
|
|||||||
const svgText = await blob.text();
|
const svgText = await blob.text();
|
||||||
const FALLBACK_SIZE = 536;
|
const FALLBACK_SIZE = 536;
|
||||||
previewWidth = previewWidth || blob.size || FALLBACK_SIZE;
|
previewWidth = previewWidth || blob.size || FALLBACK_SIZE;
|
||||||
pngConverted = await convertSvgToPng(svgText, previewWidth);
|
|
||||||
const img = new Image();
|
const result = await convertSvgToPng(svgText, previewWidth);
|
||||||
img.src = pngConverted;
|
pngConverted = result.png;
|
||||||
await new Promise((resolve) => {
|
dimensions = { width: result.width, height: result.height };
|
||||||
img.onload = () => {
|
|
||||||
dimensions = { width: img.width, height: img.height };
|
|
||||||
resolve(null);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
dimensions = await getImageDimensions(blob);
|
dimensions = await getImageDimensions(blob);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,15 +28,10 @@ export const blockMappingImageODT: DocsExporterODT['mappings']['blockMapping']['
|
|||||||
const svgText = await blob.text();
|
const svgText = await blob.text();
|
||||||
const FALLBACK_SIZE = 536;
|
const FALLBACK_SIZE = 536;
|
||||||
previewWidth = previewWidth || blob.size || FALLBACK_SIZE;
|
previewWidth = previewWidth || blob.size || FALLBACK_SIZE;
|
||||||
pngConverted = await convertSvgToPng(svgText, previewWidth);
|
|
||||||
const img = new Image();
|
const result = await convertSvgToPng(svgText, previewWidth);
|
||||||
img.src = pngConverted;
|
pngConverted = result.png;
|
||||||
await new Promise((resolve) => {
|
dimensions = { width: result.width, height: result.height };
|
||||||
img.onload = () => {
|
|
||||||
dimensions = { width: img.width, height: img.height };
|
|
||||||
resolve(null);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
dimensions = await getImageDimensions(blob);
|
dimensions = await getImageDimensions(blob);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import { convertSvgToPng } from '../utils';
|
|||||||
|
|
||||||
const PIXELS_PER_POINT = 0.75;
|
const PIXELS_PER_POINT = 0.75;
|
||||||
const FONT_SIZE = 16;
|
const FONT_SIZE = 16;
|
||||||
|
const MAX_WIDTH = 600;
|
||||||
|
|
||||||
export const blockMappingImagePDF: DocsExporterPDF['mappings']['blockMapping']['image'] =
|
export const blockMappingImagePDF: DocsExporterPDF['mappings']['blockMapping']['image'] =
|
||||||
async (block, exporter) => {
|
async (block, exporter) => {
|
||||||
const blob = await exporter.resolveFile(block.props.url);
|
const blob = await exporter.resolveFile(block.props.url);
|
||||||
let pngConverted: string | undefined;
|
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')) {
|
if (!blob.type.includes('image')) {
|
||||||
return <View wrap={false} />;
|
return <View wrap={false} />;
|
||||||
@@ -20,16 +22,33 @@ export const blockMappingImagePDF: DocsExporterPDF['mappings']['blockMapping']['
|
|||||||
if (blob.type.includes('svg')) {
|
if (blob.type.includes('svg')) {
|
||||||
const svgText = await blob.text();
|
const svgText = await blob.text();
|
||||||
const FALLBACK_SIZE = 536;
|
const FALLBACK_SIZE = 536;
|
||||||
width = width || blob.size || FALLBACK_SIZE;
|
previewWidth = previewWidth || FALLBACK_SIZE;
|
||||||
pngConverted = await convertSvgToPng(svgText, width);
|
|
||||||
|
const result = await convertSvgToPng(svgText, previewWidth);
|
||||||
|
pngConverted = result.png;
|
||||||
|
dimensions = { width: result.width, height: result.height };
|
||||||
|
} else {
|
||||||
|
dimensions = await getImageDimensions(blob);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!dimensions) {
|
||||||
|
return <View wrap={false} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<View wrap={false}>
|
<View wrap={false}>
|
||||||
<Image
|
<Image
|
||||||
src={pngConverted || blob}
|
src={pngConverted || blob}
|
||||||
style={{
|
style={{
|
||||||
width: width ? width * PIXELS_PER_POINT : undefined,
|
width: finalWidth * PIXELS_PER_POINT,
|
||||||
|
height: finalHeight * PIXELS_PER_POINT,
|
||||||
maxWidth: '100%',
|
maxWidth: '100%',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -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(
|
function caption(
|
||||||
props: Partial<DefaultProps & { caption: string; previewWidth: number }>,
|
props: Partial<DefaultProps & { caption: string; previewWidth: number }>,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -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
|
* 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.
|
* to preserve the aspect ratio, and renders the SVG onto the canvas using Canvg.
|
||||||
*
|
*
|
||||||
* @param {string} svgText - The raw SVG markup to convert.
|
* @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).
|
* @param {number} width - The desired width of the output PNG (height is auto-calculated to preserve aspect ratio).
|
||||||
* @returns {Promise<string>} 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.
|
* @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
|
// Create a canvas and render the SVG onto it
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
const ctx = canvas.getContext('2d', {
|
const ctx = canvas.getContext('2d', {
|
||||||
@@ -64,7 +67,11 @@ export async function convertSvgToPng(svgText: string, width: number) {
|
|||||||
svg.resize(width, height, true);
|
svg.resize(width, height, true);
|
||||||
await svg.render();
|
await svg.render();
|
||||||
|
|
||||||
return canvas.toDataURL('image/png');
|
return {
|
||||||
|
png: canvas.toDataURL('image/png'),
|
||||||
|
width,
|
||||||
|
height: height || width,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function docxBlockPropsToStyles(
|
export function docxBlockPropsToStyles(
|
||||||
|
|||||||
Reference in New Issue
Block a user