From 9d6583d9de4218c4b453b84243ec2534b6a1ba81 Mon Sep 17 00:00:00 2001 From: Patrik Date: Wed, 12 Mar 2025 09:48:05 -0400 Subject: [PATCH] fix: incorrect height rounding when resizing images with sharp (#11634) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR fixes an issue where the Sharp `.resize()` function would round down an auto-scaled dimension when `fastShrinkOnLoad` was enabled (enabled by default). This caused slight discrepancies in height calculations in certain edge cases. Be default (`fastShrinkOnLoad: true`), Sharp: - Uses the built-in shrink-on-load feature for JPEG and WebP - It is an optimization that prioritizes speed over precision when resizing images By setting `fastShrinkOnLoad: false`, we force Sharp to: - Perform a more accurate resize operation instead of relying on quick pre-shrink methods. ### Before / Context: - Upload an image with original dimensions of 1500 × 735 - Define an `imageSize` of the following: ``` { name: 'thumbnail', width: 300, }, ``` #### Calculation: `originalAspectRatio = 1500 / 735 ≈ 2.04081632653` `resizeHeight = 300 / 2.04081632653` `resizeHeight = 147` However, Sharp's `.resize()` calculation would output: `resizeHeight = 146` This lead to an error of: ``` [17:05:13] ERROR: extract_area: bad extract area err: { "type": "Error", "message": "extract_area: bad extract area", "stack": Error: extract_area: bad extract area } ``` ### After: Sharp's `.resize()` calculation now correctly outputs: `resizeHeight = 147` --- packages/payload/src/uploads/imageResizer.ts | 1 + test/uploads/config.ts | 4 ++++ test/uploads/e2e.spec.ts | 14 ++++++++++++++ test/uploads/payload-types.ts | 19 +++++++++++++++++++ test/uploads/test-image-1500x735.jpeg | Bin 0 -> 26182 bytes 5 files changed, 38 insertions(+) create mode 100644 test/uploads/test-image-1500x735.jpeg diff --git a/packages/payload/src/uploads/imageResizer.ts b/packages/payload/src/uploads/imageResizer.ts index e5d08180c..82560adb5 100644 --- a/packages/payload/src/uploads/imageResizer.ts +++ b/packages/payload/src/uploads/imageResizer.ts @@ -360,6 +360,7 @@ export async function resizeAndTransformImageSizes({ const prioritizeHeight = resizeAspectRatio < originalAspectRatio // Scales the image before extracting from it resized = imageToResize.resize({ + fastShrinkOnLoad: false, height: prioritizeHeight ? resizeHeight : undefined, width: prioritizeHeight ? undefined : resizeWidth, }) diff --git a/test/uploads/config.ts b/test/uploads/config.ts index 8e25e7545..183591a7a 100644 --- a/test/uploads/config.ts +++ b/test/uploads/config.ts @@ -390,6 +390,10 @@ export default buildConfigWithDefaults({ height: 300, width: 300, }, + { + name: 'undefinedHeight', + width: 300, + }, ], pasteURL: false, }, diff --git a/test/uploads/e2e.spec.ts b/test/uploads/e2e.spec.ts index 399212e14..2b3867623 100644 --- a/test/uploads/e2e.spec.ts +++ b/test/uploads/e2e.spec.ts @@ -400,6 +400,20 @@ describe('Uploads', () => { await expect(page.locator('.row-3 .cell-title')).toContainText('draft') }) + test('should upload edge case media when an image size contains an undefined height', async () => { + await page.goto(mediaURL.create) + await page.setInputFiles( + 'input[type="file"]', + path.resolve(dirname, './test-image-1500x735.jpeg'), + ) + + const filename = page.locator('.file-field__filename') + + await expect(filename).toHaveValue('test-image-1500x735.jpeg') + + await saveDocAndAssert(page) + }) + describe('filterOptions', () => { test('should restrict mimetype based on filterOptions', async () => { const audioDoc = ( diff --git a/test/uploads/payload-types.ts b/test/uploads/payload-types.ts index 1956fbcdb..5196cd9bf 100644 --- a/test/uploads/payload-types.ts +++ b/test/uploads/payload-types.ts @@ -64,6 +64,7 @@ export interface Config { auth: { users: UserAuthOperations; }; + blocks: {}; collections: { relation: Relation; audio: Audio; @@ -327,6 +328,14 @@ export interface Media { filesize?: number | null; filename?: string | null; }; + undefinedHeight?: { + url?: string | null; + width?: number | null; + height?: number | null; + mimeType?: string | null; + filesize?: number | null; + filename?: string | null; + }; }; } /** @@ -2018,6 +2027,16 @@ export interface MediaSelect { filesize?: T; filename?: T; }; + undefinedHeight?: + | T + | { + url?: T; + width?: T; + height?: T; + mimeType?: T; + filesize?: T; + filename?: T; + }; }; } /** diff --git a/test/uploads/test-image-1500x735.jpeg b/test/uploads/test-image-1500x735.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..0859fa8a486d9089f27c6f8a35ca713f4d9df03b GIT binary patch literal 26182 zcmeI4c~lce9LIl~O$fo-3f@`^D%6NpRM2|0sVE@&5D|}}KB%IA#0xL*po^$gX)WFu zUTqLdu~n)_@p@{MTJd}l3E|QTA|NUf1%Z%cr;~^X{iDy~Hej zWP=J&33Qm~>Fo&=1uy|$0F^?FN7TX)03RQ41^{aiQawO~OSlSrPW5alds0%MEzb)8 zwk!l;bDhcf(HyupOPg1ZbQ0BE8~2kWcwJj`H;H~-7Th;CL)9>1O4!nsOIL&~UFP6A zXfTX$_x2H*d&glZJ6VbyG`+qnjE57#su;=sr;YAkql;i`Ex0ebOH+M7U`x@q6uJ!d zcoZVa5|(E8juOy9k(Jn5V$%*+$m{?DilzlZS|l>R4Hb=F2cfOVuJ@3!RvrE3iu*)# z8oFWULF>Nm>6bcB&Cxl!&RZTUvFXyaTlXGLANP~?cXk^#{L>L5M~xfr;psKOd!qle z=`&`2889n&{(_Luu!Z3(R<2qd8MS8Z#!Z{Q*|K$8+^*ew_U=pAuSiTj^i#^usfU00 z^+d++nI})3K65tfkIR2v$-a8+`kh=&o;JUru&DGw*~9XR%12d?pTDTBds*MmsBbds z!rSweEPVe;**|pIVqF5EkQR!~x+p=US-7oG)O(1P-B>^I+=z~ShHkL#ba8Ymb$V`=W>NNb!ealEvKC=)bQQxm5Lkj1U`jNNNeOX?#3FNut;LqG zZVnquv@=KhmO!`@3ip88uRmf}BJ0sYY-w(m@MEj=I;;g6?1dbs~5qy=0 zpiZOik{maH-KExNaZYhFB^kO+$Z>DJVtO)^L&$NI+#N!WqvUc4IgXOk5ON$Pmq*BP zl$@53<0!d&LXM;4h@a!&@dx7PIDHI-_&KibbHvYal^pSNTqQ^R99PK^KgU&a#Lsb+ z9Px8pB}cxW<0?7g=eSCa_&KhUBYuvni^@wa@Lt2q^^s)urR_dvD&a(VPN>0zU$!MOdS@O7-`w}Z(O zWP7~B(;*&djdpC2mZ@Z)un1(0Ii}2`2-K#06U#h5dVPS)TwndnyKC)>3TpHw7o!A$ zBDHmuLk4*O0;S=Y!HA$NV2|I^{s^268x6C2;qLGcF*nF+-4LA2RO%6&MW9l%Zp>o2 ztQNs41dP%s9~$@BW?6aAGDaS(s;pD#`mrI(dTj=7-4N7(8(oR7P77t3XQ~?9W_yUA z*YQ;2k!K%NFN!p>39Jyo&D?(xAg%9&U~lv#Q`+}!vdr}RwM{{d9TB+twh`&oW^@_SLk0fq^agSL$up@h|DGwLd)#MgLB)>@%`o59^oj<*mX0N%_p0)XrRt+EtJK_mr{ zHQ-Hpd?yy+z;vC9pXz+$nNsFTy&dAyqEgo4rQSt9@JGCU zACE=w=##*}HIoP5i0BxbF=e;UlJna2HQho^=kF-5;bxPBwuJ|)a_zRnt2x5X>1dPF_+s+jsxIOFK4+vO~D5cI9fp@4nBGR}u zUG?&DD!W5ns9>}zL$+SC%2D+s$W&&0Jc^ahwl6O#z}q62i)Z8FGjRz_{=m#3F!=-X z4uQ!Zn7ITde_(0|O#Z;kBQW^`Q%hj-2PW~E#Ap5=4liK! zlxB&~