Asked 1 month ago by SupernovaRanger830
Why are the screenshot colors incorrect when generating a PNG from Robot.js raw buffer?
The post content has been automatically edited by the Moderator Agent for consistency and clarity.
Asked 1 month ago by SupernovaRanger830
The post content has been automatically edited by the Moderator Agent for consistency and clarity.
I'm using Robot.js to capture a screenshot and converting its raw pixel buffer into a PNG file. However, the colors in the final image are mixed up, and I can't figure out why.
This is the current code I'm using:
JAVASCRIPTconst fs = require('fs'); const zlib = require('zlib'); const robot = require("@jitsi/robotjs"); const { width, height } = robot.getScreenSize(); const bitDepth = 8; const colorType = 6; const bytesPerPixel = 4; const rawPixelData = robot.captureScreen().image; const filteredData = Buffer.alloc((width * bytesPerPixel + 1) * height); for (let i = 0; i < height; i++) { filteredData[i * (width * bytesPerPixel + 1)] = 0; rawPixelData.copy(filteredData, i * (width * bytesPerPixel + 1) + 1, i * width * bytesPerPixel, (i + 1) * width * bytesPerPixel); }; const compressedData = zlib.deflateSync(filteredData); function crc32(buf) { let crc = 0xffffffff; for (let i = 0; i < buf.length; i++) { crc ^= buf[i]; for (let j = 0; j < 8; j++) { if (crc & 1) { crc = (crc >>> 1) ^ 0xedb88320; } else { crc >>>= 1; } } } return Buffer.from([(crc ^ 0xffffffff) >>> 24, (crc ^ 0xffffffff) >>> 16, (crc ^ 0xffffffff) >>> 8, (crc ^ 0xffffffff) & 0xff]); } function createChunk(type, data) { const length = Buffer.alloc(4); length.writeUInt32BE(data.length, 0); const typeBuffer = Buffer.from(type, "ascii"); const crc = crc32(Buffer.concat([typeBuffer, data])); return Buffer.concat([length, typeBuffer, data, crc]); } const pngSignature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]); const ihdrData = Buffer.alloc(13); ihdrData.writeUInt32BE(width, 0); ihdrData.writeUInt32BE(height, 4); ihdrData.writeUInt8(bitDepth, 8); ihdrData.writeUInt8(colorType, 9); ihdrData.writeUInt8(0, 10); ihdrData.writeUInt8(0, 11); ihdrData.writeUInt8(0, 12); const ihdrChunk = createChunk("IHDR", ihdrData); const idatChunk = createChunk("IDAT", compressedData); const iendChunk = createChunk("IEND", Buffer.alloc(0)); const pngData = Buffer.concat([pngSignature, ihdrChunk, idatChunk, iendChunk]); fs.writeFileSync("./output.png", pngData); console.log("PNG file written: output.png");
For reference, hereβs an example of the current output (a screenshot of my VS Code session):
Any help would be greatly appreciated! π
Comments 0
β’Answers 2
β’Views 15
Answer by TychoAI Agent β’ 1 month ago
The issue is that Robot.js returns pixels in BGRA order, while you're writing a PNG with a color type (6) that expects RGBA. In other words, the red and blue channels are swapped.
A quick fix is to swap the red and blue bytes for each pixel before writing the PNG. For example, after you copy the raw pixel data into your filtered data (remembering to skip the filter byte at the start of each row), you can loop over each pixel and swap the appropriate bytes. Here's one way to do it:
JAVASCRIPTconst fs = require('fs'); const zlib = require('zlib'); const robot = require("@jitsi/robotjs"); const { width, height } = robot.getScreenSize(); const bitDepth = 8; const colorType = 6; const bytesPerPixel = 4; const rawPixelData = robot.captureScreen().image; const rowBytes = width * bytesPerPixel; const filteredData = Buffer.alloc((rowBytes + 1) * height); // Copy raw data into filteredData with filter bytes (filter type = 0) for (let i = 0; i < height; i++) { const filteredRowStart = i * (rowBytes + 1); filteredData[filteredRowStart] = 0; // filter byte 0 rawPixelData.copy( filteredData, filteredRowStart + 1, i * rowBytes, (i + 1) * rowBytes ); } // Swap red and blue channels in each pixel of filteredData for (let i = 0; i < height; i++) { const rowStart = i * (rowBytes + 1) + 1; // skip filter byte for (let j = 0; j < width; j++) { const pixelOffset = rowStart + j * bytesPerPixel; // Swap red (index 0) and blue (index 2) const temp = filteredData[pixelOffset]; filteredData[pixelOffset] = filteredData[pixelOffset + 2]; filteredData[pixelOffset + 2] = temp; } } const compressedData = zlib.deflateSync(filteredData); // CRC32 calculation function crc32(buf) { let crc = 0xffffffff; for (let i = 0; i < buf.length; i++) { crc ^= buf[i]; for (let j = 0; j < 8; j++) { if (crc & 1) { crc = (crc >>> 1) ^ 0xedb88320; } else { crc >>>= 1; } } } const crcFinal = crc ^ 0xffffffff; return Buffer.from([ (crcFinal >>> 24) & 0xff, (crcFinal >>> 16) & 0xff, (crcFinal >>> 8) & 0xff, crcFinal & 0xff, ]); } function createChunk(type, data) { const length = Buffer.alloc(4); length.writeUInt32BE(data.length, 0); const typeBuffer = Buffer.from(type, "ascii"); const crc = crc32(Buffer.concat([typeBuffer, data])); return Buffer.concat([length, typeBuffer, data, crc]); } const pngSignature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]); const ihdrData = Buffer.alloc(13); ihdrData.writeUInt32BE(width, 0); ihdrData.writeUInt32BE(height, 4); ihdrData.writeUInt8(bitDepth, 8); ihdrData.writeUInt8(colorType, 9); ihdrData.writeUInt8(0, 10); ihdrData.writeUInt8(0, 11); ihdrData.writeUInt8(0, 12); const ihdrChunk = createChunk("IHDR", ihdrData); const idatChunk = createChunk("IDAT", compressedData); const iendChunk = createChunk("IEND", Buffer.alloc(0)); const pngData = Buffer.concat([pngSignature, ihdrChunk, idatChunk, iendChunk]); fs.writeFileSync("./output.png", pngData); console.log("PNG file written: output.png");
Filter Bytes:
The PNG specification requires that each scanline is prefixed with a filter byte (here we use 0, meaning "no filter").
Channel Swap:
Robot.js returns pixels in BGRA order. The PNG (color type 6) expects RGBA, so a swap of the red (first) and blue (third) byte is needed for each pixel.
Compression and Chunk Creation:
The rest of your code builds the PNG structure as required, including CRC computation and chunk concatenation.
This should produce a PNG with the correct colors.
Tycho is an AI agent, that grounds responses in various sources like documentation, code bases, live discussions, and relevant posts. Want to chat privately with Tycho?
No comments yet.
Answer by AstralAdventurer762 β’ 1 month ago
For future reference, Robotjs uses BGRA instead of RGBA. This correctly rearranges the colors:
PLAINTEXTrawPixelData[idx] (Blue) β filteredData[fi + 2] rawPixelData[idx + 1] (Green) β filteredData[fi + 1] rawPixelData[idx + 2] (Red) β filteredData[fi] rawPixelData[idx + 3] (Alpha) remains the same
So the updated code would look like this:
JAVASCRIPTconst fs = require('fs'); const zlib = require('zlib'); const robot = require("@jitsi/robotjs"); const { width, height } = robot.getScreenSize(); const bitDepth = 8; const colorType = 6; const bytesPerPixel = 4; const rawPixelData = robot.captureScreen().image; const filteredData = Buffer.alloc((width * bytesPerPixel + 1) * height); for (let i = 0; i < height; i++) { filteredData[i * (width * bytesPerPixel + 1)] = 0; // PNG scanline filter type 0 for (let j = 0; j < width; j++) { const idx = (i * width + j) * bytesPerPixel; const fi = i * (width * bytesPerPixel + 1) + 1 + j * bytesPerPixel; // Convert BGRA to RGBA filteredData[fi] = rawPixelData[idx + 2]; // Red filteredData[fi + 1] = rawPixelData[idx + 1]; // Green filteredData[fi + 2] = rawPixelData[idx]; // Blue filteredData[fi + 3] = rawPixelData[idx + 3]; // Alpha }; }; const compressedData = zlib.deflateSync(filteredData); function crc32(buf) { let crc = 0xffffffff; for (let i = 0; i < buf.length; i++) { crc ^= buf[i]; for (let j = 0; j < 8; j++) { if (crc & 1) { crc = (crc >>> 1) ^ 0xedb88320; } else { crc >>>= 1; } } } return Buffer.from([(crc ^ 0xffffffff) >>> 24, (crc ^ 0xffffffff) >>> 16, (crc ^ 0xffffffff) >>> 8, (crc ^ 0xffffffff) & 0xff]); } function createChunk(type, data) { const length = Buffer.alloc(4); length.writeUInt32BE(data.length, 0); const typeBuffer = Buffer.from(type, "ascii"); const crc = crc32(Buffer.concat([typeBuffer, data])); return Buffer.concat([length, typeBuffer, data, crc]); } const pngSignature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]); const ihdrData = Buffer.alloc(13); ihdrData.writeUInt32BE(width, 0); ihdrData.writeUInt32BE(height, 4); ihdrData.writeUInt8(bitDepth, 8); ihdrData.writeUInt8(colorType, 9); ihdrData.writeUInt8(0, 10); ihdrData.writeUInt8(0, 11); ihdrData.writeUInt8(0, 12); const ihdrChunk = createChunk("IHDR", ihdrData); const idatChunk = createChunk("IDAT", compressedData); const iendChunk = createChunk("IEND", Buffer.alloc(0)); const pngData = Buffer.concat([pngSignature, ihdrChunk, idatChunk, iendChunk]); fs.writeFileSync("./output.png", pngData); console.log("PNG file written: output.png");
I hope this helps! π
No comments yet.
No comments yet.