├── .gitignore ├── .npmignore ├── examples ├── beep.png ├── america.png ├── repng.png ├── tomato.png ├── beep.js ├── RobotoMono-Regular.woff2 ├── tomato.js ├── large.js ├── america.js ├── repng.js └── grid.js ├── CHANGELOG.md ├── README.md ├── test.js ├── package.json ├── index.js └── cli.js /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | coverage 3 | node_modules -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples 2 | test.js 3 | .nyc_output 4 | coverage 5 | -------------------------------------------------------------------------------- /examples/beep.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxnblk/repng/HEAD/examples/beep.png -------------------------------------------------------------------------------- /examples/america.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxnblk/repng/HEAD/examples/america.png -------------------------------------------------------------------------------- /examples/repng.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxnblk/repng/HEAD/examples/repng.png -------------------------------------------------------------------------------- /examples/tomato.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxnblk/repng/HEAD/examples/tomato.png -------------------------------------------------------------------------------- /examples/beep.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default props => 4 |

{'你好'}

5 | -------------------------------------------------------------------------------- /examples/RobotoMono-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxnblk/repng/HEAD/examples/RobotoMono-Regular.woff2 -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # Changelog 3 | 4 | ## [3.0.2] 2018-08-128 5 | 6 | - Fix streams for Node 10 7 | 8 | ## [3.0.1] 2018-08-128 9 | 10 | - Fix for UTF-8 encoding 11 | 12 | ## [3.0.0] 2018-04-22 13 | 14 | - Use Puppeteer 15 | - Normalize CLI options 16 | - Removes dev server 17 | -------------------------------------------------------------------------------- /examples/tomato.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default props => 4 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /examples/large.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default props => 4 | 13 | 19 | 20 | -------------------------------------------------------------------------------- /examples/america.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | 4 | const stripes = Array.from({ length: 13 }) 5 | .map((n, i) => ( 6 | 13 | )) 14 | 15 | const America = () => ( 16 | 20 | {stripes} 21 | 25 | 26 | ) 27 | 28 | export default America 29 | 30 | -------------------------------------------------------------------------------- /examples/repng.js: -------------------------------------------------------------------------------- 1 | // JSX and ES2015 are supported as well 2 | 3 | import React from 'react' 4 | 5 | const Svg = props => ( 6 | 14 | 20 | 30 | $ repng 31 | 32 | 33 | ) 34 | 35 | export default Svg 36 | -------------------------------------------------------------------------------- /examples/grid.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | 4 | const Grid = ({ 5 | width = 256, 6 | height = 256, 7 | size = 2 8 | }) => { 9 | const rects = Array.from({ length: 32 / size }) 10 | .map((n, y) => ( 11 | Array.from({ length: 32 / size }) 12 | .map((n, x) => ( 13 | 20 | )) 21 | )) 22 | 23 | return ( 24 | 28 | {rects} 29 | 30 | ) 31 | } 32 | 33 | export default Grid 34 | 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # repng 3 | 4 | React component to PNG converter, built with [Puppeteer][puppeteer] 5 | 6 | ![](examples/repng.png) 7 | 8 | ```sh 9 | npm i -g repng 10 | ``` 11 | 12 | ```sh 13 | repng Icon.js --width 512 --height 512 --out-dir assets 14 | ``` 15 | 16 | ``` 17 | Usage 18 | $ repng 19 | 20 | Options 21 | -d --out-dir Directory to save file to 22 | -f --filename Specify a custom output filename 23 | -w --width Width of image 24 | -h --height Height of image 25 | -p --props Props in JSON format (or path to JSON file) to pass to the React component 26 | -t --type Type of output (png default) (pdf, jpeg or png) 27 | --css Path to CSS file to include 28 | --webfont Path to custom webfont for rendering 29 | --puppeteer Options for Puppeteer in JSON format 30 | ``` 31 | 32 | ### Related 33 | 34 | - [Puppeteer][puppeteer] 35 | 36 | MIT License 37 | 38 | [puppeteer]: https://github.com/GoogleChrome/puppeteer 39 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const React = require('react') 3 | const isStream = require('is-stream') 4 | const repng = require('./index') 5 | 6 | // fixture 7 | const Grid = ({ 8 | width = 256, 9 | height = 256, 10 | size = 2 11 | }) => { 12 | const rects = Array.from({ length: 32 / size }) 13 | .map((n, y) => ( 14 | Array.from({ length: 32 / size }) 15 | .map((n, x) => ( 16 | React.createElement('rect', { 17 | width: size, 18 | height: size, 19 | fill: (x % 2 === (y % 2 ? 1 : 0)) ? 'black' : 'transparent', 20 | x: size * x, 21 | y: size * y, 22 | }) 23 | )) 24 | )) 25 | 26 | return ( 27 | React.createElement('svg', { 28 | viewBox: '0 0 32 32', 29 | width, 30 | height, 31 | }, rects) 32 | ) 33 | } 34 | 35 | test('is a function', t => { 36 | t.is(typeof repng, 'function') 37 | }) 38 | 39 | test('returns a stream', async t => { 40 | const width = 128 41 | const height = width 42 | 43 | const result = await repng(Grid, { 44 | width, 45 | height, 46 | props: { 47 | width, 48 | height, 49 | } 50 | }) 51 | 52 | t.true(Buffer.isBuffer(result)) 53 | }) 54 | 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "repng", 3 | "version": "4.0.1", 4 | "description": "React component to PNG converter", 5 | "main": "index.js", 6 | "repository": "https://github.com/jxnblk/repng.git", 7 | "bin": { 8 | "repng": "./cli.js" 9 | }, 10 | "engines": { 11 | "node": ">=10.0.0" 12 | }, 13 | "scripts": { 14 | "test": "nyc ava", 15 | "cover": "nyc report --reporter html", 16 | "start": "npm run example:repng && npm run example:america && npm run example:tomato", 17 | "example:repng": "./cli.js examples/repng.js -w 512 -h 512 -d examples -f repng.png --webfont examples/RobotoMono-Regular.woff2", 18 | "example:america": "./cli.js examples/america.js -w 400 -h 300 -d examples -f america.png", 19 | "example:tomato": "./cli.js examples/tomato.js -d examples -f tomato.png", 20 | "example:beep": "./cli.js examples/beep.js -d examples -f beep.png", 21 | "help": "./cli.js --help" 22 | }, 23 | "dependencies": { 24 | "@babel/core": "^7.4.4", 25 | "@babel/preset-env": "^7.4.4", 26 | "@babel/preset-react": "^7.8.3", 27 | "@babel/register": "^7.8.6", 28 | "datauri": "^2.0.0", 29 | "meow": "^6.0.1", 30 | "ora": "^4.0.3", 31 | "puppeteer": "^2.1.1", 32 | "react": "^16.8.4", 33 | "react-dom": "^16.8.4", 34 | "read-pkg-up": "^7.0.1", 35 | "resolve-cwd": "^3.0.0" 36 | }, 37 | "devDependencies": { 38 | "ava": "^3.5.0", 39 | "is-stream": "^2.0.0", 40 | "nyc": "^15.0.0" 41 | }, 42 | "keywords": [ 43 | "react", 44 | "png", 45 | "cli" 46 | ], 47 | "author": "Brent Jackson", 48 | "license": "MIT", 49 | "ava": { 50 | "require": [ 51 | "@babel/register" 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('@babel/register')({ 2 | plugins: [ 3 | ].map(require.resolve), 4 | presets: [ 5 | '@babel/preset-env', 6 | '@babel/preset-react' 7 | ].map(require.resolve) 8 | }) 9 | 10 | const fs = require('fs') 11 | const puppeteer = require('puppeteer') 12 | const { Readable } = require('stream') 13 | const path = require('path') 14 | const { createElement: h } = require('react') 15 | const { renderToStaticMarkup } = require('react-dom/server') 16 | const Datauri = require('datauri') 17 | const resolveCWD = require('resolve-cwd') 18 | 19 | const baseCSS = `*{box-sizing:border-box}body{margin:0;font-family:system-ui,sans-serif}` 20 | 21 | const getHtmlData = ({ 22 | body, 23 | baseCSS, 24 | css, 25 | styles, 26 | webfont 27 | }) => { 28 | const fontCSS = webfont ? getWebfontCSS(webfont) : '' 29 | const html = ` 30 | 31 | 32 | ${styles} 33 | 34 | 35 | ${body}` 36 | return html 37 | } 38 | 39 | const getWebfontCSS = (fontpath) => { 40 | const { content } = new Datauri(fontpath) 41 | const [ name, ext ] = fontpath.split('/').slice(-1)[0].split('.') 42 | const css = (`@font-face { 43 | font-family: '${name}'; 44 | font-style: normal; 45 | font-weight: 400; 46 | src: url(${content}); 47 | }`) 48 | return css 49 | } 50 | 51 | module.exports = async (Component, opts = {}) => { 52 | const { 53 | css = '', 54 | filename, 55 | outDir, 56 | webfont, 57 | type = 'png', // jpeg, png and pdf are allowed 58 | } = opts 59 | 60 | const props = Object.assign({ 61 | width: opts.width, 62 | height: opts.height, 63 | }, opts.props) 64 | 65 | let styles = '' 66 | const el = h(Component, props) 67 | const body = renderToStaticMarkup(el) 68 | 69 | const html = getHtmlData({ 70 | body, 71 | baseCSS, 72 | css, 73 | styles, 74 | webfont 75 | }) 76 | 77 | const browser = await puppeteer.launch(opts.puppeteer) 78 | const page = await browser.newPage() 79 | await page.setContent(html) 80 | 81 | let rect = {} 82 | if (!opts.width && !opts.height) { 83 | const bodyEl = await page.$('body') 84 | rect = await bodyEl.boxModel() 85 | } 86 | const width = parseInt(opts.width || rect.width) 87 | const height = parseInt(opts.height || rect.height) 88 | 89 | await page.setViewport({ 90 | width, 91 | height, 92 | }) 93 | 94 | let result 95 | if (type === 'pdf') { 96 | result = await page.pdf({ 97 | width, 98 | height, 99 | }) 100 | } else { 101 | result = await page.screenshot({ 102 | type: type, 103 | clip: { 104 | x: 0, 105 | y: 0, 106 | width, 107 | height, 108 | }, 109 | omitBackground: true 110 | }) 111 | } 112 | 113 | await browser.close() 114 | 115 | return result 116 | } 117 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const fs = require('fs') 3 | const path = require('path') 4 | const meow = require('meow') 5 | const ora = require('ora') 6 | const render = require('./index') 7 | const readPkg = require('read-pkg-up') 8 | 9 | const absolute = (file = '') => !file || path.isAbsolute(file) 10 | ? file 11 | : path.join(process.cwd(), file) 12 | 13 | const getDateTime = () => { 14 | const now = new Date() 15 | const Y = now.getFullYear() 16 | const M = now.getMonth() 17 | const d = now.getDate() 18 | const h = now.getHours() 19 | const m = now.getMinutes() 20 | const s = now.getSeconds() 21 | return { 22 | date: [ Y, M, d ].join('-'), 23 | time: [ h, m, s ].join('.') 24 | } 25 | } 26 | 27 | const cli = meow(` 28 | Usage 29 | $ repng 30 | 31 | Options 32 | -d --out-dir Directory to save file to 33 | -f --filename Specify a custom output filename 34 | -w --width Width of image 35 | -h --height Height of image 36 | -p --props Props in JSON format (or path to JSON file) to pass to the React component 37 | -t --type Type of ouptut (png default) (pdf, jpeg or png) 38 | --css Path to CSS file to include 39 | --webfont Path to custom webfont for rendering 40 | `, { 41 | flags: { 42 | outDir: { 43 | type: 'string', 44 | alias: 'd' 45 | }, 46 | filename: { 47 | type: 'string', 48 | alias: 'f' 49 | }, 50 | width: { 51 | type: 'string', 52 | alias: 'w' 53 | }, 54 | height: { 55 | type: 'string', 56 | alias: 'h' 57 | }, 58 | props: { 59 | type: 'string', 60 | alias: 'p' 61 | }, 62 | css: { 63 | type: 'string' 64 | }, 65 | type: { 66 | type: 'string', 67 | alias: 't' 68 | }, 69 | puppeteer: { 70 | type: 'string', 71 | }, 72 | } 73 | }) 74 | 75 | const [ file ] = cli.input 76 | 77 | const spinner = ora(`Rendering ${file}`).start() 78 | 79 | const name = path.basename(file, path.extname(file)) 80 | const filepath = absolute(file) 81 | const { pkg } = readPkg.sync({ cwd: filepath }) 82 | const opts = Object.assign({ 83 | outDir: process.cwd(), 84 | filepath, 85 | }, cli.flags) 86 | const Component = require(filepath).default || require(filepath) 87 | 88 | opts.css = absolute(opts.css) 89 | opts.outDir = absolute(opts.outDir) 90 | 91 | if (opts.css) { 92 | opts.css = fs.readFileSync(opts.css, 'utf8') 93 | } 94 | 95 | if (opts.props) { 96 | const stat = fs.statSync(opts.props) 97 | if (stat.isFile()) { 98 | const req = path.join(process.cwd(), opts.props) 99 | try { 100 | opts.props = require(req) 101 | } catch (e) { 102 | console.log(e) 103 | opts.props = {} 104 | } 105 | } else { 106 | opts.props = JSON.parse(opts.props) 107 | } 108 | } 109 | 110 | if (opts.puppeteer) opts.puppeteer = JSON.parse(opts.puppeteer) 111 | 112 | const run = async () => { 113 | try { 114 | const image = await render(Component, opts) 115 | const { date, time } = getDateTime() 116 | const outFile = opts.filename 117 | ? opts.filename 118 | : `${name}-${date}-${time}.png` 119 | const outPath = path.join(opts.outDir, outFile) 120 | 121 | const file = fs.createWriteStream(outPath) 122 | 123 | file.on('finish', () => { 124 | spinner.succeed(`File saved to ${outPath}`) 125 | process.exit() 126 | }) 127 | 128 | file.on('error', err => { 129 | spinner.fail('Error: ' + err) 130 | }) 131 | 132 | spinner.info('Saving file') 133 | 134 | file.write(image) 135 | file.end() 136 | } catch (err) { 137 | spinner.fail(`Error: ${err}`) 138 | process.exit(1) 139 | } 140 | } 141 | 142 | run() 143 | --------------------------------------------------------------------------------