🐛(export) keep image aspect ratio in PDF columns

Ensure images in PDF columns keep their original aspect ratio
This commit is contained in:
Ovgodd
2026-09-17 08:51:01 +02:00
parent 63dfc9b126
commit 10e8b03f14
3 changed files with 40 additions and 16 deletions
+5 -1
View File
@@ -10,6 +10,10 @@ and this project adheres to
- ✨(helm) allow disallowing search engine indexing per instance #2694
### Fixed
- 🐛(export) keep image aspect ratio in PDF columns #2670
## [v5.7.0] - 2026-09-15
### Added
@@ -23,7 +27,7 @@ and this project adheres to
- ⬆️(backend) upgrade celery to version 5.6.3 #2658
- ⚡️(backend) stop using LEFT(value, LENGTH(path)) in sql queries #2668
- 🚚(project) switch docspec image to ghcr.io/docspec/api #2553
- 🚚(global) move favorite documents API endpoint
- 🚚(global) move favorite documents API endpoint
to `/documents/favorites/` #2540
### Fixed
@@ -57,7 +57,8 @@ function makeExporter(blob: Blob) {
type PDFElementProps = {
children?: React.ReactNode;
src?: string;
style?: { width?: number; height?: number };
wrap?: boolean;
style?: React.CSSProperties;
};
// Walk the React element tree to find the first node of a given type.
@@ -147,7 +148,7 @@ describe('blockMappingImagePDF', () => {
it('clamps previewWidth to MAX_WIDTH (600) before conversion and rendering', async () => {
// previewWidth=800 is clamped to 600 before being passed to convertBlobToPng.
// The converter returns 600×300 (already at the clamped size).
// Rendered: width = 600*PIXELS_PER_POINT(0.75) = 450, height = 300*PIXELS_PER_POINT(0.75) = 225.
// Rendered: width = 600*PIXELS_PER_POINT(0.75) = 450.
vi.mocked(convertBlobToPng).mockResolvedValue({
png: CANVAS_PNG_URL,
width: 600,
@@ -164,7 +165,6 @@ describe('blockMappingImagePDF', () => {
const imageEl = findInTree(result as React.ReactNode, 'pdfImage');
expect(imageEl).toBeDefined();
expect(imageEl?.props.style?.width).toBe(450);
expect(imageEl?.props.style?.height).toBe(225);
});
it('passes previewWidth to convertBlobToPng so it can resize during transcoding', async () => {
@@ -202,8 +202,8 @@ describe('blockMappingImagePDF', () => {
});
it('uses natural image dimensions for the rendered style when no previewWidth is set', async () => {
// naturalWidth=300, naturalHeight=150 → finalWidth=300, finalHeight=150.
// Rendered: width = 300*PIXELS_PER_POINT(0.75) = 225, height = 150*PIXELS_PER_POINT(0.75) = 112.5.
// naturalWidth=300, naturalHeight=150 → finalWidth=300.
// Rendered: width = 300*PIXELS_PER_POINT(0.75) = 225.
vi.mocked(convertBlobToPng).mockResolvedValue({
png: CANVAS_PNG_URL,
width: 300,
@@ -219,13 +219,16 @@ describe('blockMappingImagePDF', () => {
const imageEl = findInTree(result as React.ReactNode, 'pdfImage');
expect(imageEl).toBeDefined();
expect(imageEl?.props.style?.width).toBe(225);
expect(imageEl?.props.style?.height).toBe(112.5);
expect(imageEl?.props.style?.maxWidth).toBe('100%');
const viewEl = result as React.ReactElement<PDFElementProps>;
expect(viewEl.props.style?.alignSelf).toBe('flex-start');
expect(viewEl.props.style?.maxWidth).toBe('100%');
});
it('scales rendered style to previewWidth when it is within MAX_WIDTH', async () => {
// previewWidth=400 (< MAX_WIDTH=600), natural size 300×150.
// finalWidth=400, finalHeight=(400/300)*150≈200.
// Rendered: width = 400*PIXELS_PER_POINT(0.75) = 300, height ≈ 200*PIXELS_PER_POINT(0.75) = 150.
// Rendered: width = 400*PIXELS_PER_POINT(0.75) = 300.
vi.mocked(convertBlobToPng).mockResolvedValue({
png: CANVAS_PNG_URL,
width: 300,
@@ -241,7 +244,24 @@ describe('blockMappingImagePDF', () => {
const imageEl = findInTree(result as React.ReactNode, 'pdfImage');
expect(imageEl).toBeDefined();
expect(imageEl?.props.style?.width).toBe(300);
expect(imageEl?.props.style?.height).toBe(150);
});
it('leaves the height unset so a column-shrunk image keeps its ratio', async () => {
vi.mocked(convertBlobToPng).mockResolvedValue({
png: CANVAS_PNG_URL,
width: 400,
height: 640,
});
const result = await blockMappingImagePDF(
makeBlock({ previewWidth: 470 }),
makeExporter(new Blob(['fake-png'], { type: 'image/png' })),
0,
);
const imageEl = findInTree(result as React.ReactNode, 'pdfImage');
expect(imageEl?.props.style?.height).toBeUndefined();
expect(imageEl?.props.style?.aspectRatio).toBeUndefined();
});
it('returns an empty View when convertBlobToPng returns undefined', async () => {
@@ -7,6 +7,10 @@ import { convertBlobToPng, convertSvgToPng } from '../utils';
const PIXELS_PER_POINT = 0.75;
const FONT_SIZE = 16;
const MAX_WIDTH = 600;
const imageBoxStyle = {
alignSelf: 'flex-start' as const,
maxWidth: '100%' as const,
};
/**
* Renders an image block as a PDF element.
@@ -48,20 +52,16 @@ export const blockMappingImagePDF: DocsExporterPDF['mappings']['blockMapping']['
return <View wrap={false} />;
}
const { width, height } = result;
// 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;
const finalWidth = Math.min(previewWidth || result.width, MAX_WIDTH);
return (
<View wrap={false}>
<View wrap={false} style={imageBoxStyle}>
<Image
src={result.png}
style={{
width: finalWidth * PIXELS_PER_POINT,
height: finalHeight * PIXELS_PER_POINT,
maxWidth: '100%',
}}
/>