Load images one-by-one using Promises and async-await in JavaScript - javascript

Desired result: I want that the images (image and image2) should load one-by-one. Following is my code:
'use strict';
window.addEventListener('DOMContentLoaded', () => {
const cvs = document.querySelector('#cvs');
cvs.width = window.innerWidth;
cvs.height = window.innerHeight;
const c = cvs.getContext('2d');
let image = new Image(); // first image
image.src = 'https://images5.alphacoders.com/559/559956.jpg';
let image2 = new Image(); // second image
image2.src = 'https://www.highreshdwallpapers.com/wp-content/uploads/2013/03/Avengers-A.jpg';
let y = 0;
let loader = img => new Promise(resolve => img.onload = resolve(img, 0, y, 100, 60));
let logger = img => new Promise(resolve => img.onload = resolve(img.src + ' loaded!'));
async function loadImages() {
await logger(image).then(console.log); // this works
await logger(image2).then(console.log); // this works
await loader(image).then(c.drawImage);
y += 60;
await loader(image2).then(c.drawImage);
};
loadImages();
});
body {
margin: 0;
}
#cvs {
position: fixed;
}
<!DOCTYPE html>
<html>
<head>
<title>Page Title</title>
<link rel="stylesheet" type="text/css" href="./style.css">
</head>
<body>
<canvas id='cvs'>Canvas not supported</canvas>
<script type="text/javascript" src='./script.js'></script>
</body>
</html>
Here's my concept:
I create a function that returns a new Promise which resolves when the image loads and then draws it on the canvas context:
let loader = img => new Promise(resolve => img.onload = resolve(img, 0, height, 100, 60));
Then, I create an asynchronous function that calls the loader for every image and draws it on canvas:
async function loadImages() {
// ...
await loader(image).then(c.drawImage);
y += 60;
await loader(image2).then(c.drawImage);
};
But, for some reasons, the code isn't working. And, I get the following error: Uncaught (in promise) TypeError: Illegal invocation.
I've tried replacing c.drawImage with:
c.drawImage.bind(c), as recommended in this post and,
c.drawImage.call, in this case, I've changed my resolve as resolve(c, img, 0, height, 100, 60)
But, neither of them worked out!
What I am doing wrong?

