UNPKG

16.9 kBJavaScriptView Raw
1(function() {
2 var out$ = typeof exports != 'undefined' && exports || typeof define != 'undefined' && {} || this;
3
4 var doctype = '<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [<!ENTITY nbsp "&#160;">]>';
5
6 function isElement(obj) {
7 return obj instanceof HTMLElement || obj instanceof SVGElement;
8 }
9
10 function requireDomNode(el) {
11 if (!isElement(el)) {
12 throw new Error('an HTMLElement or SVGElement is required; got ' + el);
13 }
14 }
15
16 function isExternal(url) {
17 return url && url.lastIndexOf('http',0) == 0 && url.lastIndexOf(window.location.host) == -1;
18 }
19
20 function inlineImages(el, callback) {
21 requireDomNode(el);
22
23 var images = el.querySelectorAll('image'),
24 left = images.length,
25 checkDone = function() {
26 if (left === 0) {
27 callback();
28 }
29 };
30
31 checkDone();
32 for (var i = 0; i < images.length; i++) {
33 (function(image) {
34 var href = image.getAttributeNS("http://www.w3.org/1999/xlink", "href");
35 if (href) {
36 if (isExternal(href.value)) {
37 console.warn("Cannot render embedded images linking to external hosts: "+href.value);
38 return;
39 }
40 }
41 var canvas = document.createElement('canvas');
42 var ctx = canvas.getContext('2d');
43 var img = new Image();
44 img.crossOrigin="anonymous";
45 href = href || image.getAttribute('href');
46 if (href) {
47 img.src = href;
48 img.onload = function() {
49 canvas.width = img.width;
50 canvas.height = img.height;
51 ctx.drawImage(img, 0, 0);
52 image.setAttributeNS("http://www.w3.org/1999/xlink", "href", canvas.toDataURL('image/png'));
53 left--;
54 checkDone();
55 }
56 img.onerror = function() {
57 console.log("Could not load "+href);
58 left--;
59 checkDone();
60 }
61 } else {
62 left--;
63 checkDone();
64 }
65 })(images[i]);
66 }
67 }
68
69 function styles(el, options, cssLoadedCallback) {
70 var selectorRemap = options.selectorRemap;
71 var modifyStyle = options.modifyStyle;
72 var modifyCss = options.modifyCss || function(selector, properties) {
73 var selector = selectorRemap ? selectorRemap(selector) : selector;
74 var cssText = modifyStyle ? modifyStyle(properties) : properties;
75 return selector + " { " + cssText + " }\n";
76 };
77 var css = "";
78
79 // Each font that has an external link is saved into queue, and processed asynchronously.
80 var fontsQueue = [];
81 var sheets = document.styleSheets;
82 for (var i = 0; i < sheets.length; i++) {
83 try {
84 var rules = sheets[i].cssRules;
85 } catch (e) {
86 console.warn("Stylesheet could not be loaded: "+sheets[i].href);
87 continue;
88 }
89
90 if (rules != null) {
91 for (var j = 0, match; j < rules.length; j++, match = null) {
92 var rule = rules[j];
93 if (typeof(rule.style) != "undefined") {
94 var selectorText;
95
96 try {
97 selectorText = rule.selectorText;
98 } catch(err) {
99 console.warn('The following CSS rule has an invalid selector: "' + rule + '"', err);
100 }
101
102 try {
103 if (selectorText) {
104 match = el.querySelector(selectorText) || (el.parentNode && el.parentNode.querySelector(selectorText));
105 }
106 } catch(err) {
107 console.warn('Invalid CSS selector "' + selectorText + '"', err);
108 }
109
110 if (match) {
111 css += modifyCss(rule.selectorText, rule.style.cssText);
112 } else if(rule.cssText.match(/^@font-face/)) {
113 // below we are trying to find matches to external link. E.g.
114 // @font-face {
115 // // ...
116 // src: local('Abel'), url(https://fonts.gstatic.com/s/abel/v6/UzN-iejR1VoXU2Oc-7LsbvesZW2xOQ-xsNqO47m55DA.woff2);
117 // }
118 //
119 // This regex will save extrnal link into first capture group
120 var fontUrlRegexp = /url\(["']?(.+?)["']?\)/;
121 // TODO: This needs to be changed to support multiple url declarations per font.
122 var fontUrlMatch = rule.cssText.match(fontUrlRegexp);
123
124 var externalFontUrl = (fontUrlMatch && fontUrlMatch[1]) || '';
125 var fontUrlIsDataURI = externalFontUrl.match(/^data:/);
126 if (fontUrlIsDataURI) {
127 // We should ignore data uri - they are already embedded
128 externalFontUrl = '';
129 }
130
131 if (externalFontUrl === 'about:blank') {
132 // no point trying to load this
133 externalFontUrl = '';
134 }
135
136 if (externalFontUrl) {
137 // okay, we are lucky. We can fetch this font later
138
139 //handle url if relative
140 if (externalFontUrl.startsWith('../')) {
141 externalFontUrl = sheets[i].href + '/../' + externalFontUrl
142 } else if (externalFontUrl.startsWith('./')) {
143 externalFontUrl = sheets[i].href + '/.' + externalFontUrl
144 }
145
146 fontsQueue.push({
147 text: rule.cssText,
148 // Pass url regex, so that once font is downladed, we can run `replace()` on it
149 fontUrlRegexp: fontUrlRegexp,
150 format: getFontMimeTypeFromUrl(externalFontUrl),
151 url: externalFontUrl
152 });
153 } else {
154 // otherwise, use previous logic
155 css += rule.cssText + '\n';
156 }
157 }
158 }
159 }
160 }
161 }
162
163 // Now all css is processed, it's time to handle scheduled fonts
164 processFontQueue(fontsQueue);
165
166 function getFontMimeTypeFromUrl(fontUrl) {
167 var supportedFormats = {
168 'woff2': 'font/woff2',
169 'woff': 'font/woff',
170 'otf': 'application/x-font-opentype',
171 'ttf': 'application/x-font-ttf',
172 'eot': 'application/vnd.ms-fontobject',
173 'sfnt': 'application/font-sfnt',
174 'svg': 'image/svg+xml'
175 };
176 var extensions = Object.keys(supportedFormats);
177 for (var i = 0; i < extensions.length; ++i) {
178 var extension = extensions[i];
179 // TODO: This is not bullet proof, it needs to handle edge cases...
180 if (fontUrl.indexOf('.' + extension) > 0) {
181 return supportedFormats[extension];
182 }
183 }
184
185 // If you see this error message, you probably need to update code above.
186 console.error('Unknown font format for ' + fontUrl+ '; Fonts may not be working correctly');
187 return 'application/octet-stream';
188 }
189
190 function processFontQueue(queue) {
191 if (queue.length > 0) {
192 // load fonts one by one until we have anything in the queue:
193 var font = queue.pop();
194 processNext(font);
195 } else {
196 // no more fonts to load.
197 cssLoadedCallback(css);
198 }
199
200 function processNext(font) {
201 // TODO: This could benefit from caching.
202 var oReq = new XMLHttpRequest();
203 oReq.addEventListener('load', fontLoaded);
204 oReq.addEventListener('error', transferFailed);
205 oReq.addEventListener('abort', transferFailed);
206 oReq.open('GET', font.url);
207 oReq.responseType = 'arraybuffer';
208 oReq.send();
209
210 function fontLoaded() {
211 // TODO: it may be also worth to wait until fonts are fully loaded before
212 // attempting to rasterize them. (e.g. use https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet )
213 var fontBits = oReq.response;
214 var fontInBase64 = arrayBufferToBase64(fontBits);
215 updateFontStyle(font, fontInBase64);
216 }
217
218 function transferFailed(e) {
219 console.warn('Failed to load font from: ' + font.url);
220 console.warn(e)
221 css += font.text + '\n';
222 processFontQueue(queue);
223 }
224
225 function updateFontStyle(font, fontInBase64) {
226 var dataUrl = 'url("data:' + font.format + ';base64,' + fontInBase64 + '")';
227 css += font.text.replace(font.fontUrlRegexp, dataUrl) + '\n';
228
229 // schedule next font download on next tick.
230 setTimeout(function() {
231 processFontQueue(queue)
232 }, 0);
233 }
234
235 }
236 }
237
238 function arrayBufferToBase64(buffer) {
239 var binary = '';
240 var bytes = new Uint8Array(buffer);
241 var len = bytes.byteLength;
242
243 for (var i = 0; i < len; i++) {
244 binary += String.fromCharCode(bytes[i]);
245 }
246
247 return window.btoa(binary);
248 }
249 }
250
251 function getDimension(el, clone, dim) {
252 var v = (el.viewBox && el.viewBox.baseVal && el.viewBox.baseVal[dim]) ||
253 (clone.getAttribute(dim) !== null && !clone.getAttribute(dim).match(/%$/) && parseInt(clone.getAttribute(dim))) ||
254 el.getBoundingClientRect()[dim] ||
255 parseInt(clone.style[dim]) ||
256 parseInt(window.getComputedStyle(el).getPropertyValue(dim));
257 return (typeof v === 'undefined' || v === null || isNaN(parseFloat(v))) ? 0 : v;
258 }
259
260 function reEncode(data) {
261 data = encodeURIComponent(data);
262 data = data.replace(/%([0-9A-F]{2})/g, function(match, p1) {
263 var c = String.fromCharCode('0x'+p1);
264 return c === '%' ? '%25' : c;
265 });
266 return decodeURIComponent(data);
267 }
268
269 out$.prepareSvg = function(el, options, cb) {
270 requireDomNode(el);
271
272 options = options || {};
273 options.scale = options.scale || 1;
274 options.responsive = options.responsive || false;
275 var xmlns = "http://www.w3.org/2000/xmlns/";
276
277 inlineImages(el, function() {
278 var outer = document.createElement("div");
279 var clone = el.cloneNode(true);
280 var width, height;
281 if(el.tagName == 'svg') {
282 width = options.width || getDimension(el, clone, 'width');
283 height = options.height || getDimension(el, clone, 'height');
284 } else if(el.getBBox) {
285 var box = el.getBBox();
286 width = box.x + box.width;
287 height = box.y + box.height;
288 clone.setAttribute('transform', clone.getAttribute('transform').replace(/translate\(.*?\)/, ''));
289
290 var svg = document.createElementNS('http://www.w3.org/2000/svg','svg')
291 svg.appendChild(clone)
292 clone = svg;
293 } else {
294 console.error('Attempted to render non-SVG element', el);
295 return;
296 }
297
298 clone.setAttribute("version", "1.1");
299 if (!clone.getAttribute('xmlns')) {
300 clone.setAttributeNS(xmlns, "xmlns", "http://www.w3.org/2000/svg");
301 }
302 if (!clone.getAttribute('xmlns:xlink')) {
303 clone.setAttributeNS(xmlns, "xmlns:xlink", "http://www.w3.org/1999/xlink");
304 }
305
306 if (options.responsive) {
307 clone.removeAttribute('width');
308 clone.removeAttribute('height');
309 clone.setAttribute('preserveAspectRatio', 'xMinYMin meet');
310 } else {
311 clone.setAttribute("width", width * options.scale);
312 clone.setAttribute("height", height * options.scale);
313 }
314
315 clone.setAttribute("viewBox", [
316 options.left || 0,
317 options.top || 0,
318 width,
319 height
320 ].join(" "));
321
322 var fos = clone.querySelectorAll('foreignObject > *');
323 for (var i = 0; i < fos.length; i++) {
324 if (!fos[i].getAttribute('xmlns')) {
325 fos[i].setAttributeNS(xmlns, "xmlns", "http://www.w3.org/1999/xhtml");
326 }
327 }
328
329 outer.appendChild(clone);
330
331 // In case of custom fonts we need to fetch font first, and then inline
332 // its url into data-uri format (encode as base64). That's why style
333 // processing is done asynchonously. Once all inlining is finshed
334 // cssLoadedCallback() is called.
335 styles(el, options, cssLoadedCallback);
336
337 function cssLoadedCallback(css) {
338 // here all fonts are inlined, so that we can render them properly.
339 var s = document.createElement('style');
340 s.setAttribute('type', 'text/css');
341 s.innerHTML = "<![CDATA[\n" + css + "\n]]>";
342 var defs = document.createElement('defs');
343 defs.appendChild(s);
344 clone.insertBefore(defs, clone.firstChild);
345
346 if (cb) {
347 var outHtml = outer.innerHTML;
348 outHtml = outHtml.replace(/NS\d+:href/gi, 'xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href');
349 cb(outHtml, width, height);
350 }
351 }
352 });
353 }
354
355 out$.svgAsDataUri = function(el, options, cb) {
356 out$.prepareSvg(el, options, function(svg) {
357 var uri = 'data:image/svg+xml;base64,' + window.btoa(reEncode(doctype + svg));
358 if (cb) {
359 cb(uri);
360 }
361 });
362 }
363
364 out$.svgAsPngUri = function(el, options, cb) {
365 requireDomNode(el);
366
367 options = options || {};
368 options.encoderType = options.encoderType || 'image/png';
369 options.encoderOptions = options.encoderOptions || 0.8;
370
371 var convertToPng = function(src, w, h) {
372 var canvas = document.createElement('canvas');
373 var context = canvas.getContext('2d');
374 canvas.width = w;
375 canvas.height = h;
376
377 var pixelRatio = window.devicePixelRatio || 1;
378
379 canvas.style.width = canvas.width+'px';
380 canvas.style.height = canvas.height+'px';
381 canvas.width *= pixelRatio;
382 canvas.height *= pixelRatio;
383
384 context.setTransform(pixelRatio,0,0,pixelRatio,0,0);
385
386 if(options.canvg) {
387 options.canvg(canvas, src);
388 } else {
389 context.drawImage(src, 0, 0);
390 }
391
392 if(options.backgroundColor){
393 context.globalCompositeOperation = 'destination-over';
394 context.fillStyle = options.backgroundColor;
395 context.fillRect(0, 0, canvas.width, canvas.height);
396 }
397
398 var png;
399 try {
400 png = canvas.toDataURL(options.encoderType, options.encoderOptions);
401 } catch (e) {
402 if ((typeof SecurityError !== 'undefined' && e instanceof SecurityError) || e.name == "SecurityError") {
403 console.error("Rendered SVG images cannot be downloaded in this browser.");
404 return;
405 } else {
406 throw e;
407 }
408 }
409 cb(png);
410 }
411
412 if(options.canvg) {
413 out$.prepareSvg(el, options, convertToPng);
414 } else {
415 out$.svgAsDataUri(el, options, function(uri) {
416 var image = new Image();
417
418 image.onload = function() {
419 convertToPng(image, image.width, image.height);
420 }
421
422 image.onerror = function() {
423 console.error(
424 'There was an error loading the data URI as an image on the following SVG\n',
425 window.atob(uri.slice(26)), '\n',
426 "Open the following link to see browser's diagnosis\n",
427 uri);
428 }
429
430 image.src = uri;
431 });
432 }
433 }
434
435 out$.download = function(name, uri) {
436 if (navigator.msSaveOrOpenBlob) {
437 navigator.msSaveOrOpenBlob(uriToBlob(uri), name);
438 } else {
439 var saveLink = document.createElement('a');
440 var downloadSupported = 'download' in saveLink;
441 if (downloadSupported) {
442 saveLink.download = name;
443 saveLink.style.display = 'none';
444 document.body.appendChild(saveLink);
445 try {
446 var blob = uriToBlob(uri);
447 var url = URL.createObjectURL(blob);
448 saveLink.href = url;
449 saveLink.onclick = function() {
450 requestAnimationFrame(function() {
451 URL.revokeObjectURL(url);
452 })
453 };
454 } catch (e) {
455 console.warn('This browser does not support object URLs. Falling back to string URL.');
456 saveLink.href = uri;
457 }
458 saveLink.click();
459 document.body.removeChild(saveLink);
460 }
461 else {
462 window.open(uri, '_temp', 'menubar=no,toolbar=no,status=no');
463 }
464 }
465 }
466
467 function uriToBlob(uri) {
468 var byteString = window.atob(uri.split(',')[1]);
469 var mimeString = uri.split(',')[0].split(':')[1].split(';')[0]
470 var buffer = new ArrayBuffer(byteString.length);
471 var intArray = new Uint8Array(buffer);
472 for (var i = 0; i < byteString.length; i++) {
473 intArray[i] = byteString.charCodeAt(i);
474 }
475 return new Blob([buffer], {type: mimeString});
476 }
477
478 out$.saveSvg = function(el, name, options) {
479 requireDomNode(el);
480
481 options = options || {};
482 out$.svgAsDataUri(el, options, function(uri) {
483 out$.download(name, uri);
484 });
485 }
486
487 out$.saveSvgAsPng = function(el, name, options) {
488 requireDomNode(el);
489
490 options = options || {};
491 out$.svgAsPngUri(el, options, function(uri) {
492 out$.download(name, uri);
493 });
494 }
495
496 // if define is defined create as an AMD module
497 if (typeof define !== 'undefined') {
498 define(function() {
499 return out$;
500 });
501 }
502
503})();