mirror of
https://github.com/suitenumerique/docs.git
synced 2026-09-22 09:35:08 +02:00
🐛(export) keep image aspect ratio in PDF columns
Ensure images in PDF columns keep their original aspect ratio
This commit is contained in:
+5
-1
@@ -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%',
|
||||
}}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user