diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b8a53e10..3c6fda2c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/__tests__/imagePDF.test.tsx b/src/frontend/apps/impress/src/features/docs/doc-export/__tests__/imagePDF.test.tsx index f4408fc97..5ad9ec977 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-export/__tests__/imagePDF.test.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-export/__tests__/imagePDF.test.tsx @@ -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; + 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 () => { 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 b17d18747..4e4aa5f65 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 @@ -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 ; } - 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 ( - +