55 lines
3.8 KiB
JavaScript
55 lines
3.8 KiB
JavaScript
// Fixed 192x192 RGBA PNG codec, independent of DOM and shared by Node tests.
|
||
import { zlibSync, unzlibSync } from "./vendor/fflate/fflate.js";
|
||
const SIDE = 192, STRIDE = SIDE * 4;
|
||
const SIGNATURE = [137,80,78,71,13,10,26,10];
|
||
const crcTable = Uint32Array.from({ length: 256 }, (_, n) => {
|
||
for (let k = 0; k < 8; k++) n = n & 1 ? 0xedb88320 ^ (n >>> 1) : n >>> 1;
|
||
return n >>> 0;
|
||
});
|
||
function crc(bytes) { let n = 0xffffffff; for (const b of bytes) n = crcTable[(n ^ b) & 255] ^ (n >>> 8); return (n ^ 0xffffffff) >>> 0; }
|
||
function chunk(name, data) {
|
||
const out = new Uint8Array(data.length + 12), view = new DataView(out.buffer);
|
||
view.setUint32(0, data.length); out.set(Array.from(name, c => c.charCodeAt(0)), 4); out.set(data, 8);
|
||
view.setUint32(out.length - 4, crc(out.subarray(4, out.length - 4))); return out;
|
||
}
|
||
export function encodePixels(rgba) {
|
||
if (rgba.length !== SIDE * STRIDE) throw new Error("画笔像素长度无效");
|
||
const ihdr = new Uint8Array(13), v = new DataView(ihdr.buffer);
|
||
v.setUint32(0, SIDE); v.setUint32(4, SIDE); ihdr[8] = 8; ihdr[9] = 6;
|
||
const raw = new Uint8Array((STRIDE + 1) * SIDE);
|
||
for (let y = 0; y < SIDE; y++) raw.set(rgba.subarray(y * STRIDE, (y + 1) * STRIDE), y * (STRIDE + 1) + 1);
|
||
const parts = [new Uint8Array(SIGNATURE), chunk("IHDR", ihdr), chunk("IDAT", zlibSync(raw, { level: 6 })), chunk("IEND", new Uint8Array())];
|
||
const png = new Uint8Array(parts.reduce((n, p) => n + p.length, 0)); let at = 0;
|
||
for (const p of parts) { png.set(p, at); at += p.length; }
|
||
let binary = ""; for (const b of png) binary += String.fromCharCode(b);
|
||
return btoa(binary);
|
||
}
|
||
function paeth(a,b,c) { const p=a+b-c, x=Math.abs(p-a), y=Math.abs(p-b), z=Math.abs(p-c); return x<=y && x<=z ? a : y<=z ? b : c; }
|
||
export function decodePixels(value) {
|
||
if (typeof value !== "string" || value.length > 300000 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) throw new Error("画笔PNG无效");
|
||
const bytes = Uint8Array.from(atob(value), c => c.charCodeAt(0)), view = new DataView(bytes.buffer);
|
||
if (!SIGNATURE.every((b,i) => bytes[i] === b)) throw new Error("画笔PNG签名无效");
|
||
const parts=[]; let header=false, ended=false;
|
||
for (let at=8; at+12<=bytes.length;) {
|
||
const n=view.getUint32(at); if (n>bytes.length-at-12) throw new Error("PNG截断");
|
||
const name=String.fromCharCode(...bytes.subarray(at+4,at+8)), data=bytes.subarray(at+8,at+8+n);
|
||
if (crc(bytes.subarray(at+4,at+8+n))!==view.getUint32(at+8+n)) throw new Error("PNG校验失败");
|
||
if (name==="IHDR") { if (header || n!==13 || view.getUint32(at+8)!==SIDE || view.getUint32(at+12)!==SIDE || data[8]!==8 || data[9]!==6 || data[10] || data[11] || data[12]) throw new Error("PNG必须为192×192 RGBA8"); header=true; }
|
||
else if (name==="IDAT") { if (!header || ended) throw new Error("PNG顺序无效"); parts.push(data); }
|
||
else if (name==="IEND") { if (n || at+12!==bytes.length) throw new Error("PNG结尾无效"); ended=true; }
|
||
else if (name[0]===name[0].toUpperCase()) throw new Error("PNG不支持此块");
|
||
at+=n+12;
|
||
}
|
||
if (!header || !ended || !parts.length) throw new Error("PNG不完整");
|
||
const compressed=new Uint8Array(parts.reduce((n,p)=>n+p.length,0)); let at=0; for (const p of parts) { compressed.set(p,at); at+=p.length; }
|
||
const expected=(STRIDE+1)*SIDE, raw=unzlibSync(compressed,{out:new Uint8Array(expected+1)});
|
||
if (raw.length!==expected) throw new Error("PNG展开长度无效");
|
||
const out=new Uint8ClampedArray(SIDE*STRIDE);
|
||
for (let y=0;y<SIDE;y++) { const filter=raw[y*(STRIDE+1)]; if (filter>4) throw new Error("PNG过滤器无效");
|
||
for (let x=0;x<STRIDE;x++) { const i=y*STRIDE+x, a=x>=4?out[i-4]:0, b=y?out[i-STRIDE]:0, c=y&&x>=4?out[i-STRIDE-4]:0;
|
||
out[i]=(raw[y*(STRIDE+1)+1+x]+([0,a,b,Math.floor((a+b)/2),paeth(a,b,c)][filter]))&255;
|
||
}
|
||
}
|
||
return out;
|
||
}
|