├── .github
└── workflows
│ └── ci-workflow.yaml
├── .gitignore
├── .nvmrc
├── client.js
├── demo.client
├── index.html
├── lamborghini.gif
└── parrot.jpg
├── demo.server
├── app.js
├── lamborghini.gif
└── parrot.jpg
├── dist
├── ditherjs.dist.js
└── jquery.ditherjs.dist.js
├── jquery.js
├── lib
├── algorithms
│ ├── atkinsonDither.js
│ ├── errorDiffusionDither.js
│ └── orderedDither.js
├── client.js
├── ditherjs.js
├── server.js
└── utils.js
├── package.json
├── readme.md
├── server.js
├── spec
├── ditherjs.spec.js
└── hsl.jpg
└── webpack.config.js
/.github/workflows/ci-workflow.yaml:
--------------------------------------------------------------------------------
1 | name: Run the specs
2 | on: [push, workflow_dispatch]
3 |
4 | jobs:
5 | ci:
6 | name: Test and coverage
7 | runs-on: ubuntu-latest
8 |
9 | strategy:
10 | matrix:
11 | node-version: [12, 13, 14]
12 |
13 | steps:
14 | - uses: actions/checkout@v2
15 |
16 | - name: Set up Node ${{ matrix.node-version }}
17 | uses: actions/setup-node@v1
18 | with:
19 | node-version: ${{ matrix.node-version }}
20 |
21 | - name: Install platform dependencies
22 | run: sudo apt-get install -y libcairo2-dev libjpeg8-dev libpango1.0-dev libgif-dev
23 |
24 | - name: Install project dependencies
25 | run: |
26 | npm explore npm -g -- npm install node-gyp@latest
27 | npm install
28 | npm install canvas # optional, needed for server tests
29 |
30 | - name: Run tests
31 | run: npm run coverage
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.swp
2 | node_modules
3 | coverage
4 | .nyc_output
5 | **/npm-debug.log
6 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v10.10.0
2 |
--------------------------------------------------------------------------------
/client.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./lib/client');
2 |
--------------------------------------------------------------------------------
/demo.client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Test
5 |
10 |
11 |
12 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/demo.client/lamborghini.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielepiccone/ditherjs/aa719357cdccd8384c2ed4323c2636ba0babe799/demo.client/lamborghini.gif
--------------------------------------------------------------------------------
/demo.client/parrot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielepiccone/ditherjs/aa719357cdccd8384c2ed4323c2636ba0babe799/demo.client/parrot.jpg
--------------------------------------------------------------------------------
/demo.server/app.js:
--------------------------------------------------------------------------------
1 | var http = require("http");
2 | var fs = require('fs');
3 | var path = require('path');
4 | var DitherJs = require('../server.js');
5 |
6 | var server = http.createServer(function(req, res) {
7 | fs.readFile(path.resolve(__dirname, "parrot.jpg"), function (err, data) {
8 | if (err) {
9 | throw err;
10 | }
11 | res.writeHead(200, {"Content-Type": "image/jpeg"});
12 | var options = {
13 | step: 3
14 | };
15 | res.write(new DitherJs(options).dither(data));
16 | res.end();
17 | });
18 | });
19 |
20 |
21 | server.listen(8081);
22 | console.log('Demo running on port 8081');
23 |
--------------------------------------------------------------------------------
/demo.server/lamborghini.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielepiccone/ditherjs/aa719357cdccd8384c2ed4323c2636ba0babe799/demo.server/lamborghini.gif
--------------------------------------------------------------------------------
/demo.server/parrot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielepiccone/ditherjs/aa719357cdccd8384c2ed4323c2636ba0babe799/demo.server/parrot.jpg
--------------------------------------------------------------------------------
/dist/ditherjs.dist.js:
--------------------------------------------------------------------------------
1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.DitherJS=e():t.DitherJS=e()}(this,function(){return function(t){function e(o){if(r[o])return r[o].exports;var n=r[o]={exports:{},id:o,loaded:!1};return t[o].call(n.exports,n,n.exports,e),n.loaded=!0,n.exports}var r={};return e.m=t,e.c=r,e.p="",e(0)}([function(t,e,r){t.exports=r(4)},function(t,e){function r(t,e,r,o,n){for(var i,a,s,l,f,p,c,h,u,y,d,m,x,g,v=new Uint8ClampedArray(t),w=new Uint8ClampedArray(t),C=1/8,D=function(t,e){return 4*t+4*e*n},E=0;E",
6 | "main": "dist/ditherjs.dist.js",
7 | "homepage": "https://dpiccone.github.io/ditherjs/",
8 | "repository": {
9 | "type": "git",
10 | "url": "git@github.com:dpiccone/ditherjs.git"
11 | },
12 | "scripts": {
13 | "preversion": "webpack -p",
14 | "test": "mocha spec/",
15 | "coverage": "nyc mocha spec/",
16 | "demo:client": "webpack-dev-server --content-base demo.client",
17 | "demo:server": "node demo.server/app.js"
18 | },
19 | "keywords": [
20 | "graphic",
21 | "graphics",
22 | "palette",
23 | "palettes",
24 | "pattern",
25 | "color",
26 | "dithering",
27 | "pixel"
28 | ],
29 | "devDependencies": {
30 | "domino": "1.0.25",
31 | "express": "4.14.0",
32 | "mocha": "3.0.2",
33 | "nyc": "8.3.0",
34 | "unexpected": "10.16.0",
35 | "webpack": "1.13.2",
36 | "webpack-dev-server": "1.15.0"
37 | },
38 | "browser": {
39 | "canvas": false
40 | },
41 | "license": "CC-BY-SA-4.0"
42 | }
43 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # ditherJS
2 |
3 | [](http://creativecommons.org/licenses/by-sa/4.0/)
4 |
5 | A javascript library which dithers an image using a fixed palette.
6 |
7 | Run `npm run demo:client` or `npm run demo:sever` to see it in action.
8 |
9 | ## Installation and dependencies
10 |
11 | ```sh
12 | $ npm install ditherjs --save
13 | ```
14 |
15 | Both client and server are exposed as commonJS modules to be used with webpack or browserify.
16 |
17 | The client-side version is also published with an UMD compatible wrapper and a jQuery plugin, those versions are in `./dist`
18 |
19 | The server-side version needs [node-canvas](https://github.com/Automattic/node-canvas) installed as a peer dependency to work, this is also needed to run run the tests during development.
20 |
21 | ```sh
22 | $ npm install ditherjs canvas --save
23 | ```
24 |
25 | ## Usage and options
26 |
27 | Any DitherJS instance exposes a `dither(target, [options])` method which accepts a *selector* a *Node
* or a *buffer* as a target and an optional options object.
28 |
29 | The options can be passed directly to the method or directly in the constructor.
30 |
31 | ```javascript
32 | var options = {
33 | "step": 1, // The step for the pixel quantization n = 1,2,3...
34 | "palette": defaultPalette, // an array of colors as rgb arrays
35 | "algorithm": "ordered" // one of ["ordered", "diffusion", "atkinson"]
36 | };
37 | ```
38 |
39 | A default palette is provided which is CGA Palette 1
40 |
41 | 
42 |
43 | The palette structure is as an array of rgb colors `[[r,g,b]..]`
44 |
45 | ### Client
46 |
47 |
48 | ```javascript
49 | var DitherJS = require('ditherjs');
50 |
51 | var ditherjs = new DitherJS([,options]);
52 | ditherjs.dither(selector,[,options]); // should target
elements
53 | ```
54 |
55 | as a jQuery plugin
56 | ```javascript
57 | $('.dither').ditherJS(options);
58 | ```
59 |
60 | or directly on the element
61 | ```html
62 |
63 | ```
64 |
65 | ## Server
66 |
67 | ```javascript
68 | var DitherJS = require('ditherjs/server');
69 |
70 | var ditherjs = new DitherJS([,options]);
71 |
72 | // Get a buffer that can be loaded into a canvas
73 | var buffer = fs.readFileSync('./myBeautifulFile.jpg|gif|png');
74 |
75 | ditherjs.dither(buffer,[,options]);
76 | ```
77 |
78 | ### Testimonials
79 |
80 | Useful as a comb to a bald man. -Anon
81 |
82 | author 2014 [Daniele Piccone](http://www.danielepiccone.com)
83 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./lib/server');
2 |
--------------------------------------------------------------------------------
/spec/ditherjs.spec.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs');
2 | var unexpected = require('unexpected');
3 | var expect = unexpected.clone();
4 |
5 | var utils = require('../lib/utils.js');
6 | var noop = function () {};
7 |
8 | describe('ditherjs', function () {
9 | [require('../server.js'),require('../client.js')].forEach(function (DitherJS) {
10 |
11 | it('should expose the dithering algorithms', function () {
12 | expect(DitherJS.orderedDither, 'to be defined');
13 | expect(DitherJS.atkinsonDither, 'to be defined');
14 | expect(DitherJS.errorDiffusionDither, 'to be defined');
15 | });
16 |
17 | it('should expose the dither() method', function () {
18 | expect(DitherJS.prototype.dither, 'to be defined');
19 | });
20 |
21 | describe('constructor', function () {
22 | var ditherjs = new DitherJS();
23 |
24 | it('should get all the defaults with ', function () {
25 | expect(ditherjs.options.algorithm, 'to be', 'ordered');
26 | expect(ditherjs.options.step, 'to be', 1);
27 | expect(ditherjs.options.className, 'to be', 'dither');
28 | expect(ditherjs.options.palette, 'to be', utils.defaultPalette);
29 | });
30 | });
31 |
32 | describe('ditherImageData', function () {
33 | var ditherjs = new DitherJS();
34 |
35 | var mockImageData = {
36 | data: [],
37 | height: 0,
38 | width: 0
39 | };
40 | mockImageData.data.set = noop;
41 |
42 | it('should accept all the algorithms', function () {
43 | ['atkinson','diffusion','ordered'].forEach(function (algorithm) {
44 | var options = { algorithm: algorithm };
45 | ditherjs.ditherImageData(mockImageData, options);
46 | });
47 | });
48 |
49 | it('should throw an error for an unknown algorithm', function () {
50 | var options = { algorithm: 'mo' };
51 | try {
52 | ditherjs.ditherImageData(mockImageData, options);
53 | } catch (err) {
54 | expect(err, 'to be', utils.errors.InvalidAlgorithm);
55 | }
56 | });
57 |
58 | it('should mutate the imageData', function () {
59 | var called = false;
60 | mockImageData.data.set = function () { called = true;};
61 | ditherjs.ditherImageData(mockImageData);
62 | expect(called, 'to be true');
63 | });
64 | });
65 |
66 | describe('colorDistance', function () {
67 | var ditherjs = new DitherJS();
68 | var fn = ditherjs.colorDistance;
69 |
70 | it('should calculate the euclidean distance between colors', function () {
71 | expect(fn([1,0,0],[0,0,0]),'to be', 1);
72 | expect(fn([1,1,0],[1,1,0]),'to be', 0);
73 | expect(fn([1,0,0],[0,0,1]),'to be', 1.4142135623730951);
74 | });
75 | });
76 |
77 | describe('approximateColor', function () {
78 | var ditherjs = new DitherJS();
79 | var fn = ditherjs.approximateColor.bind(ditherjs);
80 |
81 | it('approximates the color to the closest one', function () {
82 | var palette = utils.defaultPalette;
83 | expect(fn([128,0,0], palette), 'to satisfy', [0,0,0]);
84 | expect(fn([128,0,128], palette), 'to satisfy', [255,0,255]);
85 | expect(fn([0,128,128], palette), 'to satisfy', [0,255,255]);
86 | });
87 | });
88 | });
89 |
90 | });
91 |
92 | describe('ditherjs.server', function() {
93 | var DitherJS = require('../server.js');
94 |
95 | it('should augment the base class', function () {
96 | [
97 | '_bufferToImageData',
98 | '_imageDataToBuffer'
99 | ].forEach(function (method) {
100 | expect(DitherJS.prototype.hasOwnProperty(method), 'to be', true);
101 | });
102 | });
103 |
104 | describe('_bufferToImageData', function () {
105 | var ditherjs = new DitherJS();
106 |
107 | it('should get ImageData from Buffer', function () {
108 | var buffer = fs.readFileSync(__dirname + '/hsl.jpg');
109 | var input = ditherjs._bufferToImageData(buffer);
110 | expect(input.height, 'to be defined');
111 | expect(input.width, 'to be defined');
112 | expect(input.data.length, 'to be', 262144);
113 | expect(input.data.constructor.name, 'to be', 'Uint8ClampedArray');
114 | });
115 | });
116 |
117 | describe('_imageDataToBuffer', function () {
118 | var ditherjs = new DitherJS();
119 |
120 | it('should get Buffer from ImageData', function () {
121 | var ImageData = require('canvas').ImageData;
122 | var imageData = new ImageData(10,10);
123 |
124 | var outBuffer = ditherjs._imageDataToBuffer(imageData);
125 | expect(outBuffer.constructor.name, 'to be', 'Buffer');
126 | });
127 | });
128 | });
129 |
130 | describe('ditherjs.client', function() {
131 | var DitherJS = require('../client.js');
132 |
133 | var domino = require('domino');
134 | global.window = domino.createWindow();
135 | global.document = window.document;
136 |
137 |
138 | it('should augment the base class', function () {
139 | [
140 | '_replaceElementWithCtx',
141 | '_fromImgElement',
142 | '_fromSelector'
143 | ].forEach(function (method) {
144 | expect(DitherJS.prototype.hasOwnProperty(method), 'to be', true);
145 | });
146 | });
147 |
148 | describe('dither', function () {
149 | var ditherjs = new DitherJS();
150 |
151 | it('should call _fromImgElement if the argument is a Node', function () {
152 | var node = document.createElement('img');
153 | document.body.appendChild(node);
154 |
155 | var called = false;
156 |
157 | var mockInstance = {
158 | _fromImgElement: function () { called = true; }
159 | };
160 |
161 | ditherjs.dither.call(mockInstance, node);
162 | expect(called, 'to be', true);
163 | });
164 |
165 | it('should call _fromSelector if the argument is a string', function () {
166 | var called = false;
167 |
168 | var mockInstance = {
169 | _fromSelector: function () { called = true; }
170 | };
171 |
172 | ditherjs.dither.call(mockInstance, '.foo');
173 | expect(called, 'to be', true);
174 | });
175 | });
176 |
177 | describe('_replaceElementWithCtx', function () {
178 | var ditherjs = new DitherJS();
179 |
180 | var element = document.createElement('img');
181 | element.className = 'boo dither bar';
182 | document.body.appendChild(element);
183 |
184 | it('should get the canvas context out of the element', function () {
185 | // TODO getContext() not implemented in Domino
186 | try {
187 | ditherjs._replaceElementWithCtx(element) ;
188 | } catch (err) {
189 | expect(err, 'to be defined');
190 | }
191 | });
192 | });
193 |
194 | describe('_fromImgElement', function () {
195 | var ditherjs = new DitherJS();
196 |
197 | var element = document.createElement('img');
198 | document.body.appendChild(element);
199 |
200 | it('should get the image, process it and put that in the context', function () {
201 | var MOCK_DATA = ['panda'];
202 |
203 | ditherjs.ditherImageData = function (data) {
204 | expect(data, 'to be', MOCK_DATA);
205 | data.push('mango');
206 | };
207 |
208 | ditherjs._replaceElementWithCtx = function () {
209 | return {
210 | drawImage: noop,
211 | getImageData: function () { return MOCK_DATA; },
212 | putImageData: function (data) {
213 | return expect(data, 'to satisfy', ['panda','mango']);
214 | }
215 | };
216 | };
217 |
218 | ditherjs._fromImgElement(element);
219 | });
220 | });
221 |
222 | describe('_fromSelector', function () {
223 | var ditherjs = new DitherJS();
224 |
225 | var element = document.createElement('img');
226 | element.className = 'mommo';
227 | document.body.appendChild(element);
228 |
229 | it('should call _fromImgElement on the element', function () {
230 | var mockDitherCalled = false;
231 | var mockDither = {
232 | _fromImgElement: function () { mockDitherCalled = true; }
233 | };
234 |
235 | ditherjs._fromSelector.call(mockDither,'.mommo');
236 |
237 | element.onload();
238 | expect(mockDitherCalled, 'to be', true);
239 | });
240 | });
241 |
242 | });
243 |
244 |
245 | describe('algorithms', function () {
246 | var buffer = fs.readFileSync(__dirname + '/hsl.jpg');
247 | var DitherJS = require('../server.js'); // also ../client
248 | var ditherjs = new DitherJS();
249 |
250 | describe('ordered dither', function () {
251 | it('should work', function () {
252 | var input = ditherjs._bufferToImageData(buffer);
253 |
254 | var output = DitherJS.orderedDither.call(ditherjs, input.data, utils.defaultPalette, 1, input.width, input.height);
255 |
256 | var test = Array.prototype.slice.call(output,0, 128);
257 |
258 | expect(
259 | test,
260 | 'to satisfy',
261 | [ 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 ]
262 | );
263 | });
264 | });
265 |
266 | describe('atkinson dither', function () {
267 | it('should work', function () {
268 | var input = ditherjs._bufferToImageData(buffer);
269 |
270 | var output = DitherJS.atkinsonDither.call(ditherjs, input.data, utils.defaultPalette, 1, input.width, input.height);
271 |
272 | var test = Array.prototype.slice.call(output,0, 128);
273 |
274 | expect(
275 | test,
276 | 'to satisfy',
277 | [ 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 0, 255, 255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 ]
278 | );
279 | });
280 | });
281 |
282 | describe('error diffusion dither', function () {
283 | it('should work', function () {
284 | var input = ditherjs._bufferToImageData(buffer);
285 |
286 | var output = DitherJS.errorDiffusionDither.call(ditherjs, input.data, utils.defaultPalette, 1, input.width, input.height);
287 |
288 | var test = Array.prototype.slice.call(output,0, 128);
289 |
290 | expect(
291 | test,
292 | 'to satisfy',
293 | [ 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 0, 255, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 ]
294 | );
295 | });
296 | });
297 | });
298 |
--------------------------------------------------------------------------------
/spec/hsl.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielepiccone/ditherjs/aa719357cdccd8384c2ed4323c2636ba0babe799/spec/hsl.jpg
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | entry: {
3 | "ditherjs": "./client.js",
4 | "jquery.ditherjs": "./jquery.js",
5 | },
6 | output: {
7 | path: "./dist",
8 | filename: "[name].dist.js",
9 | library: 'DitherJS',
10 | libraryTarget: 'umd'
11 | }
12 | };
13 |
--------------------------------------------------------------------------------