My JavaScript book is out! Don't miss the opportunity to upgrade your beginner or average dev skills.

Sunday, December 26, 2010

100% Client Side Image Resizing

... I know, I have said "Happy Holidays" already, but yesterday, after a (annoying) picture upload in Facebook, I had a an idea ... why on earth I should have a Java plugin to perform images resizes on Facebook? Why on earth if I don't have such plugin I have to wait the possibly extremely long upload, up to 10x slower for high quality images, stressing Facebook servers for such "simple" operation as an image resize/resample could be?

The FileReader Interface

In the W3C File API, I guess part of the HTML5 buzzword, we can find all we need to perform the operation we want totally on client side. The interface is called FileReader, and it provides functionalities to read chosen files from an input node with type file, and that's it: we can even disconnect from the network and keep resizing and saving images without problems.

The Canvas Trick

Still into HTML5 buzzword world, the canvas element and it's 2dContext.drawImage method is the key to perform a resample/resize operation. It's not about changing a DOM Image node size and show it, it's about creating a totally fresh new image with exactly desired pixels size.
Once this is done, it is possible to send via Ajax the bas64 encoded image or it is possible to simply save the created image or reuse it, or resize it again ...

The Demo Page

This is the demo page I gonna show you soon, the code is hopefully self explanatory:

<!doctype html>
<html>
<head>
<title>JavaScript Image Resample :: WebReflection</title>
</head>
<body>
<input id="width" type="text" value="320" />
<input id="height" type="text" />
<input id="file" type="file" />
<br /><span id="message"></span><br />
<div id="img"></div>
</body>
<script src="resample.js"></script>
<script>
(function (global, $width, $height, $file, $message, $img) {

// (C) WebReflection Mit Style License

// simple FileReader detection
if (!global.FileReader)
// no way to do what we are trying to do ...
return $message.innerHTML = "FileReader API not supported"
;

// async callback, received the
// base 64 encoded resampled image
function resampled(data) {
$message.innerHTML = "done";
($img.lastChild || $img.appendChild(new Image)
).src = data;
}

// async callback, fired when the image
// file has been loaded
function load(e) {
$message.innerHTML = "resampling ...";
// see resample.js
Resample(
this.result,
this._width || null,
this._height || null,
resampled
);

}

// async callback, fired if the operation
// is aborted ( for whatever reason )
function abort(e) {
$message.innerHTML = "operation aborted";
}

// async callback, fired
// if an error occur (i.e. security)
function error(e) {
$message.innerHTML = "Error: " + (this.result || e);
}

// listener for the input@file onchange
$file.addEventListener("change", function change() {
var
// retrieve the width in pixel
width = parseInt($width.value, 10),
// retrieve the height in pixels
height = parseInt($height.value, 10),
// temporary variable, different purposes
file
;
// no width and height specified
// or both are NaN
if (!width && !height) {
// reset the input simply swapping it
$file.parentNode.replaceChild(
file = $file.cloneNode(false),
$file
);
// remove the listener to avoid leaks, if any
$file.removeEventListener("change", change, false);
// reassign the $file DOM pointer
// with the new input text and
// add the change listener
($file = file).addEventListener("change", change, false);
// notify user there was something wrong
$message.innerHTML = "please specify width or height";
} else if(
// there is a files property
// and this has a length greater than 0
($file.files || []).length &&
// the first file in this list
// has an image type, hopefully
// compatible with canvas and drawImage
// not strictly filtered in this example
/^image\//.test((file = $file.files[0]).type)
) {
// reading action notification
$message.innerHTML = "reading ...";
// create a new object
file = new FileReader;
// assign directly events
// as example, Chrome does not
// inherit EventTarget yet
// so addEventListener won't
// work as expected
file.onload = load;
file.onabort = abort;
file.onerror = error;
// cheap and easy place to store
// desired width and/or height
file._width = width;
file._height = height;
// time to read as base 64 encoded
// data te selected image
file.readAsDataURL($file.files[0]);
// it will notify onload when finished
// An onprogress listener could be added
// as well, not in this demo tho (I am lazy)
} else if (file) {
// if file variable has been created
// during precedent checks, there is a file
// but the type is not the expected one
// wrong file type notification
$message.innerHTML = "please chose an image";
} else {
// no file selected ... or no files at all
// there is really nothing to do here ...
$message.innerHTML = "nothing to do";
}
}, false);
}(
// the global object
this,
// all required fields ...
document.getElementById("width"),
document.getElementById("height"),
document.getElementById("file"),
document.getElementById("message"),
document.getElementById("img")
));
</script>
</html>


The resample.js File



