├── .editorconfig
├── .gitignore
├── .npmignore
├── .travis.yml
├── LICENSE
├── README.md
├── lib
├── Posterizer.js
├── Potrace.js
├── index.js
├── types
│ ├── Bitmap.js
│ ├── Curve.js
│ ├── Histogram.js
│ ├── Opti.js
│ ├── Path.js
│ ├── Point.js
│ ├── Quad.js
│ └── Sum.js
└── utils.js
├── package.json
└── test
├── example-output-posterized.svg
├── example-output.svg
├── reference-copies
├── output-posterized.svg
├── output.svg
├── posterized-bw-threshold-170.svg
├── posterized-clouds-white-40.svg
├── posterized-yao-black-threshold-128.svg
├── posterized-yao-black-threshold-170.svg
├── posterized-yao-black-threshold-65.svg
├── potrace-bw-black-threshold-0.svg
├── potrace-bw-black-threshold-255.svg
├── potrace-bw-threshold-128.svg
├── potrace-bw-threshold-170.svg
├── potrace-bw-threshold-65.svg
├── potrace-bw-white-threshold-0.svg
├── potrace-bw-white-threshold-255.svg
├── potrace-wb-black-threshold-0.svg
├── potrace-wb-black-threshold-255.svg
├── potrace-wb-threshold-128.svg
├── potrace-wb-white-threshold-0.svg
└── potrace-wb-white-threshold-255.svg
├── sources
├── Lenna.png
├── clouds.jpg
├── white-on-black.png
└── yao.jpg
└── test.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = false
6 | indent_style = space
7 | indent_size = 2
8 |
--------------------------------------------------------------------------------
/.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 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
18 | .grunt
19 |
20 | # node-waf configuration
21 | .lock-wscript
22 |
23 | # Compiled binary addons (http://nodejs.org/api/addons.html)
24 | build/Release
25 |
26 | # Dependency directory
27 | node_modules
28 |
29 | # Optional npm cache directory
30 | .npm
31 |
32 | # Optional REPL history
33 | .node_repl_history
34 |
35 | # JetBrains IDEs
36 | .idea
37 |
38 | # Test case output
39 | test/output*.svg
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
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 | # IDE files
30 | .project
31 | .idea
32 | *.iml
33 |
34 | # Test directory
35 | test/
36 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "0.12"
4 | - "4"
5 | - "6"
6 | sudo: false
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 2, June 1991
3 |
4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6 | Everyone is permitted to copy and distribute verbatim copies
7 | of this license document, but changing it is not allowed.
8 |
9 | Preamble
10 |
11 | The licenses for most software are designed to take away your
12 | freedom to share and change it. By contrast, the GNU General Public
13 | License is intended to guarantee your freedom to share and change free
14 | software--to make sure the software is free for all its users. This
15 | General Public License applies to most of the Free Software
16 | Foundation's software and to any other program whose authors commit to
17 | using it. (Some other Free Software Foundation software is covered by
18 | the GNU Lesser General Public License instead.) You can apply it to
19 | your programs, too.
20 |
21 | When we speak of free software, we are referring to freedom, not
22 | price. Our General Public Licenses are designed to make sure that you
23 | have the freedom to distribute copies of free software (and charge for
24 | this service if you wish), that you receive source code or can get it
25 | if you want it, that you can change the software or use pieces of it
26 | in new free programs; and that you know you can do these things.
27 |
28 | To protect your rights, we need to make restrictions that forbid
29 | anyone to deny you these rights or to ask you to surrender the rights.
30 | These restrictions translate to certain responsibilities for you if you
31 | distribute copies of the software, or if you modify it.
32 |
33 | For example, if you distribute copies of such a program, whether
34 | gratis or for a fee, you must give the recipients all the rights that
35 | you have. You must make sure that they, too, receive or can get the
36 | source code. And you must show them these terms so they know their
37 | rights.
38 |
39 | We protect your rights with two steps: (1) copyright the software, and
40 | (2) offer you this license which gives you legal permission to copy,
41 | distribute and/or modify the software.
42 |
43 | Also, for each author's protection and ours, we want to make certain
44 | that everyone understands that there is no warranty for this free
45 | software. If the software is modified by someone else and passed on, we
46 | want its recipients to know that what they have is not the original, so
47 | that any problems introduced by others will not reflect on the original
48 | authors' reputations.
49 |
50 | Finally, any free program is threatened constantly by software
51 | patents. We wish to avoid the danger that redistributors of a free
52 | program will individually obtain patent licenses, in effect making the
53 | program proprietary. To prevent this, we have made it clear that any
54 | patent must be licensed for everyone's free use or not licensed at all.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | GNU GENERAL PUBLIC LICENSE
60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61 |
62 | 0. This License applies to any program or other work which contains
63 | a notice placed by the copyright holder saying it may be distributed
64 | under the terms of this General Public License. The "Program", below,
65 | refers to any such program or work, and a "work based on the Program"
66 | means either the Program or any derivative work under copyright law:
67 | that is to say, a work containing the Program or a portion of it,
68 | either verbatim or with modifications and/or translated into another
69 | language. (Hereinafter, translation is included without limitation in
70 | the term "modification".) Each licensee is addressed as "you".
71 |
72 | Activities other than copying, distribution and modification are not
73 | covered by this License; they are outside its scope. The act of
74 | running the Program is not restricted, and the output from the Program
75 | is covered only if its contents constitute a work based on the
76 | Program (independent of having been made by running the Program).
77 | Whether that is true depends on what the Program does.
78 |
79 | 1. You may copy and distribute verbatim copies of the Program's
80 | source code as you receive it, in any medium, provided that you
81 | conspicuously and appropriately publish on each copy an appropriate
82 | copyright notice and disclaimer of warranty; keep intact all the
83 | notices that refer to this License and to the absence of any warranty;
84 | and give any other recipients of the Program a copy of this License
85 | along with the Program.
86 |
87 | You may charge a fee for the physical act of transferring a copy, and
88 | you may at your option offer warranty protection in exchange for a fee.
89 |
90 | 2. You may modify your copy or copies of the Program or any portion
91 | of it, thus forming a work based on the Program, and copy and
92 | distribute such modifications or work under the terms of Section 1
93 | above, provided that you also meet all of these conditions:
94 |
95 | a) You must cause the modified files to carry prominent notices
96 | stating that you changed the files and the date of any change.
97 |
98 | b) You must cause any work that you distribute or publish, that in
99 | whole or in part contains or is derived from the Program or any
100 | part thereof, to be licensed as a whole at no charge to all third
101 | parties under the terms of this License.
102 |
103 | c) If the modified program normally reads commands interactively
104 | when run, you must cause it, when started running for such
105 | interactive use in the most ordinary way, to print or display an
106 | announcement including an appropriate copyright notice and a
107 | notice that there is no warranty (or else, saying that you provide
108 | a warranty) and that users may redistribute the program under
109 | these conditions, and telling the user how to view a copy of this
110 | License. (Exception: if the Program itself is interactive but
111 | does not normally print such an announcement, your work based on
112 | the Program is not required to print an announcement.)
113 |
114 | These requirements apply to the modified work as a whole. If
115 | identifiable sections of that work are not derived from the Program,
116 | and can be reasonably considered independent and separate works in
117 | themselves, then this License, and its terms, do not apply to those
118 | sections when you distribute them as separate works. But when you
119 | distribute the same sections as part of a whole which is a work based
120 | on the Program, the distribution of the whole must be on the terms of
121 | this License, whose permissions for other licensees extend to the
122 | entire whole, and thus to each and every part regardless of who wrote it.
123 |
124 | Thus, it is not the intent of this section to claim rights or contest
125 | your rights to work written entirely by you; rather, the intent is to
126 | exercise the right to control the distribution of derivative or
127 | collective works based on the Program.
128 |
129 | In addition, mere aggregation of another work not based on the Program
130 | with the Program (or with a work based on the Program) on a volume of
131 | a storage or distribution medium does not bring the other work under
132 | the scope of this License.
133 |
134 | 3. You may copy and distribute the Program (or a work based on it,
135 | under Section 2) in object code or executable form under the terms of
136 | Sections 1 and 2 above provided that you also do one of the following:
137 |
138 | a) Accompany it with the complete corresponding machine-readable
139 | source code, which must be distributed under the terms of Sections
140 | 1 and 2 above on a medium customarily used for software interchange; or,
141 |
142 | b) Accompany it with a written offer, valid for at least three
143 | years, to give any third party, for a charge no more than your
144 | cost of physically performing source distribution, a complete
145 | machine-readable copy of the corresponding source code, to be
146 | distributed under the terms of Sections 1 and 2 above on a medium
147 | customarily used for software interchange; or,
148 |
149 | c) Accompany it with the information you received as to the offer
150 | to distribute corresponding source code. (This alternative is
151 | allowed only for noncommercial distribution and only if you
152 | received the program in object code or executable form with such
153 | an offer, in accord with Subsection b above.)
154 |
155 | The source code for a work means the preferred form of the work for
156 | making modifications to it. For an executable work, complete source
157 | code means all the source code for all modules it contains, plus any
158 | associated interface definition files, plus the scripts used to
159 | control compilation and installation of the executable. However, as a
160 | special exception, the source code distributed need not include
161 | anything that is normally distributed (in either source or binary
162 | form) with the major components (compiler, kernel, and so on) of the
163 | operating system on which the executable runs, unless that component
164 | itself accompanies the executable.
165 |
166 | If distribution of executable or object code is made by offering
167 | access to copy from a designated place, then offering equivalent
168 | access to copy the source code from the same place counts as
169 | distribution of the source code, even though third parties are not
170 | compelled to copy the source along with the object code.
171 |
172 | 4. You may not copy, modify, sublicense, or distribute the Program
173 | except as expressly provided under this License. Any attempt
174 | otherwise to copy, modify, sublicense or distribute the Program is
175 | void, and will automatically terminate your rights under this License.
176 | However, parties who have received copies, or rights, from you under
177 | this License will not have their licenses terminated so long as such
178 | parties remain in full compliance.
179 |
180 | 5. You are not required to accept this License, since you have not
181 | signed it. However, nothing else grants you permission to modify or
182 | distribute the Program or its derivative works. These actions are
183 | prohibited by law if you do not accept this License. Therefore, by
184 | modifying or distributing the Program (or any work based on the
185 | Program), you indicate your acceptance of this License to do so, and
186 | all its terms and conditions for copying, distributing or modifying
187 | the Program or works based on it.
188 |
189 | 6. Each time you redistribute the Program (or any work based on the
190 | Program), the recipient automatically receives a license from the
191 | original licensor to copy, distribute or modify the Program subject to
192 | these terms and conditions. You may not impose any further
193 | restrictions on the recipients' exercise of the rights granted herein.
194 | You are not responsible for enforcing compliance by third parties to
195 | this License.
196 |
197 | 7. If, as a consequence of a court judgment or allegation of patent
198 | infringement or for any other reason (not limited to patent issues),
199 | conditions are imposed on you (whether by court order, agreement or
200 | otherwise) that contradict the conditions of this License, they do not
201 | excuse you from the conditions of this License. If you cannot
202 | distribute so as to satisfy simultaneously your obligations under this
203 | License and any other pertinent obligations, then as a consequence you
204 | may not distribute the Program at all. For example, if a patent
205 | license would not permit royalty-free redistribution of the Program by
206 | all those who receive copies directly or indirectly through you, then
207 | the only way you could satisfy both it and this License would be to
208 | refrain entirely from distribution of the Program.
209 |
210 | If any portion of this section is held invalid or unenforceable under
211 | any particular circumstance, the balance of the section is intended to
212 | apply and the section as a whole is intended to apply in other
213 | circumstances.
214 |
215 | It is not the purpose of this section to induce you to infringe any
216 | patents or other property right claims or to contest validity of any
217 | such claims; this section has the sole purpose of protecting the
218 | integrity of the free software distribution system, which is
219 | implemented by public license practices. Many people have made
220 | generous contributions to the wide range of software distributed
221 | through that system in reliance on consistent application of that
222 | system; it is up to the author/donor to decide if he or she is willing
223 | to distribute software through any other system and a licensee cannot
224 | impose that choice.
225 |
226 | This section is intended to make thoroughly clear what is believed to
227 | be a consequence of the rest of this License.
228 |
229 | 8. If the distribution and/or use of the Program is restricted in
230 | certain countries either by patents or by copyrighted interfaces, the
231 | original copyright holder who places the Program under this License
232 | may add an explicit geographical distribution limitation excluding
233 | those countries, so that distribution is permitted only in or among
234 | countries not thus excluded. In such case, this License incorporates
235 | the limitation as if written in the body of this License.
236 |
237 | 9. The Free Software Foundation may publish revised and/or new versions
238 | of the General Public License from time to time. Such new versions will
239 | be similar in spirit to the present version, but may differ in detail to
240 | address new problems or concerns.
241 |
242 | Each version is given a distinguishing version number. If the Program
243 | specifies a version number of this License which applies to it and "any
244 | later version", you have the option of following the terms and conditions
245 | either of that version or of any later version published by the Free
246 | Software Foundation. If the Program does not specify a version number of
247 | this License, you may choose any version ever published by the Free Software
248 | Foundation.
249 |
250 | 10. If you wish to incorporate parts of the Program into other free
251 | programs whose distribution conditions are different, write to the author
252 | to ask for permission. For software which is copyrighted by the Free
253 | Software Foundation, write to the Free Software Foundation; we sometimes
254 | make exceptions for this. Our decision will be guided by the two goals
255 | of preserving the free status of all derivatives of our free software and
256 | of promoting the sharing and reuse of software generally.
257 |
258 | NO WARRANTY
259 |
260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268 | REPAIR OR CORRECTION.
269 |
270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278 | POSSIBILITY OF SUCH DAMAGES.
279 |
280 | END OF TERMS AND CONDITIONS
281 |
282 | How to Apply These Terms to Your New Programs
283 |
284 | If you develop a new program, and you want it to be of the greatest
285 | possible use to the public, the best way to achieve this is to make it
286 | free software which everyone can redistribute and change under these terms.
287 |
288 | To do so, attach the following notices to the program. It is safest
289 | to attach them to the start of each source file to most effectively
290 | convey the exclusion of warranty; and each file should have at least
291 | the "copyright" line and a pointer to where the full notice is found.
292 |
293 | {description}
294 | Copyright (C) {year} {fullname}
295 |
296 | This program is free software; you can redistribute it and/or modify
297 | it under the terms of the GNU General Public License as published by
298 | the Free Software Foundation; either version 2 of the License, or
299 | (at your option) any later version.
300 |
301 | This program is distributed in the hope that it will be useful,
302 | but WITHOUT ANY WARRANTY; without even the implied warranty of
303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
304 | GNU General Public License for more details.
305 |
306 | You should have received a copy of the GNU General Public License along
307 | with this program; if not, write to the Free Software Foundation, Inc.,
308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
309 |
310 | Also add information on how to contact you by electronic and paper mail.
311 |
312 | If the program is interactive, make it output a short notice like this
313 | when it starts in an interactive mode:
314 |
315 | Gnomovision version 69, Copyright (C) year name of author
316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
317 | This is free software, and you are welcome to redistribute it
318 | under certain conditions; type `show c' for details.
319 |
320 | The hypothetical commands `show w' and `show c' should show the appropriate
321 | parts of the General Public License. Of course, the commands you use may
322 | be called something other than `show w' and `show c'; they could even be
323 | mouse-clicks or menu items--whatever suits your program.
324 |
325 | You should also get your employer (if you work as a programmer) or your
326 | school, if any, to sign a "copyright disclaimer" for the program, if
327 | necessary. Here is a sample; alter the names:
328 |
329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program
330 | `Gnomovision' (which makes passes at compilers) written by James Hacker.
331 |
332 | {signature of Ty Coon}, 1 April 1989
333 | Ty Coon, President of Vice
334 |
335 | This General Public License does not permit incorporating your program into
336 | proprietary programs. If your program is a subroutine library, you may
337 | consider it more useful to permit linking proprietary applications with the
338 | library. If this is what you want to do, use the GNU Lesser General
339 | Public License instead of this License.
340 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # node-potrace
2 | A NodeJS-compatible fork of [Potrace in JavaScript][potrace-by-kilobtye] with some additions, which is in turn a port of [the original Potrace][potrace] — a tool for tracing bitmaps.
3 |
4 | ## Example and demo
5 |
6 | | **Original image** | **Potrace output** | **Posterized output** |
7 | |---------------------------|------------------------------|-----------------------------------------|
8 | |  |  |  |
9 |
10 | (Example image inherited from [online demo of the browser version][potrace-js-demo])
11 |
12 | ## Usage
13 |
14 | Install
15 |
16 | ```sh
17 | npm install potrace
18 | ```
19 |
20 | Basic usage
21 |
22 | ```js
23 | var potrace = require('potrace'),
24 | fs = require('fs');
25 |
26 | potrace.trace('./path/to/image.png', function(err, svg) {
27 | if (err) throw err;
28 | fs.writeFileSync('./output.svg', svg);
29 | });
30 | ```
31 |
32 | You can also provide a configuration object as a second argument.
33 |
34 | ```js
35 | var params = {
36 | background: '#49ffd2',
37 | color: 'blue',
38 | threshold: 120
39 | };
40 |
41 | potrace.trace('./path/to/image.png', params, function(err, svg) {
42 | /*...*/
43 | });
44 | ```
45 |
46 | If you want to run Potrace algorithm multiple times on the same image with different threshold setting and merge results together in a single file - `posterize` method does exactly that.
47 |
48 | ```js
49 | potrace.posterize('./path/to/image.png', { threshold: 180, steps: 4 }, function(err, svg) {
50 | /*...*/
51 | });
52 |
53 | // or if you know exactly where you want to break it on different levels
54 |
55 | potrace.posterize('./path/to/image.png', { steps: [40, 85, 135, 180] }, function(err, svg) {
56 | /*...*/
57 | });
58 | ```
59 |
60 | ### Advanced usage and configuration
61 |
62 | Both `trace` and `posterize` methods return instances of `Potrace` and `Posterizer` classes respectively to a callback function as third argument.
63 |
64 | You can also instantiate these classes directly:
65 |
66 | ```js
67 | var potrace = require('potrace');
68 |
69 | // Tracing
70 |
71 | var trace = new potrace.Potrace();
72 |
73 | // You can also pass configuration object to the constructor
74 | trace.setParameters({
75 | threshold: 128,
76 | color: '#880000'
77 | });
78 |
79 | trace.loadImage('path/to/image.png', function(err) {
80 | if (err) throw err;
81 |
82 | trace.getSVG(); // returns SVG document contents
83 | trace.getPathTag(); // will return just tag
84 | trace.getSymbol('traced-image'); // will return tag with given ID
85 | });
86 |
87 | // Posterization
88 |
89 | var posterizer = new potrace.Posterize();
90 |
91 | posterizer.loadImage('path/to/image.png', function(err) {
92 | if (err) throw err;
93 |
94 | posterizer.setParameter({
95 | color: '#ccc',
96 | background: '#222',
97 | steps: 3,
98 | threshold: 200,
99 | fillStrategy: potrace.Posterize.FILL_MEAN
100 | });
101 |
102 | posterizer.getSVG();
103 | // or
104 | posterizer.getSymbol('posterized-image');
105 | });
106 | ```
107 |
108 | Callback function provided to `loadImage` methods will be executed in context of the `Potrace`/`Posterizer` instance, so if it doesn't go against your code style - you can just do
109 |
110 | ```js
111 | new potrace.Potrace()
112 | .loadImage('path/to/image.bmp', function() {
113 | if (err) throw err;
114 | this.getSymbol('foo');
115 | });
116 | ```
117 |
118 | [Jimp module][jimp] is used on the back end, so first argument accepted by `loadImage` method could be anything Jimp can read: a `Buffer`, local path or a url string. Supported formats are: PNG, JPEG or BMP. It also could be a Jimp instance (provided bitmap is not modified)
119 |
120 | ### Parameters
121 |
122 | `Potrace` class expects following parameters:
123 |
124 | - **turnPolicy** - how to resolve ambiguities in path decomposition. Possible values are exported as constants: `TURNPOLICY_BLACK`, `TURNPOLICY_WHITE`, `TURNPOLICY_LEFT`, `TURNPOLICY_RIGHT`, `TURNPOLICY_MINORITY`, `TURNPOLICY_MAJORITY`. Refer to [this document][potrace-algorithm] for more information (page 4)
125 | (default: `TURNPOLICY_MINORITY`)
126 | - **turdSize** - suppress speckles of up to this size
127 | (default: 2)
128 | - **alphaMax** - corner threshold parameter
129 | (default: 1)
130 | - **optCurve** - curve optimization
131 | (default: true)
132 | - **optTolerance** - curve optimization tolerance
133 | (default: 0.2)
134 | - **threshold** - threshold below which color is considered black.
135 | Should be a number in range 0..255 or `THRESHOLD_AUTO` in which case threshold will be selected automatically using [Algorithm For Multilevel Thresholding][multilevel-thresholding]
136 | (default: `THRESHOLD_AUTO`)
137 | - **blackOnWhite** - specifies colors by which side from threshold should be turned into vector shape
138 | (default: `true`)
139 | - **color** - Fill color. Will be ignored when exporting as \. (default: `COLOR_AUTO`, which means black or white, depending on `blackOnWhite` property)
140 | - **background** - Background color. Will be ignored when exporting as \. By default is not present (`COLOR_TRANSPARENT`)
141 |
142 | ---------------
143 |
144 | `Posterizer` class has same methods as `Potrace`, in exception of `.getPathTag()`.
145 | Configuration object is extended with following properties:
146 |
147 | - **fillStrategy** - determines how fill color for each layer should be selected. Possible values are exported as constants:
148 | - `FILL_DOMINANT` - most frequent color in range (used by default),
149 | - `FILL_MEAN` - arithmetic mean (average),
150 | - `FILL_MEDIAN` - median color,
151 | - `FILL_SPREAD` - ignores color information of the image and just spreads colors equally in range 0..\ (or \..255 if `blackOnWhite` is set to `false`),
152 | - **rangeDistribution** - how color stops for each layer should be selected. Ignored if `steps` is an array. Possible values are:
153 | - `RANGES_AUTO` - Performs automatic thresholding (using [Algorithm For Multilevel Thresholding][multilevel-thresholding]). Preferable method for already posterized sources, but takes long time to calculate 5 or more thresholds (exponential time complexity)
154 | *(used by default)*
155 | - `RANGES_EQUAL` - Ignores color information of the image and breaks available color space into equal chunks
156 | - **steps** - Specifies desired number of layers in resulting image. If a number provided - thresholds for each layer will be automatically calculated according to `rangeDistribution` parameter. If an array provided it expected to be an array with precomputed thresholds for each layer (in range 0..255)
157 | (default: `STEPS_AUTO` which will result in `3` or `4`, depending on `threshold` value)
158 | - **threshold** - Breaks image into foreground and background (and only foreground being broken into desired number of layers). Basically when provided it becomes a threshold for last (least opaque) layer and then `steps - 1` intermediate thresholds calculated. If **steps** is an array of thresholds and every value from the array is lower (or larger if **blackOnWhite** parameter set to `false`) than threshold - threshold will be added to the array, otherwise just ignored.
159 | (default: `Potrace.THRESHOLD_AUTO`)
160 | - *all other parameters that Potrace class accepts*
161 |
162 | **Notes:**
163 |
164 | - When number of `steps` is greater than 10 - an extra layer could be added to ensure presence of darkest/brightest colors if needed to ensure presence of probably-important-at-this-point details like shadows or line art.
165 | - With big number of layers produced image will be looking brighter overall than original due to math error at the rendering phase because of how layers are composited.
166 | - With default configuration `steps`, `threshold` and `rangeDistribution` settings all set to auto, resulting in a 4 thresholds/color stops being calculated with Multilevel Thresholding algorithm mentioned above. Calculation of 4 thresholds takes 3-5 seconds on average laptop. You may want to explicitly limit number of `steps` to 3 to moderately improve processing speed.
167 |
168 | ## Thanks to
169 |
170 | - Peter Selinger for [original Potrace tool and algorithm][potrace]
171 | - @kilobtye for original [javascript port][potrace-by-kilobtye]
172 |
173 | ## License
174 |
175 | The GNU General Public License version 2 (GPLv2). Please see [License File](LICENSE) for more information.
176 |
177 | [potrace]: http://potrace.sourceforge.net/
178 | [potrace-algorithm]: http://potrace.sourceforge.net/potrace.pdf
179 | [multilevel-thresholding]: http://www.iis.sinica.edu.tw/page/jise/2001/200109_01.pdf
180 | [potrace-by-kilobtye]: https://github.com/kilobtye/potrace
181 | [potrace-js-demo]: http://kilobtye.github.io/potrace/
182 | [jimp]: https://github.com/oliver-moran/jimp
--------------------------------------------------------------------------------
/lib/Posterizer.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var Potrace = require('./Potrace');
4 | var utils = require('./utils');
5 |
6 | /**
7 | * Takes multiple samples using {@link Potrace} with different threshold
8 | * settings and combines output into a single file.
9 | *
10 | * @param {Posterizer~Options} [options]
11 | * @constructor
12 | */
13 | function Posterizer(options) {
14 | this._potrace = new Potrace();
15 |
16 | this._calculatedThreshold = null;
17 |
18 | this._params = {
19 | threshold: Potrace.THRESHOLD_AUTO,
20 | blackOnWhite: true,
21 | steps: Posterizer.STEPS_AUTO,
22 | background: Potrace.COLOR_TRANSPARENT,
23 | fillStrategy: Posterizer.FILL_DOMINANT,
24 | rangeDistribution: Posterizer.RANGES_AUTO
25 | };
26 |
27 | if (options) {
28 | this.setParameters(options);
29 | }
30 | }
31 |
32 | // Inherit constants from Potrace class
33 | for (var key in Potrace) {
34 | if (Object.prototype.hasOwnProperty.call(Potrace, key) && key === key.toUpperCase()) {
35 | Posterizer[key] = Potrace[key];
36 | }
37 | }
38 |
39 | Posterizer.STEPS_AUTO = -1;
40 | Posterizer.FILL_SPREAD = 'spread';
41 | Posterizer.FILL_DOMINANT = 'dominant';
42 | Posterizer.FILL_MEDIAN = 'median';
43 | Posterizer.FILL_MEAN = 'mean';
44 |
45 | Posterizer.RANGES_AUTO = 'auto';
46 | Posterizer.RANGES_EQUAL = 'equal';
47 |
48 | Posterizer.prototype = {
49 | /**
50 | * Fine tuning to color ranges.
51 | *
52 | * If last range (featuring most saturated color) is larger than 10% of color space (25 units)
53 | * then we want to add another color stop, that hopefully will include darkest pixels, improving presence of
54 | * shadows and line art
55 | *
56 | * @param ranges
57 | * @private
58 | */
59 | _addExtraColorStop: function(ranges) {
60 | var blackOnWhite = this._params.blackOnWhite;
61 | var lastColorStop = ranges[ranges.length - 1];
62 | var lastRangeFrom = blackOnWhite ? 0 : lastColorStop.value;
63 | var lastRangeTo = blackOnWhite ? lastColorStop.value : 255;
64 |
65 | if (lastRangeTo - lastRangeFrom > 25 && lastColorStop.colorIntensity !== 1) {
66 | var histogram = this._getImageHistogram();
67 | var levels = histogram.getStats(lastRangeFrom, lastRangeTo).levels;
68 |
69 | var newColorStop = levels.mean + levels.stdDev <= 25 ? levels.mean + levels.stdDev
70 | : levels.mean - levels.stdDev <= 25 ? levels.mean - levels.stdDev
71 | : 25;
72 |
73 | var newStats = (blackOnWhite ? histogram.getStats(0, newColorStop) : histogram.getStats(newColorStop, 255));
74 | var color = newStats.levels.mean;
75 |
76 | ranges.push({
77 | value: Math.abs((blackOnWhite ? 0 : 255) - newColorStop),
78 | colorIntensity: isNaN(color) ? 0 : ((blackOnWhite ? 255 - color : color) / 255)
79 | });
80 | }
81 |
82 | return ranges;
83 | },
84 |
85 |
86 | /**
87 | * Calculates color intensity for each element of numeric array
88 | *
89 | * @param {number[]} colorStops
90 | * @returns {{ levels: number, colorIntensity: number }[]}
91 | * @private
92 | */
93 | _calcColorIntensity: function(colorStops) {
94 | var blackOnWhite = this._params.blackOnWhite;
95 | var colorSelectionStrat = this._params.fillStrategy;
96 | var histogram = colorSelectionStrat !== Posterizer.FILL_SPREAD ? this._getImageHistogram() : null;
97 | var fullRange = Math.abs(this._paramThreshold() - (blackOnWhite ? 0 : 255));
98 |
99 | return colorStops.map(function(threshold, index) {
100 | var nextValue = index + 1 === colorStops.length ? (blackOnWhite ? -1 : 256) : colorStops[index + 1];
101 | var rangeStart = Math.round(blackOnWhite ? nextValue + 1 : threshold);
102 | var rangeEnd = Math.round(blackOnWhite ? threshold : nextValue - 1);
103 | var factor = index / (colorStops.length - 1);
104 | var intervalSize = rangeEnd - rangeStart;
105 | var stats = histogram.getStats(rangeStart, rangeEnd);
106 | var color = -1;
107 |
108 | if (stats.pixels === 0) {
109 | return {
110 | value: threshold,
111 | colorIntensity: 0
112 | };
113 | }
114 |
115 | switch (colorSelectionStrat) {
116 | case Posterizer.FILL_SPREAD:
117 | // We want it to be 0 (255 when white on black) at the most saturated end, so...
118 | color = (blackOnWhite ? rangeStart : rangeEnd)
119 | + (blackOnWhite ? 1 : -1) * intervalSize * Math.max(0.5, fullRange / 255) * factor;
120 | break;
121 | case Posterizer.FILL_DOMINANT:
122 | color = histogram.getDominantColor(rangeStart, rangeEnd, utils.clamp(intervalSize, 1, 5));
123 | break;
124 | case Posterizer.FILL_MEAN:
125 | color = stats.levels.mean;
126 | break;
127 | case Posterizer.FILL_MEDIAN:
128 | color = stats.levels.median;
129 | break;
130 | }
131 |
132 | // We don't want colors to be too close to each other, so we introduce some spacing in between
133 | if (index !== 0) {
134 | color = blackOnWhite
135 | ? utils.clamp(color, rangeStart, rangeEnd - Math.round(intervalSize * 0.1))
136 | : utils.clamp(color, rangeStart + Math.round(intervalSize * 0.1), rangeEnd);
137 | }
138 |
139 | return {
140 | value: threshold,
141 | colorIntensity: color === -1 ? 0 : ((blackOnWhite ? 255 - color : color) / 255)
142 | };
143 | });
144 | },
145 |
146 | /**
147 | * @returns {Histogram}
148 | * @private
149 | */
150 | _getImageHistogram: function() {
151 | return this._potrace._luminanceData.histogram();
152 | },
153 |
154 | /**
155 | * Processes threshold, steps and rangeDistribution parameters and returns normalized array of color stops
156 | * @returns {*}
157 | * @private
158 | */
159 | _getRanges: function() {
160 | var steps = this._paramSteps();
161 |
162 | if (!Array.isArray(steps)) {
163 | return this._params.rangeDistribution === Posterizer.RANGES_AUTO
164 | ? this._getRangesAuto()
165 | : this._getRangesEquallyDistributed();
166 | }
167 |
168 | // Steps is array of thresholds and we want to preprocess it
169 |
170 | var colorStops = [];
171 | var threshold = this._paramThreshold();
172 | var lookingForDarkPixels = this._params.blackOnWhite;
173 |
174 | steps.forEach(function(item) {
175 | if (colorStops.indexOf(item) === -1 && utils.between(item, 0, 255)) {
176 | colorStops.push(item);
177 | }
178 | });
179 |
180 | if (!colorStops.length) {
181 | colorStops.push(threshold);
182 | }
183 |
184 | colorStops = colorStops.sort(function (a, b) {
185 | return a < b === lookingForDarkPixels ? 1 : -1;
186 | });
187 |
188 | if (lookingForDarkPixels && colorStops[0] < threshold) {
189 | colorStops.unshift(threshold);
190 | } else if (!lookingForDarkPixels && colorStops[colorStops.length - 1] < threshold) {
191 | colorStops.push(threshold);
192 | }
193 |
194 | return this._calcColorIntensity(colorStops);
195 | },
196 |
197 | /**
198 | * Calculates given (or lower) number of thresholds using automatic thresholding algorithm
199 | * @returns {*}
200 | * @private
201 | */
202 | _getRangesAuto: function() {
203 | var histogram = this._getImageHistogram();
204 | var steps = this._paramSteps(true);
205 | var colorStops;
206 |
207 | if (this._params.threshold === Potrace.THRESHOLD_AUTO) {
208 | colorStops = histogram.multilevelThresholding(steps);
209 | } else {
210 | var threshold = this._paramThreshold();
211 |
212 | colorStops = this._params.blackOnWhite
213 | ? histogram.multilevelThresholding(steps - 1, 0, threshold)
214 | : histogram.multilevelThresholding(steps - 1, threshold, 255);
215 |
216 | if (this._params.blackOnWhite) {
217 | colorStops.push(threshold);
218 | } else {
219 | colorStops.unshift(threshold);
220 | }
221 | }
222 |
223 | if (this._params.blackOnWhite) {
224 | colorStops = colorStops.reverse();
225 | }
226 |
227 | return this._calcColorIntensity(colorStops);
228 | },
229 |
230 | /**
231 | * Calculates color stops and color representing each segment, returning them
232 | * from least to most intense color (black or white, depending on blackOnWhite parameter)
233 | *
234 | * @private
235 | */
236 | _getRangesEquallyDistributed: function() {
237 | var blackOnWhite = this._params.blackOnWhite;
238 | var colorsToThreshold = blackOnWhite ? this._paramThreshold() : 255 - this._paramThreshold();
239 | var steps = this._paramSteps();
240 |
241 | var stepSize = colorsToThreshold / steps;
242 | var colorStops = [];
243 | var i = steps - 1,
244 | factor,
245 | threshold;
246 |
247 | while (i >= 0) {
248 | factor = i / (steps - 1);
249 | threshold = Math.min(colorsToThreshold, (i + 1) * stepSize);
250 | threshold = blackOnWhite ? threshold : 255 - threshold;
251 | i--;
252 |
253 | colorStops.push(threshold);
254 | }
255 |
256 | return this._calcColorIntensity(colorStops);
257 | },
258 |
259 | /**
260 | * Returns valid steps value
261 | * @param {Boolean} [count=false]
262 | * @returns {number|number[]}
263 | * @private
264 | */
265 | _paramSteps: function(count) {
266 | var steps = this._params.steps;
267 |
268 | if (Array.isArray(steps)) {
269 | return count ? steps.length : steps;
270 | }
271 |
272 | if (steps === Posterizer.STEPS_AUTO && this._params.threshold === Potrace.THRESHOLD_AUTO) {
273 | return 4;
274 | }
275 |
276 | var blackOnWhite = this._params.blackOnWhite;
277 | var colorsCount = blackOnWhite ? this._paramThreshold() : 255 - this._paramThreshold();
278 |
279 | return steps === Posterizer.STEPS_AUTO
280 | ? (colorsCount > 200 ? 4 : 3)
281 | : Math.min(colorsCount, Math.max(2, steps));
282 | },
283 |
284 | /**
285 | * Returns valid threshold value
286 | * @returns {number}
287 | * @private
288 | */
289 | _paramThreshold: function() {
290 | if (this._calculatedThreshold !== null) {
291 | return this._calculatedThreshold;
292 | }
293 |
294 | if (this._params.threshold !== Potrace.THRESHOLD_AUTO) {
295 | this._calculatedThreshold = this._params.threshold;
296 | return this._calculatedThreshold;
297 | }
298 |
299 | var twoThresholds = this._getImageHistogram().multilevelThresholding(2);
300 | this._calculatedThreshold = this._params.blackOnWhite ? twoThresholds[1] : twoThresholds[0];
301 | this._calculatedThreshold = this._calculatedThreshold || 128;
302 |
303 | return this._calculatedThreshold;
304 | },
305 |
306 | /**
307 | * Running potrace on the image multiple times with different thresholds and returns an array
308 | * of path tags
309 | *
310 | * @param {Boolean} [noFillColor]
311 | * @returns {string[]}
312 | * @private
313 | */
314 | _pathTags: function(noFillColor) {
315 | var ranges = this._getRanges();
316 | var potrace = this._potrace;
317 | var blackOnWhite = this._params.blackOnWhite;
318 |
319 | if (ranges.length >= 10) {
320 | ranges = this._addExtraColorStop(ranges);
321 | }
322 |
323 | potrace.setParameters({ blackOnWhite: blackOnWhite });
324 |
325 | var actualPrevLayersOpacity = 0;
326 |
327 | return ranges.map(function(colorStop) {
328 | var thisLayerOpacity = colorStop.colorIntensity;
329 |
330 | if (thisLayerOpacity === 0) {
331 | return '';
332 | }
333 |
334 | // NOTE: With big number of layers (something like 70) there will be noticeable math error on rendering side.
335 | // In Chromium at least image will end up looking brighter overall compared to the same layers painted in solid colors.
336 | // However it works fine with sane number of layers, and it's not like we can do much about it.
337 |
338 | var calculatedOpacity = (!actualPrevLayersOpacity || thisLayerOpacity === 1)
339 | ? thisLayerOpacity
340 | : ((actualPrevLayersOpacity - thisLayerOpacity) / (actualPrevLayersOpacity - 1));
341 |
342 | calculatedOpacity = utils.clamp(parseFloat(calculatedOpacity.toFixed(3)), 0, 1);
343 | actualPrevLayersOpacity = actualPrevLayersOpacity + (1 - actualPrevLayersOpacity) * calculatedOpacity;
344 |
345 | potrace.setParameters({ threshold: colorStop.value });
346 |
347 | var element = noFillColor ? potrace.getPathTag('') : potrace.getPathTag();
348 | element = utils.setHtmlAttr(element, 'fill-opacity', calculatedOpacity.toFixed(3));
349 |
350 | var canBeIgnored = calculatedOpacity === 0 || element.indexOf(' d=""') !== -1;
351 |
352 | // var c = Math.round(Math.abs((blackOnWhite ? 255 : 0) - 255 * thisLayerOpacity));
353 | // element = utils.setHtmlAttr(element, 'fill', 'rgb('+c+', '+c+', '+c+')');
354 | // element = utils.setHtmlAttr(element, 'fill-opacity', '');
355 |
356 | return canBeIgnored ? '' : element;
357 | });
358 | },
359 |
360 | /**
361 | * Loads image.
362 | *
363 | * @param {string|Buffer|Jimp} target Image source. Could be anything that {@link Jimp} can read (buffer, local path or url). Supported formats are: PNG, JPEG or BMP
364 | * @param {Function} callback
365 | */
366 | loadImage: function(target, callback) {
367 | var self = this;
368 |
369 | this._potrace.loadImage(target, function(err) {
370 | self._calculatedThreshold = null;
371 | callback.call(self, err);
372 | });
373 | },
374 |
375 | /**
376 | * Sets parameters. Accepts same object as {Potrace}
377 | *
378 | * @param {Posterizer~Options} params
379 | */
380 | setParameters: function(params) {
381 | if (!params) {
382 | return;
383 | }
384 |
385 | this._potrace.setParameters(params);
386 |
387 | if (params.steps && !Array.isArray(params.steps) && (!utils.isNumber(params.steps) || !utils.between(params.steps, 1, 255))) {
388 | throw new Error('Bad \'steps\' value');
389 | }
390 |
391 | for (var key in this._params) {
392 | if (this._params.hasOwnProperty(key) && params.hasOwnProperty(key)) {
393 | this._params[key] = params[key];
394 | }
395 | }
396 |
397 | this._calculatedThreshold = null;
398 | },
399 |
400 | /**
401 | * Returns image as tag. Always has viewBox specified
402 | *
403 | * @param {string} id
404 | */
405 | getSymbol: function(id) {
406 | var width = this._potrace._luminanceData.width;
407 | var height = this._potrace._luminanceData.height;
408 | var paths = this._pathTags(true);
409 |
410 | return '' +
411 | paths.join('') +
412 | '';
413 | },
414 |
415 | /**
416 | * Generates SVG image
417 | * @returns {String}
418 | */
419 | getSVG: function() {
420 | var width = this._potrace._luminanceData.width,
421 | height = this._potrace._luminanceData.height;
422 |
423 | var tags = this._pathTags(false);
424 |
425 | var svg = '';
435 |
436 | return svg.replace(/\n(?:\t*\n)+(\t*)/g, '\n$1');
437 | }
438 | };
439 |
440 | module.exports = Posterizer;
441 |
442 | /**
443 | * Posterizer options
444 | *
445 | * @typedef {Potrace~Options} Posterizer~Options
446 | * @property {Number} [steps] - Number of samples that needs to be taken (and number of layers in SVG). (default: Posterizer.STEPS_AUTO, which most likely will result in 3, sometimes 4)
447 | * @property {*} [fillStrategy] - How to select fill color for color ranges - equally spread or dominant. (default: Posterizer.FILL_DOMINANT)
448 | * @property {*} [rangeDistribution] - How to choose thresholds in-between - after equal intervals or automatically balanced. (default: Posterizer.RANGES_AUTO)
449 | */
450 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var Potrace = require('./Potrace');
4 | var Posterizer = require('./Posterizer');
5 |
6 | /**
7 | * Wrapper for Potrace that simplifies use down to one function call
8 | *
9 | * @param {string|Buffer|Jimp} file Source image, file path or {@link Jimp} instance
10 | * @param {Potrace~Options} [options]
11 | * @param {traceCallback} cb Callback function. Accepts 3 arguments: error, svg content and instance of {@link Potrace} (so it could be reused with different set of parameters)
12 | */
13 | function trace(file, options, cb) {
14 | if (arguments.length === 2) {
15 | cb = options;
16 | options = {};
17 | }
18 |
19 | var potrace = new Potrace(options);
20 |
21 | potrace.loadImage(file, function(err) {
22 | if (err) { return cb(err); }
23 | cb(null, potrace.getSVG(), potrace);
24 | });
25 | }
26 |
27 | /**
28 | * Wrapper for Potrace that simplifies use down to one function call
29 | *
30 | * @param {string|Buffer|Jimp} file Source image, file path or {@link Jimp} instance
31 | * @param {Posterizer~Options} [options]
32 | * @param {posterizeCallback} cb Callback function. Accepts 3 arguments: error, svg content and instance of {@link Potrace} (so it could be reused with different set of parameters)
33 | */
34 | function posterize(file, options, cb) {
35 | if (arguments.length === 2) {
36 | cb = options;
37 | options = {};
38 | }
39 |
40 | var posterizer = new Posterizer(options);
41 |
42 | posterizer.loadImage(file, function(err) {
43 | if (err) { return cb(err); }
44 | cb(null, posterizer.getSVG(), posterizer);
45 | });
46 | }
47 |
48 | module.exports = {
49 | trace: trace,
50 | posterize: posterize,
51 | Potrace: Potrace,
52 | Posterizer: Posterizer
53 | };
54 |
55 | /**
56 | * Callback for trace method
57 | * @callback traceCallback
58 | * @param {Error|null} err
59 | * @param {string} svg SVG document contents
60 | * @param {Potrace} [instance] Potrace class instance
61 | */
62 |
63 | /**
64 | * Callback for posterize method
65 | * @callback posterizeCallback
66 | * @param {Error|null} err
67 | * @param {string} svg SVG document contents
68 | * @param {Posterizer} [instance] Posterizer class instance
69 | */
--------------------------------------------------------------------------------
/lib/types/Bitmap.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var Point = require('./Point');
4 | var utils = require('../utils');
5 | var Histogram;
6 |
7 | /**
8 | * Represents a bitmap where each pixel can be a number in range of 0..255
9 | * Used internally to store luminance data.
10 | *
11 | * @param {Number} w
12 | * @param {Number} h
13 | * @constructor
14 | */
15 | function Bitmap(w, h) {
16 | this._histogram = null;
17 |
18 | this.width = w;
19 | this.height = h;
20 | this.size = w * h;
21 | this.arrayBuffer = new ArrayBuffer(this.size);
22 | this.data = new Uint8Array(this.arrayBuffer);
23 | }
24 |
25 | module.exports = Bitmap;
26 | Histogram = require('./Histogram');
27 |
28 | Bitmap.prototype = {
29 | /**
30 | * Returns pixel value
31 | *
32 | * @param {Number|Point} x - index, point or x
33 | * @param {Number} [y]
34 | */
35 | getValueAt: function(x, y) {
36 | var index = (typeof x === 'number' && typeof y !== 'number') ? x : this.pointToIndex(x, y);
37 | return this.data[index];
38 | },
39 |
40 | /**
41 | * Converts {@link Point} to index value
42 | *
43 | * @param {Number} index
44 | * @returns {Point}
45 | */
46 | indexToPoint: function(index) {
47 | var point = new Point();
48 |
49 | if (utils.between(index, 0, this.size)) {
50 | point.y = Math.floor(index / this.width);
51 | point.x = index - point.y * this.width;
52 | } else {
53 | point.x = -1;
54 | point.y = -1;
55 | }
56 |
57 | return point;
58 | },
59 |
60 | /**
61 | * Calculates index for point or coordinate pair
62 | *
63 | * @param {Number|Point} pointOrX
64 | * @param {Number} [y]
65 | * @returns {Number}
66 | */
67 | pointToIndex: function(pointOrX, y) {
68 | var _x = pointOrX,
69 | _y = y;
70 |
71 | if (pointOrX instanceof Point) {
72 | _x = pointOrX.x;
73 | _y = pointOrX.y;
74 | }
75 |
76 | if (!utils.between(_x, 0, this.width) || !utils.between(_y, 0, this.height)) {
77 | return -1;
78 | }
79 |
80 | return this.width * _y + _x;
81 | },
82 |
83 | /**
84 | * Makes a copy of current bitmap
85 | *
86 | * @param {Function} [iterator] optional callback, used for processing pixel value. Accepted arguments: value, index
87 | * @returns {Bitmap}
88 | */
89 | copy: function(iterator) {
90 | var bm = new Bitmap(this.width, this.height),
91 | iteratorPresent = typeof iterator === 'function',
92 | i;
93 |
94 | for (i = 0; i < this.size; i++) {
95 | bm.data[i] = iteratorPresent ? iterator(this.data[i], i) : this.data[i];
96 | }
97 |
98 | return bm;
99 | },
100 |
101 | histogram: function() {
102 | var Histogram = require('./Histogram');
103 |
104 | if (this._histogram) {
105 | return this._histogram;
106 | }
107 |
108 | this._histogram = new Histogram(this);
109 | return this._histogram;
110 | }
111 | };
--------------------------------------------------------------------------------
/lib/types/Curve.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /**
4 | * Curve type
5 | *
6 | * @param n
7 | * @constructor
8 | * @protected
9 | */
10 | function Curve(n) {
11 | this.n = n;
12 | this.tag = new Array(n);
13 | this.c = new Array(n * 3);
14 | this.alphaCurve = 0;
15 | this.vertex = new Array(n);
16 | this.alpha = new Array(n);
17 | this.alpha0 = new Array(n);
18 | this.beta = new Array(n);
19 | }
20 |
21 | module.exports = Curve;
--------------------------------------------------------------------------------
/lib/types/Histogram.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | // Histogram
4 |
5 | var utils = require('../utils');
6 | var Jimp = null; try { Jimp = require('jimp'); } catch(e) {}
7 | var Bitmap = require('./Bitmap');
8 |
9 | var COLOR_DEPTH = 256;
10 | var COLOR_RANGE_END = COLOR_DEPTH - 1;
11 |
12 | /**
13 | * Calculates array index for pair of indexes. We multiple column (x) by 256 and then add row to it,
14 | * this way `(index(i, j) + 1) === index(i, j + i)` thus we can reuse `index(i, j)` we once calculated
15 | *
16 | * Note: this is different from how indexes calculated in {@link Bitmap} class, keep it in mind.
17 | *
18 | * @param x
19 | * @param y
20 | * @returns {*}
21 | * @private
22 | */
23 | function index(x, y) {
24 | return COLOR_DEPTH * x + y;
25 | }
26 |
27 | function normalizeMinMax(levelMin, levelMax) {
28 | /**
29 | * Shared parameter normalization for methods 'multilevelThresholding', 'autoThreshold', 'getDominantColor' and 'getStats'
30 | *
31 | * @param levelMin
32 | * @param levelMax
33 | * @returns {number[]}
34 | * @private
35 | */
36 | levelMin = typeof levelMin === 'number' ? utils.clamp(Math.round(levelMin), 0, COLOR_RANGE_END) : 0;
37 | levelMax = typeof levelMax === 'number' ? utils.clamp(Math.round(levelMax), 0, COLOR_RANGE_END) : COLOR_RANGE_END;
38 |
39 | if (levelMin > levelMax) {
40 | throw new Error('Invalid range "'+ levelMin + '...' + levelMax + '"');
41 | }
42 |
43 | return [levelMin, levelMax];
44 | }
45 |
46 | /**
47 | * 1D Histogram
48 | *
49 | * @param {Number|Bitmap|Jimp} imageSource - Image to collect pixel data from. Or integer to create empty histogram for image of specific size
50 | * @param [mode] Used only for Jimp images. {@link Bitmap} currently can only store 256 values per pixel, so it's assumed that it contains values we are looking for
51 | * @constructor
52 | * @protected
53 | */
54 | function Histogram(imageSource, mode) {
55 | this.data = null;
56 | this.pixels = 0;
57 | this._sortedIndexes = null;
58 | this._cachedStats = {};
59 | this._lookupTableH = null;
60 |
61 | if (typeof imageSource === 'number') {
62 | this._createArray(imageSource);
63 | } else if (imageSource instanceof Bitmap) {
64 | this._collectValuesBitmap(imageSource);
65 | } else if (Jimp && imageSource instanceof Jimp) {
66 | this._collectValuesJimp(imageSource, mode);
67 | } else {
68 | throw new Error('Unsupported image source');
69 | }
70 | }
71 |
72 | Histogram.MODE_LUMINANCE = 'luminance';
73 | Histogram.MODE_R = 'r';
74 | Histogram.MODE_G = 'g';
75 | Histogram.MODE_B = 'b';
76 |
77 | Histogram.prototype = {
78 | /**
79 | * Initializes data array for an image of given pixel size
80 | * @param imageSize
81 | * @returns {Uint8Array|Uint16Array|Uint32Array}
82 | * @private
83 | */
84 | _createArray: function(imageSize) {
85 | var ArrayType = imageSize <= Math.pow(2, 8) ? Uint8Array
86 | : imageSize <= Math.pow(2, 16) ? Uint16Array : Uint32Array;
87 |
88 | this.pixels = imageSize;
89 |
90 | return this.data = new ArrayType(COLOR_DEPTH);
91 | },
92 |
93 | /**
94 | * Aggregates color data from {@link Jimp} instance
95 | * @param {Jimp} source
96 | * @param mode
97 | * @private
98 | */
99 | _collectValuesJimp: function(source, mode) {
100 | var pixelData = source.bitmap.data;
101 | var data = this._createArray(source.bitmap.width * source.bitmap.height);
102 |
103 | source.scan(0, 0, source.bitmap.width, source.bitmap.height, function(x, y, idx) {
104 | var val = mode === Histogram.MODE_R ? pixelData[idx]
105 | : mode === Histogram.MODE_G ? pixelData[idx + 1]
106 | : mode === Histogram.MODE_B ? pixelData[idx + 2]
107 | : utils.luminance(pixelData[idx], pixelData[idx + 1], pixelData[idx + 2]);
108 |
109 | data[val]++;
110 | });
111 | },
112 |
113 | /**
114 | * Aggregates color data from {@link Bitmap} instance
115 | * @param {Bitmap} source
116 | * @private
117 | */
118 | _collectValuesBitmap: function(source) {
119 | var data = this._createArray(source.size);
120 | var len = source.data.length;
121 | var color;
122 |
123 | for (var i = 0; i < len; i++) {
124 | color = source.data[i];
125 | data[color]++
126 | }
127 | },
128 |
129 | /**
130 | * Returns array of color indexes in ascending order
131 | * @param refresh
132 | * @returns {*}
133 | * @private
134 | */
135 | _getSortedIndexes: function(refresh) {
136 | if (!refresh && this._sortedIndexes) {
137 | return this._sortedIndexes;
138 | }
139 |
140 | var data = this.data;
141 | var indexes = new Array(COLOR_DEPTH);
142 | var i = 0;
143 |
144 | for (i; i < COLOR_DEPTH; i++) {
145 | indexes[i] = i;
146 | }
147 |
148 | indexes.sort(function(a, b) {
149 | return data[a] > data[b] ? 1 : data[a] < data[b] ? -1 : 0;
150 | });
151 |
152 | this._sortedIndexes = indexes;
153 | return indexes;
154 | },
155 |
156 | /**
157 | * Builds lookup table H from lookup tables P and S.
158 | * see {@link http://www.iis.sinica.edu.tw/page/jise/2001/200109_01.pdf|this paper} for more details
159 | *
160 | * @returns {Float64Array}
161 | * @private
162 | */
163 | _thresholdingBuildLookupTable: function() {
164 | var P = new Float64Array(COLOR_DEPTH * COLOR_DEPTH);
165 | var S = new Float64Array(COLOR_DEPTH * COLOR_DEPTH);
166 | var H = new Float64Array(COLOR_DEPTH * COLOR_DEPTH);
167 | var pixelsTotal = this.pixels;
168 | var i, j, idx, tmp;
169 |
170 | // diagonal
171 | for (i = 1; i < COLOR_DEPTH; ++i) {
172 | idx = index(i, i);
173 | tmp = this.data[i] / pixelsTotal;
174 |
175 | P[idx] = tmp;
176 | S[idx] = i * tmp;
177 | }
178 |
179 | // calculate first row (row 0 is all zero)
180 | for (i = 1; i < COLOR_DEPTH - 1; ++i) {
181 | tmp = this.data[i + 1] / pixelsTotal;
182 | idx = index(1, i);
183 |
184 | P[idx+1] = P[idx] + tmp;
185 | S[idx+1] = S[idx] + (i + 1) * tmp;
186 | }
187 |
188 | // using row 1 to calculate others
189 | for (i = 2; i < COLOR_DEPTH; i++) {
190 | for (j=i+1; j < COLOR_DEPTH; j++) {
191 | P[index(i, j)] = P[index(1, j)] - P[index(1, i-1)];
192 | S[index(i, j)] = S[index(1, j)] - S[index(1, i-1)];
193 | }
194 | }
195 |
196 | // now calculate H[i][j]
197 | for (i = 1; i < COLOR_DEPTH; ++i) {
198 | for (j = i + 1; j < COLOR_DEPTH; j++) {
199 | idx = index(i, j);
200 | H[idx] = P[idx] !== 0 ? S[idx] * S[idx] / P[idx] : 0;
201 | }
202 | }
203 |
204 | return this._lookupTableH = H;
205 | },
206 |
207 | /**
208 | * Implements Algorithm For Multilevel Thresholding
209 | * Receives desired number of color stops, returns array of said size. Could be limited to a range levelMin..levelMax
210 | *
211 | * Regardless of levelMin and levelMax values it still relies on between class variances for the entire histogram
212 | *
213 | * @param amount - how many thresholds should be calculated
214 | * @param [levelMin=0] - histogram segment start
215 | * @param [levelMax=255] - histogram segment end
216 | * @returns {number[]}
217 | */
218 | multilevelThresholding: function (amount, levelMin, levelMax) {
219 | levelMin = normalizeMinMax(levelMin, levelMax);
220 | levelMax = levelMin[1];
221 | levelMin = levelMin[0];
222 | amount = Math.min(levelMax - levelMin - 2, ~~amount);
223 |
224 | if (amount < 1) {
225 | return [];
226 | }
227 |
228 | if (!this._lookupTableH) {
229 | this._thresholdingBuildLookupTable();
230 | }
231 |
232 | var H = this._lookupTableH;
233 |
234 | var colorStops = null;
235 | var maxSig = 0;
236 |
237 | if (amount > 4) {
238 | console.log('[Warning]: Threshold computation for more than 5 levels may take a long time');
239 | }
240 |
241 | function iterateRecursive (startingPoint, prevVariance, indexes, previousDepth) {
242 | startingPoint = (startingPoint || 0) + 1;
243 | prevVariance = prevVariance || 0;
244 | indexes = indexes || (new Array(amount));
245 | previousDepth = previousDepth || 0;
246 |
247 | var depth = previousDepth + 1; // t
248 | var variance;
249 |
250 | for (var i = startingPoint; i < levelMax - amount + previousDepth; i++) {
251 | variance = prevVariance + H[index(startingPoint, i)];
252 | indexes[depth - 1] = i;
253 |
254 | if (depth + 1 < amount + 1) {
255 | // we need to go deeper
256 | iterateRecursive(i, variance, indexes, depth);
257 | } else {
258 | // enough, we can compare values now
259 | variance += H[index(i + 1, levelMax)];
260 |
261 | if (maxSig < variance) {
262 | maxSig = variance;
263 | colorStops = indexes.slice();
264 | }
265 | }
266 | }
267 | }
268 |
269 | iterateRecursive(levelMin || 0);
270 |
271 | return colorStops ? colorStops : [];
272 | },
273 |
274 | /**
275 | * Automatically finds threshold value using Algorithm For Multilevel Thresholding
276 | *
277 | * @param {number} [levelMin]
278 | * @param {number} [levelMax]
279 | * @returns {null|number}
280 | */
281 | autoThreshold: function(levelMin, levelMax) {
282 | var value = this.multilevelThresholding(1, levelMin, levelMax);
283 | return value.length ? value[0] : null;
284 | },
285 |
286 | /**
287 | * Returns dominant color in given range. Returns -1 if not a single color from the range present on the image
288 | *
289 | * @param [levelMin=0]
290 | * @param [levelMax=255]
291 | * @param [tolerance=1]
292 | * @returns {number}
293 | */
294 | getDominantColor: function(levelMin, levelMax, tolerance) {
295 | levelMin = normalizeMinMax(levelMin, levelMax);
296 | levelMax = levelMin[1];
297 | levelMin = levelMin[0];
298 | tolerance = tolerance || 1;
299 |
300 | var colors = this.data,
301 | dominantIndex = -1,
302 | dominantValue = -1,
303 | i, j, tmp;
304 |
305 | if (levelMin === levelMax) {
306 | return colors[levelMin] ? levelMin : -1;
307 | }
308 |
309 | for (i=levelMin; i <= levelMax; i++) {
310 | tmp = 0;
311 |
312 | for (j = ~~(tolerance / -2); j < tolerance; j++) {
313 | tmp += utils.between(i + j, 0, COLOR_RANGE_END) ? colors[i + j] : 0;
314 | }
315 |
316 | var summIsBigger = tmp > dominantValue;
317 | var summEqualButMainColorIsBigger = dominantValue === tmp && (dominantIndex < 0 || colors[i] > colors[dominantIndex]);
318 |
319 | if (summIsBigger || summEqualButMainColorIsBigger) {
320 | dominantIndex = i;
321 | dominantValue = tmp;
322 | }
323 | }
324 |
325 | return dominantValue <= 0 ? -1 : dominantIndex;
326 | },
327 |
328 | /**
329 | * Returns stats for histogram or its segment.
330 | *
331 | * Returned object contains median, mean and standard deviation for pixel values;
332 | * peak, mean and median number of pixels per level and few other values
333 | *
334 | * If no pixels colors from specified range present on the image - most values will be NaN
335 | *
336 | * @param {Number} [levelMin=0] - histogram segment start
337 | * @param {Number} [levelMax=255] - histogram segment end
338 | * @param {Boolean} [refresh=false] - if cached result can be returned
339 | * @returns {{levels: {mean: (number|*), median: *, stdDev: number, unique: number}, pixelsPerLevel: {mean: (number|*), median: (number|*), peak: number}, pixels: number}}
340 | */
341 | getStats: function(levelMin, levelMax, refresh) {
342 | levelMin = normalizeMinMax(levelMin, levelMax);
343 | levelMax = levelMin[1];
344 | levelMin = levelMin[0];
345 |
346 | if (!refresh && this._cachedStats[levelMin + '-' + levelMax]) {
347 | return this._cachedStats[levelMin + '-' + levelMax];
348 | }
349 |
350 | var data = this.data;
351 | var sortedIndexes = this._getSortedIndexes();
352 |
353 | var pixelsTotal = 0;
354 | var medianValue = null;
355 | var meanValue;
356 | var medianPixelIndex;
357 | var pixelsPerLevelMean;
358 | var pixelsPerLevelMedian;
359 | var tmpSumOfDeviations = 0;
360 | var tmpPixelsIterated = 0;
361 | var allPixelValuesCombined = 0;
362 | var i, tmpPixels, tmpPixelValue;
363 |
364 | var uniqueValues = 0; // counter for levels that's represented by at least one pixel
365 | var mostPixelsPerLevel = 0;
366 |
367 | // Finding number of pixels and mean
368 |
369 | for (i = levelMin; i <= levelMax; i++) {
370 | pixelsTotal += data[i];
371 | allPixelValuesCombined += data[i] * i;
372 |
373 | uniqueValues += data[i] === 0 ? 0 : 1;
374 |
375 | if (mostPixelsPerLevel < data[i]) {
376 | mostPixelsPerLevel = data[i];
377 | }
378 | }
379 |
380 | meanValue = allPixelValuesCombined / pixelsTotal;
381 | pixelsPerLevelMean = pixelsTotal / (levelMax - levelMin);
382 | pixelsPerLevelMedian = pixelsTotal / uniqueValues;
383 | medianPixelIndex = Math.floor(pixelsTotal / 2);
384 |
385 | // Finding median and standard deviation
386 |
387 | for (i = 0; i < COLOR_DEPTH; i++) {
388 | tmpPixelValue = sortedIndexes[i];
389 | tmpPixels = data[tmpPixelValue];
390 |
391 | if (tmpPixelValue < levelMin || tmpPixelValue > levelMax) {
392 | continue;
393 | }
394 |
395 | tmpPixelsIterated += tmpPixels;
396 | tmpSumOfDeviations += Math.pow(tmpPixelValue - meanValue, 2) * tmpPixels;
397 |
398 | if (medianValue === null && tmpPixelsIterated >= medianPixelIndex) {
399 | medianValue = tmpPixelValue;
400 | }
401 | }
402 |
403 | return this._cachedStats[levelMin + '-' + levelMax] = {
404 | // various pixel counts for levels (0..255)
405 |
406 | levels: {
407 | mean: meanValue,
408 | median: medianValue,
409 | stdDev: Math.sqrt(tmpSumOfDeviations / pixelsTotal),
410 | unique: uniqueValues
411 | },
412 |
413 | // what's visually represented as bars
414 | pixelsPerLevel: {
415 | mean: pixelsPerLevelMean,
416 | median: pixelsPerLevelMedian,
417 | peak: mostPixelsPerLevel
418 | },
419 |
420 | pixels: pixelsTotal
421 | };
422 | }
423 | };
424 |
425 | module.exports = Histogram;
--------------------------------------------------------------------------------
/lib/types/Opti.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var Point = require('./Point');
4 |
5 | function Opti() {
6 | this.pen = 0;
7 | this.c = [new Point(), new Point()];
8 | this.t = 0;
9 | this.s = 0;
10 | this.alpha = 0;
11 | }
12 |
13 | module.exports = Opti;
--------------------------------------------------------------------------------
/lib/types/Path.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function Path() {
4 | this.area = 0;
5 | this.len = 0;
6 | this.curve = {};
7 | this.pt = [];
8 | this.minX = 100000;
9 | this.minY = 100000;
10 | this.maxX = -1;
11 | this.maxY = -1;
12 | }
13 |
14 | module.exports = Path;
--------------------------------------------------------------------------------
/lib/types/Point.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function Point(x, y) {
4 | this.x = x || 0;
5 | this.y = y || 0;
6 | }
7 |
8 | Point.prototype = {
9 | copy: function() {
10 | return new Point(this.x, this.y);
11 | }
12 | };
13 |
14 | module.exports = Point;
--------------------------------------------------------------------------------
/lib/types/Quad.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function Quad() {
4 | this.data = [0,0,0,0,0,0,0,0,0];
5 | }
6 |
7 | Quad.prototype.at = function(x, y) {
8 | return this.data[x * 3 + y];
9 | };
10 |
11 | module.exports = Quad;
--------------------------------------------------------------------------------
/lib/types/Sum.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function Sum(x, y, xy, x2, y2) {
4 | this.x = x;
5 | this.y = y;
6 | this.xy = xy;
7 | this.x2 = x2;
8 | this.y2 = y2;
9 | }
10 |
11 | module.exports = Sum;
--------------------------------------------------------------------------------
/lib/utils.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var Point = require('./types/Point');
4 | var attrRegexps = {};
5 |
6 | function getAttrRegexp(attrName) {
7 | if (attrRegexps[attrName]) {
8 | return attrRegexps[attrName];
9 | }
10 |
11 | attrRegexps[attrName] = new RegExp(' ' + attrName + '="((?:\\\\(?=")"|[^"])+)"', 'i');
12 | return attrRegexps[attrName];
13 | }
14 |
15 | function setHtmlAttribute(html, attrName, value) {
16 | var attr = ' ' + attrName + '="' + value + '"';
17 |
18 | if (html.indexOf(' ' + attrName + '="') === -1) {
19 | html = html.replace(/<[a-z]+/i, function(beginning) { return beginning + attr; });
20 | } else {
21 | html = html.replace(getAttrRegexp(attrName), attr);
22 | }
23 |
24 | return html;
25 | }
26 |
27 | function fixed(number) {
28 | return number.toFixed(3).replace('.000', '');
29 | }
30 |
31 | function mod(a, n) {
32 | return a >= n ? a % n : a>=0 ? a : n-1-(-1-a) % n;
33 | }
34 |
35 | function xprod(p1, p2) {
36 | return p1.x * p2.y - p1.y * p2.x;
37 | }
38 |
39 | function cyclic(a, b, c) {
40 | if (a <= c) {
41 | return (a <= b && b < c);
42 | } else {
43 | return (a <= b || b < c);
44 | }
45 | }
46 |
47 | function sign(i) {
48 | return i > 0 ? 1 : i < 0 ? -1 : 0;
49 | }
50 |
51 | function quadform(Q, w) {
52 | var v = new Array(3), i, j, sum;
53 |
54 | v[0] = w.x;
55 | v[1] = w.y;
56 | v[2] = 1;
57 | sum = 0.0;
58 |
59 | for (i=0; i<3; i++) {
60 | for (j=0; j<3; j++) {
61 | sum += v[i] * Q.at(i, j) * v[j];
62 | }
63 | }
64 | return sum;
65 | }
66 |
67 | function interval(lambda, a, b) {
68 | var res = new Point();
69 |
70 | res.x = a.x + lambda * (b.x - a.x);
71 | res.y = a.y + lambda * (b.y - a.y);
72 | return res;
73 | }
74 |
75 | function dorth_infty(p0, p2) {
76 | var r = new Point();
77 |
78 | r.y = sign(p2.x - p0.x);
79 | r.x = -sign(p2.y - p0.y);
80 |
81 | return r;
82 | }
83 |
84 | function ddenom(p0, p2) {
85 | var r = dorth_infty(p0, p2);
86 |
87 | return r.y * (p2.x - p0.x) - r.x * (p2.y - p0.y);
88 | }
89 |
90 | function dpara(p0, p1, p2) {
91 | var x1, y1, x2, y2;
92 |
93 | x1 = p1.x - p0.x;
94 | y1 = p1.y - p0.y;
95 | x2 = p2.x - p0.x;
96 | y2 = p2.y - p0.y;
97 |
98 | return x1 * y2 - x2 * y1;
99 | }
100 |
101 | function cprod(p0, p1, p2, p3) {
102 | var x1, y1, x2, y2;
103 |
104 | x1 = p1.x - p0.x;
105 | y1 = p1.y - p0.y;
106 | x2 = p3.x - p2.x;
107 | y2 = p3.y - p2.y;
108 |
109 | return x1 * y2 - x2 * y1;
110 | }
111 |
112 | function iprod(p0, p1, p2) {
113 | var x1, y1, x2, y2;
114 |
115 | x1 = p1.x - p0.x;
116 | y1 = p1.y - p0.y;
117 | x2 = p2.x - p0.x;
118 | y2 = p2.y - p0.y;
119 |
120 | return x1*x2 + y1*y2;
121 | }
122 |
123 | function iprod1(p0, p1, p2, p3) {
124 | var x1, y1, x2, y2;
125 |
126 | x1 = p1.x - p0.x;
127 | y1 = p1.y - p0.y;
128 | x2 = p3.x - p2.x;
129 | y2 = p3.y - p2.y;
130 |
131 | return x1 * x2 + y1 * y2;
132 | }
133 |
134 | function ddist(p, q) {
135 | return Math.sqrt((p.x - q.x) * (p.x - q.x) + (p.y - q.y) * (p.y - q.y));
136 | }
137 |
138 | module.exports = {
139 | luminance: function (r, g, b) {
140 | return Math.round(0.2126 * r + 0.7153 * g + 0.0721 * b);
141 | },
142 |
143 | between: function(val, min, max) {
144 | return val >= min && val <= max;
145 | },
146 |
147 | clamp: function(val, min, max) {
148 | return Math.min(max, Math.max(min, val));
149 | },
150 |
151 | isNumber: function(val) {
152 | return typeof val === 'number';
153 | },
154 |
155 | setHtmlAttr: setHtmlAttribute,
156 |
157 | /**
158 | * Generates path instructions for given curve
159 | *
160 | * @param {Curve} curve
161 | * @param {Number} [scale]
162 | * @returns {string}
163 | */
164 | renderCurve: function(curve, scale) {
165 | scale = scale || 1;
166 |
167 | var startingPoint = curve.c[(curve.n - 1) * 3 + 2];
168 |
169 | var path = 'M '
170 | + fixed(startingPoint.x * scale) + ' '
171 | + fixed(startingPoint.y * scale) + ' ';
172 |
173 | curve.tag.forEach(function(tag, i) {
174 | var i3 = i * 3;
175 | var p0 = curve.c[i3];
176 | var p1 = curve.c[i3 + 1];
177 | var p2 = curve.c[i3 + 2];
178 |
179 | if (tag === "CURVE") {
180 | path += 'C ';
181 | path += fixed(p0.x * scale) + ' ' + fixed(p0.y * scale) + ', ';
182 | path += fixed(p1.x * scale) + ' ' + fixed(p1.y * scale) + ', ';
183 | path += fixed(p2.x * scale) + ' ' + fixed(p2.y * scale) + ' ';
184 | } else if (tag === "CORNER") {
185 | path += 'L ';
186 | path += fixed(p1.x * scale) + ' ' + fixed(p1.y * scale) + ' ';
187 | path += fixed(p2.x * scale) + ' ' + fixed(p2.y * scale) + ' ';
188 | }
189 | });
190 |
191 | return path;
192 | },
193 |
194 | bezier: function bezier(t, p0, p1, p2, p3) {
195 | var s = 1 - t, res = new Point();
196 |
197 | res.x = s*s*s*p0.x + 3*(s*s*t)*p1.x + 3*(t*t*s)*p2.x + t*t*t*p3.x;
198 | res.y = s*s*s*p0.y + 3*(s*s*t)*p1.y + 3*(t*t*s)*p2.y + t*t*t*p3.y;
199 |
200 | return res;
201 | },
202 |
203 | tangent: function tangent(p0, p1, p2, p3, q0, q1) {
204 | var A, B, C, a, b, c, d, s, r1, r2;
205 |
206 | A = cprod(p0, p1, q0, q1);
207 | B = cprod(p1, p2, q0, q1);
208 | C = cprod(p2, p3, q0, q1);
209 |
210 | a = A - 2 * B + C;
211 | b = -2 * A + 2 * B;
212 | c = A;
213 |
214 | d = b * b - 4 * a * c;
215 |
216 | if (a===0 || d<0) {
217 | return -1.0;
218 | }
219 |
220 | s = Math.sqrt(d);
221 |
222 | r1 = (-b + s) / (2 * a);
223 | r2 = (-b - s) / (2 * a);
224 |
225 | if (r1 >= 0 && r1 <= 1) {
226 | return r1;
227 | } else if (r2 >= 0 && r2 <= 1) {
228 | return r2;
229 | } else {
230 | return -1.0;
231 | }
232 | },
233 |
234 | mod: mod,
235 | xprod: xprod,
236 | cyclic: cyclic,
237 | sign: sign,
238 | quadform: quadform,
239 | interval: interval,
240 | dorth_infty: dorth_infty,
241 | ddenom: ddenom,
242 | dpara: dpara,
243 | cprod: cprod,
244 | iprod: iprod,
245 | iprod1: iprod1,
246 | ddist: ddist
247 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "potrace",
3 | "version": "2.1.2",
4 | "description": "Potrace in Javascript, for NodeJS",
5 | "main": "lib/index.js",
6 | "scripts": {
7 | "test": "cd test && mocha test.js --reporter spec"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/tooolbox/node-potrace.git"
12 | },
13 | "keywords": [
14 | "potrace",
15 | "trace",
16 | "tracing",
17 | "svg",
18 | "bitmap",
19 | "posterization"
20 | ],
21 | "author": "mattmc",
22 | "license": "GPL-2.0",
23 | "bugs": {
24 | "url": "https://github.com/tooolbox/node-potrace/issues"
25 | },
26 | "homepage": "https://github.com/tooolbox/node-potrace#readme",
27 | "dependencies": {
28 | "jimp": "^0.6.4"
29 | },
30 | "devDependencies": {
31 | "lodash": "^4.15.0",
32 | "mocha": "^3.0.2",
33 | "should": "^11.1.0",
34 | "should-sinon": "0.0.5",
35 | "sinon": "^1.17.5"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/test/example-output.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/reference-copies/output.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/reference-copies/posterized-yao-black-threshold-65.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/reference-copies/potrace-bw-black-threshold-0.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/reference-copies/potrace-bw-black-threshold-255.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/reference-copies/potrace-bw-threshold-128.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/reference-copies/potrace-bw-threshold-170.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/reference-copies/potrace-bw-threshold-65.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/reference-copies/potrace-bw-white-threshold-0.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/reference-copies/potrace-bw-white-threshold-255.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/reference-copies/potrace-wb-black-threshold-0.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/reference-copies/potrace-wb-black-threshold-255.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/reference-copies/potrace-wb-threshold-128.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/reference-copies/potrace-wb-white-threshold-0.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/reference-copies/potrace-wb-white-threshold-255.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/sources/Lenna.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iwsfg/node-potrace/b86608eaf3b9c5fea6d6b2034c762ea666e70ec6/test/sources/Lenna.png
--------------------------------------------------------------------------------
/test/sources/clouds.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iwsfg/node-potrace/b86608eaf3b9c5fea6d6b2034c762ea666e70ec6/test/sources/clouds.jpg
--------------------------------------------------------------------------------
/test/sources/white-on-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iwsfg/node-potrace/b86608eaf3b9c5fea6d6b2034c762ea666e70ec6/test/sources/white-on-black.png
--------------------------------------------------------------------------------
/test/sources/yao.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iwsfg/node-potrace/b86608eaf3b9c5fea6d6b2034c762ea666e70ec6/test/sources/yao.jpg
--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var _ = require('lodash'),
4 | assert = require('assert'),
5 | should = require('should'),
6 | sinon = require('sinon');
7 |
8 | require('should-sinon');
9 |
10 | var fs = require('fs'),
11 | Jimp = require('jimp'),
12 | Potrace = require('../lib/Potrace'),
13 | Posterizer = require('../lib/Posterizer'),
14 | Histogram = require('../lib/types/Histogram'),
15 | lib = require('../lib/index');
16 |
17 | var PATH_TO_YAO = './sources/yao.jpg';
18 | var PATH_TO_LENNA = './sources/Lenna.png';
19 | var PATH_TO_BLACK_AND_WHITE_IMAGE = './sources/clouds.jpg';
20 |
21 | var blackImage = new Jimp(100, 100, 0x000000FF);
22 | var whiteImage = new Jimp(100, 100, 0xFFFFFFFF);
23 |
24 | describe('Histogram class (private, responsible for auto thresholding)', function() {
25 | var histogram = null;
26 |
27 | var blackHistogram = new Histogram(blackImage, Histogram.MODE_LUMINANCE);
28 | var whiteHistogram = new Histogram(whiteImage, Histogram.MODE_LUMINANCE);
29 |
30 | before(function(done) {
31 | this.timeout(10000);
32 |
33 | Jimp.read(PATH_TO_LENNA, function(err, img) {
34 | if (err) throw err;
35 | histogram = new Histogram(img, Histogram.MODE_LUMINANCE);
36 | done();
37 | });
38 | });
39 |
40 | describe('#getDominantColor', function() {
41 | it('gives different results with different tolerance values', function() {
42 | assert.equal(histogram.getDominantColor(0, 255), 149);
43 | assert.equal(histogram.getDominantColor(0, 255, 10), 143);
44 | });
45 |
46 | it('has default argument values of 0, 255 and 1', function() {
47 | assert.equal(histogram.getDominantColor(), histogram.getDominantColor(0, 255, 1));
48 | });
49 |
50 | it('works for a segment of histogram', function() {
51 | assert.equal(41, histogram.getDominantColor(20, 80));
52 | });
53 |
54 | it('does not fail when min and max values are the same', function() {
55 | assert.equal(histogram.getDominantColor(42, 42), 42);
56 | });
57 |
58 | it('returns -1 if colors from the range are not present on image', function() {
59 | assert.equal(histogram.getDominantColor(0, 15), -1);
60 | assert.equal(histogram.getDominantColor(7, 7, 1), -1);
61 | });
62 |
63 | it('throws error if range start is larger than range end', function() {
64 | (function() {
65 | histogram.getDominantColor(80, 20);
66 | }).should.throw();
67 | });
68 |
69 | it('behaves predictably in edge cases', function() {
70 | blackHistogram.getDominantColor(0, 255).should.be.equal(0);
71 | whiteHistogram.getDominantColor(0, 255).should.be.equal(255);
72 | whiteHistogram.getDominantColor(25, 235).should.be.equal(-1);
73 |
74 | // Tolerance should not affect returned value
75 |
76 | blackHistogram.getDominantColor(0, 255, 15).should.be.equal(0);
77 | whiteHistogram.getDominantColor(0, 255, 15).should.be.equal(255);
78 | })
79 | });
80 |
81 | describe('#getStats', function() {
82 | function toFixedDeep(stats, fractionalDigits) {
83 | return _.cloneDeepWith(stats, function(val) {
84 | if (_.isNumber(val) && !_.isInteger(val)) {
85 | return parseFloat(val.toFixed(fractionalDigits));
86 | }
87 | });
88 | }
89 |
90 | it('produces expected stats object for entire histogram', function() {
91 | var expectedValue = {
92 | levels: {
93 | mean: 116.7673568725586,
94 | median: 95,
95 | stdDev: 49.42205692905339,
96 | unique: 222
97 | },
98 | pixelsPerLevel: {
99 | mean: 1028.0156862745098,
100 | median: 1180.8288288288288,
101 | peak: 2495
102 | },
103 | pixels: 262144
104 | };
105 |
106 | assert.deepEqual(
107 | toFixedDeep(histogram.getStats(), 4),
108 | toFixedDeep(expectedValue, 4)
109 | );
110 | });
111 |
112 | it('produces expected stats object for histogram segment', function() {
113 | var expectedValue = {
114 | levels: {
115 | mean: 121.89677761754915,
116 | median: 93,
117 | stdDev: 30.2466970087377,
118 | unique: 121
119 | },
120 | pixelsPerLevel: {
121 | mean: 1554.4916666666666,
122 | median: 1541.6446280991736,
123 | peak: 2495
124 | },
125 | pixels: 186539
126 | };
127 |
128 | assert.deepEqual(
129 | toFixedDeep(histogram.getStats(60, 180), 4),
130 | toFixedDeep(expectedValue, 4)
131 | );
132 | });
133 |
134 | it('throws error if range start is larger than range end', function() {
135 | (function() {
136 | histogram.getStats(255, 123);
137 | }).should.throw();
138 | });
139 |
140 | it('behaves predictably in edge cases', function() {
141 | var blackImageStats = blackHistogram.getStats();
142 | var whiteImageStats = blackHistogram.getStats();
143 |
144 | blackImageStats.levels.mean.should.be.equal(blackImageStats.levels.median);
145 | whiteImageStats.levels.mean.should.be.equal(whiteImageStats.levels.median);
146 |
147 | blackHistogram.getStats(25, 235).should.be.deepEqual(whiteHistogram.getStats(25, 235));
148 | });
149 | });
150 |
151 | describe('#multilevelThresholding', function() {
152 | it('calculates correct thresholds', function() {
153 | assert.deepEqual(histogram.multilevelThresholding(1), [111]);
154 | assert.deepEqual(histogram.multilevelThresholding(2), [ 92, 154 ]);
155 | assert.deepEqual(histogram.multilevelThresholding(3), [ 73, 121, 168 ]);
156 | });
157 |
158 | it('works for histogram segment', function() {
159 | assert.deepEqual(histogram.multilevelThresholding(2, 60, 180), [ 103, 138 ]);
160 | });
161 |
162 | it('calculates as many thresholds as can be fit in given range', function() {
163 | assert.deepEqual(histogram.multilevelThresholding(2, 102, 106), [ 103, 104 ]);
164 | assert.deepEqual(histogram.multilevelThresholding(2, 103, 106), [ 104 ]);
165 | });
166 |
167 | it('returns empty array if no colors from histogram segment is present on the image', function() {
168 | assert.deepEqual(histogram.multilevelThresholding(3, 2, 14), []);
169 | });
170 |
171 | it('throws error if range start is larger than range end', function() {
172 | (function() {
173 | histogram.multilevelThresholding(2, 180, 60);
174 | }).should.throw();
175 | });
176 |
177 | });
178 | });
179 |
180 | describe('Potrace class', function() {
181 | var jimpInstance = null;
182 |
183 | this.timeout(10000);
184 |
185 | before(function(done) {
186 | Jimp.read(PATH_TO_YAO, function(err, img) {
187 | if (err) {
188 | return done(err);
189 | }
190 |
191 | jimpInstance = img;
192 | done();
193 | });
194 | });
195 |
196 | describe('#loadImage', function() {
197 | it('instance is being passed to callback function as context', function(done) {
198 | var instance = new Potrace();
199 |
200 | instance.loadImage(PATH_TO_YAO, function(err) {
201 | this.should.be.an.instanceOf(Potrace).and.be.equal(instance);
202 | done(err);
203 | });
204 | });
205 |
206 | it('supports Jimp instances provided as source image', function(done) {
207 | var instance = new Potrace();
208 |
209 | instance.loadImage(jimpInstance, done);
210 | });
211 |
212 | it('should throw error if called before previous image was loaded', function(done) {
213 | function onImageLoad() {
214 | if (firstFinished && secondFinished) {
215 | done();
216 | }
217 | }
218 |
219 | var potraceInstance = new Potrace();
220 | var firstFinished = false;
221 | var secondFinished = false;
222 |
223 | potraceInstance.loadImage(PATH_TO_LENNA, function(err) {
224 | firstFinished = true;
225 | should(function() { should.ifError(err); }).throw(/another.*instead/i);
226 | onImageLoad();
227 | });
228 |
229 | potraceInstance.loadImage(PATH_TO_YAO, function(err) {
230 | secondFinished = true;
231 | should(function() { should.ifError(err); }).not.throw();
232 | onImageLoad();
233 | });
234 | });
235 | });
236 |
237 | describe('#_processPath', function() {
238 | var instance = new Potrace();
239 | var processingSpy = null;
240 |
241 | before(function() {
242 | processingSpy = instance._processPath = sinon.spy(Potrace.prototype._processPath);
243 | });
244 |
245 | it('should not execute until path is requested for the first time', function(done) {
246 | instance.loadImage(jimpInstance, function() {
247 | processingSpy.should.have.callCount(0);
248 | this.getSVG();
249 | processingSpy.should.have.callCount(1);
250 | done();
251 | });
252 | });
253 |
254 | it('should not execute on repetitive SVG/Symbol export', function() {
255 | instance.loadImage(jimpInstance, function() {
256 | var initialCallCount = processingSpy.callCount;
257 |
258 | this.getSVG();
259 | this.getSVG();
260 | this.getPathTag();
261 | this.getPathTag('red');
262 | this.getSymbol('symbol-id');
263 | processingSpy.should.have.callCount(initialCallCount);
264 | });
265 | });
266 |
267 | it('should not execute after change of foreground/background colors', function() {
268 | instance.loadImage(jimpInstance, function() {
269 | var initialCallCount = processingSpy.callCount;
270 |
271 | this.setParameters({ color: 'red' });
272 | this.getSVG();
273 |
274 | this.setParameters({ background: 'crimson' });
275 | this.getSVG();
276 |
277 | processingSpy.should.have.callCount(initialCallCount);
278 | });
279 | });
280 | });
281 |
282 | describe('#getSVG', function() {
283 | var instanceYao = new Potrace();
284 |
285 | before(function(done) {
286 | instanceYao.loadImage(jimpInstance, done);
287 | });
288 |
289 | it('produces expected results with different thresholds', function() {
290 | var expected;
291 |
292 | expected = fs.readFileSync('./reference-copies/potrace-bw-threshold-128.svg', { encoding: 'utf8' });
293 | instanceYao.setParameters({ threshold: 128 });
294 | assert.equal(instanceYao.getSVG(), expected, 'Image with threshold 128 does not match with reference copy');
295 |
296 | expected = fs.readFileSync('./reference-copies/potrace-bw-threshold-65.svg', { encoding: 'utf8' });
297 | instanceYao.setParameters({ threshold: 65 });
298 | assert.equal(instanceYao.getSVG(), expected, 'Image with threshold 65 does not match with reference copy');
299 |
300 | expected = fs.readFileSync('./reference-copies/potrace-bw-threshold-170.svg', { encoding: 'utf8' });
301 | instanceYao.setParameters({ threshold: 170 });
302 | assert.equal(instanceYao.getSVG(), expected, 'Image with threshold 170 does not match with reference copy');
303 | });
304 |
305 | it('produces expected white on black image with threshold 170', function(done) {
306 | var instance = new Potrace({
307 | threshold: 128,
308 | blackOnWhite: false,
309 | color: 'cyan',
310 | background: 'darkred'
311 | });
312 |
313 | instance.loadImage(PATH_TO_BLACK_AND_WHITE_IMAGE, function(err) {
314 | if (err) return done(err);
315 |
316 | var expected = fs.readFileSync('./reference-copies/potrace-wb-threshold-128.svg', { encoding: 'utf8' });
317 | var actual = instance.getSVG();
318 |
319 | assert.equal(actual, expected);
320 | done();
321 | });
322 | });
323 | });
324 |
325 | describe('#getSymbol', function() {
326 | var instanceYao = new Potrace();
327 |
328 | before(function(done) {
329 | instanceYao.loadImage(jimpInstance, done);
330 | });
331 |
332 | it('should not have fill color or background', function() {
333 | instanceYao.setParameters({
334 | color: 'red',
335 | background: 'cyan'
336 | });
337 |
338 | var symbol = instanceYao.getSymbol('whatever');
339 |
340 | symbol.should.not.match(/]+(?:fill="\s*"|fill='\s*'|)[^>]*>/i);
342 | });
343 | });
344 |
345 | describe('behaves predictably in edge cases', function() {
346 | var instance = new Potrace();
347 |
348 | var bwBlackThreshold0;
349 | var bwBlackThreshold255;
350 | var bwWhiteThreshold0;
351 | var bwWhiteThreshold255;
352 | var wbWhiteThreshold0;
353 | var wbWhiteThreshold255;
354 | var wbBlackThreshold0;
355 | var wbBlackThreshold255;
356 |
357 | before(function() {
358 | bwBlackThreshold0 = fs.readFileSync('./reference-copies/potrace-bw-black-threshold-0.svg', { encoding: 'utf8' });
359 | bwBlackThreshold255 = fs.readFileSync('./reference-copies/potrace-bw-black-threshold-255.svg', { encoding: 'utf8' });
360 | bwWhiteThreshold0 = fs.readFileSync('./reference-copies/potrace-bw-white-threshold-0.svg', { encoding: 'utf8' });
361 | bwWhiteThreshold255 = fs.readFileSync('./reference-copies/potrace-bw-white-threshold-255.svg', { encoding: 'utf8' });
362 |
363 | wbWhiteThreshold0 = fs.readFileSync('./reference-copies/potrace-wb-white-threshold-0.svg', { encoding: 'utf8' });
364 | wbWhiteThreshold255 = fs.readFileSync('./reference-copies/potrace-wb-white-threshold-255.svg', { encoding: 'utf8' });
365 | wbBlackThreshold0 = fs.readFileSync('./reference-copies/potrace-wb-black-threshold-0.svg', { encoding: 'utf8' });
366 | wbBlackThreshold255 = fs.readFileSync('./reference-copies/potrace-wb-black-threshold-255.svg', { encoding: 'utf8' });
367 | });
368 |
369 | it('compares colors against threshold in the same way as original tool', function(done) {
370 | instance.loadImage(blackImage, function(err) {
371 | if (err) { return done(err); }
372 |
373 | instance.setParameters({ blackOnWhite: true, threshold: 0 });
374 | instance.getSVG().should.be.equal(bwBlackThreshold0);
375 |
376 | instance.setParameters({ blackOnWhite: true, threshold: 255 });
377 | instance.getSVG().should.be.equal(bwBlackThreshold255);
378 |
379 | instance.loadImage(whiteImage, function() {
380 | if (err) { return done(err); }
381 |
382 | instance.setParameters({ blackOnWhite: true, threshold: 0 });
383 | instance.getSVG().should.be.equal(bwWhiteThreshold0);
384 |
385 | instance.setParameters({ blackOnWhite: true, threshold: 255 });
386 | instance.getSVG().should.be.equal(bwWhiteThreshold255);
387 |
388 | done();
389 | });
390 | });
391 | });
392 |
393 | it('acts in the same way when colors are inverted', function(done) {
394 | instance.loadImage(whiteImage, function(err) {
395 | if (err) { return done(err); }
396 | instance.setParameters({ blackOnWhite: false, threshold: 255 });
397 | instance.getSVG().should.be.equal(wbWhiteThreshold255);
398 |
399 | instance.setParameters({ blackOnWhite: false, threshold: 0 });
400 | instance.getSVG().should.be.equal(wbWhiteThreshold0);
401 |
402 | instance.loadImage(blackImage, function() {
403 | if (err) { return done(err); }
404 |
405 | instance.setParameters({ blackOnWhite: false, threshold: 255 });
406 | instance.getSVG().should.be.equal(wbBlackThreshold255);
407 |
408 | instance.setParameters({ blackOnWhite: false, threshold: 0 });
409 | instance.getSVG().should.be.equal(wbBlackThreshold0);
410 |
411 | done();
412 | });
413 | });
414 | });
415 | });
416 | });
417 |
418 | describe('Posterizer class', function() {
419 | var jimpInstance = null;
420 | var sharedPosterizerInstance = new Posterizer();
421 |
422 | this.timeout(10000);
423 |
424 | before(function(done) {
425 | Jimp.read(PATH_TO_YAO, function(err, img) {
426 | if (err) {
427 | return done(err);
428 | }
429 |
430 | jimpInstance = img;
431 | done();
432 | });
433 | });
434 |
435 | describe('#_getRanges', function() {
436 | var posterizer = new Posterizer();
437 |
438 | function getColorStops() {
439 | return posterizer._getRanges().map(function(item) {
440 | return item.value;
441 | });
442 | }
443 |
444 | before(function(done) {
445 | posterizer.loadImage(PATH_TO_YAO, done);
446 | });
447 |
448 | it('returns correctly calculated color stops with "equally spread" distribution', function() {
449 | posterizer.setParameters({
450 | rangeDistribution: Posterizer.RANGES_EQUAL,
451 | threshold: 200,
452 | steps: 4,
453 | blackOnWhite: true
454 | });
455 |
456 | getColorStops().should.be.deepEqual([200, 150, 100, 50]);
457 |
458 | posterizer.setParameters({
459 | rangeDistribution: Posterizer.RANGES_EQUAL,
460 | threshold: 155,
461 | steps: 4,
462 | blackOnWhite: false
463 | });
464 |
465 | getColorStops().should.be.deepEqual([155, 180, 205, 230]);
466 |
467 | posterizer.setParameters({
468 | rangeDistribution: Posterizer.RANGES_EQUAL,
469 | threshold: Potrace.THRESHOLD_AUTO,
470 | steps: 4,
471 | blackOnWhite: true
472 | });
473 |
474 | getColorStops().should.be.deepEqual([206, 154.5, 103, 51.5]);
475 | });
476 |
477 | it('returns correctly calculated color stops with "auto" distribution', function() {
478 | posterizer.setParameters({
479 | rangeDistribution: Posterizer.RANGES_AUTO,
480 | threshold: Potrace.THRESHOLD_AUTO,
481 | steps: 3,
482 | blackOnWhite: true
483 | });
484 |
485 | getColorStops().should.be.deepEqual([219, 156, 71]);
486 |
487 | posterizer.setParameters({
488 | rangeDistribution: Posterizer.RANGES_AUTO,
489 | threshold: Potrace.THRESHOLD_AUTO,
490 | steps: 3,
491 | blackOnWhite: false
492 | });
493 |
494 | getColorStops().should.be.deepEqual([71, 156, 219]);
495 |
496 | // Now with predefined threshold
497 |
498 | posterizer.setParameters({
499 | rangeDistribution: Posterizer.RANGES_AUTO,
500 | threshold: 128,
501 | steps: 4,
502 | blackOnWhite: true
503 | });
504 |
505 | getColorStops().should.be.deepEqual([128, 97, 62, 24]);
506 |
507 | posterizer.setParameters({
508 | rangeDistribution: Posterizer.RANGES_AUTO,
509 | threshold: 128,
510 | steps: 4,
511 | blackOnWhite: false
512 | });
513 |
514 | getColorStops().should.be.deepEqual([128, 166, 203, 237]);
515 | });
516 |
517 | it('correctly handles predefined array of color stops', function() {
518 | posterizer.setParameters({
519 | steps: [20, 60, 80, 160],
520 | threshold: 120,
521 | blackOnWhite: true
522 | });
523 |
524 | getColorStops().should.be.deepEqual([160, 80, 60, 20]);
525 |
526 | posterizer.setParameters({
527 | steps: [20, 60, 80, 160],
528 | threshold: 180,
529 | blackOnWhite: true
530 | });
531 |
532 | getColorStops().should.be.deepEqual([180, 160, 80, 60, 20]);
533 |
534 | posterizer.setParameters({
535 | steps: [20, 60, 80, 160],
536 | threshold: 180,
537 | blackOnWhite: false
538 | });
539 |
540 | getColorStops().should.be.deepEqual([20, 60, 80, 160, 180]);
541 |
542 | posterizer.setParameters({
543 | steps: [212, 16, 26, 50, 212, 128, 211],
544 | threshold: 180,
545 | blackOnWhite: false
546 | });
547 |
548 | getColorStops().should.be.deepEqual([16, 26, 50, 128, 211, 212], 'Duplicated items should be present only once');
549 |
550 | posterizer.setParameters({
551 | steps: [15, 42, 200, 460, 0, -10],
552 | threshold: 180,
553 | blackOnWhite: false
554 | });
555 |
556 | getColorStops().should.be.deepEqual([0, 15, 42, 200], 'Values out of range should be ignored');
557 | });
558 | });
559 |
560 | describe('#loadImage', function() {
561 | it('instance is being passed to callback function as context', function(done) {
562 | sharedPosterizerInstance.loadImage(PATH_TO_YAO, function(err) {
563 | this.should.be.an.instanceOf(Posterizer).and.be.equal(sharedPosterizerInstance);
564 | done(err);
565 | });
566 | });
567 | });
568 |
569 | describe('#getSVG', function() {
570 | var instanceYao = sharedPosterizerInstance;
571 |
572 | it('produces expected results with different thresholds', function() {
573 | var expected;
574 |
575 | instanceYao.setParameters({ threshold: 128 });
576 | expected = fs.readFileSync('./reference-copies/posterized-yao-black-threshold-128.svg', { encoding: 'utf8' });
577 | assert.equal(instanceYao.getSVG(), expected, 'Image with threshold 128 does not match with reference copy');
578 |
579 | instanceYao.setParameters({ threshold: 65 });
580 | expected = fs.readFileSync('./reference-copies/posterized-yao-black-threshold-65.svg', { encoding: 'utf8' });
581 | assert.equal(instanceYao.getSVG(), expected, 'Image with threshold 65 does not match with reference copy');
582 |
583 | instanceYao.setParameters({ threshold: 170 });
584 | expected = fs.readFileSync('./reference-copies/posterized-yao-black-threshold-170.svg', { encoding: 'utf8' });
585 | assert.equal(instanceYao.getSVG(), expected, 'Image with threshold 170 does not match with reference copy');
586 | });
587 |
588 | it('produces expected white on black image with threshold 170', function(done) {
589 | var instance = new Posterizer({
590 | threshold: 40,
591 | blackOnWhite: false,
592 | steps: 3,
593 | color: 'beige',
594 | background: '#222'
595 | });
596 |
597 | instance.loadImage('sources/clouds.jpg', function(err) {
598 | if (err) return done(err);
599 |
600 | var expected = fs.readFileSync('./reference-copies/posterized-clouds-white-40.svg', { encoding: 'utf8' });
601 | var actual = instance.getSVG();
602 |
603 | assert.equal(actual, expected);
604 | done();
605 | });
606 | });
607 | });
608 |
609 | describe('#getSymbol', function() {
610 | var instanceYao = new Posterizer();
611 |
612 | before(function(done) {
613 | instanceYao.loadImage(jimpInstance, done);
614 | });
615 |
616 | it('should not have fill color or background', function() {
617 | instanceYao.setParameters({
618 | color: 'red',
619 | background: 'cyan',
620 | steps: 3
621 | });
622 |
623 | var symbol = instanceYao.getSymbol('whatever');
624 |
625 | symbol.should.not.match(/]+(?:fill="\s*"|fill='\s*'|)[^>]*>/i);
627 | });
628 | });
629 |
630 | describe('edge cases', function() {
631 | var instance = new Posterizer();
632 |
633 | it('does not break on images filled with one color', function(done) {
634 | instance.loadImage(blackImage, function(err) {
635 | if (err) { return done(err); }
636 |
637 | // black image should give us one black layer...
638 | instance.setParameters({ blackOnWhite: true, threshold: 128 });
639 | instance.getSVG().should.match(/