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