// SPDX-License-Identifier: LGPL-3.0-or-later import {exifOrientation,itemLocations} from './heic-orientation.js'; const error=()=>Error('This HEIC is not supported. Use an 8-bit SDR still image in sRGB or Display P3, or convert it to JPG first.'); export function heicPreflight(bytes){ if(bytes.length>20*1024*1024)throw Error('Keep each HEIC under 20 MB and 16 megapixels.'); const view=new DataView(bytes.buffer,bytes.byteOffset,bytes.byteLength),text=(a,b)=>String.fromCharCode(...bytes.subarray(a,b));let count=0,dimensions=0,colorSpace=null,brand=false,hasTransform=false,idat=null,locations=new Map();const exifItems=new Set(); const color=value=>{if(colorSpace&&colorSpace!==value)throw error();colorSpace=value;}; function walk(start,end,depth=0){ if(depth>8)throw error(); while(start10000||start+8>end)throw error();let size=view.getUint32(start),header=8;const type=text(start+4,start+8); if(size===1){if(start+16>end)throw error();const large=view.getBigUint64(start+8);if(large>BigInt(Number.MAX_SAFE_INTEGER))throw error();size=Number(large);header=16;}else if(size===0)size=end-start; if(sizeend)throw error();const data=start+header,stop=start+size; if(type==='ftyp'){if(stop-data<8)throw error();const brands=[text(data,data+4)];for(let i=data+8;i+4<=stop;i+=4)brands.push(text(i,i+4));if(brands.some(b=>['msf1','hevc','hevx','hevs','avis'].includes(b)))throw error();brand=brands.some(b=>['heic','heix','mif1'].includes(b));} if(type==='moov')throw error(); if(['irot','imir'].includes(type))hasTransform=true; if(type==='idat')idat={start:data,end:stop}; if(type==='iloc')locations=itemLocations(bytes,data,stop); if(type==='infe'){ const version=bytes[data],idSize=version===2?2:version===3?4:0; if(idSize){if(data+6+idSize+4>stop)throw error();const id=idSize===2?view.getUint16(data+4):view.getUint32(data+4);if(text(data+6+idSize,data+10+idSize)==='Exif')exifItems.add(id);if(exifItems.size>16)throw error();} } if(type==='ispe'){if(stop-data<12)throw error();const w=view.getUint32(data+4),h=view.getUint32(data+8);if(!w||!h||w*h>16000000)throw Error('Keep each HEIC under 20 MB and 16 megapixels.');dimensions++;} if(type==='pixi'){if(stop-data<5)throw error();const n=bytes[data+4];if(data+5+n>stop||!n||bytes.subarray(data+5,data+5+n).some(b=>b!==8))throw error();} if(type==='hvcC'&&(stop-data<19||(bytes[data+17]&7)!==0||(bytes[data+18]&7)!==0))throw error(); if(type==='auxC'&&/hdr|gainmap/i.test(text(data,Math.min(stop,data+256))))throw error(); if(type==='colr'){ const kind=text(data,data+4); if(kind==='nclx'){if(stop-data<11)throw error();const primary=view.getUint16(data+4),transfer=view.getUint16(data+6);if(transfer!==13||![1,12].includes(primary))throw error();color(primary===12?'display-p3':'srgb');} else if(['prof','rICC'].includes(kind))color(iccColor(bytes.subarray(data+4,stop))); else throw error(); } if(type==='iinf')walk(data+(bytes[data]===0?6:8),stop,depth+1);else if(type==='meta')walk(data+4,stop,depth+1);else if(['iprp','ipco'].includes(type))walk(data,stop,depth+1); start=stop; } } walk(0,bytes.length);if(!brand||!dimensions)throw error();return {colorSpace:colorSpace||'srgb',orientation:exifOrientation(bytes,exifItems,locations,idat,hasTransform)}; } function iccColor(bytes){ if(bytes.length<132)throw error();const v=new DataView(bytes.buffer,bytes.byteOffset,bytes.byteLength),str=(a,b)=>String.fromCharCode(...bytes.subarray(a,b)); if(str(16,20)!=='RGB ')throw error();const tags=new Map(),n=v.getUint32(128);if(n>128||132+n*12>bytes.length)throw error(); for(let i=0;ibytes.length)throw error();tags.set(str(p,p+4),{offset,size});} const matrix=[];for(const name of ['rXYZ','gXYZ','bXYZ']){const t=tags.get(name);if(!t||t.size<20||str(t.offset,t.offset+4)!=='XYZ ')throw error();matrix.push(...[8,12,16].map(i=>v.getInt32(t.offset+i)/65536));} // Standard D50-adapted sRGB and Display-P3 matrices. Unknown profiles are // retained by refusing conversion, rather than silently assigning sRGB. const spaces={srgb:[.4361,.2225,.0139,.3851,.7169,.0971,.1431,.0606,.7141],'display-p3':[.5151,.2412,-.0011,.292,.6922,.0419,.1571,.0666,.7841]}; for(const [space,expected]of Object.entries(spaces))if(matrix.every((v,i)=>Math.abs(v-expected[i])<.002)){ // Only the standard sRGB/Display-P3 parametric transfer curve is accepted. for(const name of ['rTRC','gTRC','bTRC']){const t=tags.get(name);if(!t||t.size<32||str(t.offset,t.offset+4)!=='para'||![3,4].includes(v.getUint16(t.offset+8)))throw error();const expected=[2.4,1/1.055,.055/1.055,1/12.92,.04045];if(expected.some((x,i)=>Math.abs(v.getInt32(t.offset+12+i*4)/65536-x)>.002))throw error();} return space; } throw error(); }