Pages

Thursday, January 18, 2018

Resizing selected pictures a browser before uploading them to a REST resource in a backend

The sample application uploads multipart data comprising data from several text inputs together with several photo files select in file type input to a JAX-RS rest resource. Note, multipart data is not mentioned in JAX-RS specification. So the back end uses RESTEasy-specific features. Before uploading, the files are resized in the browser. Then, they are scaled down to thumbnails in the back end.

The web application is adapted for Wildfly, but it works as well with Tomcat if the scope of RESTEasy-related dependencies is changed from provided to the default by removing it.

How to style a file input

The input will accept multiple but only images. One cannot change much the file type input. A workaround is to use a label tag and hide the input with css. Note, the ugly styling here serves merely to demonstrate that styling is possible.

<label id='dropbox' for='fileInput'><img src="imgs/File-Upload-icon.png"/>Select photos</label>
<input id='fileInput' type="file" accept="image/*" multiple />

A sample css:

input[type=file] {
    display: none;
}
label img {
    max-height: 1.5em;
}

label {
    border: 1px solid;
    display: inline-block;
    padding: 0.3em;
}
Resizing selected files using canvas and its function onBlob

The unique resized files are stored in an array:

var selectedFiles = []; // the array with the unique resized files that will be uploaded

When new pictures are selected using the file input, a change event listener is invoked:

$('input[type=file]').change(function () {
    resizeAndShowThumbs(this.files);
});
function resizeAndShowThumbs(files) {
    for (var c = 0; c < files.length; c++) {
        var file = files[c];
        if (file.type.startsWith("image/") && isFileNotYetIncluded(file)) {
            resize(file, showThumb);
        }
    }
}
function isFileNotYetIncluded(file) {
    for (var c = 0; c < selectedFiles.length; c++) {
        if (selectedFiles[c].originalNameSize.equals(file)) { // file has name and size read-only properties
            return false;
        }
    }
    return true;
}

The event listener calls the resize function only if a file is not yet included in the array. The files are identified by their names and initial sizes. After a file is resized the callback showThumb is called.

function showThumb(file) {
    selectedFiles.push(file);
    showMessage();
    $previewList.append('<li><p>' + file.originalNameSize.name + '</p><img src="' + URL.createObjectURL(file)
            + '"  onload="window.URL.revokeObjectURL(this.src);"/></li>');
}

The resized picture have jpeg compression. The problem with resizing is that sometimes a resized jpeg-compressed file is has a bigger size than the source file with bigger dimensions. So the file with smaller size is selected between the source and resized files. On the back-end the pictures are converted into thumbnails using ImageIO class, which accepts only jpg, bmp, gif, png formats. In the unlikely case of the source file having an unacceptable format, the resized jpeg file will be uploaded even if it is bigger.

var MAX_SIZE = 1200, MIME = 'image/jpeg', JPEG_QUALITY = 0.95;
// the files types accepted by java ImageIO
var acceptableTypes = ["image/gif", "image/png", "image/jpeg", "image/bmp"]; 

function size(size) {
    var i = Math.floor(Math.log(size) / Math.log(1024));
    return (size / Math.pow(1024, i)).toFixed(2) * 1 + ['b', 'kb', 'Mb'][i];
}

function resizePhoto(file, callback) {
    var image = new Image();
    image.onload = function ( ) {
        URL.revokeObjectURL(this.src);
        var canvas = document.createElement('canvas');
        var width = this.width;
        var height = this.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);
        canvas.toBlob(callback.bind(null, this.width, this.height, width, height), MIME, JPEG_QUALITY);
    };
    image.src = URL.createObjectURL(file);
}


function chooseSmallerFile(file, resizedFile) {
    if (file.size > resizedFile.size) {
        console.log('the resized file is smaller');
        return resizedFile;
    } else {
        // resized is bigger than the original
        // however, java ImageIO supports only  jpg, bmp, gif, png, which perferctly match mime types, the front-end should send only those types
        // if the file type is none of image/gif, image/png, image/jpeg, image/bmp use the bigger resized file
        console.warn('resized is bigger the the original');
        if (acceptableTypes.indexOf(file.type) >= 0) {
            return file;
        } else {
            console.warn('but the source file type is unacceptable: ' + file.type);
            return  resizedFile;
        }
    }
}

 function resize(file, callback) {
    resizePhoto(file, function (originalWidth, originalHeight, resizedWidth, resizedHeight, resizedFile) {
        console.log('filename=' + file.name + '; size=' + size(file.size) + '=>' + size(resizedFile.size)
                + '; dimensions=' + originalWidth + '/' + originalHeight + '=>' + resizedWidth + '/' + resizedHeight);
        var smallerFile = chooseSmallerFile(file, resizedFile);
        smallerFile.originalNameSize = new NameAndSize(file.name, file.size); // name is erased in the resized file. the name and size are used to select unique files
        callback(smallerFile);
    });
};

The resizing code produces in the console lots of file size related debug messages. For example, when many pictures coming from different sources are selected:

The console messages indicate that sometimes it is cheaper to upload the original file with the bigger dimensions:

Dragging and dropping photos

Instead of clicking the file input label, one can drop on it the files dragged from any file browser. To implement drag and drop, only few lines are required:

$('#dropbox').on("dragenter", onDragEnter).on("dragover", onDragOver).on("drop", onDrop);

function onDragEnter(e) {
    e.stopPropagation();
    e.preventDefault();
}

function onDragOver(e) {
    e.stopPropagation();
    e.preventDefault();
}

function onDrop(e) {
    e.stopPropagation();
    e.preventDefault();
    resizeAndShowThumbs(e.originalEvent.dataTransfer.files);
}

How the resized photos together with values from other inputs can be posted as multipart form data to a REST resource is described in a separate post, because this one would be to long.

No comments:

Post a Comment