UNPKG

23.4 kBJavaScriptView Raw
1var mime = require('mime-types');
2var DOMParser = require('xmldom').DOMParser;
3var XMLparser = new DOMParser();
4var fastXmlParser = require('fast-xml-parser');
5var request = require('request');
6// var FontFaceObserver = require('fontfaceobserver');
7var deasync = require('deasync');
8var xmlNs = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>';
9var vm = require('vm2');
10var fs = require('fs');
11var gm = require('gm').subClass({imageMagick: true});
12var spawnSync = require('child_process').spawnSync;
13var spawn = require('child_process').spawn;
14var execSync = require('child_process').execSync;
15var extend = require('util')._extend;
16var opentype = require('opentype.js');
17var async = require('async');
18var defaultFontsDir = __dirname + '/../fonts';
19var promiseLibrary = typeof global.Promise === 'function' ? global.Promise : require('es6-promise').Promise;
20var isWin = /^win/.test(process.platform);
21var defaultParallelsTasks = 100;
22var convertQueue = async.queue(workerForConverting, defaultParallelsTasks);
23var fonts = {};
24var defaultBounds = {left: 0, top: 0, width: 1024, height: 768};
25var vectorImageParams = ['background', 'border', 'blur', 'contrast', 'crop', 'frame', 'gamma', 'monochrome', 'negative', 'noise', 'quality'];
26var childProcess;
27
28childProcess = spawn(isWin ? 'magick' : 'convert');
29childProcess.on('error', function(err) {
30 if (err.code === 'ENOENT') {
31 console.warn('Warning! Please install imagemagick utility. (https://www.imagemagick.org/script/binary-releases.php)');
32 }
33});
34childProcess.stdin.on('error', function(err) {});
35childProcess.stdin.write('');
36childProcess.stdin.end();
37
38childProcess = spawn('rsvg-convert');
39childProcess.on('error', function(err) {
40 if (err.code === 'ENOENT') {
41 console.warn('Warning! Please install rsvglib utility. (https://github.com/AnyChart/AnyChart-NodeJS)');
42 }
43});
44childProcess.stdin.on('error', function(err) {});
45childProcess.stdin.write('');
46childProcess.stdin.end();
47
48var uuidv4 = require('uuid/v4');
49var JSDOM = require('jsdom').JSDOM;
50
51var iframeDoc = null,
52 rootDoc = null,
53 iframes = {},
54 anychart = typeof anychart === 'undefined' ? anychart : void 0;
55
56if (anychart) {
57 var doc = anychart.global() && anychart.global().document || createDocument();
58 setAsRootDocument(doc);
59} else {
60 setAsRootDocument(createDocument());
61 anychart = require('anychart')(rootDoc.defaultView);
62}
63
64//region --- Utils and settings
65function setAsRootDocument(doc) {
66 rootDoc = doc;
67 var window = rootDoc.defaultView;
68 window.setTimeout = function(code, delay, arguments) {};
69 window.setInterval = function(code, delay, arguments) {};
70 anychartify(rootDoc);
71
72 return rootDoc
73}
74
75function createDocument() {
76 return (new JSDOM('', {runScripts: 'dangerously'})).window.document;
77}
78
79function createSandbox(containerTd) {
80 var iframeId = 'iframe_' + uuidv4();
81 var iframe = rootDoc.createElement('iframe');
82 iframes[iframeId] = iframe;
83 iframe.setAttribute('id', iframeId);
84 rootDoc.body.appendChild(iframe);
85 iframeDoc = iframe.contentDocument;
86 var div = iframeDoc.createElement('div');
87 div.setAttribute('id', containerTd);
88 iframeDoc.body.appendChild(div);
89
90 return iframeId;
91}
92
93function clearSandbox(iframeId) {
94 var iFrame = rootDoc.getElementById(iframeId);
95 if (!iframeId || !iFrame) return;
96 iframeDoc = iFrame.contentDocument;
97 var iframeWindow = iframeDoc.defaultView;
98 iframeWindow.anychart = null;
99 iframeWindow.acgraph = null;
100 iframeDoc.createElementNS = null;
101 iframeDoc.body.innerHTML = '';
102 iFrame.contentDocument = null;
103 rootDoc.body.removeChild(iFrame);
104 delete iframes[iframeId];
105}
106
107function isPercent(value) {
108 if (value == null)
109 return false;
110 var l = value.length - 1;
111 return (typeof value == 'string') && l >= 0 && value.indexOf('%', l) == l;
112}
113
114function isDef(value) {
115 return value != void 0;
116}
117
118function isVectorFormat(type) {
119 return type === 'pdf' || type === 'ps' || type === 'svg';
120}
121
122function applyImageParams(img, params) {
123 for (var i = 0, len = vectorImageParams.length; i < len; i++) {
124 var paramName = vectorImageParams[i];
125 var value = params[paramName];
126 if (value)
127 img[paramName].apply(img, Object.prototype.toString.call(value) === '[object Array]' ? value : [value]);
128 }
129}
130
131function isFunction(value) {
132 return typeof(value) == 'function';
133}
134
135function concurrency(count) {
136 // var availableProcForExec = getAvailableProcessesCount();
137 //
138 // if (count > availableProcForExec) {
139 // count = availableProcForExec;
140 // console.log('Warning! You can spawn only ' + availableProcForExec + ' process at a time.');
141 // }
142 convertQueue.concurrency = count;
143}
144
145function getBBox() {
146 var text = this.textContent;
147 var fontSize = parseFloat(this.getAttribute('font-size'));
148 var fontFamily = this.getAttribute('font-family');
149 if (fontFamily) fontFamily = fontFamily.toLowerCase();
150 var fontWeight = this.getAttribute('font-weight');
151 if (fontWeight) fontWeight = fontWeight.toLowerCase();
152 var fontStyle = this.getAttribute('font-style');
153 if (fontStyle) fontStyle = fontStyle.toLowerCase();
154
155 var fontsArr = fontFamily.split(', ');
156
157 var font;
158 for (var i = 0, len = fontsArr.length; i < len; i++) {
159 var name = fontsArr[i] + (fontWeight == 'normal' || !isNaN(+fontWeight) ? '' : ' ' + fontWeight) + (fontStyle == 'normal' ? '' : ' ' + fontStyle);
160 if (font = fonts[name])
161 break;
162 }
163
164 if (!font)
165 font = fonts['verdana'];
166
167 var scale = 1 / font.unitsPerEm * fontSize;
168
169 var top = -font.ascender * scale;
170 var height = Math.abs(top) + Math.abs(font.descender * scale);
171
172 var width = 0;
173
174 font.forEachGlyph(text, 0, 0, fontSize, undefined, function(glyph, x, y, fontSize, options) {
175 var metrics = glyph.getMetrics();
176 metrics.xMin *= scale;
177 metrics.xMax *= scale;
178 metrics.leftSideBearing *= scale;
179 metrics.rightSideBearing *= scale;
180
181 width += Math.abs(metrics.xMax - metrics.xMin) + metrics.leftSideBearing + metrics.rightSideBearing
182 });
183
184 return {x: 0, y: top, width: width, height: height};
185}
186
187function anychartify(doc) {
188 doc.createElementNS = function(ns, tagName) {
189 var elem = doc.createElement(tagName);
190 elem.getBBox = elem.getBBox || getBBox;
191 return elem;
192 };
193}
194
195function prepareDocumentForRender(doc) {
196 anychartify(doc);
197
198 var window = doc.defaultView;
199 window.anychart = anychart;
200 window.acgraph = anychart.graphics;
201 window.isNodeJS = true;
202 window.defaultBounds = defaultBounds;
203 window.setTimeout = function(code, delay, arguments) {};
204 window.setInterval = function(code, delay, arguments) {};
205}
206
207function getParams(args) {
208 var arrLength = args.length;
209 var lastArg = args[arrLength - 1];
210 var callback = isFunction(lastArg) ? lastArg : null;
211
212 var options = arrLength === 1 ? void 0 : callback ? arrLength > 2 ? args[arrLength - 2] : void 0: lastArg;
213 var params = {};
214
215 params.callback = callback;
216
217 if (typeof options === 'string') {
218 params.outputType = options;
219 } else if (typeof options === 'object') {
220 extend(params, options)
221 }
222
223 var target = args[0];
224 if (target && !isDef(params.dataType)) {
225 if (typeof target === 'string') {
226 target = target.replace(/^[\s\xa0]+|[\s\xa0]+$/g, '');
227
228 try {
229 JSON.parse(target);
230 params.dataType = 'json';
231 } catch (e) {
232 if (fastXmlParser.validate(target)) {
233 if (target.lastIndexOf('<svg') !== -1) {
234 params.dataType = 'svg';
235 } else {
236 params.dataType = 'xml';
237 }
238 } else {
239 params.dataType = 'javascript';
240 }
241 }
242 } else {
243 var isChart = typeof target.draw === 'function';
244 var isStage = typeof target.resume === 'function';
245 params.dataType = isChart ? 'chart' : isStage ? 'stage' : void 0;
246 }
247 }
248
249 if (!params.outputType) {
250 params.outputType = 'jpg';
251 }
252
253 if (!isDef(params.containerId)) {
254 if (params.dataType === 'javascript') {
255 var regex = /\.container\(('|")(.*)('|")\)/g;
256 var result = regex.exec(target);
257 params.containerId = result ? result[2] : 'container';
258 } else {
259 params.containerId = 'container';
260 }
261 }
262
263 return params;
264}
265
266function fixSvg(svg) {
267 return svg
268 //jsdom bug - (https://github.com/tmpvar/jsdom/issues/620)
269 .replace(/textpath/g, 'textPath')
270 .replace(/lineargradient/g, 'linearGradient')
271 .replace(/radialgradient/g, 'radialGradient')
272 .replace(/clippath/g, 'clipPath')
273 .replace(/patternunits/g, 'patternUnits')
274 //fixes for wrong id naming
275 .replace(/(id=")#/g, '$1')
276 .replace(/(url\()##/g, '$1#')
277 //anychart a11y
278 .replace(/aria-label=".*?"/g, '')
279}
280
281function applyResourcesToDoc(params, resources, callback) {
282 var document = params.document;
283 var head = document.getElementsByTagName('head')[0];
284 var window = document.defaultView;
285 var scripts = '';
286
287 for (var i = 0, len = resources.length; i < len; i++) {
288 var resource = resources[i];
289 var type = resource.type;
290
291 if (type == mime.contentType('css')) {
292 var style = document.createElement('style');
293 style.innerHTML = resource.body;
294 head.appendChild(style);
295
296 //todo font loading
297 // var font = new FontFaceObserver('Conv_interdex');
298 // font.load().then(function () {
299 //
300 // });
301 } else if (type == mime.contentType('js')) {
302 scripts += ' ' + resource.body + ';';
303 }
304 }
305
306 var err = null;
307 try {
308 var script = new vm.VM({
309 timeout: 5000,
310 sandbox: window
311 });
312 script.run(scripts);
313 } catch (e) {
314 console.log(e);
315 err = e;
316 }
317
318 return callback(err, params);
319}
320
321function loadExternalResources(resources, params, callback) {
322 if (Object.prototype.toString.call(resources) === '[object Array]') {
323 var loadedResources = [];
324 for (var i = 0, len = resources.length; i < len; i++) {
325 request
326 .get(resources[i], function(err, response, body) {
327 if (err) {
328 loadedResources.push('');
329 } else {
330 loadedResources.push({
331 type: mime.contentType(response.headers['content-type']),
332 body: body
333 });
334 }
335
336 if (resources.length === loadedResources.length) {
337 return applyResourcesToDoc(params, loadedResources, callback);
338 }
339 });
340 }
341 if (resources.length === loadedResources.length) {
342 return applyResourcesToDoc(params, loadedResources, callback);
343 }
344 } else {
345 return callback(null, params);
346 }
347}
348
349function getSvgString(svgElement, width, height) {
350 var svg = '';
351 if (svgElement) {
352 if (!width || isPercent(width))
353 width = defaultBounds.width;
354 if (!height || isPercent(height))
355 height = defaultBounds.height;
356
357 svgElement.setAttribute('width', width);
358 svgElement.setAttribute('height', height);
359 svg = xmlNs + svgElement.outerHTML;
360 }
361 return svg;
362}
363
364function getSvg(target, params, callback) {
365 var dataType = params.dataType;
366
367 if (dataType === 'svg') {
368 return callback(null, fixSvg(target), params);
369 } else {
370 var svg, container, svgElement;
371 var res = params.resources;
372
373 var isChart = dataType === 'chart';
374 var isStage = dataType === 'stage';
375
376 if (!params.document && !(isChart || isStage)) {
377 params.iframeId = createSandbox(params.containerId);
378 params.document = iframeDoc;
379 }
380 if (params.document)
381 prepareDocumentForRender(params.document);
382
383 return loadExternalResources(res, params, function(err, params) {
384 var document = params.document;
385 var window = document && document.defaultView;
386 var bounds, width, height;
387
388 if (window)
389 anychart.global(window);
390
391 if (dataType === 'javascript') {
392 var script = new vm.VM({
393 timeout: 10000,
394 sandbox: {
395 anychart: window.anychart
396 }
397 });
398 script.run(target);
399
400 var svgElements = document.getElementsByTagName('svg');
401 var chartToDispose = [];
402 for (var i = 0, len = svgElements.length; i < len; i++) {
403 svgElement = svgElements[i];
404 if (!svgElement) continue;
405 var id = svgElement.getAttribute('ac-id');
406 var stage = anychart.graphics.getStage(id);
407 if (stage) {
408 var charts = stage.getCharts();
409 for (var chartId in charts) {
410 var chart = charts[chartId];
411 bounds = chart.bounds();
412 svg = getSvgString(svgElement, bounds.width(), bounds.height());
413 chartToDispose.push(chart);
414 }
415 stage.dispose();
416 }
417 }
418
419 for (i = 0, len = chartToDispose.length; i < len; i++) {
420 chartToDispose[i].dispose();
421 }
422 } else {
423 if (dataType === 'json') {
424 target = anychart.fromJson(target);
425 isChart = true;
426 } else if (dataType === 'xml') {
427 target = anychart.fromXml(XMLparser.parseFromString(target));
428 isChart = true;
429 }
430 target.container(params.containerId);
431
432 if (isChart || isStage) {
433 if (target.animation)
434 target.animation(false);
435 if (target.a11y)
436 target.a11y(false);
437
438 container = target.container();
439 if (!container) {
440 if (params.document) {
441 var div = params.document.createElement('div');
442 div.setAttribute('id', params.containerId);
443 params.document.body.appendChild(div);
444 } else if (isChart) {
445 //todo (blackart) for resolve this case need to add link to chart instance for parent anychart object. while it's not true will be used this approach.
446 params.iframeId = createSandbox(params.containerId);
447 params.document = iframeDoc;
448 prepareDocumentForRender(params.document);
449 anychart.global(params.document.defaultView);
450 target = anychart.fromJson(target.toJson());
451 } else {
452 console.warn('Warning! Cannot find context of executing. Please define \'document\' in exporting params.');
453 return callback(null, '', params);
454 }
455 target.container(params.containerId);
456 container = target.container();
457 }
458
459 if (isChart) {
460 target.draw();
461
462 bounds = target.bounds();
463 width = bounds.width();
464 height = bounds.height();
465 } else {
466 target.resume();
467
468 bounds = target.getBounds();
469 width = bounds.width;
470 height = bounds.height;
471 }
472
473 svgElement = isChart ? container.getStage().domElement() : target.domElement();
474 svg = getSvgString(svgElement, width, height);
475
476 if (dataType === 'json' && dataType === 'xml') {
477 target.dispose();
478 }
479 } else {
480 console.warn('Warning! Wrong format of incoming data.');
481 svg = '';
482 }
483 }
484 clearSandbox(params.iframeId);
485
486 return callback(null, fixSvg(svg), params);
487 });
488 }
489}
490
491function getAvailableProcessesCount() {
492 //nix way
493 var procMetrics = execSync('ulimit -u && ps ax | wc -l').toString().trim().split(/\n\s+/g);
494 return procMetrics[0] - procMetrics[1];
495}
496
497function workerForConverting(task, done) {
498 if (isVectorFormat(task.params.outputType)) {
499 var childProcess;
500 var callBackAlreadyCalled = false;
501 try {
502 var params = ['-f', task.params.outputType];
503 if (isDef(task.params.width))
504 params.push('-w', task.params.width);
505 if (isDef(task.params.height))
506 params.push('-h', task.params.height);
507 if (isDef(task.params['aspect-ratio']) && String(task.params['aspect-ratio']).toLowerCase() != 'false')
508 params.push('-a');
509 if (isDef(task.params.background))
510 params.push('-b', task.params.background);
511
512 childProcess = spawn('rsvg-convert', params);
513 var buffer;
514 childProcess.stdout.on('data', function(data) {
515 try {
516 var prevBufferLength = (buffer ? buffer.length : 0),
517 newBuffer = new Buffer(prevBufferLength + data.length);
518
519 if (buffer) {
520 buffer.copy(newBuffer, 0, 0);
521 }
522
523 data.copy(newBuffer, prevBufferLength, 0);
524
525 buffer = newBuffer;
526 } catch (err) {
527 if (!callBackAlreadyCalled) {
528 done(err, null);
529 callBackAlreadyCalled = true;
530 }
531 }
532 });
533
534 childProcess.on('close', function(code, signal) {
535 if (!code && !callBackAlreadyCalled) {
536 done(null, buffer);
537 } else {
538 console.warn('Unexpected close of child process with code %s signal %s', code, signal);
539 }
540 });
541
542 childProcess.stderr.on('data', function(data) {
543 if (!callBackAlreadyCalled) {
544 done(new Error(data), null);
545 callBackAlreadyCalled = true;
546 }
547 });
548
549 childProcess.on('error', function(err) {
550 if (err.code === 'ENOENT') {
551 console.warn('Warning! Please install librsvg package.');
552 }
553 if (!callBackAlreadyCalled) {
554 done(err, null);
555 callBackAlreadyCalled = true;
556 }
557 });
558
559 childProcess.stdin.write(task.svg);
560 childProcess.stdin.end();
561 } catch (err) {
562 if (!callBackAlreadyCalled) {
563 done(err, null);
564 callBackAlreadyCalled = true;
565 }
566 }
567 } else {
568 var img = gm(Buffer.from(task.svg, 'utf8'));
569 applyImageParams(img, task.params);
570 img.toBuffer(task.params.outputType, done);
571
572 // var childProcess;
573 // try {
574 // childProcess = spawn(isWin ? 'magick' : 'convert', ['svg:-', task.params.outputType + ':-']);
575 // var buffer;
576 // childProcess.stdout.on('data', function(data) {
577 // console.log('data');
578 // try {
579 // var prevBufferLength = (buffer ? buffer.length : 0),
580 // newBuffer = new Buffer(prevBufferLength + data.length);
581 //
582 // if (buffer) {
583 // buffer.copy(newBuffer, 0, 0);
584 // }
585 //
586 // data.copy(newBuffer, prevBufferLength, 0);
587 //
588 // buffer = newBuffer;
589 // } catch (err) {
590 // done(err, null);
591 // }
592 // });
593 //
594 // childProcess.on('close', function(code) {
595 // if (!code) {
596 // done(null, buffer);
597 // }
598 // });
599 //
600 // childProcess.on('error', function(err) {
601 // if (err.code == 'ENOENT') {
602 // console.log('Warning! Please install imagemagick utility. (https://www.imagemagick.org/script/binary-releases.php)');
603 // }
604 // done(err, null);
605 // });
606 //
607 // childProcess.stdin.write(task.svg);
608 // childProcess.stdin.end();
609 // } catch (err) {
610 // done(err, null);
611 // }
612 }
613}
614
615function loadDefaultFontsSync() {
616 var fontFilesList = fs.readdirSync(defaultFontsDir);
617
618 for (var i = 0, len = fontFilesList.length; i < len; i++) {
619 var fileName = fontFilesList[i];
620 var font = opentype.loadSync(defaultFontsDir + '/' + fileName);
621 fonts[font.names.fullName.en.toLowerCase()] = font;
622 }
623
624 return fonts;
625}
626
627function convertSvgToImageData(svg, params, callback) {
628 convertQueue.push({svg: svg, params: params}, callback);
629}
630
631function convertSvgToImageDataSync(svg, params) {
632 if (isVectorFormat(params.outputType)) {
633 var convertParams = ['-f', params.outputType];
634 if (isDef(params.width))
635 convertParams.push('-w', params.width);
636 if (isDef(params.height))
637 convertParams.push('-h', params.height);
638 if (isDef(params['aspect-ratio']) && String(params['aspect-ratio']).toLowerCase() !== 'false')
639 convertParams.push('-a');
640 if (isDef(params.background))
641 convertParams.push('-b', params.background);
642
643 return spawnSync('rsvg-convert', convertParams, {input: svg}).stdout;
644
645 } else {
646 // convert = spawnSync(isWin ? 'magick' : 'convert', ['svg:-', params.outputType + ':-'], {input: svg});
647 var done = false, data = null, error = null;
648
649 var img = gm(Buffer.from(svg, 'utf8'));
650 applyImageParams(img, params);
651 img.toBuffer(params.outputType, function(err, buffer) {
652 data = buffer;
653 error = err;
654 done = true;
655 });
656 deasync.loopWhile(function() {
657 return !done;
658 });
659 return data;
660 }
661}
662
663//endregion utils
664
665//region --- API
666var AnychartExport = function() {
667};
668
669AnychartExport.prototype.exportTo = function(target, options, callback) {
670 if (!target) {
671 console.warn('Can\'t read input data for exporting.');
672 }
673
674 var params = getParams(arguments);
675 callback = params.callback;
676
677 if (typeof callback === 'function') {
678 try {
679 getSvg(target, params, function(err, svg, params) {
680 if (params.outputType === 'svg') {
681 process.nextTick(function() {
682 callback(err, svg);
683 });
684 } else {
685 convertSvgToImageData(svg, params, callback);
686 }
687 });
688 } catch (e) {
689 callback(e, null);
690 return;
691 }
692 } else {
693 return new promiseLibrary(function(resolve, reject) {
694 try {
695 getSvg(target, params, function(err, svg, params) {
696 if (params.outputType === 'svg') {
697 process.nextTick(function() {
698 if (err) reject(err);
699 else resolve(svg);
700 });
701 } else {
702 var done = function(err, image) {
703 if (err) reject(err);
704 else resolve(image);
705 };
706 convertSvgToImageData(svg, params, done);
707 }
708 })
709 } catch (e) {
710 reject(e);
711 return;
712 }
713 })
714 }
715};
716
717AnychartExport.prototype.exportToSync = function(target, options) {
718 var params = getParams(arguments);
719 return getSvg(target, params, function(err, svg, params) {
720 return params.outputType === 'svg' ? svg : convertSvgToImageDataSync(svg, params);
721 });
722};
723
724AnychartExport.prototype.loadFont = function(path, callback) {
725 if (typeof callback == 'function') {
726 opentype.load(path, function(err, font) {
727 if (!err)
728 fonts[font.names.fullName.en.toLowerCase()] = font;
729
730 callback(err, font);
731 });
732 } else {
733 return new promiseLibrary(function(resolve, reject) {
734 opentype.load(path, function(err, font) {
735 if (err) {
736 reject(err);
737 } else {
738 fonts[font.names.fullName.en.toLowerCase()] = font;
739 resolve(font);
740 }
741 });
742 })
743 }
744};
745
746AnychartExport.prototype.loadFontSync = function(path) {
747 return fonts[font.names.fullName.en.toLowerCase()] = opentype.loadSync(path);
748};
749
750AnychartExport.prototype.anychartify = anychartify;
751
752var AnychartExportWrapper = function(anychartInst) {
753 if (anychartInst) {
754 anychart = anychartInst;
755 setAsRootDocument(anychart.global().document);
756 }
757 return new AnychartExport();
758};
759
760AnychartExportWrapper.exportTo = AnychartExport.prototype.exportTo;
761
762AnychartExportWrapper.exportToSync = AnychartExport.prototype.exportToSync;
763
764AnychartExportWrapper.loadFont = AnychartExport.prototype.loadFont;
765
766AnychartExportWrapper.loadFontSync = AnychartExport.prototype.loadFontSync;
767
768AnychartExportWrapper.anychartify = AnychartExport.prototype.anychartify;
769
770//endregion
771
772loadDefaultFontsSync();
773
774module.exports = AnychartExportWrapper;
\No newline at end of file