├── .gitignore
├── .travis.yml
├── CONTRIBUTING.md
├── Dockerfile
├── README.md
├── binding.gyp
├── get_regvalue.py
├── index.js
├── package.json
├── src
├── imagemagick.cc
└── imagemagick.h
└── test
├── benchmark.js
├── broken.png
├── leak.js
├── orientation-suite
├── LICENSE
├── Landscape_1.jpg
├── Landscape_2.jpg
├── Landscape_3.jpg
├── Landscape_4.jpg
├── Landscape_5.jpg
├── Landscape_6.jpg
├── Landscape_7.jpg
├── Landscape_8.jpg
├── README.markdown
└── VERSION
├── test.CMYK.jpg
├── test.async.js
├── test.auto.orient.js
├── test.background.js
├── test.background.png
├── test.colorspace.js
├── test.crop.js
├── test.crop.png
├── test.ext.tga
├── test.exthint.js
├── test.getPixelColor.png
├── test.gravity.js
├── test.jpg
├── test.js
├── test.maxmemory.jpg
├── test.png
├── test.promises.js
├── test.quantizeColors.png
├── test.stream.js
├── test.trim.jpg
├── test.trim.js
└── test.wide.png
/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 | node_modules/
3 | test/out*
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | dist: trusty
2 | language: node_js
3 | node_js:
4 | - "14"
5 | - "12"
6 | - "10"
7 | - "8"
8 | env:
9 | - CC=clang CXX=clang++ npm_config_clang=1
10 | addons:
11 | apt:
12 | packages:
13 | - libmagick++-dev
14 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Please:
2 |
3 | * Provide command line arguments, relevant output, and sample images when submitting issues or pull requests.
4 |
5 | * Make sure to note version information for your operating system and ImageMagick.
6 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM dockerfile/nodejs
2 | # https://registry.hub.docker.com/u/dockerfile/nodejs/
3 |
4 | RUN apt-get update
5 | RUN apt-get -y install \
6 | git \
7 | imagemagick \
8 | libmagick++-dev \
9 | node-gyp \
10 | emacs
11 |
12 | RUN cd /data && git clone https://github.com/mash/node-imagemagick-native.git
13 | WORKDIR /data/node-imagemagick-native
14 |
15 | RUN npm install --unsafe-perm
16 |
17 | # to test pull requests
18 | RUN git config --local --add remote.origin.fetch "+refs/pull/*/head:refs/remotes/pr/*" && \
19 | git fetch
20 |
21 | CMD ["bash"]
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # node-imagemagick-native
2 |
3 | [ImageMagick](http://www.imagemagick.org/)'s [Magick++](http://www.imagemagick.org/Magick++/) binding for [Node](http://nodejs.org/).
4 |
5 | Features
6 |
7 | * Native bindings to the C/C++ Magick++ library
8 | * Async, sync, stream and promises API
9 | * Support for `convert`, `identify`, `composite`, and other utility functions
10 |
11 | [](https://travis-ci.org/elad/node-imagemagick-native)
12 |
13 | Table of contents
14 |
15 | * [Examples](#examples)
16 | * [Convert formats](#example-convert) (PNG to JPEG)
17 | * [Blur](#example-blur)
18 | * [Resize](#example-resize)
19 | * [Rotate, flip, and mirror](#example-rotate-flip-mirror)
20 | * [API Reference](#api)
21 | * [`convert`](#convert)
22 | * [`identify`](#identify)
23 | * [`quantizeColors`](#quantizeColors)
24 | * [`composite`](#composite)
25 | * [`getConstPixels`](#getConstPixels)
26 | * [`quantumDepth`](#quantumDepth)
27 | * [`version`](#version)
28 | * [Promises](#promises)
29 | * [Installation](#installation)
30 | * [Linux / Mac OS X](#installation-unix)
31 | * [Windows](#installation-windows)
32 | * [Performance](#performance)
33 | * [Contributing](#contributing)
34 | * [License](#license)
35 |
36 |
37 |
38 | ## Examples
39 |
40 |
41 |
42 | ### Convert formats
43 |
44 | Convert from one format to another with quality control:
45 |
46 | ```js
47 | fs.writeFileSync('after.png', imagemagick.convert({
48 | srcData: fs.readFileSync('before.jpg'),
49 | format: 'PNG',
50 | quality: 100 // (best) to 1 (worst)
51 | }));
52 | ```
53 |
54 | Original JPEG:
55 |
56 | 
57 |
58 | Converted to PNG:
59 |
60 | quality 100 | quality 50 | quality 1
61 | :---: | :---: | :---:
62 |  |  | 
63 |
64 | *Image courtesy of [David Yu](https://www.flickr.com/photos/davidyuweb/14175248591).*
65 |
66 |
67 |
68 | ### Blur
69 |
70 | Blur image:
71 |
72 | ```js
73 | fs.writeFileSync('after.jpg', imagemagick.convert({
74 | srcData: fs.readFileSync('before.jpg'),
75 | blur: 5
76 | }));
77 | ```
78 |
79 |  becomes 
80 |
81 | *Image courtesy of [Tambako The Jaguar](https://www.flickr.com/photos/tambako/3574360498).*
82 |
83 |
84 |
85 | ### Resize
86 |
87 | Resized images by specifying `width` and `height`. There are three resizing styles:
88 |
89 | * `aspectfill`: Default. The resulting image will be exactly the specified size, and may be cropped.
90 | * `aspectfit`: Scales the image so that it will not have to be cropped.
91 | * `fill`: Squishes or stretches the image so that it fills exactly the specified size.
92 |
93 | ```js
94 | fs.writeFileSync('after_resize.jpg', imagemagick.convert({
95 | srcData: fs.readFileSync('before_resize.jpg'),
96 | width: 100,
97 | height: 100,
98 | resizeStyle: 'aspectfill', // is the default, or 'aspectfit' or 'fill'
99 | gravity: 'Center' // optional: position crop area when using 'aspectfill'
100 | }));
101 | ```
102 |
103 | Original:
104 |
105 | 
106 |
107 | Resized:
108 |
109 | aspectfill | aspectfit | fill
110 | :---: | :---: | :---:
111 |  |  | 
112 |
113 | *Image courtesy of [Christoph](https://www.flickr.com/photos/scheinwelten/381994831).*
114 |
115 |
116 |
117 | ### Rotate, flip, and mirror
118 |
119 | Rotate and flip images, and combine the two to mirror:
120 |
121 | ```js
122 | fs.writeFileSync('after_rotateflip.jpg', imagemagick.convert({
123 | srcData: fs.readFileSync('before_rotateflip.jpg'),
124 | rotate: 180,
125 | flip: true
126 | }));
127 | ```
128 |
129 | Original:
130 |
131 | 
132 |
133 | Modified:
134 |
135 | rotate 90 degrees | rotate 180 degrees | flip | flip + rotate 180 degrees = mirror
136 | :---: | :---: | :---: | :---:
137 |  |  |  | 
138 |
139 | *Image courtesy of [Bill Gracey](https://www.flickr.com/photos/9422878@N08/6482704235).*
140 |
141 |
142 |
143 | ## API Reference
144 |
145 |
146 |
147 | ### convert(options, [callback])
148 |
149 | Convert a buffer provided as `options.srcData` and return a Buffer.
150 |
151 | The `options` argument can have following values:
152 |
153 | {
154 | srcData: required. Buffer with binary image data
155 | srcFormat: optional. force source format if not detected (e.g. 'ICO'), one of http://www.imagemagick.org/script/formats.php
156 | quality: optional. 1-100 integer, default 75. JPEG/MIFF/PNG compression level.
157 | trim: optional. default: false. trims edges that are the background color.
158 | trimFuzz: optional. [0-1) float, default 0. trimmed color distance to edge color, 0 is exact.
159 | width: optional. px.
160 | height: optional. px.
161 | density optional. Integer dpi value to convert
162 | resizeStyle: optional. default: 'aspectfill'. can be 'aspectfit', 'fill'
163 | aspectfill: keep aspect ratio, get the exact provided size.
164 | aspectfit: keep aspect ratio, get maximum image that fits inside provided size
165 | fill: forget aspect ratio, get the exact provided size
166 | crop: no resize, get provided size with [x|y]offset
167 | xoffset: optional. default 0: when use crop resizeStyle x margin
168 | yoffset: optional. default 0: when use crop resizeStyle y margin
169 | gravity: optional. default: 'Center'. used to position the crop area when resizeStyle is 'aspectfill'
170 | can be 'NorthWest', 'North', 'NorthEast', 'West',
171 | 'Center', 'East', 'SouthWest', 'South', 'SouthEast', 'None'
172 | format: optional. output format, ex: 'JPEG'. see below for candidates
173 | filter: optional. resize filter. ex: 'Lagrange', 'Lanczos'. see below for candidates
174 | blur: optional. ex: 0.8
175 | strip: optional. default: false. strips comments out from image.
176 | rotate: optional. degrees.
177 | flip: optional. vertical flip, true or false.
178 | autoOrient: optional. default: false. Auto rotate and flip using orientation info.
179 | colorspace: optional. String: Out file use that colorspace ['CMYK', 'sRGB', ...]
180 | debug: optional. true or false
181 | ignoreWarnings: optional. true or false
182 | }
183 |
184 | Notes
185 |
186 | * `format` values can be found [here](http://www.imagemagick.org/script/formats.php)
187 | * `filter` values can be found [here](http://www.imagemagick.org/script/command-line-options.php?ImageMagick=9qgp8o06f469m3cna9lfigirc5#filter)
188 |
189 | An optional `callback` argument can be provided, in which case `convert` will run asynchronously. When it is done, `callback` will be called with the error and the result buffer:
190 |
191 | ```js
192 | imagemagick.convert({
193 | // options
194 | }, function (err, buffer) {
195 | // check err, use buffer
196 | });
197 | ```
198 |
199 | There is also a stream version:
200 |
201 | ```js
202 | fs.createReadStream('input.png').pipe(imagemagick.streams.convert({
203 | // options
204 | })).pipe(fs.createWriteStream('output.png'));
205 | ```
206 |
207 |
208 |
209 | ### identify(options, [callback])
210 |
211 | Identify a buffer provided as `srcData` and return an object.
212 |
213 | The `options` argument can have following values:
214 |
215 | {
216 | srcData: required. Buffer with binary image data
217 | debug: optional. true or false
218 | ignoreWarnings: optional. true or false
219 | }
220 |
221 | An optional `callback` argument can be provided, in which case `identify` will run asynchronously. When it is done, `callback` will be called with the error and the result object:
222 |
223 | ```js
224 | imagemagick.identify({
225 | // options
226 | }, function (err, result) {
227 | // check err, use result
228 | });
229 | ```
230 |
231 | The method returns an object similar to:
232 |
233 | ```js
234 | {
235 | format: 'JPEG',
236 | width: 3904,
237 | height: 2622,
238 | depth: 8,
239 | colorspace: 'sRGB',
240 | density : {
241 | width : 300,
242 | height : 300
243 | },
244 | exif: {
245 | orientation: 0 // if none exists or e.g. 3 (portrait iPad pictures)
246 | }
247 | }
248 | ```
249 |
250 |
251 |
252 | ### quantizeColors(options)
253 |
254 | Quantize the image to a specified amount of colors from a buffer provided as `srcData` and return an array.
255 |
256 | The `options` argument can have following values:
257 |
258 | {
259 | srcData: required. Buffer with binary image data
260 | colors: required. number of colors to extract, defaults to 5
261 | debug: optional. true or false
262 | ignoreWarnings: optional. true or false
263 | }
264 |
265 | The method returns an array similar to:
266 |
267 | ```js
268 | [
269 | {
270 | r: 83,
271 | g: 56,
272 | b: 35,
273 | hex: '533823'
274 | },
275 | {
276 | r: 149,
277 | g: 110,
278 | b: 73,
279 | hex: '956e49'
280 | },
281 | {
282 | r: 165,
283 | g: 141,
284 | b: 111,
285 | hex: 'a58d6f'
286 | }
287 | ]
288 | ```
289 |
290 |
291 |
292 | ### composite(options, [callback])
293 |
294 | Composite a buffer provided as `options.compositeData` on a buffer provided as `options.srcData` with gravity specified by `options.gravity` and return a Buffer.
295 |
296 | The `options` argument can have following values:
297 |
298 | {
299 | srcData: required. Buffer with binary image data
300 | compositeData: required. Buffer with binary image data
301 | gravity: optional. Can be one of 'CenterGravity' 'EastGravity' 'ForgetGravity' 'NorthEastGravity' 'NorthGravity' 'NorthWestGravity' 'SouthEastGravity' 'SouthGravity' 'SouthWestGravity' 'WestGravity'
302 | debug: optional. true or false
303 | ignoreWarnings: optional. true or false
304 | }
305 |
306 | An optional `callback` argument can be provided, in which case `composite` will run asynchronously. When it is done, `callback` will be called with the error and the result buffer:
307 |
308 | ```js
309 | imagemagick.composite(options, function (err, buffer) {
310 | // check err, use buffer
311 | });
312 | ```
313 |
314 | This library currently provide only these, please try [node-imagemagick](https://github.com/rsms/node-imagemagick/) if you want more.
315 |
316 |
317 |
318 | ### getConstPixels(options)
319 |
320 | Get pixels of provided rectangular region.
321 |
322 | The `options` argument can have following values:
323 |
324 | {
325 | srcData: required. Buffer with binary image data
326 | x: required. x,y,columns,rows provide the area of interest.
327 | y: required.
328 | columns: required.
329 | rows: required.
330 | }
331 |
332 | Example usage:
333 |
334 | ```js
335 | // retrieve first pixel of image
336 | var pixels = imagemagick.getConstPixels({
337 | srcData: imageBuffer, // white image
338 | x: 0,
339 | y: 0,
340 | columns: 1,
341 | rows: 1
342 | });
343 | ```
344 |
345 | Returns:
346 |
347 | ```js
348 | [ { red: 65535, green: 65535, blue: 65535, opacity: 65535 } ]
349 | ```
350 |
351 | Where each color value's size is `imagemagick.quantumDepth` bits.
352 |
353 |
354 |
355 | ### quantumDepth
356 |
357 | Return ImageMagick's QuantumDepth, which is defined in compile time.
358 | ex: 16
359 |
360 |
361 |
362 | ### version
363 |
364 | Return ImageMagick's version as string.
365 | ex: '6.7.7'
366 |
367 |
368 |
369 | ## Promises
370 |
371 | The namespace promises expose functions convert, composite and identify that returns a Promise.
372 |
373 | Examples:
374 |
375 | ````js
376 | // convert
377 | imagemagick.promises.convert({ /* OPTIONS */ })
378 | .then(function(buff) { /* Write buffer */ })
379 | .catch(function(err) { /* log err */ })
380 |
381 | // ES8
382 | const buff = await imagemagick.promises.convert({ /* OPTIONS */ })
383 |
384 | // composite
385 | imagemagick.promises.composite({ /* OPTIONS */ })
386 | .then(function(buff) { /* Write buffer */ })
387 | .catch(function(err) { /* log err */ })
388 |
389 | // ES8
390 | const buff = await imagemagick.promises.composite({ /* OPTIONS */ })
391 |
392 | // identify
393 | imagemagick.promises.identify({ /* OPTIONS */ })
394 | .then(function(info) { /* Write buffer */ })
395 | .catch(function(err) { /* log err */ })
396 |
397 | // ES8
398 | const info = await imagemagick.promises.identify({ /* OPTIONS */ })
399 | ````
400 |
401 |
402 |
403 | ## Installation
404 |
405 |
406 |
407 | ### Linux / Mac OS X
408 |
409 | Install [ImageMagick](http://www.imagemagick.org/) with headers before installing this module.
410 | Tested with ImageMagick 6.7.7 on CentOS 6 and Mac OS X Lion, Ubuntu 12.04 .
411 |
412 | brew install imagemagick
413 |
414 | or
415 |
416 | sudo yum install ImageMagick-c++ ImageMagick-c++-devel
417 |
418 | or
419 |
420 | sudo apt-get install libmagick++-dev
421 |
422 | Make sure you can find Magick++-config in your PATH. Packages on some newer distributions, such as Ubuntu 16.04, might be missing a link into `/usr/bin`. If that is the case, do this.
423 |
424 | sudo ln -s `ls /usr/lib/\`uname -p\`-linux-gnu/ImageMagick-*/bin-Q16/Magick++-config | head -n 1` /usr/local/bin/
425 |
426 | Then:
427 |
428 | npm install imagemagick-native
429 |
430 | **Installation notes**
431 |
432 | * RHEL/CentOS: If the version of ImageMagick required by `node-imagemagick-native` is not available in an official RPM repository, please try the `-last` version offered by Les RPM de Remi, for example:
433 |
434 | ```
435 | sudo yum remove -y ImageMagick
436 | sudo yum install -y http://rpms.famillecollet.com/enterprise/remi-release-6.rpm
437 | sudo yum install -y --enablerepo=remi ImageMagick-last-c++-devel
438 | ```
439 |
440 | * Mac OS X: You might need to install `pkgconfig` first:
441 |
442 | ```
443 | brew install pkgconfig
444 | ```
445 |
446 |
447 |
448 | ### Windows
449 |
450 | Tested on Windows 7 x64.
451 |
452 | 1. Install Python 2.7.3 only 2.7.3 nothing else works quite right!
453 |
454 | If you use Cygwin ensure you don't have Python installed in Cygwin setup as there will be some confusion about what version to use.
455 |
456 | 2. Install [Visual Studio C++ 2010 Express](http://www.microsoft.com/en-us/download/details.aspx?id=8279)
457 |
458 | 3. (64-bit only) [Install Windows 7 64-bit SDK](http://www.microsoft.com/en-us/download/details.aspx?id=8279)
459 |
460 | 4. Install [Imagemagick dll binary x86 Q16](http://www.imagemagick.org/download/binaries/ImageMagick-6.9.8-4-Q16-x86-dll.exe) or [Imagemagick dll binary x64 Q16](http://www.imagemagick.org/download/binaries/ImageMagick-6.9.8-4-Q16-x64-dll.exe), check for libraries and includes during install.
461 |
462 | Then:
463 |
464 | npm install imagemagick-native
465 |
466 |
467 |
468 | ## Performance - simple thumbnail creation
469 |
470 | imagemagick: 16.09ms per iteration
471 | imagemagick-native: 0.89ms per iteration
472 |
473 | See `node test/benchmark.js` for details.
474 |
475 | **Note:** `node-imagemagick-native`'s primary advantage is that it uses ImageMagick's API directly rather than by executing one of its command line tools. This means that it will be much faster when the amount of time spent inside the library is small and less so otherwise. See [issue #46](https://github.com/mash/node-imagemagick-native/issues/46) for discussion.
476 |
477 |
478 |
479 | ## Contributing
480 |
481 | This project follows the ["OPEN Open Source"](https://gist.github.com/substack/e205f5389890a1425233) philosophy. If you submit a pull request and it gets merged you will most likely be given commit access to this repository.
482 |
483 |
484 |
485 | ## License (MIT)
486 |
487 | Copyright (c) Masakazu Ohtsuka
488 |
489 | Permission is hereby granted, free of charge, to any person obtaining a copy
490 | of this software and associated documentation files (the 'Software'), to deal
491 | in the Software without restriction, including without limitation the rights
492 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
493 | copies of the Software, and to permit persons to whom the Software is
494 | furnished to do so, subject to the following conditions:
495 |
496 | The above copyright notice and this permission notice shall be included in
497 | all copies or substantial portions of the Software.
498 |
499 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
500 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
501 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
502 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
503 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
504 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
505 | THE SOFTWARE.
506 |
--------------------------------------------------------------------------------
/binding.gyp:
--------------------------------------------------------------------------------
1 | {
2 | 'conditions': [
3 | ['OS=="win"', {
4 | 'variables': {
5 | 'MAGICK_ROOT%': ' {
44 | func(options, function(err, buff) {
45 | if (err) {
46 | return reject(err);
47 | }
48 | resolve(buff);
49 | });
50 | });
51 | };
52 | }
53 |
54 | module.exports.promises = {
55 | convert: promisify(module.exports.convert),
56 | identify: promisify(module.exports.identify),
57 | composite: promisify(module.exports.composite),
58 | };
59 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "imagemagick-native",
3 | "description": "ImageMagick's Magick++ bindings for NodeJS",
4 | "keywords": [
5 | "imagemagick",
6 | "magick++",
7 | "resize",
8 | "convert"
9 | ],
10 | "version": "1.9.3",
11 | "license": "MIT",
12 | "repository": {
13 | "type": "git",
14 | "url": "git://github.com/mash/node-imagemagick-native.git"
15 | },
16 | "author": "Masakazu Ohtsuka (http://maaash.jp/)",
17 | "contributors": [],
18 | "main": "./index.js",
19 | "scripts": {
20 | "test": "tap test/test*.js",
21 | "install": "node-gyp rebuild"
22 | },
23 | "engines": {
24 | "node": ">=4"
25 | },
26 | "dependencies": {
27 | "nan": "2.x"
28 | },
29 | "devDependencies": {
30 | "tap": "*"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/imagemagick.cc:
--------------------------------------------------------------------------------
1 | #ifndef BUILDING_NODE_EXTENSION
2 | #define BUILDING_NODE_EXTENSION
3 | #endif // BUILDING_NODE_EXTENSION
4 |
5 | #if _MSC_VER && _MSC_VER < 1900
6 | #define snprintf _snprintf
7 | #endif
8 |
9 | #include "imagemagick.h"
10 | #include
11 | #include
12 | #include
13 | #include
14 |
15 | // RAII to reset image magick's resource limit
16 | class LocalResourceLimiter
17 | {
18 | public:
19 | LocalResourceLimiter()
20 | : originalMemory(0),
21 | memoryLimited(0),
22 | originalDisk(0),
23 | diskLimited(0) {
24 | }
25 | ~LocalResourceLimiter() {
26 | if (memoryLimited) {
27 | MagickCore::SetMagickResourceLimit(MagickCore::MemoryResource,originalMemory);
28 | }
29 | if (diskLimited) {
30 | MagickCore::SetMagickResourceLimit(MagickCore::DiskResource,originalDisk);
31 | }
32 | }
33 | void LimitMemory(MagickCore::MagickSizeType to) {
34 | if ( ! memoryLimited ) {
35 | memoryLimited = true;
36 | originalMemory = MagickCore::GetMagickResourceLimit(MagickCore::MemoryResource);
37 | }
38 | MagickCore::SetMagickResourceLimit(MagickCore::MemoryResource, to);
39 | }
40 | void LimitDisk(MagickCore::MagickSizeType to) {
41 | if ( ! diskLimited ) {
42 | diskLimited = true;
43 | originalDisk = MagickCore::GetMagickResourceLimit(MagickCore::DiskResource);
44 | }
45 | MagickCore::SetMagickResourceLimit(MagickCore::DiskResource, to);
46 | }
47 |
48 | private:
49 | MagickCore::MagickSizeType originalMemory;
50 | bool memoryLimited;
51 | MagickCore::MagickSizeType originalDisk;
52 | bool diskLimited;
53 | };
54 |
55 | // Base context for calls shared on sync and async code paths
56 | struct im_ctx_base {
57 | Nan::Callback * callback;
58 | std::string error;
59 |
60 | char* srcData;
61 | size_t length;
62 | int debug;
63 | int ignoreWarnings;
64 | std::string srcFormat;
65 |
66 | // generated blob by convert or composite
67 | Magick::Blob dstBlob;
68 |
69 | virtual ~im_ctx_base() {}
70 | };
71 | // Extra context for identify
72 | struct identify_im_ctx : im_ctx_base {
73 | Magick::Image image;
74 |
75 | identify_im_ctx() {}
76 | };
77 | // Extra context for convert
78 | struct convert_im_ctx : im_ctx_base {
79 | unsigned int maxMemory;
80 |
81 | unsigned int width;
82 | unsigned int height;
83 | unsigned int xoffset;
84 | unsigned int yoffset;
85 | bool strip;
86 | bool trim;
87 | bool autoOrient;
88 | double trimFuzz;
89 | std::string resizeStyle;
90 | std::string gravity;
91 | std::string format;
92 | std::string filter;
93 | std::string blur;
94 | std::string background;
95 | Magick::ColorspaceType colorspace;
96 | unsigned int quality;
97 | int rotate;
98 | int density;
99 | int flip;
100 |
101 | convert_im_ctx() {}
102 | };
103 | // Extra context for composite
104 | struct composite_im_ctx : im_ctx_base {
105 | char* compositeData;
106 | size_t compositeLength;
107 |
108 | std::string gravity;
109 |
110 | composite_im_ctx() {}
111 | };
112 |
113 |
114 | inline Local WrapPointer(char *ptr, size_t length) {
115 | Nan::EscapableHandleScope scope;
116 | return scope.Escape(Nan::CopyBuffer(ptr, length).ToLocalChecked());
117 | }
118 | inline Local WrapPointer(char *ptr) {
119 | return WrapPointer(ptr, 0);
120 | }
121 |
122 |
123 | #define RETURN_BLOB_OR_ERROR(req) \
124 | do { \
125 | im_ctx_base* _context = static_cast(req->data); \
126 | if (!_context->error.empty()) { \
127 | Nan::ThrowError(_context->error.c_str()); \
128 | } else { \
129 | const Local _retBuffer = WrapPointer((char *)_context->dstBlob.data(), _context->dstBlob.length()); \
130 | info.GetReturnValue().Set(_retBuffer); \
131 | } \
132 | delete req; \
133 | } while(0);
134 |
135 |
136 | bool ReadImageMagick(Magick::Image *image, Magick::Blob srcBlob, std::string srcFormat, im_ctx_base *context) {
137 | if( ! srcFormat.empty() ){
138 | if (context->debug) printf( "reading with format: %s\n", srcFormat.c_str() );
139 | image->magick( srcFormat.c_str() );
140 | }
141 |
142 | try {
143 | image->read( srcBlob );
144 | }
145 | catch (Magick::Warning& warning) {
146 | if (!context->ignoreWarnings) {
147 | context->error = warning.what();
148 | return false;
149 | } else if (context->debug) {
150 | printf("warning: %s\n", warning.what());
151 | }
152 | }
153 | catch (std::exception& err) {
154 | context->error = err.what();
155 | return false;
156 | }
157 | catch (...) {
158 | context->error = std::string("unhandled error");
159 | return false;
160 | }
161 | return true;
162 | }
163 |
164 | void AutoOrient(Magick::Image *image) {
165 | switch (image->orientation()) {
166 | case Magick::OrientationType::UndefinedOrientation: // No orientation info
167 | case Magick::OrientationType::TopLeftOrientation:
168 | break;
169 | case Magick::OrientationType::TopRightOrientation:
170 | image->rotate(180);
171 | image->flip();
172 | break;
173 | case Magick::OrientationType::BottomRightOrientation:
174 | image->rotate(180);
175 | break;
176 | case Magick::OrientationType::BottomLeftOrientation:
177 | image->flip();
178 | break;
179 | case Magick::OrientationType::LeftTopOrientation:
180 | image->flip();
181 | image->rotate(90);
182 | break;
183 | case Magick::OrientationType::RightTopOrientation:
184 | image->rotate(90);
185 | break;
186 | case Magick::OrientationType::RightBottomOrientation:
187 | image->flip();
188 | image->rotate(270);
189 | break;
190 | case Magick::OrientationType::LeftBottomOrientation:
191 | image->rotate(270);
192 | break;
193 | }
194 |
195 | // Erase orientation metadata after rotating the image to avoid double-rotation
196 | image->orientation(Magick::OrientationType::UndefinedOrientation);
197 | }
198 |
199 | void DoConvert(uv_work_t* req) {
200 |
201 | convert_im_ctx* context = static_cast(req->data);
202 |
203 | MagickCore::SetMagickResourceLimit(MagickCore::ThreadResource, 1);
204 | LocalResourceLimiter limiter;
205 |
206 | int debug = context->debug;
207 |
208 | if (debug) printf( "debug: on\n" );
209 | if (debug) printf( "ignoreWarnings: %d\n", context->ignoreWarnings );
210 |
211 | if (context->maxMemory > 0) {
212 | limiter.LimitMemory(context->maxMemory);
213 | limiter.LimitDisk(context->maxMemory); // avoid using unlimited disk as cache
214 | if (debug) printf( "maxMemory set to: %d\n", context->maxMemory );
215 | }
216 |
217 | Magick::Blob srcBlob( context->srcData, context->length );
218 |
219 | Magick::Image image;
220 |
221 | if ( !ReadImageMagick(&image, srcBlob, context->srcFormat, context) )
222 | return;
223 |
224 | if (!context->background.empty()) {
225 | try {
226 | Magick::Color bg(context->background.c_str());
227 | Magick::Image background(image.size(), bg);
228 |
229 | if (debug) {
230 | printf("background: %s\n", static_cast(bg).c_str());
231 | }
232 |
233 | background.composite(image, Magick::ForgetGravity, Magick::OverCompositeOp);
234 | image.composite(background, Magick::ForgetGravity, Magick::CopyCompositeOp);
235 | } catch ( Magick::WarningOption &warning ){
236 | if (debug) printf("Warning: %s\n", warning.what());
237 | }
238 | }
239 |
240 | if (debug) printf("original width,height: %d, %d\n", (int) image.columns(), (int) image.rows());
241 |
242 | unsigned int width = context->width;
243 | if (debug) printf( "width: %d\n", width );
244 |
245 | unsigned int height = context->height;
246 | if (debug) printf( "height: %d\n", height );
247 |
248 | if ( context->strip ) {
249 | if (debug) printf( "strip: true\n" );
250 | image.strip();
251 | }
252 |
253 | if ( context->trim ) {
254 | if (debug) printf( "trim: true\n" );
255 | double trimFuzz = context->trimFuzz;
256 | if ( trimFuzz != trimFuzz ) {
257 | image.trim();
258 | } else {
259 | if (debug) printf( "fuzz: %lf\n", trimFuzz );
260 | double fuzz = image.colorFuzz();
261 | image.colorFuzz(trimFuzz);
262 | image.trim();
263 | image.colorFuzz(fuzz);
264 | if (debug) printf( "restored fuzz: %lf\n", fuzz );
265 | }
266 | if (debug) printf( "trimmed width,height: %d, %d\n", (int) image.columns(), (int) image.rows() );
267 | }
268 |
269 | const char* resizeStyle = context->resizeStyle.c_str();
270 | if (debug) printf( "resizeStyle: %s\n", resizeStyle );
271 |
272 | const char* gravity = context->gravity.c_str();
273 | if ( strcmp("Center", gravity)!=0
274 | && strcmp("East", gravity)!=0
275 | && strcmp("West", gravity)!=0
276 | && strcmp("North", gravity)!=0
277 | && strcmp("South", gravity)!=0
278 | && strcmp("NorthEast", gravity)!=0
279 | && strcmp("NorthWest", gravity)!=0
280 | && strcmp("SouthEast", gravity)!=0
281 | && strcmp("SouthWest", gravity)!=0
282 | && strcmp("None", gravity)!=0
283 | ) {
284 | context->error = std::string("gravity not supported");
285 | return;
286 | }
287 | if (debug) printf( "gravity: %s\n", gravity );
288 |
289 | if( ! context->format.empty() ){
290 | if (debug) printf( "format: %s\n", context->format.c_str() );
291 | image.magick( context->format.c_str() );
292 | }
293 |
294 | if( ! context->filter.empty() ){
295 | const char *filter = context->filter.c_str();
296 |
297 | ssize_t option_info = MagickCore::ParseCommandOption(MagickCore::MagickFilterOptions, Magick::MagickFalse, filter);
298 | if (option_info != -1) {
299 | if (debug) printf( "filter: %s\n", filter );
300 | image.filterType( (Magick::FilterTypes)option_info );
301 | }
302 | else {
303 | context->error = std::string("filter not supported");
304 | return;
305 | }
306 | }
307 |
308 | if( ! context->blur.empty() ) {
309 | double blur = atof (context->blur.c_str());
310 | if (debug) printf( "blur: %.1f\n", blur );
311 | image.blur(0, blur);
312 | }
313 |
314 | if ( width || height ) {
315 | if ( ! width ) { width = image.columns(); }
316 | if ( ! height ) { height = image.rows(); }
317 |
318 | // do resize
319 | if ( strcmp( resizeStyle, "aspectfill" ) == 0 ) {
320 | // ^ : Fill Area Flag ('^' flag)
321 | // is not implemented in Magick++
322 | // and gravity: center, extent doesnt look like working as exptected
323 | // so we do it ourselves
324 |
325 | // keep aspect ratio, get the exact provided size, crop top/bottom or left/right if necessary
326 | double aspectratioExpected = (double)height / (double)width;
327 | double aspectratioOriginal = (double)image.rows() / (double)image.columns();
328 | unsigned int xoffset = 0;
329 | unsigned int yoffset = 0;
330 | unsigned int resizewidth;
331 | unsigned int resizeheight;
332 |
333 | if ( aspectratioExpected > aspectratioOriginal ) {
334 | // expected is taller
335 | resizewidth = (unsigned int)( (double)height / (double)image.rows() * (double)image.columns() + 1. );
336 | resizeheight = height;
337 | if ( strstr(gravity, "West") != NULL ) {
338 | xoffset = 0;
339 | }
340 | else if ( strstr(gravity, "East") != NULL ) {
341 | xoffset = (unsigned int)( resizewidth - width );
342 | }
343 | else {
344 | xoffset = (unsigned int)( (resizewidth - width) / 2. );
345 | }
346 | yoffset = 0;
347 | }
348 | else {
349 | // expected is wider
350 | resizewidth = width;
351 | resizeheight = (unsigned int)( (double)width / (double)image.columns() * (double)image.rows() + 1. );
352 | xoffset = 0;
353 | if ( strstr(gravity, "North") != NULL ) {
354 | yoffset = 0;
355 | }
356 | else if ( strstr(gravity, "South") != NULL ) {
357 | yoffset = (unsigned int)( resizeheight - height );
358 | }
359 | else {
360 | yoffset = (unsigned int)( (resizeheight - height) / 2. );
361 | }
362 | }
363 |
364 | if (debug) printf( "resize to: %d, %d\n", resizewidth, resizeheight );
365 | Magick::Geometry resizeGeometry( resizewidth, resizeheight, 0, 0, 0, 0 );
366 | try {
367 | image.zoom( resizeGeometry );
368 | }
369 | catch (std::exception& err) {
370 | std::string message = "image.resize failed with error: ";
371 | message += err.what();
372 | context->error = message;
373 | return;
374 | }
375 | catch (...) {
376 | context->error = std::string("unhandled error");
377 | return;
378 | }
379 |
380 | if ( strcmp ( gravity, "None" ) != 0 ) {
381 | // limit canvas size to cropGeometry
382 | if (debug) printf( "crop to: %d, %d, %d, %d\n", width, height, xoffset, yoffset );
383 | Magick::Geometry cropGeometry( width, height, xoffset, yoffset, 0, 0 );
384 |
385 | Magick::Color transparent( "transparent" );
386 | if ( strcmp( context->format.c_str(), "PNG" ) == 0 ) {
387 | // make background transparent for PNG
388 | // JPEG background becomes black if set transparent here
389 | transparent.alpha( 1. );
390 | }
391 |
392 | #if MagickLibVersion > 0x654
393 | image.extent( cropGeometry, transparent );
394 | #else
395 | image.extent( cropGeometry );
396 | #endif
397 | }
398 |
399 | }
400 | else if ( strcmp ( resizeStyle, "aspectfit" ) == 0 ) {
401 | // keep aspect ratio, get the maximum image which fits inside specified size
402 | char geometryString[ 32 ];
403 | sprintf( geometryString, "%dx%d", width, height );
404 | if (debug) printf( "resize to: %s\n", geometryString );
405 |
406 | try {
407 | image.zoom( geometryString );
408 | }
409 | catch (std::exception& err) {
410 | std::string message = "image.resize failed with error: ";
411 | message += err.what();
412 | context->error = message;
413 | return;
414 | }
415 | catch (...) {
416 | context->error = std::string("unhandled error");
417 | return;
418 | }
419 | }
420 | else if ( strcmp ( resizeStyle, "fill" ) == 0 ) {
421 | // change aspect ratio and fill specified size
422 | char geometryString[ 32 ];
423 | sprintf( geometryString, "%dx%d!", width, height );
424 | if (debug) printf( "resize to: %s\n", geometryString );
425 |
426 | try {
427 | image.zoom( geometryString );
428 | }
429 | catch (std::exception& err) {
430 | std::string message = "image.resize failed with error: ";
431 | message += err.what();
432 | context->error = message;
433 | return;
434 | }
435 | catch (...) {
436 | context->error = std::string("unhandled error");
437 | return;
438 | }
439 | }
440 | else if ( strcmp ( resizeStyle, "crop" ) == 0 ) {
441 | unsigned int xoffset = context->xoffset;
442 | unsigned int yoffset = context->yoffset;
443 |
444 | if ( ! xoffset ) { xoffset = 0; }
445 | if ( ! yoffset ) { yoffset = 0; }
446 |
447 | // limit canvas size to cropGeometry
448 | if (debug) printf( "crop to: %d, %d, %d, %d\n", width, height, xoffset, yoffset );
449 | Magick::Geometry cropGeometry( width, height, xoffset, yoffset, 0, 0 );
450 |
451 | Magick::Color transparent( "transparent" );
452 | if ( strcmp( context->format.c_str(), "PNG" ) == 0 ) {
453 | // make background transparent for PNG
454 | // JPEG background becomes black if set transparent here
455 | transparent.alpha( 1. );
456 | }
457 |
458 | #if MagickLibVersion > 0x654
459 | image.extent( cropGeometry, transparent );
460 | #else
461 | image.extent( cropGeometry );
462 | #endif
463 |
464 | }
465 | else {
466 | context->error = std::string("resizeStyle not supported");
467 | return;
468 | }
469 | if (debug) printf( "resized to: %d, %d\n", (int)image.columns(), (int)image.rows() );
470 | }
471 |
472 | if ( context->quality ) {
473 | if (debug) printf( "quality: %d\n", context->quality );
474 | image.quality( context->quality );
475 | }
476 |
477 | if ( context->autoOrient ) {
478 | if ( debug ) printf( "autoOrient\n" );
479 | AutoOrient(&image);
480 | }
481 | else {
482 | if ( context->rotate ) {
483 | if (debug) printf( "rotate: %d\n", context->rotate );
484 | image.rotate( context->rotate );
485 | }
486 |
487 | if ( context->flip ) {
488 | if ( debug ) printf( "flip\n" );
489 | image.flip();
490 | }
491 | }
492 |
493 | if (context->density) {
494 | image.density(Magick::Geometry(context->density, context->density));
495 | }
496 |
497 | if( context->colorspace != Magick::UndefinedColorspace ){
498 | if (debug) printf( "colorspace: %s\n", MagickCore::CommandOptionToMnemonic(MagickCore::MagickColorspaceOptions, static_cast(context->colorspace)) );
499 | image.colorSpace( context->colorspace );
500 | }
501 |
502 | Magick::Blob dstBlob;
503 | try {
504 | image.write( &dstBlob );
505 | }
506 | catch (std::exception& err) {
507 | std::string message = "image.write failed with error: ";
508 | message += err.what();
509 | context->error = message;
510 | return;
511 | }
512 | catch (...) {
513 | context->error = std::string("unhandled error");
514 | return;
515 | }
516 | context->dstBlob = dstBlob;
517 | }
518 |
519 | // Make callback from convert or composite
520 | void GeneratedBlobAfter(uv_work_t* req) {
521 | Nan::HandleScope scope;
522 |
523 | im_ctx_base* context = static_cast(req->data);
524 | delete req;
525 |
526 | Local argv[2];
527 |
528 | if (!context->error.empty()) {
529 | argv[0] = Exception::Error(Nan::New(context->error.c_str()).ToLocalChecked());
530 | argv[1] = Nan::Undefined();
531 | }
532 | else {
533 | argv[0] = Nan::Undefined();
534 | argv[1] = WrapPointer((char *)context->dstBlob.data(), context->dstBlob.length());
535 | }
536 |
537 | Nan::TryCatch try_catch; // don't quite see the necessity of this
538 |
539 | Nan::AsyncResource resource("GeneratedBlobAfter");
540 | context->callback->Call(2, argv, &resource);
541 |
542 | delete context->callback;
543 |
544 | delete context;
545 |
546 | if (try_catch.HasCaught()) {
547 | #if NODE_VERSION_AT_LEAST(0, 12, 0)
548 | Nan::FatalException(try_catch);
549 | #else
550 | FatalException(try_catch);
551 | #endif
552 | }
553 | }
554 |
555 | // input
556 | // info[ 0 ]: options. required, object with following key,values
557 | // {
558 | // srcData: required. Buffer with binary image data
559 | // quality: optional. 0-100 integer, default 75. JPEG/MIFF/PNG compression level.
560 | // trim: optional. default: false. trims edges that are the background color.
561 | // trimFuzz: optional. [0-1) float, default 0. trimmed color distance to edge color, 0 is exact.
562 | // width: optional. px.
563 | // height: optional. px.
564 | // resizeStyle: optional. default: "aspectfill". can be "aspectfit", "fill"
565 | // gravity: optional. default: "Center". used when resizeStyle is "aspectfill"
566 | // can be "NorthWest", "North", "NorthEast", "West",
567 | // "Center", "East", "SouthWest", "South", "SouthEast", "None"
568 | // format: optional. one of http://www.imagemagick.org/script/formats.php ex: "JPEG"
569 | // filter: optional. ex: "Lagrange", "Lanczos". see ImageMagick's magick/option.c for candidates
570 | // blur: optional. ex: 0.8
571 | // strip: optional. default: false. strips comments out from image.
572 | // maxMemory: optional. set the maximum width * height of an image that can reside in the pixel cache memory.
573 | // debug: optional. 1 or 0
574 | // }
575 | // info[ 1 ]: callback. optional, if present runs async and returns result with callback(error, buffer)
576 | NAN_METHOD(Convert) {
577 | Nan::HandleScope();
578 |
579 | bool isSync = (info.Length() == 1);
580 |
581 | if ( info.Length() < 1 ) {
582 | return Nan::ThrowError("convert() requires 1 (option) argument!");
583 | }
584 |
585 | if ( ! info[ 0 ]->IsObject() ) {
586 | return Nan::ThrowError("convert()'s 1st argument should be an object");
587 | }
588 |
589 | if( ! isSync && ! info[ 1 ]->IsFunction() ) {
590 | return Nan::ThrowError("convert()'s 2nd argument should be a function");
591 | }
592 |
593 | Local