update demo page and add object drawing capability
[face-privacy-filter.git] / web_demo / demo-framework.js
1 /*
2   ===============LICENSE_START=======================================================
3   Acumos Apache-2.0
4   ===================================================================================
5   Copyright (C) 2017-2018 AT&T Intellectual Property & Tech Mahindra. All rights reserved.
6   ===================================================================================
7   This Acumos software file is distributed by AT&T and Tech Mahindra
8   under the Apache License, Version 2.0 (the "License");
9   you may not use this file except in compliance with the License.
10   You may obtain a copy of the License at
11
12   http://www.apache.org/licenses/LICENSE-2.0
13
14   This file is distributed on an "AS IS" BASIS,
15   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   See the License for the specific language governing permissions and
17   limitations under the License.
18   ===============LICENSE_END=========================================================
19 */
20 /**
21  face-privacy.js - send frames to an face privacy service; clone from image-classes.js
22
23  Videos or camera are displayed locally and frames are periodically sent to GPU image-net classifier service (developed by Zhu Liu) via http post.
24  For webRTC, See: https://gist.github.com/greenido/6238800
25
26  D. Gibbon 6/3/15
27  D. Gibbon 4/19/17 updated to new getUserMedia api, https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
28  D. Gibbon 8/1/17 adapted for system
29  E. Zavesky 10/19/17 adapted for video+image
30  E. Zavesky 05/05/18 adapted for row-based image and other results
31  E. Zavesky 05/30/18 adapted for single image input, github preview, safe posting (forked from model-specific code)
32  E. Zavesky 07/11/18 allow proto type grouping, proto text file download, binary chunk upload (as proto)
33  E. Zavesky 07/27/18 update for use of video list for auto-population of ribbon with div, not image; add rect drawing function
34  */
35
36
37 /**
38  * main entry point
39  */
40 function demo_init(objSetting) {
41     if (!objSetting) objSetting = {};
42
43     // clone/extend the default input from our main script
44     $(document.body).data('hdparams', $.extend(true, objSetting, {      // store global vars in the body element
45         classificationServer: getUrlParameter('url-image'), // default to what's in our url prameter
46         protoObj: null,   // to be back-filled after protobuf load {'root':obj, 'methods':{'xx':{'typeIn':x, 'typeOut':y}} }
47         protoPayloadInput: null,   //payload for encoded message download (if desired)
48         protoInputName: 'in',  //input name for proto creation
49         protoPayloadOutput: null,   //payload for encoded message download (if desired)
50         protoOutputName: 'out',  //output name for proto download
51         protoKeys: null,  // currently selected protobuf method (if any)
52         frameCounter: 0,
53         totalFrames: 900000,    // stop after this many frames just to avoid sending frames forever if someone leaves page up
54         frameInterval: 500,             // Milliseconds to sleep between sending frames to reduce server load and reduce results updates
55         frameTimer: -1,         // frame clock for processing
56         maxSrcVideoWidth: 512,  // maximum image width for processing
57         serverIsLocal: true,    // server is local versus 'firewall' version
58         imageIsWaiting: false,  // blocking to prevent too many queued frames
59         colorSet: [ "#FF0000", "#FFFF00", "#00FF00", "#00FFFF", "#0000FF", "#FF00FF", 
60                     "#FFBFBF", "#FFFFBF", "#BFFFBF", "#BFFFFF", "#BFBFFF", "#FFBFFF"],  // colors for rect and highlight
61
62         // functional customizations for each demo
63         documentTitle: "Protobuf Demo",
64         mediaList: [],        //relative URLs of media files
65         protoList: [],        //relative URLs of proto files to include
66         domHeaders: { "Content-type": "text/plain;charset=UTF-8" },   //defaults for headers
67         // TODO: should be binary ideally, domHeaders: { "Content-type": "application/octet-stream;charset=UTF-8" },   //defaults for headers
68
69         // Objects from DOM elements
70         video: document.getElementById('srcVideo'),
71         srcImgCanvas: document.getElementById('srcImgCanvas'),  // we have a 'src' source image
72         srcImgCanvasBack: null,  // our back-frame to switch canvas
73         srcImgCanvasActive: 0,
74     }));
75
76     var hd = $(document.body).data('hdparams');
77     if (hd.video) {
78         hd.video.addEventListener("loadedmetadata", newVideo);
79     }
80     //create clone of canvas for better show of data
81     if (hd.srcImgCanvas) {
82         var domCanvas = $(hd.srcImgCanvas);
83         var idCopy = domCanvas.attr('id')+'_clone';
84         domCanvas.clone().attr('id', idCopy).appendTo(domCanvas.parent());
85         hd.srcImgCanvasBack = document.getElementById(idCopy);
86         $(hd.srcImgCanvasBack).hide();        
87     }
88
89
90     $("#protoSource").prop("disabled",true).click(downloadBlobProto);
91     $("#protoInput").prop("disabled",true).click(downloadBlobIn);
92     $("#protoOutput").prop("disabled",true).click(downloadBlobOut);
93     $("#resultText").hide();
94     $("#protoBinary").change(doPostBinaryFile);
95
96
97     //add text input tweak
98     $("#serverUrl").change(function() {
99         $(document.body).data('hdparams')['classificationServer'] = $(this).val();
100         updateLink("serverLink");
101     }).val($(document.body).data('hdparams')['classificationServer'])
102     //set launch link at first
103     $("#protoMethod").change(function() {
104         updateProto($(this).attr('id'));
105     });
106
107     //if protobuf is enabled, fire load event for it as well
108     hd.protoObj = {};  //clear from last load
109     $.each(hd.protoList, function(idx, protoTuple) {     //load relevant protobuf tuples
110         protobuf_load.apply(this, protoTuple);      //load each file independently
111         $("#protoSource").prop("disabled",false);
112     });
113
114     // add div to change video or image source
115     $.each(hd.mediaList, function(key) {
116         //var button = $('<button/>').text(videos[key].name).attr('movie', videos[key].url);
117         var div_area = $('<div/>');
118         var img_dom = $("<img src='"+hd.mediaList[key].img+"' />");
119         if (hd.mediaList[key].movie) {
120             img_dom.attr("movie", hd.mediaList[key].movie);
121         }
122         div_area.append(img_dom);
123         div_area.append($("<span />").append($("<a href='"+hd.mediaList[key].source+"' target='_new' />").text(hd.mediaList[key].name)));
124         $("#sourceRibbon").append(div_area);
125     });
126     
127     //add the file upload capability
128     var div_area = $('<div/>');
129     div_area.append($("<label />").text("Upload Image").append("<br />"));
130     div_area.append($("<input id='imageLoader' name='imageLoader' type='file' />"));
131     $("#sourceRibbon").append(div_area);
132     $("#imageLoader").change(function(e) {
133         clearInterval(hd.frameTimer);   // stop the processing
134         hd.video.pause();
135         $(hd.video).hide();
136         // $(hd.srcImgCanvas).show();
137         var reader = new FileReader();
138         reader.onload = function(event){
139             switchImage(event.target.result);
140         }
141         reader.readAsDataURL(e.target.files[0]);
142     });
143
144     // add buttons to change video or image
145         $("#sourceRibbon").children("div").click(function() {
146         var $this = $(this);
147         $this.siblings().removeClass('selected'); //clear other selection
148         $this.addClass('selected');
149
150         var movieAttr = $this.attr('movie');
151         var objImg = $this.children('img')[0];
152         if (objImg) {
153             movieAttr = $(objImg).attr('movie');
154             objImg = $(objImg);
155         }
156
157         clearInterval(hd.frameTimer);   // stop the processing
158         hd.video.pause();
159
160         if (movieAttr) {
161             switchVideo(movieAttr);
162         }
163         else {
164             $(hd.video).hide();
165             // $(hd.srcImgCanvas).show();
166             if (objImg) 
167                 switchImage(objImg.attr('src'));
168         }
169         }).first().click();
170 }
171
172 // trick for two-canvas fetch (essentially using a frame buffer https://en.wikipedia.org/wiki/Framebuffer#Page_flipping)
173 function canvas_get(getActive=true) {
174     var hd = $(document.body).data('hdparams');
175     if (getActive)
176         return (hd.srcImgCanvasActive == 0) ? hd.srcImgCanvas : hd.srcImgCanvasBack;
177     return (hd.srcImgCanvasActive != 0) ? hd.srcImgCanvas : hd.srcImgCanvasBack;
178 }
179
180 // flip display of the two canvases 
181 function canvas_flip() {
182     var hd = $(document.body).data('hdparams');
183     var canvasHide = canvas_get(true);
184     var canvasShow = canvas_get(false);
185     hd.srcImgCanvasActive = (hd.srcImgCanvasActive+1) % 2;
186     $(canvasHide).hide();
187     $(canvasShow).show();
188     return canvasShow;
189 }
190
191 function protobuf_load(pathProto, forceSelect) {
192     protobuf.load(pathProto, function(err, root) {
193         if (err) {
194             console.log("[protobuf]: Error!: "+err);
195             throw err;
196         }
197         var domSelect = $("#protoMethod");
198         var numMethods = domSelect.children().length;
199         $.each(root.nested, function(namePackage, objPackage) {    // walk all
200             if ('Model' in objPackage && 'methods' in objPackage.Model) {    // walk to model and functions...
201                 var typeSummary = {'root':root, 'methods':{}, 'path':pathProto };
202                 var fileBase = pathProto.split(/[\\\/]/);       // added 7/11/18 for proto context
203                 fileBase = fileBase[fileBase.length - 1];
204                 var domGroup = $("<optgroup label='"+fileBase+" - "+namePackage+"' >");
205                 $.each(objPackage.Model.methods, function(nameMethod, objMethod) {  // walk methods
206                     typeSummary['methods'][nameMethod] = {};
207                     typeSummary['methods'][nameMethod]['typeIn'] = namePackage+'.'+objMethod.requestType;
208                     typeSummary['methods'][nameMethod]['typeOut'] = namePackage+'.'+objMethod.responseType;
209                     typeSummary['methods'][nameMethod]['service'] = namePackage+'.'+nameMethod;
210
211                     //create HTML object as well
212                     var namePretty = namePackage+"."+nameMethod;
213                     var domOpt = $("<option />").attr("value", namePretty).text(
214                         nameMethod+ " (input: "+objMethod.requestType
215                         +", output: "+objMethod.responseType+")");
216                     if (numMethods==0) {    // first method discovery
217                         domSelect.append($("<option />").attr("value","").text("(disabled, not loaded)")); //add 'disabled'
218                     }
219                     if (forceSelect) {
220                         domOpt.attr("selected", 1);
221                     }
222                     domGroup.append(domOpt);
223                     numMethods++;
224                 });
225                 domSelect.append(domGroup);
226                 $(document.body).data('hdparams').protoObj[namePackage] = typeSummary;   //save new method set
227                 $("#protoContainer").show();
228             }
229         });
230         console.log("[protobuf]: Load successful, found "+numMethods+" model methods.");
231     });
232 }
233
234 // modify the link and update our url
235 function updateLink(domId) {
236     var newServer = $(document.body).data('hdparams')['classificationServer'];
237     var sNewUrl = updateQueryStringParameter(window.location.href, "url-image", newServer, "?");
238     $("#"+domId).attr('href', sNewUrl);
239     $("#serverUrl").val(newServer);
240     //window.history.pushState({}, $(document.body).data('hdparams')['documentTitle'], sNewUrl);
241 }
242
243 // update proto link
244 function updateProto(domProtoCombo) {
245     var nameProtoMethod = $("#"+domProtoCombo+" option:selected").attr('value');
246     $(document.body).data('hdparams').protoKeys = null;
247     if (nameProtoMethod && nameProtoMethod.length) {     //valid protobuf type?
248         var partsURL = $(document.body).data('hdparams').classificationServer.split("/");
249         var protoKeys = nameProtoMethod.split(".", 2);       //modified for multiple detect/pixelate models
250         $(document.body).data('hdparams').protoKeys = protoKeys;
251         partsURL[partsURL.length-1] = protoKeys[1];
252         $(document.body).data('hdparams').classificationServer = partsURL.join("/");   //rejoin with new endpoint
253         updateLink("serverLink", $(document.body).data('hdparams').classificationServer);
254     }
255 }
256
257 // https://stackoverflow.com/a/6021027
258 function updateQueryStringParameter(uri, key, value, separator) {
259     var re = new RegExp("([?&])" + key + "=.*?(&|$)", "i");
260     if (uri.match(re)) {
261         return uri.replace(re, '$1' + key + "=" + value + '$2');
262     }
263     if (!separator) //allow forced/override
264        separator = uri.indexOf('?') !== -1 ? "&" : "?";
265     return uri + separator + key + "=" + value;
266 }
267
268 // https://stackoverflow.com/a/3354511
269 window.onpopstate = function(e){
270     if(e.state){
271         //document.getElementById("content").innerHTML = e.state.html;
272         $(document.body).data('hdparams')['documentTitle'] = e.state.pageTitle;
273     }
274 };
275
276 function getUrlParameter(key) {
277     var re = new RegExp("([?&])" + key + "=(.*?)(&|$)", "i");
278     var match = window.location.search.match(re)
279     if (match) {
280         //console.log(match);
281         return match[match.length-2];
282     }
283 };
284
285 /**
286  * Change the video source and restart
287  */
288 function switchVideo(movieAttr) {
289         var hd = $(document.body).data('hdparams');
290
291     // Set the video source based on URL specified in the 'videos' list, or select camera input
292     $(hd.video).show();
293     // $(hd.srcImgCanvas).hide();
294     if (movieAttr == "Camera") {
295         var constraints = {audio: false, video: true};
296         navigator.mediaDevices.getUserMedia(constraints)
297             .then(function(mediaStream) {
298                 hd.video.srcObject = mediaStream;
299                 hd.video.play();
300             })
301             .catch(function(err) {
302                 console.log(err.name + ": " + err.message);
303             });
304     } else {
305         var mp4 = document.getElementById("mp4");
306         mp4.setAttribute("src", movieAttr);
307         hd.video.load();
308         hd.video.autoplay = true;
309         newVideo();
310     }
311 }
312
313 /**
314  * Called after a new video has loaded (at least the video metadata has loaded)
315  */
316 function newVideo() {
317         var hd = $(document.body).data('hdparams');
318         hd.frameCounter = 0;
319         hd.imageIsWaiting = false;
320
321         // set processing canvas size based on source video
322         var pwidth = hd.video.videoWidth;
323         var pheight = hd.video.videoHeight;
324         if (pwidth > hd.maxSrcVideoWidth) {
325                 pwidth = hd.maxSrcVideoWidth;
326                 pheight = Math.floor((pwidth / hd.video.videoWidth) * pheight); // preserve aspect ratio
327     }
328     var canvasAct = canvas_get();
329         canvasAct.width = pwidth;
330         canvasAct.height = pheight;
331
332     updateProto("protoMethod");
333     hd.frameTimer = setInterval(nextFrame, hd.frameInterval); // start the processing
334 }
335
336
337 /**
338  * process the next video frame
339  */
340 function nextFrame() {
341         var hd = $(document.body).data('hdparams');
342         if (hd.video.ended || hd.video.paused) {
343                 return;
344         }
345     switchImage(hd.video, true);
346 }
347
348 function switchImage(imgSrc, isVideo) {
349     var canvas = canvas_get(false);
350     if (!isVideo) {
351         var img = new Image();
352         img.crossOrigin = "Anonymous";
353         img.onload = function () {
354             var ctx = canvas.getContext('2d');
355             var canvasCopy = document.createElement("canvas");
356             var copyContext = canvasCopy.getContext("2d");
357
358             var ratio = 1;
359
360             //console.log( $(document.body).data('hdparams'));
361             //console.log( [ img.width, img.height]);
362             // https://stackoverflow.com/a/2412606
363             if(img.width > $(document.body).data('hdparams')['canvasMaxW'])
364                 ratio = $(document.body).data('hdparams')['canvasMaxW'] / img.width;
365             if(ratio*img.height > $(document.body).data('hdparams')['canvasMaxH'])
366                 ratio = $(document.body).data('hdparams')['canvasMaxH'] / img.height;
367
368             canvasCopy.width = img.width;
369             canvasCopy.height = img.height;
370             copyContext.drawImage(img, 0, 0);
371
372             canvas.width = img.width * ratio;
373             canvas.height = img.height * ratio;
374             ctx.drawImage(canvasCopy, 0, 0, canvasCopy.width, canvasCopy.height, 0, 0, canvas.width, canvas.height);
375             //document.removeChild(canvasCopy);
376             updateProto("protoMethod");
377             doPostImage(canvas, '#resultsDiv', '#destImg', canvas.toDataURL());
378         }
379         img.src = imgSrc;  //copy source, let image load
380     }
381     else if (!$(document.body).data('hdparams').imageIsWaiting) {
382         var ctx = canvas.getContext('2d');
383         var canvasCopy = document.createElement("canvas");
384         var copyContext = canvasCopy.getContext("2d");
385         var ratio = 1;
386
387         if(imgSrc.videoWidth > $(document.body).data('hdparams')['canvasMaxW'])
388             ratio = $(document.body).data('hdparams')['canvasMaxW'] / imgSrc.videoWidth;
389         if(ratio*imgSrc.videoHeight > $(document.body).data('hdparams')['canvasMaxH'])
390             ratio = $(document.body).data('hdparams')['canvasMaxH'] / canvasCopy.height;
391
392         //console.log("Canvas Copy:"+canvasCopy.width+"/"+canvasCopy.height);
393         //console.log("Canvas Ratio:"+ratio);
394         //console.log("Video: "+imgSrc.videoWidth+"x"+imgSrc.videoHeight);
395         canvasCopy.width = imgSrc.videoWidth;     //large as possible
396         canvasCopy.height = imgSrc.videoHeight;
397         copyContext.drawImage(imgSrc, 0, 0);
398
399         canvas.width = canvasCopy.width * ratio;
400         canvas.height = canvasCopy.height * ratio;
401         ctx.drawImage(canvasCopy, 0, 0, canvasCopy.width, canvasCopy.height, 0, 0, canvas.width, canvas.height);
402         //document.removeChild(canvasCopy);
403         doPostImage(canvas, '#resultsDiv', '#destImg', canvas.toDataURL());
404     }
405 }
406
407
408 /**
409  * post an image from the canvas to the service
410  */
411 function doPostImage(srcCanvas, dstDiv, dstImg, imgPlaceholder) {
412     var dataURL = srcCanvas.toDataURL('image/jpeg', 1.0);
413     var hd = $(document.body).data('hdparams');
414     var sendPayload = null;
415
416     hd.imageIsWaiting = true;
417     var domHeaders = {};
418
419     //console.log("[doPostImage]: Selected method ... '"+typeInput+"'");
420     if (hd.protoKeys) {     //valid protobuf type?
421         var blob = dataURItoBlob(dataURL, true);
422         domHeaders = $.extend({}, hd.domHeaders);       //rewrite with defaults
423
424         // fields from .proto file at time of writing...
425         // message Image {
426         //   string mime_type = 1;
427         //   bytes image_binary = 2;
428         // }
429
430         //TODO: should we always assume this is input? answer: for now, YES, always image input!
431         var inputPayload = { "mimeType": blob.type, "imageBinary": blob.bytes };
432
433         // ---- method for processing from a type ----
434         var msgInput = hd.protoObj[hd.protoKeys[0]]['root'].lookupType(hd.protoObj[hd.protoKeys[0]]['methods'][hd.protoKeys[1]]['typeIn']);
435         // Verify the payload if necessary (i.e. when possibly incomplete or invalid)
436         var errMsg = msgInput.verify(inputPayload);
437         if (errMsg) {
438             var strErr = "[doPostImage]: Error during type verify for object input into protobuf method. ("+errMsg+")";
439             $(dstDiv).empty().html(strErr);
440             console.log(strErr);
441             throw Error(strErr);
442         }
443         // Create a new message
444         var msgTransmit = msgInput.create(inputPayload);
445         // Encode a message to an Uint8Array (browser) or Buffer (node)
446         sendPayload = msgInput.encode(msgTransmit).finish();
447
448         //downloadBlob(sendPayload, 'protobuf.bin', 'application/octet-stream');
449         // NOTE: TO TEST THIS BINARY BLOB, use some command-line magic like this...
450         //  protoc --decode=mMJuVapnmIbrHlZGKyuuPDXsrkzpGqcr.FaceImage model.proto < protobuf.bin
451         $("#protoInput").prop("disabled",false);
452         hd.protoPayloadInput = sendPayload;
453         // got the input name, from the type, use it here
454         hd.protoInputName = hd.protoObj[hd.protoKeys[0]]['methods'][hd.protoKeys[1]]['typeIn'];
455         hd.protoOutputName = hd.protoObj[hd.protoKeys[0]]['methods'][hd.protoKeys[1]]['typeOut'];
456
457         //request.responseType = 'arraybuffer';
458     }
459     else if (hd.protoList.length) {
460         var strErr = "[doPostImage]: Proto method expected but unavailable in POST, aborting send.";
461         console.log(strErr);
462         throw Error(strErr);
463     }
464     else {
465         var blob = dataURItoBlob(dataURL, false);
466         sendPayload = new FormData();
467         if (hd.serverIsLocal) {
468             serviceURL = hd.classificationServer;
469             sendPayload.append("image_binary", blob);
470             sendPayload.append("mime_type", blob.type);
471         }
472         else {      //disabled now for direct URL specification
473             serviceURL = hd.classificationServerFirewall;
474             sendPayload.append("myFile", blob);
475             sendPayload.append("rtnformat", "json");
476             sendPayload.append("myList", "5");  // limit the number of classes (max 1000)
477         }
478     }
479     doPostPayload(sendPayload, domHeaders, dstDiv, dstImg, imgPlaceholder);
480 }
481
482
483 function doPostBinaryFile(e)  {
484     // https://stackoverflow.com/a/10811427
485     // https://stackoverflow.com/a/17512132
486     var fileReader = new FileReader();
487     fileReader.onload = function(e) {
488         console.log("[doPostBinaryFile]: Sending uploaded binary file of length "+e.target.result.byteLength);
489         doPostBlob(e.target.result);
490     };
491     var fileLocal = $(this)[0].files[0];
492     fileReader.readAsArrayBuffer(fileLocal);
493
494     //usage: $("#ingredient_file").change(doPostBinaryFile);
495 }
496
497 function doPostBlob(blobData)
498 {
499     doPostPayload(blobData, null, '#resultsDiv', '#destImg', null);
500 }
501
502
503 function doPostPayload(sendPayload, domHeaders, dstDiv, dstImg, imgPlaceholder)
504 {
505     var hd = $(document.body).data('hdparams');
506     hd.imageIsWaiting = true;
507
508     dstDiv = $(dstDiv);
509     $("#postSpinner").remove();     //erase previously existing one
510     dstDiv.append($("<div id='postSpinner' class='spinner'>&nbsp;</div>"));
511     if (dstImg)     //convert to jquery dom object
512         dstImg = $(dstImg);
513     if (!domHeaders)    //pull existing headers from config
514         domHeaders = $(document.body).data('hdparams').domHeaders;
515
516     //$(dstImg).addClaas('workingImage').siblings('.spinner').remove().after($("<span class='spinner'>&nbsp;</span>"));
517     $.ajax({
518         type: 'POST',
519         url: hd.classificationServer,
520         data: sendPayload,
521         crossDomain: true,
522         dataType: 'native',
523         xhrFields: {
524             responseType: 'arraybuffer'
525         },
526         processData: false,
527         headers: domHeaders,
528         error: function (data, textStatus, errorThrown) {
529             //console.log(textStatus);
530             if (textStatus=="error") {
531                 textStatus += " (Was the transform URL valid? Was the right method selected?) ";
532             }
533             var errStr = "Error: Failed javascript POST (err: "+textStatus+","+errorThrown+")";
534             console.log(errStr);
535             dstDiv.html(errStr);
536             hd.imageIsWaiting = false;
537             return false;
538         },
539         success: function(data, textStatus, jqXHR) {
540             // what do we do with a good processing result?
541             //
542             //  data: the raw body from the response
543             //  dstImg: the dom element of a destination image
544             //  methodKeys: which protomethod was selected
545             //  dstImg: the dom element of a destination image (if available)
546             //  imgPlaceholder: the exported canvas image from last source
547             //
548             canvas_flip();
549             var returnState = processResult(data, dstDiv, hd.protoKeys, dstImg, imgPlaceholder);
550             hd.imageIsWaiting = false;
551             return returnState;
552         }
553         });
554 }
555
556 /**
557  * convert base64/URLEncoded data component to raw binary data held in a string
558  *
559  * Stoive, http://stackoverflow.com/questions/4998908/convert-data-uri-to-file-then-append-to-formdata
560  */
561 function dataURItoBlob(dataURI, wantBytes) {
562     // convert base64/URLEncoded data component to raw binary data held in a string
563     var byteString;
564     if (dataURI.split(',')[0].indexOf('base64') >= 0)
565         byteString = atob(dataURI.split(',')[1]);
566     else
567         byteString = unescape(dataURI.split(',')[1]);
568
569     // separate out the mime component
570     var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
571
572     // write the bytes of the string to a typed array
573     var ia = new Uint8Array(byteString.length);
574     for (var i = 0; i < byteString.length; i++) {
575         ia[i] = byteString.charCodeAt(i);
576     }
577     //added for returning bytes directly
578     if (wantBytes) {
579         return {'bytes':ia, 'type':mimeString};
580     }
581     return new Blob([ia], {type:mimeString});
582 }
583
584 // https://stackoverflow.com/a/12713326
585 function Uint8ToString(u8a){
586   var CHUNK_SZ = 0x8000;
587   var c = [];
588   for (var i=0; i < u8a.length; i+=CHUNK_SZ) {
589     c.push(String.fromCharCode.apply(null, u8a.subarray(i, i+CHUNK_SZ)));
590   }
591   return c.join("");
592 }
593
594 function BlobToDataURI(data, mime) {
595     var b64encoded = btoa(Uint8ToString(data));
596     return "data:"+mime+";base64,"+b64encoded;
597 }
598
599 // ----- diagnostic tool to download binary blobs ----
600 function downloadBlobOut() {
601     var hd = $(document.body).data('hdparams');
602     return downloadBlob(hd.protoPayloadOutput, "protobuf."+hd.protoOutputName+".bin");
603 }
604
605 function downloadBlobIn() {
606     var hd = $(document.body).data('hdparams');
607     return downloadBlob(hd.protoPayloadInput, "protobuf."+hd.protoInputName+".bin");
608 }
609
610 function downloadBlobProto() {
611     var namePackage = $(document.body).data('hdparams').protoKeys[0];
612     var pathProto = $(document.body).data('hdparams').protoObj[namePackage]['path'];
613     protobuf.util.fetch(pathProto, {binary:true}, function(statusStr, data) {
614         var fileBase = pathProto.split(/[\\\/]/);       // added 7/11/18 for proto context
615         fileBase = fileBase[fileBase.length - 1];
616         return downloadBlob(data, fileBase, "text/plain");
617     });
618 }
619
620
621 //  https://stackoverflow.com/a/33622881
622 function downloadBlob(data, fileName, mimeType) {
623     //if there is no data, filename, or mime provided, make our own
624     if (!data)
625         data = $(document.body).data('hdparams').protoPayloadInput;
626     if (!fileName)
627         fileName = "protobuf.bin";
628     if (!mimeType)
629         mimeType = "application/octet-stream";
630
631     var blob, url;
632     blob = new Blob([data], {
633         type: mimeType
634     });
635     url = window.URL.createObjectURL(blob);
636     downloadURL(url, fileName, mimeType);
637     setTimeout(function() {
638         return window.URL.revokeObjectURL(url);
639     }, 1000);
640 };
641
642 function downloadURL(data, fileName) {
643     var a;
644     a = document.createElement('a');
645     a.href = data;
646     a.download = fileName;
647     document.body.appendChild(a);
648     a.style = 'display: none';
649     a.click();
650     a.remove();
651 };
652
653
654 //load image that has been uploaded into a canvas
655 function handleImage(e){
656     var reader = new FileReader();
657     reader.onload = function(event){
658         switchImage(event.target.result);
659     }
660     reader.readAsDataURL(e.target.files[0]);
661 }
662
663
664 // draw a region in the source canvas 
665 function canvas_rect(clear_first, r_left, r_top, r_width, r_height, r_color) {
666     if (!r_color) r_color = "blue";
667
668     var line_width = 4;
669     var src_canvas = canvas_get();
670     
671     var hd = $(document.body).data('hdparams');
672     var ctx = src_canvas.getContext('2d');
673     if (clear_first) {
674         ctx.clearRect(0, 0, src_canvas.width, src_canvas.height);
675     }
676
677     //key to starting different colors
678     ctx.beginPath();
679     ctx.lineWidth=line_width;
680     var offsWidth = Math.floor(line_width/2);
681     ctx.strokeStyle=r_color;
682     ctx.moveTo(r_left+offsWidth, r_top+offsWidth);
683     ctx.lineTo(r_left+offsWidth+r_width, r_top+offsWidth);
684     ctx.lineTo(r_left+offsWidth+r_width, r_top+offsWidth+r_height);
685     ctx.lineTo(r_left+offsWidth, r_top+offsWidth+r_height);
686     ctx.lineTo(r_left+offsWidth, r_top+offsWidth);
687     ctx.stroke();
688     //ctx.strokeRect(r_left+line_width, r_top+line_width, r_width, r_height);
689     //console.log("[canvas_rect]: "+r_left+","+r_top+"x"+r_width+","+r_height+", color:"+r_color);
690 }
691
692