123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325 |
- /** ## jquery.flot.composeImages.js
- This plugin is used to expose a function used to overlap several canvases and
- SVGs, for the purpose of creating a snaphot out of them.
- ### When composeImages is used:
- When multiple canvases and SVGs have to be overlapped into a single image
- and their offset on the page, must be preserved.
- ### Where can be used:
- In creating a downloadable snapshot of the plots, axes, cursors etc of a graph.
- ### How it works:
- The entry point is composeImages function. It expects an array of objects,
- which should be either canvases or SVGs (or a mix). It does a prevalidation
- of them, by verifying if they will be usable or not, later in the flow.
- After selecting only usable sources, it passes them to getGenerateTempImg
- function, which generates temporary images out of them. This function
- expects that some of the passed sources (canvas or SVG) may still have
- problems being converted to an image and makes sure the promises system,
- used by composeImages function, moves forward. As an example, SVGs with
- missing information from header or with unsupported content, may lead to
- failure in generating the temporary image. Temporary images are required
- mostly on extracting content from SVGs, but this is also where the x/y
- offsets are extracted for each image which will be added. For SVGs in
- particular, their CSS rules have to be applied.
- After all temporary images are generated, they are overlapped using
- getExecuteImgComposition function. This is where the destination canvas
- is set to the proper dimensions. It is then output by composeImages.
- This function returns a promise, which can be used to wait for the whole
- composition process. It requires to be asynchronous, because this is how
- temporary images load their data.
- */
- (function($) {
- "use strict";
- const GENERALFAILURECALLBACKERROR = -100; //simply a negative number
- const SUCCESSFULIMAGEPREPARATION = 0;
- const EMPTYARRAYOFIMAGESOURCES = -1;
- const NEGATIVEIMAGESIZE = -2;
- var pixelRatio = 1;
- var browser = $.plot.browser;
- var getPixelRatio = browser.getPixelRatio;
- function composeImages(canvasOrSvgSources, destinationCanvas) {
- var validCanvasOrSvgSources = canvasOrSvgSources.filter(isValidSource);
- pixelRatio = getPixelRatio(destinationCanvas.getContext('2d'));
- var allImgCompositionPromises = validCanvasOrSvgSources.map(function(validCanvasOrSvgSource) {
- var tempImg = new Image();
- var currentPromise = new Promise(getGenerateTempImg(tempImg, validCanvasOrSvgSource));
- return currentPromise;
- });
- var lastPromise = Promise.all(allImgCompositionPromises).then(getExecuteImgComposition(destinationCanvas), failureCallback);
- return lastPromise;
- }
- function isValidSource(canvasOrSvgSource) {
- var isValidFromCanvas = true;
- var isValidFromContent = true;
- if ((canvasOrSvgSource === null) || (canvasOrSvgSource === undefined)) {
- isValidFromContent = false;
- } else {
- if (canvasOrSvgSource.tagName === 'CANVAS') {
- if ((canvasOrSvgSource.getBoundingClientRect().right === canvasOrSvgSource.getBoundingClientRect().left) ||
- (canvasOrSvgSource.getBoundingClientRect().bottom === canvasOrSvgSource.getBoundingClientRect().top)) {
- isValidFromCanvas = false;
- }
- }
- }
- return isValidFromContent && isValidFromCanvas && (window.getComputedStyle(canvasOrSvgSource).visibility === 'visible');
- }
- function getGenerateTempImg(tempImg, canvasOrSvgSource) {
- tempImg.sourceDescription = '<info className="' + canvasOrSvgSource.className + '" tagName="' + canvasOrSvgSource.tagName + '" id="' + canvasOrSvgSource.id + '">';
- tempImg.sourceComponent = canvasOrSvgSource;
- return function doGenerateTempImg(successCallbackFunc, failureCallbackFunc) {
- tempImg.onload = function(evt) {
- tempImg.successfullyLoaded = true;
- successCallbackFunc(tempImg);
- };
- tempImg.onabort = function(evt) {
- tempImg.successfullyLoaded = false;
- 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);
- successCallbackFunc(tempImg); //call successCallback, to allow snapshot of all working images
- };
- tempImg.onerror = function(evt) {
- tempImg.successfullyLoaded = false;
- 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);
- successCallbackFunc(tempImg); //call successCallback, to allow snapshot of all working images
- };
- generateTempImageFromCanvasOrSvg(canvasOrSvgSource, tempImg);
- };
- }
- function getExecuteImgComposition(destinationCanvas) {
- return function executeImgComposition(tempImgs) {
- var compositionResult = copyImgsToCanvas(tempImgs, destinationCanvas);
- return compositionResult;
- };
- }
- function copyCanvasToImg(canvas, img) {
- img.src = canvas.toDataURL('image/png');
- }
- function getCSSRules(document) {
- var styleSheets = document.styleSheets,
- rulesList = [];
- for (var i = 0; i < styleSheets.length; i++) {
- // in Chrome, the external CSS files are empty when the page is directly loaded from disk
- var rules = styleSheets[i].cssRules || [];
- for (var j = 0; j < rules.length; j++) {
- var rule = rules[j];
- rulesList.push(rule.cssText);
- }
- }
- return rulesList;
- }
- function embedCSSRulesInSVG(rules, svg) {
- var text = [
- '<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">',
- '<style>',
- '/* <![CDATA[ */',
- rules.join('\n'),
- '/* ]]> */',
- '</style>',
- svg.innerHTML,
- '</svg>'
- ].join('\n');
- return text;
- }
- function copySVGToImgMostBrowsers(svg, img) {
- var rules = getCSSRules(document),
- source = embedCSSRulesInSVG(rules, svg);
- source = patchSVGSource(source);
- var blob = new Blob([source], {type: "image/svg+xml;charset=utf-8"}),
- domURL = self.URL || self.webkitURL || self,
- url = domURL.createObjectURL(blob);
- img.src = url;
- }
- function copySVGToImgSafari(svg, img) {
- // Use this method to convert a string buffer array to a binary string.
- // Do so by breaking up large strings into smaller substrings; this is necessary to avoid the
- // "maximum call stack size exceeded" exception that can happen when calling 'String.fromCharCode.apply'
- // with a very long array.
- function buildBinaryString (arrayBuffer) {
- var binaryString = "";
- const utf8Array = new Uint8Array(arrayBuffer);
- const blockSize = 16384;
- for (var i = 0; i < utf8Array.length; i = i + blockSize) {
- const binarySubString = String.fromCharCode.apply(null, utf8Array.subarray(i, i + blockSize));
- binaryString = binaryString + binarySubString;
- }
- return binaryString;
- };
- var rules = getCSSRules(document),
- source = embedCSSRulesInSVG(rules, svg),
- data,
- utf8BinaryString;
- source = patchSVGSource(source);
- // Encode the string as UTF-8 and convert it to a binary string. The UTF-8 encoding is required to
- // capture unicode characters correctly.
- utf8BinaryString = buildBinaryString(new (TextEncoder || TextEncoderLite)('utf-8').encode(source));
- data = "data:image/svg+xml;base64," + btoa(utf8BinaryString);
- img.src = data;
- }
- function patchSVGSource(svgSource) {
- var source = '';
- //add name spaces.
- if (!svgSource.match(/^<svg[^>]+xmlns="http:\/\/www\.w3\.org\/2000\/svg"/)) {
- source = svgSource.replace(/^<svg/, '<svg xmlns="http://www.w3.org/2000/svg"');
- }
- if (!svgSource.match(/^<svg[^>]+"http:\/\/www\.w3\.org\/1999\/xlink"/)) {
- source = svgSource.replace(/^<svg/, '<svg xmlns:xlink="http://www.w3.org/1999/xlink"');
- }
- //add xml declaration
- return '<?xml version="1.0" standalone="no"?>\r\n' + source;
- }
- function copySVGToImg(svg, img) {
- if (browser.isSafari() || browser.isMobileSafari()) {
- copySVGToImgSafari(svg, img);
- } else {
- copySVGToImgMostBrowsers(svg, img);
- }
- }
- function adaptDestSizeToZoom(destinationCanvas, sources) {
- function containsSVGs(source) {
- return source.srcImgTagName === 'svg';
- }
- if (sources.find(containsSVGs) !== undefined) {
- if (pixelRatio < 1) {
- destinationCanvas.width = destinationCanvas.width * pixelRatio;
- destinationCanvas.height = destinationCanvas.height * pixelRatio;
- }
- }
- }
- function prepareImagesToBeComposed(sources, destination) {
- var result = SUCCESSFULIMAGEPREPARATION;
- if (sources.length === 0) {
- result = EMPTYARRAYOFIMAGESOURCES; //nothing to do if called without sources
- } else {
- var minX = sources[0].genLeft;
- var minY = sources[0].genTop;
- var maxX = sources[0].genRight;
- var maxY = sources[0].genBottom;
- var i = 0;
- for (i = 1; i < sources.length; i++) {
- if (minX > sources[i].genLeft) {
- minX = sources[i].genLeft;
- }
- if (minY > sources[i].genTop) {
- minY = sources[i].genTop;
- }
- }
- for (i = 1; i < sources.length; i++) {
- if (maxX < sources[i].genRight) {
- maxX = sources[i].genRight;
- }
- if (maxY < sources[i].genBottom) {
- maxY = sources[i].genBottom;
- }
- }
- if ((maxX - minX <= 0) || (maxY - minY <= 0)) {
- result = NEGATIVEIMAGESIZE; //this might occur on hidden images
- } else {
- destination.width = Math.round(maxX - minX);
- destination.height = Math.round(maxY - minY);
- for (i = 0; i < sources.length; i++) {
- sources[i].xCompOffset = sources[i].genLeft - minX;
- sources[i].yCompOffset = sources[i].genTop - minY;
- }
- adaptDestSizeToZoom(destination, sources);
- }
- }
- return result;
- }
- function copyImgsToCanvas(sources, destination) {
- var prepareImagesResult = prepareImagesToBeComposed(sources, destination);
- if (prepareImagesResult === SUCCESSFULIMAGEPREPARATION) {
- var destinationCtx = destination.getContext('2d');
- for (var i = 0; i < sources.length; i++) {
- if (sources[i].successfullyLoaded === true) {
- destinationCtx.drawImage(sources[i], sources[i].xCompOffset * pixelRatio, sources[i].yCompOffset * pixelRatio);
- }
- }
- }
- return prepareImagesResult;
- }
- function adnotateDestImgWithBoundingClientRect(srcCanvasOrSvg, destImg) {
- destImg.genLeft = srcCanvasOrSvg.getBoundingClientRect().left;
- destImg.genTop = srcCanvasOrSvg.getBoundingClientRect().top;
- if (srcCanvasOrSvg.tagName === 'CANVAS') {
- destImg.genRight = destImg.genLeft + srcCanvasOrSvg.width;
- destImg.genBottom = destImg.genTop + srcCanvasOrSvg.height;
- }
- if (srcCanvasOrSvg.tagName === 'svg') {
- destImg.genRight = srcCanvasOrSvg.getBoundingClientRect().right;
- destImg.genBottom = srcCanvasOrSvg.getBoundingClientRect().bottom;
- }
- }
- function generateTempImageFromCanvasOrSvg(srcCanvasOrSvg, destImg) {
- if (srcCanvasOrSvg.tagName === 'CANVAS') {
- copyCanvasToImg(srcCanvasOrSvg, destImg);
- }
- if (srcCanvasOrSvg.tagName === 'svg') {
- copySVGToImg(srcCanvasOrSvg, destImg);
- }
- destImg.srcImgTagName = srcCanvasOrSvg.tagName;
- adnotateDestImgWithBoundingClientRect(srcCanvasOrSvg, destImg);
- }
- function failureCallback() {
- return GENERALFAILURECALLBACKERROR;
- }
- // used for testing
- $.plot.composeImages = composeImages;
- function init(plot) {
- // used to extend the public API of the plot
- plot.composeImages = composeImages;
- }
- $.plot.plugins.push({
- init: init,
- name: 'composeImages',
- version: '1.0'
- });
- })(jQuery);
|