Mobile front-end image compression upload

Posted by nylex on Mon, 20 May 2019 00:21:31 +0200

ABSTRACT: Before doing a small game platform project, there was a "user center" module, which involved the function of uploading avatars. When uploading pictures on mobile terminals, they are all local pictures of mobile phones, and the local pictures are generally relatively large. Take smart phones today, usually many pictures are taken in two or three trillion. If uploaded directly in this way, the pictures will be too big. If users use mobile traffic, it is obviously not a good way to upload pictures completely. So it is necessary to compress before uploading. After searching for a lot of information on the Internet, we tried many methods and encountered many pits. For example, Android succeeded in compressing uploaded pictures, but could not upload them on ios. It took a long time to find the pit of ios. This kind of picture has been proved to be feasible by practice. At last, it can be compressed to less than 200k required by our backend. Such a feasible method must be shown to you.

 

At present, all kinds of new API s of HTML5 have been well implemented on mobile webkit. According to caniuse, FileReader, Blob and Formdata objects used in this demo have been implemented in most mobile browsers (safari 6.0+, android 3.0+), so compressing pictures directly in the front-end has become a necessary function for many mobile image uploading.

Compressing pictures on the mobile side and uploading them are mainly used for filereader, canvas and formdata, the three h5 api s. Logic is not difficult. The whole process is:

(1) When users upload pictures using input file, they use filereader to read image data uploaded by users (base64 format)

(2) Put the image data into the img object, then draw the IMG onto canvas, then call canvas.toDataURL to compress the image.

(3) Get the compressed image data in base64 format, convert it into binary and plug it into form data, then submit form data through XmlHttpRequest.

In these three steps, the image compression and upload are completed.

It seems quite simple to say, but there are still some pits. Next, we will analyze it directly with code:

Getting Picture Data

The first step is to get the image data, that is, to listen for the change event of input file, and then to get the uploaded file object files, to convert the files of the class array into an array, and then to do forEach traversal.

Then determine the file type and do not process if it is not an image. If it is a picture, instantiate a filereader, read the uploaded file data in base64 format, and judge the length of the data. If the picture is larger than 200KB, compress method is called to compress, otherwise upload method is called to 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 pictures 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></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 less than 200 kb, upload it directly
            if (result.length <= maxsize) {
                $(li).css("background-image", "url(" + result + ")");
                img = null;
                upload(result, file.type, $(li));

                return;
            }

            //After loading the picture, compress it and upload it.
            if (img.complete) {
                callback();
            } else {
                img.onload = callback;
            }

            function callback() {
                var data = compress(img);

                $(li).css("background-image", "url(" + data + ")");

                upload(data, file.type, $(li));

                img = null;
            }

        };

        reader.readAsDataURL(file);
    })
};


Compressed pictures

After acquiring the image data, compress can be used to compress the image. Compressed images are not directly drawn to canvas and then call the toData URL.

In IOS, canvas draws pictures with two limitations:

The first is the size of the picture. If the size of the picture exceeds two million pixels, the picture can not be drawn on canvas. When drawing image is called, there will be no error, but when you use toDataURL to get the picture data, you get the empty picture data.

Moreover, the size of canvas is limited. If the size of canvas is larger than about five million pixels (i.e. width-height product), not only the picture can not be drawn, but also nothing else can be drawn.

To deal with the first limitation, tile drawing is the way to deal with it. Tile drawing, that is to say, divide the picture into several blocks and draw it on canvas. The way in my code is to divide the picture into one million pixels and then draw it on canvas.

For the second limitation, my approach is to compress the width of the image appropriately. For the sake of insurance, the upper limit in my code is 4 million pixels, and if the image is larger than 4 million pixels, it will be compressed to less than 4 million pixels. Four megapixels should be enough. In all, the width and height are 2000X2000.

In this way, two limitations on IOS are solved.

In addition to the limitations mentioned above, there are two pits. One is that the toData URL of canvas can only compress jpg. When the user uploads a picture that is png, it needs to be converted to jpg, that is, using canvas.toData URL ('image/jpeg', 0.1), the type is set to jpeg, and the compression ratio is controlled by itself.

The other is that if png turns to JPG and draws to canvas, the transparent area will turn black when it turns to jpg, because the transparent pixel of canvas defaults to RGBA (0,0,0,0,0,0), so it turns to RGBA (0,0,1), that is, the transparent background will turn black. The solution is to lay a white background on canvas before drawing.

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 size below 4 million pixels.
    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;

    //Underlay
    ctx.fillStyle = "#fff";
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    //Use tiles if the image pixels are larger than 1 million
    var count;
    if ((count = width * height / 1000000) > 1) {
        count = ~~(Math.sqrt(count) + 1); //Calculate how many tiles to divide

        //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);
    }

    //Minimum compression
    var ndata = canvas.toDataURL('image/jpeg', 0.1);

    console.log('Before compression:' + initSize);
    console.log('After compression:' + ndata.length);
    console.log('Compression ratio:' + ~~(100 * (initSize - ndata.length) / initSize) + "%");

    tCanvas.width = tCanvas.height = canvas.width = canvas.height = 0;

    return ndata;
}

 

Upload pictures

After image compression, it can be uploaded into form data. First, the base64 data is converted into a string, then an ArrayBuffer is instantiated, then the string is transferred into ArrayBuffer in 8-bit integer format, then through BlobBuilder or Blob object, the ArrayBuffer of 8-bit integer is converted into a binary object blob, and then the blob object append into form data, and then through AJ Buffer, the ArrayBuffer of 8-bit integer is converted into a binary object blob. Ax can be sent to the background.

XmlHttpRequest2 not only can send large data, but also has many API s to get the progress of sending, which is also implemented in my code.

//Picture upload, converting base64's image into binary object and inserting it into form data upload
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('Upload success:' + xhr.responseText);

            clearInterval(loop);

            //Upload the message when it is received
            $li.find(".progress span").animate({
                'width': "100%"
            }, pecent < 95 ? 200 : 0, function() {
                $(this).html("Upload success");
            });

            $(".pic-list").append('<a href="' + xhr.responseText + '">' + xhr.responseText + '<img src="' + xhr.responseText + '" /></a>')
        }
    };

    //Data transmission progress, the first 50% show 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);

    //Fifty percent of the data is simulated
    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);
}

 

Up to now, the entire uploaded front-end image compression is completed, because it is submitted using form data, so the background receives data when it is processed as the normal form submission data.

Topics: Mobile iOS less Android