29 lines
2.1 KiB
JavaScript
29 lines
2.1 KiB
JavaScript
'use strict';
|
|||
|
|
const {PNG}=require('pngjs');
|
||
|
|
const {inflateSync}=require('node:zlib');
|
||
|
|
const {visualFingerprint}=require('./visual-hash.cjs');
|
||
|
|
const SIGNATURE=Buffer.from([137,80,78,71,13,10,26,10]);
|
||
|
|
function decodePNG(input){
|
||
|
|
const bytes=Buffer.from(input);
|
||
|
|
if(bytes.length<45||bytes.length>10*1024*1024||!bytes.subarray(0,8).equals(SIGNATURE)||bytes.readUInt32BE(8)!==13||bytes.toString('ascii',12,16)!=='IHDR')throw Error('Invalid bounded PNG reference');
|
||
|
|
const width=bytes.readUInt32BE(16),height=bytes.readUInt32BE(20),depth=bytes[24],type=bytes[25];
|
||
|
|
if(width<16||height<16||width>8192||height>8192||width*height>8*1024*1024||depth!==8||![0,2,4,6].includes(type)||bytes[26]!==0||bytes[27]!==0||bytes[28]!==0)throw Error('Unsupported PNG dimensions or encoding');
|
||
|
|
const parts=[];let ended=false;
|
||
|
|
for(let offset=8;offset<bytes.length;){
|
||
|
|
if(offset+12>bytes.length)throw Error('Truncated PNG');
|
||
|
|
const length=bytes.readUInt32BE(offset),chunk=bytes.toString('ascii',offset+4,offset+8),next=offset+12+length;
|
||
|
|
if(next>bytes.length||ended||(chunk==='IHDR'&&offset!==8)||['acTL','fcTL','fdAT'].includes(chunk))throw Error('Unsupported PNG structure');
|
||
|
|
if(chunk==='IDAT')parts.push(bytes.subarray(offset+8,offset+8+length));
|
||
|
|
if(chunk==='IEND'){if(length!==0||next!==bytes.length)throw Error('Invalid PNG end');ended=true;}
|
||
|
|
offset=next;
|
||
|
|
}
|
||
|
|
if(!ended||!parts.length)throw Error('Incomplete PNG');
|
||
|
|
const channels={0:1,2:3,4:2,6:4}[type],expected=(width*channels+1)*height;
|
||
|
|
const inflated=inflateSync(Buffer.concat(parts),{maxOutputLength:expected+1});
|
||
|
|
try{if(inflated.length!==expected)throw Error('PNG data size differs from header');}finally{inflated.fill(0);}
|
||
|
|
const image=PNG.sync.read(bytes,{checkCRC:true});
|
||
|
|
if(image.width!==width||image.height!==height||image.data.length!==width*height*4)throw Error('PNG output dimensions invalid');
|
||
|
|
return {rgba:image.data,width,height};
|
||
|
|
}
|
||
|
|
function fingerprintPNG(bytes){const image=decodePNG(bytes);try{return {...visualFingerprint(image),width:image.width,height:image.height};}finally{image.rgba.fill(0);}}
|
||
|
|
module.exports={decodePNG,fingerprintPNG};
|