├── .circleci └── config.yml ├── .eslintignore ├── .eslintrc.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── docs ├── arduino.cpp.png ├── banner.js.png ├── flask.py.png ├── foreach_seq_diag.svg ├── rails.rb.png └── react.jsx.png ├── package.json ├── poi.config.js ├── src ├── app.js ├── camera.js ├── code.jsx ├── index.html ├── style.css └── themes │ └── tomorrow.css ├── src2png ├── test ├── .eslintrc.yml ├── fixtures │ ├── code │ │ ├── input │ │ │ ├── example.cpp │ │ │ ├── example.jsx │ │ │ ├── example.py │ │ │ └── example.rb │ │ └── output │ │ │ ├── example.cpp.png │ │ │ ├── example.jsx.png │ │ │ ├── example.py.png │ │ │ └── example.rb.png │ └── images │ │ └── croppable.png ├── integration │ └── integration_test.sh ├── unit │ └── camera_test.js └── utils │ ├── images_identical │ └── install_fira_code └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | checkout_code: 4 | docker: 5 | - image: regviz/node-xcb 6 | working_directory: ~/repo 7 | steps: 8 | - checkout 9 | - run: 10 | command: echo $CIRCLE_SHA1 > .circle-sha 11 | - save_cache: 12 | key: v1-repo-{{ checksum ".circle-sha" }} 13 | paths: 14 | - ~/repo 15 | 16 | install_dependencies: 17 | docker: 18 | - image: regviz/node-xcb 19 | working_directory: ~/repo 20 | steps: 21 | - run: 22 | command: echo $CIRCLE_SHA1 > .circle-sha 23 | - restore_cache: 24 | keys: 25 | - v1-repo-{{ checksum ".circle-sha" }} 26 | - restore_cache: 27 | keys: 28 | - v1-dependencies-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }} 29 | - run: yarn install 30 | - run: test/utils/install_fira_code 31 | - save_cache: 32 | paths: 33 | - node_modules 34 | - ~/.fonts 35 | key: v1-dependencies-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }} 36 | 37 | lint: 38 | docker: 39 | - image: regviz/node-xcb 40 | working_directory: ~/repo 41 | steps: 42 | - run: 43 | command: echo $CIRCLE_SHA1 > .circle-sha 44 | - restore_cache: 45 | keys: 46 | - v1-repo-{{ checksum ".circle-sha" }} 47 | - restore_cache: 48 | keys: 49 | - v1-dependencies-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }} 50 | - run: yarn lint 51 | 52 | test_unit: 53 | docker: 54 | - image: regviz/node-xcb 55 | working_directory: ~/repo 56 | steps: 57 | - run: 58 | command: echo $CIRCLE_SHA1 > .circle-sha 59 | - restore_cache: 60 | keys: 61 | - v1-repo-{{ checksum ".circle-sha" }} 62 | - restore_cache: 63 | keys: 64 | - v1-dependencies-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }} 65 | - run: yarn test 66 | 67 | test_integration: 68 | docker: 69 | - image: regviz/node-xcb 70 | working_directory: ~/repo 71 | steps: 72 | - run: 73 | command: echo $CIRCLE_SHA1 > .circle-sha 74 | - restore_cache: 75 | keys: 76 | - v1-repo-{{ checksum ".circle-sha" }} 77 | - restore_cache: 78 | keys: 79 | - v1-dependencies-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }} 80 | - run: yarn test-integration 81 | - store_artifacts: 82 | path: ~/repo/tmp 83 | 84 | workflows: 85 | version: 2 86 | build_and_test: 87 | jobs: 88 | - checkout_code 89 | - install_dependencies: 90 | requires: 91 | - checkout_code 92 | - lint: 93 | requires: 94 | - install_dependencies 95 | - test_unit: 96 | requires: 97 | - install_dependencies 98 | - test_integration: 99 | requires: 100 | - install_dependencies 101 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | test/fixtures/**/* 2 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | parserOptions: 2 | ecmaVersion: 2017 3 | 4 | extends: 5 | - standard 6 | - vue 7 | # - plugin:import/errors 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | tmp/ 3 | src/tmp/ 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2017 Matt Lewis (matt@mplewis.com) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # src2png 2 | 3 | Turn your source code into beautiful syntax-highlighted images. Great for presentations. 4 | 5 | 6 | 7 | # Examples 8 | 9 | [React (JSX)](https://facebook.github.io/react/tutorial/tutorial.html) | [Ruby on Rails](https://bitbucket.org/railstutorial/sample_app_4th_ed/src/5dd7038b99dd331285cf003cfd3f59ba06376027/app/controllers/password_resets_controller.rb?at=master&fileviewer=file-view-default) | [Python](https://github.com/allisson/flask-example/blob/master/accounts/views.py) | [C++](https://github.com/arduino/Arduino/blob/master/hardware/arduino/avr/libraries/Wire/src/Wire.cpp) 10 | ------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------- 11 | ![](/docs/react.jsx.png) | ![](/docs/rails.rb.png) | ![](/docs/flask.py.png) | ![](/docs/arduino.cpp.png) 12 | 13 | # Usage 14 | 15 | Install the [Fira Code](https://github.com/tonsky/FiraCode) font. 16 | 17 | ```sh 18 | yarn install 19 | brew install imagemagick # trims image margins 20 | ./src2png YOUR_SOURCE_FILE [YOUR_SOURCE_FILE [...]] 21 | ls ./tmp # screenshots are saved here 22 | ``` 23 | 24 | # How It Works 25 | 26 | * Starts a [Poi](https://github.com/egoist/poi) dev server 27 | * Poi is a build tool that provides live hot reloading, Webpack, and Babel 28 | * Poi loads `app.js`, a Vue app 29 | * Vue mounts `code.jsx`, a component that presents the code in a webpage 30 | * `code.jsx` uses [Prism](http://prismjs.com/) to syntax highlight the code 31 | * Loads [Puppeteer](https://github.com/GoogleChrome/puppeteer) 32 | * Puppeteer starts an instance of Headless Chrome 33 | * Chrome is used to render the highlighted code 34 | * Renders, trims whitespace, and saves screenshots for each file (see diagram below) 35 | 36 | ![](/docs/foreach_seq_diag.svg) 37 | 38 | # FAQ 39 | 40 | **Why did you do this?** 41 | 42 | I needed high-quality screenshots of syntax-highlighted code snippets for a presentation. 43 | 44 | Chrome is an excellent rendering engine, and there are tons of JS libraries that apply syntax highlighting to code. 45 | 46 | **Why did you do this in a headless Chrome browser and dev server? Isn't there something simpler?** 47 | 48 | Not for rendering text nicely. The alternatives are: 49 | 50 | * laying out and coloring text manually in a visualization language like Processing 51 | * building a PDF, coloring it, and converting it to PNG 52 | * rendering and coloring text manually in ImageMagick, PIL, or other image libraries that aren't designed for text layout or flowing 53 | * manually laying out text lines, coloring them, and rendering – basically, building my own text rendering engine in JS Canvas 54 | 55 | **You're really starting a dev server to serve documents to Headless Chrome and using hot reloading as a production feature?** 56 | 57 | Yes. 58 | 59 | **Oh god, this is horrifying. You have built a monster and it is made of JavaScript.** 60 | 61 | Yes it is. Yes I have. 62 | 63 | I am sorry. This Lovecraftian amalgamation of software works too well for its own good. 64 | 65 | **Do you plan on releasing this on NPM?** 66 | 67 | Not as long as it still sucks (starts a dev server via subprocesses, has a bad CLI, etc). 68 | 69 | **How do I change the theme/font/style?** 70 | 71 | Put themes in `src/themes` and change the CSS import in `code.jsx`. 72 | 73 | Write style overrides in `src/style.css`. 74 | 75 | **It doesn't add syntax highlighting to my file. How do I make it work?** 76 | 77 | Prism probably doesn't recognize your file's extension as the name of a format. Check out `extensionCodes` in `src/code.jsx` and add a mapping from your file extension to a supported Prism format name. 78 | 79 | # License 80 | 81 | MIT 82 | -------------------------------------------------------------------------------- /docs/arduino.cpp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mplewis/src2png/e20825edaa9153abbf31a18bfe388e9bafebe35a/docs/arduino.cpp.png -------------------------------------------------------------------------------- /docs/banner.js.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mplewis/src2png/e20825edaa9153abbf31a18bfe388e9bafebe35a/docs/banner.js.png -------------------------------------------------------------------------------- /docs/flask.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mplewis/src2png/e20825edaa9153abbf31a18bfe388e9bafebe35a/docs/flask.py.png -------------------------------------------------------------------------------- /docs/foreach_seq_diag.svg: -------------------------------------------------------------------------------- 1 | Note left of Node.js: For each input file: 2 | Node.js -> Poi: Copy input file 3 | Note right of Poi: Dev server hot update, \nregenerate page content 4 | Puppeteer -> Poi: Request: page content 5 | Poi -> Puppeteer: Response: highlighted code 6 | Note right of Puppeteer: Render screenshot\n and save PNG 7 | Node.jsNode.jsPoiPoiPuppeteerPuppeteerFor each input file:Copy input fileDev server hot update,regenerate page contentRequest: page contentResponse: highlighted codeRender screenshotand save PNG -------------------------------------------------------------------------------- /docs/rails.rb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mplewis/src2png/e20825edaa9153abbf31a18bfe388e9bafebe35a/docs/rails.rb.png -------------------------------------------------------------------------------- /docs/react.jsx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mplewis/src2png/e20825edaa9153abbf31a18bfe388e9bafebe35a/docs/react.jsx.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "src2png", 3 | "version": "0.0.0", 4 | "main": "src/app.js", 5 | "author": "Matt Lewis ", 6 | "license": "MIT", 7 | "scripts": { 8 | "dev": "poi dev", 9 | "test": "mocha test test/unit/**/*_test.js", 10 | "test-server": "mocha test --watch test/unit/**/*_test.js", 11 | "test-integration": "test/integration/integration_test.sh", 12 | "lint": "eslint --ext js,jsx ." 13 | }, 14 | "devDependencies": { 15 | "chai": "^4.1.2", 16 | "eslint": "^3.0.0", 17 | "eslint-config-standard": "^10.2.1", 18 | "eslint-config-vue": "^2.0.2", 19 | "eslint-plugin-import": "^2.7.0", 20 | "eslint-plugin-node": "^5.1.1", 21 | "eslint-plugin-promise": "^3.5.0", 22 | "eslint-plugin-standard": "^3.0.1", 23 | "eslint-plugin-vue": "^2.1.0", 24 | "mocha": "^3.5.2" 25 | }, 26 | "dependencies": { 27 | "jimp": "^0.2.28", 28 | "poi": "^9.3.5", 29 | "prismjs": "^1.6.0", 30 | "prismjs-components-loader": "^2.0.0", 31 | "puppeteer": "^0.10.2", 32 | "raw-loader": "^0.5.1", 33 | "shelljs": "^0.7.8", 34 | "vue": "^2.4.2" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /poi.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | html: { 3 | template: 'src/index.html' 4 | }, 5 | webpack (config) { 6 | config.module.rules.push({ 7 | test: /\.(code|path)$/, 8 | use: [{ loader: 'raw-loader' }] 9 | }) 10 | return config 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Code from './code' 3 | 4 | const vue = new Vue({ ...Code }) 5 | vue.$mount('#app') 6 | -------------------------------------------------------------------------------- /src/camera.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require('puppeteer') 2 | const shell = require('shelljs') 3 | const cp = require('child_process') 4 | const path = require('path') 5 | const fs = require('fs') 6 | 7 | const DEV_SERVER = 'http://localhost:4000' 8 | 9 | // https://medium.com/@dtinth/making-unhandled-promise-rejections-crash-the-node-js-process-ffc27cfcc9dd 10 | process.on('unhandledRejection', err => { throw err }) 11 | 12 | function sourceFiles () { 13 | const files = process.argv.slice(2) 14 | if (files.length === 0) { 15 | console.error('Usage: node src/camera.js YOUR_SOURCE_FILE') 16 | process.exit(1) 17 | } 18 | return files 19 | } 20 | 21 | function startDevServer () { 22 | const proc = cp.spawn('yarn', ['dev']) 23 | proc.stdout.on('data', data => console.log(data.toString('utf8'))) 24 | proc.stderr.on('data', data => console.log(data.toString('utf8'))) 25 | proc.on('close', code => console.log(`child proc exited with code ${code}`)) 26 | } 27 | 28 | async function startBrowser () { 29 | // Puppeteer/Chrome run in root mode in the Docker image - disable non-root enforcement during testing 30 | const args = process.env.CI ? ['--no-sandbox', '--disable-setuid-sandbox'] : [] 31 | return puppeteer.launch({ args }) 32 | } 33 | 34 | async function pageForDevServer (browser) { 35 | const page = await browser.newPage() 36 | 37 | let ready = false 38 | while (!ready) { 39 | try { 40 | await page.goto(DEV_SERVER) 41 | ready = true 42 | } catch (e) { 43 | shell.exec('sleep 0.25') 44 | } 45 | } 46 | 47 | return page 48 | } 49 | 50 | async function screenshot (browser, dst) { 51 | const page = await pageForDevServer(browser) 52 | 53 | let [width, height] = await page.evaluate(() => window.codeDimensions) 54 | width = parseInt(width * 1.1) 55 | height = parseInt(height * 1.2) 56 | 57 | await page.setViewport({ width, height }) 58 | await page.screenshot({ path: dst }) 59 | return page.evaluate(() => window.error) 60 | } 61 | 62 | async function screenshotAndSaveAll (browser, files) { 63 | const errors = [] 64 | for (const src of files) { 65 | const dst = `tmp/${path.basename(src)}.png` 66 | 67 | shell.cp(src, 'src/tmp/source.code') 68 | fs.writeFileSync('src/tmp/source.path', src) 69 | 70 | const error = await screenshot(browser, dst) 71 | if (error) errors.push(error) 72 | 73 | trim(dst) 74 | show(dst) 75 | } 76 | 77 | return errors 78 | } 79 | 80 | function trim (path) { 81 | shell.exec(`convert ${path} -trim ${path}`) 82 | } 83 | 84 | function show (dst) { 85 | shell.exec(`open ${dst}`) 86 | } 87 | 88 | async function main () { 89 | const files = sourceFiles() 90 | shell.mkdir('-p', 'src/tmp') 91 | shell.mkdir('-p', 'tmp') 92 | 93 | startDevServer() 94 | 95 | const browser = await startBrowser() 96 | const errors = await screenshotAndSaveAll(browser, files) 97 | browser.close() 98 | 99 | for (const error of errors) { 100 | console.error(error) 101 | } 102 | 103 | process.exit() // kills any child processes (dev server) 104 | } 105 | 106 | module.exports = { trim, main } 107 | -------------------------------------------------------------------------------- /src/code.jsx: -------------------------------------------------------------------------------- 1 | import Prism from 'prismjs' 2 | import PrismLoader from 'prismjs-components-loader' 3 | import componentIndex from 'prismjs-components-loader/lib/all-components' 4 | 5 | import './themes/tomorrow.css' 6 | import './style.css' 7 | 8 | import sourceCode from './tmp/source.code' 9 | import sourcePath from './tmp/source.path' 10 | 11 | const extensionCodes = { 12 | js: 'javascript', 13 | py: 'python', 14 | rb: 'ruby', 15 | ts: 'typescript' 16 | } 17 | 18 | const extension = sourcePath.match(/.+\.(.+)/)[1] 19 | const langCode = extensionCodes[extension] || extension 20 | const langClass = `language-${langCode}` 21 | 22 | const prismLoader = new PrismLoader(componentIndex) 23 | try { 24 | prismLoader.load(Prism, langCode) 25 | } catch (e) { 26 | console.warn(e) 27 | window.error = e 28 | } 29 | 30 | const Code = { 31 | mounted () { 32 | const codeElem = this.$refs.code 33 | window.codeDimensions = [codeElem.offsetWidth, codeElem.offsetHeight] 34 | Prism.highlightAll() 35 | }, 36 | 37 | render () { 38 | return ( 39 |
40 |
41 |           
42 |             {sourceCode}
43 |           
44 |         
45 |
46 | ) 47 | } 48 | } 49 | 50 | export default Code 51 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | .code-elem[class*="language-"] { 2 | display: inline-block; 3 | font-family: 'Fira Code'; 4 | font-size: 72px; 5 | white-space: pre; 6 | } 7 | 8 | pre[class*="language-"], code[class*="language-"] { 9 | margin: 0; 10 | padding: 0; 11 | } 12 | -------------------------------------------------------------------------------- /src/themes/tomorrow.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Based on files from https://github.com/idleberg/base16-prism 4 | 5 | Name: Base16 Tomorrow Light 6 | Author: Chris Kempson (http://chriskempson.com) 7 | 8 | Prism template by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/prism/) 9 | Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) 10 | 11 | */ 12 | 13 | code[class*="language-"], 14 | pre[class*="language-"] { 15 | color: #ffffff; 16 | font-family: Consolas, Menlo, Monaco, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", "Courier New", Courier, monospace; 17 | font-size: 14px; 18 | line-height: 1.375; 19 | direction: ltr; 20 | text-align: left; 21 | word-spacing: normal; 22 | 23 | -moz-tab-size: 4; 24 | -o-tab-size: 4; 25 | tab-size: 4; 26 | 27 | -webkit-hyphens: none; 28 | -moz-hyphens: none; 29 | -ms-hyphens: none; 30 | hyphens: none; 31 | white-space: pre; 32 | white-space: pre-wrap; 33 | word-break: break-all; 34 | word-wrap: break-word; 35 | background: #ffffff; 36 | color: #373b41; 37 | } 38 | 39 | /* Code blocks */ 40 | pre[class*="language-"] { 41 | padding: 1em; 42 | margin: .5em 0; 43 | overflow: auto; 44 | } 45 | 46 | /* Inline code */ 47 | :not(pre) > code[class*="language-"] { 48 | padding: .1em; 49 | border-radius: .3em; 50 | } 51 | 52 | .token.comment, 53 | .token.prolog, 54 | .token.doctype, 55 | .token.cdata { 56 | color: #b4b7b4; 57 | } 58 | 59 | .token.punctuation { 60 | color: #373b41; 61 | } 62 | 63 | .namespace { 64 | opacity: .7; 65 | } 66 | 67 | .token.null, 68 | .token.operator, 69 | .token.boolean, 70 | .token.number { 71 | color: #f5871f; 72 | } 73 | .token.property { 74 | color: #eab700; 75 | } 76 | .token.tag { 77 | color: #4271ae; 78 | } 79 | .token.string { 80 | color: #3e999f; 81 | } 82 | .token.selector { 83 | color: #8959a8; 84 | } 85 | .token.attr-name { 86 | color: #f5871f; 87 | } 88 | .token.entity, 89 | .token.url, 90 | .language-css .token.string, 91 | .style .token.string { 92 | color: #3e999f; 93 | } 94 | 95 | .token.attr-value, 96 | .token.keyword, 97 | .token.control, 98 | .token.directive, 99 | .token.unit { 100 | color: #718c00; 101 | } 102 | 103 | .token.statement, 104 | .token.regex, 105 | .token.atrule { 106 | color: #3e999f; 107 | } 108 | 109 | .token.placeholder, 110 | .token.variable { 111 | color: #4271ae; 112 | } 113 | 114 | .token.important { 115 | color: #c82829; 116 | font-weight: bold; 117 | } 118 | 119 | .token.entity { 120 | cursor: help; 121 | } 122 | 123 | pre > code.highlight { 124 | outline: .4em solid red; 125 | outline-offset: .4em; 126 | } 127 | -------------------------------------------------------------------------------- /src2png: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('./src/camera').main() 4 | -------------------------------------------------------------------------------- /test/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | mocha: true 3 | -------------------------------------------------------------------------------- /test/fixtures/code/input/example.cpp: -------------------------------------------------------------------------------- 1 | extern "C" { 2 | #include 3 | #include 4 | #include 5 | #include "utility/twi.h" 6 | } 7 | 8 | #include "Wire.h" 9 | 10 | uint8_t TwoWire::transmitting = 0; 11 | void (*TwoWire::user_onRequest)(void); 12 | void (*TwoWire::user_onReceive)(int); 13 | 14 | // must be called in: 15 | // slave tx event callback 16 | // or after beginTransmission(address) 17 | size_t TwoWire::write(uint8_t data) { 18 | if (transmitting) { 19 | // in master transmitter mode 20 | // don't bother if buffer is full 21 | if (txBufferLength >= BUFFER_LENGTH) { 22 | setWriteError(); 23 | return 0; 24 | } 25 | // put byte in tx buffer 26 | txBuffer[txBufferIndex] = data; 27 | ++txBufferIndex; 28 | // update amount in buffer 29 | txBufferLength = txBufferIndex; 30 | } else { 31 | // in slave send mode 32 | // reply to master 33 | twi_transmit(&data, 1); 34 | } 35 | return 1; 36 | } 37 | 38 | // must be called in: 39 | // slave rx event callback 40 | // or after requestFrom(address, numBytes) 41 | int TwoWire::available(void) { return rxBufferLength - rxBufferIndex; } 42 | 43 | // must be called in: 44 | // slave rx event callback 45 | // or after requestFrom(address, numBytes) 46 | int TwoWire::read(void) { 47 | int value = -1; 48 | 49 | // get each successive byte on each call 50 | if (rxBufferIndex < rxBufferLength) { 51 | value = rxBuffer[rxBufferIndex]; 52 | ++rxBufferIndex; 53 | } 54 | 55 | return value; 56 | } 57 | -------------------------------------------------------------------------------- /test/fixtures/code/input/example.jsx: -------------------------------------------------------------------------------- 1 | class Game extends React.Component { 2 | constructor() { 3 | super(); 4 | this.state = { 5 | history: [ 6 | { 7 | squares: Array(9).fill(null) 8 | } 9 | ], 10 | stepNumber: 0, 11 | xIsNext: true 12 | }; 13 | } 14 | 15 | render() { 16 | const history = this.state.history; 17 | const current = history[this.state.stepNumber]; 18 | const winner = calculateWinner(current.squares); 19 | 20 | const moves = history.map((step, move) => { 21 | const desc = move ? "Move #" + move : "Game start"; 22 | return ( 23 |
  • 24 | this.jumpTo(move)}>{desc} 25 |
  • 26 | ); 27 | }); 28 | 29 | let status; 30 | if (winner) { 31 | status = "Winner: " + winner; 32 | } else { 33 | status = "Next player: " + (this.state.xIsNext ? "X" : "O"); 34 | } 35 | 36 | return ( 37 |
    38 |
    39 | this.handleClick(i)} 42 | /> 43 |
    44 |
    45 |
    {status}
    46 |
      {moves}
    47 |
    48 |
    49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/fixtures/code/input/example.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from flask import ( 3 | Blueprint, render_template, session, g, flash, request, redirect, url_for, 4 | current_app 5 | ) 6 | from accounts.models import User 7 | from accounts.forms import ( 8 | LoginForm, SignupForm, SignupConfirmForm, RecoverPasswordForm, 9 | RecoverPasswordConfirmForm 10 | ) 11 | from common.utils import get_signer 12 | 13 | 14 | accounts_app = Blueprint('accounts_app', __name__) 15 | 16 | 17 | @accounts_app.before_app_request 18 | def load_user(): 19 | g.user = None 20 | if 'user_id' in session: 21 | try: 22 | g.user = User.objects.get(pk=session['user_id']) 23 | except: 24 | pass 25 | 26 | 27 | @accounts_app.route('/login/', methods=['GET', 'POST']) 28 | def login(): 29 | next = request.values.get('next', '/') 30 | form = LoginForm() 31 | form.next.data = next 32 | if form.validate_on_submit(): 33 | session['user_id'] = unicode(form.user.pk) 34 | flash(u'Login successfully', 'success') 35 | return redirect(next) 36 | return render_template('accounts/login.html', form=form) 37 | 38 | 39 | @accounts_app.route('/logout/') 40 | def logout(): 41 | next = request.args.get('next', '/') 42 | flash(u'Logout successfully', 'success') 43 | session.pop('user_id', None) 44 | return redirect(next) 45 | 46 | 47 | @accounts_app.route('/signup/', methods=['GET', 'POST']) 48 | def signup(): 49 | form = SignupForm() 50 | if form.validate_on_submit(): 51 | form.save() 52 | flash( 53 | u'Check your email to confirm registration.', 54 | 'success' 55 | ) 56 | return redirect(url_for('pages_app.index')) 57 | return render_template('accounts/signup.html', form=form) 58 | -------------------------------------------------------------------------------- /test/fixtures/code/input/example.rb: -------------------------------------------------------------------------------- 1 | class PasswordResetsController < ApplicationController 2 | before_action :get_user, only: [:edit, :update] 3 | before_action :valid_user, only: [:edit, :update] 4 | before_action :check_expiration, only: [:edit, :update] # Case (1) 5 | 6 | def new 7 | end 8 | 9 | def create 10 | @user = User.find_by(email: params[:password_reset][:email].downcase) 11 | if @user 12 | @user.create_reset_digest 13 | @user.send_password_reset_email 14 | flash[:info] = "Email sent with password reset instructions" 15 | redirect_to root_url 16 | else 17 | flash.now[:danger] = "Email address not found" 18 | render 'new' 19 | end 20 | end 21 | 22 | def edit 23 | end 24 | 25 | def update 26 | if params[:user][:password].empty? # Case (3) 27 | @user.errors.add(:password, :blank) 28 | render 'edit' 29 | elsif @user.update_attributes(user_params) # Case (4) 30 | log_in @user 31 | flash[:success] = "Password has been reset." 32 | redirect_to @user 33 | else 34 | render 'edit' # Case (2) 35 | end 36 | end 37 | 38 | private 39 | 40 | def user_params 41 | params.require(:user).permit(:password, :password_confirmation) 42 | end 43 | 44 | # Before filters 45 | 46 | def get_user 47 | @user = User.find_by(email: params[:email]) 48 | end 49 | 50 | # Confirms a valid user. 51 | def valid_user 52 | unless (@user && @user.activated? && 53 | @user.authenticated?(:reset, params[:id])) 54 | redirect_to root_url 55 | end 56 | end 57 | 58 | # Checks expiration of reset token. 59 | def check_expiration 60 | if @user.password_reset_expired? 61 | flash[:danger] = "Password reset has expired." 62 | redirect_to new_password_reset_url 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /test/fixtures/code/output/example.cpp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mplewis/src2png/e20825edaa9153abbf31a18bfe388e9bafebe35a/test/fixtures/code/output/example.cpp.png -------------------------------------------------------------------------------- /test/fixtures/code/output/example.jsx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mplewis/src2png/e20825edaa9153abbf31a18bfe388e9bafebe35a/test/fixtures/code/output/example.jsx.png -------------------------------------------------------------------------------- /test/fixtures/code/output/example.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mplewis/src2png/e20825edaa9153abbf31a18bfe388e9bafebe35a/test/fixtures/code/output/example.py.png -------------------------------------------------------------------------------- /test/fixtures/code/output/example.rb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mplewis/src2png/e20825edaa9153abbf31a18bfe388e9bafebe35a/test/fixtures/code/output/example.rb.png -------------------------------------------------------------------------------- /test/fixtures/images/croppable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mplewis/src2png/e20825edaa9153abbf31a18bfe388e9bafebe35a/test/fixtures/images/croppable.png -------------------------------------------------------------------------------- /test/integration/integration_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # http://redsymbol.net/articles/unofficial-bash-strict-mode/ 4 | set -euo pipefail 5 | IFS=$'\n\t' 6 | 7 | ./src2png test/fixtures/code/input/* 8 | test/utils/images_identical tmp/example.cpp.png test/fixtures/code/output/example.cpp.png 9 | test/utils/images_identical tmp/example.jsx.png test/fixtures/code/output/example.jsx.png 10 | test/utils/images_identical tmp/example.py.png test/fixtures/code/output/example.py.png 11 | test/utils/images_identical tmp/example.rb.png test/fixtures/code/output/example.rb.png 12 | -------------------------------------------------------------------------------- /test/unit/camera_test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect 2 | 3 | const shell = require('shelljs') 4 | const Jimp = require('jimp') 5 | 6 | const { trim } = require('../../src/camera') 7 | 8 | describe('camera', function () { 9 | describe('trim', function () { 10 | context('with a source image', function () { 11 | const TO_CROP = 'test/tmp/croppable.png' 12 | before(function () { 13 | shell.mkdir('-p', 'test/tmp') 14 | shell.cp('test/fixtures/images/croppable.png', TO_CROP) 15 | }) 16 | 17 | it('trims whitespace from images', async function () { 18 | const orig = await Jimp.read(TO_CROP) 19 | expect(orig.bitmap.width).to.eq(200) 20 | expect(orig.bitmap.height).to.eq(200) 21 | 22 | trim(TO_CROP) 23 | 24 | const trimmed = await Jimp.read(TO_CROP) 25 | expect(trimmed.bitmap.width).to.eq(100) 26 | expect(trimmed.bitmap.height).to.eq(100) 27 | }) 28 | }) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /test/utils/images_identical: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const Jimp = require('jimp') 4 | 5 | // https://medium.com/@dtinth/making-unhandled-promise-rejections-crash-the-node-js-process-ffc27cfcc9dd 6 | process.on('unhandledRejection', err => { throw err }) 7 | 8 | function buffersEqual (a, b) { 9 | if (!Buffer.isBuffer(a)) return undefined 10 | if (!Buffer.isBuffer(b)) return undefined 11 | if (typeof a.equals === 'function') return a.equals(b) 12 | if (a.length !== b.length) return false 13 | 14 | for (var i = 0; i < a.length; i++) { 15 | if (a[i] !== b[i]) return false 16 | } 17 | 18 | return true 19 | } 20 | 21 | async function imageData (path) { 22 | const img = await Jimp.read(path) 23 | return img.bitmap.data 24 | } 25 | 26 | async function main () { 27 | const firstPath = process.argv[2] 28 | const secondPath = process.argv[3] 29 | const firstData = await imageData(firstPath) 30 | const secondData = await imageData(secondPath) 31 | 32 | if (!buffersEqual(firstData, secondData)) { 33 | console.error(`Images did not match: ${firstPath}, ${secondPath}`) 34 | process.exit(1) 35 | } 36 | 37 | console.log(`Images identical: ${firstPath}, ${secondPath}`) 38 | } 39 | 40 | main() 41 | -------------------------------------------------------------------------------- /test/utils/install_fira_code: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # http://redsymbol.net/articles/unofficial-bash-strict-mode/ 4 | set -euo pipefail 5 | IFS=$'\n\t' 6 | 7 | # https://github.com/tonsky/FiraCode/issues/4#issuecomment-69215023 8 | mkdir -p ~/.fonts 9 | # changing this hardcoded version of Fira Code may break the pre-captured screenshot fixtures! 10 | wget https://github.com/tonsky/FiraCode/raw/862454fcdaff57c869892d0e82ed348646005444/distr/otf/FiraCode-Regular.otf -O ~/.fonts/FiraCode-Regular.otf 11 | fc-cache -v 12 | fc-list | grep 'Fira Code' 13 | --------------------------------------------------------------------------------