import assert from'node:assert/strict';import{test}from'node:test';import{loadTypescript}from'./support/load-typescript.mjs'; const load=()=>loadTypescript(new URL('../src/input/PhotoMetadata.ts',import.meta.url)); function jpeg(orientation,width=4032,height=3024){const bytes=new Uint8Array(51),v=new DataView(bytes.buffer);bytes.set([255,216,255,225,0,34,69,120,105,102,0,0,73,73,42,0,8,0,0,0,1,0,18,1,3,0,1,0,0,0,orientation,0,0,0,0,0,0,0,255,192,0,11,8],0);v.setUint16(43,height);v.setUint16(45,width);bytes.set([1,1,17,0],47);return bytes;} test('JPEG EXIF1–8 dimensions swap only on transpose orientations',async()=>{const{readPhotoMetadata}=await load();for(let o=1;o<=8;o++){const m=await readPhotoMetadata(new Blob([jpeg(o)]));assert.equal(m.orientation,o);assert.deepEqual([m.width,m.height],o>=5?[3024,4032]:[4032,3024]);}}); test('PNG and WebP header dimensions do not decode full image',async()=>{const{readPhotoMetadata}=await load();const p=new Uint8Array(24);p.set([137,80,78,71,13,10,26,10]);new DataView(p.buffer).setUint32(16,8000);new DataView(p.buffer).setUint32(20,6000);assert.equal((await readPhotoMetadata(new Blob([p]))).width,8000);const w=new Uint8Array(30);w.set(new TextEncoder().encode('RIFFxxxxWEBPVP8X'));w[24]=255;w[25]=15;w[27]=255;w[28]=7;assert.deepEqual(await readPhotoMetadata(new Blob([w])),{width:4096,height:2048,orientation:1});}); test('truncated/malformed metadata safely falls back and read budget is bounded',async()=>{const{readPhotoMetadata,PHOTO_HEADER_BYTES}=await load();assert.equal(await readPhotoMetadata(new Blob([new Uint8Array(3)])),null);let length;await readPhotoMetadata({slice(start,end){length=end-start;return new Blob([new Uint8Array(3)]);}});assert.ok(length<=256*1024);assert.equal(length,PHOTO_HEADER_BYTES);}); test('known large file requests its canonical dimensions on the first decoder invocation',async()=>{const old=globalThis.createImageBitmap,calls=[];globalThis.createImageBitmap=async(_source,opts)=>{calls.push(opts);return{width:opts.resizeWidth,height:opts.resizeHeight,close(){}};};try{const{createPhotoSnapshot}=await loadTypescript(new URL('../src/input/PhotoSnapshot.ts',import.meta.url));const result=await createPhotoSnapshot(new Blob([jpeg(6)]),1,new AbortController().signal);assert.equal(calls.length,1);assert.deepEqual([calls[0].resizeWidth,calls[0].resizeHeight],[768,1024]);assert.deepEqual([result.width,result.height],[768,1024]);}finally{globalThis.createImageBitmap=old;}}); test('unknown PNG/WebP orientation never forces raw-aspect decoder resize',async()=>{const old=globalThis.createImageBitmap,calls=[];const p=new Uint8Array(24);p.set([137,80,78,71,13,10,26,10]);new DataView(p.buffer).setUint32(16,4032);new DataView(p.buffer).setUint32(20,3024);globalThis.createImageBitmap=async(_source,opts)=>{calls.push(opts);return calls.length===1?{width:3024,height:4032,close(){}}:{width:768,height:1024,close(){}};};try{const{createPhotoSnapshot}=await loadTypescript(new URL('../src/input/PhotoSnapshot.ts',import.meta.url));const result=await createPhotoSnapshot(new Blob([p]),1,new AbortController().signal);assert.equal(calls[0].resizeWidth,undefined);assert.equal(calls[1].resizeWidth,768);assert.equal(result.width,768);}finally{globalThis.createImageBitmap=old;}}); function tiff(o){const b=new Uint8Array(26),v=new DataView(b.buffer);b.set([73,73,42,0,8,0,0,0,1,0,18,1,3,0,1,0,0,0,o]);return b;} function chunk(type,data,png){const b=new Uint8Array(8+data.length+(png?4:data.length%2)),v=new DataView(b.buffer);b.set(new TextEncoder().encode(type),png?4:0);v.setUint32(png?0:4,data.length,!png);b.set(data,8);return b;} test('PNG/WebP EXIF after large encoded payload is read with bounded slices for rotations and mirrors',async()=>{const{readPhotoMetadata,PHOTO_HEADER_BYTES}=await load();for(const png of[true,false])for(let o=1;o<=8;o++){let bytes;if(png){const ihdr=new Uint8Array(13),v=new DataView(ihdr.buffer);v.setUint32(0,8000);v.setUint32(4,6000);bytes=new Blob([new Uint8Array([137,80,78,71,13,10,26,10]),chunk('IHDR',ihdr,true),chunk('IDAT',new Uint8Array(1024*1024),true),chunk('eXIf',tiff(o),true),chunk('IEND',new Uint8Array(),true)]);}else{const vp8x=new Uint8Array(10);vp8x[0]=8;vp8x[4]=63;vp8x[5]=31;vp8x[7]=111;vp8x[8]=23;const body=new Blob([chunk('VP8X',vp8x,false),chunk('VP8 ',new Uint8Array(1024*1024),false),chunk('EXIF',tiff(o),false)]),head=new Uint8Array(12);head.set(new TextEncoder().encode('RIFF'));new DataView(head.buffer).setUint32(4,body.size+4,true);head.set(new TextEncoder().encode('WEBP'),8);bytes=new Blob([head,body]);}let read=0;const file={size:bytes.size,slice(start,end){read+=end-start;return bytes.slice(start,end);}};const metadata=await readPhotoMetadata(file);assert.deepEqual(metadata,{width:o>=5?6000:8000,height:o>=5?8000:6000,orientation:o});assert.ok(read<=2*PHOTO_HEADER_BYTES);}}); test('JPEG without EXIF positively records orientation1 when scan reaches image data',async()=>{const{readPhotoMetadata}=await load(),b=new Uint8Array([255,216,255,192,0,11,8,11,208,15,160,1,1,17,0,255,218,0,2]);assert.deepEqual(await readPhotoMetadata(new Blob([b])),{width:4000,height:3024,orientation:1});});