Tooling
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
// Greyscale PNG encoding, 8-bit and 16-bit.
|
||||
//
|
||||
// Both are here because the Unreal landscape export needs both - a 16-bit height and an 8-bit weightmap
|
||||
// per paint layer per tile - and because a canvas cannot produce either. toBlob writes 8-bit RGBA and
|
||||
// Unreal's landscape importer wants single-channel, so the weightmaps would have to be un-RGBA'd on the
|
||||
// way in; the heights have no 16-bit canvas path at all. Writing the chunks directly is less code than
|
||||
// working around either.
|
||||
//
|
||||
// Compression is CompressionStream('deflate'), which is the zlib wrapper PNG asks for, not the raw
|
||||
// DEFLATE that 'deflate-raw' would give. Filter 0 (None) on every scanline: the rows here are either
|
||||
// smooth height ramps or near-flat weight fields, and Paeth would cost a pass over the image to save a
|
||||
// few per cent of a file that is written once and read once.
|
||||
|
||||
const _crc32Table = (() => {
|
||||
const t = new Uint32Array(256);
|
||||
for (let n = 0; n < 256; n++) {
|
||||
let c = n;
|
||||
for (let k = 0; k < 8; k++) c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
|
||||
t[n] = c;
|
||||
}
|
||||
return t;
|
||||
})();
|
||||
|
||||
function crc32(buf) {
|
||||
let crc = 0xFFFFFFFF;
|
||||
for (let i = 0; i < buf.length; i++) crc = _crc32Table[(crc ^ buf[i]) & 0xFF] ^ (crc >>> 8);
|
||||
return (crc ^ 0xFFFFFFFF) >>> 0;
|
||||
}
|
||||
|
||||
function chunk(type, data) {
|
||||
const out = new Uint8Array(4 + 4 + data.length + 4);
|
||||
const dv = new DataView(out.buffer);
|
||||
dv.setUint32(0, data.length);
|
||||
for (let i = 0; i < 4; i++) out[4 + i] = type.charCodeAt(i);
|
||||
out.set(data, 8);
|
||||
dv.setUint32(8 + data.length, crc32(out.subarray(4, 8 + data.length)));
|
||||
return out;
|
||||
}
|
||||
|
||||
async function assemble(width, height, bitDepth, raw) {
|
||||
const ihdr = new Uint8Array(13);
|
||||
const iv = new DataView(ihdr.buffer);
|
||||
iv.setUint32(0, width);
|
||||
iv.setUint32(4, height);
|
||||
ihdr[8] = bitDepth;
|
||||
ihdr[9] = 0; // colour type 0: greyscale
|
||||
|
||||
const cs = new CompressionStream('deflate');
|
||||
const writer = cs.writable.getWriter();
|
||||
writer.write(raw);
|
||||
writer.close();
|
||||
const body = new Uint8Array(await new Response(cs.readable).arrayBuffer());
|
||||
|
||||
const parts = [
|
||||
new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]),
|
||||
chunk('IHDR', ihdr),
|
||||
chunk('IDAT', body),
|
||||
chunk('IEND', new Uint8Array(0)),
|
||||
];
|
||||
const png = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
||||
let at = 0;
|
||||
for (const part of parts) { png.set(part, at); at += part.length; }
|
||||
return new Blob([png], { type: 'image/png' });
|
||||
}
|
||||
|
||||
/** 16-bit greyscale PNG from a Uint16Array of width * height, row-major. Big-endian, as PNG requires. */
|
||||
export async function encodeGray16(width, height, data) {
|
||||
const rowLen = 1 + width * 2;
|
||||
const raw = new Uint8Array(height * rowLen);
|
||||
for (let y = 0; y < height; y++) {
|
||||
const off = y * rowLen;
|
||||
raw[off] = 0;
|
||||
for (let x = 0; x < width; x++) {
|
||||
const v = data[y * width + x];
|
||||
raw[off + 1 + x * 2] = (v >> 8) & 0xFF;
|
||||
raw[off + 2 + x * 2] = v & 0xFF;
|
||||
}
|
||||
}
|
||||
return assemble(width, height, 16, raw);
|
||||
}
|
||||
|
||||
/** 8-bit greyscale PNG from a Uint8Array of width * height, row-major. */
|
||||
export async function encodeGray8(width, height, data) {
|
||||
const rowLen = 1 + width;
|
||||
const raw = new Uint8Array(height * rowLen);
|
||||
for (let y = 0; y < height; y++) {
|
||||
const off = y * rowLen;
|
||||
raw[off] = 0;
|
||||
raw.set(data.subarray(y * width, (y + 1) * width), off + 1);
|
||||
}
|
||||
return assemble(width, height, 8, raw);
|
||||
}
|
||||
Reference in New Issue
Block a user