@@ -3,8 +3,8 @@ import assert from 'node:assert/strict';
33import fs from 'node:fs' ;
44import os from 'node:os' ;
55import path from 'node:path' ;
6- import { PNG } from 'pngjs ' ;
7- import { resizePngFileToMaxSize } from '../png.ts' ;
6+ import { deflateSync } from 'node:zlib ' ;
7+ import { PNG , resizePngFileToMaxSize } from '../png.ts' ;
88
99test ( 'resizePngFileToMaxSize leaves smaller images unchanged' , async ( ) => {
1010 const filePath = tmpPngPath ( 'unchanged' ) ;
@@ -20,6 +20,91 @@ test('resizePngFileToMaxSize leaves smaller images unchanged', async () => {
2020 assert . deepEqual ( readPngPixel ( unchanged , 3 , 1 ) , [ 45 , 90 , 135 , 255 ] ) ;
2121} ) ;
2222
23+ test ( 'PNG sync reader decodes filtered RGB image data' , ( ) => {
24+ const png = PNG . sync . read (
25+ encodeTestPng ( {
26+ width : 2 ,
27+ height : 1 ,
28+ bitDepth : 8 ,
29+ colorType : 2 ,
30+ rawScanlines : Buffer . from ( [ 1 , 10 , 20 , 30 , 40 , 60 , 100 ] ) ,
31+ } ) ,
32+ ) ;
33+
34+ assert . equal ( png . width , 2 ) ;
35+ assert . equal ( png . height , 1 ) ;
36+ assert . deepEqual ( readPngPixel ( png , 0 , 0 ) , [ 10 , 20 , 30 , 255 ] ) ;
37+ assert . deepEqual ( readPngPixel ( png , 1 , 0 ) , [ 50 , 80 , 130 , 255 ] ) ;
38+ } ) ;
39+
40+ test ( 'PNG sync reader decodes indexed color and transparency' , ( ) => {
41+ const png = PNG . sync . read (
42+ encodeTestPng ( {
43+ width : 4 ,
44+ height : 1 ,
45+ bitDepth : 2 ,
46+ colorType : 3 ,
47+ palette : Buffer . from ( [ 255 , 0 , 0 , 0 , 255 , 0 , 0 , 0 , 255 , 20 , 30 , 40 ] ) ,
48+ transparency : Buffer . from ( [ 255 , 200 , 80 , 255 ] ) ,
49+ rawScanlines : Buffer . from ( [ 0 , 0b00011011 ] ) ,
50+ } ) ,
51+ ) ;
52+
53+ assert . deepEqual ( readPngPixel ( png , 0 , 0 ) , [ 255 , 0 , 0 , 255 ] ) ;
54+ assert . deepEqual ( readPngPixel ( png , 1 , 0 ) , [ 0 , 255 , 0 , 200 ] ) ;
55+ assert . deepEqual ( readPngPixel ( png , 2 , 0 ) , [ 0 , 0 , 255 , 80 ] ) ;
56+ assert . deepEqual ( readPngPixel ( png , 3 , 0 ) , [ 20 , 30 , 40 , 255 ] ) ;
57+ } ) ;
58+
59+ test ( 'PNG sync reader decodes Adam7 interlaced RGB image data' , ( ) => {
60+ const png = PNG . sync . read (
61+ encodeTestPng ( {
62+ width : 3 ,
63+ height : 3 ,
64+ bitDepth : 8 ,
65+ colorType : 2 ,
66+ interlace : 1 ,
67+ rawScanlines : Buffer . from ( [
68+ 0 ,
69+ ...rgb ( 0 , 0 ) ,
70+ 0 ,
71+ ...rgb ( 2 , 0 ) ,
72+ 0 ,
73+ ...rgb ( 0 , 2 ) ,
74+ ...rgb ( 2 , 2 ) ,
75+ 0 ,
76+ ...rgb ( 1 , 0 ) ,
77+ 0 ,
78+ ...rgb ( 1 , 2 ) ,
79+ 0 ,
80+ ...rgb ( 0 , 1 ) ,
81+ ...rgb ( 1 , 1 ) ,
82+ ...rgb ( 2 , 1 ) ,
83+ ] ) ,
84+ } ) ,
85+ ) ;
86+
87+ for ( let y = 0 ; y < 3 ; y += 1 ) {
88+ for ( let x = 0 ; x < 3 ; x += 1 ) {
89+ assert . deepEqual ( readPngPixel ( png , x , y ) , [ ...rgb ( x , y ) , 255 ] ) ;
90+ }
91+ }
92+ } ) ;
93+
94+ test ( 'PNG sync reader rejects invalid chunk CRCs' , ( ) => {
95+ const bytes = encodeTestPng ( {
96+ width : 1 ,
97+ height : 1 ,
98+ bitDepth : 8 ,
99+ colorType : 2 ,
100+ rawScanlines : Buffer . from ( [ 0 , ...rgb ( 0 , 0 ) ] ) ,
101+ } ) ;
102+ const lastByte = bytes . length - 1 ;
103+ bytes [ lastByte ] = ( bytes [ lastByte ] ?? 0 ) ^ 0xff ;
104+
105+ assert . throws ( ( ) => PNG . sync . read ( bytes ) , / I n v a l i d P N G .* c h u n k C R C / ) ;
106+ } ) ;
107+
23108function tmpPngPath ( prefix : string ) : string {
24109 return path . join (
25110 fs . mkdtempSync ( path . join ( os . tmpdir ( ) , `agent-device-png-${ prefix } -` ) ) ,
@@ -56,3 +141,57 @@ function readPngPixel(png: PNG, x: number, y: number): number[] {
56141 png . data [ offset + 3 ] ?? 0 ,
57142 ] ;
58143}
144+
145+ function encodeTestPng ( params : {
146+ width : number ;
147+ height : number ;
148+ bitDepth : number ;
149+ colorType : number ;
150+ rawScanlines : Buffer ;
151+ interlace ?: 0 | 1 ;
152+ palette ?: Buffer ;
153+ transparency ?: Buffer ;
154+ } ) : Buffer {
155+ const ihdr = Buffer . alloc ( 13 ) ;
156+ ihdr . writeUInt32BE ( params . width , 0 ) ;
157+ ihdr . writeUInt32BE ( params . height , 4 ) ;
158+ ihdr [ 8 ] = params . bitDepth ;
159+ ihdr [ 9 ] = params . colorType ;
160+ ihdr [ 10 ] = 0 ;
161+ ihdr [ 11 ] = 0 ;
162+ ihdr [ 12 ] = params . interlace ?? 0 ;
163+
164+ return Buffer . concat ( [
165+ Buffer . from ( [ 0x89 , 0x50 , 0x4e , 0x47 , 0x0d , 0x0a , 0x1a , 0x0a ] ) ,
166+ encodeTestChunk ( 'IHDR' , ihdr ) ,
167+ ...( params . palette ? [ encodeTestChunk ( 'PLTE' , params . palette ) ] : [ ] ) ,
168+ ...( params . transparency ? [ encodeTestChunk ( 'tRNS' , params . transparency ) ] : [ ] ) ,
169+ encodeTestChunk ( 'IDAT' , deflateSync ( params . rawScanlines ) ) ,
170+ encodeTestChunk ( 'IEND' , Buffer . alloc ( 0 ) ) ,
171+ ] ) ;
172+ }
173+
174+ function rgb ( x : number , y : number ) : [ number , number , number ] {
175+ return [ x * 40 + 10 , y * 50 + 20 , x * 30 + y * 20 + 30 ] ;
176+ }
177+
178+ function encodeTestChunk ( type : string , data : Buffer ) : Buffer {
179+ const typeBuffer = Buffer . from ( type , 'ascii' ) ;
180+ const chunk = Buffer . alloc ( 8 + data . length + 4 ) ;
181+ chunk . writeUInt32BE ( data . length , 0 ) ;
182+ typeBuffer . copy ( chunk , 4 ) ;
183+ data . copy ( chunk , 8 ) ;
184+ chunk . writeUInt32BE ( crc32 ( Buffer . concat ( [ typeBuffer , data ] ) ) , 8 + data . length ) ;
185+ return chunk ;
186+ }
187+
188+ function crc32 ( buffer : Buffer ) : number {
189+ let crc = 0xffffffff ;
190+ for ( const byte of buffer ) {
191+ crc ^= byte ;
192+ for ( let bit = 0 ; bit < 8 ; bit += 1 ) {
193+ crc = crc & 1 ? 0xedb88320 ^ ( crc >>> 1 ) : crc >>> 1 ;
194+ }
195+ }
196+ return ( crc ^ 0xffffffff ) >>> 0 ;
197+ }
0 commit comments