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