How to compress an image via Javascript in the browser?
Solution 1
In short:
- Read the files using the HTML5 FileReader API with .readAsArrayBuffer
- Create a Blob with the file data and get its url with window.URL.createObjectURL(blob)
- Create new Image element and set it's src to the file blob url
- Send the image to the canvas. The canvas size is set to desired output size
- Get the scaled-down data back from canvas via canvas.toDataURL("image/jpeg",0.7) (set your own output format and quality)
- Attach new hidden inputs to the original form and transfer the dataURI images basically as normal text
- On backend, read the dataURI, decode from Base64, and save it
Source: code.
Solution 2
I see two things missing from the other answers:
-
canvas.toBlob
(when available) is more performant thancanvas.toDataURL
, and also async. - the file -> image -> canvas -> file conversion loses EXIF data; in particular, data about image rotation commonly set by modern phones/tablets.
The following script deals with both points:
// From https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob, needed for Safari:
if (!HTMLCanvasElement.prototype.toBlob) {
Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
value: function(callback, type, quality) {
var binStr = atob(this.toDataURL(type, quality).split(',')[1]),
len = binStr.length,
arr = new Uint8Array(len);
for (var i = 0; i < len; i++) {
arr[i] = binStr.charCodeAt(i);
}
callback(new Blob([arr], {type: type || 'image/png'}));
}
});
}
window.URL = window.URL || window.webkitURL;
// Modified from https://stackoverflow.com/a/32490603, cc by-sa 3.0
// -2 = not jpeg, -1 = no data, 1..8 = orientations
function getExifOrientation(file, callback) {
// Suggestion from http://code.flickr.net/2012/06/01/parsing-exif-client-side-using-javascript-2/:
if (file.slice) {
file = file.slice(0, 131072);
} else if (file.webkitSlice) {
file = file.webkitSlice(0, 131072);
}
var reader = new FileReader();
reader.onload = function(e) {
var view = new DataView(e.target.result);
if (view.getUint16(0, false) != 0xFFD8) {
callback(-2);
return;
}
var length = view.byteLength, offset = 2;
while (offset < length) {
var marker = view.getUint16(offset, false);
offset += 2;
if (marker == 0xFFE1) {
if (view.getUint32(offset += 2, false) != 0x45786966) {
callback(-1);
return;
}
var little = view.getUint16(offset += 6, false) == 0x4949;
offset += view.getUint32(offset + 4, little);
var tags = view.getUint16(offset, little);
offset += 2;
for (var i = 0; i < tags; i++)
if (view.getUint16(offset + (i * 12), little) == 0x0112) {
callback(view.getUint16(offset + (i * 12) + 8, little));
return;
}
}
else if ((marker & 0xFF00) != 0xFF00) break;
else offset += view.getUint16(offset, false);
}
callback(-1);
};
reader.readAsArrayBuffer(file);
}
// Derived from https://stackoverflow.com/a/40867559, cc by-sa
function imgToCanvasWithOrientation(img, rawWidth, rawHeight, orientation) {
var canvas = document.createElement('canvas');
if (orientation > 4) {
canvas.width = rawHeight;
canvas.height = rawWidth;
} else {
canvas.width = rawWidth;
canvas.height = rawHeight;
}
if (orientation > 1) {
console.log("EXIF orientation = " + orientation + ", rotating picture");
}
var ctx = canvas.getContext('2d');
switch (orientation) {
case 2: ctx.transform(-1, 0, 0, 1, rawWidth, 0); break;
case 3: ctx.transform(-1, 0, 0, -1, rawWidth, rawHeight); break;
case 4: ctx.transform(1, 0, 0, -1, 0, rawHeight); break;
case 5: ctx.transform(0, 1, 1, 0, 0, 0); break;
case 6: ctx.transform(0, 1, -1, 0, rawHeight, 0); break;
case 7: ctx.transform(0, -1, -1, 0, rawHeight, rawWidth); break;
case 8: ctx.transform(0, -1, 1, 0, 0, rawWidth); break;
}
ctx.drawImage(img, 0, 0, rawWidth, rawHeight);
return canvas;
}
function reduceFileSize(file, acceptFileSize, maxWidth, maxHeight, quality, callback) {
if (file.size <= acceptFileSize) {
callback(file);
return;
}
var img = new Image();
img.onerror = function() {
URL.revokeObjectURL(this.src);
callback(file);
};
img.onload = function() {
URL.revokeObjectURL(this.src);
getExifOrientation(file, function(orientation) {
var w = img.width, h = img.height;
var scale = (orientation > 4 ?
Math.min(maxHeight / w, maxWidth / h, 1) :
Math.min(maxWidth / w, maxHeight / h, 1));
h = Math.round(h * scale);
w = Math.round(w * scale);
var canvas = imgToCanvasWithOrientation(img, w, h, orientation);
canvas.toBlob(function(blob) {
console.log("Resized image to " + w + "x" + h + ", " + (blob.size >> 10) + "kB");
callback(blob);
}, 'image/jpeg', quality);
});
};
img.src = URL.createObjectURL(file);
}
Example usage:
inputfile.onchange = function() {
// If file size > 500kB, resize such that width <= 1000, quality = 0.9
reduceFileSize(this.files[0], 500*1024, 1000, Infinity, 0.9, blob => {
let body = new FormData();
body.set('file', blob, blob.name || "file.jpg");
fetch('/upload-image', {method: 'POST', body}).then(...);
});
};
Solution 3
@PsychoWoods' answer is good. I would like to offer my own solution. This Javascript function takes an image data URL and a width, scales it to the new width, and returns a new data URL.
// Take an image URL, downscale it to the given width, and return a new image URL.
function downscaleImage(dataUrl, newWidth, imageType, imageArguments) {
"use strict";
var image, oldWidth, oldHeight, newHeight, canvas, ctx, newDataUrl;
// Provide default values
imageType = imageType || "image/jpeg";
imageArguments = imageArguments || 0.7;
// Create a temporary image so that we can compute the height of the downscaled image.
image = new Image();
image.src = dataUrl;
oldWidth = image.width;
oldHeight = image.height;
newHeight = Math.floor(oldHeight / oldWidth * newWidth)
// Create a temporary canvas to draw the downscaled image on.
canvas = document.createElement("canvas");
canvas.width = newWidth;
canvas.height = newHeight;
// Draw the downscaled image on the canvas and return the new data URL.
ctx = canvas.getContext("2d");
ctx.drawImage(image, 0, 0, newWidth, newHeight);
newDataUrl = canvas.toDataURL(imageType, imageArguments);
return newDataUrl;
}
This code can be used anywhere you have a data URL and want a data URL for a downscaled image.
Solution 4
You can take a look at image-conversion,Try it here --> demo page
Solution 5
I find that there's simpler solution compared to the accepted answer.
Read the files using the HTML5 FileReader API with.readAsArrayBuffer
- Create a Blob with the file data and get its url with
window.URL.createObjectURL(blob)
- Create new Image element and set it's src to the file blob url
- Send the image to the canvas. The canvas size is set to desired output size
- Get the scaled-down data back from canvas
viacanvas.toDataURL("image/jpeg",0.7)
(set your own output format and quality)- Attach new hidden inputs to the original form
and transfer the dataURI images basically as normal textOn backend, read the dataURI, decode from Base64, andsave it
As per your question:
Is there a way to compress an image (mostly jpeg, png and gif) directly browser-side, before uploading it
My solution:
-
Create a blob with the file directly with
URL.createObjectURL(inputFileElement.files[0])
. -
Same as accepted answer.
-
Same as accepted answer. Worth mentioning that, canvas size is necessary and use
img.width
andimg.height
to setcanvas.width
andcanvas.height
. Notimg.clientWidth
. -
Get the scale-down image by
canvas.toBlob(callbackfunction(blob){}, 'image/jpeg', 0.5)
. Setting'image/jpg'
has no effect.image/png
is also supported. Make a newFile
object inside thecallbackfunction
body withlet compressedImageBlob = new File([blob])
. -
Add new hidden inputs or send via javascript . Server doesn't have to decode anything.
Check https://javascript.info/binary for all information. I came up the solution after reading this chapter.
Code:
<!DOCTYPE html>
<html>
<body>
<form action="upload.php" method="post" enctype="multipart/form-data">
Select image to upload:
<input type="file" name="fileToUpload" id="fileToUpload" multiple>
<input type="submit" value="Upload Image" name="submit">
</form>
</body>
</html>
This code looks far less scary than the other answers..
Update:
One has to put everything inside img.onload
. Otherwise canvas
will not be able to get the image's width and height correctly as the time canvas
is assigned.
function upload(){
var f = fileToUpload.files[0];
var fileName = f.name.split('.')[0];
var img = new Image();
img.src = URL.createObjectURL(f);
img.onload = function(){
var canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
var ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
canvas.toBlob(function(blob){
console.info(blob.size);
var f2 = new File([blob], fileName + ".jpeg");
var xhr = new XMLHttpRequest();
var form = new FormData();
form.append("fileToUpload", f2);
xhr.open("POST", "upload.php");
xhr.send(form);
}, 'image/jpeg', 0.5);
}
}
3.4MB
.png
file compression test with image/jpeg
argument set.
|0.9| 777KB |
|0.8| 383KB |
|0.7| 301KB |
|0.6| 251KB |
|0.5| 219kB |
pomeh
Note to all SO beginner: Please read (and embrace!) Jon Skeet's helpful hints on how to write a good question (or at least his short version here) - one that has a chance that someone can answer it. I'm a French developer, I like to improve my knowledge in JavaScript (mainly jQuery and Node.js,), PHP (Symfony), HTML5 and C#
Updated on July 08, 2022Comments
-
pomeh almost 2 years
TL;DR;
Is there a way to compress an image (mostly jpeg, png and gif) directly browser-side, before uploading it ? I'm pretty sure JavaScript can do this, but I can't find a way to achieve it.
Here's the full scenario I would like to implement:- the user goes to my website, and choose an image via an
input type="file"
element, - this image is retrieved via JavaScript, we do some verification such as correct file format, maximum file size etc,
- if every thing is OK, a preview of the image is displayed on the page,
- the user can do some basic operations such as rotate the image by 90°/-90°, crop it following a pre-defined ratio, etc, or the user can upload another image and return to step 1,
- when the user is satisfied, the edited image is then compressed and "saved" locally (not saved to a file, but in the browser memory/page),-
- the user fill a form with data like name, age etc,
- the user click on the "Finish" button, then the form containing datas + compressed image is sent to the server (without AJAX),
The full process up to the last step should be done client side, and should be compatible on latest Chrome and Firefox, Safari 5+ and IE 8+. If possible, only JavaScript should be used (but I'm pretty sure this is not possible).
I've not code anything right now, but I've thought about it already. File reading locally is possible via File API, image previewing and editing could be done using Canvas element, but I can't find a way to do the image compression part.
According to html5please.com and caniuse.com, supporting those browser is quite hard (thanks to IE), but could be done using polyfill such as FlashCanvas and FileReader.
Actually, the goal is to reduce file size, so I see image compression as a solution. But, I know that uploaded images are going to be displayed on my website, every time at the same place, and I know the dimension of this display area (eg. 200x400). So, I could resize the image to fit those dimensions, thus reducing file size. I have no idea what would be the compression ratio for this technique.
What do you think ? Do you have any advice to tell me ? Do you know any way to compress an image browser-side in JavaScript ? Thanks for your replies.
- the user goes to my website, and choose an image via an
-
pomeh over 11 yearsThanks a lot ! This is what I was looking for. Do you know how good the compression ratio is with this technique ?
-
psychowood over 11 yearsApart from the network transmission (you are sending Base64 encoded content, which is not the best one), the image compression alghorithm is one of the standard ones, the size depends on the quality and format you choose.
-
user1111929 about 9 years@NicholasKyriakides I can confirm that
canvas.toDataURL("image/jpeg",0.7)
effectively compresses it, it saves JPEG with quality 70 (as opposed to the default, quality 100). -
nicholaswmin about 9 years@user1111929 If you can revert it back to quality 100 then it is compression, otherwise if it's a one way street and you can't get it back to quality 100, it is downscaling - Unless I got my terms and understanding mixed up, not sure about that.
-
user1111929 about 9 yearsI'm sorry, I understood downscaling as just lowering the height and width, and compression as actually reducing the quality. Not sure about my terms either, but at least we agree on the contents. :)
-
Billybobbonnet about 9 years@Nicholas Kyriakides, this is not good distinction to make. Most of the codecs are not lossless, so they would fit into your "downscaling" definition (i.e. you cant revert to 100).
-
Tatarize about 8 yearsYou should convert from Base64 on the front end, append it to the FormData and submit it in binary as a file. Then you aren't adding pointless 33% to the file size and limited by the postdata size etc.
-
Tatarize about 8 yearsDownscaling refers to making images of a smaller size in terms of height and width. This really is compression. It's lossy compression but certainly compression. It isn't downscaling the pixels, it just nudges some of the pixels to being the same color so that the compression can hit those colors in fewer bits. JPEG has built in compression for the pixels anyway, but but in lossy mode it says that a few colors off can be called the same color. That's still compression. Downscaling with regard to graphics typically refers to a change in the actual size.
-
hellol11 over 7 yearsI just want to say this: the file can go straight to
URL.createObjectUrl()
without turning the file into a blob; the file counts as a blob. -
lifeisbeautiful over 7 yearswithout reducing the quality
-
visulo about 7 yearsplease, can you give me more detail about this example, how to call the function and how returned the result?
-
Vivian River about 7 yearsHere's an example: danielsadventure.info/Html/scaleimage.html Be sure to read the source of the page to see how it works.
-
Ricardo Ruiz Romero almost 7 yearsToBlob did the trick for me, creating a file and recieving in the $_FILES array on the server. Thank you!
-
hostingutilities.com about 6 yearsPerhaps something has changed since you posted this, but the second argument to that canvas.toDataURL function you mentioned is the amount of compression you want to apply.
-
Barabas almost 6 yearsI would add the explanation of
quality
,resolution
andimageType
(format of this) -
Cedric Arnould almost 6 yearsweb.archive.org/web/20171226190510/danielsadventure.info/Html/… For other people who wants to read the link propose by @DanielAllenLangdon
-
SanSolo over 5 yearsPlease add some information about the linked resources
-
Matt Pope over 5 yearsAs a heads up, sometimes image.width/height will return 0 since it hasn't loaded. You might need to convert this into an async function and listen to image.onload to get the correct image with and height.
-
Garvit Jain almost 5 yearsLooks great! But will this work on all browsers, web and mobile? (Let's ignore IE)
-
gabrielstuff almost 4 yearsThe demo page link is broken. You can test here: demo.wangyulue.com/image-conversion
-
user1432181 over 3 yearsI've added a demonstrator jsfiddle for psychowood's code sample should anyone need: jsfiddle.net/Abeeee/0wxeugrt/9
-
mangeshbhuskute about 3 yearsI like your solution, but How can we preserve EXIF data?
-
Artem Dumanov over 2 yearsThis is so simple and useful. Guys, don't even spend your time reading other answers...
-
Adel Mourad over 2 yearsIt is the best plugin so far, super easy to use and powerful. This can be the best answer. Thanks for sharing
-
Zac over 2 yearsBest answer here so far. Clean code as well
-
Adelin over 2 yearsstill can't understand a couple of things about this, like the relation between all values like accuracy and size for example ..
-
Nico Serrano over 2 yearsI used this function but was getting
application/octet-stream
instead ofimage/jpeg
in the server. To solve it I sent theblob
itself instead ofnew File(...)
-
Artur Müller Romanov over 2 years
-1
because as soon asimage.src
is assigned, the image gets loadedasynchronously
. If you have an image that is larger thantiny
your code will not run and won't throw any error messages. Had to figure this out the hard way.