Created
January 16, 2016 19:33
-
-
Save tomazzaman/a9d1c975eba44ba53879 to your computer and use it in GitHub Desktop.
Generate PNG from a React-powered SVG. Server-side.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Run this with `babel-node generateSVG.js` | |
*/ | |
import fs from 'fs'; | |
import path from 'path'; | |
import { Readable } from 'stream'; | |
import childProcess from 'child_process'; | |
import phantomjs from 'phantomjs'; | |
import im from 'imagemagick'; | |
import tmp from 'tmp'; | |
import React from 'react'; | |
import ReactDOMServer from 'react-dom/server'; | |
import LineChart from '../js/components/chart/LineChart.js'; | |
/** | |
* Generate raw SVG string with React and components from the client app. | |
* Note that we have to force "xmlns" attribute into the final SVG, otherwise | |
* PhantomJS will be unable to parse it correctly, resulting in broken chart | |
* @param {[type]} data JSON array of performance data | |
* @return {Buffer} SVG string converted to buffer | |
*/ | |
function generateStaticMarkupSVG(data) { | |
const chart = ReactDOMServer.renderToStaticMarkup( | |
<LineChart data={ data } font="MuseoSans" /> | |
); | |
const buffer = chart.replace('<svg ', '<svg xmlns="http://www.w3.org/2000/svg" '); | |
return new Buffer(buffer); | |
} | |
/** | |
* Removes the prefix from Base64 encoded image string | |
* @param {Buffer} buffer Buffer we get from PhantomJS | |
* @return {Buffer} Buffer without the prefix | |
*/ | |
function removePrefix(buffer) { | |
const PREFIX = "data:image/png;base64,"; | |
const result = buffer.toString(); | |
return new Buffer(result.substring(PREFIX.length), "base64"); | |
} | |
/** | |
* Launch PhantomJS script as a child process. This is how PhantomJS works. | |
* @param {Array} args Arguments to pass to the child process | |
* @return {Process} NodeJS process (with stdout and stdin) | |
*/ | |
function launchPhantomJS(args) { | |
return childProcess.execFile(phantomjs.path, args, { maxBuffer: Infinity }); | |
} | |
/** | |
* Send the buffer of our SVG chart to PhantomJS child process through stdin. | |
* @param {Buffer} sourceBuffer | |
* @param {Process} child | |
*/ | |
function writeBufferToChild(sourceBuffer, child) { | |
child.stdin.setEncoding('utf-8'); | |
child.stdin.write(sourceBuffer.toString("base64")); | |
child.stdin.end(); | |
} | |
const ImageMagickArgs = [ | |
'-filter', 'Triangle', | |
'-define', 'filter:support=2', | |
'-unsharp', '0.25x0.25+8+0.065', | |
'-dither', 'None', | |
'-posterize', '136', | |
'-define', 'png:compression-filter=1', | |
'-define', 'png:compression-level=9', | |
'-define', 'png:compression-strategy=4', | |
'-define', 'png:exclude-chunk=all', | |
'-interlace', 'none', | |
'-interpolate', 'integer', | |
'-colorspace', 'sRGB', | |
'-strip', | |
'-resize', '1200x600', | |
'-depth', '16', | |
'-quality','100', | |
]; | |
/** | |
* Take the pre-set params for ImageMagick and generate two additional | |
* parameters: input file and output file, which should be positioned as the | |
* first and last elements of the configuration array, respectively. | |
* Also generate a temporary file to save the pre-resized image string to | |
* @param {Buffer} buffer Base64 encoded PNG string | |
* @param {String} partnerTrackingName Tracking name for file name generation | |
* @return {Array} ImageMagick params array | |
*/ | |
function generateIMParams(buffer, partnerTrackingName) { | |
const tmpFile = tmp.fileSync(); | |
fs.writeFileSync(tmpFile.name, buffer); | |
const epoch = Math.floor((new Date).getTime()/1000); | |
const finalFileName = `report-${partnerTrackingName}-${epoch}.png`; | |
const outputPath = path.resolve('reports', finalFileName); | |
ImageMagickArgs.unshift(tmpFile.name); // put the input file at the beginning | |
ImageMagickArgs.push(outputPath); // put the output file at the end | |
return ImageMagickArgs; | |
} | |
/** | |
* Change the following twi variables when implementing a call with Ruby | |
*/ | |
import data from '../js/data.js'; | |
const partnerTrackingName = 'johnCena'; | |
const sourceBuffer = generateStaticMarkupSVG(data); | |
const phantomChild = launchPhantomJS([path.resolve(__dirname, 'render.js')]); | |
let base64EncodedImage = ''; | |
phantomChild.stdout.on('data', (chunk) => base64EncodedImage += chunk ); | |
/** | |
* When Phantom ends with generating the (Base64 encoded) image, we we remove | |
* the prefix (because it results in corrupt image), generate the params for | |
* ImageMagick, and run it to downsize the image | |
*/ | |
phantomChild.stdout.on('end', function() { | |
const buffer = removePrefix(base64EncodedImage); | |
const args = generateIMParams(buffer, partnerTrackingName); | |
im.convert(args, err => { | |
if (err) throw err; | |
process.stdout.write(args.pop()); | |
}); | |
}); | |
writeBufferToChild(sourceBuffer, phantomChild); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* This file is called as a child process to make sure it's running in a | |
* PhantomJS-aware environment. | |
* ES6 can't be used here. | |
*/ | |
const page = require('webpage').create(); | |
const system = require('system'); | |
const sourceBase64 = system.stdin.readLine(); | |
/** | |
* Set some defaults. Since our SVG chart is 600x300 pixels, we create 4 times | |
* larger viewport, in order to make sure the resolution is good. We resize | |
* the image to smaller dimension in parent process. | |
*/ | |
page.viewportSize = { width: 2400, height: 1200 }; | |
page.clipRect = { top: 0, left: 0, width: 2400, height: 1200 }; | |
page.zoomFactor = 4; | |
/** | |
* Opens a page with PhantomJS, and it has Base64-encoded string of the SVG | |
* image in the address bar, instead of a regular address, which is valid. | |
* Writes result to stdout, which parent's process is able to read and process. | |
*/ | |
const PREFIX = "data:image/svg+xml;base64,"; | |
page.open(PREFIX + sourceBase64, function() { | |
const result = "data:image/png;base64," + page.renderBase64("PNG"); | |
system.stdout.write(result); | |
phantom.exit(); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment