├── .gitignore ├── README.md ├── examples ├── 1_draw_ncc_logo.js ├── 2_early_access.js ├── 3_get_return_values.js ├── 4_gradients_and_patterns.js ├── 5_images.js ├── 6_shadow_canvas.js └── dummy.jpg ├── footage ├── flow.png ├── flow.svg ├── logo.png └── logo.svg ├── index.js ├── lib ├── chrome-launcher.ts ├── ncc.html ├── ncc.ico ├── ncc.js ├── ncc.ts └── stripe.png ├── license.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | logo 5 |

6 | 7 | ### About 8 | **ncc** (or node-chrome-canvas) utilizes Googles [Chrome-Browser](https://www.google.com/chrome/browser/) and its [remote debugging protocol](https://developers.google.com/chrome-developer-tools/docs/debugger-protocol) to give [Node.js](http://nodejs.org/) access to a full-blown HTML5 Canvas-Element and its 2d-Context. 9 | In contrast to [canvas](https://www.npmjs.org/package/canvas) (that may satisfy your needs as well) which uses [Cairo](http://cairographics.org/) to sham a canvas, **ncc** works with a real [HTMLCanvasElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement) in a Browser-Context. 10 | 11 | Behind the curtains of the familiar Canvas-API, **ncc** uses a single WebSocket-Connection and some command-bundeling-logic to gain its performance. 12 | 13 | ### Quickstart 14 | ``` 15 | npm install ncc 16 | ``` 17 | ```javascript 18 | var ncc = require('ncc') 19 | 20 | var canvas = ncc(); 21 | 22 | canvas.width = canvas.height = 256; 23 | 24 | var ctx = canvas.getContext('2d'); 25 | 26 | ctx.fillStyle = "slateGray"; 27 | ctx.fillRect(28, 28, 200, 200)(); // function call is intentional! 28 | ``` 29 | 30 | ### Examples 31 | 32 | - **[draw ncc logo](https://github.com/indus/ncc/blob/master/examples/1_draw_ncc_logo.js)** 33 | >> **learn** how to setup ncc and draw shapes to canvas 34 | - **[early access](https://github.com/indus/ncc/blob/master/examples/2_early_access.js)** 35 | >> **learn** how to start using ncc even before it is fully set up 36 | - **[get return values](https://github.com/indus/ncc/blob/master/examples/3_get_return_values.js)** 37 | >> **learn** how to get return values of non-void functions 38 | - **[gardients/patterns](https://github.com/indus/ncc/blob/master/examples/4_gradients_and_patterns.js)** 39 | >> **learn** how to use gradients and patterns 40 | - **[images](https://github.com/indus/ncc/blob/master/examples/5_images.js)** 41 | >> **learn** how to apply images from urls or the filesystem 42 | - **[shadow canvas](https://github.com/indus/ncc/blob/master/examples/6_shadow_canvas.js)** 43 | >> **learn** how work with more than one canvas 44 | 45 | ### API 46 | 47 | **ncc** follows the native [Web API Interfaces](https://developer.mozilla.org/en-US/docs/Web/API)... 48 | [HTMLCanvasElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement), 49 | [HTMLImageElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement), 50 | [CanvasRenderingContext2D](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D), 51 | [CanvasGradient](https://developer.mozilla.org/en-US/docs/Web/API/CanvasGradient), 52 | [CanvasPattern](https://developer.mozilla.org/en-US/docs/Web/API/CanvasPattern) 53 | ... as close as possible. 54 | 55 | Differences are a result of the asynchronous nature of **ncc**. All object creations, method calls and property manipulations don't get processed directly, but get serialized and stored until a return value is necessary and a request is therefore unavoidable. 56 | Every 'Object' provided by **ncc** (and also every return value of a method) is actually a function to trigger a synchronization. You can pass a error-first-callback ( 'function(error, result){...}' ) to such a function to receive the return value of the last action (see [examples](https://github.com/indus/ncc#examples)). 57 |

58 | flowchart 59 |

