├── .eslintrc.json
├── .project
├── .settings
├── .jsdtscope
├── org.eclipse.wst.jsdt.ui.superType.container
└── org.eclipse.wst.jsdt.ui.superType.name
├── LICENSE
├── README.md
├── bower.json
├── docimages
├── cover_problem.png
├── option_presets.png
├── option_presets_small.png
├── s1.png
├── s10.png
├── s11.png
├── s12.png
├── s13.png
├── s14.png
├── s15.png
├── s2.png
├── s3.png
├── s4.png
├── s7.png
├── s8.png
├── s9.png
└── transparency_problem.png
├── imagetracer_examples.html
├── imagetracer_options_gallery.html
├── imagetracer_test_automation.html
├── imagetracer_v1.2.6.js
├── nodecli
├── PNG.js
├── PNGReader.js
├── READTHIS.md
└── nodecli.js
├── options.md
├── package.json
├── panda.png
├── process_overview.md
├── simplify_interop.html
├── smiley.png
├── smileyRGB.png
├── testimages
├── 1.png
├── 10.png
├── 11.png
├── 12.png
├── 13.png
├── 14.png
├── 15.png
├── 16.png
├── 2.png
├── 3.png
├── 4.png
├── 5.png
├── 6.png
├── 7.png
├── 8.png
├── 9.png
└── combined.png
└── version_history.md
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true
4 | },
5 | "extends": "eslint:recommended",
6 | "rules": {
7 | "indent": [
8 | 2,
9 | "tab"
10 | ],
11 | "linebreak-style": [
12 | 2,
13 | "windows"
14 | ],
15 | "quotes": [
16 | 2,
17 | "single"
18 | ],
19 | "semi": [
20 | 2,
21 | "always"
22 | ]
23 | }
24 | }
--------------------------------------------------------------------------------
/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | imagetracerjs
4 |
5 |
6 |
7 |
8 |
9 | org.eclipse.wst.jsdt.core.javascriptValidator
10 |
11 |
12 |
13 |
14 |
15 | org.eclipse.wst.jsdt.core.jsNature
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.settings/.jsdtscope:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.settings/org.eclipse.wst.jsdt.ui.superType.container:
--------------------------------------------------------------------------------
1 | org.eclipse.wst.jsdt.launching.JRE_CONTAINER
--------------------------------------------------------------------------------
/.settings/org.eclipse.wst.jsdt.ui.superType.name:
--------------------------------------------------------------------------------
1 | Global
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | This is free and unencumbered software released into the public domain.
2 |
3 | Anyone is free to copy, modify, publish, use, compile, sell, or
4 | distribute this software, either in source code form or as a compiled
5 | binary, for any purpose, commercial or non-commercial, and by any
6 | means.
7 |
8 | In jurisdictions that recognize copyright laws, the author or authors
9 | of this software dedicate any and all copyright interest in the
10 | software to the public domain. We make this dedication for the benefit
11 | of the public at large and to the detriment of our heirs and
12 | successors. We intend this dedication to be an overt act of
13 | relinquishment in perpetuity of all present and future rights to this
14 | software under copyright law.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
24 | For more information, please refer to
25 |
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # imagetracerjs
2 | 
3 |
4 | Simple raster image tracer and vectorizer written in JavaScript.
5 |
6 | ---
7 |
8 | ## Table of contents
9 | - [Getting started](#getting-started)
10 | - [News](#news)
11 | - [API](#api)
12 | - [Options](#options)
13 | - [Examples](#examples)
14 | - [InkScape extension](#inkscape-extension)
15 | - [Process overview](#process-overview)
16 | - [License](#license)
17 |
18 | ---
19 |
20 | ## Getting started
21 |
22 | ### Using in the Browser
23 | Include the script:
24 | ```javascript
25 |
26 | ```
27 | Then:
28 | ```javascript
29 | // Loading an image, tracing with the 'posterized2' option preset, and appending the SVG to an element with id="svgcontainer"
30 | ImageTracer.imageToSVG(
31 |
32 | 'panda.png', /* input filename / URL */
33 |
34 | function(svgstr){ ImageTracer.appendSVGString( svgstr, 'svgcontainer' ); }, /* callback function to run on SVG string result */
35 |
36 | 'posterized2' /* Option preset */
37 |
38 | );
39 | ```
40 |
41 | ### Using with Node.js
42 |
43 | Node.js Command line interface example:
44 |
45 | ```
46 | imagetracerjs/nodecli>node nodecli ../panda.png outfilename panda.svg scale 10
47 | ```
48 |
49 | Expected result:
50 |
51 | ```
52 | imagetracerjs/nodecli/panda.svg was saved!
53 | ```
54 |
55 | ---
56 |
57 | ## News
58 |
59 | ### 1.2.6
60 | - NEW: [InkScape extension](#inkscape-extension)
61 | - FIXED: hole shape parent search (Issues #31 #39)
62 | - FIXED: Handle (absolute) paths in CLI correctly Issue #42
63 |
64 | ### 1.2.5
65 | - RGBA ImageData check in colorquantization(), solving Issue #24 and #18
66 |
67 | ### 1.2.4
68 | - ```options.layering``` : default 0 = sequential, new method ; 1 = parallel, old method. (Enhancement Issue #17)
69 | - case insensitive option preset names
70 | - README.md reorganizing
71 |
72 | [Version history](https://github.com/jankovicsandras/imagetracerjs/blob/master/version_history.md)
73 |
74 | ---
75 |
76 | ## API
77 | |Function name|Arguments|Returns|Run type|
78 | |-------------|---------|-------|--------|
79 | |```imageToSVG```|```image_url /*string*/ , callback /*function*/ , options /*optional object or preset name*/```|Nothing, ```callback(svgstring)``` will be executed|Asynchronous, Browser only|
80 | |```imagedataToSVG```|```imagedata /*object*/ , options /*optional object or preset name*/```|```svgstring /*string*/```|Synchronous, Browser & Node.js|
81 | |```imageToTracedata```|```image_url /*string*/ , callback /*function*/ , options /*optional object or preset name*/```|Nothing, ```callback(tracedata)``` will be executed|Asynchronous, Browser only|
82 | |```imagedataToTracedata```|```imagedata /*object*/ , options /*optional object or preset name*/```|```tracedata /*object*/```|Synchronous, Browser & Node.js|
83 |
84 | ```imagedata``` is standard [ImageData](https://developer.mozilla.org/en-US/docs/Web/API/ImageData) here, ```canvas``` is [canvas](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas) .
85 |
86 | ### Helper Functions (Browser only)
87 | |Function name|Arguments|Returns|Run type|
88 | |-------------|---------|-------|--------|
89 | |```appendSVGString```|```svgstring /*string*/, parentid /*string*/```|Nothing, an SVG will be appended to the container DOM element with id=parentid.|Synchronous, Browser only|
90 | |```loadImage```|```url /*string*/, callback /*function*/```|Nothing, loading an image from a URL, then executing ```callback(canvas)```|Asynchronous, Browser only|
91 | |```getImgdata```|```canvas /*object*/```|```imagedata /*object*/```|Synchronous, Browser only|
92 |
93 | ```imagedata``` is standard [ImageData](https://developer.mozilla.org/en-US/docs/Web/API/ImageData) here, ```canvas``` is [canvas](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas) .
94 | There are more functions for advanced users, read the source if you are interested. :)
95 |
96 | "Browser only" means that Node.js doesn't have built-in canvas and DOM support as of 2018, so loading an image to an ImageData object needs an external library.
97 |
98 | ---
99 |
100 | ## Options
101 | You can use an option preset name (string) or an [options object](https://github.com/jankovicsandras/imagetracerjs/blob/master/options.md) to control the tracing and rendering process.
102 |
103 | 
104 |
105 | These strings can be passed instead of the options object:
106 | ```'default'```
107 | ```'posterized1'```
108 | ```'posterized2'```
109 | ```'posterized3'```
110 | ```'curvy'```
111 | ```'sharp'```
112 | ```'detailed'```
113 | ```'smoothed'```
114 | ```'grayscale'```
115 | ```'fixedpalette'```
116 | ```'randomsampling1'```
117 | ```'randomsampling2'```
118 | ```'artistic1'```
119 | ```'artistic2'```
120 | ```'artistic3'```
121 | ```'artistic4'```
122 |
123 | [Read more about options.](https://github.com/jankovicsandras/imagetracerjs/blob/master/options.md)
124 |
125 | ---
126 |
127 | ## Examples
128 |
129 | ### Using in the Browser
130 | Include the script:
131 | ```javascript
132 |
133 | ```
134 | Then
135 | ```javascript
136 | // Loading smiley.png, tracing and calling alert callback on the SVG string result
137 | ImageTracer.imageToSVG( 'smiley.png', alert );
138 |
139 |
140 | // Almost the same with options, and the ImageTracer.appendSVGString callback will append the SVG
141 | ImageTracer.imageToSVG( 'smiley.png', ImageTracer.appendSVGString, { ltres:0.1, qtres:1, scale:10, strokewidth:5 } );
142 |
143 |
144 | // This uses the 'posterized2' option preset and appends the SVG to an element with id="svgcontainer"
145 | ImageTracer.imageToSVG(
146 | 'panda.png',
147 | function(svgstr){ ImageTracer.appendSVGString( svgstr, 'svgcontainer' ); },
148 | 'posterized2'
149 | );
150 |
151 |
152 | // The helper function loadImage() loads an image to a canvas, then executing callback:
153 | // appending the canvas to a div here.
154 | ImageTracer.loadImage(
155 | 'panda.png',
156 | function(canvas){ (document.getElementById('canvascontainer')).appendChild(canvas); }
157 | );
158 |
159 |
160 | // ImageData can be traced to an SVG string synchronously.
161 | ImageTracer.loadImage(
162 | 'smiley.png',
163 | function(canvas){
164 |
165 | // Getting ImageData from canvas with the helper function getImgdata().
166 | var imgd = ImageTracer.getImgdata( canvas );
167 |
168 | // Synchronous tracing to SVG string
169 | var svgstr = ImageTracer.imagedataToSVG( imgd, { scale:5 } );
170 |
171 | // Appending SVG
172 | ImageTracer.appendSVGString( svgstr, 'svgcontainer' );
173 |
174 | }
175 | );
176 |
177 |
178 | // This will load an image, trace it when loaded, and execute callback on the tracedata:
179 | // stringifying and alerting it here.
180 | ImageTracer.imageToTracedata(
181 | 'smiley.png',
182 | function(tracedata){ alert( JSON.stringify( tracedata ) ); },
183 | { ltres:0.1, qtres:1, scale:10 }
184 | );
185 |
186 |
187 | // imagedataToTracedata() is very similar to the previous functions. This returns tracedata synchronously.
188 | ImageTracer.loadImage(
189 | 'smiley.png',
190 | function(canvas){
191 |
192 | // Getting ImageData from canvas with the helper function getImgdata().
193 | var imgd = ImageTracer.getImgdata(canvas);
194 |
195 | // Synchronous tracing to tracedata
196 | var tracedata = ImageTracer.imagedataToTracedata( imgd, { ltres:1, qtres:0.01, scale:10 } );
197 |
198 | alert( JSON.stringify( tracedata ) );
199 | }
200 | );
201 | ```
202 |
203 | ### Using with Node.js CLI
204 |
205 | Node.js Command line interface example:
206 |
207 | ```
208 | imagetracerjs/nodecli>node nodecli ../panda.png outfilename panda.svg scale 10
209 | ```
210 |
211 | Expected result:
212 |
213 | ```
214 | imagetracerjs/nodecli/panda.svg was saved!
215 | ```
216 |
217 | CLI parameter names are supported both with and without trailing dash: ```-scale 10``` and ```scale 10``` are both correct.
218 | Almost all options are supported, except ```pal``` and ```layercontainerid```.
219 |
220 | ### Simple Node.js converting example
221 |
222 | ```javascript
223 | "use strict";
224 |
225 | var fs = require('fs');
226 |
227 | var ImageTracer = require( __dirname + '/../imagetracer_v1.2.6' );
228 |
229 | // This example uses https://github.com/arian/pngjs
230 | // , but other libraries can be used to load an image file to an ImageData object.
231 | var PNGReader = require( __dirname + '/PNGReader' );
232 |
233 | // Input and output filepaths / URLs
234 | var infilepath = __dirname + '/' + 'panda.png';
235 | var outfilepath = __dirname + '/' + 'panda.svg';
236 |
237 |
238 | fs.readFile(
239 |
240 | infilepath,
241 |
242 | function( err, bytes ){ // fs.readFile callback
243 | if(err){ console.log(err); throw err; }
244 |
245 | var reader = new PNGReader(bytes);
246 |
247 | reader.parse( function( err, png ){ // PNGReader callback
248 | if(err){ console.log(err); throw err; }
249 |
250 | // creating an ImageData object
251 | var myImageData = { width:png.width, height:png.height, data:png.pixels };
252 |
253 | // tracing to SVG string
254 | var options = { scale: 5 }; // options object; option preset string can be used also
255 |
256 | var svgstring = ImageTracer.imagedataToSVG( myImageData, options );
257 |
258 | // writing to file
259 | fs.writeFile(
260 | outfilepath,
261 | svgstring,
262 | function(err){ if(err){ console.log(err); throw err; } console.log( outfilepath + ' was saved!' ); }
263 | );
264 |
265 | });// End of reader.parse()
266 |
267 | }// End of readFile callback()
268 |
269 | );// End of fs.readFile()
270 | ```
271 |
272 | ### Tracedata processing / Simplify.js example
273 | It's possible to process the traced geometry and color data before SVG rendering. This example [simplify_interop.html](https://github.com/jankovicsandras/imagetracerjs/blob/master/simplify_interop.html) shows polyline simplification. You need to download simplify.js from https://github.com/mourner/simplify-js .
274 |
275 | ---
276 |
277 | ## InkScape extension
278 | ImageTracer is available as an InkScape extension: [https://inkscape.org/~MarioVoigt/%E2%98%85imagetracerjs-for-inkscape-1x](https://inkscape.org/~MarioVoigt/%E2%98%85imagetracerjs-for-inkscape-1x)
279 |
280 | [Screenshots](https://github.com/jankovicsandras/imagetracerjs/issues/47)
281 |
282 | Thanks Mario Voigt!
283 |
284 | ---
285 |
286 | ## Process overview
287 | See [Process overview and Ideas for improvement](https://github.com/jankovicsandras/imagetracerjs/blob/master/process_overview.md)
288 |
289 | ---
290 |
291 | ## License
292 | ### The Unlicense / PUBLIC DOMAIN
293 |
294 | This is free and unencumbered software released into the public domain.
295 |
296 | Anyone is free to copy, modify, publish, use, compile, sell, or
297 | distribute this software, either in source code form or as a compiled
298 | binary, for any purpose, commercial or non-commercial, and by any
299 | means.
300 |
301 | In jurisdictions that recognize copyright laws, the author or authors
302 | of this software dedicate any and all copyright interest in the
303 | software to the public domain. We make this dedication for the benefit
304 | of the public at large and to the detriment of our heirs and
305 | successors. We intend this dedication to be an overt act of
306 | relinquishment in perpetuity of all present and future rights to this
307 | software under copyright law.
308 |
309 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
310 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
311 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
312 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
313 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
314 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
315 | OTHER DEALINGS IN THE SOFTWARE.
316 |
317 | For more information, please refer to [http://unlicense.org](http://unlicense.org)
318 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "imagetracerjs",
3 | "description": "raster image tracer and vectorizer, bitmap to SVG converter",
4 | "repository": {
5 | "type": "git",
6 | "url": "https://github.com/jankovicsandras/imagetracerjs.git"
7 | },
8 | "main": "imagetracer_v1.2.6.js",
9 | "keywords": [
10 | "image",
11 | "tracer",
12 | "tracing",
13 | "vector",
14 | "raster",
15 | "vectorize",
16 | "vectorizing",
17 | "convert",
18 | "conversion",
19 | "converting",
20 | "bitmap",
21 | "svg",
22 | "bmp",
23 | "png",
24 | "jpg",
25 | "jpeg",
26 | "gif"
27 | ],
28 | "authors": [ "András Jankovics" ],
29 | "license": "Unlicense",
30 | "bugs": {
31 | "url": "https://github.com/jankovicsandras/imagetracerjs/issues"
32 | },
33 | "homepage": "https://github.com/jankovicsandras/imagetracerjs#readme"
34 | }
35 |
--------------------------------------------------------------------------------
/docimages/cover_problem.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jankovicsandras/imagetracerjs/cb0c84a309df5e75614d3b5166cdc77a56f12a98/docimages/cover_problem.png
--------------------------------------------------------------------------------
/docimages/option_presets.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jankovicsandras/imagetracerjs/cb0c84a309df5e75614d3b5166cdc77a56f12a98/docimages/option_presets.png
--------------------------------------------------------------------------------
/docimages/option_presets_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jankovicsandras/imagetracerjs/cb0c84a309df5e75614d3b5166cdc77a56f12a98/docimages/option_presets_small.png
--------------------------------------------------------------------------------
/docimages/s1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jankovicsandras/imagetracerjs/cb0c84a309df5e75614d3b5166cdc77a56f12a98/docimages/s1.png
--------------------------------------------------------------------------------
/docimages/s10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jankovicsandras/imagetracerjs/cb0c84a309df5e75614d3b5166cdc77a56f12a98/docimages/s10.png
--------------------------------------------------------------------------------
/docimages/s11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jankovicsandras/imagetracerjs/cb0c84a309df5e75614d3b5166cdc77a56f12a98/docimages/s11.png
--------------------------------------------------------------------------------
/docimages/s12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jankovicsandras/imagetracerjs/cb0c84a309df5e75614d3b5166cdc77a56f12a98/docimages/s12.png
--------------------------------------------------------------------------------
/docimages/s13.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jankovicsandras/imagetracerjs/cb0c84a309df5e75614d3b5166cdc77a56f12a98/docimages/s13.png
--------------------------------------------------------------------------------
/docimages/s14.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jankovicsandras/imagetracerjs/cb0c84a309df5e75614d3b5166cdc77a56f12a98/docimages/s14.png
--------------------------------------------------------------------------------
/docimages/s15.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jankovicsandras/imagetracerjs/cb0c84a309df5e75614d3b5166cdc77a56f12a98/docimages/s15.png
--------------------------------------------------------------------------------
/docimages/s2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jankovicsandras/imagetracerjs/cb0c84a309df5e75614d3b5166cdc77a56f12a98/docimages/s2.png
--------------------------------------------------------------------------------
/docimages/s3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jankovicsandras/imagetracerjs/cb0c84a309df5e75614d3b5166cdc77a56f12a98/docimages/s3.png
--------------------------------------------------------------------------------
/docimages/s4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jankovicsandras/imagetracerjs/cb0c84a309df5e75614d3b5166cdc77a56f12a98/docimages/s4.png
--------------------------------------------------------------------------------
/docimages/s7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jankovicsandras/imagetracerjs/cb0c84a309df5e75614d3b5166cdc77a56f12a98/docimages/s7.png
--------------------------------------------------------------------------------
/docimages/s8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jankovicsandras/imagetracerjs/cb0c84a309df5e75614d3b5166cdc77a56f12a98/docimages/s8.png
--------------------------------------------------------------------------------
/docimages/s9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jankovicsandras/imagetracerjs/cb0c84a309df5e75614d3b5166cdc77a56f12a98/docimages/s9.png
--------------------------------------------------------------------------------
/docimages/transparency_problem.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jankovicsandras/imagetracerjs/cb0c84a309df5e75614d3b5166cdc77a56f12a98/docimages/transparency_problem.png
--------------------------------------------------------------------------------
/imagetracer_examples.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
77 |
78 |
79 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/imagetracer_options_gallery.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | imagetracer.js options gallery
8 |
32 |
33 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |  Original | | | | |
62 | | | | | |
63 | | | | | |
64 | | | | | |
65 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/imagetracer_test_automation.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | imagetracer.js test automation
6 |
15 |
16 |
251 |
252 |
253 |
254 |
294 |
295 | Result (if Draw SVG is active)
296 |
297 |
298 | Traced SVG | Original raster | SVG rendered as raster | Difference |
299 |
300 |
301 |
302 | |
303 | |
304 | |
305 | |
306 |
307 |
308 |
309 |
310 | Measurements
311 |
312 |
313 |
314 |
315 | RowID |
316 | Filename |
317 | width (pixels) |
318 | height (pixels) |
319 | area (pixels) |
320 | SVG string length (bytes) |
321 | Tracing time (ms) |
322 | Rendering time (ms) |
323 | Nr. of paths |
324 | RGBA difference (cummulative RGBA difference / (area*4)) |
325 | Different pixels (%) |
326 | Options |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
--------------------------------------------------------------------------------
/imagetracer_v1.2.6.js:
--------------------------------------------------------------------------------
1 | /*
2 | imagetracer.js version 1.2.6
3 | Simple raster image tracer and vectorizer written in JavaScript.
4 | andras@jankovics.net
5 | */
6 |
7 | /*
8 |
9 | The Unlicense / PUBLIC DOMAIN
10 |
11 | This is free and unencumbered software released into the public domain.
12 |
13 | Anyone is free to copy, modify, publish, use, compile, sell, or
14 | distribute this software, either in source code form or as a compiled
15 | binary, for any purpose, commercial or non-commercial, and by any
16 | means.
17 |
18 | In jurisdictions that recognize copyright laws, the author or authors
19 | of this software dedicate any and all copyright interest in the
20 | software to the public domain. We make this dedication for the benefit
21 | of the public at large and to the detriment of our heirs and
22 | successors. We intend this dedication to be an overt act of
23 | relinquishment in perpetuity of all present and future rights to this
24 | software under copyright law.
25 |
26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
27 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
28 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
29 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
30 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
31 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
32 | OTHER DEALINGS IN THE SOFTWARE.
33 |
34 | For more information, please refer to http://unlicense.org/
35 |
36 | */
37 |
38 | (function(){ 'use strict';
39 |
40 | function ImageTracer(){
41 | var _this = this;
42 |
43 | this.versionnumber = '1.2.6',
44 |
45 | ////////////////////////////////////////////////////////////
46 | //
47 | // API
48 | //
49 | ////////////////////////////////////////////////////////////
50 |
51 | // Loading an image from a URL, tracing when loaded,
52 | // then executing callback with the scaled svg string as argument
53 | this.imageToSVG = function( url, callback, options ){
54 | options = _this.checkoptions(options);
55 | // loading image, tracing and callback
56 | _this.loadImage(
57 | url,
58 | function(canvas){
59 | callback(
60 | _this.imagedataToSVG( _this.getImgdata(canvas), options )
61 | );
62 | },
63 | options
64 | );
65 | },// End of imageToSVG()
66 |
67 | // Tracing imagedata, then returning the scaled svg string
68 | this.imagedataToSVG = function( imgd, options ){
69 | options = _this.checkoptions(options);
70 | // tracing imagedata
71 | var td = _this.imagedataToTracedata( imgd, options );
72 | // returning SVG string
73 | return _this.getsvgstring(td, options);
74 | },// End of imagedataToSVG()
75 |
76 | // Loading an image from a URL, tracing when loaded,
77 | // then executing callback with tracedata as argument
78 | this.imageToTracedata = function( url, callback, options ){
79 | options = _this.checkoptions(options);
80 | // loading image, tracing and callback
81 | _this.loadImage(
82 | url,
83 | function(canvas){
84 | callback(
85 | _this.imagedataToTracedata( _this.getImgdata(canvas), options )
86 | );
87 | },
88 | options
89 | );
90 | },// End of imageToTracedata()
91 |
92 | // Tracing imagedata, then returning tracedata (layers with paths, palette, image size)
93 | this.imagedataToTracedata = function( imgd, options ){
94 | options = _this.checkoptions(options);
95 |
96 | // 1. Color quantization
97 | var ii = _this.colorquantization( imgd, options );
98 |
99 | if(options.layering === 0){// Sequential layering
100 |
101 | // create tracedata object
102 | var tracedata = {
103 | layers : [],
104 | palette : ii.palette,
105 | width : ii.array[0].length-2,
106 | height : ii.array.length-2
107 | };
108 |
109 | // Loop to trace each color layer
110 | for(var colornum=0; colornum pathscan -> internodes -> batchtracepaths
113 | var tracedlayer =
114 | _this.batchtracepaths(
115 |
116 | _this.internodes(
117 |
118 | _this.pathscan(
119 | _this.layeringstep( ii, colornum ),
120 | options.pathomit
121 | ),
122 |
123 | options
124 |
125 | ),
126 |
127 | options.ltres,
128 | options.qtres
129 |
130 | );
131 |
132 | // adding traced layer
133 | tracedata.layers.push(tracedlayer);
134 |
135 | }// End of color loop
136 |
137 | }else{// Parallel layering
138 | // 2. Layer separation and edge detection
139 | var ls = _this.layering( ii );
140 |
141 | // Optional edge node visualization
142 | if(options.layercontainerid){ _this.drawLayers( ls, _this.specpalette, options.scale, options.layercontainerid ); }
143 |
144 | // 3. Batch pathscan
145 | var bps = _this.batchpathscan( ls, options.pathomit );
146 |
147 | // 4. Batch interpollation
148 | var bis = _this.batchinternodes( bps, options );
149 |
150 | // 5. Batch tracing and creating tracedata object
151 | var tracedata = {
152 | layers : _this.batchtracelayers( bis, options.ltres, options.qtres ),
153 | palette : ii.palette,
154 | width : imgd.width,
155 | height : imgd.height
156 | };
157 |
158 | }// End of parallel layering
159 |
160 | // return tracedata
161 | return tracedata;
162 |
163 | },// End of imagedataToTracedata()
164 |
165 | this.optionpresets = {
166 | 'default': {
167 |
168 | // Tracing
169 | corsenabled : false,
170 | ltres : 1,
171 | qtres : 1,
172 | pathomit : 8,
173 | rightangleenhance : true,
174 |
175 | // Color quantization
176 | colorsampling : 2,
177 | numberofcolors : 16,
178 | mincolorratio : 0,
179 | colorquantcycles : 3,
180 |
181 | // Layering method
182 | layering : 0,
183 |
184 | // SVG rendering
185 | strokewidth : 1,
186 | linefilter : false,
187 | scale : 1,
188 | roundcoords : 1,
189 | viewbox : false,
190 | desc : false,
191 | lcpr : 0,
192 | qcpr : 0,
193 |
194 | // Blur
195 | blurradius : 0,
196 | blurdelta : 20
197 |
198 | },
199 | 'posterized1': { colorsampling:0, numberofcolors:2 },
200 | 'posterized2': { numberofcolors:4, blurradius:5 },
201 | 'curvy': { ltres:0.01, linefilter:true, rightangleenhance:false },
202 | 'sharp': { qtres:0.01, linefilter:false },
203 | 'detailed': { pathomit:0, roundcoords:2, ltres:0.5, qtres:0.5, numberofcolors:64 },
204 | 'smoothed': { blurradius:5, blurdelta: 64 },
205 | 'grayscale': { colorsampling:0, colorquantcycles:1, numberofcolors:7 },
206 | 'fixedpalette': { colorsampling:0, colorquantcycles:1, numberofcolors:27 },
207 | 'randomsampling1': { colorsampling:1, numberofcolors:8 },
208 | 'randomsampling2': { colorsampling:1, numberofcolors:64 },
209 | 'artistic1': { colorsampling:0, colorquantcycles:1, pathomit:0, blurradius:5, blurdelta: 64, ltres:0.01, linefilter:true, numberofcolors:16, strokewidth:2 },
210 | 'artistic2': { qtres:0.01, colorsampling:0, colorquantcycles:1, numberofcolors:4, strokewidth:0 },
211 | 'artistic3': { qtres:10, ltres:10, numberofcolors:8 },
212 | 'artistic4': { qtres:10, ltres:10, numberofcolors:64, blurradius:5, blurdelta: 256, strokewidth:2 },
213 | 'posterized3': { ltres: 1, qtres: 1, pathomit: 20, rightangleenhance: true, colorsampling: 0, numberofcolors: 3,
214 | mincolorratio: 0, colorquantcycles: 3, blurradius: 3, blurdelta: 20, strokewidth: 0, linefilter: false,
215 | roundcoords: 1, pal: [ { r: 0, g: 0, b: 100, a: 255 }, { r: 255, g: 255, b: 255, a: 255 } ] }
216 | },// End of optionpresets
217 |
218 | // creating options object, setting defaults for missing values
219 | this.checkoptions = function(options){
220 | options = options || {};
221 | // Option preset
222 | if(typeof options === 'string'){
223 | options = options.toLowerCase();
224 | if( _this.optionpresets[options] ){ options = _this.optionpresets[options]; }else{ options = {}; }
225 | }
226 | // Defaults
227 | var ok = Object.keys(_this.optionpresets['default']);
228 | for(var k=0; k
233 | return options;
234 | },// End of checkoptions()
235 |
236 | ////////////////////////////////////////////////////////////
237 | //
238 | // Vectorizing functions
239 | //
240 | ////////////////////////////////////////////////////////////
241 |
242 | // 1. Color quantization
243 | // Using a form of k-means clustering repeatead options.colorquantcycles times. http://en.wikipedia.org/wiki/Color_quantization
244 | this.colorquantization = function( imgd, options ){
245 | var arr = [], idx=0, cd,cdl,ci, paletteacc = [], pixelnum = imgd.width * imgd.height, i, j, k, cnt, palette;
246 |
247 | // imgd.data must be RGBA, not just RGB
248 | if( imgd.data.length < pixelnum * 4 ){
249 | var newimgddata = new Uint8ClampedArray(pixelnum * 4);
250 | for(var pxcnt = 0; pxcnt < pixelnum ; pxcnt++){
251 | newimgddata[pxcnt*4 ] = imgd.data[pxcnt*3 ];
252 | newimgddata[pxcnt*4+1] = imgd.data[pxcnt*3+1];
253 | newimgddata[pxcnt*4+2] = imgd.data[pxcnt*3+2];
254 | newimgddata[pxcnt*4+3] = 255;
255 | }
256 | imgd.data = newimgddata;
257 | }// End of RGBA imgd.data check
258 |
259 | // Filling arr (color index array) with -1
260 | for( j=0; j 0 ){ imgd = _this.blur( imgd, options.blurradius, options.blurdelta ); }
275 |
276 | // Repeat clustering step options.colorquantcycles times
277 | for( cnt=0; cnt < options.colorquantcycles; cnt++ ){
278 |
279 | // Average colors from the second iteration
280 | if(cnt>0){
281 | // averaging paletteacc for palette
282 | for( k=0; k < palette.length; k++ ){
283 |
284 | // averaging
285 | if( paletteacc[k].n > 0 ){
286 | palette[k] = { r: Math.floor( paletteacc[k].r / paletteacc[k].n ),
287 | g: Math.floor( paletteacc[k].g / paletteacc[k].n ),
288 | b: Math.floor( paletteacc[k].b / paletteacc[k].n ),
289 | a: Math.floor( paletteacc[k].a / paletteacc[k].n ) };
290 | }
291 |
292 | // Randomizing a color, if there are too few pixels and there will be a new cycle
293 | if( ( paletteacc[k].n/pixelnum < options.mincolorratio ) && ( cnt < options.colorquantcycles-1 ) ){
294 | palette[k] = { r: Math.floor(Math.random()*255),
295 | g: Math.floor(Math.random()*255),
296 | b: Math.floor(Math.random()*255),
297 | a: Math.floor(Math.random()*255) };
298 | }
299 |
300 | }// End of palette loop
301 | }// End of Average colors from the second iteration
302 |
303 | // Reseting palette accumulator for averaging
304 | for( i=0; i < palette.length; i++ ){ paletteacc[i] = { r:0, g:0, b:0, a:0, n:0 }; }
305 |
306 | // loop through all pixels
307 | for( j=0; j < imgd.height; j++ ){
308 | for( i=0; i < imgd.width; i++ ){
309 |
310 | // pixel index
311 | idx = (j*imgd.width+i)*4;
312 |
313 | // find closest color from palette by measuring (rectilinear) color distance between this pixel and all palette colors
314 | ci=0; cdl = 1024; // 4 * 256 is the maximum RGBA distance
315 | for( k=0; k imgd.data[idx ] ? palette[k].r - imgd.data[idx ] : imgd.data[idx ] - palette[k].r ) +
320 | ( palette[k].g > imgd.data[idx+1] ? palette[k].g - imgd.data[idx+1] : imgd.data[idx+1] - palette[k].g ) +
321 | ( palette[k].b > imgd.data[idx+2] ? palette[k].b - imgd.data[idx+2] : imgd.data[idx+2] - palette[k].b ) +
322 | ( palette[k].a > imgd.data[idx+3] ? palette[k].a - imgd.data[idx+3] : imgd.data[idx+3] - palette[k].a );
323 |
324 | // Remember this color if this is the closest yet
325 | if(cd p.y) !== (pa[j].y > p.y)) && (p.x < (pa[j].x - pa[i].x) * (p.y - pa[i].y) / (pa[j].y - pa[i].y) + pa[i].x) )
495 | ? !isin : isin;
496 | }
497 |
498 | return isin;
499 | },
500 |
501 | // Lookup tables for pathscan
502 | // pathscan_combined_lookup[ arr[py][px] ][ dir ] = [nextarrpypx, nextdir, deltapx, deltapy];
503 | this.pathscan_combined_lookup = [
504 | [[-1,-1,-1,-1], [-1,-1,-1,-1], [-1,-1,-1,-1], [-1,-1,-1,-1]],// arr[py][px]===0 is invalid
505 | [[ 0, 1, 0,-1], [-1,-1,-1,-1], [-1,-1,-1,-1], [ 0, 2,-1, 0]],
506 | [[-1,-1,-1,-1], [-1,-1,-1,-1], [ 0, 1, 0,-1], [ 0, 0, 1, 0]],
507 | [[ 0, 0, 1, 0], [-1,-1,-1,-1], [ 0, 2,-1, 0], [-1,-1,-1,-1]],
508 |
509 | [[-1,-1,-1,-1], [ 0, 0, 1, 0], [ 0, 3, 0, 1], [-1,-1,-1,-1]],
510 | [[13, 3, 0, 1], [13, 2,-1, 0], [ 7, 1, 0,-1], [ 7, 0, 1, 0]],
511 | [[-1,-1,-1,-1], [ 0, 1, 0,-1], [-1,-1,-1,-1], [ 0, 3, 0, 1]],
512 | [[ 0, 3, 0, 1], [ 0, 2,-1, 0], [-1,-1,-1,-1], [-1,-1,-1,-1]],
513 |
514 | [[ 0, 3, 0, 1], [ 0, 2,-1, 0], [-1,-1,-1,-1], [-1,-1,-1,-1]],
515 | [[-1,-1,-1,-1], [ 0, 1, 0,-1], [-1,-1,-1,-1], [ 0, 3, 0, 1]],
516 | [[11, 1, 0,-1], [14, 0, 1, 0], [14, 3, 0, 1], [11, 2,-1, 0]],
517 | [[-1,-1,-1,-1], [ 0, 0, 1, 0], [ 0, 3, 0, 1], [-1,-1,-1,-1]],
518 |
519 | [[ 0, 0, 1, 0], [-1,-1,-1,-1], [ 0, 2,-1, 0], [-1,-1,-1,-1]],
520 | [[-1,-1,-1,-1], [-1,-1,-1,-1], [ 0, 1, 0,-1], [ 0, 0, 1, 0]],
521 | [[ 0, 1, 0,-1], [-1,-1,-1,-1], [-1,-1,-1,-1], [ 0, 2,-1, 0]],
522 | [[-1,-1,-1,-1], [-1,-1,-1,-1], [-1,-1,-1,-1], [-1,-1,-1,-1]]// arr[py][px]===15 is invalid
523 | ],
524 |
525 | // 3. Walking through an edge node array, discarding edge node types 0 and 15 and creating paths from the rest.
526 | // Walk directions (dir): 0 > ; 1 ^ ; 2 < ; 3 v
527 | this.pathscan = function( arr, pathomit ){
528 | var paths=[], pacnt=0, pcnt=0, px=0, py=0, w = arr[0].length, h = arr.length,
529 | dir=0, pathfinished=true, holepath=false, lookuprow;
530 |
531 | for(var j=0; j paths[pacnt].boundingbox[2] ){ paths[pacnt].boundingbox[2] = px-1; }
558 | if( (py-1) < paths[pacnt].boundingbox[1] ){ paths[pacnt].boundingbox[1] = py-1; }
559 | if( (py-1) > paths[pacnt].boundingbox[3] ){ paths[pacnt].boundingbox[3] = py-1; }
560 |
561 | // Next: look up the replacement, direction and coordinate changes = clear this cell, turn if required, walk forward
562 | lookuprow = _this.pathscan_combined_lookup[ arr[py][px] ][ dir ];
563 | arr[py][px] = lookuprow[0]; dir = lookuprow[1]; px += lookuprow[2]; py += lookuprow[3];
564 |
565 | // Close path
566 | if( (px-1 === paths[pacnt].points[0].x ) && ( py-1 === paths[pacnt].points[0].y ) ){
567 | pathfinished = true;
568 |
569 | // Discarding paths shorter than pathomit
570 | if( paths[pacnt].points.length < pathomit ){
571 | paths.pop();
572 | }else{
573 |
574 | paths[pacnt].isholepath = holepath ? true : false;
575 |
576 | // Finding the parent shape for this hole
577 | if(holepath){
578 |
579 | var parentidx = 0, parentbbox = [-1,-1,w+1,h+1];
580 | for(var parentcnt=0; parentcnt < pacnt; parentcnt++){
581 | if( (!paths[parentcnt].isholepath) &&
582 | _this.boundingboxincludes( paths[parentcnt].boundingbox , paths[pacnt].boundingbox ) &&
583 | _this.boundingboxincludes( parentbbox , paths[parentcnt].boundingbox ) &&
584 | _this.pointinpoly( paths[pacnt].points[0], paths[parentcnt].points )
585 | ){
586 | parentidx = parentcnt;
587 | parentbbox = paths[parentcnt].boundingbox;
588 | }
589 | }
590 |
591 | paths[parentidx].holechildren.push( pacnt );
592 |
593 | }// End of holepath parent finding
594 |
595 | pacnt++;
596 |
597 | }
598 |
599 | }// End of Close path
600 |
601 | pcnt++;
602 |
603 | }// End of Path points loop
604 |
605 | }// End of Follow path
606 |
607 | }// End of i loop
608 | }// End of j loop
609 |
610 | return paths;
611 | },// End of pathscan()
612 |
613 | this.boundingboxincludes = function( parentbbox, childbbox ){
614 | return ( ( parentbbox[0] < childbbox[0] ) && ( parentbbox[1] < childbbox[1] ) && ( parentbbox[2] > childbbox[2] ) && ( parentbbox[3] > childbbox[3] ) );
615 | },// End of boundingboxincludes()
616 |
617 | // 3. Batch pathscan
618 | this.batchpathscan = function( layers, pathomit ){
619 | var bpaths = [];
620 | for(var k in layers){
621 | if(!layers.hasOwnProperty(k)){ continue; }
622 | bpaths[k] = _this.pathscan( layers[k], pathomit );
623 | }
624 | return bpaths;
625 | },
626 |
627 | // 4. interpollating between path points for nodes with 8 directions ( East, SouthEast, S, SW, W, NW, N, NE )
628 | this.internodes = function( paths, options ){
629 | var ins = [], palen=0, nextidx=0, nextidx2=0, previdx=0, previdx2=0, pacnt, pcnt;
630 |
631 | // paths loop
632 | for(pacnt=0; pacnt 0){
652 | ins[pacnt].points[ ins[pacnt].points.length-1 ].linesegment = _this.getdirection(
653 | ins[pacnt].points[ ins[pacnt].points.length-1 ].x,
654 | ins[pacnt].points[ ins[pacnt].points.length-1 ].y,
655 | paths[pacnt].points[pcnt].x,
656 | paths[pacnt].points[pcnt].y
657 | );
658 | }
659 |
660 | // This corner point
661 | ins[pacnt].points.push({
662 | x : paths[pacnt].points[pcnt].x,
663 | y : paths[pacnt].points[pcnt].y,
664 | linesegment : _this.getdirection(
665 | paths[pacnt].points[pcnt].x,
666 | paths[pacnt].points[pcnt].y,
667 | (( paths[pacnt].points[pcnt].x + paths[pacnt].points[nextidx].x ) /2),
668 | (( paths[pacnt].points[pcnt].y + paths[pacnt].points[nextidx].y ) /2)
669 | )
670 | });
671 |
672 | }// End of right angle enhance
673 |
674 | // interpolate between two path points
675 | ins[pacnt].points.push({
676 | x : (( paths[pacnt].points[pcnt].x + paths[pacnt].points[nextidx].x ) /2),
677 | y : (( paths[pacnt].points[pcnt].y + paths[pacnt].points[nextidx].y ) /2),
678 | linesegment : _this.getdirection(
679 | (( paths[pacnt].points[pcnt].x + paths[pacnt].points[nextidx].x ) /2),
680 | (( paths[pacnt].points[pcnt].y + paths[pacnt].points[nextidx].y ) /2),
681 | (( paths[pacnt].points[nextidx].x + paths[pacnt].points[nextidx2].x ) /2),
682 | (( paths[pacnt].points[nextidx].y + paths[pacnt].points[nextidx2].y ) /2)
683 | )
684 | });
685 |
686 | }// End of pathpoints loop
687 |
688 | }// End of paths loop
689 |
690 | return ins;
691 | },// End of internodes()
692 |
693 | this.testrightangle = function( path, idx1, idx2, idx3, idx4, idx5 ){
694 | return ( (( path.points[idx3].x === path.points[idx1].x) &&
695 | ( path.points[idx3].x === path.points[idx2].x) &&
696 | ( path.points[idx3].y === path.points[idx4].y) &&
697 | ( path.points[idx3].y === path.points[idx5].y)
698 | ) ||
699 | (( path.points[idx3].y === path.points[idx1].y) &&
700 | ( path.points[idx3].y === path.points[idx2].y) &&
701 | ( path.points[idx3].x === path.points[idx4].x) &&
702 | ( path.points[idx3].x === path.points[idx5].x)
703 | )
704 | );
705 | },// End of testrightangle()
706 |
707 | this.getdirection = function( x1, y1, x2, y2 ){
708 | var val = 8;
709 | if(x1 < x2){
710 | if (y1 < y2){ val = 1; }// SouthEast
711 | else if(y1 > y2){ val = 7; }// NE
712 | else { val = 0; }// E
713 | }else if(x1 > x2){
714 | if (y1 < y2){ val = 3; }// SW
715 | else if(y1 > y2){ val = 5; }// NW
716 | else { val = 4; }// W
717 | }else{
718 | if (y1 < y2){ val = 2; }// S
719 | else if(y1 > y2){ val = 6; }// N
720 | else { val = 8; }// center, this should not happen
721 | }
722 | return val;
723 | },// End of getdirection()
724 |
725 | // 4. Batch interpollation
726 | this.batchinternodes = function( bpaths, options ){
727 | var binternodes = [];
728 | for (var k in bpaths) {
729 | if(!bpaths.hasOwnProperty(k)){ continue; }
730 | binternodes[k] = _this.internodes(bpaths[k], options);
731 | }
732 | return binternodes;
733 | },
734 |
735 | // 5. tracepath() : recursively trying to fit straight and quadratic spline segments on the 8 direction internode path
736 |
737 | // 5.1. Find sequences of points with only 2 segment types
738 | // 5.2. Fit a straight line on the sequence
739 | // 5.3. If the straight line fails (distance error > ltres), find the point with the biggest error
740 | // 5.4. Fit a quadratic spline through errorpoint (project this to get controlpoint), then measure errors on every point in the sequence
741 | // 5.5. If the spline fails (distance error > qtres), find the point with the biggest error, set splitpoint = fitting point
742 | // 5.6. Split sequence and recursively apply 5.2. - 5.6. to startpoint-splitpoint and splitpoint-endpoint sequences
743 |
744 | this.tracepath = function( path, ltres, qtres ){
745 | var pcnt=0, segtype1, segtype2, seqend, smp = {};
746 | smp.segments = [];
747 | smp.boundingbox = path.boundingbox;
748 | smp.holechildren = path.holechildren;
749 | smp.isholepath = path.isholepath;
750 |
751 | while(pcnt < path.points.length){
752 | // 5.1. Find sequences of points with only 2 segment types
753 | segtype1 = path.points[pcnt].linesegment; segtype2 = -1; seqend=pcnt+1;
754 | while(
755 | ((path.points[seqend].linesegment === segtype1) || (path.points[seqend].linesegment === segtype2) || (segtype2 === -1))
756 | && (seqend < path.points.length-1) ){
757 |
758 | if((path.points[seqend].linesegment!==segtype1) && (segtype2===-1)){ segtype2 = path.points[seqend].linesegment; }
759 | seqend++;
760 |
761 | }
762 | if(seqend === path.points.length-1){ seqend = 0; }
763 |
764 | // 5.2. - 5.6. Split sequence and recursively apply 5.2. - 5.6. to startpoint-splitpoint and splitpoint-endpoint sequences
765 | smp.segments = smp.segments.concat( _this.fitseq(path, ltres, qtres, pcnt, seqend) );
766 |
767 | // forward pcnt;
768 | if(seqend>0){ pcnt = seqend; }else{ pcnt = path.points.length; }
769 |
770 | }// End of pcnt loop
771 |
772 | return smp;
773 | },// End of tracepath()
774 |
775 | // 5.2. - 5.6. recursively fitting a straight or quadratic line segment on this sequence of path nodes,
776 | // called from tracepath()
777 | this.fitseq = function( path, ltres, qtres, seqstart, seqend ){
778 | // return if invalid seqend
779 | if( (seqend>path.points.length) || (seqend<0) ){ return []; }
780 | // variables
781 | var errorpoint=seqstart, errorval=0, curvepass=true, px, py, dist2;
782 | var tl = (seqend-seqstart); if(tl<0){ tl += path.points.length; }
783 | var vx = (path.points[seqend].x-path.points[seqstart].x) / tl,
784 | vy = (path.points[seqend].y-path.points[seqstart].y) / tl;
785 |
786 | // 5.2. Fit a straight line on the sequence
787 | var pcnt = (seqstart+1) % path.points.length, pl;
788 | while(pcnt != seqend){
789 | pl = pcnt-seqstart; if(pl<0){ pl += path.points.length; }
790 | px = path.points[seqstart].x + vx * pl; py = path.points[seqstart].y + vy * pl;
791 | dist2 = (path.points[pcnt].x-px)*(path.points[pcnt].x-px) + (path.points[pcnt].y-py)*(path.points[pcnt].y-py);
792 | if(dist2>ltres){curvepass=false;}
793 | if(dist2>errorval){ errorpoint=pcnt; errorval=dist2; }
794 | pcnt = (pcnt+1)%path.points.length;
795 | }
796 | // return straight line if fits
797 | if(curvepass){ return [{ type:'L', x1:path.points[seqstart].x, y1:path.points[seqstart].y, x2:path.points[seqend].x, y2:path.points[seqend].y }]; }
798 |
799 | // 5.3. If the straight line fails (distance error>ltres), find the point with the biggest error
800 | var fitpoint = errorpoint; curvepass = true; errorval = 0;
801 |
802 | // 5.4. Fit a quadratic spline through this point, measure errors on every point in the sequence
803 | // helpers and projecting to get control point
804 | var t=(fitpoint-seqstart)/tl, t1=(1-t)*(1-t), t2=2*(1-t)*t, t3=t*t;
805 | var cpx = (t1*path.points[seqstart].x + t3*path.points[seqend].x - path.points[fitpoint].x)/-t2 ,
806 | cpy = (t1*path.points[seqstart].y + t3*path.points[seqend].y - path.points[fitpoint].y)/-t2 ;
807 |
808 | // Check every point
809 | pcnt = seqstart+1;
810 | while(pcnt != seqend){
811 | t=(pcnt-seqstart)/tl; t1=(1-t)*(1-t); t2=2*(1-t)*t; t3=t*t;
812 | px = t1 * path.points[seqstart].x + t2 * cpx + t3 * path.points[seqend].x;
813 | py = t1 * path.points[seqstart].y + t2 * cpy + t3 * path.points[seqend].y;
814 |
815 | dist2 = (path.points[pcnt].x-px)*(path.points[pcnt].x-px) + (path.points[pcnt].y-py)*(path.points[pcnt].y-py);
816 |
817 | if(dist2>qtres){curvepass=false;}
818 | if(dist2>errorval){ errorpoint=pcnt; errorval=dist2; }
819 | pcnt = (pcnt+1)%path.points.length;
820 | }
821 | // return spline if fits
822 | if(curvepass){ return [{ type:'Q', x1:path.points[seqstart].x, y1:path.points[seqstart].y, x2:cpx, y2:cpy, x3:path.points[seqend].x, y3:path.points[seqend].y }]; }
823 | // 5.5. If the spline fails (distance error>qtres), find the point with the biggest error
824 | var splitpoint = fitpoint; // Earlier: Math.floor((fitpoint + errorpoint)/2);
825 |
826 | // 5.6. Split sequence and recursively apply 5.2. - 5.6. to startpoint-splitpoint and splitpoint-endpoint sequences
827 | return _this.fitseq( path, ltres, qtres, seqstart, splitpoint ).concat(
828 | _this.fitseq( path, ltres, qtres, splitpoint, seqend ) );
829 |
830 | },// End of fitseq()
831 |
832 | // 5. Batch tracing paths
833 | this.batchtracepaths = function(internodepaths,ltres,qtres){
834 | var btracedpaths = [];
835 | for(var k in internodepaths){
836 | if(!internodepaths.hasOwnProperty(k)){ continue; }
837 | btracedpaths.push( _this.tracepath(internodepaths[k],ltres,qtres) );
838 | }
839 | return btracedpaths;
840 | },
841 |
842 | // 5. Batch tracing layers
843 | this.batchtracelayers = function(binternodes, ltres, qtres){
844 | var btbis = [];
845 | for(var k in binternodes){
846 | if(!binternodes.hasOwnProperty(k)){ continue; }
847 | btbis[k] = _this.batchtracepaths(binternodes[k], ltres, qtres);
848 | }
849 | return btbis;
850 | },
851 |
852 | ////////////////////////////////////////////////////////////
853 | //
854 | // SVG Drawing functions
855 | //
856 | ////////////////////////////////////////////////////////////
857 |
858 | // Rounding to given decimals https://stackoverflow.com/questions/11832914/round-to-at-most-2-decimal-places-in-javascript
859 | this.roundtodec = function(val,places){ return +val.toFixed(places); },
860 |
861 | // Getting SVG path element string from a traced path
862 | this.svgpathstring = function( tracedata, lnum, pathnum, options ){
863 |
864 | var layer = tracedata.layers[lnum], smp = layer[pathnum], str='', pcnt;
865 |
866 | // Line filter
867 | if(options.linefilter && (smp.segments.length < 3)){ return str; }
868 |
869 | // Starting path element, desc contains layer and path number
870 | str = '= 0; pcnt--){
909 | str += hsmp.segments[pcnt].type +' ';
910 | if(hsmp.segments[pcnt].hasOwnProperty('x3')){
911 | str += hsmp.segments[pcnt].x2 * options.scale +' '+ hsmp.segments[pcnt].y2 * options.scale +' ';
912 | }
913 |
914 | str += hsmp.segments[pcnt].x1 * options.scale +' '+ hsmp.segments[pcnt].y1 * options.scale +' ';
915 | }
916 |
917 | }else{
918 |
919 | if(hsmp.segments[ hsmp.segments.length-1 ].hasOwnProperty('x3')){
920 | str += 'M '+ _this.roundtodec( hsmp.segments[ hsmp.segments.length-1 ].x3 * options.scale ) +' '+ _this.roundtodec( hsmp.segments[ hsmp.segments.length-1 ].y3 * options.scale ) +' ';
921 | }else{
922 | str += 'M '+ _this.roundtodec( hsmp.segments[ hsmp.segments.length-1 ].x2 * options.scale ) +' '+ _this.roundtodec( hsmp.segments[ hsmp.segments.length-1 ].y2 * options.scale ) +' ';
923 | }
924 |
925 | for(pcnt = hsmp.segments.length-1; pcnt >= 0; pcnt--){
926 | str += hsmp.segments[pcnt].type +' ';
927 | if(hsmp.segments[pcnt].hasOwnProperty('x3')){
928 | str += _this.roundtodec( hsmp.segments[pcnt].x2 * options.scale ) +' '+ _this.roundtodec( hsmp.segments[pcnt].y2 * options.scale ) +' ';
929 | }
930 | str += _this.roundtodec( hsmp.segments[pcnt].x1 * options.scale ) +' '+ _this.roundtodec( hsmp.segments[pcnt].y1 * options.scale ) +' ';
931 | }
932 |
933 |
934 | }// End of creating hole path string
935 |
936 | str += 'Z '; // Close path
937 |
938 | }// End of holepath check
939 |
940 | // Closing path element
941 | str += '" />';
942 |
943 | // Rendering control points
944 | if(options.lcpr || options.qcpr){
945 | for(pcnt=0; pcnt';
948 | str += '';
949 | str += '';
950 | str += '';
951 | }
952 | if( (!smp.segments[pcnt].hasOwnProperty('x3')) && options.lcpr){
953 | str += '';
954 | }
955 | }
956 |
957 | // Hole children control points
958 | for( var hcnt=0; hcnt < smp.holechildren.length; hcnt++){
959 | var hsmp = layer[ smp.holechildren[hcnt] ];
960 | for(pcnt=0; pcnt';
963 | str += '';
964 | str += '';
965 | str += '';
966 | }
967 | if( (!hsmp.segments[pcnt].hasOwnProperty('x3')) && options.lcpr){
968 | str += '';
969 | }
970 | }
971 | }
972 | }// End of Rendering control points
973 |
974 | return str;
975 |
976 | },// End of svgpathstring()
977 |
978 | // Converting tracedata to an SVG string
979 | this.getsvgstring = function( tracedata, options ){
980 |
981 | options = _this.checkoptions(options);
982 |
983 | var w = tracedata.width * options.scale, h = tracedata.height * options.scale;
984 |
985 | // SVG start
986 | var svgstr = '';
1003 |
1004 | return svgstr;
1005 |
1006 | },// End of getsvgstring()
1007 |
1008 | // Comparator for numeric Array.sort
1009 | this.compareNumbers = function(a,b){ return a - b; },
1010 |
1011 | // Convert color object to rgba string
1012 | this.torgbastr = function(c){ return 'rgba('+c.r+','+c.g+','+c.b+','+c.a+')'; },
1013 |
1014 | // Convert color object to SVG color string
1015 | this.tosvgcolorstr = function(c, options){
1016 | return 'fill="rgb('+c.r+','+c.g+','+c.b+')" stroke="rgb('+c.r+','+c.g+','+c.b+')" stroke-width="'+options.strokewidth+'" opacity="'+c.a/255.0+'" ';
1017 | },
1018 |
1019 | // Helper function: Appending an