├── .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 |
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 |
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 |
--------------------------------------------------------------------------------