Load image from a Buffer/Blob - javascript

I am trying to take an image from the clipboard within electron, and put it on a canvas.
In the electron main process, I have this function which is triggered by a menu item. If I write img.toBitmap().toString() to the console it outputs the image data, so I know that there is an image there.
export function pasteImage() {
let img = clipboard.readImage()
console.log('send paste message')
mainWindow.webContents.send('paste-image', img.toBitmap())
}
Next I have this method which takes the buffer and converts it to a blob. It then should load it into the image and draw the image to the canvas.
public pasteImage(image: Buffer, x: number = 0, y: number = 0) {
let blob = new Blob([image], { type: 'image/bmp' })
let url = URL.createObjectURL(blob)
let img = new Image
img.addEventListener('load', () => {
console.log('loaded image')
this.ctx.drawImage(img, x, y)
URL.revokeObjectURL(url)
})
img.src = url
}
ipcRenderer.addListener('paste-image', (e: EventEmitter, img: Buffer) => {
let canvas = document.createElement('canvas')
let layer = new layer(canvas)
layer.pasteImage(img)
})
The issue I am having is that the load event never gets triggered, however the method pasteImage(image, x, y) is executing.

Related

Trying to get base64 png image into p5.js image

So as the title says, i'm trying to load a base64 encoded png image into a p5js image, here's a simplification of how im doing it:
PS: I'm using a base64 image because it's generated from a server
var img;
function setup() {
// Canvas setup code ....
img = loadImage('loading.png'); // show an image which says that it's loading
img.loadPixels();
// More setup code ...
}
// ...
function draw() {
image(img, 0, 0, img.width, img.height);
// ... more code
}
// ...
function receivedCallback(data) { // This function gets called once we receive the data
// Data looks like this: { width: 100, height: 100, data: "...base64 data...." }
img.resize(data.width, data.height);
var imgData = data.data;
var imgBlob = new Blob([imgData], { type: "image/png" }); // Create a blob
var urlCreator = window.URL || window.webkitURL;
var imageUrl = urlCreator.createObjectURL(imgBlob); // Craft the url
img.src = imageUrl;
img.loadPixels();
img.updatePixels();
}
But, it's not working which is why I am asking here.
If there is any way to do it I would appreciate it very much.
Thanks in advance.
EDIT
Steve's didn't quite work, I had to replace img.src = 'data:image/png;base64,' + data.data with img = loadImage('data:image/png;base64,' + data.data);
You can use Data URLs directly. It can look something like this:
function receivedCallback(data) { // This function gets called once we receive the data
// Data looks like this: { width: 100, height: 100, data: "...base64 data...." }
loadImage('data:image/png;base64,' + data.data, function (newImage) {
img = newImage;
});
}
If you need to use blobs for some reason, you can look into base64 to blob conversions, like this answer.
Steve's didn't quite work, I had to load the image instead of directly changing it's source.
So instead of img.src = 'data:image/png;base64,' + data.data.
I needed to do img = loadImage('data:image/png;base64,' + data.data);
Thanks to Steve for his answer though!
There's no difference between loading a base64 image and a regular image from a file in P5.js. Simply replace the URL in your loadImage call with the base64 data.
In the interests of a runnable, complete example (note that the base64 image is very small):
const imgData = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII";
let img;
function preload() {
img = loadImage(imgData);
}
function draw() {
image(img, 0, 0);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.5.0/p5.min.js"></script>
If your data is supposed to load at some indeterminate point in the future, say, after an interaction, then you can use a callback to determine when the image has loaded. This is somewhat use-case specific, but the pattern might look like:
const imgData = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII";
let img;
function setup() {
createCanvas(300, 300);
}
function draw() {
clear();
textSize(14);
text("click to load image", 10, 10);
if (img) {
image(img, 0, 0);
}
}
function mousePressed() {
if (!img) {
loadImage(imgData, imgResult => {
img = imgResult;
});
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.5.0/p5.min.js"></script>
If you have some async code in there, no problem. Just call loadImage whenever you have the base64 string ready, for example:
function mousePressed() {
if (!img) {
fetch("your endpoint")
.then(response => response.json())
.then(data => {
// prepend "data:image/png;base64," if
// `data.data` doesn't already have it
loadImage(data.data, imgResult => {
img = imgResult;
});
});
}
}
All this said, loading an image in response to a click is a somewhat odd pattern for most apps. Usually, you'd want to preload all of your asssets in the preload function so that you can render them immediately when requested. If you load via a fetch call in response to an event, there'll likely be an ugly-looking delay (or needing to show a temporary message/placeholder image/spinner) before rendering the image, so make sure this isn't an xy problem.

marge-images with canvas in Node.js

I'm trying to use the merge-images script (https://github.com/lukechilds/merge-images) to merge some images into a single one using nodejs.
I'm having trouble understanding what ecatly I'm supposed to provide in the then() method of mergeImages.
This is what I have so far:
const mergeImages = require('merge-images');
const { createCanvas, Canvas, Image } = require('canvas');
const width = 2880;
const height = 2880;
var canvas = createCanvas(width, height);
const ctx = canvas.getContext('2d');
var img = new Image();
img.onload = () => ctx.drawImage(img, 0, 0);
img.onerror = err => { throw err }
img.src = './result.png';
mergeImages([
'./lunas_parts/1.png', './lunas_parts/2.png', './lunas_parts/3.png',
], {
Canvas: Canvas,
Image: Image
})
.then(b64 => img = b64);
I do have an empty result.png image in the right location, as well as the 1, 2 and 3 .png files. The console isn't showing any error when I execute the above script, but result.png remains empty after the execution.
Is the canvas image source I'm using in then() not correct? What am I supposed to pass there exactly?
Thanks in advance for any help.

Reuse an Image with Canvas

I have the following code;
export default async function draw(elRef : RefObject<HTMLCanvasElement>, tileData : TileProps) {
const canvas = elRef.current!;
const ctx = canvas.getContext('2d')!;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
tileData.forEach(async tile => {
const image = await loadImage(tile.tileType);
ctx.drawImage(image, 0, 0, TILE_WIDTH, TILE_HEIGHT, tile.coordinates.x, tile.coordinates.y, TILE_WIDTH, TILE_HEIGHT);
})
}
Every time draw is called with an array of image src's to render onto the canvas, loadImage is called;
const loadImage = (tileType : string) : Promise<HTMLImageElement> => {
return new Promise((resolve, reject) => {
const image = new Image();
image.addEventListener('load', () => resolve(image));
image.addEventListener('error', reject);
image.src = "./media/images/tiles/Dog.png";
})
}
However, since draw will be constantly called, it's going to call loadImage repeatedly.
How can I save an image so I won't need to consistantly load it everytime I want to place it onto a canvas?
You can make your loadImage memoized, so you do not request new image if it is exactly same image, you can do this by making an in memory cache of the parameters you pass to the loadImage and return from it if they are same parameters.
Is your tileData ever changing? If not you do not even need to call the loadImage inside the draw.

Is there a way to get binary data from an image file without using canvas API?

I want to access and manipulate an image's binary data using javascript. As far as I could research, it can be done using Canvas API - creating a canvas and a context in memory, and then using their built-in methods. What I've come up looks like this:
function fileToBase64(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(<string>reader.result);
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(file);
});
}
function base64ToImageData(base64: string): Promise<ImageData> {
return new Promise((resolve, reject) => {
const img = new Image();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
img.onload = () => {
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
resolve(imageData);
}
img.onerror = () => reject();
img.src = base64;
});
}
And what I don't like about this approach is that it is verbose and probably uses more resources than it should.
So the question is: Is there a way to get a binary representation of an image without using canvas API?
you could construct a ImageData manually but for that you need a Uint8ClampedArray. For that you would also have to decode a binary file to pixel data yourself. So you probably want to use canvas or offscreanCanvas for that anyway
but there are some tools to ease the way to get imageData from blob using createImageBitmap
This imageBitmap contains the pixel data but you are not able to read it, but at least it will be faster to paint it to a canvas element.
(async () => {
// just to simulate a file you would get from file input
const res = await fetch('https://httpbin.org/image/png')
const blob = await res.blob()
const file = new File([blob], 'image.png', blob)
async function getImageData (blob) {
const bitmap = await createImageBitmap(blob)
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = bitmap.width
canvas.height = bitmap.height
ctx.drawImage(bitmap, 0, 0)
return ctx.getImageData(0, 0, canvas.width, canvas.height)
}
getImageData(blob).then(imageData => console.log(imageData.width))
})()
but you might not use createImageBitmap due to compatibility issues, so then i suggest that you instead of creating a image from file input -> base64 -> image with the FileReader that you instead go from file to image using a pointer references with object urls
const img = new Image()
img.src = URL.createObjectURL(file)
this will use less resources cuz you don't need to encode it to base64 and decode it back to binary again, it will use less memory since it will just load the image from your disk instead directly

Is it possible to create a thumbnail with the FileReader API?

I've got the proof-of-concept I need for loading in multiple images with FileReader.readAsDataURL().
However, my clients' workflow is such that they load in hundreds of images at a time which crashes the browser.
Is there any way to load in these images in as actual thumbnails (16k vs 16Mb)?
First, don't use a FileReader, at all.
Instead, to display any data from a Blob, use the URL.createObjectURL method.
FileReader will load the binary data thrice in memory (one when reading the Blob for conversion, one as Base64 String and one when passed as
the src of your HTMLElement.)
In case of a file stored on the user-disk, a blobURL will load the data only once in memory, from the HTMLElement. A blobURL is indeed a direct pointer to the Blob's data.
So this will already free a lot of memory for you.
inp.onchange = e => {
for(let file of inp.files) {
let url = URL.createObjectURL(file);
let img = new Image(200);
img.src = url;
document.body.appendChild(img);
}
};
<input type="file" webkitdirectory accepts="image/*" id="inp">
Now, if it is not enough, to generate thumbnails, you could draw all these images on canvases and reduce their size if needed.
But keep in mind that to be able to do so, you'd have to first load the original image's data anyway, and that you cannot be sure how will browser cleanup this used memory. So might do more harm than anything by creating these thumbnail versions.
Anyway, here is a basic example of how it could get implemented:
inp.onchange = e => {
Promise.all([...inp.files].map(toThumbnail))
.then(imgs => document.body.append.apply(document.body, imgs.filter(v => v)))
.catch(console.error);
};
function toThumbnail(file) {
return loadImage(file)
.then(drawToCanvas)
.catch(_ => {});
}
function loadImage(file) {
const url = URL.createObjectURL(file);
const img = new Image();
return new Promise((res, rej) => {
img.onload = e => res(img);
img.onerror = rej;
img.src = url;
});
};
function drawToCanvas(img) {
const w = Math.min(img.naturalWidth, 200);
const r = w / img.naturalWidth;
const h = img.naturalHeight * r;
const canvas = Object.assign(
document.createElement('canvas'), {
width: w,
height: h
}
);
canvas.getContext('2d').drawImage(img, 0, 0, w, h);
return canvas;
}
<input type="file" webkitdirectory accepts="image/*" id="inp">

Categories