How to save binary buffer to png file in nodejs? - javascript

I have binary nodejs Buffer object that contains bitmap information. How do make image from the buffer and save it to file?
Edit:
I tried using the file system package as #herchu said but if I do this:
let robot = require("robotjs")
let fs = require('fs')
let size = 200
let img = robot.screen.capture(0, 0, size, size)
let path = 'myfile.png'
let buffer = img.image
fs.open(path, 'w', function (err, fd) {
if (err) {
// Something wrong creating the file
}
fs.write(fd, buffer, 0, buffer.length, null, function (err) {
// Something wrong writing contents!
})
})
I get

Although solutions by #herchu and #Jake work, they are extremely slow (10-15s in my experience).
Jimp supports converting Raw Pixel Buffer into PNG out-of-the-box and works a lot faster (sub-second).
const img = robot.screen.capture(0, 0, width, height).image;
new Jimp({data: img, width, height}, (err, image) => {
image.write(fileName);
});

Note: I am editing my answer according to your last edits
If you are using Robotjs, check that its Bitmap object contains a Buffer to raw pixels data -- not a PNG or any other file format contents, just pixels next to each other (exactly 200 x 200 elements in your case).
I have not found any function to write contents in other format in the Robotjs library (not that I know it either), so in this answer I am using a different library, Jimp, for the image manipulation.
let robot = require("robotjs")
let fs = require('fs')
let Jimp = require('jimp')
let size = 200
let rimg = robot.screen.capture(0, 0, size, size)
let path = 'myfile.png'
// Create a new blank image, same size as Robotjs' one
let jimg = new Jimp(size, size);
for (var x=0; x<size; x++) {
for (var y=0; y<size; y++) {
// hex is a string, rrggbb format
var hex = rimg.colorAt(x, y);
// Jimp expects an Int, with RGBA data,
// so add FF as 'full opaque' to RGB color
var num = parseInt(hex+"ff", 16)
// Set pixel manually
jimg.setPixelColor(num, x, y);
}
}
jimg.write(path)
Note that the conversion is done by manually iterating through all pixels; this is slow in JS. Also there are some details on how each library handles their pixel format, so some manipulation was needed in the loop -- it should be clear from the embedded comments.

Adding this as an addendum to accepted answer from #herchu, this code sample processes/converts the raw bytes much more quickly (< 1s for me for a full screen). Hope this is helpful to someone.
var jimg = new Jimp(width, height);
for (var x=0; x<width; x++) {
for (var y=0; y<height; y++) {
var index = (y * rimg.byteWidth) + (x * rimg.bytesPerPixel);
var r = rimg.image[index];
var g = rimg.image[index+1];
var b = rimg.image[index+2];
var num = (r*256) + (g*256*256) + (b*256*256*256) + 255;
jimg.setPixelColor(num, x, y);
}
}

Four times faster!
About 280ms and 550Kb for full screen 1920x1080, if use this script.
I found this pattern when I compared 2 byte threads per byte to the forehead.
const robotjs = require('robotjs');
const Jimp = require('jimp');
const app = require('express').Router();
app.get('/screenCapture', (req, res)=>{
let image = robotjs.screen.capture();
for(let i=0; i < image.image.length; i++){
if(i%4 == 0){
[image.image[i], image.image[i+2]] = [image.image[i+2], image.image[i]];
}
}
var jimg = new Jimp(image.width, image.height);
jimg.bitmap.data = image.image;
jimg.getBuffer(Jimp.MIME_PNG, (err, result)=>{
res.set('Content-Type', Jimp.MIME_PNG);
res.send(result);
});
});
If you add this code before jimp.getBuffer you'll get about 210ms and 320Kb for full screen
jimg.rgba(true);
jimg.filterType(1);
jimg.deflateLevel(5);
jimg.deflateStrategy(1);

