@@ -68,7 +68,7 @@ function readPng(buffer: Buffer): PNG {
6868 const { metadata, idatChunks } = collectPngChunks ( buffer ) ;
6969 if ( ! metadata ) throw new Error ( 'PNG is missing IHDR' ) ;
7070 if ( idatChunks . length === 0 ) throw new Error ( 'PNG is missing IDAT' ) ;
71- const inflated = inflateSync ( Buffer . concat ( idatChunks ) ) ;
71+ const inflated = inflatePngData ( idatChunks , metadata ) ;
7272 return new PNG ( {
7373 width : metadata . width ,
7474 height : metadata . height ,
@@ -129,6 +129,20 @@ function* iteratePngChunks(buffer: Buffer): Generator<PngChunk> {
129129 }
130130}
131131
132+ function inflatePngData ( idatChunks : Buffer [ ] , metadata : PngMetadata ) : Buffer {
133+ const expectedLength = inflatedByteLength ( metadata ) ;
134+ try {
135+ const inflated = inflateSync ( Buffer . concat ( idatChunks ) , { maxOutputLength : expectedLength } ) ;
136+ if ( inflated . length !== expectedLength ) throw new Error ( 'PNG pixel data is truncated' ) ;
137+ return inflated ;
138+ } catch ( error ) {
139+ if ( isZlibOutputLimitError ( error ) ) {
140+ throw new Error ( `PNG pixel data exceeds expected length ${ expectedLength } ` ) ;
141+ }
142+ throw error ;
143+ }
144+ }
145+
132146function writePng ( png : PNG ) : Buffer {
133147 const scanlineLength = png . width * 4 ;
134148 const raw = Buffer . alloc ( ( scanlineLength + 1 ) * png . height ) ;
@@ -297,6 +311,23 @@ function decodeInterlacedPixels(inflated: Buffer, metadata: PngMetadata): Buffer
297311 return output ;
298312}
299313
314+ function inflatedByteLength ( metadata : PngMetadata ) : number {
315+ if ( metadata . interlace === 0 ) return filteredScanlineByteLength ( metadata , metadata . height ) ;
316+
317+ let byteLength = 0 ;
318+ for ( const pass of ADAM7_PASSES ) {
319+ const width = interlacePassSize ( metadata . width , pass . x , pass . dx ) ;
320+ const height = interlacePassSize ( metadata . height , pass . y , pass . dy ) ;
321+ if ( width === 0 || height === 0 ) continue ;
322+ byteLength += filteredScanlineByteLength ( { ...metadata , width, height } , height ) ;
323+ }
324+ return byteLength ;
325+ }
326+
327+ function filteredScanlineByteLength ( metadata : PngMetadata , height : number ) : number {
328+ return ( scanlineByteLength ( metadata ) + 1 ) * height ;
329+ }
330+
300331function interlacePassSize ( size : number , start : number , step : number ) : number {
301332 if ( size <= start ) return 0 ;
302333 return Math . floor ( ( size - start + step - 1 ) / step ) ;
@@ -313,25 +344,22 @@ function readPixel(
313344 const bytesPerSample = metadata . bitDepth === 16 ? 2 : 1 ;
314345 const channels = COLOR_CHANNELS . get ( metadata . colorType ) ! ;
315346 const offset = x * channels * bytesPerSample ;
316- const sample = ( channel : number ) : number => line [ offset + channel * bytesPerSample ] ! ;
317- const fullSample = ( channel : number ) : number =>
318- metadata . bitDepth === 16 ? line . readUInt16BE ( offset + channel * 2 ) : sample ( channel ) ;
347+ const rawSample = ( channel : number ) : number =>
348+ metadata . bitDepth === 16
349+ ? line . readUInt16BE ( offset + channel * 2 )
350+ : line [ offset + channel * bytesPerSample ] ! ;
351+ const sample = ( channel : number ) : number => scaleSample ( rawSample ( channel ) , metadata . bitDepth ) ;
319352
320353 if ( metadata . colorType === 0 ) {
321354 const gray = sample ( 0 ) ;
322- const transparent = matchesTransparentGray ( fullSample ( 0 ) , metadata ) ;
355+ const transparent = matchesTransparentGray ( rawSample ( 0 ) , metadata ) ;
323356 return [ gray , gray , gray , transparent ? 0 : 255 ] ;
324357 }
325358 if ( metadata . colorType === 2 ) {
326359 const red = sample ( 0 ) ;
327360 const green = sample ( 1 ) ;
328361 const blue = sample ( 2 ) ;
329- const transparent = matchesTransparentRgb (
330- fullSample ( 0 ) ,
331- fullSample ( 1 ) ,
332- fullSample ( 2 ) ,
333- metadata ,
334- ) ;
362+ const transparent = matchesTransparentRgb ( rawSample ( 0 ) , rawSample ( 1 ) , rawSample ( 2 ) , metadata ) ;
335363 return [ red , green , blue , transparent ? 0 : 255 ] ;
336364 }
337365 if ( metadata . colorType === 4 ) {
@@ -391,6 +419,11 @@ function filterBytesPerPixel(metadata: PngMetadata): number {
391419 return Math . max ( 1 , Math . ceil ( ( channels * metadata . bitDepth ) / 8 ) ) ;
392420}
393421
422+ function scaleSample ( sample : number , bitDepth : number ) : number {
423+ if ( bitDepth === 16 ) return Math . round ( ( sample / 0xffff ) * 0xff ) ;
424+ return sample ;
425+ }
426+
394427function matchesTransparentGray ( sample : number , metadata : PngMetadata ) : boolean {
395428 if ( ! metadata . transparency || metadata . transparency . length < 2 ) return false ;
396429 return sample === metadata . transparency . readUInt16BE ( 0 ) ;
@@ -444,3 +477,7 @@ function validateDimension(value: number, label: string): number {
444477 if ( ! Number . isInteger ( value ) || value < 1 ) throw new Error ( `PNG ${ label } must be positive` ) ;
445478 return value ;
446479}
480+
481+ function isZlibOutputLimitError ( error : unknown ) : boolean {
482+ return error instanceof Error && 'code' in error && error . code === 'ERR_BUFFER_TOO_LARGE' ;
483+ }
0 commit comments