src/pyams_skin/resources/js/ext/flot/jquery.flot.composeImages.js
changeset 566 a1707c607eec
parent 565 318533413200
child 567 bca1726b1d85
equal deleted inserted replaced
565:318533413200 566:a1707c607eec
     1 /** ## jquery.flot.composeImages.js
       
     2 
       
     3 This plugin is used to expose a function used to overlap several canvases and
       
     4 SVGs, for the purpose of creating a snaphot out of them.
       
     5 
       
     6 ### When composeImages is used:
       
     7 When multiple canvases and SVGs have to be overlapped into a single image
       
     8 and their offset on the page, must be preserved.
       
     9 
       
    10 ### Where can be used:
       
    11 In creating a downloadable snapshot of the plots, axes, cursors etc of a graph.
       
    12 
       
    13 ### How it works:
       
    14 The entry point is composeImages function. It expects an array of objects,
       
    15 which should be either canvases or SVGs (or a mix). It does a prevalidation
       
    16 of them, by verifying if they will be usable or not, later in the flow.
       
    17 After selecting only usable sources, it passes them to getGenerateTempImg
       
    18 function, which generates temporary images out of them. This function
       
    19 expects that some of the passed sources (canvas or SVG) may still have
       
    20 problems being converted to an image and makes sure the promises system,
       
    21 used by composeImages function, moves forward. As an example, SVGs with
       
    22 missing information from header or with unsupported content, may lead to
       
    23 failure in generating the temporary image. Temporary images are required
       
    24 mostly on extracting content from SVGs, but this is also where the x/y
       
    25 offsets are extracted for each image which will be added. For SVGs in
       
    26 particular, their CSS rules have to be applied.
       
    27 After all temporary images are generated, they are overlapped using
       
    28 getExecuteImgComposition function. This is where the destination canvas
       
    29 is set to the proper dimensions. It is then output by composeImages.
       
    30 This function returns a promise, which can be used to wait for the whole
       
    31 composition process. It requires to be asynchronous, because this is how
       
    32 temporary images load their data.
       
    33 */
       
    34 
       
    35 (function($) {
       
    36     "use strict";
       
    37     const GENERALFAILURECALLBACKERROR = -100; //simply a negative number
       
    38     const SUCCESSFULIMAGEPREPARATION = 0;
       
    39     const EMPTYARRAYOFIMAGESOURCES = -1;
       
    40     const NEGATIVEIMAGESIZE = -2;
       
    41     var pixelRatio = 1;
       
    42     var browser = $.plot.browser;
       
    43     var getPixelRatio = browser.getPixelRatio;
       
    44 
       
    45     function composeImages(canvasOrSvgSources, destinationCanvas) {
       
    46         var validCanvasOrSvgSources = canvasOrSvgSources.filter(isValidSource);
       
    47         pixelRatio = getPixelRatio(destinationCanvas.getContext('2d'));
       
    48 
       
    49         var allImgCompositionPromises = validCanvasOrSvgSources.map(function(validCanvasOrSvgSource) {
       
    50             var tempImg = new Image();
       
    51             var currentPromise = new Promise(getGenerateTempImg(tempImg, validCanvasOrSvgSource));
       
    52             return currentPromise;
       
    53         });
       
    54 
       
    55         var lastPromise = Promise.all(allImgCompositionPromises).then(getExecuteImgComposition(destinationCanvas), failureCallback);
       
    56         return lastPromise;
       
    57     }
       
    58 
       
    59     function isValidSource(canvasOrSvgSource) {
       
    60         var isValidFromCanvas = true;
       
    61         var isValidFromContent = true;
       
    62         if ((canvasOrSvgSource === null) || (canvasOrSvgSource === undefined)) {
       
    63             isValidFromContent = false;
       
    64         } else {
       
    65             if (canvasOrSvgSource.tagName === 'CANVAS') {
       
    66                 if ((canvasOrSvgSource.getBoundingClientRect().right === canvasOrSvgSource.getBoundingClientRect().left) ||
       
    67                     (canvasOrSvgSource.getBoundingClientRect().bottom === canvasOrSvgSource.getBoundingClientRect().top)) {
       
    68                     isValidFromCanvas = false;
       
    69                 }
       
    70             }
       
    71         }
       
    72         return isValidFromContent && isValidFromCanvas && (window.getComputedStyle(canvasOrSvgSource).visibility === 'visible');
       
    73     }
       
    74 
       
    75     function getGenerateTempImg(tempImg, canvasOrSvgSource) {
       
    76         tempImg.sourceDescription = '<info className="' + canvasOrSvgSource.className + '" tagName="' + canvasOrSvgSource.tagName + '" id="' + canvasOrSvgSource.id + '">';
       
    77         tempImg.sourceComponent = canvasOrSvgSource;
       
    78 
       
    79         return function doGenerateTempImg(successCallbackFunc, failureCallbackFunc) {
       
    80             tempImg.onload = function(evt) {
       
    81                 tempImg.successfullyLoaded = true;
       
    82                 successCallbackFunc(tempImg);
       
    83             };
       
    84 
       
    85             tempImg.onabort = function(evt) {
       
    86                 tempImg.successfullyLoaded = false;
       
    87                 console.log('Can\'t generate temp image from ' + tempImg.sourceDescription + '. It is possible that it is missing some properties or its content is not supported by this browser. Source component:', tempImg.sourceComponent);
       
    88                 successCallbackFunc(tempImg); //call successCallback, to allow snapshot of all working images
       
    89             };
       
    90 
       
    91             tempImg.onerror = function(evt) {
       
    92                 tempImg.successfullyLoaded = false;
       
    93                 console.log('Can\'t generate temp image from ' + tempImg.sourceDescription + '. It is possible that it is missing some properties or its content is not supported by this browser. Source component:', tempImg.sourceComponent);
       
    94                 successCallbackFunc(tempImg); //call successCallback, to allow snapshot of all working images
       
    95             };
       
    96 
       
    97             generateTempImageFromCanvasOrSvg(canvasOrSvgSource, tempImg);
       
    98         };
       
    99     }
       
   100 
       
   101     function getExecuteImgComposition(destinationCanvas) {
       
   102         return function executeImgComposition(tempImgs) {
       
   103             var compositionResult = copyImgsToCanvas(tempImgs, destinationCanvas);
       
   104             return compositionResult;
       
   105         };
       
   106     }
       
   107 
       
   108     function copyCanvasToImg(canvas, img) {
       
   109         img.src = canvas.toDataURL('image/png');
       
   110     }
       
   111 
       
   112     function getCSSRules(document) {
       
   113         var styleSheets = document.styleSheets,
       
   114             rulesList = [];
       
   115         for (var i = 0; i < styleSheets.length; i++) {
       
   116             // CORS requests for style sheets throw and an exception on Chrome > 64
       
   117             try {
       
   118                 // in Chrome, the external CSS files are empty when the page is directly loaded from disk
       
   119                 var rules = styleSheets[i].cssRules || [];
       
   120                 for (var j = 0; j < rules.length; j++) {
       
   121                     var rule = rules[j];
       
   122                     rulesList.push(rule.cssText);
       
   123                 }
       
   124             } catch (e) {
       
   125                 console.log('Failed to get some css rules');
       
   126             }
       
   127         }
       
   128         return rulesList;
       
   129     }
       
   130 
       
   131     function embedCSSRulesInSVG(rules, svg) {
       
   132         var text = [
       
   133             '<svg class="snapshot ' + svg.classList + '" width="' + svg.width.baseVal.value * pixelRatio + '" height="' + svg.height.baseVal.value * pixelRatio + '" viewBox="0 0 ' + svg.width.baseVal.value + ' ' + svg.height.baseVal.value + '" xmlns="http://www.w3.org/2000/svg">',
       
   134             '<style>',
       
   135             '/* <![CDATA[ */',
       
   136             rules.join('\n'),
       
   137             '/* ]]> */',
       
   138             '</style>',
       
   139             svg.innerHTML,
       
   140             '</svg>'
       
   141         ].join('\n');
       
   142         return text;
       
   143     }
       
   144 
       
   145     function copySVGToImgMostBrowsers(svg, img) {
       
   146         var rules = getCSSRules(document),
       
   147             source = embedCSSRulesInSVG(rules, svg);
       
   148 
       
   149         source = patchSVGSource(source);
       
   150 
       
   151         var blob = new Blob([source], {type: "image/svg+xml;charset=utf-8"}),
       
   152             domURL = self.URL || self.webkitURL || self,
       
   153             url = domURL.createObjectURL(blob);
       
   154         img.src = url;
       
   155     }
       
   156 
       
   157     function copySVGToImgSafari(svg, img) {
       
   158         // Use this method to convert a string buffer array to a binary string.
       
   159         // Do so by breaking up large strings into smaller substrings; this is necessary to avoid the
       
   160         // "maximum call stack size exceeded" exception that can happen when calling 'String.fromCharCode.apply'
       
   161         // with a very long array.
       
   162         function buildBinaryString (arrayBuffer) {
       
   163             var binaryString = "";
       
   164             const utf8Array = new Uint8Array(arrayBuffer);
       
   165             const blockSize = 16384;
       
   166             for (var i = 0; i < utf8Array.length; i = i + blockSize) {
       
   167                 const binarySubString = String.fromCharCode.apply(null, utf8Array.subarray(i, i + blockSize));
       
   168                 binaryString = binaryString + binarySubString;
       
   169             }
       
   170             return binaryString;
       
   171         };
       
   172 
       
   173         var rules = getCSSRules(document),
       
   174             source = embedCSSRulesInSVG(rules, svg),
       
   175             data,
       
   176             utf8BinaryString;
       
   177 
       
   178         source = patchSVGSource(source);
       
   179 
       
   180         // Encode the string as UTF-8 and convert it to a binary string. The UTF-8 encoding is required to
       
   181         // capture unicode characters correctly.
       
   182         utf8BinaryString = buildBinaryString(new (TextEncoder || TextEncoderLite)('utf-8').encode(source));
       
   183 
       
   184         data = "data:image/svg+xml;base64," + btoa(utf8BinaryString);
       
   185         img.src = data;
       
   186     }
       
   187 
       
   188     function patchSVGSource(svgSource) {
       
   189         var source = '';
       
   190         //add name spaces.
       
   191         if (!svgSource.match(/^<svg[^>]+xmlns="http:\/\/www\.w3\.org\/2000\/svg"/)) {
       
   192             source = svgSource.replace(/^<svg/, '<svg xmlns="http://www.w3.org/2000/svg"');
       
   193         }
       
   194         if (!svgSource.match(/^<svg[^>]+"http:\/\/www\.w3\.org\/1999\/xlink"/)) {
       
   195             source = svgSource.replace(/^<svg/, '<svg xmlns:xlink="http://www.w3.org/1999/xlink"');
       
   196         }
       
   197 
       
   198         //add xml declaration
       
   199         return '<?xml version="1.0" standalone="no"?>\r\n' + source;
       
   200     }
       
   201 
       
   202     function copySVGToImg(svg, img) {
       
   203         if (browser.isSafari() || browser.isMobileSafari()) {
       
   204             copySVGToImgSafari(svg, img);
       
   205         } else {
       
   206             copySVGToImgMostBrowsers(svg, img);
       
   207         }
       
   208     }
       
   209 
       
   210     function adaptDestSizeToZoom(destinationCanvas, sources) {
       
   211         function containsSVGs(source) {
       
   212             return source.srcImgTagName === 'svg';
       
   213         }
       
   214 
       
   215         if (sources.find(containsSVGs) !== undefined) {
       
   216             if (pixelRatio < 1) {
       
   217                 destinationCanvas.width = destinationCanvas.width * pixelRatio;
       
   218                 destinationCanvas.height = destinationCanvas.height * pixelRatio;
       
   219             }
       
   220         }
       
   221     }
       
   222 
       
   223     function prepareImagesToBeComposed(sources, destination) {
       
   224         var result = SUCCESSFULIMAGEPREPARATION;
       
   225         if (sources.length === 0) {
       
   226             result = EMPTYARRAYOFIMAGESOURCES; //nothing to do if called without sources
       
   227         } else {
       
   228             var minX = sources[0].genLeft;
       
   229             var minY = sources[0].genTop;
       
   230             var maxX = sources[0].genRight;
       
   231             var maxY = sources[0].genBottom;
       
   232             var i = 0;
       
   233 
       
   234             for (i = 1; i < sources.length; i++) {
       
   235                 if (minX > sources[i].genLeft) {
       
   236                     minX = sources[i].genLeft;
       
   237                 }
       
   238 
       
   239                 if (minY > sources[i].genTop) {
       
   240                     minY = sources[i].genTop;
       
   241                 }
       
   242             }
       
   243 
       
   244             for (i = 1; i < sources.length; i++) {
       
   245                 if (maxX < sources[i].genRight) {
       
   246                     maxX = sources[i].genRight;
       
   247                 }
       
   248 
       
   249                 if (maxY < sources[i].genBottom) {
       
   250                     maxY = sources[i].genBottom;
       
   251                 }
       
   252             }
       
   253 
       
   254             if ((maxX - minX <= 0) || (maxY - minY <= 0)) {
       
   255                 result = NEGATIVEIMAGESIZE; //this might occur on hidden images
       
   256             } else {
       
   257                 destination.width = Math.round(maxX - minX);
       
   258                 destination.height = Math.round(maxY - minY);
       
   259 
       
   260                 for (i = 0; i < sources.length; i++) {
       
   261                     sources[i].xCompOffset = sources[i].genLeft - minX;
       
   262                     sources[i].yCompOffset = sources[i].genTop - minY;
       
   263                 }
       
   264 
       
   265                 adaptDestSizeToZoom(destination, sources);
       
   266             }
       
   267         }
       
   268         return result;
       
   269     }
       
   270 
       
   271     function copyImgsToCanvas(sources, destination) {
       
   272         var prepareImagesResult = prepareImagesToBeComposed(sources, destination);
       
   273         if (prepareImagesResult === SUCCESSFULIMAGEPREPARATION) {
       
   274             var destinationCtx = destination.getContext('2d');
       
   275 
       
   276             for (var i = 0; i < sources.length; i++) {
       
   277                 if (sources[i].successfullyLoaded === true) {
       
   278                     destinationCtx.drawImage(sources[i], sources[i].xCompOffset * pixelRatio, sources[i].yCompOffset * pixelRatio);
       
   279                 }
       
   280             }
       
   281         }
       
   282         return prepareImagesResult;
       
   283     }
       
   284 
       
   285     function adnotateDestImgWithBoundingClientRect(srcCanvasOrSvg, destImg) {
       
   286         destImg.genLeft = srcCanvasOrSvg.getBoundingClientRect().left;
       
   287         destImg.genTop = srcCanvasOrSvg.getBoundingClientRect().top;
       
   288 
       
   289         if (srcCanvasOrSvg.tagName === 'CANVAS') {
       
   290             destImg.genRight = destImg.genLeft + srcCanvasOrSvg.width;
       
   291             destImg.genBottom = destImg.genTop + srcCanvasOrSvg.height;
       
   292         }
       
   293 
       
   294         if (srcCanvasOrSvg.tagName === 'svg') {
       
   295             destImg.genRight = srcCanvasOrSvg.getBoundingClientRect().right;
       
   296             destImg.genBottom = srcCanvasOrSvg.getBoundingClientRect().bottom;
       
   297         }
       
   298     }
       
   299 
       
   300     function generateTempImageFromCanvasOrSvg(srcCanvasOrSvg, destImg) {
       
   301         if (srcCanvasOrSvg.tagName === 'CANVAS') {
       
   302             copyCanvasToImg(srcCanvasOrSvg, destImg);
       
   303         }
       
   304 
       
   305         if (srcCanvasOrSvg.tagName === 'svg') {
       
   306             copySVGToImg(srcCanvasOrSvg, destImg);
       
   307         }
       
   308 
       
   309         destImg.srcImgTagName = srcCanvasOrSvg.tagName;
       
   310         adnotateDestImgWithBoundingClientRect(srcCanvasOrSvg, destImg);
       
   311     }
       
   312 
       
   313     function failureCallback() {
       
   314         return GENERALFAILURECALLBACKERROR;
       
   315     }
       
   316 
       
   317     // used for testing
       
   318     $.plot.composeImages = composeImages;
       
   319 
       
   320     function init(plot) {
       
   321         // used to extend the public API of the plot
       
   322         plot.composeImages = composeImages;
       
   323     }
       
   324 
       
   325     $.plot.plugins.push({
       
   326         init: init,
       
   327         name: 'composeImages',
       
   328         version: '1.0'
       
   329     });
       
   330 })(jQuery);