I suggest you to take a look on sharp as it has superior performance metrics over jimp.
The issue with robotjs screen capturing, which actually happened to be very efficient, is BGRA color model and not RGBA. So you would need to do additional color rotation.
Also, as we take screenshot from the desktop I can't imagine the case where we would need transperency. So, I suggest to ignore it.
const [left, top, width, height] = [0, 0, 100, 100]
const channels = 3
const {image, width: cWidth, height: cHeight, bytesPerPixel, byteWidth} = robot.screen.capture(left, right, width, height)
const uint8array = new Uint8Array(cWidth*cHeight*channels);
for(let h=0; h<cHeight; h+=1) {
for(let w=0; w<cWidth; w+=1) {
let offset = (h*cWidth + w)*channels
let offset2 = byteWidth*h + w*bytesPerPixel
uint8array[offset] = image.readUInt8(offset2 + 2)
uint8array[offset + 1] = image.readUInt8(offset2 + 1)
uint8array[offset + 2] = image.readUInt8(offset2 + 0)
}
}
await sharp(Buffer.from(uint8array), {
raw: {
width: cWidth,
height: cHeight,
channels,
}
}).toFile('capture.png')
I use intermediate array here, but you actually can just to swap in the result of the screen capture.

Related

How do I insert my image data from array into canvas so that I can download it?

I'm making a web app for drawing pixel art. The app itself works, I can choose canvas size, colors and tools to draw. Now I want to generate an image from it.
My canvas is a grid of divs that change background color when you draw on them. I made a function that creates a 2D array with each item having 4 values (red, green, blue, alpha).
Here's a function I use:
function rgb2pngStart(val) {
let finish = val.length - 1;
let sliced = val.slice(4, finish)
let stringAr = sliced.split(', ');
let R = parseInt(stringAr[0]);
let G = parseInt(stringAr[1]);
let B = parseInt(stringAr[2]);
let pixelArray = [R, G, B, 255];
return pixelArray;
}
function createArray() {
let sideInPixels = document.documentElement.style.getPropertyValue('--number');
let canvasX = [];
let canvasY = [];
for (let i = 1; i <= sideInPixels; i++) {
for (let j = 1; j <= sideInPixels; j++) {
let pixel = document.getElementById(i + ' ' + j).style.backgroundColor;
canvasX[j - 1] = rgb2pngStart(pixel);
}
canvasY[i - 1] = canvasX;
}
return canvasY;
}
I was trying to create a canvas, using some old answers here, and put my data into imagedata and then show that image to user so that he could download it. But I don't know how do I do that...
So, how do I generate image from this array? Or what should I change for it to work?
The most convenient approach would be to use an actual <canvas> element: you can create ImageData from your RGBA pixel information and put it to the canvas context — and then generate a data URL or a blob, which can be used to create an object URL (both of which can be used as download targets).

Pica: cannot use getImageData on canvas, make sure fingerprinting protection isn't enabled

I am getting this error while using npm pica library. I want to resize image using this library. Have tried two ways
I. by passing image url directly to pica.resize(imageurl, canvas) and
II. by passing image buffer to pica.resize(imageBuffer,canvas). But it shows error Pica: cannot use getImageData on canvas, make sure fingerprinting protection isn't enabled. when I run code.
// Convert Base64 to BufferArray
const buf = Buffer.from(img, 'base64');
let binary = '';
const chunk = 8 * 1024;
let i;
for (i = 0; i < buf.length / chunk; i += 1) {
binary += String.fromCharCode.apply(null, [
...buf.slice(i * chunk, (i + 1) * chunk),
]);
}
binary += String.fromCharCode.apply(null, [...buf.slice(i * chunk)]);
// Dimensions for the image
const width = 1200;
const height = 627;
// Instantiate the canvas object
const can = canvas.createCanvas(width,height);
// const context = canvas.getContext("2d");
console.log(binary)
pica.resize(binary, can)
.then(result => console.log(result));

JS OffscreenCanvas.toDataURL

