Office 365 Saturday Perth #O365PER summary

On 22nd of May I had the pleasure of presenting O365 Saturday Perth.  In the past years, we have called this SharePoint Saturday.  We re-branded the name and this year had a mix of content from SharePoint On-Premises, SharePoint Online as well as Azure and Office 365.

People from Perth are always early risers - the room was quite packed early in the day.

Here's James Milne presenting the keynote.

I presented Introducing PhantomJS: Headless browser for SharePoint/Online.

Had lots of feedback about what people found it interesting, many people said it has potential for scenarios that they didn't think were previously possible.  I'm very keen to hear more feedback especially for different business cases.

Some of the feedback and demos will be rolled into an updated content over the year, so stay tuned.

Here are the PowerPoint and ZIP files for the presentation.

 

SharePoint Add-in: Accessing Webcam with Only Javascript

This blog post details how to access your Webcam via Javascript through the browser, and upload that content to a SharePoint library.  Then, with an added bonus, set it to be your User Profile Picture.

I have build this User Profile Webcam solution as an Add-in for SharePoint (was App for SharePoint).  This is a SharePoint Hosted app.  All the code runs in the browser and access SharePoint via the SharePoint Online API.

Step 1.  Access your webcam.

Modern Browsers:  Immersive Internet Explorer, Firefox, Chrome and Safari all has ways to access your webcam via

getUserMedia

 

But this doesn't work on IE (Desktop).  So for that we'll add a Polyfill (Flash). 

 

There's a project on github that does this nicely, and I use large chunks of the code from their demo.

https://github.com/addyosmani/getUserMedia.js

<script type="text/javascript" src="../Scripts/html5.js"></script>
<script type="text/javascript" src="../Scripts/getUserMedia.min.js"></script>
<!-- Add your JavaScript to the following file -->
<script type="text/javascript" src="../Scripts/App.js"></script>


<div id="webcam"></div>
<canvas id="canvas" height="240" width="320"></canvas>

<br />
<button class="btn" style="width:140px;" id="takeSnapshot">Take a picture</button>
this.snapshotBtn = document.getElementById('takeSnapshot');
$(this.snapshotBtn).click(this.getSnapshot);

this.getSnapshot = function () {
    // If the current context is WebRTC/getUserMedia (something
    // passed back from the shim to avoid doing further feature
    // detection), we handle getting video/images for our canvas 
    // from our HTML5 <video> element.
    if (App.options.context === 'webrtc') {
        var video = document.getElementsByTagName('video')[0];
        //App.canvas.width = video.videoWidth;
        //App.canvas.height = video.videoHeight;
        App.canvas.getContext('2d').drawImage(video, 0, 0, App.canvas.width, App.canvas.height);

        // Otherwise, if the context is Flash, we ask the shim to
        // directly call window.webcam, where our shim is located
        // and ask it to capture for us.
    } else if (App.options.context === 'flash') {
        window.webcam.capture();
        App.changeFilter();
    }
    else {
        alert('No context was supplied to getSnapshot()');
        return false;
    }
}

The code basically takes either the webrtc object and copy the image to the App canvas.  Or use the flash polyfill and Flash will write the image to the canvas element.

 

Step 2. Binary Format

This part is hairy.  But you are a developer, and dealing with Binary Encoding in JavaScript is what we do.

/*
Now, get canvas and do decoding.
1. canvas can return image data in png, in dataURL format.
2. strip the heading string "data:image/png;base64,"
*/

var ctx = App.canvas.getContext('2d');
var imageData = ctx.getImageData(0, 0, App.canvas.width, App.canvas.height);
var dataURL = App.canvas.toDataURL('image/png');
var base64 = dataURL.replace(/^data:image\/png;base64,/, "");

This is base64 dataUrl format.

 

/*
Reading.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding
https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#Solution_.232_.E2.80.93_rewriting_atob%28%29_and_btoa%28%29_using_TypedArrays_and_UTF-8

We need to convert DataURL to ByteArray
Mozilla has these two functions.
*/

function b64ToUint6(nChr) {
    return nChr > 64 && nChr < 91 ?
        nChr - 65
      : nChr > 96 && nChr < 123 ?
        nChr - 71
      : nChr > 47 && nChr < 58 ?
        nChr + 4
      : nChr === 43 ?
        62
      : nChr === 47 ?
        63
      :
        0;
}

function base64DecToArr(sBase64, nBlocksSize) {
    var
      sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""), nInLen = sB64Enc.length,
      nOutLen = nBlocksSize ? Math.ceil((nInLen * 3 + 1 >> 2) / nBlocksSize) * nBlocksSize : nInLen * 3 + 1 >> 2, taBytes = new Uint8Array(nOutLen);

    for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) {
        nMod4 = nInIdx & 3;
        nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 6 * (3 - nMod4);
        if (nMod4 === 3 || nInLen - nInIdx === 1) {
            for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) {
                taBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255;
            }
            nUint24 = 0;
        }
    }
    return taBytes;
}

var byteArray = base64DecToArr(base64);

This gives us ByteArray.  We're getting close.

