Compression and uploading of Mobile front-end images
Abstract: I was working on a mini-game platform project. A "user center" module involved the Avatar upload function. When uploading images on the Mobile End, the local images are uploaded on the mobile phone, while the local images are generally relatively large. For the current smartphone, many images are usually taken at two or three megabytes. If the image is uploaded in this way, it will be too large. If the user uses mobile traffic, it is obviously not a good way to upload images completely. Therefore, compression is necessary before uploading. After finding a lot of information on the Internet, I tried many methods and encountered many pitfalls. For example, android can compress and upload images successfully, I couldn't upload it on ios, but it took a long time to discover the pitfalls of ios. This has been proved to be feasible by practice. A few megabytes of images can be compressed to within KB of Our backend requirements! This feasible method must be shown to everyone. [ps: It's all about other people's methods ~].
Currently, all new HTML5 APIs are well implemented in Mobile webkit. According to caniuse, The FileReader, Blob, and Formdata objects used in this demo have been implemented in most mobile device browsers (safari6.0 +, android 3.0 + ), therefore, compressing images directly at the front end has become a required feature for uploading images on mobile devices.
The three h5 APIs of filereader, canvas, and formdata are used to compress images on the Mobile End and upload images. Logic is not difficult. The whole process is:
(1) When you use input file to upload an image, use filereader to read the uploaded image data (in base64 format)
(2) Pass the image data to the img object, draw the img to the canvas, and then call canvas. toDataURL to compress the image.
(3) Get the compressed base64 Format Image Data, convert it to binary and insert formdata, and then submit formdata through XmlHttpRequest.
In these three steps, the image is compressed and uploaded.
It seems quite simple to say, but there are still some pitfalls. Next, we will analyze the Code directly:
[1] retrieving image data
First, obtain the image data, that is, listen to the change event of the input file, then obtain the uploaded file object files, convert the files of the class array into an array, and then perform forEach traversal.
Then the file type is determined. If it is not an image, no processing is performed. If an image is instantiated, A filereader reads the uploaded file data in base64 format to determine the Data Length. If the image is larger than kb, compress is called for compression; otherwise, the upload method is called for upload.
Filechooser. onchange = function () {if (! This. files. length) return; var files = Array. prototype. slice. call (this. files); if (files. length> 9) {alert ("up to 9 images can be uploaded at the same time"); return;} files. forEach (function (file, I) {if (! /\/(?: Jpeg | png | gif)/I. test (file. type) return; var reader = new FileReader (); var li = document. createElement ("li"); li. innerHTML = '<div class = "progress"> <span> </div>'; $ (". img-list "). append ($ (li); reader. onload = function () {var result = this. result; var img = new Image (); img. src = result; // if the image size is smaller than kb, directly upload if (result. length <= maxsize) {detail (li2.16.css ("background-image", "url (" + result + ")"); img = null; upload (result, file. type, $ (li); return;} // compress the image after it is loaded, and then upload if (img. complete) {callback ();} else {img. onload = callback;} function callback () {var data = compress (img); callback (li2.16.css ("background-image", "url (" + data + ")"); upload (data, file. type, $ (li); img = null ;}}; reader. readAsDataURL (file );})};
[2] compressing Images
After obtaining the image data, you can compress the image. The compressed image does not directly draw the image to the canvas and then call toDataURL.
In IOS, there are two restrictions on canvas image painting:
The first is the image size. If the image size exceeds 2 million pixels, the image cannot be drawn to the canvas. No error will be reported when drawImage is called, however, when you use toDataURL to retrieve image data, you obtain null image data.
Furthermore, there is a limit on the size of the canvas. If the size of the canvas is greater than or equal to 5 million pixels (that is, the product of width and height, nothing else can be drawn.
To address the first limitation, the solution is to draw the tiles. Tile painting, that is, to divide an image into multiple parts and draw it onto the canvas. In my code, we split the image into 1 million pixels and draw it onto the canvas.
In response to the second limitation, my solution is to properly compress the image width and height. In my code, for the sake of security, the upper limit is 4 million pixels, if the image size is greater than 4 million pixels, It is compressed to less than 4 million pixels. An image of 4 million pixels should be enough. In this case, the width and height are X.
In this way, the two restrictions on IOS are solved.
In addition to the restrictions described above, there are two pitfalls. One is that the toDataURL of canvas can only be compressed into jpg files. When the image uploaded by the user is png, it needs to be converted into jpg files, that is, canvas is used in a unified manner. toDataURL ('image/jpeg ', 0.1) is set to jpeg, and the compression ratio is controlled by yourself.
The other is that if it is png to jpg, when it is drawn to the canvas, there is a transparent area in the canvas, when it is converted to jpg, the transparent area will become black, because the default transparency pixel of the canvas is rgba (,), converting it to jpg will become rgba (,), that is, the transparent background will become black. The solution is to lay a white background on the canvas.
Function compress (img) {var initSize = img. src. length; var width = img. width; var height = img. height; // if the image is larger than 4 million pixels, calculate the compression ratio and press the image size below 4 million var ratio; if (ratio = width * height/4000000)> 1) {ratio = Math. sqrt (ratio); width/= ratio; height/= ratio;} else {ratio = 1;} canvas. width = width; canvas. height = height; // bottom color ctx. fillStyle = "# fff"; ctx. fillRect (0, 0, canvas. width, canvas. height );// If the image pixel is greater than 1 million, use the tile to draw var count; if (count = width * height/1000000)> 1) {count = ~~ (Math. sqrt (count) + 1); // calculate the number of tiles to be divided. // calculate the width and height of each tile. var nw = ~~ (Width/count); var nh = ~~ (Height/count); tCanvas. width = nw; tCanvas. height = nh; for (var I = 0; I <count; I ++) {for (var j = 0; j <count; j ++) {tctx. drawImage (img, I * nw * ratio, j * nh * ratio, nw * ratio, nh * ratio, 0, 0, nw, nh); ctx. drawImage (tCanvas, I * nw, j * nh, nw, nh) ;}} else {ctx. drawImage (img, 0, 0, width, height);} // perform the minimum compression var ndata = canvas. toDataURL ('image/jpeg ', 0.1); console. log ('before compression: '+ InitSize); console. log ('compressed: '+ ndata. length); console. log ('Compression ratio:' + ~~ (100 * (initSize-ndata. length)/initSize) + "%"); tCanvas. width = tCanvas. height = canvas. width = canvas. height = 0; return ndata ;}
[3] upload images
After compressing the image, you can insert it into formdata for upload. First, convert base64 data into a string, then instantiate an ArrayBuffer, and then pass the string into ArrayBuffer in an 8-bit integer format, then, use BlobBuilder or Blob Object to convert the ArrayBuffer of an 8-bit integer into a binary object blob, append the blob Object To formdata, and send it to the background through ajax.
In XmlHttpRequest2, not only big data can be sent, but also an API for obtaining the sending progress, which is also implemented in my code.
// Upload the image. Convert the base64 image into a binary object and insert it into formdata. upload the function upload (basestr, type, $ li) {var text = window. atob (basestr. split (",") [1]); var buffer = new ArrayBuffer (text. length); var ubuffer = new Uint8Array (buffer); var pecent = 0, loop = null; for (var I = 0; I <text. length; I ++) {ubuffer [I] = text. charCodeAt (I);} var Builder = window. webKitBlobBuilder | window. mozBlobBuilder; var blob; if (Builder) {var Builder = new Builder (); builder. append (buffer); blob = builder. getBlob (type);} else {blob = new window. blob ([buffer], {type: type});} var xhr = new XMLHttpRequest (); var formdata = new FormData (); formdata. append ('imagefile', blob); xhr. open ('post', '/cupload'); xhr. onreadystatechange = function () {if (xhr. readyState = 4 & xhr. status = 200) {console. log ('uploaded successfully: '+ xhr. responseText); clea RInterval (loop); // when the message is received, the upload is complete $ li. find (". progress span "). animations ({'width': "100%"}, pecent <95? 200: 0, function () {upload (this).html ("uploaded successfully") ;}); $ (". pic-list "). append ('<a href = "' + xhr. responseText + '">' + xhr. responseText + ' </a>') }}; // data sending progress. The first 50% shows the progress xhr. upload. addEventListener ('progress', function (e) {if (loop) return; pecent = ~~ (100 * e. loaded/e. total)/2; $ li. find (". progress span ").css ('width', pecent +" % "); if (pecent = 50) {mockProgress () ;}}, false ); // function mockProgress () {if (loop) return; loop = setInterval (function () {pecent ++; $ li. find (". progress span ").css ('width', pecent +" % "); if (pecent = 99) {clearInterval (loop) ;}, 100)} xhr. send (formdata );}
At this point, the compression of the uploaded front-end image is complete. Because formdata is used for submission, the data can be processed in the background just like the data submitted by the common form.