Your loadImages returns undefined (since you don't return anything explicitly), so drawImage won't do anything.
Even though it's not entirely clear what you wish to do, I have the feeling that you wanted to draw both images in this then().
In order to do as little modifications in your code as possible, you could just return both images in an Array, then in the then iterate over this array and call drawImage on it.
You'd also have to pass all your arguments as an array in the loader function.
Also, note that you had a typo in img.onload = resolve(.., it should be img.onload = e => resolve(..., otherwise resolve gets called directly.
And also remember that the onload event will fire only once, so once you awaited for it, it on't happen again (unless you force it to).
window.addEventListener('DOMContentLoaded', () => {
const cvs = document.querySelector('#cvs');
cvs.width = window.innerWidth;
cvs.height = window.innerHeight;
const c = cvs.getContext('2d');
let image = new Image(); // first image
image.src = 'https://images5.alphacoders.com/559/559956.jpg';
let image2 = new Image(); // second image
image2.src = 'https://www.highreshdwallpapers.com/wp-content/uploads/2013/03/Avengers-A.jpg';
let height = 0;
let loader = img => new Promise(resolve => {
// resolve the arguments as an Array
img.onload = e => resolve([img, 0, height, 100, 60]);
// force resetting the src, otherwise onload may already have fired
img.src = img.src;
});
async function loadImages() {
const a = await loader(image);
height += 60;
const b = await loader(image2);
// you must return something if you it to be passed in the then()
return [a, b];
};
loadImages().then(arr => {
arr.forEach(args => c.drawImage.apply(c, args));
}).catch(console.error);
});
<canvas id='cvs'>Canvas not supported</canvas>
But if I may, your code is far from being clear... You'd probably win by being more verbose, and split your logic differently:
First deal with loading the assets, then deal with your rendering objects (here the arguments you pass to drawImage).
Mixing both will just make your code harder to maintain.
Also, your variable named height would probably be better called y.

Related

How to create video srcObject from VideoFrame?

I'm learning webcodecs now, and I saw things as below:
So I wonder maybe it can play video on video element with several pictures. I tried many times but it still can't work.
I create videoFrame from pictures, and then use MediaStreamTrackGenerator to creates a media track. But the video appears black when call play().
Here is my code:
const test = async () => {
const imgSrcList = [
'https://gw.alicdn.com/imgextra/i4/O1CN01CeTlwJ1Pji9Pu6KW6_!!6000000001877-2-tps-62-66.png',
'https://gw.alicdn.com/imgextra/i3/O1CN01h7tWZr1ZiTEk1K02I_!!6000000003228-2-tps-62-66.png',
'https://gw.alicdn.com/imgextra/i4/O1CN01CSwWiA1xflg5TnI9b_!!6000000006471-2-tps-62-66.png',
];
const imgEleList: HTMLImageElement[] = [];
await Promise.all(
imgSrcList.map((src, index) => {
return new Promise((resolve) => {
let img = new Image();
img.src = src;
img.crossOrigin = 'anonymous';
img.onload = () => {
imgEleList[index] = img;
resolve(true);
};
});
}),
);
const trackGenerator = new MediaStreamTrackGenerator({ kind: 'video' });
const writer = trackGenerator.writable.getWriter();
await writer.ready;
for (let i = 0; i < imgEleList.length; i++) {
const frame = new VideoFrame(imgEleList[i], {
duration: 500,
timestamp: i * 500,
alpha: 'keep',
});
await writer.write(frame);
frame.close();
}
// Call ready again to ensure that all chunks are written before closing the writer.
await writer.ready.then(() => {
writer.close();
});
const stream = new MediaStream();
stream.addTrack(trackGenerator);
const videoEle = document.getElementById('video') as HTMLVideoElement;
videoEle.onloadedmetadata = () => {
videoEle.play();
};
videoEle.srcObject = stream;
};
Thanks!
Disclaimer:
I am not an expert in this field and it's my first use of this API in this way. The specs and the current implementation don't seem to match, and it's very likely that things will change in the near future. So take this answer with all the salt you can, it is only backed by trial.
There are a few things that seems wrong in your implementation:
duration and timestamp are set in micro-seconds, that's 1/1,000,000s. Your 500 duration is then only half a millisecond, that would be something like 2000FPS and your three images would get all displayed in 1.5ms. You will want to change that.
In current Chrome's implementation, you need to specify the displayWidth and displayHeight members of the VideoFrameInit dictionary (though if I read the specs correctly that should have defaulted to the source image's width and height).
Then there is something I'm less sure about, but it seems that you can't batch-write many frames. It seems that the timestamp field is kind of useless in this case (even though it's required to be there, even with nonsensical values). Once again, specs have changed so it's hard to know if it's an implementation bug, or if it's supposed to work like that, but anyway it is how it is (unless I too missed something).
So to workaround that limitation you'll need to write periodically to the stream and append the frames when you want them to appear.
Here is one example of this, trying to keep it close to your own implementation by writing a new frame to the WritableStream when we want it to be presented.
const test = async() => {
const imgSrcList = [
'https://gw.alicdn.com/imgextra/i4/O1CN01CeTlwJ1Pji9Pu6KW6_!!6000000001877-2-tps-62-66.png',
'https://gw.alicdn.com/imgextra/i3/O1CN01h7tWZr1ZiTEk1K02I_!!6000000003228-2-tps-62-66.png',
'https://gw.alicdn.com/imgextra/i4/O1CN01CSwWiA1xflg5TnI9b_!!6000000006471-2-tps-62-66.png',
];
// rewrote this part to use ImageBitmaps,
// using HTMLImageElement works too
// but it's less efficient
const imgEleList = await Promise.all(
imgSrcList.map((src) => fetch(src)
.then(resp => resp.ok && resp.blob())
.then(createImageBitmap)
)
);
const trackGenerator = new MediaStreamTrackGenerator({
kind: 'video'
});
const duration = 1000 * 1000; // in µs (1/1,000,000s)
let i = 0;
const presentFrame = async() => {
i++;
const writer = trackGenerator.writable.getWriter();
const img = imgEleList[i % imgEleList.length];
await writer.ready;
const frame = new VideoFrame(img, {
duration, // value doesn't mean much, but required
timestamp: i * duration, // ditto
alpha: 'keep',
displayWidth: img.width * 2, // required
displayHeight: img.height * 2, // required
});
await writer.write(frame);
frame.close();
await writer.ready;
// unlock our Writable so we can write again at next frame
writer.releaseLock();
setTimeout(presentFrame, duration / 1000);
}
presentFrame();
const stream = new MediaStream();
stream.addTrack(trackGenerator);
const videoEle = document.getElementById('video');
videoEle.srcObject = stream;
};
test().catch(console.error)
<video id=video controls autoplay muted></video>

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.

canvas.toBlob giving null sometimes

I am setting up a canvas and then drawing an image into that canvas, followed by calling canvas.toBlob(function(blob)...), but I am finding the blob argument inside the toBlob is sometimes null.
Why would this be? Should I be waiting for something after drawImage or something (even though in the snippet - you can see I wait for the image to be loaded before proceeding)?
//--------------------------------------------------
function doImageFileInsert(fileinput) {
var newImg = document.createElement('img');
var img = fileinput.files[0];
let reader = new FileReader();
reader.onload = (e) => {
let base64 = e.target.result;
newImg.src = base64;
doTest(newImg);
};
reader.readAsDataURL(img);
fileinput.value = ''; // reset ready for another file
}
//--------------------------------------------------
function doTest(imgElem) {
console.log('doTest');
var canvas = document.createElement("canvas");
var w = imgElem.width;
var h = imgElem.height;
canvas.width = w;
canvas.height = h;
var ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(imgElem, 0, 0, w, h);
canvas.toBlob(function(blob) {
if (blob) {
console.log('blob is good');
} else {
console.log('blob is null');
alert('blob is null');
}
}, 'image/jpeg', 0.9);
}
canvas, div {
border:solid 1px grey;
padding:10px;
margin:5px;
border-radius:9px;
}
img {
width:100%;
height:auto;
}
<input type='file' value='Add' onChange='doImageFileInsert(this)'>
Also available at https://jsfiddle.net/Abeeee/rtwcge5h/24/.
If you add images via the Choose-File button enough times then you get the problem (alert('blob is null')).
There are only a few reasons why toBlob would produce null:
A bug in the browser's encoder (never seen it myself).
A canvas whose area is bigger than the maximum supported by the UA.
A canvas whose width or height is 0.
Since you are not waiting for the image to load, its width and height properties are still 0, and you fall in the third bullet above since you do set the canvas's size to these.
So to fix your error, wait for the image has loaded before doing anything with it.
Also, note that you should almost never use FileReader.readAsDataURL(), and certainly not to display media files from a Blob, instead generate a blob:// URI from these Blobs using URL.createObjectURL().
But in your case, you can even use the better createImageBitmap API which will take care of loading the image in your Blob and will generate a memory friendly ImageBitmap object which is ready to be drawn by the GPU without any more computation.
Only Safari hasn't implemented the basics of this API yet, but they should soon (it's exposed behind flags, and unflagged in TP version), and I wrote a polyfill you can use to fix the holes in various implementations.
const input = document.querySelector("input");
input.oninput = async (evt) => {
const img = await createImageBitmap(input.files[0]);
const canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
// I hope you'll do something more here,
// reencoding an iamge to JPEG through the Canvas API is a bad idea
// Canvas encoders aim at speed, not quality
canvas.toBlob( (blob) => {
console.log( blob );
}, "image/jpeg", 0.9 );
};
<!-- createImageBitmap polyfill for Safari -->
<script src="https://cdn.jsdelivr.net/gh/Kaiido/createImageBitmap/dist/createImageBitmap.js"></script>
<input type="file" accept="image/*">
Okay, it looks like it was the caller to the canvas work that was the problem - and indeed the image had not been loaded fully by the time the drawImage (probably) was run.
The original call to doTest() was
function doImageFileInsert(fileinput) {
var contenteditable = document.getElementById('mydiv');
var newImg = document.createElement('img');
//contenteditable.appendChild(newImg);
var img = fileinput.files[0];
let reader = new FileReader();
reader.onload = (e) => {
let base64 = e.target.result;
newImg.src = base64;
doTest(newImg); <-----
};
reader.readAsDataURL(img);
}
but it was the call that was at fault
changing it to
function doImageFileInsert(fileinput) {
var contenteditable = document.getElementById('mydiv');
var newImg = document.createElement('img');
//contenteditable.appendChild(newImg);
var img = fileinput.files[0];
let reader = new FileReader();
reader.onload = (e) => {
let base64 = e.target.result;
newImg.src = base64;
//doTest(newImg);
newImg.onload = (e) => { doTest(newImg); }; <-----
};
reader.readAsDataURL(img);
}
seems to fix it. A working version can be seen in https://jsfiddle.net/Abeeee/rtwcge5h/25/
Here is a more modern approach if you wish to use it
/**
* #param {HTMLInputElement} fileInput
*/
async function doImageFileInsert (fileInput) {
const img = new Image()
img.src = URL.createObjectURL(fileInput.files[0])
await img.decode()
doTest(img)
}
or this that don't work in all browser (would require a polyfill)
/**
* #param {HTMLInputElement} fileInput
*/
function doImageFileInsert (fileInput) {
createImageBitmap(fileInput.files[0]).then(doTest)
}
The FileReader is such a legacy pattern now days when there is new promise based read methods on the blob itself and that you can also use URL.createObjectURL(file) instead of wasting time encoding a file to base64 url (only to be decoded back to binary again by the <img>) it's a waste of time, processing, and RAM.
img.decode()
Blob#instance_methods
createObjectURL

JavaScript all images loading before await

I have a simple JavaScript file that gets multiple image files from a local device and displays a thumb of the images. So far so good.
Once the thumbs are displayed I have a button that calls the addImages() function. I need to display a slide show of the images, one at a time, for 3 seconds. However, what is happening is all the images are showing at the same time, then the sleep function is called, and then remove the image. Here is my code:
function addImages() {
var preview = document.querySelector('#display');
async function readAndPreview(file) {
var reader = new FileReader();
reader.addEventListener("load", async function () {
var image = new Image();
image.height = 400;
image.width = 400;
image.title = file.name;
image.border = 5;
image.src = this.result;
preview.appendChild(image);
sleep(3000).then(()=> preview.removeChild(image))
}, false);
reader.readAsDataURL(file);
}
if(files) {
[].forEach.call(files, readAndPreview);
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
It looks like the code iterates through to appendChild and then starts again until all images are added and then calls the sleep functions and then removeChild. Why???
Any help will be appreciated. Thanks.
I have changed a lot of the implementation as proposed by you.
Major changes:-
Having one instance of file reader
Having single image for which we keep on changing the src url after designated time. (Came to this conclusion after I saw that you're trying to append and remove the same image element after certain duration). So yes I am assuming that the focus will always be one image at a time and so the below code.
let files // you must have an array initialized I think
let reader;
const preview = document.querySelector('#display');
let image = new Image();
image.height = 400;
image.width = 400;
image.title = file.name;
image.border = 5;
preview.appendChild(image);
function initReader(){
reader = new FileReader();
reader.addEventListener("load", function () {
image.src = this.result;
}, false);
}
function addImages() {
initReader();
async function readAndPreview() {
for (let index = 0;index<files.length;index++){
reader.readAsDataURL(files[index]);
await sleep(3000);
}
}
readAndPreview();
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
I haven't run this so there might be scope of mistakes. But this approach should work for your use-case.
Also there is scope of improvement in this approach for smooth transition from one image to another. Usually one can preload all images via blobs URL.createObjectURL and store them in an array over which we loop and update the image src as we did above. Will be much faster since the data is already at client side.

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