I'd like to tell you that I spend a good 3 evenings working to this point.  Try debugging that code and see if your eyes bleed!

/*

Finally, 

The ByteArray needs to be encoded as a string, and in the POST call, "binaryStringRequestBody" needs to be "true"

https://msdn.microsoft.com/en-us/library/office/dn769086.aspx
http://blogs.msdn.com/b/uksharepoint/archive/2013/04/20/uploading-files-using-the-rest-api-and-client-side-techniques.aspx
http://sharepoint.stackexchange.com/questions/54218/sharepoint-2013-rest-api-upload-image

*/

var binaryString = '';
var len = byteArray.byteLength;
for (var i = 0; i < len; i++) {
    binaryString += String.fromCharCode(byteArray[i])
}

SharePoint API wants data in BinaryString format.  Which is basically each byte of the ByteArray encoded as a concatenated string.

 

 

Step 3.  Upload to SharePoint

/*
Use RequestExecutor to post file back to sharepoint
*/

var appWebUrl = context.get_url();
var requestExecutor = new SP.RequestExecutor(appWebUrl);

var uploadPictureEndPoint = appWebUrl + "/_api/web/lists/getByTitle(@TargetLibrary)/RootFolder/Files/add(url=@TargetFileName,overwrite='true')?" +
    "&@TargetLibrary='" + "Pictures" + "'" +
    "&@TargetFileName='" + _spPageContextInfo.userLoginName + ".png" + "'";

/*
Using RequestExecutor, don't need REQUESTDIGEST
*/
//var digest = $("#__REQUESTDIGEST").val();

requestExecutor.executeAsync({
    url: uploadPictureEndPoint,
    method: "POST",
    headers: {
        "Accept": "application/json;odata=verbose"
    },
    contentType: "application/json;odata=verbose",
    binaryStringRequestBody: true,  // binaryStringRequestBody must be true
    body: binaryString,
    success: function (x, y, z) {
        alert("Success! Your file was uploaded to SharePoint.");
    },
    error: function (x, y, z) {
        alert("Oooooops... it looks like something went wrong uploading your file.");
    }
});

 

Step 4.  Setting User Profile Picture.

 

var appWebUrl = context.get_url();
var requestExecutor = new SP.RequestExecutor(appWebUrl);
var setPictureEndpoint = appWebUrl + "/_api/sp.userprofiles.peoplemanager/setmyprofilepicture";

requestExecutor.executeAsync({
    url: setPictureEndpoint,
    method: "POST",
    headers: {
        "Accept": "application/json;odata=verbose"
    },
    contentType: "application/json;odata=verbose",
    binaryStringRequestBody: true,
    body: App.binary,
    success: function (data) {
        alert('Set My Profile Picture succeeded, it will take a few seconds for the change to be propagated.');
    },
    error: function (error) {
        alert("Oooooops... it looks like something went wrong updating your profile picture (no permission?).");
    }
});

For this API call to /setmyprofilepicture to succeed, your App must have additional permissions.

This will ask the user (or Tenant Administrator) to grant the correct permission.

If you don't grant this permission, the webcam can still save picture to library, but it won't be able to set picture as user profile picture.

Summary

  • Get Webcam via Browser
  • Process canvas data to imageUrl to byteArray to BinaryString
  • Upload to SharePoint Library
  • Set as User Profile Picture
  • Achieve Zoolander face!
  • You can download the App for free from the Office Store to see it in action.  
    Feel free as a developer to hit F12 and step through the code.
  • Leave a comment below and let me know what you think.  For example, I think there's a need for an Outlook Add-in that uses the webcam.  Especially, if it can run on an iPad.

 

InfoPath Javascript - fixing image control tooltip.

In a previous blog post I discussed using Picture Controls to host lots of images within InfoPath and manage the tooltip.

InfoPath FormServer renders picture buttons with a pop-up Picture Icon that will hide any alt-text that you've set on that picture.

The key is in the Core.js file, which you should NOT modify.  As this is what could happen if you modify core javascript files.

function LinkedPicture_OnMouseOver(a, c) {
    ULSrLq:;
    var b = ViewDataNode_GetViewDataNodeFromHtml(a),
        d = PictureControl_GetPrimaryDataNodeValue(b);

    /* This modification hides the picture icon when the control is disabled */
    var e = PictureControl_GetPicture(a);
    if (e && e.alt == "Picture") {
        e.alt = "";  e.title = "";
    }
    if (typeof(a.disabled) != "undefined" && a.disabled) {
        return;
    }
    /* end modification */

    a = PictureControl_EnsureControl(a);
    d != "" && PictureControl_ShowPictureIcon(a, b, true);
    LeafControl_OnMouseOver(a, c);
}
LinkedPicture.OnMouseOver = LinkedPicture_OnMouseOver;

So instead, you should create a separate webpart page, put the Form control on that page, and add this script override for the LinkedPicture_OnMouseOver function. 

The key is "if (a.disabled) return"

Then in your InfoPath form template, set the control to read-only will ensure the Picture icon doesn't appear on hover, allowing the title text on your image to show up on the browser.