Let's say I have an array that contains several image URLs
const imgs = [
'https://placehold.it/120x120&text=image1',
'https://placehold.it/120x120&text=image2',
'https://placehold.it/120x120&text=image3'
]
And I want to get those images as base 64 strings on the fly in preparation for a network request. What would I have to do to accomplish this using javascript?
I've currently tried:
const getDataUrl = (url) => {
return new Promise((resolve, reject) => {
let img = new Image()
let canvas = document.createElement('canvas')
img.setAttribute('crossOrigin', 'anonymous')
img.onload = function(){
canvas.height = img.height
canvas.width = img.width
canvas.getContext('2d').drawImage(img, 0, 0)
resolve(canvas.toDataUrl('png'))
}
img.src = url
})
}
let dataUrls = []
for(let img of imgs){
getDataUrl(img).then(res => {
dataUrls.push(res)
})
console.log(dataUrls)
}
But the promise does not wait for the image to load and resolve and I get an empty array
So I then tried a recursive solution:
let dataUrl = ''
const getDataUrl = (url) => {
let img = new Image()
let canvas = document.createElement('canvas')
img.setAttribute('crossOrigin', 'anonymous')
img.onload = function(){
canvas.height = img.height
canvas.width = img.width
canvas.getContext('2d').drawImage(img, 0, 0)
dataUrl = canvas.toDataUrl('png')
}
img.src = url
if(checkIntegrity()) return dataUrl
}
const checkIntegrity = () => {
if(dataUrl.length > 0){
return true
}else{
return checkIntegrity()
}
}
which is unsavory because I had to rely on dataUrl being in the global scope and didn't work anyway because I get a too much recursion error when I run this.
Finally, I thought I could try predefining the onLoad function and passing the resolve function as an argument to that:
const onLoad = (img, resolve) => {
let canvas = document.createElement('canvas')
canvas.height = img.height
canvas.width = img.width
canvas.getContext('2d').drawImage(img, 0, 0)
resolve(canvas.toDataURL('png'))
}
const getDataUrl = (url) => {
return new Promise((resolve, reject) => {
let img = new Image()
img.setAttribute('crossOrigin', 'anonymous')
img.onload = onLoad(img, resolve)
img.src = url
})
}
for(let img of imgs){
getDataUrl(img).then(res => {
console.log(res)
dataUrls.push(res)
})
}
console.log(dataUrls)
which was my most successfull attempt, but it ends up returning data:, from every call so it isn't working either. Here is a fiddle with that code: https://jsfiddle.net/5o4Lq3bh/34/
Barring this, I'm at my wit's end. I also tried manipulating dom mounted images instead of javascript image objects and using a counter with a loaded check and a different recursive function but I get the same error, too much recursion.
My main issue seems to be the iteration. I am pretty sure for / of is synchronous, so I'm guessing the issue is that I can't pass around resolve willy nilly to other functions and expect to get valid results.
This would be easy to do on load but it has to happen on the fly, any help is appreciated.
The first snippet and the third snippet you shared has some issues.
First snippet, your console.log will always print an empty array because it's outside the then statement.
for(let img of imgs){
getDataUrl(img).then(res => {
dataUrls.push(res)
})
console.log(dataUrls) // will always print [] because this statement should be within then function
}
Third snippet, you are calling onLoad function, instead of passing it as a event handler
img.onload = onLoad(img, resolve) // this will call onLoad, this is not an event handler
The below code works, but this doesn't utilize the full feature of promises
const getDataUrl = (url) => {
return new Promise((resolve, reject) => {
let img = new Image()
img.setAttribute('crossOrigin', 'anonymous')
img.onload = function(){
let canvas = document.createElement('canvas')
canvas.height = img.height
canvas.width = img.width
canvas.getContext('2d').drawImage(img, 0, 0)
let b64String = canvas.toDataURL('png');
resolve(b64String)
}
img.src = url
})
}
for(let img of imgs){
getDataUrl(img).then(res => {
dataUrls.push(res)
console.log(dataUrls.length)
})
}
If you want a much clear approach then use the below
const imgs = [
'https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/pie.png',
'https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/pie.png',
'https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/pie.png'
]
let dataUrls = []
let images = [];
for(let imgUrl of imgs){
images.push(new Promise((resolve, reject) => {
let img = new Image();
img.setAttribute('crossOrigin', 'anonymous')
img.onload = function(){
let canvas = document.createElement('canvas')
canvas.height = img.height
canvas.width = img.width
canvas.getContext('2d').drawImage(img, 0, 0)
let b64String = canvas.toDataURL('png');
dataUrls.push(b64String);
resolve(b64String)
}
img.src = imgUrl
}));
}
Promise.all(images).then(function(){
console.log(dataUrls)
})
https://jsfiddle.net/karthick6891/5o4Lq3bh/52/
Note: FYI, your fiddle didn;t work for me too, I had the same cross origin issue like everyone has posted.
Related
I am building a plugin for FIGMA, where the user selects multiple images, which I then save in an array, that I send to be interpreted.
I have 2 issues with my code
img.src = bin; does not trigger img.onload, but if I set img.src = "literal string", the onload method workds.
the imageData array sent at the end is undefined, I assume because of my bad understanding of async functions.
I would appreciate your help figuring this out. Thank you
P.S. this is pure javascript, and you don't need to know FIGMA to follow the code.
<input type="file" id="images" accept="image/png" multiple />
<button id="button">Create image</button>
<script>
const button = document.getElementById('button');
button.onclick = () => {
const files = document.getElementById('images').files;
function readFile(index) {
console.log(1);
if (index >= files.length) {return};
console.log(2);
const file = files[index];
const imageCaption = file.name;
var reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = function (e) {
console.log(4);
// get file content
const bin = e.target.result;
const imageBytes = new Uint8Array(bin);
//Get Image width and height
var img = new Image();
img.src = bin;
img.onload = function () {
console.log(6);
const width = img.width;
const height = img.height;
console.log("imageCaption: " + imageCaption);
console.log("width: " + width);
console.log("height: " + height);
console.log("imageBytes: " + imageBytes);
var data = {imageBytes, imageCaption, width, height};
//Read Next File
nextData = readFile(index + 1);
if( nextData ) {
data.concat(nextData)
}
return data;
}
}
}
//Call function to Read and Send Images
const imageData = readFile(0);
//Send Data
parent.postMessage({
pluginMessage: {
type: 'send-image',
imageData,
}
}, '*');
A friend ended up helping me with it. Thank you Hiba!
const button = document.getElementById('button');
const input = document.getElementById('input');
button.addEventListener('click', async () => {
const files = input.files ? [...input.files] : [];
const data = await Promise.all(
files.map(async (file) => await getImageData(file))
);
console.log(data);
});
async function getImageData(file) {
// get binary data from file
const bin = await file.arrayBuffer();
// translate bin data into bytes
const imageBytes = new Uint8Array(bin)
// create data blob from bytes
const blob = new Blob([imageBytes], { type: "image/png" });
// create html image element and assign blob as src
const img = new Image();
img.src = URL.createObjectURL(blob);
// get width and height from rendered image
await img.decode();
const { width, height } = img;
const data = { image: blob, caption: file.name, width, height };
return data;
}
I have the following code (snippet out of a larger program and within an async function):
foo = questionData.media ? (await new Promise(resolve => {
const image = new Image();
image.onload = function () {
const canvas = document.ceateElement("canvas");
canvas.width = this.width;
canvas.height = this.height;
canvas.drawImage(image, 0, 0, this.width, this.height);
resolve(canvas.toDataURL("image/jpg"));
};
image.src = "https://example.com/" + questionData.media;
})) : false;
I keep getting SyntaxError: Unexpected token 'new' on the first line, and I have no idea why. When I try to use parenthesis:
foo = questionData.media ? (await (new Promise(resolve => {
const image = new Image();
image.onload = function () {
const canvas = document.ceateElement("canvas");
canvas.width = this.width;
canvas.height = this.height;
canvas.drawImage(image, 0, 0, this.width, this.height);
resolve(canvas.toDataURL("image/jpg"));
};
image.src = "https://example.com/" + questionData.media;
}))) : false;
And I get Uncaught ReferenceError: await is not defined.
Meanwhile, I have this code that works just fine:
data.push(await new Promise(resolve => {
const req = new XMLHttpRequest();
req.onreadystatechange = function () {
if (this.readyState === 4 && this.status === 200) {
resolve(this.responseText);
}
};
req.open("GET", `https://example.com/${id}/answer`, true);
req.send();
}));
What am I missing?
It sounds like you aren't in an async function, so await does not get interpreted as a keyword, but as a variable name. If you put the entire initial snippet into an async function, it'll work as expected.
What's going on with
Unexpected token 'new'
and
await is not defined
is that, when you're not in an async function, those words are interpreted as normal identifiers (variable names) - if you switch them out for another placeholder variable name, it makes more sense for why the syntax would be invalid:
foo = questionData.media ? (someVariable new Promise(resolve => {
foo = questionData.media ? (someVariable (new Promise(resolve => {
Neither of those makes sense, and so the parser will give you errors as expected, when you use await when not in an async function.
I'm trying to valdiate a multiple image upload using jQuery. The validation proccess get the images dimensions and check if it is greater than 338x450. But I know very little about javascript. Can someone help me? Here's the code I'm trying:
HTML:
<input class="form-control" id="ads-photos" name="ads-photos[]" type="file" accept="image/jpeg, image/jpg" multiple />
javascript
var isFilesOk;
$('#ads-photos').change(function (evt) {
validateDimensions();
});
function validateDimensions() {
var fi = document.getElementById('ads-photos');
if (fi.files.length > 0) {
for (var i = 0; i <= fi.files.length - 1; i++) {
var fileName, fileExtension, fileSize, fileType, dateModified;
fileName = fi.files.item(i).name;
fileExtension = fileName.replace(/^.*\./, '');
if (fileExtension == 'jpg' || fileExtension == 'jpeg') {
if (readImageFile(fi.files.item(i))) {
alert(isFilesOk);
} else {
alert(isFilesOk);
}
}
}
}
// GET THE IMAGE WIDTH AND HEIGHT USING fileReader() API.
function readImageFile(file) {
var reader = new FileReader(); // CREATE AN NEW INSTANCE.
reader.onload = function (e) {
var img = new Image();
img.src = e.target.result;
img.onload = function () {
var w = this.width;
var h = this.height;
if (this.width >= 338 && this.height >= 450) {
isFilesOk = true;
} else {
isFilesOk = false;
}
}
};
reader.readAsDataURL(file);
return isFilesOk;
}
}
A example link
The above code needs analyze all the images sended by the user to know if they all have minimum required dimensions. But I'm not getting it to work.
The issue here is that the readImageFile method is asynchronous. The work it's doing happens later, after it's executed when the onload event is triggered on the file.
The best solution will depend a little on the environment you're targeting (eg: whether you need to support old IE, etc), but if you're able to use modern JS with Promises, this will be easier.
Basically, for each file that's selected, you want to get a Promise indicating whether the file is okay or not. A good way to do this is map the array to an array of Promises and use Promise.all to get a callback when they're all done.
Here's the method to get that for a single file:
function fileIsValid(file) {
return new Promise((resolve) => {
var reader = new FileReader(); // CREATE AN NEW INSTANCE.
reader.onload = function (e) {
var img = new Image();
img.src = e.target.result;
img.onload = function () {
var w = this.width;
var h = this.height;
const isValid = w >= 338 && h >= 450;
resolve(isValid);
};
};
reader.readAsDataURL(file);
});
}
And then using this method:
const fi = document.getElementById('ads-photos');
const promises = [];
for (let i = 0; i <= fi.files.length - 1; i++) {
const file = fi.files.item(i);
if (/\.jpe?g$/.test(file.name)) {
promises.push(fileIsValid(file));
}
}
Promise.all(promises).then((results) => {
// results is an array of booleans
const allValid = results.every(Boolean);
if (allValid) {
alert('Yeah!');
} else {
alert('Nope');
}
});
There's a few things to note here:
you could make it shortcut once a single error is found by making the promise reject if it's invalid, and Promise.all then won't wait for any other pending promises. This is a bit of a style question about whether failed validation should result in a rejected promise. Up to you on that. In this case, it's reading from a local file system and I presume they'll be selecting a relatively small number of files, so short-circuiting probably won't make any noticeable difference.
it doesn't handle actual errors (eg: what if a file is unable to be loaded?).
I have an webpage where the user will drop a video file, and the page will upload the video, and generate a thumbnail based on a timestamp that the user provide.
For the moment, I am just trying to generate the thumbnail based on the FIRST frame of the video.
here is a quick exemple on my current progress :
(please use chrome as firefox will complain about the https link, and also, sorry if it autodownload an image)
https://stackblitz.com/edit/rxjs-qc8iag
import { Observable, throwError } from 'rxjs'
const VIDEO = {
imageFromFrame(videoSrc: any): Observable<any> {
return new Observable<any>((obs) => {
const canvas = document.createElement('canvas')
const video = document.createElement('video')
const context = canvas.getContext('2d')
const source = document.createElement('source');
source.setAttribute('src', videoSrc);
video.appendChild(source);
document.body.appendChild(canvas)
document.body.appendChild(video)
if (!context) {
throwError(`Couldn't retrieve context 2d`)
obs.complete()
return
}
video.load()
video.addEventListener('loadedmetadata', function () {
console.log('loadedmetadata')
// Set canvas dimensions same as video dimensions
canvas.width = video.videoWidth
canvas.height = video.videoHeight
})
video.addEventListener('canplay', function () {
console.log('canplay')
canvas.style.display = 'inline'
context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight)
// Convert canvas image to Base64
const img = canvas.toDataURL("image/png")
// Convert Base64 image to binary
obs.next(VIDEO.dataURItoBlob(img))
obs.complete()
})
})
},
dataURItoBlob(dataURI: string): Blob {
// convert base64/URLEncoded data component to raw binary data held in a string
var byteString
if (dataURI.split(',')[0].indexOf('base64') >= 0) byteString = atob(dataURI.split(',')[1])
else byteString = unescape(dataURI.split(',')[1])
// separate out the mime component
var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]
// write the bytes of the string to a typed array
var ia = new Uint8Array(byteString.length)
for (var i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i)
}
return new Blob([ia], { type: mimeString })
},
}
VIDEO.imageFromFrame('https://www.learningcontainer.com/wp-content/uploads/2020/05/sample-mp4-file.mp4?_=1').subscribe((r) => {
var a = document.createElement('a')
document.body.appendChild(a)
const url = window.URL.createObjectURL(r)
a.href = url
a.download = 'sdf'
a.click()
window.URL.revokeObjectURL(url)
})
The problem is, the image it download is empty, and do not represent the first frame of the video. but the video should have been loaded. and drawed in the canvas.
I am trying to solve it, but if someone could help me found out the issue, thanks.
for anyone looking, I made this which work correctly (need improvement but the idea is there). I use observable cause the flow of my app use observable, but you can change to promise or whatever =>
imageFromFrame(
videoFile: File,
options: { frameTimeInSeconds: number; filename?: string; fileType?: string } = {
frameTimeInSeconds: 0.1,
}
): Observable<File> {
return new Observable<any>((obs) => {
const canvas = document.createElement('canvas')
const video = document.createElement('video')
const source = document.createElement('source')
const context = canvas.getContext('2d')
const urlRef = URL.createObjectURL(videoFile)
video.style.display = 'none'
canvas.style.display = 'none'
source.setAttribute('src', urlRef)
video.setAttribute('crossorigin', 'anonymous')
video.appendChild(source)
document.body.appendChild(canvas)
document.body.appendChild(video)
if (!context) {
throwError(`Couldn't retrieve context 2d`)
obs.complete()
return
}
video.currentTime = options.frameTimeInSeconds
video.load()
video.addEventListener('loadedmetadata', function () {
canvas.width = video.videoWidth
canvas.height = video.videoHeight
})
video.addEventListener('loadeddata', function () {
context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight)
canvas.toBlob((blob) => {
if (!blob) {
return
}
obs.next(
new File([blob], options.filename || FILES.getName(videoFile.name), {
type: options.fileType || 'image/png',
})
)
obs.complete()
URL.revokeObjectURL(urlRef)
video.remove()
canvas.remove()
}, options.fileType || 'image/png')
})
})
},
Hello me need get variable from function
me return in answer undefined
function etr() {
var img = new Image();
img.onload = paramsImg;
img.src = picFile.result;
function paramsImg() {
return img.height;
};
};
var vvvr = etr();
alert(vvvr);
function etr() {
var img = new Image();
img.onload = paramsImg;
img.src = picFile.result;
function paramsImg() {
return img.height;
};
};
In your function, you mentioned paramsImg before its even loaded, so its not visible to img.onload.
paramsImg is declared simply as function, its not have scope outside the object. You need to use this keyword or mention fn with prototype.
function etr(picFile){
this.paramsImg = function(){
return img.height;
};
var img = new Image();
img.onload = this.paramsImg;
img.src = picFile.result;
}
picfile = {
result: 10
}
var vvvr = new etr(picfile);
alert(vvvr.paramsImg());
Your function etr doesn't return anything. I see that you are trying to return from an event handler for onload, but that only returns from the paramsImg function and not from etr (which has already returned before the image loads). You should wither make etr accept a callback function or return a Promise so that you can alert the images height after the image has loaded. Here is an example with a Promise:
function etr() {
return new Promise( ( resolve, reject ) => {
var img = new Image();
img.onload = _ => resolve(img.height);
img.onerror = reject;
img.src = picFile.result;
} );
};
var picFile = { result: 'https://dummyimage.com/600x1234/000/fff' };
(async function ( ) {
var vvvr = await etr();
alert(vvvr);
})( );