UNPKG

anychart-nodejs

Version:

AnyChart NodeJS module provides an easy way to generate SVG, JPG and PNG images of the charts on the server side.

441 lines (379 loc) 13.8 kB
(function(anychart, factory) { if (typeof module === 'object' && typeof module.exports === 'object') { if (typeof anychart.getGlobal == 'function') { factory.call(this, anychart); } else { module.exports = function(anychart) { if (typeof anychart.global != 'function') { throw new Error('anychart-export requires a anychart'); } return factory.call(this, anychart); }; } } else { factory.call(this, anychart) } })(typeof anychart !== 'undefined' ? anychart : this, function(anychart) { var document = anychart.global().document; var xmlNs = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'; var vm = require('vm2'); var fs = require('fs'); var gm = require('gm').subClass({imageMagick: true}); var spawnSync = require('child_process').spawnSync; var spawn = require('child_process').spawn; var execSync = require('child_process').execSync; var extend = require('util')._extend; var opentype = require('opentype.js'); var async = require('async'); var defaultFontsDir = __dirname + '/../fonts'; var promiseLibrary = typeof global.Promise == 'function' ? global.Promise : require('es6-promise').Promise; var isWin = /^win/.test(process.platform); var defaultParallelsTasks = 100; var convertQueue = async.queue(workerForConverting, defaultParallelsTasks); var fonts = {}; var defaultBounds = {left: 0, top: 0, width: 1024, height: 768}; var vectorImageParams = ['background', 'border', 'blur', 'contrast', 'crop', 'frame', 'gamma', 'monochrome', 'negative', 'noise', 'quality']; childProcess = spawn(isWin ? 'magick' : 'convert'); childProcess.on('error', function(err) { if (err.code == 'ENOENT') { console.warn('Warning! Please install imagemagick utility. (https://www.imagemagick.org/script/binary-releases.php)'); } }); //region --- Utils and settings function isPercent(value) { if (value == null) return false; var l = value.length - 1; return (typeof value == 'string') && l >= 0 && value.indexOf('%', l) == l; } function isVectorFormat(type) { return type === 'pdf' || type === 'ps'|| type === 'svg'; } function applyImageParams(img, params) { for (var i = 0, len = vectorImageParams.length; i < len; i++) { var paramName = vectorImageParams[i]; var value = params[paramName]; if (value) img[paramName].apply(img, Object.prototype.toString.call(value) === '[object Array]' ? value : [value]); } } function isFunction(value) { return typeof(value) == 'function'; } function concurrency(count) { // var availableProcForExec = getAvailableProcessesCount(); // // if (count > availableProcForExec) { // count = availableProcForExec; // console.log('Warning! You can spawn only ' + availableProcForExec + ' process at a time.'); // } convertQueue.concurrency = count; } function anychartify(doc) { doc.createElementNS = function(ns, tagName) { var elem = document.createElement(tagName); elem.getBBox = function() { var text = elem.textContent; var fontSize = parseFloat(elem.getAttribute('font-size')); var fontFamily = elem.getAttribute('font-family'); if (fontFamily) fontFamily = fontFamily.toLowerCase(); var fontWeight = elem.getAttribute('font-weight'); if (fontWeight) fontWeight = fontWeight.toLowerCase(); var fontStyle = elem.getAttribute('font-style'); if (fontStyle) fontStyle = fontStyle.toLowerCase(); var fontsArr = fontFamily.split(', '); var font; for (var i = 0, len = fontsArr.length; i < len; i++) { var name = fontsArr[i] + (fontWeight == 'normal' || !isNaN(+fontWeight) ? '' : ' ' + fontWeight) + (fontStyle == 'normal' ? '' : ' ' + fontStyle); if (font = fonts[name]) break; } if (!font) font = fonts['verdana']; var scale = 1 / font.unitsPerEm * fontSize; var top = -font.ascender * scale; var height = Math.abs(top) + Math.abs(font.descender * scale); var width = 0; font.forEachGlyph(text, 0, 0, fontSize, undefined, function(glyph, x, y, fontSize, options) { var metrics = glyph.getMetrics(); metrics.xMin *= scale; metrics.xMax *= scale; metrics.leftSideBearing *= scale; metrics.rightSideBearing *= scale; width += Math.abs(metrics.xMax - metrics.xMin) + metrics.leftSideBearing + metrics.rightSideBearing }); return {x: 0, y: top, width: width, height: height}; }; return elem; }; } function getParams(args) { var arrLength = args.length; var lastArg = args[arrLength - 1]; var callback = isFunction(lastArg) ? lastArg : null; var options = arrLength == 1 ? undefined : callback ? args[arrLength - 2] : lastArg; var params = {}; if (typeof options == 'string') { params.type = options; } else if (typeof options == 'object') { extend(params, options) } return params; } function fixSvg(svg) { return svg //jsdom bug - (https://github.com/tmpvar/jsdom/issues/620) .replace(/textpath/g, 'textPath') .replace(/lineargradient/g, 'linearGradient') .replace(/radialgradient/g, 'radialGradient') .replace(/clippath/g, 'clipPath') .replace(/patternunits/g, 'patternUnits') //fixes for wrong id naming .replace(/(id=")#/g, '$1') .replace(/(url\()##/g, '$1#') //anychart a11y .replace(/aria-label=".*?"/g, '') } function getSvg(target, params) { var svg, ns, container; if (typeof target === 'string') { if (target.lastIndexOf('<', 0) === 0) { svg = target; } else { var doc = params.document; var window = doc.defaultView; try { var script = new vm.VM({ timeout: 5000, sandbox: window }); script.run(target); } catch (e) { console.warn('While script execution an error has occurred: ' + e.message); } svgElement = doc.getElementsByTagName('svg')[0]; svg = xmlNs + (svgElement ? svgElement.outerHTML : ''); } } else { var isChart = typeof target.draw === 'function'; var isStage = typeof target.resume === 'function'; var svgElement; if (isChart) { if (target.animation) target.animation(false); if (target.a11y) target.a11y(false); container = target.container(); if (!container) { console.warn('Warning! Target chart has not container. Use container() method to set it.'); return ''; } // var bounds = target.bounds(); // if (isPercent(bounds.left()) || isPercent(bounds.top()) || isPercent(bounds.width()) || isPercent(bounds.height())) { // target.bounds(defaultBounds); // console.warn('Warning! Bounds of chart should be set in pixels. See https://api.anychart.com/7.14.3/anychart.core.Chart#bounds how do it.'); // } target.draw(); svgElement = container.container().getElementsByTagName('svg')[0]; svg = xmlNs + svgElement.outerHTML; target.dispose(); } else if (isStage) { container = target.container(); if (!container) { console.warn('Warning! Target chart has not container. Use container() method to set it.'); return ''; } svgElement = container.getElementsByTagName('svg')[0]; svg = ns + svgElement.outerHTML; } else { console.warn('Warning! Wrong format of incoming data.'); svg = ''; } } return fixSvg(svg); } function getAvailableProcessesCount() { //nix way var procMetrics = execSync('ulimit -u && ps ax | wc -l').toString().trim().split(/\n\s+/g); return procMetrics[0] - procMetrics[1]; } function workerForConverting(task, done) { if (isVectorFormat(task.params.type)) { var childProcess; try { childProcess = spawn('rsvg-convert', ['-f', task.params.type]); var buffer; childProcess.stdin.write(task.svg); childProcess.stdin.end(); childProcess.stdout.on('data', function(data) { try { var prevBufferLength = (buffer ? buffer.length : 0), newBuffer = new Buffer(prevBufferLength + data.length); if (buffer) { buffer.copy(newBuffer, 0, 0); } data.copy(newBuffer, prevBufferLength, 0); buffer = newBuffer; } catch (err) { done(err, null); } }); childProcess.on('close', function(code, signal) { if (!code) { done(null, buffer); } else { console.warn('Unexpected close of process with code %s signal %s', code, signal); } }); childProcess.stderr.on('data', function(data) { done(new Error(data), null); }); childProcess.on('error', function(err) { if (err.code === 'ENOENT') { console.warn('Warning! Please install librsvg package.'); } done(err, null); }); } catch (err) { done(err, null); } } else { var img = gm(Buffer.from(task.svg, 'utf8')); applyImageParams(img, task.params); img.toBuffer(task.params.type, done); // var childProcess; // try { // childProcess = spawn(isWin ? 'magick' : 'convert', ['svg:-', task.params.type + ':-']); // var buffer; // childProcess.stdout.on('data', function(data) { // console.log('data'); // try { // var prevBufferLength = (buffer ? buffer.length : 0), // newBuffer = new Buffer(prevBufferLength + data.length); // // if (buffer) { // buffer.copy(newBuffer, 0, 0); // } // // data.copy(newBuffer, prevBufferLength, 0); // // buffer = newBuffer; // } catch (err) { // done(err, null); // } // }); // // childProcess.on('close', function(code) { // if (!code) { // done(null, buffer); // } // }); // // childProcess.on('error', function(err) { // if (err.code == 'ENOENT') { // console.log('Warning! Please install imagemagick utility. (https://www.imagemagick.org/script/binary-releases.php)'); // } // done(err, null); // }); // // childProcess.stdin.write(task.svg); // childProcess.stdin.end(); // } catch (err) { // done(err, null); // } } } function loadDefaultFontsSync() { var fontFilesList = fs.readdirSync(defaultFontsDir); for (var i = 0, len = fontFilesList.length; i < len; i++) { var fileName = fontFilesList[i]; var font = opentype.loadSync(defaultFontsDir + '/' + fileName); fonts[font.names.fullName.en.toLowerCase()] = font; } return fonts; } function convertSvgToImageData(svg, params, callback) { convertQueue.push({svg: svg, params: params}, callback); } function convertSvgToImageDataSync(svg, params) { var convert = spawnSync('convert', ['svg:-', params.type + ':-'], { input: svg }); return convert.stdout; } //endregion utils //region --- API function exportTo(target, options, callback) { if (!target) { console.warn('Can\'t read input data for exporting.'); } var params = getParams(arguments); // params.target = target; if (typeof callback == 'function') { if (params.type == 'svg') { process.nextTick(function() { var svg = getSvg(target, params); callback(null, svg); }) } else { var svg = getSvg(target, params); convertSvgToImageData(svg, params, callback); } } else { return new promiseLibrary(function(resolve, reject) { if (params.type == 'svg') { process.nextTick(function() { var svg = getSvg(target, params); resolve(svg); }) } else { var svg = getSvg(target, params); var done = function(err, image) { if (err) reject(err); else resolve(image); }; convertSvgToImageData(svg, params, done); } }) } } function exportToSync(target, options) { var params = getParams(arguments); var svg = getSvg(target, params); return params.type == 'svg' ? svg : convertSvgToImageDataSync(svg, params); } function loadFont(path, callback) { if (typeof callback == 'function') { opentype.load(path, function(err, font) { if (!err) fonts[font.names.fullName.en.toLowerCase()] = font; callback(err, font); }); } else { return new promiseLibrary(function(resolve, reject) { opentype.load(path, function(err, font) { if (err) { reject(err); } else { fonts[font.names.fullName.en.toLowerCase()] = font; resolve(font); } }); }) } } function loadFontSync(path) { return fonts[font.names.fullName.en.toLowerCase()] = opentype.loadSync(path); } //endregion loadDefaultFontsSync(); anychartify(document); exports.exportTo = exportTo; exports.exportToSync = exportToSync; exports.loadFont = loadFont; exports.loadFontSync = loadFontSync; exports.concurrency = concurrency; return exports; });