60 | The **Canvas-** RenderingContext2D, -Gradient and -Pattern Proxys are fully implemented. 61 | The **HTML-** CanvasElement and -ImageElement Proxys only necessary properties and functions. For example they both implement a 'width' and 'height' attribute but don´t have further DOM functionality. 62 | 63 | Methods and properties beyond the native API are marked with a leading underscore and they are hidden from console by default (e.g. 'image._toFile(fileName, <callback>)' to write an image to the filesystem). 64 | 65 | #### proxy - creators 66 | 67 | * **ncc(** <options> **,** <callback> **)** >>> **[canvas]** 68 | **ncc(** <callback> **)** >>> **[canvas]** 69 | 70 | options (with defaults) 71 | ```javascript 72 | { logLevel: 'info', //['log','info','warn','error'] 73 | port: 9222, 74 | retry: 9, 75 | retryDelay: 500, 76 | headless: false 77 | } 78 | ``` 79 | 80 | * **ncc.createCanvas()** >>> **[canvas]** *if one is not enough* 81 | 82 | * **ncc.createImage(** <src> **,** <onloadFn> **,** <onerrorFn> **)** >>> **[image]** 83 | 84 | * **nccCanvas.getContext(** *[nativeAPI](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement#Methods)* **)** >>> **[context2d]** 85 | 86 | * **context2d.createLinearGradient(** *[nativeAPI](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D#createLinearGradient())* **)** >>> **[linearGradient]** 87 | **context2d.createRadialGradient(** *[nativeAPI](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D#createRadialGradient())* **)** >>> **[radialGradient]** 88 | **context2d.createPattern(** *[nativeAPI](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D#createPattern())* **)** >>> **[pattern]** 89 | 90 | -------------------------------------------------------------------------------- /examples/1_draw_ncc_logo.js: -------------------------------------------------------------------------------- 1 | // NCC Example 1 - draw ncc logo 2 | 3 | var ncc = require('../index.js'); // require('ncc'); 4 | 5 | console.time("ncc startup time"); 6 | 7 | // --- INFO --- 8 | // ncc uses error-first callbacks 9 | 10 | ncc({ logLevel: 'trace' }, function (err, canvas) { 11 | 12 | if (err) throw err; 13 | 14 | console.timeEnd("ncc startup time"); 15 | console.time("ncc draw time"); 16 | 17 | // --- INFO --- 18 | // all attributes are getters/setters and default to the initial values implemented in chrome 19 | 20 | canvas.width = 256; 21 | canvas.height = 256; 22 | 23 | var ctx = canvas.getContext("2d"); 24 | 25 | ctx.fillStyle = "#a3195b"; 26 | ctx.strokeStyle = "transparent"; 27 | ctx.beginPath(); 28 | ctx.moveTo(253, 186); 29 | ctx.lineTo(244.1, 186); 30 | ctx.bezierCurveTo(212.1, 186, 186.1, 160, 186.1, 128); 31 | ctx.bezierCurveTo(186.1, 96, 212.1, 70, 244.1, 70); 32 | ctx.lineTo(253, 70); 33 | ctx.lineTo(253, 105.7); 34 | ctx.lineTo(244.1, 105.7); 35 | ctx.bezierCurveTo(231.8, 105.7, 221.8, 115.7, 221.8, 128); 36 | ctx.bezierCurveTo(221.8, 140.3, 231.8, 150.3, 244.1, 150.3); 37 | ctx.lineTo(253, 150.3); 38 | ctx.lineTo(253, 186); 39 | ctx.closePath(); 40 | ctx.fill(); 41 | ctx.stroke(); 42 | ctx.restore(); 43 | ctx.restore(); 44 | ctx.save(); 45 | ctx.fillStyle = "#4a4a49"; 46 | ctx.beginPath(); 47 | ctx.moveTo(244.1, 38.7); 48 | ctx.lineTo(253, 38.7); 49 | ctx.lineTo(253, 3); 50 | ctx.lineTo(244.1, 3); 51 | ctx.bezierCurveTo(202.6, 3, 165.8, 23.3, 143, 54.5); 52 | ctx.bezierCurveTo(134.4, 24.8, 106.9, 3, 74.4, 3); 53 | ctx.bezierCurveTo(61.4, 3, 49.2, 6.5, 38.7, 12.6); 54 | ctx.lineTo(38.7, 3); 55 | ctx.lineTo(3, 3); 56 | ctx.lineTo(3, 235.1); 57 | ctx.lineTo(38.7, 235.1); 58 | ctx.lineTo(38.7, 74.4); 59 | ctx.bezierCurveTo(38.7, 54.7, 54.7, 38.7, 74.4, 38.7); 60 | ctx.bezierCurveTo(94.1, 38.7, 110.1, 54.7, 110.1, 74.4); 61 | ctx.lineTo(110.1, 253); 62 | ctx.lineTo(145.8, 253); 63 | ctx.lineTo(145.8, 205.2); 64 | ctx.bezierCurveTo(168.7, 234.3, 204.2, 253, 244, 253); 65 | ctx.lineTo(252.9, 253); 66 | ctx.lineTo(252.9, 217.3); 67 | ctx.lineTo(244, 217.3); 68 | ctx.bezierCurveTo(194.8, 217.3, 154.7, 177.2, 154.7, 128); 69 | ctx.bezierCurveTo(154.8, 78.8, 194.8, 38.7, 244.1, 38.7); 70 | ctx.closePath(); 71 | ctx.fill(); 72 | 73 | // --- INFO --- 74 | // nothing of the above actually happend. You used a ncc-proxy-object 75 | // every property assignment and function call was serialized into a remote-debugging command 76 | // to actually trigger the action and see a result you have to call a function: 77 | 78 | ctx(function (err, ctx) { 79 | if (err) throw err; 80 | 81 | console.timeEnd("ncc draw time"); 82 | console.log("Tataa!"); 83 | }) 84 | 85 | // --- ALTERNATIVES --- 86 | // this trigger-function is almost everywhere in ncc! 87 | // you can call it directly on the last ctx method, somewhere in between, 88 | // or on any other ncc-proy-object (e.g 'canvas') with an optinal callback: 89 | // 90 | // "ctx.fill()()" 91 | // "canvas()" 92 | }) 93 | -------------------------------------------------------------------------------- /examples/2_early_access.js: -------------------------------------------------------------------------------- 1 | // NCC Example 2 - early access 2 | 3 | var ncc = require('../index.js'); // require('ncc'); 4 | 5 | // --- INFO --- 6 | // 'ncc' returns the very same canvas two times; in the startup-callback and also directly 7 | // you can use canvas before the startup has finished, all callbacks will be invoked in order when 'ncc' is ready 8 | 9 | var canvas = ncc(function (err, canvas) { 10 | if (err) throw err; 11 | 12 | console.log("... in order of creation!"); 13 | }) 14 | 15 | // --- ALTERNATIVES --- 16 | // you can call 'ncc' with no callback at all: 17 | // 18 | // "var canvas = ncc()" 19 | // 20 | // ... but keep in mind that you will miss all eventual startup-errors 21 | 22 | canvas.width = 400; 23 | canvas.height = 100; 24 | 25 | var ctx = canvas.getContext("2d"); 26 | 27 | ctx.fillStyle = "slateGray"; 28 | ctx.font = "30px Arial"; 29 | ctx.textAlign = "center"; 30 | ctx.fillText("NCC Example 2 - early access", canvas.width / 2, 60, canvas.width-50); 31 | 32 | ctx(function (err, res) { 33 | if (err) throw err; 34 | 35 | console.log("all callbacks get invoked ..."); 36 | }) 37 | 38 | -------------------------------------------------------------------------------- /examples/3_get_return_values.js: -------------------------------------------------------------------------------- 1 | // NCC Example 3 - get return values 2 | 3 | var ncc = require('../index.js'); // require('ncc'); 4 | 5 | var canvas = ncc(function (err, canvas) { 6 | if (err) throw err; 7 | 8 | 9 | var ctx = canvas.getContext("2d"); 10 | ctx.font = "30px Arial"; 11 | var text = "look how exact this fits" 12 | 13 | ctx.measureText(text)(function (err, val) { 14 | if (err) throw err; 15 | 16 | // --- INFO --- 17 | // 'val' is whatever the function-call would have returned directly in the browser 18 | 19 | console.log(">>> textWidth: '" + val.width + "'"); 20 | 21 | canvas.width = val.width; 22 | canvas.height = 22; 23 | 24 | ctx.fillStyle = "slateGray"; 25 | ctx.fillRect(0, 0, val.width, 22); 26 | 27 | ctx.font = "30px Arial"; 28 | ctx.fillStyle = "white"; 29 | ctx.fillText(text, 0, 22); 30 | 31 | // --- INFO --- 32 | // the callback allways follows the function call: 33 | // 34 | // 'canvas.toDataURL()(callback)' not! 'canvas.toDataURL(callback)' 35 | 36 | canvas.toDataURL('image/jpeg', .5)(function (err, val) { 37 | if (err) throw err; 38 | 39 | console.log(">>> dataURL: '" + val.substring(0, 40) + "...' [length: " + val.length + "]"); 40 | }) 41 | }); 42 | }) 43 | -------------------------------------------------------------------------------- /examples/4_gradients_and_patterns.js: -------------------------------------------------------------------------------- 1 | // NCC Example 4 - gradients and patterns 2 | 3 | var ncc = require('../index.js'); // require('ncc'); 4 | 5 | var canvas = ncc({ logLevel: 'trace' }, function (err, canvas) { 6 | if (err) throw err; 7 | 8 | canvas.width = 256; 9 | canvas.height = 256; 10 | 11 | var ctx = canvas.getContext("2d"); 12 | 13 | // --- INFO --- 14 | // first we fill the canvas with a gray-white gradient from ul to lr 15 | 16 | var grd = ctx.createLinearGradient(0, 0, 256, 256); 17 | grd.addColorStop(0, "slateGray"); 18 | grd.addColorStop(1, "white"); 19 | 20 | ctx.fillStyle = grd; 21 | ctx.fillRect(0, 0, 256, 256) 22 | 23 | // --- INFO --- 24 | // now we reuse the filled canvas in a pattern and draw it back to canvas 25 | 26 | var pat = ctx.createPattern(canvas, "repeat"); 27 | ctx.rect(0, 0, 256, 256); 28 | ctx.fillStyle = pat; 29 | ctx.scale(.1, .1) 30 | 31 | ctx.fill()(function (err, res) { 32 | if (err) throw err; 33 | 34 | console.error("Tataa!"); 35 | }); 36 | 37 | // --- ALTERNATIVES --- 38 | // in example 3 you learned return values are accessible through callbacks 39 | // this is also true for gradients and patterns: 40 | // 41 | // "ctx.createLinearGradient(0, 0, width, height)(function(err,gra){...)" 42 | // 43 | // but you also have the 'early-access' option allready shown for the initial canvas 44 | // in example 2. This is holds for all ncc-proxys-ojects (e.g image, ctx, ...) 45 | }) 46 | -------------------------------------------------------------------------------- /examples/5_images.js: -------------------------------------------------------------------------------- 1 | // NCC Example 5 - images 2 | 3 | var ncc = require('../index.js'); // require('ncc'); 4 | var canvas = ncc(function (err, canvas) { 5 | if (err) throw err; 6 | 7 | var img = ncc.createImage(); 8 | 9 | img.onerror = function (err) { 10 | console.error("img Error:", err); 11 | } 12 | 13 | img.onload = function (img) { 14 | 15 | // --- INFO --- 16 | // after loaded the img has 'width' and 'height' attributes 17 | 18 | canvas.width = img.width+20; 19 | canvas.height = img.height+20; 20 | 21 | var ctx = canvas.getContext("2d"); 22 | ctx.drawImage(img, 10, 10)(function (err,res) { 23 | if (err) throw err; 24 | 25 | console.log("Hi! My name is Stefan, but you can call me 'indus'!"); 26 | }); 27 | } 28 | 29 | // --- INFO --- 30 | // setting 'src' triggers image loading: 31 | // 32 | // from the filesystem: 'img.src = "path/to/image.png"' 33 | // from a URL: 'img.src = "http://www.yourSite.com/image.png"' ('https://...' and 'ftp://..' is not supported) 34 | // from a dataURL: 'img.src = "data:image/png;base64, ..."' 35 | 36 | img.src = __dirname + "/dummy.jpg" 37 | 38 | 39 | // --- ALTERNATIVES --- 40 | // 'createImage' allows to pass all necessary arguments directly: 41 | // 42 | // 'ncc.createImage(,,)' 43 | 44 | 45 | // --- INFO --- 46 | // an image-proxy-object has a hidden property to access its data as 'base64' encoded dataURL 47 | // 48 | // 'var dataURL = img._base64' 49 | // 50 | // and it also has a hidden function to write it directly to the filesystem 51 | // 52 | // 'img._toFile('path/to/newImg.png',)' 53 | 54 | }) 55 | -------------------------------------------------------------------------------- /examples/6_shadow_canvas.js: -------------------------------------------------------------------------------- 1 | // NCC Example 6 - shadow-canvas 2 | 3 | var ncc = require('../index.js'); // require('ncc'); 4 | 5 | // --- INFO --- 6 | // first we create a shadow-canvas and fill it with a simple stroke pattern 7 | 8 | var shadow_canvas = ncc.createCanvas() 9 | 10 | shadow_canvas.width = 150; 11 | shadow_canvas.height = 150; 12 | 13 | var ctx_shadow = shadow_canvas.getContext("2d"); 14 | 15 | ctx_shadow.strokeStyle = "slateGrey"; 16 | 17 | for (var i = 20; i < 150; i += 10) { 18 | ctx_shadow.lineWidth = i / 50; 19 | ctx_shadow.strokeRect((150 - i) / 2, (150 - i) / 2, i, i); 20 | console.log((150 - i) / 2, (150 - i) / 2, i, i); 21 | } 22 | 23 | 24 | ncc(function (err, canvas_main) { 25 | if (err) throw err; 26 | 27 | // --- INFO --- 28 | // now after startup finished we use the shadow-canvas to draw it on the main-canvas two times 29 | 30 | canvas_main.width = 256; 31 | canvas_main.height = 256; 32 | 33 | var ctx_main = canvas_main.getContext("2d"); 34 | 35 | ctx_main.save() 36 | ctx_main.translate(128, 128); 37 | ctx_main.rotate(Math.PI / 180 * 45); 38 | ctx_main.translate(-75, -75); 39 | 40 | ctx_main.drawImage(shadow_canvas, 0, 0) 41 | ctx_main.restore() 42 | 43 | ctx_main.translate(128, 128); 44 | ctx_main.rotate(Math.PI / 180 * 90); 45 | ctx_main.translate(-75, -75); 46 | 47 | ctx_main.drawImage(shadow_canvas, 0, 0); 48 | 49 | // --- INFO --- 50 | // to give garbage collection a chance you should nullify all proxy-objects (image, canvas, etc.) that are no longer in use 51 | // every proxy-object has a hidden attribute '_remote' that has to be set to 'null' explicitly: 52 | 53 | shadow_canvas = shadow_canvas._remote = null; 54 | 55 | ctx_main(function (err, res) { 56 | if (err) throw err; 57 | console.log("Tataa!"); 58 | }) 59 | 60 | // --- INFO --- 61 | // there is no difference between a shadow-canvas and the main-canvas, besides that they are not showing up in the window 62 | }) 63 | 64 | -------------------------------------------------------------------------------- /examples/dummy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/indus/ncc/6fc9b86421ae4b3132b0b7df406c756bf4e6f890/examples/dummy.jpg -------------------------------------------------------------------------------- /footage/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/indus/ncc/6fc9b86421ae4b3132b0b7df406c756bf4e6f890/footage/flow.png -------------------------------------------------------------------------------- /footage/flow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 14 | 18 | 19 | 20 | 21 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 37 | 38 | 39 | 40 | 41 | 42 | 44 | 45 | 46 | 47 | 48 | 49 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 77 | 78 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /footage/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/indus/ncc/6fc9b86421ae4b3132b0b7df406c756bf4e6f890/footage/logo.png -------------------------------------------------------------------------------- /footage/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 10 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ncc v0.2.x 3 | * 4 | * Copyright 2014 Stefan Keim (indus) 5 | * Released under the MIT license 6 | * https://github.com/indus/ncc/blob/master/license.md 7 | * 8 | * Date: 2014-05-13 9 | */ 10 | 11 | module.exports = require('./lib/ncc'); -------------------------------------------------------------------------------- /lib/chrome-launcher.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a stripped down and bundeled version of 3 | * - https://github.com/GoogleChrome/lighthouse/blob/master/lighthouse-cli/chrome-launcher.ts 4 | * - https://github.com/GoogleChrome/lighthouse/blob/master/lighthouse-cli/chrome-finder.ts 5 | * It be replaced when the ChromeLauncher becomes a module: https://github.com/GoogleChrome/lighthouse/issues/2092 6 | * But for now this saves us about 60 MB of modules 7 | */ 8 | 9 | /** 10 | * @license 11 | * Copyright 2016 Google Inc. All rights reserved. 12 | * 13 | * Licensed under the Apache License, Version 2.0 (the "License"); 14 | * you may not use this file except in compliance with the License. 15 | * You may obtain a copy of the License at 16 | * 17 | * http://www.apache.org/licenses/LICENSE-2.0 18 | * 19 | * Unless required by applicable law or agreed to in writing, software 20 | * distributed under the License is distributed on an "AS IS" BASIS, 21 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 22 | * See the License for the specific language governing permissions and 23 | * limitations under the License. 24 | */ 25 | 26 | 'use strict'; 27 | 28 | var ChromeLauncher = (() => { 29 | var childProcess = require('child_process'); 30 | var fs = require('fs'); 31 | var path = require('path'); 32 | 33 | const mkdirp = require('mkdirp'); 34 | var net = require('net'); 35 | const rimraf = require('rimraf'); 36 | const spawn = childProcess.spawn; 37 | const execSync = childProcess.execSync; 38 | const isWindows = process.platform === 'win32'; 39 | const execFileSync = require('child_process').execFileSync; 40 | 41 | const newLineRegex = /\r?\n/; 42 | 43 | type Priorities = Array<{ regex: RegExp, weight: number }>; 44 | 45 | function darwin() { 46 | const suffixes = ['/Contents/MacOS/Google Chrome Canary', '/Contents/MacOS/Google Chrome']; 47 | 48 | const LSREGISTER = '/System/Library/Frameworks/CoreServices.framework' + 49 | '/Versions/A/Frameworks/LaunchServices.framework' + 50 | '/Versions/A/Support/lsregister'; 51 | 52 | const installations: Array = []; 53 | 54 | execSync( 55 | `${LSREGISTER} -dump` + 56 | ' | grep -i \'google chrome\\( canary\\)\\?.app$\'' + 57 | ' | awk \'{$1=""; print $0}\'') 58 | .toString() 59 | .split(newLineRegex) 60 | .forEach((inst: string) => { 61 | suffixes.forEach(suffix => { 62 | const execPath = path.join(inst.trim(), suffix); 63 | if (canAccess(execPath)) { 64 | installations.push(execPath); 65 | } 66 | }); 67 | }); 68 | 69 | // Retains one per line to maintain readability. 70 | // clang-format off 71 | const priorities: Priorities = [ 72 | { regex: new RegExp(`^${process.env.HOME}/Applications/.*Chrome.app`), weight: 50 }, 73 | { regex: new RegExp(`^${process.env.HOME}/Applications/.*Chrome Canary.app`), weight: 51 }, 74 | { regex: /^\/Applications\/.*Chrome.app/, weight: 100 }, 75 | { regex: /^\/Applications\/.*Chrome Canary.app/, weight: 101 }, 76 | { regex: /^\/Volumes\/.*Chrome.app/, weight: -2 }, 77 | { regex: /^\/Volumes\/.*Chrome Canary.app/, weight: -1 } 78 | ]; 79 | // clang-format on 80 | 81 | return sort(installations, priorities); 82 | } 83 | 84 | /** 85 | * Look for linux executables in 3 ways 86 | * 1. Look into LIGHTHOUSE_CHROMIUM_PATH env variable 87 | * 2. Look into the directories where .desktop are saved on gnome based distro's 88 | * 3. Look for google-chrome-stable & google-chrome executables by using the which command 89 | */ 90 | function linux() { 91 | let installations: Array = []; 92 | 93 | // 1. Look into LIGHTHOUSE_CHROMIUM_PATH env variable 94 | if (canAccess(process.env.LIGHTHOUSE_CHROMIUM_PATH)) { 95 | installations.push(process.env.LIGHTHOUSE_CHROMIUM_PATH); 96 | } 97 | 98 | // 2. Look into the directories where .desktop are saved on gnome based distro's 99 | const desktopInstallationFolders = [ 100 | path.join(require('os').homedir(), '.local/share/applications/'), 101 | '/usr/share/applications/', 102 | ]; 103 | desktopInstallationFolders.forEach(folder => { 104 | installations = installations.concat(findChromeExecutables(folder)); 105 | }); 106 | 107 | // Look for google-chrome-stable & google-chrome executables by using the which command 108 | const executables = [ 109 | 'google-chrome-stable', 110 | 'google-chrome', 111 | ]; 112 | executables.forEach((executable: string) => { 113 | try { 114 | const chromePath = execFileSync('which', [executable]).toString().split(newLineRegex)[0]; 115 | 116 | if (canAccess(chromePath)) { 117 | installations.push(chromePath); 118 | } 119 | } catch (e) { 120 | // Not installed. 121 | } 122 | }); 123 | 124 | if (!installations.length) { 125 | throw new Error( 126 | 'The environment variable LIGHTHOUSE_CHROMIUM_PATH must be set to ' + 127 | 'executable of a build of Chromium version 54.0 or later.'); 128 | } 129 | 130 | const priorities: Priorities = [ 131 | { regex: /chrome-wrapper$/, weight: 51 }, { regex: /google-chrome-stable$/, weight: 50 }, 132 | { regex: /google-chrome$/, weight: 49 }, 133 | { regex: new RegExp(process.env.LIGHTHOUSE_CHROMIUM_PATH), weight: 100 } 134 | ]; 135 | 136 | return sort(uniq(installations.filter(Boolean)), priorities); 137 | } 138 | 139 | function win32() { 140 | const installations: Array = []; 141 | const suffixes = [ 142 | '\\Google\\Chrome SxS\\Application\\chrome.exe', '\\Google\\Chrome\\Application\\chrome.exe' 143 | ]; 144 | const prefixes = 145 | [process.env.LOCALAPPDATA, process.env.PROGRAMFILES, process.env['PROGRAMFILES(X86)']]; 146 | 147 | if (canAccess(process.env.LIGHTHOUSE_CHROMIUM_PATH)) { 148 | installations.push(process.env.LIGHTHOUSE_CHROMIUM_PATH); 149 | } 150 | 151 | prefixes.forEach(prefix => suffixes.forEach(suffix => { 152 | const chromePath = path.join(prefix, suffix); 153 | if (canAccess(chromePath)) { 154 | installations.push(chromePath); 155 | } 156 | })); 157 | return installations; 158 | } 159 | 160 | function sort(installations: Array, priorities: Priorities) { 161 | const defaultPriority = 10; 162 | return installations 163 | // assign priorities 164 | .map((inst: string) => { 165 | for (const pair of priorities) { 166 | if (pair.regex.test(inst)) { 167 | return [inst, pair.weight]; 168 | } 169 | } 170 | return [inst, defaultPriority]; 171 | }) 172 | // sort based on priorities 173 | .sort((a, b) => (b)[1] - (a)[1]) 174 | // remove priority flag 175 | .map(pair => pair[0]); 176 | } 177 | 178 | function canAccess(file: string): Boolean { 179 | if (!file) { 180 | return false; 181 | } 182 | 183 | try { 184 | fs.accessSync(file); 185 | return true; 186 | } catch (e) { 187 | return false; 188 | } 189 | } 190 | 191 | function uniq(arr: Array) { 192 | return Array.from(new Set(arr)); 193 | } 194 | 195 | function findChromeExecutables(folder: string): Array { 196 | const argumentsRegex = /(^[^ ]+).*/; // Take everything up to the first space 197 | const chromeExecRegex = '^Exec=\/.*\/(google|chrome|chromium)-.*'; 198 | 199 | let installations: Array = []; 200 | if (canAccess(folder)) { 201 | // Output of the grep & print looks like: 202 | // /opt/google/chrome/google-chrome --profile-directory 203 | // /home/user/Downloads/chrome-linux/chrome-wrapper %U 204 | let execPaths = execSync(`grep -ER "${chromeExecRegex}" ${folder} | awk -F '=' '{print $2}'`) 205 | .toString() 206 | .split(newLineRegex) 207 | .map((execPath: string) => execPath.replace(argumentsRegex, '$1')); 208 | 209 | execPaths.forEach((execPath: string) => canAccess(execPath) && installations.push(execPath)); 210 | } 211 | 212 | return installations; 213 | } 214 | 215 | var chromeFinder = { 216 | darwin: darwin, 217 | linux: linux, 218 | win32: win32 219 | } 220 | 221 | class ChromeLauncher { 222 | prepared = false; 223 | pollInterval: number = 500; 224 | autoSelectChrome: boolean; 225 | TMP_PROFILE_DIR: string; 226 | outFile?: number; 227 | errFile?: number; 228 | pidFile: string; 229 | startingUrl: string; 230 | chromeFlags: Array; 231 | chrome?: any; 232 | port: number; 233 | 234 | constructor(opts: { 235 | startingUrl?: string, 236 | chromeFlags?: Array, 237 | autoSelectChrome?: boolean, 238 | port?: number 239 | } = {}) { 240 | // choose the first one (default) 241 | this.autoSelectChrome = defaults(opts.autoSelectChrome, true); 242 | this.startingUrl = defaults(opts.startingUrl, 'about:blank'); 243 | this.chromeFlags = defaults(opts.chromeFlags, []); 244 | this.port = defaults(opts.port, 9222); 245 | } 246 | 247 | flags() { 248 | const flags = [ 249 | `--remote-debugging-port=${this.port}`, 250 | // Disable built-in Google Translate service 251 | '--disable-translate', 252 | // Disable all chrome extensions entirely 253 | '--disable-extensions', 254 | // Disable various background network services, including extension updating, 255 | // safe browsing service, upgrade detector, translate, UMA 256 | '--disable-background-networking', 257 | // Disable fetching safebrowsing lists, likely redundant due to disable-background-networking 258 | '--safebrowsing-disable-auto-update', 259 | // Disable syncing to a Google account 260 | '--disable-sync', 261 | // Disable reporting to UMA, but allows for collection 262 | '--metrics-recording-only', 263 | // Disable installation of default apps on first run 264 | '--disable-default-apps', 265 | // Skip first run wizards 266 | '--no-first-run', 267 | // Place Chrome profile in a custom location we'll rm -rf later 268 | `--user-data-dir=${this.TMP_PROFILE_DIR}` 269 | ]; 270 | 271 | if (process.platform === 'linux') { 272 | flags.push('--disable-setuid-sandbox'); 273 | } 274 | 275 | flags.push(...this.chromeFlags); 276 | flags.push(this.startingUrl); 277 | 278 | return flags; 279 | } 280 | 281 | prepare() { 282 | switch (process.platform) { 283 | case 'darwin': 284 | case 'linux': 285 | this.TMP_PROFILE_DIR = unixTmpDir(); 286 | break; 287 | 288 | case 'win32': 289 | this.TMP_PROFILE_DIR = win32TmpDir(); 290 | break; 291 | 292 | default: 293 | throw new Error('Platform ' + process.platform + ' is not supported'); 294 | } 295 | 296 | this.outFile = fs.openSync(`${this.TMP_PROFILE_DIR}/chrome-out.log`, 'a'); 297 | this.errFile = fs.openSync(`${this.TMP_PROFILE_DIR}/chrome-err.log`, 'a'); 298 | 299 | // fix for Node4 300 | // you can't pass a fd to fs.writeFileSync 301 | this.pidFile = `${this.TMP_PROFILE_DIR}/chrome.pid`; 302 | 303 | console.log('ChromeLauncher', `created ${this.TMP_PROFILE_DIR}`); 304 | 305 | this.prepared = true; 306 | } 307 | 308 | run() { 309 | if (!this.prepared) { 310 | this.prepare(); 311 | } 312 | 313 | return Promise.resolve() 314 | .then(() => { 315 | const installations = (chromeFinder)[process.platform](); 316 | 317 | if (installations.length < 1) { 318 | return Promise.reject(new Error('No Chrome Installations Found')); 319 | } else if (installations.length === 1 || this.autoSelectChrome) { 320 | return installations[0]; 321 | } 322 | 323 | //return ask('Choose a Chrome installation to use with Lighthouse', installations); 324 | }) 325 | .then(execPath => this.spawn(execPath)); 326 | } 327 | 328 | spawn(execPath: string) { 329 | const spawnPromise = new Promise(resolve => { 330 | if (this.chrome) { 331 | console.log('ChromeLauncher', `Chrome already running with pid ${this.chrome.pid}.`); 332 | return resolve(this.chrome.pid); 333 | } 334 | 335 | const chrome = spawn( 336 | execPath, this.flags(), { detached: true, stdio: ['ignore', this.outFile, this.errFile] }); 337 | this.chrome = chrome; 338 | 339 | fs.writeFileSync(this.pidFile, chrome.pid.toString()); 340 | 341 | console.log('ChromeLauncher', `Chrome running with pid ${chrome.pid} on port ${this.port}.`); 342 | resolve(chrome.pid); 343 | }); 344 | 345 | return spawnPromise.then(pid => Promise.all([pid, this.waitUntilReady()])); 346 | } 347 | 348 | cleanup(client?: any) { 349 | if (client) { 350 | client.removeAllListeners(); 351 | client.end(); 352 | client.destroy(); 353 | client.unref(); 354 | } 355 | } 356 | 357 | // resolves if ready, rejects otherwise 358 | isDebuggerReady(): Promise<{}> { 359 | return new Promise((resolve, reject) => { 360 | const client = net.createConnection(this.port); 361 | client.once('error', err => { 362 | this.cleanup(client); 363 | reject(err); 364 | }); 365 | client.once('connect', () => { 366 | this.cleanup(client); 367 | resolve(); 368 | }); 369 | }); 370 | } 371 | 372 | // resolves when debugger is ready, rejects after 10 polls 373 | waitUntilReady() { 374 | const launcher = this; 375 | 376 | return new Promise((resolve, reject) => { 377 | let retries = 0; 378 | let waitStatus = 'Waiting for browser.'; 379 | (function poll() { 380 | if (retries === 0) { 381 | console.log('ChromeLauncher', waitStatus); 382 | } 383 | retries++; 384 | waitStatus += '..'; 385 | console.log('ChromeLauncher', waitStatus); 386 | 387 | launcher.isDebuggerReady() 388 | .then(() => { 389 | console.log('ChromeLauncher', waitStatus); 390 | resolve(); 391 | }) 392 | .catch(err => { 393 | if (retries > 10) { 394 | return reject(err); 395 | } 396 | delay(launcher.pollInterval).then(poll); 397 | }); 398 | })(); 399 | }); 400 | } 401 | 402 | kill() { 403 | return new Promise(resolve => { 404 | if (this.chrome) { 405 | this.chrome.on('close', () => { 406 | this.destroyTmp().then(resolve); 407 | }); 408 | 409 | console.log('ChromeLauncher', 'Killing all Chrome Instances'); 410 | try { 411 | if (isWindows) { 412 | execSync(`taskkill /pid ${this.chrome.pid} /T /F`); 413 | } else { 414 | process.kill(-this.chrome.pid); 415 | } 416 | } catch (err) { 417 | console.log('ChromeLauncher', `Chrome could not be killed ${err.message}`); 418 | } 419 | 420 | delete this.chrome; 421 | } else { 422 | // fail silently as we did not start chrome 423 | resolve(); 424 | } 425 | }); 426 | } 427 | 428 | destroyTmp() { 429 | return new Promise(resolve => { 430 | if (!this.TMP_PROFILE_DIR) { 431 | return resolve(); 432 | } 433 | 434 | console.log('ChromeLauncher', `Removing ${this.TMP_PROFILE_DIR}`); 435 | 436 | if (this.outFile) { 437 | fs.closeSync(this.outFile); 438 | delete this.outFile; 439 | } 440 | 441 | if (this.errFile) { 442 | fs.closeSync(this.errFile); 443 | delete this.errFile; 444 | } 445 | 446 | rimraf(this.TMP_PROFILE_DIR, () => resolve()); 447 | }); 448 | } 449 | }; 450 | 451 | function defaults(val: T | undefined, def: T): T { 452 | return typeof val === 'undefined' ? def : val; 453 | } 454 | 455 | function delay(time: number) { 456 | return new Promise(resolve => setTimeout(resolve, time)); 457 | } 458 | 459 | function unixTmpDir() { 460 | return execSync('mktemp -d -t ncc.XXXXXXX').toString().trim(); 461 | } 462 | 463 | function win32TmpDir() { 464 | const winTmpPath = process.env.TEMP || process.env.TMP || 465 | (process.env.SystemRoot || process.env.windir) + '\\temp'; 466 | const randomNumber = Math.floor(Math.random() * 9e7 + 1e7); 467 | const tmpdir = path.join(winTmpPath, 'ncc.' + randomNumber); 468 | 469 | mkdirp.sync(tmpdir); 470 | return tmpdir; 471 | } 472 | return ChromeLauncher; 473 | })() 474 | 475 | -------------------------------------------------------------------------------- /lib/ncc.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | ncc 6 | 7 | 8 | 31 | 32 | 33 | 34 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /lib/ncc.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/indus/ncc/6fc9b86421ae4b3132b0b7df406c756bf4e6f890/lib/ncc.ico -------------------------------------------------------------------------------- /lib/ncc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a stripped down and bundeled version of 3 | * - https://github.com/GoogleChrome/lighthouse/blob/master/lighthouse-cli/chrome-launcher.ts 4 | * - https://github.com/GoogleChrome/lighthouse/blob/master/lighthouse-cli/chrome-finder.ts 5 | * It be replaced when the ChromeLauncher becomes a module: https://github.com/GoogleChrome/lighthouse/issues/2092 6 | * But for now this saves us about 60 MB of modules 7 | */ 8 | /** 9 | * @license 10 | * Copyright 2016 Google Inc. All rights reserved. 11 | * 12 | * Licensed under the Apache License, Version 2.0 (the "License"); 13 | * you may not use this file except in compliance with the License. 14 | * You may obtain a copy of the License at 15 | * 16 | * http://www.apache.org/licenses/LICENSE-2.0 17 | * 18 | * Unless required by applicable law or agreed to in writing, software 19 | * distributed under the License is distributed on an "AS IS" BASIS, 20 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | * See the License for the specific language governing permissions and 22 | * limitations under the License. 23 | */ 24 | 'use strict'; 25 | var ChromeLauncher = (() => { 26 | var childProcess = require('child_process'); 27 | var fs = require('fs'); 28 | var path = require('path'); 29 | const mkdirp = require('mkdirp'); 30 | var net = require('net'); 31 | const rimraf = require('rimraf'); 32 | const spawn = childProcess.spawn; 33 | const execSync = childProcess.execSync; 34 | const isWindows = process.platform === 'win32'; 35 | const execFileSync = require('child_process').execFileSync; 36 | const newLineRegex = /\r?\n/; 37 | function darwin() { 38 | const suffixes = ['/Contents/MacOS/Google Chrome Canary', '/Contents/MacOS/Google Chrome']; 39 | const LSREGISTER = '/System/Library/Frameworks/CoreServices.framework' + 40 | '/Versions/A/Frameworks/LaunchServices.framework' + 41 | '/Versions/A/Support/lsregister'; 42 | const installations = []; 43 | execSync(`${LSREGISTER} -dump` + 44 | ' | grep -i \'google chrome\\( canary\\)\\?.app$\'' + 45 | ' | awk \'{$1=""; print $0}\'') 46 | .toString() 47 | .split(newLineRegex) 48 | .forEach((inst) => { 49 | suffixes.forEach(suffix => { 50 | const execPath = path.join(inst.trim(), suffix); 51 | if (canAccess(execPath)) { 52 | installations.push(execPath); 53 | } 54 | }); 55 | }); 56 | // Retains one per line to maintain readability. 57 | // clang-format off 58 | const priorities = [ 59 | { regex: new RegExp(`^${process.env.HOME}/Applications/.*Chrome.app`), weight: 50 }, 60 | { regex: new RegExp(`^${process.env.HOME}/Applications/.*Chrome Canary.app`), weight: 51 }, 61 | { regex: /^\/Applications\/.*Chrome.app/, weight: 100 }, 62 | { regex: /^\/Applications\/.*Chrome Canary.app/, weight: 101 }, 63 | { regex: /^\/Volumes\/.*Chrome.app/, weight: -2 }, 64 | { regex: /^\/Volumes\/.*Chrome Canary.app/, weight: -1 } 65 | ]; 66 | // clang-format on 67 | return sort(installations, priorities); 68 | } 69 | /** 70 | * Look for linux executables in 3 ways 71 | * 1. Look into LIGHTHOUSE_CHROMIUM_PATH env variable 72 | * 2. Look into the directories where .desktop are saved on gnome based distro's 73 | * 3. Look for google-chrome-stable & google-chrome executables by using the which command 74 | */ 75 | function linux() { 76 | let installations = []; 77 | // 1. Look into LIGHTHOUSE_CHROMIUM_PATH env variable 78 | if (canAccess(process.env.LIGHTHOUSE_CHROMIUM_PATH)) { 79 | installations.push(process.env.LIGHTHOUSE_CHROMIUM_PATH); 80 | } 81 | // 2. Look into the directories where .desktop are saved on gnome based distro's 82 | const desktopInstallationFolders = [ 83 | path.join(require('os').homedir(), '.local/share/applications/'), 84 | '/usr/share/applications/', 85 | ]; 86 | desktopInstallationFolders.forEach(folder => { 87 | installations = installations.concat(findChromeExecutables(folder)); 88 | }); 89 | // Look for google-chrome-stable & google-chrome executables by using the which command 90 | const executables = [ 91 | 'google-chrome-stable', 92 | 'google-chrome', 93 | ]; 94 | executables.forEach((executable) => { 95 | try { 96 | const chromePath = execFileSync('which', [executable]).toString().split(newLineRegex)[0]; 97 | if (canAccess(chromePath)) { 98 | installations.push(chromePath); 99 | } 100 | } 101 | catch (e) { 102 | // Not installed. 103 | } 104 | }); 105 | if (!installations.length) { 106 | throw new Error('The environment variable LIGHTHOUSE_CHROMIUM_PATH must be set to ' + 107 | 'executable of a build of Chromium version 54.0 or later.'); 108 | } 109 | const priorities = [ 110 | { regex: /chrome-wrapper$/, weight: 51 }, { regex: /google-chrome-stable$/, weight: 50 }, 111 | { regex: /google-chrome$/, weight: 49 }, 112 | { regex: new RegExp(process.env.LIGHTHOUSE_CHROMIUM_PATH), weight: 100 } 113 | ]; 114 | return sort(uniq(installations.filter(Boolean)), priorities); 115 | } 116 | function win32() { 117 | const installations = []; 118 | const suffixes = [ 119 | '\\Google\\Chrome SxS\\Application\\chrome.exe', '\\Google\\Chrome\\Application\\chrome.exe' 120 | ]; 121 | const prefixes = [process.env.LOCALAPPDATA, process.env.PROGRAMFILES, process.env['PROGRAMFILES(X86)']]; 122 | if (canAccess(process.env.LIGHTHOUSE_CHROMIUM_PATH)) { 123 | installations.push(process.env.LIGHTHOUSE_CHROMIUM_PATH); 124 | } 125 | prefixes.forEach(prefix => suffixes.forEach(suffix => { 126 | const chromePath = path.join(prefix, suffix); 127 | if (canAccess(chromePath)) { 128 | installations.push(chromePath); 129 | } 130 | })); 131 | return installations; 132 | } 133 | function sort(installations, priorities) { 134 | const defaultPriority = 10; 135 | return installations 136 | .map((inst) => { 137 | for (const pair of priorities) { 138 | if (pair.regex.test(inst)) { 139 | return [inst, pair.weight]; 140 | } 141 | } 142 | return [inst, defaultPriority]; 143 | }) 144 | .sort((a, b) => b[1] - a[1]) 145 | .map(pair => pair[0]); 146 | } 147 | function canAccess(file) { 148 | if (!file) { 149 | return false; 150 | } 151 | try { 152 | fs.accessSync(file); 153 | return true; 154 | } 155 | catch (e) { 156 | return false; 157 | } 158 | } 159 | function uniq(arr) { 160 | return Array.from(new Set(arr)); 161 | } 162 | function findChromeExecutables(folder) { 163 | const argumentsRegex = /(^[^ ]+).*/; // Take everything up to the first space 164 | const chromeExecRegex = '^Exec=\/.*\/(google|chrome|chromium)-.*'; 165 | let installations = []; 166 | if (canAccess(folder)) { 167 | // Output of the grep & print looks like: 168 | // /opt/google/chrome/google-chrome --profile-directory 169 | // /home/user/Downloads/chrome-linux/chrome-wrapper %U 170 | let execPaths = execSync(`grep -ER "${chromeExecRegex}" ${folder} | awk -F '=' '{print $2}'`) 171 | .toString() 172 | .split(newLineRegex) 173 | .map((execPath) => execPath.replace(argumentsRegex, '$1')); 174 | execPaths.forEach((execPath) => canAccess(execPath) && installations.push(execPath)); 175 | } 176 | return installations; 177 | } 178 | var chromeFinder = { 179 | darwin: darwin, 180 | linux: linux, 181 | win32: win32 182 | }; 183 | class ChromeLauncher { 184 | constructor(opts = {}) { 185 | this.prepared = false; 186 | this.pollInterval = 500; 187 | // choose the first one (default) 188 | this.autoSelectChrome = defaults(opts.autoSelectChrome, true); 189 | this.startingUrl = defaults(opts.startingUrl, 'about:blank'); 190 | this.chromeFlags = defaults(opts.chromeFlags, []); 191 | this.port = defaults(opts.port, 9222); 192 | } 193 | flags() { 194 | const flags = [ 195 | `--remote-debugging-port=${this.port}`, 196 | // Disable built-in Google Translate service 197 | '--disable-translate', 198 | // Disable all chrome extensions entirely 199 | '--disable-extensions', 200 | // Disable various background network services, including extension updating, 201 | // safe browsing service, upgrade detector, translate, UMA 202 | '--disable-background-networking', 203 | // Disable fetching safebrowsing lists, likely redundant due to disable-background-networking 204 | '--safebrowsing-disable-auto-update', 205 | // Disable syncing to a Google account 206 | '--disable-sync', 207 | // Disable reporting to UMA, but allows for collection 208 | '--metrics-recording-only', 209 | // Disable installation of default apps on first run 210 | '--disable-default-apps', 211 | // Skip first run wizards 212 | '--no-first-run', 213 | // Place Chrome profile in a custom location we'll rm -rf later 214 | `--user-data-dir=${this.TMP_PROFILE_DIR}` 215 | ]; 216 | if (process.platform === 'linux') { 217 | flags.push('--disable-setuid-sandbox'); 218 | } 219 | flags.push(...this.chromeFlags); 220 | flags.push(this.startingUrl); 221 | return flags; 222 | } 223 | prepare() { 224 | switch (process.platform) { 225 | case 'darwin': 226 | case 'linux': 227 | this.TMP_PROFILE_DIR = unixTmpDir(); 228 | break; 229 | case 'win32': 230 | this.TMP_PROFILE_DIR = win32TmpDir(); 231 | break; 232 | default: 233 | throw new Error('Platform ' + process.platform + ' is not supported'); 234 | } 235 | this.outFile = fs.openSync(`${this.TMP_PROFILE_DIR}/chrome-out.log`, 'a'); 236 | this.errFile = fs.openSync(`${this.TMP_PROFILE_DIR}/chrome-err.log`, 'a'); 237 | // fix for Node4 238 | // you can't pass a fd to fs.writeFileSync 239 | this.pidFile = `${this.TMP_PROFILE_DIR}/chrome.pid`; 240 | console.log('ChromeLauncher', `created ${this.TMP_PROFILE_DIR}`); 241 | this.prepared = true; 242 | } 243 | run() { 244 | if (!this.prepared) { 245 | this.prepare(); 246 | } 247 | return Promise.resolve() 248 | .then(() => { 249 | const installations = chromeFinder[process.platform](); 250 | if (installations.length < 1) { 251 | return Promise.reject(new Error('No Chrome Installations Found')); 252 | } 253 | else if (installations.length === 1 || this.autoSelectChrome) { 254 | return installations[0]; 255 | } 256 | //return ask('Choose a Chrome installation to use with Lighthouse', installations); 257 | }) 258 | .then(execPath => this.spawn(execPath)); 259 | } 260 | spawn(execPath) { 261 | const spawnPromise = new Promise(resolve => { 262 | if (this.chrome) { 263 | console.log('ChromeLauncher', `Chrome already running with pid ${this.chrome.pid}.`); 264 | return resolve(this.chrome.pid); 265 | } 266 | const chrome = spawn(execPath, this.flags(), { detached: true, stdio: ['ignore', this.outFile, this.errFile] }); 267 | this.chrome = chrome; 268 | fs.writeFileSync(this.pidFile, chrome.pid.toString()); 269 | console.log('ChromeLauncher', `Chrome running with pid ${chrome.pid} on port ${this.port}.`); 270 | resolve(chrome.pid); 271 | }); 272 | return spawnPromise.then(pid => Promise.all([pid, this.waitUntilReady()])); 273 | } 274 | cleanup(client) { 275 | if (client) { 276 | client.removeAllListeners(); 277 | client.end(); 278 | client.destroy(); 279 | client.unref(); 280 | } 281 | } 282 | // resolves if ready, rejects otherwise 283 | isDebuggerReady() { 284 | return new Promise((resolve, reject) => { 285 | const client = net.createConnection(this.port); 286 | client.once('error', err => { 287 | this.cleanup(client); 288 | reject(err); 289 | }); 290 | client.once('connect', () => { 291 | this.cleanup(client); 292 | resolve(); 293 | }); 294 | }); 295 | } 296 | // resolves when debugger is ready, rejects after 10 polls 297 | waitUntilReady() { 298 | const launcher = this; 299 | return new Promise((resolve, reject) => { 300 | let retries = 0; 301 | let waitStatus = 'Waiting for browser.'; 302 | (function poll() { 303 | if (retries === 0) { 304 | console.log('ChromeLauncher', waitStatus); 305 | } 306 | retries++; 307 | waitStatus += '..'; 308 | console.log('ChromeLauncher', waitStatus); 309 | launcher.isDebuggerReady() 310 | .then(() => { 311 | console.log('ChromeLauncher', waitStatus); 312 | resolve(); 313 | }) 314 | .catch(err => { 315 | if (retries > 10) { 316 | return reject(err); 317 | } 318 | delay(launcher.pollInterval).then(poll); 319 | }); 320 | })(); 321 | }); 322 | } 323 | kill() { 324 | return new Promise(resolve => { 325 | if (this.chrome) { 326 | this.chrome.on('close', () => { 327 | this.destroyTmp().then(resolve); 328 | }); 329 | console.log('ChromeLauncher', 'Killing all Chrome Instances'); 330 | try { 331 | if (isWindows) { 332 | execSync(`taskkill /pid ${this.chrome.pid} /T /F`); 333 | } 334 | else { 335 | process.kill(-this.chrome.pid); 336 | } 337 | } 338 | catch (err) { 339 | console.log('ChromeLauncher', `Chrome could not be killed ${err.message}`); 340 | } 341 | delete this.chrome; 342 | } 343 | else { 344 | // fail silently as we did not start chrome 345 | resolve(); 346 | } 347 | }); 348 | } 349 | destroyTmp() { 350 | return new Promise(resolve => { 351 | if (!this.TMP_PROFILE_DIR) { 352 | return resolve(); 353 | } 354 | console.log('ChromeLauncher', `Removing ${this.TMP_PROFILE_DIR}`); 355 | if (this.outFile) { 356 | fs.closeSync(this.outFile); 357 | delete this.outFile; 358 | } 359 | if (this.errFile) { 360 | fs.closeSync(this.errFile); 361 | delete this.errFile; 362 | } 363 | rimraf(this.TMP_PROFILE_DIR, () => resolve()); 364 | }); 365 | } 366 | } 367 | ; 368 | function defaults(val, def) { 369 | return typeof val === 'undefined' ? def : val; 370 | } 371 | function delay(time) { 372 | return new Promise(resolve => setTimeout(resolve, time)); 373 | } 374 | function unixTmpDir() { 375 | return execSync('mktemp -d -t ncc.XXXXXXX').toString().trim(); 376 | } 377 | function win32TmpDir() { 378 | const winTmpPath = process.env.TEMP || process.env.TMP || 379 | (process.env.SystemRoot || process.env.windir) + '\\temp'; 380 | const randomNumber = Math.floor(Math.random() * 9e7 + 1e7); 381 | const tmpdir = path.join(winTmpPath, 'ncc.' + randomNumber); 382 | mkdirp.sync(tmpdir); 383 | return tmpdir; 384 | } 385 | return ChromeLauncher; 386 | })(); 387 | /// 388 | var http = require('http'), fs = require('fs'), ws = require('ws'), path = require('path'); 389 | var DEBUG = false; 390 | var logger; 391 | var NCC = Object.defineProperties((options_, callback_) => { 392 | if (typeof (options_) == 'function') { 393 | callback_ = options_; 394 | options_ = null; 395 | } 396 | var callback = callback_; 397 | if (options_) 398 | for (var key in NCC.options) 399 | if (options_[key] !== undefined) 400 | NCC.options[key] = options_[key]; 401 | logger = require('tracer').colorConsole({ 402 | format: "[ncc] {{message}}", 403 | level: NCC.options.logLevel 404 | }); 405 | var canvas = NCC.createCanvas(undefined, undefined, true); 406 | var attempts = 0; 407 | function connect() { 408 | var url = `http://localhost:${NCC.options.port}/json`; 409 | http.get(url, res => { 410 | var rdJson = ''; 411 | res.on('data', chunk => rdJson += chunk); 412 | res.on('end', () => { 413 | var ncc_ = JSON.parse(rdJson).find(i => i.title === "ncc" || path.basename(i.url) === "ncc.html"); 414 | if (!ncc_) { 415 | if (attempts < NCC.options.retry) { 416 | attempts++; 417 | logger.info(`connecting [retry ${attempts}/${NCC.options.retry}]`); 418 | setTimeout(connect, NCC.options.retryDelay); 419 | } 420 | else 421 | logger.error('connection failed'); 422 | return; 423 | } 424 | Object.defineProperties(rdp, { ws: { value: new ws(ncc_.webSocketDebuggerUrl) } }); 425 | rdp.ws.on('open', () => { 426 | logger.info("connected"); 427 | function checkReadyState() { 428 | rdp.ws.once('message', (data) => { 429 | data = JSON.parse(data); 430 | if (!(data.result && data.result.result.value == "complete")) 431 | return checkReadyState(); 432 | logger.info(`document.readyState is "complete"`); 433 | rdp((err, res) => { 434 | if (err) 435 | logger.error(`[ncc] error: ${err.message}`); 436 | if (callback) 437 | err ? callback(err, null) : callback(null, canvas, rdp); 438 | }); 439 | }); 440 | rdp.ws.send(`{"id":0,"method":"Runtime.evaluate", "params":{"expression":"document.readyState"}}`, err => err && checkReadyState()); 441 | } 442 | checkReadyState(); 443 | }); 444 | rdp.ws.on('close', () => logger.info("session closed")); 445 | }); 446 | }); 447 | } 448 | var index = path.join(__dirname, 'ncc.html'); 449 | var launcher = new ChromeLauncher({ 450 | port: NCC.options.port, 451 | autoSelectChrome: true, 452 | startingUrl: NCC.options.headless ? index : '', 453 | chromeFlags: NCC.options.headless ? 454 | ['--window-size=0,0', '--disable-gpu', '--headless'] : 455 | [`--app=${index}`] 456 | }); 457 | const exitHandler = (err) => { 458 | rdp.ws.terminate(); 459 | launcher.kill().then(() => process.exit(-1)); 460 | }; 461 | process.on('SIGINT', exitHandler); 462 | process.on('unhandledRejection', exitHandler); 463 | process.on('rejectionHandled', exitHandler); 464 | process.on('uncaughtException', exitHandler); 465 | launcher.run() 466 | .then((a) => { 467 | logger.info("chrome started"); 468 | connect(); 469 | }) 470 | .catch(err => { 471 | return launcher.kill().then(() => { 472 | logger.error("failed starting chrome"); 473 | throw err; 474 | }, logger.error); 475 | }); 476 | return canvas; 477 | }, { 478 | options: { 479 | enumerable: true, 480 | writable: true, 481 | value: { 482 | logLevel: 'info', 483 | port: 9222, 484 | retry: 9, 485 | retryDelay: 500, 486 | headless: false 487 | } 488 | }, 489 | createCanvas: { 490 | enumerable: true, 491 | value: function (width, height, main) { 492 | if (!main) { 493 | var uid = NCC.uid('canvas'); 494 | rdp(`var ${uid} = document.createElement('canvas')`); 495 | } 496 | var canvas = (callback) => { 497 | rdp(callback ? (err, res) => { 498 | err ? callback(err, null) : callback(null, canvas); 499 | } : undefined); 500 | return canvas; 501 | }; 502 | CanvasPDM._uid.value = main ? 'canvas' : uid; 503 | Object.defineProperties(canvas, CanvasPDM); 504 | CanvasPDM._uid.value = ''; 505 | canvas.width = width; 506 | canvas.height = height; 507 | return canvas; 508 | } 509 | }, 510 | createImage: { 511 | enumerable: true, 512 | value: function (src, onload, onerror) { 513 | var uid = NCC.uid('image'); 514 | rdp(`var ${uid} = new Image()`); 515 | var image = (callback) => { 516 | rdp(callback ? (err, res) => { 517 | err ? callback(err, null) : callback(null, image); 518 | } : undefined); 519 | return image; 520 | }; 521 | ImagePDM._uid.value = uid; 522 | Object.defineProperties(image, ImagePDM); 523 | ImagePDM._uid.value = ''; 524 | image.src = src; 525 | image.onload = onload; 526 | image.onerror = onerror; 527 | return image; 528 | } 529 | }, 530 | uid: { 531 | enumerable: false, 532 | value: type => `${type}_${Math.random().toString(36).slice(2)}` 533 | } 534 | }); 535 | // RDP | Remote Debugging Protocol (the bridge to chrome) 536 | var rdp = Object.defineProperties((_) => { 537 | if (typeof _ == 'string') { 538 | logger.log(`< ${_}`); 539 | rdp.cmd += `${_};`; 540 | return rdp; 541 | } 542 | if (_ !== null) { 543 | if (rdp.cmd === '') { 544 | _(); 545 | return rdp; 546 | } 547 | rdp.queue.push({ 548 | cmd: rdp.cmd, 549 | callback: _ 550 | }); 551 | rdp.cmd = ''; 552 | } 553 | if (!rdp.queue[0] || rdp.req == rdp.queue[0] || !rdp.ws) 554 | return rdp; 555 | rdp.req = rdp.queue[0]; 556 | logger.trace(`> ${rdp.req.cmd.split(';').join(';\n ')}`); 557 | rdp.ws.once('message', data => { 558 | data.error && logger.error(data.error); 559 | !data.error && logger.log(data.result); 560 | data = JSON.parse(data); 561 | var err = data.error || data.result.wasThrown ? data.result.result.description : null, res = err ? null : data.result.result; 562 | if (rdp.req.callback) 563 | rdp.req.callback(err, res); 564 | rdp.req = rdp.queue.shift(); 565 | rdp(null); 566 | }); 567 | rdp.ws.send(`{"id":0,"method":"Runtime.evaluate", "params":{"expression":"${rdp.req.cmd}"}}`, err => err && rdp()); 568 | return rdp; 569 | }, { 570 | cmd: { enumerable: DEBUG, writable: true, value: '' }, 571 | queue: { enumerable: DEBUG, value: [] } 572 | }); 573 | var CanvasPDM = { 574 | // private properties 575 | _uid: { 576 | configurable: true, 577 | enumerable: DEBUG, 578 | value: "canvas" 579 | }, 580 | _remote: { 581 | enumerable: DEBUG, 582 | set: function (null_) { 583 | if (null_ === null) { 584 | if (this._uid == 'canvas') 585 | return logger.error('you cannot delete the main canvas'); 586 | rdp(`${this._uid} = null`); 587 | Object.defineProperty(this, '_uid', { value: null }); 588 | this._ctx = null; 589 | } 590 | else 591 | return logger.error('"_remote" can only be set to "null"'); 592 | } 593 | }, 594 | _ctx: { 595 | enumerable: DEBUG, 596 | writable: true, 597 | value: null 598 | }, 599 | // Properties || proxies with defaults 600 | width_: { 601 | enumerable: DEBUG, 602 | writable: true, 603 | value: 300 604 | }, 605 | height_: { 606 | enumerable: DEBUG, 607 | writable: true, 608 | value: 150 609 | }, 610 | // Web API: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement 611 | // Properties || getters/setters || https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement#Properties 612 | width: { 613 | enumerable: true, 614 | get: function () { 615 | return this.width_; 616 | }, 617 | set: function (width) { 618 | if (width === undefined) 619 | return; 620 | rdp(`${this._uid}.width = ${width}`); 621 | return this.width_ = width; 622 | } 623 | }, 624 | height: { 625 | enumerable: true, 626 | get: function () { 627 | return this.height_; 628 | }, 629 | set: function (height) { 630 | if (height === undefined) 631 | return; 632 | rdp(`${this._uid}.height = ${height}`); 633 | return this.height_ = height; 634 | } 635 | }, 636 | // Methods || https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement#Methods 637 | getContext: { 638 | enumerable: true, 639 | value: function (contextId) { 640 | if (contextId == '2d') { 641 | var uid = this._uid == 'canvas' ? 'context2d' : NCC.uid('context2d'); 642 | rdp(`var ${uid} = ${this._uid}.getContext('2d')`); 643 | var context2d = (callback) => { 644 | rdp(callback ? (err, res) => { 645 | err ? callback(err, null) : callback(null, context2d); 646 | } : undefined); 647 | return context2d; 648 | }; 649 | context2dPDM._uid.value = uid; 650 | context2dPDM['canvas'].value = this; 651 | Object.defineProperties(context2d, context2dPDM); 652 | context2dPDM._uid.value = ''; 653 | return context2d; 654 | } 655 | else 656 | logger.error(`${contextId} is not implemented`); 657 | } 658 | }, 659 | toDataURL: { 660 | enumerable: true, 661 | value: function (type, args) { 662 | rdp(`${this._uid}.toDataURL(${`'${type}'` || ''})`); 663 | return (callback) => { 664 | rdp((err, res) => { 665 | if (err) 666 | return callback(err, null); 667 | callback(err, res.value); 668 | }); 669 | }; 670 | } 671 | } 672 | }; 673 | var context2dPDM = { 674 | // private properties 675 | _uid: { 676 | enumerable: DEBUG, 677 | value: '' 678 | }, 679 | _remote: { 680 | enumerable: DEBUG, 681 | set: function (null_) { 682 | if (null_ === null) { 683 | rdp(`${this._uid} = null`); 684 | Object.defineProperty(this, '_uid', { value: null }); 685 | } 686 | else 687 | logger.error('"_remote" can only be set to "null"'); 688 | } 689 | }, 690 | // Attributes || proxies with defaults 691 | fillStyle_: { writable: true, enumerable: DEBUG, value: '#000000' }, 692 | font_: { writable: true, enumerable: DEBUG, value: '10px sans-serif' }, 693 | globalAlpha_: { writable: true, enumerable: DEBUG, value: 1.0 }, 694 | globalCompositeOperation_: { writable: true, enumerable: DEBUG, value: 'source-over' }, 695 | lineCap_: { writable: true, enumerable: DEBUG, value: 'butt' }, 696 | lineDashOffset_: { writable: true, enumerable: DEBUG, value: 0 }, 697 | lineJoin_: { writable: true, enumerable: DEBUG, value: 'miter' }, 698 | lineWidth_: { writable: true, enumerable: DEBUG, value: 1.0 }, 699 | miterLimit_: { writable: true, enumerable: DEBUG, value: 10 }, 700 | shadowBlur_: { writable: true, enumerable: DEBUG, value: 0 }, 701 | shadowColor_: { writable: true, enumerable: DEBUG, value: 'rgba(0, 0, 0, 0)' }, 702 | shadowOffsetX_: { writable: true, enumerable: DEBUG, value: 0 }, 703 | shadowOffsetY_: { writable: true, enumerable: DEBUG, value: 0 }, 704 | strokeStyle_: { writable: true, enumerable: DEBUG, value: '#000000' }, 705 | textAlign_: { writable: true, enumerable: DEBUG, value: 'start' }, 706 | textBaseline_: { writable: true, enumerable: DEBUG, value: 'alphabetic' }, 707 | webkitBackingStorePixelRatio_: { writable: true, enumerable: DEBUG, value: 1 }, 708 | webkitImageSmoothingEnabled_: { writable: true, enumerable: DEBUG, value: true }, 709 | // Web API: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingcontext2d 710 | // Attributes || getters/setters || https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingcontext2d#Attributes 711 | canvas: { 712 | enumerable: true, value: null // will be overridden on creation 713 | }, 714 | fillStyle: { 715 | enumerable: true, get: function () { return this.fillStyle_; }, 716 | set: function (fillStyle) { 717 | rdp(`${this._uid}.fillStyle = ${fillStyle._uid || `'${fillStyle}'`}`); 718 | return this.fillStyle_ = fillStyle; 719 | } 720 | }, 721 | font: { 722 | enumerable: true, get: function () { return this.font_; }, 723 | set: function (font) { 724 | rdp(`${this._uid}.font = '${font}'`); 725 | return this.font_ = font; 726 | } 727 | }, 728 | globalAlpha: { 729 | enumerable: true, get: function () { return this.globalAlpha_; }, 730 | set: function (globalAlpha) { 731 | rdp(`${this._uid}.globalAlpha = ${globalAlpha}`); 732 | return this.globalAlpha_ = globalAlpha; 733 | } 734 | }, 735 | globalCompositeOperation: { 736 | enumerable: true, get: function () { return this.globalCompositeOperation_; }, 737 | set: function (globalCompositeOperation) { 738 | rdp(`${this._uid}.globalCompositeOperation = '${globalCompositeOperation}'`); 739 | return this.globalCompositeOperation_ = globalCompositeOperation; 740 | } 741 | }, 742 | lineCap: { 743 | enumerable: true, get: function () { return this.lineCap_; }, 744 | set: function (lineCap) { 745 | rdp(`${this._uid}.lineCap = '${lineCap}'`); 746 | return this.lineCap_ = lineCap; 747 | } 748 | }, 749 | lineDashOffset: { 750 | enumerable: true, get: function () { return this.lineDashOffset_; }, 751 | set: function (lineDashOffset) { 752 | rdp(`${this._uid}.lineDashOffset = ${lineDashOffset}`); 753 | return this.lineDashOffset_ = lineDashOffset; 754 | } 755 | }, 756 | lineJoin: { 757 | enumerable: true, get: function () { return this.lineJoin_; }, 758 | set: function (lineJoin) { 759 | rdp(`${this._uid}.lineJoin = '${lineJoin}'`); 760 | return this.lineJoin_ = lineJoin; 761 | } 762 | }, 763 | lineWidth: { 764 | enumerable: true, get: function () { return this.lineWidth_; }, 765 | set: function (lineWidth) { 766 | rdp(`${this._uid}.lineWidth = ${lineWidth}`); 767 | return this.lineWidth_ = lineWidth; 768 | } 769 | }, 770 | miterLimit: { 771 | enumerable: true, get: function () { return this.miterLimit_; }, 772 | set: function (miterLimit) { 773 | rdp(`${this._uid}.miterLimit = ${miterLimit}`); 774 | return this.miterLimit_ = miterLimit; 775 | } 776 | }, 777 | shadowBlur: { 778 | enumerable: true, get: function () { return this.shadowBlur_; }, 779 | set: function (shadowBlur) { 780 | rdp(`${this._uid}.shadowBlur = ${shadowBlur}`); 781 | return this.shadowBlur_ = shadowBlur; 782 | } 783 | }, 784 | shadowColor: { 785 | enumerable: true, get: function () { return this.shadowColor; }, 786 | set: function (shadowColor) { 787 | rdp(`${this._uid}.shadowColor = '${shadowColor}'`); 788 | return this.shadowColor_ = shadowColor; 789 | } 790 | }, 791 | shadowOffsetX: { 792 | enumerable: true, get: function () { return this.shadowOffsetX_; }, 793 | set: function (shadowOffsetX) { 794 | rdp(`${this._uid}.shadowOffsetX = ${shadowOffsetX}`); 795 | return this.shadowOffsetX_ = shadowOffsetX; 796 | } 797 | }, 798 | shadowOffsetY: { 799 | enumerable: true, get: function () { return this.shadowOffsetY_; }, 800 | set: function (shadowOffsetY) { 801 | rdp(`${this._uid}.shadowOffsetY = ${shadowOffsetY}`); 802 | return this.shadowOffsetY_ = shadowOffsetY; 803 | } 804 | }, 805 | strokeStyle: { 806 | enumerable: true, get: function () { return this.strokeStyle_; }, 807 | set: function (strokeStyle) { 808 | rdp(`${this._uid}.strokeStyle = ${strokeStyle._uid || `'${strokeStyle}'`}`); 809 | return this.strokeStyle_ = strokeStyle; 810 | } 811 | }, 812 | textAlign: { 813 | enumerable: true, get: function () { return this.textAlign_; }, 814 | set: function (textAlign) { 815 | rdp(`${this._uid}.textAlign = '${textAlign}'`); 816 | return this.textAlign_ = textAlign; 817 | } 818 | }, 819 | textBaseline: { 820 | enumerable: true, get: function () { return this.textBaseline_; }, 821 | set: function (textBaseline) { 822 | rdp(`${this._uid}.textBaseline = '${textBaseline}'`); 823 | return this.textBaseline_ = textBaseline; 824 | } 825 | }, 826 | webkitBackingStorePixelRatio: { 827 | enumerable: true, get: function () { return this.webkitBackingStorePixelRatio_; }, 828 | set: function (webkitBackingStorePixelRatio) { 829 | rdp(`${this._uid}.webkitBackingStorePixelRatio = ${webkitBackingStorePixelRatio}`); 830 | return this.webkitBackingStorePixelRatio_ = webkitBackingStorePixelRatio; 831 | } 832 | }, 833 | webkitImageSmoothingEnabled: { 834 | enumerable: true, get: function () { return this.webkitImageSmoothingEnabled_; }, 835 | set: function (webkitImageSmoothingEnabled) { 836 | rdp(`${this._uid}.webkitImageSmoothingEnabled = ${webkitImageSmoothingEnabled}`); 837 | return this.webkitImageSmoothingEnabled_ = webkitImageSmoothingEnabled; 838 | } 839 | }, 840 | // Methods || https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingcontext2d#Methods 841 | arc: { 842 | enumerable: true, 843 | value: function (x, y, radius, startAngle, endAngle, anticlockwise) { 844 | return rdp(`${this._uid}.arc(${Array.prototype.slice.call(arguments, 0).join(',')})`); 845 | } 846 | }, 847 | arcTo: { 848 | enumerable: true, 849 | value: function (x1, y1, x2, y2, radius) { 850 | return rdp(`${this._uid}.arcTo(${x1},${y1},${x2},${y2},${radius})`); 851 | } 852 | }, 853 | beginPath: { 854 | enumerable: true, 855 | value: function () { 856 | return rdp(`${this._uid}.beginPath()`); 857 | } 858 | }, 859 | bezierCurveTo: { 860 | enumerable: true, 861 | value: function (cp1x, cp1y, cp2x, cp2y, x, y) { 862 | return rdp(`${this._uid}.bezierCurveTo(${cp1x},${cp1y},${cp2x},${cp2y},${x},${y})`); 863 | } 864 | }, 865 | clearRect: { 866 | enumerable: true, 867 | value: function (x, y, width, height) { 868 | return rdp(`${this._uid}.clearRect(${x},${y},${width},${height})`); 869 | } 870 | }, 871 | clip: { 872 | enumerable: true, 873 | value: function () { 874 | return rdp(`${this._uid}.clip()`); 875 | } 876 | }, 877 | closePath: { 878 | enumerable: true, 879 | value: function () { 880 | return rdp(`${this._uid}.closePath()`); 881 | } 882 | }, 883 | createImageData: { 884 | enumerable: true, 885 | value: function (width, height) { 886 | if (width.height != undefined) { 887 | height = width.height; 888 | width = width.width; 889 | } 890 | return (callback) => { 891 | callback(null, { 892 | data: new Uint8ClampedArray(Array.apply(null, new Array(width * height * 4)).map(Number.prototype.valueOf, 0)), 893 | width: width, 894 | height: height 895 | }); 896 | }; 897 | } 898 | }, 899 | createLinearGradient: { 900 | enumerable: true, 901 | value: function (x0, y0, x1, y1) { 902 | var uid = NCC.uid('linearGradient'); 903 | rdp(`var ${uid} = ${this._uid}.createLinearGradient(${x0},${y0},${x1},${y1})`); 904 | var linearGradient = (callback) => { 905 | rdp(callback ? (err, res) => { 906 | err ? callback(err, null) : callback(null, linearGradient); 907 | } : undefined); 908 | return linearGradient; 909 | }; 910 | GradientPDM._uid.value = uid; 911 | Object.defineProperties(linearGradient, GradientPDM); 912 | GradientPDM._uid.value = ''; 913 | return linearGradient; 914 | } 915 | }, 916 | createPattern: { 917 | enumerable: true, 918 | value: function (image, repetition) { 919 | var uid = NCC.uid('pattern'); 920 | rdp(`var ${uid} = ${this._uid}.createPattern(${image._uid},'${repetition}')`); 921 | var pattern = (callback) => { 922 | rdp(callback ? (err, res) => { 923 | err ? callback(err, null) : callback(null, pattern); 924 | } : undefined); 925 | return pattern; 926 | }; 927 | PatternPDM._uid.value = uid; 928 | Object.defineProperties(pattern, PatternPDM); 929 | PatternPDM._uid.value = ''; 930 | return pattern; 931 | } 932 | }, 933 | createRadialGradient: { 934 | enumerable: true, 935 | value: function (x0, y0, r0, x1, y1, r1) { 936 | var uid = NCC.uid('pattern'); 937 | rdp(`var ${uid} = ${this._uid}.createRadialGradient(${x0},${y0},${r0},${x1},${y1},${r1})`); 938 | var radialGradient = (callback) => { 939 | rdp(callback ? (err, res) => { 940 | err ? callback(err, null) : callback(null, radialGradient); 941 | } : undefined); 942 | return radialGradient; 943 | }; 944 | GradientPDM._uid.value = NCC.uid('radialGradient'); 945 | Object.defineProperties(radialGradient, GradientPDM); 946 | GradientPDM._uid.value = ''; 947 | return radialGradient; 948 | } 949 | }, 950 | drawImage: { 951 | enumerable: true, 952 | value: function (image, a1, a2, a3, a4, a5, a6, a7, a8) { 953 | return rdp(`${this._uid}.drawImage(${image._uid}, ${Array.prototype.slice.call(arguments, 1).join(',')})`); 954 | } 955 | }, 956 | // no use 957 | //drawCustomFocusRing: { //RETURN/ boolean //IN/ Element element 958 | // enumerable:true, 959 | // value: function (element) { 960 | // rdp(`${this._uid}.drawCustomFocusRing(" + element + ")`); 961 | // return this; 962 | // } 963 | //}, 964 | // no use 965 | //drawSystemFocusRing: { //RETURN/ void //IN/ Element element 966 | // enumerable:true, 967 | // value: function (element) { 968 | // rdp(`${this._uid}.drawSystemFocusRinelementg()`); 969 | // return this; 970 | // } 971 | //}, 972 | fill: { 973 | enumerable: true, 974 | value: function () { 975 | return rdp(`${this._uid}.fill()`); 976 | } 977 | }, 978 | fillRect: { 979 | enumerable: true, 980 | value: function (x, y, width, height) { 981 | return rdp(`${this._uid}.fillRect(${x},${y},${width},${height})`); 982 | } 983 | }, 984 | fillText: { 985 | enumerable: true, 986 | value: function (text, x, y, maxWidth) { 987 | return rdp(`${this._uid}.fillText('${text}',${Array.prototype.slice.call(arguments, 1).join(',')})`); 988 | } 989 | }, 990 | getImageData: { 991 | enumerable: true, 992 | value: function (x, y, width, height) { 993 | rdp(`Array.prototype.slice.call(${this._uid}.getImageData(${x},${y},${width},${height}).data).join(',')`); 994 | return (callback) => { 995 | rdp((err, res) => { 996 | if (err) 997 | return callback(err, null); 998 | var imageData = { 999 | data: new Uint8ClampedArray(res.value.split(',')), 1000 | width: width, 1001 | height: height 1002 | }; 1003 | callback(null, imageData); 1004 | }); 1005 | }; 1006 | } 1007 | }, 1008 | getLineDash: { 1009 | enumerable: true, 1010 | value: function () { 1011 | rdp(`${this._uid}.getLineDash().join(',')`); 1012 | return (callback) => { 1013 | rdp((err, res) => { 1014 | if (err) 1015 | return callback(err); 1016 | res.value = res.value.split(','); 1017 | for (var i = 0, l = res.value.length; i < l; i++) 1018 | res.value[i] = +res.value[i]; 1019 | callback(err, res.value); 1020 | }); 1021 | }; 1022 | } 1023 | }, 1024 | isPointInPath: { 1025 | enumerable: true, 1026 | value: function (x, y) { 1027 | rdp(`${this._uid}.isPointInPath(${x},${y})`); 1028 | return (callback) => { 1029 | rdp((err, res) => { 1030 | callback(err, res.value); 1031 | }); 1032 | }; 1033 | } 1034 | }, 1035 | isPointInStroke: { 1036 | enumerable: true, 1037 | value: function (x, y) { 1038 | rdp(`${this._uid}.isPointInStroke(${x},${y})`); 1039 | return (callback) => { 1040 | rdp((err, res) => { 1041 | callback(err, res.value); 1042 | }); 1043 | }; 1044 | } 1045 | }, 1046 | lineTo: { 1047 | enumerable: true, 1048 | value: function (x, y) { 1049 | return rdp(`${this._uid}.lineTo(${x},${y})`); 1050 | } 1051 | }, 1052 | measureText: { 1053 | enumerable: true, 1054 | value: function (text) { 1055 | rdp(`${this._uid}.measureText('${text}').width`); 1056 | return (callback) => { 1057 | rdp((err, res) => { 1058 | if (err) 1059 | return callback(err); 1060 | callback(null, { width: res.value }); 1061 | }); 1062 | }; 1063 | } 1064 | }, 1065 | moveTo: { 1066 | enumerable: true, 1067 | value: function (x, y) { 1068 | return rdp(`${this._uid}.moveTo(${x},${y})`); 1069 | } 1070 | }, 1071 | putImageData: { 1072 | enumerable: true, 1073 | value: function (imagedata, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight) { 1074 | return rdp(`var data = [${Array.prototype.slice.call(imagedata.data).join(',')}]; var iD = ${this._uid}.createImageData(${imagedata.width}, ${imagedata.height}); for (var i = 0, l = iD.data.length; i < l; i++) iD.data[i] = +data[i]; ${this._uid}.putImageData(iD, ${Array.prototype.slice.call(arguments, 1).join(',')})`); 1075 | } 1076 | }, 1077 | quadraticCurveTo: { 1078 | enumerable: true, 1079 | value: function (cpx, cpy, x, y) { 1080 | return rdp(`${this._uid}.quadraticCurveTo(${cpx},${cpy},${x},${y})`); 1081 | } 1082 | }, 1083 | rect: { 1084 | enumerable: true, 1085 | value: function (x, y, width, height) { 1086 | return rdp(`${this._uid}.rect(${x},${y},${width},${height})`); 1087 | } 1088 | }, 1089 | restore: { 1090 | enumerable: true, 1091 | value: function () { 1092 | return rdp(`${this._uid}.restore()`); 1093 | } 1094 | }, 1095 | rotate: { 1096 | enumerable: true, 1097 | value: function (angle) { 1098 | return rdp(`${this._uid}.rotate(${angle})`); 1099 | } 1100 | }, 1101 | save: { 1102 | enumerable: true, 1103 | value: function () { 1104 | return rdp(`${this._uid}.save()`); 1105 | } 1106 | }, 1107 | scale: { 1108 | enumerable: true, 1109 | value: function (x, y) { 1110 | return rdp(`${this._uid}.scale(${x},${y})`); 1111 | } 1112 | }, 1113 | // no use 1114 | //scrollPathIntoView: { //RETURN/ void //IN/ 1115 | // enumerable: true, 1116 | // value: function () { 1117 | // rdp(`${this._uid}.scrollPathIntoView()`); 1118 | // return this; 1119 | // } 1120 | //}, 1121 | setLineDash: { 1122 | enumerable: true, 1123 | value: function (segments) { 1124 | return rdp(`${this._uid}.setLineDash([${segments.join(',')}])`); 1125 | } 1126 | }, 1127 | setTransform: { 1128 | enumerable: true, 1129 | value: function (m11, m12, m21, m22, dx, dy) { 1130 | return rdp(`${this._uid}.setTransform(${m11},${m12},${m21},${m22},${dx},${dy})`); 1131 | } 1132 | }, 1133 | stroke: { 1134 | enumerable: true, 1135 | value: function () { 1136 | return rdp(`${this._uid}.stroke()`); 1137 | } 1138 | }, 1139 | strokeRect: { 1140 | enumerable: true, 1141 | value: function (x, y, w, h) { 1142 | return rdp(`${this._uid}.strokeRect(${x},${y},${w},${h})`); 1143 | } 1144 | }, 1145 | strokeText: { 1146 | enumerable: true, 1147 | value: function (text, x, y, maxWidth) { 1148 | rdp(`${this._uid}.strokeText('${text}',${(Array.prototype.slice.call(arguments, 1).join(','))})`); 1149 | return this; 1150 | } 1151 | }, 1152 | transform: { 1153 | enumerable: true, 1154 | value: function (m11, m12, m21, m22, dx, dy) { 1155 | return rdp(`${this._uid}.transform(${m11},${m12},${m21},${m22},${dx},${dy})`); 1156 | } 1157 | }, 1158 | translate: { 1159 | enumerable: true, 1160 | value: function (x, y) { 1161 | return rdp(`${this._uid}.translate(${x},${y})`); 1162 | } 1163 | } 1164 | }; 1165 | var GradientPDM = { 1166 | // private properties 1167 | _uid: { 1168 | enumerable: DEBUG, 1169 | value: '' 1170 | }, 1171 | _remote: { 1172 | enumerable: DEBUG, 1173 | set: function (null_) { 1174 | if (null_ === null) { 1175 | rdp(`${this._uid} = null`); 1176 | Object.defineProperty(this, '_uid', { value: null }); 1177 | } 1178 | else 1179 | logger.error('"_remote" can only be set to "null"'); 1180 | } 1181 | }, 1182 | // Web API: https://developer.mozilla.org/en-US/docs/Web/API/CanvasGradient 1183 | // Methods 1184 | addColorStop: { 1185 | enumerable: true, 1186 | value: function (offset, color) { 1187 | return rdp(`${this._uid}.addColorStop(${offset},'${color}')`); 1188 | } 1189 | } 1190 | }; 1191 | var PatternPDM = { 1192 | // private properties 1193 | _uid: { 1194 | enumerable: DEBUG, 1195 | value: '' 1196 | }, 1197 | _remote: { 1198 | enumerable: DEBUG, 1199 | set: function (null_) { 1200 | if (null_ === null) { 1201 | rdp(`${this._uid} = null`); 1202 | Object.defineProperty(this, '_uid', { value: null }); 1203 | } 1204 | else 1205 | logger.error('"_remote" can only be set to "null"'); 1206 | } 1207 | }, 1208 | }; 1209 | var mimeMap = { 1210 | png: 'image/png', 1211 | webp: 'image/webp', 1212 | jpeg: 'image/jpeg', 1213 | jpg: 'image/jpeg', 1214 | svg: 'image/svg+xml', 1215 | gif: 'image/gif' 1216 | }; 1217 | var regExp_http = new RegExp('^(http:\\/\\/.+)', 'i'); 1218 | var regExp_data = new RegExp('^(data:image\\/\\w+;base64,.+)'); 1219 | var regExp_type = new RegExp('^data:image\\/(\\w+);base64,'); 1220 | var ImagePDM = { 1221 | // private properties 1222 | _uid: { 1223 | enumerable: DEBUG, 1224 | value: '' 1225 | }, 1226 | _remote: { 1227 | enumerable: DEBUG, 1228 | set: function (null_) { 1229 | if (null_ === null) { 1230 | rdp(`${this._uid} = null`); 1231 | Object.defineProperty(this, '_uid', { value: null }); 1232 | } 1233 | else 1234 | logger.error('"_remote" can only be set to "null"'); 1235 | } 1236 | }, 1237 | // Properties 1238 | src_: { 1239 | enumerable: DEBUG, 1240 | writable: true, 1241 | value: '' 1242 | }, 1243 | width_: { 1244 | enumerable: DEBUG, 1245 | writable: true, 1246 | value: undefined 1247 | }, 1248 | height_: { 1249 | enumerable: DEBUG, 1250 | writable: true, 1251 | value: undefined 1252 | }, 1253 | _base64_: { 1254 | enumerable: DEBUG, 1255 | writable: true, 1256 | value: null 1257 | }, 1258 | _base64: { 1259 | enumerable: DEBUG, 1260 | get: function () { 1261 | return this._base64_; 1262 | }, 1263 | set: function (base64) { 1264 | rdp(`${this._uid}.src = '${base64}'`); 1265 | rdp(() => { 1266 | rdp(`${this._uid}.width + '_' + ${this._uid}.height`); 1267 | rdp((err, res) => { 1268 | if (err && this.onerror) 1269 | return this.onerror(err); 1270 | var size = res.value.split('_'); 1271 | this.width_ = +size[0]; 1272 | this.height_ = +size[1]; 1273 | if (this.onload) 1274 | return this.onload(this); 1275 | }); 1276 | }); 1277 | this._base64_ = base64; 1278 | return this._base64_; 1279 | } 1280 | }, 1281 | // Methods 1282 | _toFile: { 1283 | enumerable: DEBUG, 1284 | value: function (filename, callback) { 1285 | var head = regExp_type.exec(this._base64_), type = filename.split('.').pop(); 1286 | if (!head || !head[1] || (head[1] != ((type == "jpg") ? "jpeg" : type))) 1287 | if (callback) 1288 | return callback(`type mismatch ${head ? head[1] : "'unknown'"} !> ${type}`); 1289 | else 1290 | throw new Error(`type mismatch ${head ? head[1] : "'unknown'"} !> ${type}`); 1291 | logger.info(`[ncc] writing image to: ${filename}`); 1292 | fs.writeFile(filename, new Buffer(this._base64_.replace(/^data:image\/\w+;base64,/, ''), 'base64'), {}, callback); 1293 | } 1294 | }, 1295 | // Web API 1296 | // Properties 1297 | src: { 1298 | enumerable: true, 1299 | get: function () { 1300 | return this.src_; 1301 | }, 1302 | set: function (src) { 1303 | var img = this; 1304 | this._src = src; 1305 | if (!src || src === '') 1306 | return; 1307 | if (regExp_data.test(src)) 1308 | img._base64 = src; 1309 | else if (regExp_http.test(src)) { 1310 | logger.info(`[ncc] loading image from URL: ${src}`); 1311 | http.get(src, function (res) { 1312 | var data = ''; 1313 | res.setEncoding('base64'); 1314 | if (res.statusCode != 200) { 1315 | if (img.onerror) 1316 | return img.onerror(`loading image failed with status ${res.statusCode}`); 1317 | else 1318 | logger.error(`loading image failed with status ${res.statusCode}`); 1319 | } 1320 | res.on('data', function (chunk) { data += chunk; }); 1321 | res.on('end', function () { 1322 | img._base64 = `data:${(res.headers["content-type"] || mimeMap[src.split('.').pop()])};base64,${data}`; 1323 | logger.info('[ncc] loading image from URL completed'); 1324 | }); 1325 | }).on('error', this.onerror || function (err) { 1326 | if (img.onerror) 1327 | return img.onerror(err); 1328 | else 1329 | logger.error(`loading image failed with err ${err}`); 1330 | }); 1331 | } 1332 | else { 1333 | logger.info(`[ncc] loading image from FS: ${src}`); 1334 | fs.readFile(src, 'base64', function (err, data) { 1335 | if (err) { 1336 | if (img.onerror) 1337 | img.onerror(err); 1338 | else 1339 | logger.error(`loading image failed with err ${err}`); 1340 | } 1341 | img._base64 = `data:${mimeMap[src.split('.').pop()]};base64,${data}`; 1342 | logger.info('[ncc] loading image from FS completed'); 1343 | }); 1344 | } 1345 | return this.src_; 1346 | } 1347 | }, 1348 | onload: { 1349 | writable: true, 1350 | enumerable: true, 1351 | value: undefined 1352 | }, 1353 | onerror: { 1354 | writable: true, 1355 | enumerable: true, 1356 | value: undefined 1357 | }, 1358 | width: { 1359 | enumerable: true, 1360 | get: function () { 1361 | return this.width_; 1362 | } 1363 | }, 1364 | height: { 1365 | enumerable: true, 1366 | get: function () { 1367 | return this.height_; 1368 | } 1369 | } 1370 | }; 1371 | module.exports = NCC; 1372 | -------------------------------------------------------------------------------- /lib/ncc.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | var http = require('http') 4 | , fs = require('fs') 5 | , ws = require('ws') 6 | , path = require('path'); 7 | 8 | var DEBUG = false; 9 | 10 | var logger; 11 | 12 | interface NCC { 13 | (options?, callback?): Canvas; 14 | options: any; 15 | createCanvas(width?: number, height?: number): Canvas; 16 | createImage(src?: string, onload?: Function, onerror?: Function): Image; 17 | uid(type?: string): string; 18 | log(msg: string, level?: number): void; 19 | } 20 | 21 | var NCC = Object.defineProperties( 22 | (options_, callback_) => { 23 | if (typeof (options_) == 'function') { 24 | callback_ = options_; 25 | options_ = null; 26 | } 27 | 28 | var callback = callback_; 29 | 30 | if (options_) 31 | for (var key in NCC.options) 32 | if (options_[key] !== undefined) 33 | NCC.options[key] = options_[key]; 34 | 35 | 36 | logger = require('tracer').colorConsole({ 37 | format: "[ncc] {{message}}", 38 | level: NCC.options.logLevel 39 | }); 40 | 41 | var canvas = NCC.createCanvas(undefined, undefined, true); 42 | 43 | var attempts = 0; 44 | function connect() { 45 | var url = `http://localhost:${NCC.options.port}/json`; 46 | 47 | http.get(url, res => { 48 | var rdJson = ''; 49 | 50 | res.on('data', chunk => rdJson += chunk); 51 | 52 | res.on('end', () => { 53 | var ncc_ = JSON.parse(rdJson).find(i => i.title === "ncc" || path.basename(i.url) === "ncc.html"); 54 | 55 | if (!ncc_) { 56 | if (attempts < NCC.options.retry) { 57 | attempts++; 58 | logger.info(`connecting [retry ${attempts}/${NCC.options.retry}]`); 59 | setTimeout(connect, NCC.options.retryDelay); 60 | } else logger.error('connection failed'); 61 | return; 62 | } 63 | 64 | Object.defineProperties(rdp, { ws: { value: new ws(ncc_.webSocketDebuggerUrl) } }); 65 | 66 | rdp.ws.on('open', () => { 67 | logger.info("connected"); 68 | 69 | function checkReadyState() { 70 | rdp.ws.once('message', (data) => { 71 | data = JSON.parse(data); 72 | if (!(data.result && data.result.result.value == "complete")) 73 | return checkReadyState(); 74 | 75 | logger.info(`document.readyState is "complete"`); 76 | rdp((err, res) => { 77 | 78 | if (err) 79 | logger.error(`[ncc] error: ${err.message}`); 80 | if (callback) 81 | err ? callback(err, null) : callback(null, canvas, rdp); 82 | }) 83 | }) 84 | rdp.ws.send(`{"id":0,"method":"Runtime.evaluate", "params":{"expression":"document.readyState"}}`, err => err && checkReadyState()); 85 | } 86 | checkReadyState() 87 | 88 | 89 | }); 90 | 91 | rdp.ws.on('close', () => logger.info("session closed")); 92 | 93 | 94 | }) 95 | }) 96 | } 97 | 98 | var index = path.join(__dirname, 'ncc.html'); 99 | var launcher = new ChromeLauncher({ 100 | port: NCC.options.port, 101 | autoSelectChrome: true, 102 | startingUrl: NCC.options.headless ? index : '', 103 | chromeFlags: NCC.options.headless ? 104 | ['--window-size=0,0', '--disable-gpu', '--headless'] : 105 | [`--app=${index}`] 106 | }) 107 | 108 | const exitHandler = (err) => { 109 | rdp.ws.terminate() 110 | launcher.kill().then(() => process.exit(-1)); 111 | }; 112 | 113 | process.on('SIGINT', exitHandler); 114 | process.on('unhandledRejection', exitHandler); 115 | process.on('rejectionHandled', exitHandler); 116 | process.on('uncaughtException', exitHandler); 117 | 118 | launcher.run() 119 | .then((a) => { 120 | logger.info("chrome started"); 121 | connect() 122 | }) 123 | .catch(err => { 124 | return launcher.kill().then(() => { 125 | logger.error("failed starting chrome"); 126 | throw err; 127 | }, logger.error); 128 | }); 129 | 130 | return canvas; 131 | 132 | }, { 133 | options: { 134 | enumerable: true, 135 | writable: true, 136 | value: { 137 | logLevel: 'info', 138 | port: 9222, 139 | retry: 9, 140 | retryDelay: 500, 141 | headless: false 142 | } 143 | }, 144 | createCanvas: { 145 | enumerable: true, 146 | value: function (width?: number, height?: number, main?: boolean) { 147 | 148 | if (!main) { 149 | var uid = NCC.uid('canvas') 150 | rdp(`var ${uid} = document.createElement('canvas')`); 151 | } 152 | 153 | var canvas: any = (callback?) => { 154 | rdp(callback ? (err, res) => { 155 | err ? callback(err, null) : callback(null, canvas); 156 | } : undefined); 157 | return canvas; 158 | } 159 | 160 | CanvasPDM._uid.value = main ? 'canvas' : uid; 161 | Object.defineProperties(canvas, CanvasPDM); 162 | CanvasPDM._uid.value = ''; 163 | 164 | canvas.width = width; 165 | canvas.height = height; 166 | 167 | return canvas; 168 | } 169 | }, 170 | createImage: { 171 | enumerable: true, 172 | value: function (src?: string, onload?: Function, onerror?: Function) { 173 | var uid = NCC.uid('image') 174 | rdp(`var ${uid} = new Image()`); 175 | var image: any = (callback?) => { 176 | rdp(callback ? (err, res) => { 177 | err ? callback(err, null) : callback(null, image); 178 | } : undefined); 179 | return image; 180 | } 181 | 182 | ImagePDM._uid.value = uid; 183 | Object.defineProperties(image, ImagePDM); 184 | ImagePDM._uid.value = ''; 185 | 186 | image.src = src; 187 | image.onload = onload; 188 | image.onerror = onerror; 189 | 190 | return image 191 | 192 | } 193 | }, 194 | uid: { 195 | enumerable: false, 196 | value: type => `${type}_${Math.random().toString(36).slice(2)}` 197 | } 198 | }) 199 | 200 | 201 | 202 | 203 | interface RDP { 204 | (_?: any): RDP; 205 | cmd: string; 206 | queue: any[]; 207 | ws: any; 208 | } 209 | 210 | // RDP | Remote Debugging Protocol (the bridge to chrome) 211 | var rdp = Object.defineProperties( 212 | (_): RDP => { 213 | 214 | if (typeof _ == 'string') { 215 | logger.log(`< ${_}`); 216 | rdp.cmd += `${_};`; 217 | return rdp; 218 | } 219 | 220 | if (_ !== null) { 221 | if (rdp.cmd === '') { 222 | _(); 223 | return rdp; 224 | } 225 | rdp.queue.push({ 226 | cmd: rdp.cmd, 227 | callback: _ 228 | }); 229 | rdp.cmd = ''; 230 | } 231 | 232 | if (!rdp.queue[0] || rdp.req == rdp.queue[0] || !rdp.ws) return rdp; 233 | 234 | rdp.req = rdp.queue[0]; 235 | 236 | logger.trace(`> ${rdp.req.cmd.split(';').join(';\n ')}`); 237 | 238 | rdp.ws.once('message', data => { 239 | data.error && logger.error(data.error) 240 | !data.error && logger.log(data.result); 241 | 242 | data = JSON.parse(data); 243 | 244 | var err = data.error || data.result.wasThrown ? data.result.result.description : null, 245 | res = err ? null : data.result.result; 246 | 247 | if (rdp.req.callback) rdp.req.callback(err, res); 248 | rdp.req = rdp.queue.shift(); 249 | rdp(null); 250 | }); 251 | 252 | rdp.ws.send(`{"id":0,"method":"Runtime.evaluate", "params":{"expression":"${rdp.req.cmd}"}}`, err => err && rdp()); 253 | 254 | return rdp; 255 | }, { 256 | cmd: { enumerable: DEBUG, writable: true, value: '' }, 257 | queue: { enumerable: DEBUG, value: [] } 258 | }) 259 | 260 | // Callback || abstract interface 261 | interface Callback { 262 | (callback?: Function): Callback; 263 | } 264 | 265 | // ProxyObj || abstract interface 266 | interface ProxyObj extends Callback { 267 | _uid: string; 268 | _remote: any; 269 | } 270 | 271 | // Canvas 272 | interface Canvas extends ProxyObj { 273 | (callback?): Canvas; 274 | _ctx: Context2d; 275 | width: number; 276 | height: number; 277 | getContext: (contextId: string) => Context2d; 278 | } 279 | 280 | var CanvasPDM = { 281 | 282 | // private properties 283 | _uid: { 284 | configurable: true, 285 | enumerable: DEBUG, 286 | value: "canvas" 287 | }, 288 | _remote: { 289 | enumerable: DEBUG, 290 | set: function (null_) { 291 | if (null_ === null) { 292 | if (this._uid == 'canvas') 293 | return logger.error('you cannot delete the main canvas') 294 | rdp(`${this._uid} = null`); 295 | Object.defineProperty(this, '_uid', { value: null }); 296 | this._ctx = null; 297 | } else return logger.error('"_remote" can only be set to "null"') 298 | } 299 | }, 300 | 301 | _ctx: { 302 | enumerable: DEBUG, 303 | writable: true, 304 | value: null 305 | }, 306 | 307 | // Properties || proxies with defaults 308 | width_: { 309 | enumerable: DEBUG, 310 | writable: true, 311 | value: 300 312 | }, 313 | height_: { 314 | enumerable: DEBUG, 315 | writable: true, 316 | value: 150 317 | }, 318 | 319 | // Web API: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement 320 | 321 | // Properties || getters/setters || https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement#Properties 322 | width: { 323 | enumerable: true, 324 | get: function () { 325 | return this.width_; 326 | }, 327 | set: function (width) { 328 | if (width === undefined) return; 329 | rdp(`${this._uid}.width = ${width}`) 330 | return this.width_ = width; 331 | } 332 | }, 333 | height: { 334 | enumerable: true, 335 | get: function () { 336 | return this.height_; 337 | }, 338 | set: function (height) { 339 | if (height === undefined) return; 340 | rdp(`${this._uid}.height = ${height}`) 341 | return this.height_ = height; 342 | } 343 | }, 344 | 345 | // Methods || https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement#Methods 346 | getContext: { 347 | enumerable: true, 348 | value: function (contextId: string) { 349 | if (contextId == '2d') { 350 | 351 | var uid = this._uid == 'canvas' ? 'context2d' : NCC.uid('context2d') 352 | rdp(`var ${uid} = ${this._uid}.getContext('2d')`); 353 | 354 | var context2d: any = (callback?) => { 355 | rdp(callback ? (err, res) => { 356 | err ? callback(err, null) : callback(null, context2d); 357 | } : undefined); 358 | return context2d; 359 | } 360 | 361 | context2dPDM._uid.value = uid; 362 | context2dPDM['canvas'].value = this; 363 | Object.defineProperties(context2d, context2dPDM); 364 | context2dPDM._uid.value = ''; 365 | 366 | return context2d; 367 | } else 368 | logger.error(`${contextId} is not implemented`); 369 | } 370 | }, 371 | toDataURL: { 372 | enumerable: true, 373 | value: function (type, args) { 374 | 375 | rdp(`${this._uid}.toDataURL(${`'${type}'` || ''})`); 376 | 377 | 378 | return (callback) => { 379 | rdp((err, res) => { 380 | if (err) 381 | return callback(err, null); 382 | callback(err, res.value); 383 | }); 384 | }; 385 | } 386 | } 387 | }; 388 | 389 | 390 | // context2d 391 | interface Context2d extends ProxyObj { 392 | (callback?): Context2d; 393 | canvas: Canvas; 394 | fillStyle: any; 395 | font: string; 396 | globalAlpha: number; 397 | globalCompositeOperation: string; 398 | lineCap: string; 399 | lineJoin: string; 400 | lineWidth: number; 401 | miterLimit: number; 402 | shadowBlur: number; 403 | shadowColor: string; 404 | shadowOffsetX: number; 405 | shadowOffsetY: number; 406 | strokeStyle: any; 407 | textAlign: string; 408 | textBaseline: string; 409 | webkitBackingStorePixelRatio: number; 410 | webkitImageSmoothingEnabled: boolean; 411 | arc(x: number, y: number, radius: number, startAngle?: number, endAngle?: number, anticlockwise?: boolean): Context2d; 412 | arcTo(x1: number, y1: number, x2: number, y2: number, radius: number): Context2d; 413 | beginPath(): Context2d; 414 | bezierCurveTo(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number): Context2d; 415 | clearRect(x: number, y: number, w: number, h: number): Context2d; 416 | clip(): Context2d; 417 | closePath(): Context2d; 418 | createImageData(imageDataOrSw: any, sh?: number): Context2d; 419 | createLinearGradient(x0: number, y0: number, x1: number, y1: number): Context2d; 420 | createPattern(image: HTMLElement, repetition: string): Context2d; 421 | createRadialGradient(x0: number, y0: number, r0: number, x1: number, y1: number, r1: number): Context2d; 422 | drawImage(image: HTMLElement, offsetX: number, offsetY: number, width?: number, height?: number, canvasOffsetX?: number, canvasOffsetY?: number, canvasImageWidth?: number, canvasImageHeight?: number): Context2d; 423 | fill(): Context2d; 424 | fillRect(x: number, y: number, w: number, h: number): Context2d; 425 | fillText(text: string, x: number, y: number, maxWidth?: number): Context2d; 426 | getImageData(sx: number, sy: number, sw: number, sh: number): Context2d; 427 | getLineDash(): Context2d; 428 | isPointInPath(x: number, y: number): Context2d; 429 | isPointInStroke(x: number, y: number): Context2d; 430 | lineTo(x: number, y: number): Context2d; 431 | measureText(text: string): Context2d; 432 | moveTo(x: number, y: number): Context2d; 433 | putImageData(imagedata: ImageData, dx: number, dy: number, dirtyX?: number, dirtyY?: number, dirtyWidth?: number, dirtyHeight?: number): Context2d; 434 | quadraticCurveTo(cpx: number, cpy: number, x: number, y: number): Context2d; 435 | rect(x: number, y: number, w: number, h: number): Context2d; 436 | restore(): Context2d; 437 | rotate(angle: number): Context2d; 438 | save(): Context2d; 439 | scale(x: number, y: number): Context2d; 440 | setLineDash(): Context2d; 441 | setTransform(m11: number, m12: number, m21: number, m22: number, dx: number, dy: number): Context2d; 442 | stroke(): Context2d; 443 | strokeRect(x: number, y: number, w: number, h: number): Context2d; 444 | strokeText(text: string, x: number, y: number, maxWidth?: number): Context2d; 445 | transform(m11: number, m12: number, m21: number, m22: number, dx: number, dy: number): Context2d; 446 | translate(x: number, y: number): Context2d; 447 | } 448 | 449 | var context2dPDM = { 450 | 451 | // private properties 452 | _uid: { 453 | enumerable: DEBUG, 454 | value: '' 455 | }, 456 | _remote: { 457 | enumerable: DEBUG, 458 | set: function (null_) { 459 | if (null_ === null) { 460 | rdp(`${this._uid} = null`); 461 | Object.defineProperty(this, '_uid', { value: null }); 462 | } else 463 | logger.error('"_remote" can only be set to "null"') 464 | } 465 | }, 466 | 467 | // Attributes || proxies with defaults 468 | fillStyle_: { writable: true, enumerable: DEBUG, value: '#000000' }, 469 | font_: { writable: true, enumerable: DEBUG, value: '10px sans-serif' }, 470 | globalAlpha_: { writable: true, enumerable: DEBUG, value: 1.0 }, 471 | globalCompositeOperation_: { writable: true, enumerable: DEBUG, value: 'source-over' }, 472 | lineCap_: { writable: true, enumerable: DEBUG, value: 'butt' }, 473 | lineDashOffset_: { writable: true, enumerable: DEBUG, value: 0 }, 474 | lineJoin_: { writable: true, enumerable: DEBUG, value: 'miter' }, 475 | lineWidth_: { writable: true, enumerable: DEBUG, value: 1.0 }, 476 | miterLimit_: { writable: true, enumerable: DEBUG, value: 10 }, 477 | shadowBlur_: { writable: true, enumerable: DEBUG, value: 0 }, 478 | shadowColor_: { writable: true, enumerable: DEBUG, value: 'rgba(0, 0, 0, 0)' }, 479 | shadowOffsetX_: { writable: true, enumerable: DEBUG, value: 0 }, 480 | shadowOffsetY_: { writable: true, enumerable: DEBUG, value: 0 }, 481 | strokeStyle_: { writable: true, enumerable: DEBUG, value: '#000000' }, 482 | textAlign_: { writable: true, enumerable: DEBUG, value: 'start' }, 483 | textBaseline_: { writable: true, enumerable: DEBUG, value: 'alphabetic' }, 484 | webkitBackingStorePixelRatio_: { writable: true, enumerable: DEBUG, value: 1 }, 485 | webkitImageSmoothingEnabled_: { writable: true, enumerable: DEBUG, value: true }, 486 | 487 | // Web API: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingcontext2d 488 | 489 | // Attributes || getters/setters || https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingcontext2d#Attributes 490 | canvas: { 491 | enumerable: true, value: null // will be overridden on creation 492 | }, 493 | fillStyle: { 494 | enumerable: true, get: function () { return this.fillStyle_; }, 495 | set: function (fillStyle) { 496 | rdp(`${this._uid}.fillStyle = ${fillStyle._uid || `'${fillStyle}'`}`); 497 | return this.fillStyle_ = fillStyle; 498 | } 499 | }, 500 | font: { 501 | enumerable: true, get: function () { return this.font_; }, 502 | set: function (font) { 503 | rdp(`${this._uid}.font = '${font}'`); 504 | return this.font_ = font; 505 | } 506 | }, 507 | globalAlpha: { 508 | enumerable: true, get: function () { return this.globalAlpha_; }, 509 | set: function (globalAlpha) { 510 | rdp(`${this._uid}.globalAlpha = ${globalAlpha}`); 511 | return this.globalAlpha_ = globalAlpha; 512 | } 513 | }, 514 | globalCompositeOperation: { 515 | enumerable: true, get: function () { return this.globalCompositeOperation_; }, 516 | set: function (globalCompositeOperation) { 517 | rdp(`${this._uid}.globalCompositeOperation = '${globalCompositeOperation}'`); 518 | return this.globalCompositeOperation_ = globalCompositeOperation; 519 | } 520 | }, 521 | lineCap: { 522 | enumerable: true, get: function () { return this.lineCap_; }, 523 | set: function (lineCap) { 524 | rdp(`${this._uid}.lineCap = '${lineCap}'`); 525 | return this.lineCap_ = lineCap; 526 | } 527 | }, 528 | lineDashOffset: { 529 | enumerable: true, get: function () { return this.lineDashOffset_; }, 530 | set: function (lineDashOffset) { 531 | rdp(`${this._uid}.lineDashOffset = ${lineDashOffset}`); 532 | return this.lineDashOffset_ = lineDashOffset; 533 | } 534 | }, 535 | lineJoin: { 536 | enumerable: true, get: function () { return this.lineJoin_; }, 537 | set: function (lineJoin) { 538 | rdp(`${this._uid}.lineJoin = '${lineJoin}'`); 539 | return this.lineJoin_ = lineJoin; 540 | } 541 | }, 542 | lineWidth: { 543 | enumerable: true, get: function () { return this.lineWidth_; }, 544 | set: function (lineWidth) { 545 | rdp(`${this._uid}.lineWidth = ${lineWidth}`); 546 | return this.lineWidth_ = lineWidth; 547 | } 548 | }, 549 | miterLimit: { 550 | enumerable: true, get: function () { return this.miterLimit_; }, 551 | set: function (miterLimit) { 552 | rdp(`${this._uid}.miterLimit = ${miterLimit}`); 553 | return this.miterLimit_ = miterLimit; 554 | } 555 | }, 556 | shadowBlur: { 557 | enumerable: true, get: function () { return this.shadowBlur_; }, 558 | set: function (shadowBlur) { 559 | rdp(`${this._uid}.shadowBlur = ${shadowBlur}`); 560 | return this.shadowBlur_ = shadowBlur; 561 | } 562 | }, 563 | shadowColor: { 564 | enumerable: true, get: function () { return this.shadowColor; }, 565 | set: function (shadowColor) { 566 | rdp(`${this._uid}.shadowColor = '${shadowColor}'`); 567 | return this.shadowColor_ = shadowColor; 568 | } 569 | }, 570 | shadowOffsetX: { 571 | enumerable: true, get: function () { return this.shadowOffsetX_; }, 572 | set: function (shadowOffsetX) { 573 | rdp(`${this._uid}.shadowOffsetX = ${shadowOffsetX}`); 574 | return this.shadowOffsetX_ = shadowOffsetX; 575 | } 576 | }, 577 | shadowOffsetY: { 578 | enumerable: true, get: function () { return this.shadowOffsetY_; }, 579 | set: function (shadowOffsetY) { 580 | rdp(`${this._uid}.shadowOffsetY = ${shadowOffsetY}`); 581 | return this.shadowOffsetY_ = shadowOffsetY; 582 | } 583 | }, 584 | strokeStyle: { 585 | enumerable: true, get: function () { return this.strokeStyle_; }, 586 | set: function (strokeStyle) { 587 | rdp(`${this._uid}.strokeStyle = ${strokeStyle._uid || `'${strokeStyle}'`}`); 588 | return this.strokeStyle_ = strokeStyle; 589 | } 590 | }, 591 | textAlign: { 592 | enumerable: true, get: function () { return this.textAlign_; }, 593 | set: function (textAlign) { 594 | rdp(`${this._uid}.textAlign = '${textAlign}'`); 595 | return this.textAlign_ = textAlign; 596 | } 597 | }, 598 | textBaseline: { 599 | enumerable: true, get: function () { return this.textBaseline_; }, 600 | set: function (textBaseline) { 601 | rdp(`${this._uid}.textBaseline = '${textBaseline}'`); 602 | return this.textBaseline_ = textBaseline; 603 | } 604 | }, 605 | webkitBackingStorePixelRatio: { 606 | enumerable: true, get: function () { return this.webkitBackingStorePixelRatio_; }, 607 | set: function (webkitBackingStorePixelRatio) { 608 | rdp(`${this._uid}.webkitBackingStorePixelRatio = ${webkitBackingStorePixelRatio}`); 609 | return this.webkitBackingStorePixelRatio_ = webkitBackingStorePixelRatio; 610 | } 611 | }, 612 | webkitImageSmoothingEnabled: { 613 | enumerable: true, get: function () { return this.webkitImageSmoothingEnabled_; }, 614 | set: function (webkitImageSmoothingEnabled) { 615 | rdp(`${this._uid}.webkitImageSmoothingEnabled = ${webkitImageSmoothingEnabled}`); 616 | return this.webkitImageSmoothingEnabled_ = webkitImageSmoothingEnabled; 617 | } 618 | }, 619 | 620 | // Methods || https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingcontext2d#Methods 621 | arc: { //RETURN/ void //IN/ in float x, in float y, in float radius, in float startAngle, in float endAngle, in boolean anticlockwise Optional 622 | enumerable: true, 623 | value: function (x, y, radius, startAngle, endAngle, anticlockwise) { 624 | return rdp(`${this._uid}.arc(${Array.prototype.slice.call(arguments, 0).join(',')})`); 625 | } 626 | }, 627 | 628 | arcTo: { //RETURN/ void //IN/ in float x1, in float y1, in float x2, in float y2, in float radius 629 | enumerable: true, 630 | value: function (x1, y1, x2, y2, radius) { 631 | return rdp(`${this._uid}.arcTo(${x1},${y1},${x2},${y2},${radius})`); 632 | } 633 | }, 634 | 635 | beginPath: { //RETURN/ void //IN/ 636 | enumerable: true, 637 | value: function () { 638 | return rdp(`${this._uid}.beginPath()`); 639 | } 640 | }, 641 | 642 | bezierCurveTo: { //RETURN/ void //IN/ in float cp1x, in float cp1y, in float cp2x, in float cp2y, in float x, in float y 643 | enumerable: true, 644 | value: function (cp1x, cp1y, cp2x, cp2y, x, y) { 645 | return rdp(`${this._uid}.bezierCurveTo(${cp1x},${cp1y},${cp2x},${cp2y},${x},${y})`); 646 | } 647 | }, 648 | 649 | clearRect: { //RETURN/ void //IN/ in float x, in float y, in float width, in float height 650 | enumerable: true, 651 | value: function (x, y, width, height) { 652 | return rdp(`${this._uid}.clearRect(${x},${y},${width},${height})`); 653 | } 654 | }, 655 | 656 | clip: { //RETURN/ void //IN/ 657 | enumerable: true, 658 | value: function () { 659 | return rdp(`${this._uid}.clip()`); 660 | } 661 | }, 662 | 663 | closePath: { //RETURN/ void //IN/ 664 | enumerable: true, 665 | value: function () { 666 | return rdp(`${this._uid}.closePath()`); 667 | } 668 | }, 669 | 670 | createImageData: { //RETURN/ ImageData //IN/ in float width, in float height 671 | enumerable: true, 672 | value: function (width, height) { 673 | 674 | if (width.height != undefined) { // if image data is passed 675 | height = width.height; 676 | width = width.width; 677 | } 678 | 679 | return (callback) => { 680 | callback(null, { 681 | data: new Uint8ClampedArray(Array.apply(null, new Array(width * height * 4)).map(Number.prototype.valueOf, 0)), 682 | width: width, 683 | height: height 684 | }); 685 | }; 686 | } 687 | }, 688 | 689 | createLinearGradient: { //RETURN/ nsIDOMCanvasGradient //IN/ in float x0, in float y0, in float x1, in float y1 690 | enumerable: true, 691 | value: function (x0, y0, x1, y1) { 692 | var uid = NCC.uid('linearGradient') 693 | rdp(`var ${uid} = ${this._uid}.createLinearGradient(${x0},${y0},${x1},${y1})`); 694 | 695 | var linearGradient: any = (callback?) => { 696 | rdp(callback ? (err, res) => { 697 | err ? callback(err, null) : callback(null, linearGradient); 698 | } : undefined); 699 | return linearGradient; 700 | } 701 | 702 | GradientPDM._uid.value = uid; 703 | Object.defineProperties(linearGradient, GradientPDM); 704 | GradientPDM._uid.value = ''; 705 | return linearGradient; 706 | } 707 | }, 708 | 709 | createPattern: { //RETURN/ nsIDOMCanvasPattern //IN/ in nsIDOMHTMLElement image, in DOMString repetition 710 | enumerable: true, 711 | value: function (image, repetition) { 712 | 713 | var uid = NCC.uid('pattern'); 714 | rdp(`var ${uid} = ${this._uid}.createPattern(${image._uid},'${repetition}')`); 715 | 716 | var pattern: any = (callback?) => { 717 | rdp(callback ? (err, res) => { 718 | err ? callback(err, null) : callback(null, pattern); 719 | } : undefined); 720 | return pattern; 721 | } 722 | 723 | PatternPDM._uid.value = uid; 724 | Object.defineProperties(pattern, PatternPDM); 725 | PatternPDM._uid.value = ''; 726 | 727 | return pattern; 728 | } 729 | }, 730 | 731 | createRadialGradient: { //RETURN/ nsIDOMCanvasGradient //IN/ in float x0, in float y0, in float r0, in float x1, in float y1, in float r1 732 | enumerable: true, 733 | value: function (x0, y0, r0, x1, y1, r1) { 734 | 735 | var uid = NCC.uid('pattern') 736 | rdp(`var ${uid} = ${this._uid}.createRadialGradient(${x0},${y0},${r0},${x1},${y1},${r1})`); 737 | 738 | var radialGradient: any = (callback?) => { 739 | rdp(callback ? (err, res) => { 740 | err ? callback(err, null) : callback(null, radialGradient); 741 | } : undefined); 742 | return radialGradient; 743 | } 744 | 745 | GradientPDM._uid.value = NCC.uid('radialGradient'); 746 | Object.defineProperties(radialGradient, GradientPDM); 747 | GradientPDM._uid.value = ''; 748 | 749 | return radialGradient; 750 | } 751 | }, 752 | 753 | drawImage: { //RETURN/ void //IN/ in nsIDOMElement image, in float a1, in float a2, in float a3 Optional, in float a4 Optional, in float a5 Optional, in float a6 Optional, in float a7 Optional, in float a8 Optional 754 | enumerable: true, 755 | value: function (image, a1, a2, a3, a4, a5, a6, a7, a8) { 756 | return rdp(`${this._uid}.drawImage(${image._uid}, ${Array.prototype.slice.call(arguments, 1).join(',')})`); 757 | } 758 | }, 759 | 760 | // no use 761 | //drawCustomFocusRing: { //RETURN/ boolean //IN/ Element element 762 | // enumerable:true, 763 | // value: function (element) { 764 | // rdp(`${this._uid}.drawCustomFocusRing(" + element + ")`); 765 | // return this; 766 | // } 767 | //}, 768 | 769 | // no use 770 | //drawSystemFocusRing: { //RETURN/ void //IN/ Element element 771 | // enumerable:true, 772 | // value: function (element) { 773 | // rdp(`${this._uid}.drawSystemFocusRinelementg()`); 774 | // return this; 775 | // } 776 | //}, 777 | 778 | fill: { //RETURN/ void //IN/ 779 | enumerable: true, 780 | value: function () { 781 | return rdp(`${this._uid}.fill()`); 782 | } 783 | }, 784 | 785 | fillRect: { //RETURN/ void //IN/ in float x, in float y, in float width, in float height 786 | enumerable: true, 787 | value: function (x, y, width, height) { 788 | return rdp(`${this._uid}.fillRect(${x},${y},${width},${height})`); 789 | } 790 | }, 791 | 792 | fillText: { //RETURN/ void //IN/ in DOMString text, in float x, in float y, in float maxWidth Optional 793 | enumerable: true, 794 | value: function (text, x, y, maxWidth) { 795 | return rdp(`${this._uid}.fillText('${text}',${Array.prototype.slice.call(arguments, 1).join(',')})`); 796 | } 797 | }, 798 | 799 | getImageData: { //RETURN/ //IN/ in float x, in float y, in float width, in float height 800 | enumerable: true, 801 | value: function (x, y, width, height) { 802 | rdp(`Array.prototype.slice.call(${this._uid}.getImageData(${x},${y},${width},${height}).data).join(',')`); 803 | return (callback) => { 804 | rdp((err, res) => { 805 | if (err) 806 | return callback(err, null); 807 | 808 | var imageData = { 809 | data: new Uint8ClampedArray(res.value.split(',')), 810 | width: width, 811 | height: height 812 | }; 813 | 814 | callback(null, imageData); 815 | }); 816 | }; 817 | } 818 | }, 819 | 820 | getLineDash: { //RETURN/ sequence //IN/ 821 | enumerable: true, 822 | value: function () { 823 | rdp(`${this._uid}.getLineDash().join(',')`); 824 | return (callback) => { 825 | rdp((err, res) => { 826 | if (err) 827 | return callback(err); 828 | 829 | res.value = res.value.split(','); 830 | for (var i = 0, l = res.value.length; i < l; i++) 831 | res.value[i] = +res.value[i]; 832 | 833 | callback(err, res.value); 834 | }); 835 | }; 836 | } 837 | }, 838 | 839 | isPointInPath: { //RETURN/ boolean //IN/ in float x, in float y 840 | enumerable: true, 841 | value: function (x, y) { 842 | rdp(`${this._uid}.isPointInPath(${x},${y})`); 843 | return (callback) => { 844 | rdp((err, res) => { 845 | callback(err, res.value); 846 | }); 847 | }; 848 | } 849 | }, 850 | 851 | isPointInStroke: { //RETURN/ boolean //IN/ in float x, in float y 852 | enumerable: true, 853 | value: function (x, y) { 854 | rdp(`${this._uid}.isPointInStroke(${x},${y})`); 855 | return (callback) => { 856 | rdp((err, res) => { 857 | callback(err, res.value); 858 | }); 859 | }; 860 | } 861 | }, 862 | 863 | lineTo: { //RETURN/ void //IN/ in float x, in float y 864 | enumerable: true, 865 | value: function (x, y) { 866 | return rdp(`${this._uid}.lineTo(${x},${y})`); 867 | } 868 | }, 869 | 870 | measureText: { //RETURN/ nsIDOMTextMetrics //IN/ in DOMString text 871 | enumerable: true, 872 | value: function (text) { 873 | rdp(`${this._uid}.measureText('${text}').width`); 874 | return (callback) => { 875 | rdp((err, res) => { 876 | if (err) 877 | return callback(err); 878 | 879 | callback(null, { width: res.value }); 880 | }); 881 | }; 882 | } 883 | }, 884 | 885 | moveTo: { //RETURN/ void //IN/ in float x, in float y 886 | enumerable: true, 887 | value: function (x, y) { 888 | return rdp(`${this._uid}.moveTo(${x},${y})`); 889 | } 890 | }, 891 | 892 | putImageData: { //RETURN/ void //IN/ in ImageData imagedata, in float dx, double dy, in float dirtyX Optional, in float dirtyY Optional, in float dirtyWidth Optional, in float dirtyHeight Optional 893 | enumerable: true, 894 | value: function (imagedata, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight) { 895 | return rdp(`var data = [${Array.prototype.slice.call(imagedata.data).join(',')}]; var iD = ${this._uid}.createImageData(${imagedata.width}, ${imagedata.height}); for (var i = 0, l = iD.data.length; i < l; i++) iD.data[i] = +data[i]; ${this._uid}.putImageData(iD, ${Array.prototype.slice.call(arguments, 1).join(',')})`); 896 | } 897 | }, 898 | 899 | quadraticCurveTo: { //RETURN/ void //IN/ in float cpx, in float cpy, in float x, in float y 900 | enumerable: true, 901 | value: function (cpx, cpy, x, y) { 902 | return rdp(`${this._uid}.quadraticCurveTo(${cpx},${cpy},${x},${y})`); 903 | } 904 | }, 905 | 906 | rect: { //RETURN/ void //IN/ in float x, in float y, in float width, in float height 907 | enumerable: true, 908 | value: function (x, y, width, height) { 909 | return rdp(`${this._uid}.rect(${x},${y},${width},${height})`); 910 | } 911 | }, 912 | 913 | restore: { //RETURN/ void //IN/ 914 | enumerable: true, 915 | value: function () { 916 | return rdp(`${this._uid}.restore()`); 917 | } 918 | }, 919 | 920 | rotate: { //RETURN/ void //IN/ in float angle 921 | enumerable: true, 922 | value: function (angle) { 923 | return rdp(`${this._uid}.rotate(${angle})`); 924 | } 925 | }, 926 | 927 | save: { //RETURN/ void //IN/ 928 | enumerable: true, 929 | value: function () { 930 | return rdp(`${this._uid}.save()`); 931 | } 932 | }, 933 | 934 | scale: { //RETURN/ void //IN/ in float x, in float y 935 | enumerable: true, 936 | value: function (x, y) { 937 | return rdp(`${this._uid}.scale(${x},${y})`); 938 | } 939 | }, 940 | 941 | // no use 942 | //scrollPathIntoView: { //RETURN/ void //IN/ 943 | // enumerable: true, 944 | // value: function () { 945 | // rdp(`${this._uid}.scrollPathIntoView()`); 946 | // return this; 947 | // } 948 | //}, 949 | 950 | setLineDash: { //RETURN/ void //IN/ in sequence segments 951 | enumerable: true, 952 | value: function (segments) { 953 | return rdp(`${this._uid}.setLineDash([${segments.join(',')}])`); 954 | } 955 | }, 956 | 957 | setTransform: { //RETURN/ void //IN/ in float m11, in float m12, in float m21, in float m22, in float dx, in float dy 958 | enumerable: true, 959 | value: function (m11, m12, m21, m22, dx, dy) { 960 | return rdp(`${this._uid}.setTransform(${m11},${m12},${m21},${m22},${dx},${dy})`); 961 | } 962 | }, 963 | 964 | stroke: { //RETURN/ void //IN/ 965 | enumerable: true, 966 | value: function () { 967 | return rdp(`${this._uid}.stroke()`); 968 | } 969 | }, 970 | 971 | strokeRect: { //RETURN/ void //IN/ in float x, in float y, in float w, in float h 972 | enumerable: true, 973 | value: function (x, y, w, h) { 974 | return rdp(`${this._uid}.strokeRect(${x},${y},${w},${h})`); 975 | } 976 | }, 977 | 978 | strokeText: { //RETURN/ void //IN/ in DOMString text, in float x, in float y, in float maxWidth Optional 979 | enumerable: true, 980 | value: function (text, x, y, maxWidth) { 981 | rdp(`${this._uid}.strokeText('${text}',${(Array.prototype.slice.call(arguments, 1).join(','))})`); 982 | return this; 983 | } 984 | }, 985 | 986 | transform: { //RETURN/ void //IN/ in float m11, in float m12, in float m21, in float m22, in float dx, in float dy 987 | enumerable: true, 988 | value: function (m11, m12, m21, m22, dx, dy) { 989 | return rdp(`${this._uid}.transform(${m11},${m12},${m21},${m22},${dx},${dy})`); 990 | } 991 | }, 992 | 993 | translate: { //RETURN/ void //IN/ in float x, in float y 994 | enumerable: true, 995 | value: function (x, y) { 996 | return rdp(`${this._uid}.translate(${x},${y})`); 997 | } 998 | } 999 | 1000 | }; 1001 | 1002 | 1003 | // Gradient 1004 | interface Gradient extends ProxyObj { 1005 | (callback?): Gradient; 1006 | addColorStop(offset: number, color: string): Callback; 1007 | } 1008 | 1009 | var GradientPDM = { 1010 | 1011 | // private properties 1012 | _uid: { 1013 | enumerable: DEBUG, 1014 | value: '' 1015 | }, 1016 | _remote: { 1017 | enumerable: DEBUG, 1018 | set: function (null_) { 1019 | if (null_ === null) { 1020 | rdp(`${this._uid} = null`); 1021 | Object.defineProperty(this, '_uid', { value: null }); 1022 | } else 1023 | logger.error('"_remote" can only be set to "null"') 1024 | } 1025 | }, 1026 | 1027 | // Web API: https://developer.mozilla.org/en-US/docs/Web/API/CanvasGradient 1028 | 1029 | // Methods 1030 | 1031 | addColorStop: { 1032 | enumerable: true, 1033 | value: function (offset, color) { 1034 | return rdp(`${this._uid}.addColorStop(${offset},'${color}')`); 1035 | } 1036 | } 1037 | }; 1038 | 1039 | 1040 | // Pattern 1041 | interface Pattern extends ProxyObj { 1042 | (callback?): Pattern; 1043 | } 1044 | 1045 | var PatternPDM = { 1046 | 1047 | // private properties 1048 | _uid: { 1049 | enumerable: DEBUG, 1050 | value: '' 1051 | }, 1052 | _remote: { 1053 | enumerable: DEBUG, 1054 | set: function (null_) { 1055 | if (null_ === null) { 1056 | rdp(`${this._uid} = null`); 1057 | Object.defineProperty(this, '_uid', { value: null }); 1058 | } else 1059 | logger.error('"_remote" can only be set to "null"') 1060 | } 1061 | }, 1062 | 1063 | // Web API: https://developer.mozilla.org/en-US/docs/Web/API/CanvasPattern 1064 | }; 1065 | 1066 | 1067 | // Image 1068 | interface Image extends ProxyObj { 1069 | (callback?): Image; 1070 | _base64: string; 1071 | _toFile(): any; 1072 | src: string; 1073 | onload(): any; 1074 | onerror(): any; 1075 | width: number; 1076 | height: number; 1077 | } 1078 | 1079 | var mimeMap = { 1080 | png: 'image/png', 1081 | webp: 'image/webp', 1082 | jpeg: 'image/jpeg', 1083 | jpg: 'image/jpeg', 1084 | svg: 'image/svg+xml', 1085 | gif: 'image/gif' 1086 | }; 1087 | 1088 | var regExp_http = new RegExp('^(http:\\/\\/.+)', 'i'); 1089 | var regExp_data = new RegExp('^(data:image\\/\\w+;base64,.+)'); 1090 | var regExp_type = new RegExp('^data:image\\/(\\w+);base64,'); 1091 | 1092 | 1093 | var ImagePDM = { 1094 | 1095 | // private properties 1096 | _uid: { 1097 | enumerable: DEBUG, 1098 | value: '' 1099 | }, 1100 | _remote: { 1101 | enumerable: DEBUG, 1102 | set: function (null_) { 1103 | if (null_ === null) { 1104 | rdp(`${this._uid} = null`); 1105 | Object.defineProperty(this, '_uid', { value: null }); 1106 | } else 1107 | logger.error('"_remote" can only be set to "null"') 1108 | } 1109 | }, 1110 | 1111 | // Properties 1112 | src_: { 1113 | enumerable: DEBUG, 1114 | writable: true, 1115 | value: '' 1116 | }, 1117 | width_: { 1118 | enumerable: DEBUG, 1119 | writable: true, 1120 | value: undefined 1121 | }, 1122 | height_: { 1123 | enumerable: DEBUG, 1124 | writable: true, 1125 | value: undefined 1126 | }, 1127 | _base64_: { 1128 | enumerable: DEBUG, 1129 | writable: true, 1130 | value: null 1131 | }, 1132 | _base64: { 1133 | enumerable: DEBUG, 1134 | get: function () { 1135 | return this._base64_; 1136 | }, 1137 | set: function (base64) { 1138 | rdp(`${this._uid}.src = '${base64}'`); 1139 | rdp(() => { 1140 | rdp(`${this._uid}.width + '_' + ${this._uid}.height`); 1141 | rdp((err, res) => { 1142 | if (err && this.onerror) 1143 | return this.onerror(err); 1144 | 1145 | var size = res.value.split('_'); 1146 | this.width_ = +size[0]; 1147 | this.height_ = +size[1]; 1148 | 1149 | if (this.onload) 1150 | return this.onload(this); 1151 | }); 1152 | }); 1153 | 1154 | this._base64_ = base64; 1155 | return this._base64_; 1156 | } 1157 | }, 1158 | 1159 | // Methods 1160 | _toFile: { 1161 | enumerable: DEBUG, 1162 | value: function (filename, callback) { 1163 | var head = regExp_type.exec(this._base64_), 1164 | type = filename.split('.').pop(); 1165 | 1166 | 1167 | if (!head || !head[1] || (head[1] != ((type == "jpg") ? "jpeg" : type))) 1168 | if (callback) return callback(`type mismatch ${head ? head[1] : "'unknown'"} !> ${type}`); 1169 | else throw new Error(`type mismatch ${head ? head[1] : "'unknown'"} !> ${type}`) 1170 | 1171 | logger.info(`[ncc] writing image to: ${filename}`); 1172 | fs.writeFile(filename, new Buffer(this._base64_.replace(/^data:image\/\w+;base64,/, ''), 'base64'), {}, callback); 1173 | } 1174 | }, 1175 | 1176 | // Web API 1177 | 1178 | // Properties 1179 | src: { 1180 | enumerable: true, 1181 | get: function () { 1182 | return this.src_; 1183 | }, 1184 | set: function (src) { 1185 | var img = this; 1186 | this._src = src; 1187 | if (!src || src === '') return; 1188 | 1189 | if (regExp_data.test(src)) img._base64 = src; 1190 | else if (regExp_http.test(src)) { 1191 | logger.info(`[ncc] loading image from URL: ${src}`); 1192 | http.get(src, function (res) { 1193 | var data = ''; 1194 | res.setEncoding('base64'); 1195 | 1196 | if (res.statusCode != 200) { 1197 | if (img.onerror) return img.onerror(`loading image failed with status ${res.statusCode}`); 1198 | else logger.error(`loading image failed with status ${res.statusCode}`); 1199 | } 1200 | 1201 | res.on('data', function (chunk) { data += chunk; }); 1202 | 1203 | res.on('end', function () { 1204 | img._base64 = `data:${(res.headers["content-type"] || mimeMap[src.split('.').pop()])};base64,${data}`; 1205 | logger.info('[ncc] loading image from URL completed'); 1206 | }); 1207 | 1208 | }).on('error', this.onerror || function (err) { 1209 | if (img.onerror) return img.onerror(err); 1210 | else logger.error(`loading image failed with err ${err}`); 1211 | }); 1212 | } else { 1213 | logger.info(`[ncc] loading image from FS: ${src}`); 1214 | fs.readFile(src, 'base64', function (err, data) { 1215 | if (err) { 1216 | if (img.onerror) img.onerror(err); 1217 | else logger.error(`loading image failed with err ${err}`); 1218 | } 1219 | img._base64 = `data:${mimeMap[src.split('.').pop()]};base64,${data}`; 1220 | logger.info('[ncc] loading image from FS completed'); 1221 | }); 1222 | } 1223 | return this.src_; 1224 | } 1225 | }, 1226 | onload: { 1227 | writable: true, 1228 | enumerable: true, 1229 | value: undefined 1230 | }, 1231 | onerror: { 1232 | writable: true, 1233 | enumerable: true, 1234 | value: undefined 1235 | }, 1236 | width: { 1237 | enumerable: true, 1238 | get: function () { 1239 | return this.width_; 1240 | } 1241 | }, 1242 | height: { 1243 | enumerable: true, 1244 | get: function () { 1245 | return this.height_; 1246 | } 1247 | } 1248 | 1249 | }; 1250 | 1251 | 1252 | module.exports = NCC; -------------------------------------------------------------------------------- /lib/stripe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/indus/ncc/6fc9b86421ae4b3132b0b7df406c756bf4e6f890/lib/stripe.png -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | ncc is released under the terms of the [MIT license](http://en.wikipedia.org/wiki/MIT_License). 2 | 3 | The MIT License is simple and easy to understand and it places almost no restrictions on what you can do with ncc. 4 | 5 | You are free to use ncc in any other project (even commercial projects) as long as the copyright header is left intact. 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Stefan Keim (indus)", 3 | "name": "ncc", 4 | "description": "node-chrome-canvas | a simple to use and performant HTML5 canvas for Node.js", 5 | "version": "0.3.6", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "dev": "run-p dev:*", 9 | "dev:tsc_lib": "tsc -w --target ES6 --outfile lib/ncc.js lib/ncc.ts", 10 | "dev:run": "nodemon index.js" 11 | }, 12 | "dependencies": { 13 | "mkdirp": "^0.5.1", 14 | "rimraf": "^2.6.1", 15 | "tracer": "^0.8.7", 16 | "ws": "^2.3.1" 17 | }, 18 | "devDependencies": { 19 | "@types/node": "^7.0.18" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git://github.com/indus/ncc.git" 24 | }, 25 | "bugs": { 26 | "url": "https://github.com/indus/ncc2/issues" 27 | }, 28 | "keywords": [ 29 | "canvas", 30 | "chrome", 31 | "draw", 32 | "image", 33 | "images", 34 | "graphic", 35 | "graphics", 36 | "gif", 37 | "png", 38 | "webp", 39 | "jpg", 40 | "jpeg" 41 | ], 42 | "homepage": "https://github.com/indus/ncc2#readme", 43 | "license": "MIT" 44 | } 45 | --------------------------------------------------------------------------------