Files
UnrealPrototyping/Tools/Orogen/js/png-write.js
T
2026-09-25 17:02:24 +03:00

93 lines
3.5 KiB
JavaScript

// 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);
}