I have this code which reads a file (an image) from the local filesystem and I want to resize the image so I can upload it to a server in a smaller format. The problem I am having is that I can't figure out how to resive it and convert it back to base64 so that I can send that base64 string to Cloudinary servers. The file is more than 2 megabytes, as it is, and I believe I can get it down to less than half an MB if I resize the file.
$scope.addLocalImage = function (file) {
var reader = new FileReader();
reader.onload = function () {
var tempImage = new Image();
tempImage.src = reader.result; // to get the base64 result
var height = tempImage.height;
var width = tempImage.width;
if (height > 100) { // max height for our purposes is 100 pixels
width = width / (height / 100);
height = 100;
}
if (width > 150) { // max width for our purposes is 150 pixels
height = height / (width / 150);
width = 150;
}
var c = document.createElement('canvas');
c.width = width;
c.height = height;
var ctx = c.getContext("2d");
ctx.drawImage(tempImage, 0, 0, width, height);
var b64str = c.toDataURL("image/jpeg"); // this is not base64, how can I get a base64 string?
var localImage = {
originalImageURL: b64str,
origin: "local",
imageObject:{
result: b64str
}
};
$scope.suggestedImages.push(localImage);
$scope.selectImage($scope.suggestedImages.length - 1); // Select new image
$scope.$apply();
};
reader.readAsDataURL(file); //this initiates the loading of file to browser
};
the problem is that "var b64str = c.toDataURL("image/jpeg");" yields a malformed string, it's not base64. If I were to guess I would think this line is incorrect, or I need to add some code here to convert the canvas to base64. The malformed string is "data:,", which looks the the beginning of a base64 string, but is truncated
The image needs an onload handler, so drawing to the canvas doesn't happen before the image has loaded
$scope.addLocalImage = function (file) {
var reader = new FileReader();
reader.onload = function () {
var tempImage = new Image();
tempImage.onload = function() {
var height = tempImage.height;
var width = tempImage.width;
if (height > 100) { // max height for our purposes is 100 pixels
width = width / (height / 100);
height = 100;
}
if (width > 150) { // max width for our purposes is 150 pixels
height = height / (width / 150);
width = 150;
}
var c = document.createElement('canvas');
c.width = width;
c.height = height;
var ctx = c.getContext("2d");
ctx.drawImage(tempImage, 0, 0, width, height);
var b64str = c.toDataURL("image/jpeg");
var localImage = {
originalImageURL: b64str,
origin: "local",
imageObject:{
result: b64str
}
};
$scope.suggestedImages.push(localImage);
$scope.selectImage($scope.suggestedImages.length - 1); // Select new image
$scope.$apply();
}
tempImage.src = reader.result; // to get the base64 result
}
reader.readAsDataURL(file); //this initiates the loading of file to browser
}
Related
I'm trying hard to accomplish and understand the following 'monstercode' that I wrote. Basically, this function should fire when the File input dialog (HTML) was fired. But the code goes well only a few lines. It loads the image, and then in the first alert() it shows me the base64 of the original image, but then, later in the code, when the actual image should be loaded in a canvas, it returns (in another alert()) height 0, probably meaning that the image wasn't loaded correctly or at all.
The HTML input form (simplified) looks like this:
<input type='File' accept='image/*' onChange='changeBtn(this);' />
And the function below:
function changeBtn(elem) {
selfile = document.getElementById(elem.id);
if ('files' in selfile) {
var file = selfile.files[0];
if ('name' in file) {
str1 = file.name;
}
if ('size' in file) {
str1 += ' (' + ((file.size / 1024) / 1024).toFixed(2) + 'MB)';
}
document.getElementById("label_" + elem.id).innerHTML = str1;
var FR= new FileReader();
var img = document.createElement("img");
FR.onload = function(e) {
img.src = e.target.result;
alert(e.target.result); // Returns B64 of the original image
};
FR.readAsDataURL(file);
var canvas = document.createElement('canvas');
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
var MAX_WIDTH = 800;
var MAX_HEIGHT = 600;
var width = img.width;
var height = img.height;
alert(height); // Returns 0
if (width > height) {
if (width > MAX_WIDTH) {
height *= MAX_WIDTH / width;
width = MAX_WIDTH;
}
} else {
if (height > MAX_HEIGHT) {
width *= MAX_HEIGHT / height;
height = MAX_HEIGHT;
}
}
canvas.width = width;
canvas.height = height;
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, width, height);
var dataurl = canvas.toDataURL("image/png");
alert(dataurl); // Returns only "data; " and nothing more
}
}
Thanks in advance for any help. Seems quite simple, but I don't get it. I rewrote the code at least 3 times, but I get always the same result.
Figured it out!
I was calling FR.readAsDataURL(file); before loading the image into it, leading it to load an undefined file.
If we look at following code that has failed:
var FR= new FileReader();
var img = document.createElement("img");
FR.onload = function(e) {
img.src = e.target.result;
alert(e.target.result); // Returns B64 of the original image
};
FR.readAsDataURL(file);
we now know why. First var FR= new FileReader(); is called, then FR.readAsDataURL(file); and then, at last, when the onload fires: img.src = e.target.result;. That's the wrong order. The right order is:
function changeBtn(elem) {
selfile = document.getElementById(elem.id);
if ('files' in selfile) {
var file = selfile.files[0];
if ('name' in file) {
str1 = file.name;
}
if ('size' in file) {
str1 += ' (' + ((file.size / 1024) / 1024).toFixed(2) + 'MB)';
}
document.getElementById("label_" + elem.id).innerHTML = str1;
var FR= new FileReader();
FR.readAsDataURL(file);
var img = document.createElement("img");
FR.onload = function(e) {
img.src = e.target.result;
alert(e.target.result);
var canvas = document.createElement('canvas');
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
var MAX_WIDTH = 800;
var MAX_HEIGHT = 600;
var width = img.width;
var height = img.height;
alert(height);
if (width > height) {
if (width > MAX_WIDTH) {
height *= MAX_WIDTH / width;
width = MAX_WIDTH;
}
} else {
if (height > MAX_HEIGHT) {
width *= MAX_HEIGHT / height;
height = MAX_HEIGHT;
}
}
canvas.width = width;
canvas.height = height;
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, width, height);
var dataurl = canvas.toDataURL("image/png");
alert(dataurl);
};
}
}
I have found a few different posts and even questions on stackoverflow answering this question. I am basically implementing this same thing as this post.
So here is my issue. When I upload the photo, I also need to submit the rest of the form. Here is my html:
<form id="uploadImageForm" enctype="multipart/form-data">
<input name="imagefile[]" type="file" id="takePictureField" accept="image/*" onchange="uploadPhotos(\'#{imageUploadUrl}\')" />
<input id="name" value="#{name}" />
... a few more inputs ...
</form>
Previously, I did not need to resize the image, so my javascript looked like this:
window.uploadPhotos = function(url){
var data = new FormData($("form[id*='uploadImageForm']")[0]);
$.ajax({
url: url,
data: data,
cache: false,
contentType: false,
processData: false,
type: 'POST',
success: function(data){
... handle error...
}
}
});
};
This all worked great... now that I need to resize the images... how can I replace the image in the form so that the resized one is posted and not the uploaded image?
window.uploadPhotos = function(url){
var resizedImage;
// Read in file
var file = event.target.files[0];
// Ensure it's an image
if(file.type.match(/image.*/)) {
console.log('An image has been loaded');
// Load the image
var reader = new FileReader();
reader.onload = function (readerEvent) {
var image = new Image();
image.onload = function (imageEvent) {
// Resize the image
var canvas = document.createElement('canvas'),
max_size = 1200,
width = image.width,
height = image.height;
if (width > height) {
if (width > max_size) {
height *= max_size / width;
width = max_size;
}
} else {
if (height > max_size) {
width *= max_size / height;
height = max_size;
}
}
canvas.width = width;
canvas.height = height;
canvas.getContext('2d').drawImage(image, 0, 0, width, height);
resizedImage = canvas.toDataURL('image/jpeg');
}
image.src = readerEvent.target.result;
}
reader.readAsDataURL(file);
}
// TODO: Need some logic here to switch out which photo is being posted...
var data = new FormData($("form[id*='uploadImageForm']")[0]);
$.ajax({
url: url,
data: data,
cache: false,
contentType: false,
processData: false,
type: 'POST',
success: function(data){
... handle error...
}
}
});
};
I've thought about moving the file input out of the form and having a hidden input in the form that I set the value of to the value of the resized image... But I'm wondering if I can just replace the image that is already in the form.
Here is what I ended up doing and it worked great.
First I moved the file input outside of the form so that it is not submitted:
<input name="imagefile[]" type="file" id="takePictureField" accept="image/*" onchange="uploadPhotos(\'#{imageUploadUrl}\')" />
<form id="uploadImageForm" enctype="multipart/form-data">
<input id="name" value="#{name}" />
... a few more inputs ...
</form>
Then I changed the uploadPhotos function to handle only the resizing:
window.uploadPhotos = function(url){
// Read in file
var file = event.target.files[0];
// Ensure it's an image
if(file.type.match(/image.*/)) {
console.log('An image has been loaded');
// Load the image
var reader = new FileReader();
reader.onload = function (readerEvent) {
var image = new Image();
image.onload = function (imageEvent) {
// Resize the image
var canvas = document.createElement('canvas'),
max_size = 544,// TODO : pull max size from a site config
width = image.width,
height = image.height;
if (width > height) {
if (width > max_size) {
height *= max_size / width;
width = max_size;
}
} else {
if (height > max_size) {
width *= max_size / height;
height = max_size;
}
}
canvas.width = width;
canvas.height = height;
canvas.getContext('2d').drawImage(image, 0, 0, width, height);
var dataUrl = canvas.toDataURL('image/jpeg');
var resizedImage = dataURLToBlob(dataUrl);
$.event.trigger({
type: "imageResized",
blob: resizedImage,
url: dataUrl
});
}
image.src = readerEvent.target.result;
}
reader.readAsDataURL(file);
}
};
As you can see I'm using canvas.toDataURL('image/jpeg'); to change the resized image into a dataUrl adn then I call the function dataURLToBlob(dataUrl); to turn the dataUrl into a blob that I can then append to the form. When the blob is created, I trigger a custom event. Here is the function to create the blob:
/* Utility function to convert a canvas to a BLOB */
var dataURLToBlob = function(dataURL) {
var BASE64_MARKER = ';base64,';
if (dataURL.indexOf(BASE64_MARKER) == -1) {
var parts = dataURL.split(',');
var contentType = parts[0].split(':')[1];
var raw = parts[1];
return new Blob([raw], {type: contentType});
}
var parts = dataURL.split(BASE64_MARKER);
var contentType = parts[0].split(':')[1];
var raw = window.atob(parts[1]);
var rawLength = raw.length;
var uInt8Array = new Uint8Array(rawLength);
for (var i = 0; i < rawLength; ++i) {
uInt8Array[i] = raw.charCodeAt(i);
}
return new Blob([uInt8Array], {type: contentType});
}
/* End Utility function to convert a canvas to a BLOB */
Finally, here is my event handler that takes the blob from the custom event, appends the form and then submits it.
/* Handle image resized events */
$(document).on("imageResized", function (event) {
var data = new FormData($("form[id*='uploadImageForm']")[0]);
if (event.blob && event.url) {
data.append('image_data', event.blob);
$.ajax({
url: event.url,
data: data,
cache: false,
contentType: false,
processData: false,
type: 'POST',
success: function(data){
//handle errors...
}
});
}
});
if any interested I've made a typescript version:
interface IResizeImageOptions {
maxSize: number;
file: File;
}
const resizeImage = (settings: IResizeImageOptions) => {
const file = settings.file;
const maxSize = settings.maxSize;
const reader = new FileReader();
const image = new Image();
const canvas = document.createElement('canvas');
const dataURItoBlob = (dataURI: string) => {
const bytes = dataURI.split(',')[0].indexOf('base64') >= 0 ?
atob(dataURI.split(',')[1]) :
unescape(dataURI.split(',')[1]);
const mime = dataURI.split(',')[0].split(':')[1].split(';')[0];
const max = bytes.length;
const ia = new Uint8Array(max);
for (var i = 0; i < max; i++) ia[i] = bytes.charCodeAt(i);
return new Blob([ia], {type:mime});
};
const resize = () => {
let width = image.width;
let height = image.height;
if (width > height) {
if (width > maxSize) {
height *= maxSize / width;
width = maxSize;
}
} else {
if (height > maxSize) {
width *= maxSize / height;
height = maxSize;
}
}
canvas.width = width;
canvas.height = height;
canvas.getContext('2d').drawImage(image, 0, 0, width, height);
let dataUrl = canvas.toDataURL('image/jpeg');
return dataURItoBlob(dataUrl);
};
return new Promise((ok, no) => {
if (!file.type.match(/image.*/)) {
no(new Error("Not an image"));
return;
}
reader.onload = (readerEvent: any) => {
image.onload = () => ok(resize());
image.src = readerEvent.target.result;
};
reader.readAsDataURL(file);
})
};
and here's the javascript result:
var resizeImage = function (settings) {
var file = settings.file;
var maxSize = settings.maxSize;
var reader = new FileReader();
var image = new Image();
var canvas = document.createElement('canvas');
var dataURItoBlob = function (dataURI) {
var bytes = dataURI.split(',')[0].indexOf('base64') >= 0 ?
atob(dataURI.split(',')[1]) :
unescape(dataURI.split(',')[1]);
var mime = dataURI.split(',')[0].split(':')[1].split(';')[0];
var max = bytes.length;
var ia = new Uint8Array(max);
for (var i = 0; i < max; i++)
ia[i] = bytes.charCodeAt(i);
return new Blob([ia], { type: mime });
};
var resize = function () {
var width = image.width;
var height = image.height;
if (width > height) {
if (width > maxSize) {
height *= maxSize / width;
width = maxSize;
}
} else {
if (height > maxSize) {
width *= maxSize / height;
height = maxSize;
}
}
canvas.width = width;
canvas.height = height;
canvas.getContext('2d').drawImage(image, 0, 0, width, height);
var dataUrl = canvas.toDataURL('image/jpeg');
return dataURItoBlob(dataUrl);
};
return new Promise(function (ok, no) {
if (!file.type.match(/image.*/)) {
no(new Error("Not an image"));
return;
}
reader.onload = function (readerEvent) {
image.onload = function () { return ok(resize()); };
image.src = readerEvent.target.result;
};
reader.readAsDataURL(file);
});
};
usage is like:
resizeImage({
file: $image.files[0],
maxSize: 500
}).then(function (resizedImage) {
console.log("upload resized image")
}).catch(function (err) {
console.error(err);
});
or (async/await):
const config = {
file: $image.files[0],
maxSize: 500
};
const resizedImage = await resizeImage(config)
console.log("upload resized image")
If some of you, like me, encounter orientation problems I have combined the solutions here with a exif orientation fix
https://gist.github.com/SagiMedina/f00a57de4e211456225d3114fd10b0d0
I have made my own version without actually using the file reader. Instead I use createObjectUrl which is supported by majority of modern browsers.
/**
* Function scaling an image from a file input to specified dimensions
* If the specified dimensions are not proportional to image dimensions the output image will be cropped at center
*
* #param file {File} Input file of a form
* #param dimensions {{width: number, height: number}} Dimenstions of the output image
* #returns {Promise<Blob | null>} Promise resolving to a scale image or a null if provided an invalid file type
*/
export async function scaleImageBeforeUpload(file: File, dimensions: {width: number, height: number}): Promise<Blob | null> {
// ensure the file is an image
if (!file.type.match(/image.*/)) return null;
const image = new Image();
image.src = URL.createObjectURL(file);
await new Promise<Event>((res) => image.onload = res);
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d", {alpha: true});
canvas.width = dimensions.width;
canvas.height = dimensions.height;
if (image.height <= image.width) {
const scaleProportions = canvas.height / image.height;
const scaledWidth = scaleProportions * image.width;
context.drawImage(image, (canvas.width - scaledWidth)/2, 0, scaledWidth, canvas.height);
}
else {
const scaleProportions = canvas.width / image.width;
const scaledHeight = scaleProportions * image.height;
context.drawImage(image, 0, (canvas.height - scaledHeight)/2, canvas.width, scaledHeight);
}
return new Promise((res) => canvas.toBlob(res));
}
In 2022 we have some new APIs available to us. This is the solution I came up with. We don't have to mess with FileReader API or image onload callback.
The following code accepts a file object or a Blob object and outputs a blob of a cropped, centered, resized image, and also converts it to webp.
export default async (file, size) => {
size ??= 256
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = size
canvas.height = size
const bitmap = await createImageBitmap(file)
const { width, height } = bitmap
const ratio = Math.max(size / width, size / height)
const x = (size - (width * ratio)) / 2
const y = (size - (height * ratio)) / 2
ctx.drawImage(bitmap, 0, 0, width, height, x, y, width * ratio, height * ratio)
return new Promise(resolve => {
canvas.toBlob(blob => {
resolve(blob)
}, 'image/webp', 1)
})
}
Here is our implementation of a jpeg resize on client side to a fixed size of 1024 or 768. The work scenario is that a user selects an image (original image) he/she wants to upload (without display). If dimension of the original image is larger than 1024 or 768, then resize the image proportionally. The code below calculates the width and height of should-be and performs resize.
There are 2 alert() to show the image size before and after resize. We find that the file size is not reduced. What could be wrong with the code?
$(function(){
$('#uploaded_file_file_for_upload').change(function(){
var _URL = window.URL || window.webkitURL;
var img, img_width = 0, img_height = 0, max_width = 1024, max_height = 768;
var f = $(this)[0].files[0];
alert(f.size);
if (f.type == 'image/jpeg' || f.type == 'image/jpg') {
img = new Image();
img.onload = function () {
img_width = this.width;
img_height = this.height;
};
img.src = _URL.createObjectURL(f);
if (img_width > max_width){
img_height = Math.ceil(img_height * max_width / img_width);
img_width = max_width;
} else if (img_height > max_height) {
img_width = Math.ceil(img_width * max_height / img_height);
img_height = max_height;
};
resize(img, img_width, img_height);
var f1 = $(this)[0].files[0];
alert(f1.size);
};
function resize(image, width, height) {
var mainCanvas = document.createElement("canvas");
mainCanvas.width = width;
mainCanvas.height = height;
var ctx = mainCanvas.getContext("2d");
ctx.drawImage(image, 0, 0, width, height);
$('#uploaded_file_file_for_upload').attr('src', mainCanvas.toDataURL("image/jpeg"));
};
return false;
});
});
check the naturalWidth from an image instead of css width
I have found a few different posts and even questions on stackoverflow answering this question. I am basically implementing this same thing as this post.
So here is my issue. When I upload the photo, I also need to submit the rest of the form. Here is my html:
<form id="uploadImageForm" enctype="multipart/form-data">
<input name="imagefile[]" type="file" id="takePictureField" accept="image/*" onchange="uploadPhotos(\'#{imageUploadUrl}\')" />
<input id="name" value="#{name}" />
... a few more inputs ...
</form>
Previously, I did not need to resize the image, so my javascript looked like this:
window.uploadPhotos = function(url){
var data = new FormData($("form[id*='uploadImageForm']")[0]);
$.ajax({
url: url,
data: data,
cache: false,
contentType: false,
processData: false,
type: 'POST',
success: function(data){
... handle error...
}
}
});
};
This all worked great... now that I need to resize the images... how can I replace the image in the form so that the resized one is posted and not the uploaded image?
window.uploadPhotos = function(url){
var resizedImage;
// Read in file
var file = event.target.files[0];
// Ensure it's an image
if(file.type.match(/image.*/)) {
console.log('An image has been loaded');
// Load the image
var reader = new FileReader();
reader.onload = function (readerEvent) {
var image = new Image();
image.onload = function (imageEvent) {
// Resize the image
var canvas = document.createElement('canvas'),
max_size = 1200,
width = image.width,
height = image.height;
if (width > height) {
if (width > max_size) {
height *= max_size / width;
width = max_size;
}
} else {
if (height > max_size) {
width *= max_size / height;
height = max_size;
}
}
canvas.width = width;
canvas.height = height;
canvas.getContext('2d').drawImage(image, 0, 0, width, height);
resizedImage = canvas.toDataURL('image/jpeg');
}
image.src = readerEvent.target.result;
}
reader.readAsDataURL(file);
}
// TODO: Need some logic here to switch out which photo is being posted...
var data = new FormData($("form[id*='uploadImageForm']")[0]);
$.ajax({
url: url,
data: data,
cache: false,
contentType: false,
processData: false,
type: 'POST',
success: function(data){
... handle error...
}
}
});
};
I've thought about moving the file input out of the form and having a hidden input in the form that I set the value of to the value of the resized image... But I'm wondering if I can just replace the image that is already in the form.
Here is what I ended up doing and it worked great.
First I moved the file input outside of the form so that it is not submitted:
<input name="imagefile[]" type="file" id="takePictureField" accept="image/*" onchange="uploadPhotos(\'#{imageUploadUrl}\')" />
<form id="uploadImageForm" enctype="multipart/form-data">
<input id="name" value="#{name}" />
... a few more inputs ...
</form>
Then I changed the uploadPhotos function to handle only the resizing:
window.uploadPhotos = function(url){
// Read in file
var file = event.target.files[0];
// Ensure it's an image
if(file.type.match(/image.*/)) {
console.log('An image has been loaded');
// Load the image
var reader = new FileReader();
reader.onload = function (readerEvent) {
var image = new Image();
image.onload = function (imageEvent) {
// Resize the image
var canvas = document.createElement('canvas'),
max_size = 544,// TODO : pull max size from a site config
width = image.width,
height = image.height;
if (width > height) {
if (width > max_size) {
height *= max_size / width;
width = max_size;
}
} else {
if (height > max_size) {
width *= max_size / height;
height = max_size;
}
}
canvas.width = width;
canvas.height = height;
canvas.getContext('2d').drawImage(image, 0, 0, width, height);
var dataUrl = canvas.toDataURL('image/jpeg');
var resizedImage = dataURLToBlob(dataUrl);
$.event.trigger({
type: "imageResized",
blob: resizedImage,
url: dataUrl
});
}
image.src = readerEvent.target.result;
}
reader.readAsDataURL(file);
}
};
As you can see I'm using canvas.toDataURL('image/jpeg'); to change the resized image into a dataUrl adn then I call the function dataURLToBlob(dataUrl); to turn the dataUrl into a blob that I can then append to the form. When the blob is created, I trigger a custom event. Here is the function to create the blob:
/* Utility function to convert a canvas to a BLOB */
var dataURLToBlob = function(dataURL) {
var BASE64_MARKER = ';base64,';
if (dataURL.indexOf(BASE64_MARKER) == -1) {
var parts = dataURL.split(',');
var contentType = parts[0].split(':')[1];
var raw = parts[1];
return new Blob([raw], {type: contentType});
}
var parts = dataURL.split(BASE64_MARKER);
var contentType = parts[0].split(':')[1];
var raw = window.atob(parts[1]);
var rawLength = raw.length;
var uInt8Array = new Uint8Array(rawLength);
for (var i = 0; i < rawLength; ++i) {
uInt8Array[i] = raw.charCodeAt(i);
}
return new Blob([uInt8Array], {type: contentType});
}
/* End Utility function to convert a canvas to a BLOB */
Finally, here is my event handler that takes the blob from the custom event, appends the form and then submits it.
/* Handle image resized events */
$(document).on("imageResized", function (event) {
var data = new FormData($("form[id*='uploadImageForm']")[0]);
if (event.blob && event.url) {
data.append('image_data', event.blob);
$.ajax({
url: event.url,
data: data,
cache: false,
contentType: false,
processData: false,
type: 'POST',
success: function(data){
//handle errors...
}
});
}
});
if any interested I've made a typescript version:
interface IResizeImageOptions {
maxSize: number;
file: File;
}
const resizeImage = (settings: IResizeImageOptions) => {
const file = settings.file;
const maxSize = settings.maxSize;
const reader = new FileReader();
const image = new Image();
const canvas = document.createElement('canvas');
const dataURItoBlob = (dataURI: string) => {
const bytes = dataURI.split(',')[0].indexOf('base64') >= 0 ?
atob(dataURI.split(',')[1]) :
unescape(dataURI.split(',')[1]);
const mime = dataURI.split(',')[0].split(':')[1].split(';')[0];
const max = bytes.length;
const ia = new Uint8Array(max);
for (var i = 0; i < max; i++) ia[i] = bytes.charCodeAt(i);
return new Blob([ia], {type:mime});
};
const resize = () => {
let width = image.width;
let height = image.height;
if (width > height) {
if (width > maxSize) {
height *= maxSize / width;
width = maxSize;
}
} else {
if (height > maxSize) {
width *= maxSize / height;
height = maxSize;
}
}
canvas.width = width;
canvas.height = height;
canvas.getContext('2d').drawImage(image, 0, 0, width, height);
let dataUrl = canvas.toDataURL('image/jpeg');
return dataURItoBlob(dataUrl);
};
return new Promise((ok, no) => {
if (!file.type.match(/image.*/)) {
no(new Error("Not an image"));
return;
}
reader.onload = (readerEvent: any) => {
image.onload = () => ok(resize());
image.src = readerEvent.target.result;
};
reader.readAsDataURL(file);
})
};
and here's the javascript result:
var resizeImage = function (settings) {
var file = settings.file;
var maxSize = settings.maxSize;
var reader = new FileReader();
var image = new Image();
var canvas = document.createElement('canvas');
var dataURItoBlob = function (dataURI) {
var bytes = dataURI.split(',')[0].indexOf('base64') >= 0 ?
atob(dataURI.split(',')[1]) :
unescape(dataURI.split(',')[1]);
var mime = dataURI.split(',')[0].split(':')[1].split(';')[0];
var max = bytes.length;
var ia = new Uint8Array(max);
for (var i = 0; i < max; i++)
ia[i] = bytes.charCodeAt(i);
return new Blob([ia], { type: mime });
};
var resize = function () {
var width = image.width;
var height = image.height;
if (width > height) {
if (width > maxSize) {
height *= maxSize / width;
width = maxSize;
}
} else {
if (height > maxSize) {
width *= maxSize / height;
height = maxSize;
}
}
canvas.width = width;
canvas.height = height;
canvas.getContext('2d').drawImage(image, 0, 0, width, height);
var dataUrl = canvas.toDataURL('image/jpeg');
return dataURItoBlob(dataUrl);
};
return new Promise(function (ok, no) {
if (!file.type.match(/image.*/)) {
no(new Error("Not an image"));
return;
}
reader.onload = function (readerEvent) {
image.onload = function () { return ok(resize()); };
image.src = readerEvent.target.result;
};
reader.readAsDataURL(file);
});
};
usage is like:
resizeImage({
file: $image.files[0],
maxSize: 500
}).then(function (resizedImage) {
console.log("upload resized image")
}).catch(function (err) {
console.error(err);
});
or (async/await):
const config = {
file: $image.files[0],
maxSize: 500
};
const resizedImage = await resizeImage(config)
console.log("upload resized image")
If some of you, like me, encounter orientation problems I have combined the solutions here with a exif orientation fix
https://gist.github.com/SagiMedina/f00a57de4e211456225d3114fd10b0d0
I have made my own version without actually using the file reader. Instead I use createObjectUrl which is supported by majority of modern browsers.
/**
* Function scaling an image from a file input to specified dimensions
* If the specified dimensions are not proportional to image dimensions the output image will be cropped at center
*
* #param file {File} Input file of a form
* #param dimensions {{width: number, height: number}} Dimenstions of the output image
* #returns {Promise<Blob | null>} Promise resolving to a scale image or a null if provided an invalid file type
*/
export async function scaleImageBeforeUpload(file: File, dimensions: {width: number, height: number}): Promise<Blob | null> {
// ensure the file is an image
if (!file.type.match(/image.*/)) return null;
const image = new Image();
image.src = URL.createObjectURL(file);
await new Promise<Event>((res) => image.onload = res);
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d", {alpha: true});
canvas.width = dimensions.width;
canvas.height = dimensions.height;
if (image.height <= image.width) {
const scaleProportions = canvas.height / image.height;
const scaledWidth = scaleProportions * image.width;
context.drawImage(image, (canvas.width - scaledWidth)/2, 0, scaledWidth, canvas.height);
}
else {
const scaleProportions = canvas.width / image.width;
const scaledHeight = scaleProportions * image.height;
context.drawImage(image, 0, (canvas.height - scaledHeight)/2, canvas.width, scaledHeight);
}
return new Promise((res) => canvas.toBlob(res));
}
In 2022 we have some new APIs available to us. This is the solution I came up with. We don't have to mess with FileReader API or image onload callback.
The following code accepts a file object or a Blob object and outputs a blob of a cropped, centered, resized image, and also converts it to webp.
export default async (file, size) => {
size ??= 256
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = size
canvas.height = size
const bitmap = await createImageBitmap(file)
const { width, height } = bitmap
const ratio = Math.max(size / width, size / height)
const x = (size - (width * ratio)) / 2
const y = (size - (height * ratio)) / 2
ctx.drawImage(bitmap, 0, 0, width, height, x, y, width * ratio, height * ratio)
return new Promise(resolve => {
canvas.toBlob(blob => {
resolve(blob)
}, 'image/webp', 1)
})
}
I have a canvas into which I would like to load a Base64 string that represents a png image. However, the following code just displays a blank white box and I am baffled as to why. When I look at the data in the canvas, it looks identical to a canvas that gets its data from a FileReader object (also below). Any help deducing this issue is greatly appreciated!
This code shows a white canvas:
html
<canvas id="canvas" width="114" height="114" style="z-index: 999999; display: none; padding-left: 50px"></canvas>
javascript
var websiteIconData = $('#ctl00_ContentPlaceHolder1_websiteIcon');
if (websiteIconData.val() != '') {
var canvas = document.getElementById('canvas');
var ctx = document.getElementById('canvas').getContext('2d');
var loadedImg = new Image();
loadedImg.onload = function () {
ctx.drawImage(this, 0, 0);
Debugger.log(ctx);
};
loadedImg.src = websiteIconData.val();
canvas.style.display = 'block';
}
This code shows the image:
$('#loadWebsiteIcon').on({
change: function (ev) {
var reader = new FileReader();
reader.onload = function (e) {
function draw() {
var ctx = document.getElementById('canvas').getContext('2d');
var img = new Image();
img.onload = function () {
var MAX_WIDTH = 114;
var MAX_HEIGHT = 114;
var width = img.width;
var height = img.height;
if (width > MAX_WIDTH) {
width = MAX_WIDTH;
}
if (height > MAX_HEIGHT) {
height = MAX_HEIGHT;
}
ctx.drawImage(img, 0, 0, width, height);
for (var i = 0; i <= document.images.length; i++) {
}
Debugger.log(ctx);
};
img.src = e.target.result;
}
draw();
};
reader.readAsDataURL(ev.target.files[0]);
var canvas = document.getElementById('canvas');
canvas.style.display = 'block';
var imgData = canvas.toDataURL();
$('#ctl00_ContentPlaceHolder1_websiteIcon').val(imgData);
Debugger.log(imgData);
}
});
Be careful how Base64 is parsed. If it is parsed for email, it will insert a character ever 76 lines by default. Most Base64 encoders have an option to turn this off. I am looking at MIME::Base64
From that document :
The returned encoded string is broken into lines of no more than 76 characters each and it >will end with $eol unless it is empty.
where $eol was one of the arguments. In the case of this module, setting it to an empty string would prevent the base64 from being broken up.
It turns out the issue had little to do with the canvas API or the Base64 encoding. It was more an issue of the canvas.toDataURL(); function getting called before the image had been loaded in the canvas. Moving this line to within the img.onload function seemed to do the trick. Below is the correct code:
function initImageChange() {
//this is will be a string in Base64, in this case it is an <input type="text">... tag
var imageData = $('#imageData');
//this is a canvas tag in on the page
var canvas = document.getElementById('canvas');
//load the image from the imageData var if it exists
if (imageData.val() != '') {
var ctx2=canvas.getContext('2d');
var loadedImg = new Image();
loadedImg.onload = function() {
ctx2.drawImage(this, 0, 0);
};
loadedImg.src = imageData.val();
canvas.style.display = 'block';
}
//this is a function that loads an image from a file onto the canvas and
//sends the Base64 representation of this image to the text input tag. This
//could fairly easily be send directly to a post request or saved some other way
$('#loadImage').on({
change: function (ev) {
var ctx = document.getElementById('canvas').getContext('2d');
var dataURL = '';
var reader = new FileReader();
reader.onload = function (e) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
function draw() {
var img = new Image();
img.onload = function () {
var MAX_WIDTH = 130;
var MAX_HEIGHT = 110;
var width = img.width;
var height = img.height;
if (width > MAX_WIDTH) {
width = MAX_WIDTH;
}
if (height > MAX_HEIGHT) {
height = MAX_HEIGHT;
}
ctx.drawImage(img, 0, 0, width, height);
dataURL = canvas.toDataURL();
$('#ctl00_ContentPlaceHolder1_websiteIcon').val(dataURL);
ctx.restore();
};
img.src = e.target.result;
}
draw();
};
reader.readAsDataURL(ev.target.files[0]);
canvas.style.display = 'block';
}
});
}