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