|
| 1 | +const {unsigned: crc32} = require('buffer-crc32'); |
| 2 | +const zlib = require('node:zlib'); |
| 3 | +const { PNG } = require('../constants'); |
| 4 | + |
| 5 | +const COMPRESSION_LEVEL = 3; |
| 6 | + |
| 7 | +const convertRgbaToScanlines = (rgba, width, height) => { |
| 8 | + const stride = width * PNG.RGBA_CHANNELS; |
| 9 | + const scanlines = Buffer.allocUnsafe(height * (1 + stride)); // extra byte for filter |
| 10 | + |
| 11 | + let scanlineOffset = 0; |
| 12 | + let pixelDataOffset = 0; |
| 13 | + |
| 14 | + for (let y = 0; y < height; y++) { |
| 15 | + scanlineOffset = scanlines.writeUInt8(PNG.FILTER_NO_FILTER, scanlineOffset); |
| 16 | + scanlineOffset += rgba.copy(scanlines, scanlineOffset, pixelDataOffset, pixelDataOffset + stride); |
| 17 | + |
| 18 | + pixelDataOffset += stride; |
| 19 | + } |
| 20 | + |
| 21 | + if (scanlineOffset !== scanlines.byteLength) { |
| 22 | + throw new Error('Got malformed input while trying to convert rgba to png'); |
| 23 | + } |
| 24 | + |
| 25 | + return scanlines; |
| 26 | +} |
| 27 | + |
| 28 | +exports.convertRgbaToPng = (rgba, width, height, compressionLevel = COMPRESSION_LEVEL) => { |
| 29 | + const scanlines = convertRgbaToScanlines(rgba, width, height); |
| 30 | + const compressedData = zlib.deflateSync(scanlines, { level: compressionLevel }); |
| 31 | + const resultBuffer = Buffer.allocUnsafe(PNG.MIN_ASSIST_BYTES + compressedData.length); |
| 32 | + |
| 33 | + let pointer = 0; |
| 34 | + |
| 35 | + // signature |
| 36 | + pointer += PNG.SIGNATURE.copy(resultBuffer); |
| 37 | + |
| 38 | + // IHDR |
| 39 | + const ihdrPointer = (pointer = resultBuffer.writeUInt32BE(PNG.IHDR_LENGTH, pointer)); |
| 40 | + pointer += resultBuffer.write("IHDR", pointer, "ascii"); |
| 41 | + pointer = resultBuffer.writeUInt32BE(width, pointer); |
| 42 | + pointer = resultBuffer.writeUInt32BE(height, pointer); |
| 43 | + pointer = resultBuffer.writeUInt8(PNG.BIT_DEPTH_EIGHT_BIT, pointer); |
| 44 | + pointer = resultBuffer.writeUInt8(PNG.COLOR_TYPE_RGBA, pointer); |
| 45 | + pointer = resultBuffer.writeUInt8(PNG.COMPRESSION_DEFLATE, pointer); |
| 46 | + pointer = resultBuffer.writeUInt8(PNG.FILTER_NO_FILTER, pointer); |
| 47 | + pointer = resultBuffer.writeUInt8(PNG.INTERLACE_NO_INTERLACE, pointer); |
| 48 | + const ihdrCrc = crc32(Buffer.from(resultBuffer.buffer, ihdrPointer, pointer - ihdrPointer)); |
| 49 | + pointer = resultBuffer.writeUInt32BE(ihdrCrc, pointer); |
| 50 | + |
| 51 | + // IDAT |
| 52 | + const idatPointer = (pointer = resultBuffer.writeUInt32BE(compressedData.length, pointer)); |
| 53 | + pointer += resultBuffer.write("IDAT", idatPointer, "ascii"); |
| 54 | + pointer += compressedData.copy(resultBuffer, pointer); |
| 55 | + const idatCrc = crc32(Buffer.from(resultBuffer.buffer, idatPointer, pointer - idatPointer)); |
| 56 | + pointer = resultBuffer.writeUInt32BE(idatCrc, pointer); |
| 57 | + |
| 58 | + // IEND (empty) |
| 59 | + const iendPointer = (pointer = resultBuffer.writeUInt32BE(0, pointer)); |
| 60 | + pointer += resultBuffer.write("IEND", pointer, "ascii"); |
| 61 | + const iendCrc = crc32(Buffer.from(resultBuffer.buffer, iendPointer, pointer - iendPointer)); |
| 62 | + pointer = resultBuffer.writeUInt32BE(iendCrc, pointer); |
| 63 | + |
| 64 | + if (pointer !== resultBuffer.byteLength) { |
| 65 | + throw new Error("Got malformed input while trying to convert rgba to png"); |
| 66 | + } |
| 67 | + return resultBuffer; |
| 68 | +}; |
0 commit comments