HTMLCanvasElement has toDataURL(), but OffscreenCanvas does not have. What a surprise.
Ok, so how can i get this toDataURL() to work with Worker-s? I have a ready canvas (fully drawn), and can send it to a Worker. But then what can i do from there?
The only solution i have, is to manually do all operations to create an image/png. So i found this page from 2010. (I am not sure if that is what i need though.) And further it provides this code, from where it generates a PNG and makes it to base64.
And my final question:
1 - Is there some reasonable way to get toDataURL() from Worker, OR
2 - Is there any library or something designed to for this job, OR
3 - Using all functionalities of HTMLCanvasElement and OffscreenCanvas, how should the following code be adapted to replace toDataURL()?
Here are the two functions from the code im linking to. (They are really complicated for me, and i understand almost nothing from getDump())
// output a PNG string
this.getDump = function() {
// compute adler32 of output pixels + row filter bytes
var BASE = 65521; /* largest prime smaller than 65536 */
var NMAX = 5552; /* NMAX is the largest n such that 255n(n+1)/2 + (n+1)(BASE-1) <= 2^32-1 */
var s1 = 1;
var s2 = 0;
var n = NMAX;
for (var y = 0; y < this.height; y++) {
for (var x = -1; x < this.width; x++) {
s1+= this.buffer[this.index(x, y)].charCodeAt(0);
s2+= s1;
if ((n-= 1) == 0) {
s1%= BASE;
s2%= BASE;
n = NMAX;
}
}
}
s1%= BASE;
s2%= BASE;
write(this.buffer, this.idat_offs + this.idat_size - 8, byte4((s2 << 16) | s1));
// compute crc32 of the PNG chunks
function crc32(png, offs, size) {
var crc = -1;
for (var i = 4; i < size-4; i += 1) {
crc = _crc32[(crc ^ png[offs+i].charCodeAt(0)) & 0xff] ^ ((crc >> 8) & 0x00ffffff);
}
write(png, offs+size-4, byte4(crc ^ -1));
}
crc32(this.buffer, this.ihdr_offs, this.ihdr_size);
crc32(this.buffer, this.plte_offs, this.plte_size);
crc32(this.buffer, this.trns_offs, this.trns_size);
crc32(this.buffer, this.idat_offs, this.idat_size);
crc32(this.buffer, this.iend_offs, this.iend_size);
// convert PNG to string
return "\211PNG\r\n\032\n"+this.buffer.join('');
}
Here it is quite clear what is going on:
// output a PNG string, Base64 encoded
this.getBase64 = function() {
var s = this.getDump();
// If the current browser supports the Base64 encoding
// function, then offload the that to the browser as it
// will be done in native code.
if ((typeof window.btoa !== 'undefined') && (window.btoa !== null)) {
return window.btoa(s);
}
var ch = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
var c1, c2, c3, e1, e2, e3, e4;
var l = s.length;
var i = 0;
var r = "";
do {
c1 = s.charCodeAt(i);
e1 = c1 >> 2;
c2 = s.charCodeAt(i+1);
e2 = ((c1 & 3) << 4) | (c2 >> 4);
c3 = s.charCodeAt(i+2);
if (l < i+2) { e3 = 64; } else { e3 = ((c2 & 0xf) << 2) | (c3 >> 6); }
if (l < i+3) { e4 = 64; } else { e4 = c3 & 0x3f; }
r+= ch.charAt(e1) + ch.charAt(e2) + ch.charAt(e3) + ch.charAt(e4);
} while ((i+= 3) < l);
return r;
}
Thanks
First, I'll note you probably don't want a data URL of your image file, data URLs are really a less performant way to deal with files than their binary equivalent Blob, and almost you can do with a data URL can actually and should generally be done with a Blob and a Blob URI instead.
Now that's been said, you can very well still generate a data URL from an OffscreenCanvas.
This is a two step process:
call its convertToBlob method
read the generated Blob as a dataURL using a FileReader[Sync]
const worker = new Worker(getWorkerURL());
worker.onmessage = e => console.log(e.data);
function getWorkerURL() {
return URL.createObjectURL(
new Blob([worker_script.textContent])
);
}
<script id="worker_script" type="ws">
const canvas = new OffscreenCanvas(150,150);
const ctx = canvas.getContext('webgl');
canvas[
canvas.convertToBlob
? 'convertToBlob' // specs
: 'toBlob' // current Firefox
]()
.then(blob => {
const dataURL = new FileReaderSync().readAsDataURL(blob);
postMessage(dataURL);
});
</script>
Since what you want is actually to render what this OffscreenCanvas did produce, you'd be better to generate your OffscreenCanvas by transferring the control of a visible one.
This way you can send the ImageBitmap directly to the UI without any memory overhead.
const offcanvas = document.getElementById('canvas')
.transferControlToOffscreen();
const worker = new Worker(getWorkerURL());
worker.postMessage({canvas: offcanvas}, [offcanvas]);
function getWorkerURL() {
return URL.createObjectURL(
new Blob([worker_script.textContent])
);
}
<canvas id="canvas"></canvas>
<script id="worker_script" type="ws">
onmessage = e => {
const canvas = e.data.canvas;
const gl = canvas.getContext('webgl');
gl.viewport(0, 0,
gl.drawingBufferWidth, gl.drawingBufferHeight);
gl.enable(gl.SCISSOR_TEST);
// make some slow noise (we're in a Worker)
for(let y=0; y<gl.drawingBufferHeight; y++) {
for(let x=0; x<gl.drawingBufferWidth; x++) {
gl.scissor(x, y, 1, 1);
gl.clearColor(Math.random(), Math.random(), Math.random(), 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
}
}
// draw to visible <canvas> in FF
if(gl.commit) gl.commit();
};
</script>
If you really absolutely need an <img>, then create a BlobURI from the generated Blob. But note that doing so, you do keep the image in memory once (which is still far better than the thrice induced by data URL, but still, don't do this with animated content).
const worker = new Worker(getWorkerURL());
worker.onmessage = e => {
document.getElementById('img').src = e.data;
}
function getWorkerURL() {
return URL.createObjectURL(
new Blob([worker_script.textContent])
);
}
img.onerror = e => {
document.body.textContent = '';
const a = document.createElement('a');
a.href = "https://jsfiddle.net/5yhg2c9L/";
a.textContent = "Your browser doesn't like StackSnippet's null origined iframe, please try again from this jsfiddle";
document.body.append(a);
};
<img id="img">
<script id="worker_script" type="ws">
const canvas = new OffscreenCanvas(150,150);
const gl = canvas.getContext('webgl');
gl.viewport(0, 0,
gl.drawingBufferWidth, gl.drawingBufferHeight);
gl.enable(gl.SCISSOR_TEST);
// make some slow noise (we're in a Worker)
for(let y=0; y<gl.drawingBufferHeight; y++) {
for(let x=0; x<gl.drawingBufferWidth; x++) {
gl.scissor(x, y, 1, 1);
gl.clearColor(Math.random(), Math.random(), Math.random(), 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
}
}
canvas[
canvas.convertToBlob
? 'convertToBlob' // specs
: 'toBlob' // current Firefox
]()
.then(blob => {
const blobURL = URL.createObjectURL(blob);
postMessage(blobURL);
});
</script>
(Note that you could also transfer an ImageBitmap from the Worker to the main thread and then draw it on a visible canvas, but in this case, using a tranferred context is even better.)
Blob is great, but actually toBlob && convertToBlob are much much slower compared to toDataUrl on Chrome, I really dont understand why....
On chrome, a 1920*1080 canvas, toDataUrl toke me 20ms, toBlob toke 300-500ms, convertToBlob toke 800ms.
So if the performance is an issue, I would rather use toDataUrl in main thread.

How to quickly find all colors in an image

I need to analyze an image to find all colors in a PNG or GIF image. I currently load the image to a canvas, then get the image data, then loop through every pixel and check it against each color in the palette. It takes forever, the browser thinks the script has stopped and it sometimes just crashes. Hoping there is a better way.
//load file
var fileReader = new FileReader();
fileReader.onload = function(e) {
var img = new Image();
img.onload = function() {
//create a new pixel with the images dimentions
newPixel(this.width, this.height, []);
//draw the image onto the canvas
context.drawImage(img, 0, 0);
var colorPalette = [];
var imagePixelData = context.getImageData(0,0,this.width, this.height).data;
console.log(imagePixelData)
for (var i = 0; i < imagePixelData.length; i += 4) {
var color = rgbToHex(imagePixelData[i],imagePixelData[i + 1],imagePixelData[i + 2]);
if (colorPalette.indexOf(color) == -1) {
colorPalette.push(color);
//don't allow more than 256 colors to be added
if (colorPalette.length >= settings.maxColorsOnImportedImage) {
alert('The image loaded seems to have more than '+settings.maxColorsOnImportedImage+' colors.')
break;
}
}
}
createColorPalette(colorPalette, false);
//track google event
ga('send', 'event', 'Pixel Editor Load', colorPalette.length, this.width+'/'+this.height); /*global ga*/
};
img.src = e.target.result;
};
fileReader.readAsDataURL(this.files[0]);
This will speed things up a little.
The function indexof will search all of the array if it can not find an entry. This can be very slow, you can improve the speed of the search by adding to the top of the array moving down then searching for the index from the top most entry. This ensures that the search is only over added entries not the entire array.
Also use 32Bit words rather than 8bit bytes, use typed arrays as they are significantly quicker that pushing onto a array and don't convert to hex inside the loop it is redundant and can be done after on the smaller data set.
See comments for logic.
//draw the image onto the canvas
ctx.drawImage(image, 0, 0);
// get size
const size = image.width * image.height;
// create pallete buffer
const colors32 = new Uint32Array(256);
// current pos of palette entry
var palettePos = colors32.length - 1; // Start from top as this will speed up indexOf function
// pixel data as 32 bit words so you can handle a pixel at a time rather than bytes.
const imgData = new Uint32Array(ctx.getImageData(0, 0, image.width, image.height).data.buffer);;
// hold the color. If the images are low colour (less 256) it is highly probable that many pixels will
// be the same as the previous. You can avoid the index search if this is the case.
var color= colors32[palettePos --] = imgData[0]; // assign first pixels to ease logic in loop
for (var i = 1; i < size && palettePos >= 0; i += 1) { // loop till al pixels read if palette full
if(color !== imgData[i]){ // is different than previouse
if (colors32.indexOf(imgData[i], palettePos) === -1) { // is in the pallet
color = colors32[palettePos --] = imgData[i]; // add it
}
}
}
// all the performance issues are over so now convert to the palette format you wanted.
const colorPalette = [];
colors32.reverse();
const paletteSize = (255 - palettePos) * 4;
const colors8 = new Uint8Array(colors32.buffer);
for(i = 0; i < paletteSize; i += 4){
colorPalette.push(rgbToHex(colors8[i],colors8[i + 1],colors8[i + 2]));
}

HTML Canvas putImageData causing frame overlap

I am trying to read pixels (frame by frame) from websocket and render it on to canvas (in grayscale). Data is being displayed properly, just that pixels of the current frame are displayed overlapped on previous frame. Thus picture gets smudged after few frames.
I am tying to clear out the previous frame before rendering current one. What would be the proper way to do it?
I am using putImageData() to render the frame. I have tried clearRect() before calling putImageData(). and have tried clearing Imgdata (Imgdata.data = [];) array before populating it again but none of these things worked.
` var canvas = document.querySelector("canvas");
var context = canvas.getContext("2d");
var Imgdata = context.createImageData(100,100);
var numPixel = Imgdata.data.length/4;
ws.onmessage = function (event) {
var bytes = new Uint8Array(event.data);
console.log("data: " + numPixel + " bytes: "+ bytes.length);
//Imgdata.data = [];
for (var i = 0; i < numPixel; i++) {
Imgdata.data[(i*4)+Math.round(bytes[i]/85)] = (bytes[i]%85)*3;
Imgdata.data[(i*4)+3] = 255;
Imgdata.data[(i*4)+0] = bytes[i];
Imgdata.data[(i*4)+1] = bytes[i];
Imgdata.data[(i*4)+2] = bytes[i];
Imgdata.data[(i*4)+3] = 255;
}
//context.clearRect(0, 0, canvas.width, canvas.height);
context.putImageData(Imgdata,0,0);
};
`
I would say the cause is most likely due to the first line inside the for loop, without knowing the data i dont know what it does but id does not look right.
// ???
Imgdata.data[(i*4)+Math.round(bytes[i]/85)] = (bytes[i]%85)*3;
If the data is random then this would randomly set one of the colour channels of each pixel.
Apart from that I can not see any errors that would cause what you describe.
I have rewritten the code only as a suggestion to improve the performance a little.
const canvas = document.querySelector("canvas");
const context = canvas.getContext("2d");
const imgdata = context.createImageData(100,100);
const pix = imgdata.data;
pix.fill(255); // set all to 255 To cover the alpha channel
const numPixel = pix.length; // best leave as byte count
ws.onmessage = function (event) {
const inData = event.data;
var inIndex = 0;
var i = 0;
while(i < numPixel){
pix[i ++] = pix[i ++] = pix[i ++] = inData[inIndex ++];
i++;
}
context.putImageData(imgdata,0,0);
};
That should work event if the incoming data is incomplete the function putImageData completely replaces the pixels. If any pixels are undefined from the socket data they will be set to zero and will show up as black.

Categories

Resources