Wednesday, April 24, 2013

Windows 8 Orientation Check

Ideally we would like to restrict rotation for some specific pages in a Windows 8 mobile app. For example, always show landscape mode for one page and portrait mode for another page. This is super simple in Android where we can configure an activity's screen orientation in AndroidManifest.xml file or set it through code. After some research, unfortunately I found it's impossible doing such simple thing in Windows 8.

As a workaround we display a warning message on the screen when the orentation is not in desired mode. First I tried OrientationSensor :

    var orientationSensor = Windows.Devices.Sensors.SimpleOrientationSensor.getDefault();
    if (orientationSensor) 
        orientationSensor.addEventListener("orientationchanged", onCameraOrientationChanged);
The event is set in a few pages not globally, but I found the event is not reliable and it's not fired sometimes when the screen is rotated. So I go back to the "resize" event and it always works:
    window.addEventListener("resize", orientationCheck);
    function orientationCheck() {
        var isOnCertainPage = 0; // Check if it's current page
        if (isOnCertainPage) { 
            if (Windows.UI.ViewManagement.ApplicationView.value != 
               Windows.UI.ViewManagement.ApplicationViewState.fullScreenLandscape) {
                // Show warning message 
            }
        }
    }

Friday, April 12, 2013

Using HTML5 Canvas to Manipulate Image in Windows 8

I presented a way to upload multiple images in my previous post. Sometime we need to manipulate the images first, such as resizing, compress, then do the upload. There's a few ways to do such job using Windows 8 JavaScript. The easiest one is use HTML5 canvas:

    // Convert image dataUri format to blob format
    function dataURItoBlob(dataURI) {
        var byteString = atob(dataURI.split(',')[1]);
        var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]

        var byteArray = new Uint8Array(byteString.length);
        for (var i = 0; i < byteString.length; i++) {
            byteArray[i] = byteString.charCodeAt(i) & 0xff;
        }

        return new Blob([byteArray], { type: mimeString });
    }

    // Use HTML5 canvas to resize and compress an image
    function resizeAndCompressImageToBlob(image, length, quality) {
        var isLandScape = image.naturalWidth > image.naturalHeight;
        var width = isLandScape ? length :  parseInt((length / image.naturalHeight) * image.naturalWidth)
        var heigh = isLandScape ? parseInt((width / image.naturalWidth) * image.naturalHeight) : length;
        var cvs = document.createElement('canvas');
        cvs.width = width;
        cvs.height = heigh;
        cvs.getContext("2d").drawImage(image, 0, 0, width, heigh);
        var dataUri = cvs.toDataURL("image/jpeg", quality);
        var imgBlob = dataURItoBlob(dataUri);
        return imgBlob;
    }

    // Load image asynchronously
    function loadImageAsync(src) {
        return new WinJS.Promise(function (c, e, p) {
            var img = new Image();
            img.src = src;
            img.addEventListener("load", function () {
                c(img);
            });
        });
    }

    function uploadMultipleImages(url, formFields) {
        return new WinJS.Promise(function (c, e, p) {
            var dataToSend = new FormData();
            var filePicker = new Windows.Storage.Pickers.FileOpenPicker();
            filePicker.fileTypeFilter.replaceAll([".jpg", ".jpeg"]);
            filePicker.suggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.picturesLibrary;

            filePicker.pickMultipleFilesAsync().then(function (files) {
                if (files.length === 0) {
                    console.log("No file selected");
                    return;
                }
                if (formFields && formFields.length > 0) {
                    formFields.forEach(function (formField) {
                        for (var key in formField) {
                            dataToSend.append(key, formField[key]); // Add field value
                        }
                    });
                }
                var imgPromises = []; // Promises for all image loading
                files.forEach(function (file, index) {
                    var objectUrl = URL.createObjectURL(file, { oneTimeOnly: true });
                    var loadImage = loadImageAsync(objectUrl, file.name);
                    imgPromises.push(loadImage);
                });

                // Wait for all async calls completion 
                WinJS.Promise.join(imgPromises).then(function (data) { 
                    var savePromieses = [];
                    for (var i = 0; i < data.length; i++) {
                        var imageBlob = resizeAndCompressImageToBlob(data[i], 1200, 0.8);
                        dataToSend.append(files[i].name, imageBlob, files[i].name);
                    }

                    var options = { url: url, type: "POST", data: dataToSend };
                    WinJS.xhr(options).then(function (response) {
                        c(response);
                    },
                    function (xhrError) {
                        e(xhrError);
                    });

                },
                function (joinError) {
                    e(joinError);
                });
            });
        });
    }

Monday, April 08, 2013

Upload Multiple Images in WinJS

This post shows a WinJS/HTML solution to upload multiple images in Windows 8. The same method can apply to other binary files, not limited to images.

Requirement : use HTML POST to upload multiple images to a server at one shot with some form values

Solution : use FormData to build the raw data and use WinJS.xhr to post the data

Code :

    /**
     * Select multiple images and upload to the server
     * @param string url: Server URL
     * @param array formFields: other form values to send
     * @return a Promise object
     */ 
    function uploadMultipleImages(url, formFields) {
        return new WinJS.Promise(function (c, e, p) {
            var dataToSend = new FormData();
            var filePicker = new Windows.Storage.Pickers.FileOpenPicker();
            filePicker.fileTypeFilter.replaceAll([".jpg", ".jpeg"]);
            filePicker.suggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.picturesLibrary;

            filePicker.pickMultipleFilesAsync().then(function (files) {
                if (files.length === 0) {
                    console.log("No file selected");
                    return;
                }
                if (formFields && formFields.length > 0) {
                    formFields.forEach(function (formField) {
                        for (var key in formField) {
                            dataToSend.append(key, formField[key]); // Add field value
                        }
                    });
                }

                var imgPromises = []; // Promises for all images access 
                files.forEach(function (file, index) {
                    imgPromises.push(file.openAsync(Windows.Storage.FileAccessMode.read));
                });

                WinJS.Promise.join(imgPromises).then(function (data) { // Wait for all promises 
                    for (var i = 0; i < data.length; i++) {
                        // data[i] is the file stream
                        var blob = MSApp.createBlobFromRandomAccessStream("image/jpg", data[i]);
                        dataToSend.append(files[i].name, blob, files[i].name);
                    }

                    var options = { url: url, type: "POST", data: dataToSend };
                    WinJS.xhr(options).then(function (response) {
                        c(response);
                    },
                    function (xhrError) {
                        e(xhrError);
                    });
                },
                function (joinError) {
                    e(joinError);
                });
            });
        });
    }

Usage example :

        var url = "http://testServer/FileUpload/OCR/";
        var formValues = [];
        formValues.push({ Token: "1234567890" });
        uploadMultipleImages(url, formValues).then(function (response) {
            var result = response.responseText;
            //DO STUFF
        });

Note : It's important to include the third parameter when appending blob data:

    var blob = MSApp.createBlobFromRandomAccessStream("image/jpg", data[i]);
    dataToSend.append(files[i].name, blob, files[i].name);
This will result in following HTTP POST:
-----------------------------multipart-form-data-boundary
Content-Disposition: form-data; name="File1.JPG"; filename="File1.JPG"
Content-Type: image/jpg;

--image binary--
If the third parameter is skipped: formData.append(objName, objValue); then the filename property will be set to "blob" as below which is not accepted in many server implementations:
-----------------------------multipart-form-data-boundary
Content-Disposition: form-data; name="File1.JPG"; filename="blob"
Content-Type: image/jpg;

--image binary--