var Resample = (function (canvas) {

// (C) WebReflection Mit Style License

// Resample function, accepts an image
// as url, base64 string, or Image/HTMLImgElement
// optional width or height, and a callback
// to invoke on operation complete
function Resample(img, width, height, onresample) {
var
// check the image type
load = typeof img == "string",
// Image pointer
i = load || img
;
// if string, a new Image is needed
if (load) {
i = new Image;
// with propers callbacks
i.onload = onload;
i.onerror = onerror;
}
// easy/cheap way to store info
i._onresample = onresample;
i._width = width;
i._height = height;
// if string, we trust the onload event
// otherwise we call onload directly
// with the image as callback context
load ? (i.src = img) : onload.call(img);
}

// just in case something goes wrong
function onerror() {
throw ("not found: " + this.src);
}

// called when the Image is ready
function onload() {
var
// minifier friendly
img = this,
// the desired width, if any
width = img._width,
// the desired height, if any
height = img._height,
// the callback
onresample = img._onresample
;
// if width and height are both specified
// the resample uses these pixels
// if width is specified but not the height
// the resample respects proportions
// accordingly with orginal size
// same is if there is a height, but no width
width == null && (width = round(img.width * height / img.height));
height == null && (height = round(img.height * width / img.width));
// remove (hopefully) stored info
delete img._onresample;
delete img._width;
delete img._height;
// when we reassign a canvas size
// this clears automatically
// the size should be exactly the same
// of the final image
// so that toDataURL ctx method
// will return the whole canvas as png
// without empty spaces or lines
canvas.width = width;
canvas.height = height;
// drawImage has different overloads
// in this case we need the following one ...
context.drawImage(
// original image
img,
// starting x point
0,
// starting y point
0,
// image width
img.width,
// image height
img.height,
// destination x point
0,
// destination y point
0,
// destination width
width,
// destination height
height
);
// retrieve the canvas content as
// base4 encoded PNG image
// and pass the result to the callback
onresample(canvas.toDataURL("image/png"));
}

var
// point one, use every time ...
context = canvas.getContext("2d"),
// local scope shortcut
round = Math.round
;

return Resample;

}(
// lucky us we don't even need to append
// and render anything on the screen
// let's keep this DOM node in RAM
// for all resizes we want
this.document.createElement("canvas"))
);


The Resample Demo In Action

First input for the width, second input for the height, if one out of 2 is defined, the resize maintain the aspect ratio.
You can even disconnect your machine from the network, since nothing is absolutely stored or saved in my website, everything simply runs in your machine.
Compatibility? Minefield and latest Chrome work pretty well. I don't have my MacMini with me right now but I will test eventually WebKit nightly later.
Happy end of 2010

32 comments:

Arnab C said...

This is totally awesome, and I am gonna use it in my next project, thanks a ton for sharing.

Cheers!

Brian said...

Looks good, I did some similar work, and made a wrapper for the FileReader component that you might find useful.

It allows drag and drop onto the page, and multiple file reads at one time. You can get the source at: https://github.com/bgrins/filereader.js

I created it for my CSS sprite generator: http://instantsprite.com.

Beben Koben said...

wow its a cool my master...
result for image in base64 code...
cheers^^

Fady said...

Brilliant! Thanx a lot for this very useful article.

However, I'm finding troubles submitting the resampled image in a classic html form:

< form...>
< input type="file" id="img" name="img" />


//Set the resampled image in the input form
function resampled(data) {
$img.value = data;
}

This is still submiting the initial image. Any ideas ?

Cheers

Andrea Giammarchi said...

easy solution is to send the base64 encoded version of the image ( via file uploader or other techniques )

have a look into my noSWFUpload project as well

http://code.google.com/p/noswfupload/

Fady said...

Thank u Andrea! I've been looking on the web, but unable to find any clear indication on how to use file uploader or other... As it's a basic upload, I'm really looking for the easiest way to upload the resampled image...
I'd be grateful if you can send me any link showing a sample code...
Thanx!

Job Resource said...

Looks great how would we modify this to upload the file to the server?

Andrea Giammarchi said...

canvas.toDataURL("image/png") or canvas.toDataURL("image/jpeg") ?

Anonymous said...

Hey Andrea,



Nice work. But how can we store it into another variable and upload the converted image to server.



I am new to JavaScript, so please help me.

Vishwa said...

@ pandeyanand and Job,
If you do a simple form submit use java script to get the data of src attribute and assign it to a hidden input.(you will need to do some string work at the client side or server side in order to omit the "data:image/png;base64," part).
I think i dont need to explain the ajax part.
In the server if you use php try this out.



If you use java use BASE64Decoder class from sun.misc.BASE64Decoder package.

Andrea, Nice work.well done.wish if IE6 wasn't here.

Mete Kavruk said...

hi this script is perfect until rookies like us do not know how to code 'uploading the file to server' part...Could you please publish it also ? thanks...

Unknown said...

Any new idea for modify or implement the image upload to server via PHP or similar?

I cant get the resized image... :(

Unknown said...

Hello, nice script and really fast/dynamic, but... How can I get the image data and pass this to a File element for make a post and upload via PHP/similar to the server side?

Any ideas?

Unknown said...

Worked fine, pass through other file variable via jquery and some coding for call external process for upload file in PHP, works like a charm! :))

James Woo (gatzat) said...

Well, it's works fine in desktop but in mobile, when i try to upload an image more than 1MB, it could upload successfully, but the image will become squeezed. any idea?

i'm using iPhone 5 with iOS6.

Andrea Giammarchi said...

is it more than 1Mb only?

Unknown said...
This comment has been removed by the author.
Andrea Giammarchi said...

