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); |
|