├── .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 |
16 |
--------------------------------------------------------------------------------
/examples/large.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default props =>
4 |
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 |
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 |
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 |
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 | 
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 |
--------------------------------------------------------------------------------