either IE10 does not support bind or some canvas API such toDataURL() or something else?

Unknown said...

No it is supporting by IE 10. I did the test with the demo and everything works fine! My problem is that I call the Resample function from a Mootools Object and then in this function I call for my method placeNewAvatar.

In the example, everything is done inside differents functions

Andrea Giammarchi said...

so this code works then... oh well, I need to see/know/test your stuff then, nothing I can solve without seeing the whole thing and a debugger ^_^

Andrea Giammarchi said...

so this code works then... oh well, I need to see/know/test your stuff then, nothing I can solve without seeing the whole thing and a debugger ^_^

Mete Kavruk said...

thanks for the code.saving lives. but i want to use multi file select option. i have designed a good interface with your script hidden inside. With my interface you can select or deselect photos for any further process requested. i thought to loop through array $file.files[i] at the line file.readAsDataURL($file.files[0]); but it does process the last index... i call a php file creating images with unique name... If i can finish i can share the whole modified script here... cheers...

Andrea Giammarchi said...

you need to new FileReader each file ... it would be easier to check your code to help. Is it in github or somewhere public?

Mete Kavruk said...

i tried that below.
file.readAsDataURL($file.files[0]);
file.readAsDataURL($file.files[1]); and resample.js calls a php file with unique filename but only last index gets processed. you mean new file = new FileReader; i also tried it now to repeat the block of code for each file but does not help. I can successfully take the number of files selected and their names in files[] array.

Mete Kavruk said...

you can find your script in my interface on http://www.emlaksitesitasarim.com/submitresize/fotograf.htm when sorted out the multiple file upload part i will also make changing the the order of uploaded photos by extra user menus.

Andrea Giammarchi said...

Mete you cannot do this file.readAsDataURL($file.files[0]); and file.readAsDataURL($file.files[1]); because file has only one onload.

Again, you need to create a new FileReader **per each** image in the list.

Mete Kavruk said...

hi again, i put filereader structure into a function and call it but still the latest photo in the array is generated but if i put (very strange) an alert command between generations it succeeds creating all photos.

function dosya(isim) {

file = new FileReader;
// assign directly events
// as example, Chrome does not
// inherit EventTarget yet
// so addEventListener won't
// work as expected
file.onload = load;
file.onabort = abort;
file.onerror = error;

// cheap and easy place to store
// desired width and/or height
file._width = width;
file._height = height;
// time to read as base 64 encoded
// data te selected image
file.readAsDataURL(isim);
file.onloadend= bitti;
alert($file.files[0].name) *** if this line exists multiple files are generated!

}



dosya($file.files[0]);
dosya($file.files[1]);

Unknown said...

This is exactly what I was looking for, Great Job Andrea! I do have one question. I'm not an experienced programer so bear with me please. I have a need for the image to be saved in JPG format. I did some research and modified the code in to read:

onresample(canvas.toDataURL("image/jpeg", 0.70));

This worked fine for Firefox and Chrome but as usual not in IE 10. IE 10 still wants to save in PNG format. Any ideas on how to get IE 10 to save in JPG format?

Unknown said...

just THANK YOU !

Unknown said...

Thank you for this so much. I would like to use it to upload a down sized image to a mysql db. I have the upload working with the original sized image just fine and have your demo working as well, I just dont know how to tie the two together. My javascript is very limited. this is the PHP code I use to upload.

$image_name1 = mysql_real_escape_string($_FILES['image1']['name']);
// resize here
$image_data1 = mysql_real_escape_string(file_get_contents($_FILES['image1']['tmp_name']));
$image_type1 = mysql_real_escape_string($_FILES['image1']['type']);

$image_name2 = mysql_real_escape_string($_FILES['image2']['name']);
// resize here
$image_data2 = mysql_real_escape_string(file_get_contents($_FILES['image2']['tmp_name']));
$image_type2 = mysql_real_escape_string($_FILES['image2']['type']);

//update database
$pdo = Database::connect();
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$sql = "UPDATE rcd
SET rcd_pass1 = ". $rcd1.",
rcd_pass2 = ". $rcd2.",
rcd_notes = '". $notes ."',
rcd_photo1 = '". $image_data1 ."',
rcd_photo1a = '". $image_name1 ."',
rcd_photo2 = '". $image_data2 ."',
rcd_photo2a = '". $image_name2 ."'
WHERE insp_id = ". $insp_id;
$q = $pdo->prepare($sql);
$q->execute();
//$row = $q->fetch(PDO::FETCH_ASSOC);

Database::disconnect();

Andrea Giammarchi said...

if you upload both images I find the client side solution pointless since you have imagemagik in your server that is probably faster than double upload original/reduced

In any case, you cannot send the image as file field, you need to pass its base64 representation as extra POST parameter and base64_decode on the server side before treathing it as a BLOB in your database.

Also, since you use PDO I wonder why you manually escape everything instead of letting PDO do the job directly which is easier to write, read, and most likely more performant and reliable plus you can swap database whenever you want without changing mysql_real_escape_string per each operation ;-)

Cheers

Anonymous said...

This is what a called a life saver!