Compare commits

..

195 Commits

Author SHA1 Message Date
Lovell Fuller
5c51612982 Version bumps ahead of v0.7.1 2014-10-28 11:12:40 +00:00
Lovell Fuller
a472adeb74 Fail fast for Buffer with unsupported format #105 2014-10-28 10:53:11 +00:00
Lovell Fuller
2e61839387 Prepare preinstall.sh for inclusion in master 2014-10-28 09:59:28 +00:00
Lovell Fuller
51805ef657 Add root check and support for Debian 7, Mint 13 2014-10-27 22:32:39 +00:00
Lovell Fuller
5856e41a62 Merge pull request #111 from papandreou/preinstallOnDebianWheezy
Build from source on Debian Wheezy
2014-10-27 15:47:29 +00:00
Andreas Lind
ffbe6b7d76 Build from source on Debian Wheezy. 2014-10-27 15:23:40 +01:00
Lovell Fuller
ed6a966534 Add shell script to install libvips 2014-10-24 10:57:45 +01:00
Lovell Fuller
97fc2a2a3a Ensure resize error from C++ is wrapped
Thanks to @papandreou
2014-10-23 09:39:37 +01:00
Lovell Fuller
3e1be7a33a Merge pull request #107 from papandreou/doNotUnrefUnparsableImages
Do not call g_object_unref when imageType comes out as UNKNOWN.
2014-10-23 09:23:37 +01:00
Lovell Fuller
4a4dd7f987 Merge pull request #106 from papandreou/errorInstances
Pass errors as Error instances rather than strings.
2014-10-23 09:22:15 +01:00
Andreas Lind
005c628352 Do not call g_object_unref when imageType comes out as UNKNOWN.
Avoids "(sharp:23220): GLib-GObject-CRITICAL **: g_object_unref: assertion 'G_IS_OBJECT (object)' failed" dumped to the console when an unsupported or invalid image is loaded.
2014-10-23 08:32:23 +02:00
Andreas Lind
1bd316de80 Pass errors as Error instances rather than strings. 2014-10-23 08:27:10 +02:00
Lovell Fuller
49b44d8238 Ensure compilation on RHEL/Centos 6 gcc 2014-10-21 22:01:43 +01:00
Lovell Fuller
8bc1981891 Refactor C++ code
Split features into their own file

Style is becoming less C, more C++
2014-10-21 21:38:19 +01:00
Lovell Fuller
db6dc6431b Add support for mirroring #62 2014-10-21 14:47:08 +01:00
Lovell Fuller
f214673c3c Add Coveralls badge 2014-10-21 12:57:24 +01:00
Lovell Fuller
ffe00ee398 Travis CI + Coveralls integration 2014-10-21 12:40:11 +01:00
Lovell Fuller
6cade5bd7f Increase unit test coverage to ~95% 2014-10-21 12:22:23 +01:00
Lovell Fuller
a531b5917e Add details about running leak and bench tests 2014-10-20 15:54:44 +01:00
Lovell Fuller
ca561daedf Test code refactor #63
Use mocha for feature-specific unit tests

Add test coverage report via istanbul
2014-10-20 15:32:56 +01:00
Lovell Fuller
f4cb577cb4 Code lint of new features 2014-10-17 12:26:53 +01:00
Lovell Fuller
91be57cbce Code lint of secondary tests 2014-10-17 12:26:38 +01:00
Lovell Fuller
78596545b0 Dependency version bumps ahead of 0.7.0 2014-10-17 12:15:31 +01:00
Lovell Fuller
9f6cc33858 Memory leak mop-up of new feature spillage 2014-10-13 22:53:43 +01:00
Lovell Fuller
d82de45b7e Thank you Brandon 2014-10-13 17:51:48 +01:00
Lovell Fuller
b7bbf58624 Document new extract method 2014-10-13 15:41:10 +01:00
Lovell Fuller
945d941c7b Convert image to sRGB sooner for embed 2014-10-13 15:39:52 +01:00
Lovell Fuller
2605bf966f Merge pull request #102 from brandonaaron/extract
Add support for stand-alone, pre-resize, and post-resize extraction
2014-10-07 15:19:41 +01:00
Brandon Aaron
83b72a1ede Add support for stand-alone, pre-resize, and post-resize extraction with a topOffset, leftOffset, width, and height. 2014-10-07 09:56:55 -04:00
Lovell Fuller
6190ca4307 Ensure conversion to sRGB occurs before background #99 2014-10-07 10:17:20 +01:00
Lovell Fuller
c2fcf7fc4a Add libvips install info for Debian Jessie #100 2014-10-06 11:27:29 +01:00
Lovell Fuller
37cb4339e2 Import embedded colour profile, if present #99 2014-10-06 10:45:58 +01:00
Lovell Fuller
46f229e308 Use own Angle enum to wrap jcupitt/libvips@154796a 2014-10-06 10:22:45 +01:00
Lovell Fuller
7f8f38f666 PNG compression level range is 0-9 #95 2014-10-03 14:50:15 +01:00
Lovell Fuller
fb0769a327 Improve image reference handling 2014-10-03 11:45:06 +01:00
Lovell Fuller
b84cc3d49e Unit test hardening 2014-10-03 11:42:10 +01:00
Lovell Fuller
0cba506bc4 Allow embed with rgba background #86 #89
Small memory leak mop-up related to #94
2014-10-02 20:02:14 +01:00
Lovell Fuller
5cdfbba55c Merge pull request #94 from gasi/background-flatten
Add `background` and `flatten` APIs
2014-10-01 20:51:52 +01:00
Daniel Gasienica
6145231936 Add background and flatten APIs 2014-09-30 17:58:57 -07:00
Lovell Fuller
513b07ddcf Add hasAlpha attribute to metadata API
Provides internal sharp_image_has_alpha method

Hopefully this helps with #91
2014-09-30 13:35:42 +01:00
Lovell Fuller
150971fa92 Add details of gulp.js plugin 2014-09-30 13:35:10 +01:00
Lovell Fuller
ac85d88c9c Update code style, jshint checks and EOLs 2014-09-19 11:55:52 +01:00
Lovell Fuller
1c79d6fb5d Add support for greyscale conversion #43 2014-09-16 11:22:26 +01:00
Lovell Fuller
d41321254a Update benchmarks using c3.xlarge AWS instance 2014-09-13 20:37:57 +01:00
Lovell Fuller
515b4656e6 Improve Stream error handling #88 2014-09-12 19:30:57 +01:00
Lovell Fuller
34c96ff925 Correct ...magick... test dependencies 2014-09-11 13:45:34 +01:00
Lovell Fuller
b8a04cc4ef Add link to Centos 6.5 CI via snap-ci 2014-09-08 23:58:13 +01:00
Lovell Fuller
ddc3f6e9c6 Add RHEL libvips installation - close #78 2014-09-08 23:53:01 +01:00
Lovell Fuller
3699e61c20 Improve support for extension-less output filenames 2014-09-07 08:59:11 +01:00
Lovell Fuller
2820218609 Add format property to output info
Output format may be derived from input format
2014-09-04 11:38:05 +01:00
Lovell Fuller
eb3e739f7b Expose libvips thread pool size 2014-09-03 19:52:03 +01:00
Lovell Fuller
87f6e83988 Add memory leak check #21
Runs unit tests via valgrind
2014-09-02 13:59:35 +01:00
Lovell Fuller
5728efd32b Add support for gamma correction #43 2014-08-30 14:12:42 +01:00
Lovell Fuller
bac367b005 Notify V8 GC of libvips cache size #21 2014-08-26 23:28:58 +01:00
Lovell Fuller
0d89131f66 Prevent small memory leak with non-empty argv0 #21 2014-08-26 15:25:36 +01:00
Lovell Fuller
8380be4be3 Add nearest neighbour interpolation
Suitable for image enlargement
2014-08-26 09:38:27 +01:00
Lovell Fuller
d0f51363bf Re-order output option methods
Add explicit list of and links to contributors
2014-08-26 09:09:01 +01:00
Lovell Fuller
15160d3b61 Merge pull request #83 from julianojulio/master
Add support for maintaining metadata in output image, thanks @julianojulio #83
2014-08-26 08:31:58 +01:00
Juliano Julio Costa
c5efb77bad New withMetadata([boolean]) to keep metadata in the generated images. 2014-08-25 15:59:21 -04:00
Lovell Fuller
b877751b2d Add support for C++11 on Mac 2014-08-24 21:41:09 +01:00
Lovell Fuller
40db482fd8 Version bumps 2014-08-24 21:40:23 +01:00
Lovell Fuller
98554e919c Small doc improvements ahead of v0.6.0 2014-08-22 16:57:05 +01:00
Lovell Fuller
017bf1e905 Expose libvips interpolators #69 2014-08-22 16:50:24 +01:00
Lovell Fuller
6498fc3a9e Ignore libmagick's inconsistent GIF colour spaces 2014-08-22 14:31:34 +01:00
Lovell Fuller
8ba71c94f4 Add usage example of metadata method 2014-08-22 14:27:46 +01:00
Lovell Fuller
f2f3eb76e1 Add method for fast access to image metadata #32 2014-08-22 14:17:43 +01:00
Lovell Fuller
5fe945fca8 Add gravity support to crop #45 2014-08-21 11:01:25 +01:00
Lovell Fuller
8ef0851a49 Expose libvips' op-cache max items #76 2014-08-19 20:30:21 +01:00
Lovell Fuller
e45956db6c Correct usage examples for new Stream-based API 2014-08-18 19:34:55 +01:00
Lovell Fuller
7cc9f7e2e0 Add Xcode Command Line Tools install info #80 2014-08-18 19:11:53 +01:00
Lovell Fuller
df3903532d Initial Duplex Stream support 2014-08-07 14:00:34 +01:00
Lovell Fuller
46456c9a2a Clear cached warnings after success 2014-08-06 11:34:16 +01:00
Lovell Fuller
e98f2fc013 Small documentation improvement plus version bump 2014-07-20 22:46:26 +01:00
Lovell Fuller
7df7a505ee Merge pull request #68 from chanon/master
Pass additional info (width, height) to output callback
2014-07-20 22:22:34 +01:00
Chanon
d40bdcc6ac Sends width and height as another parameter to the callback. Fixes issue #67 2014-07-20 23:52:28 +07:00
Lovell Fuller
1cce56b024 Use tile cache for interlace output
Doubles performance of Adam7 interlaced PNG output
2014-07-09 23:47:04 +01:00
Lovell Fuller
2126f9afc1 Ensure xcode/clang uses cflags
Silence pedantic warnings triggered by V8 in 0.11
2014-07-09 21:14:40 +01:00
Lovell Fuller
41420eedcf Add details of Heroku buildpack, thanks @alex88 #57 2014-07-06 13:20:41 +01:00
Lovell Fuller
1b6ab19b6d Remove fftw dependency when compiling libvips from src #57 2014-07-03 21:12:18 +01:00
Lovell Fuller
fbe5c18762 Expose depth of task queue 2014-06-25 23:21:39 +01:00
Lovell Fuller
8acb0ed5d0 Enable libvips C++ interpolators in Travis CI 2014-06-18 21:08:09 +01:00
Lovell Fuller
430e04d894 Update (and reduce) dependencies ahead of v0.5.1 2014-06-17 20:24:22 +01:00
Lovell Fuller
012edb4379 Support existing path to pkg-config lib for Heroku #57 2014-06-17 19:55:41 +01:00
Lovell Fuller
11ead360a9 Add Promises benchmark 2014-06-13 19:20:39 +01:00
Lovell Fuller
84a059d7e3 Rotate should throw Error, not String.
Minor JSLint and whitespace fixes.
2014-06-13 18:57:54 +01:00
Lovell Fuller
b1b070ae5c Revert "use native Promise if available"
This reverts commit 261a90c8a2.
2014-06-13 18:46:36 +01:00
Lovell Fuller
4eb910fec9 Add support for bicubic and Nohalo interpolation #41 2014-06-08 20:46:03 +01:00
Lovell Fuller
f0a9d82bf7 Always convert to sRGB colourspace to prevent libwebp segfault #58 2014-06-07 22:49:15 +01:00
Lovell Fuller
6d20a1ca81 Simplify gyp to support non-standard brew installs #56 2014-06-05 22:19:02 +01:00
Lovell Fuller
eca2787213 Add @jonathanong to contributors - thank you!
Bump versions
2014-06-05 22:18:47 +01:00
Lovell Fuller
8d146accf3 Correct docs - callback is optional for png 2014-06-05 22:00:39 +01:00
Lovell Fuller
b635d015cd Merge pull request #55 from jonathanong/native-promises
use native Promise if available
2014-06-04 20:48:17 +01:00
Jonathan Ong
261a90c8a2 use native Promise if available 2014-06-04 09:03:37 -07:00
Lovell Fuller
4ae22b3425 Merge pull request #51 from jonathanong/deprecate-write
show deprecation warning for .write()
2014-06-04 10:43:59 +01:00
Lovell Fuller
c9aa9c7723 Merge pull request #49 from jonathanong/strings-are-not-errors
fix error types, support promises on convenience methods
2014-06-04 10:35:06 +01:00
Jonathan Ong
5e0b5969da show deprecation warning for .write() 2014-06-03 20:53:37 -07:00
Jonathan Ong
ae6d5e69b1 fix error types, support promises on convenience methods
closes #48
2014-06-03 20:41:15 -07:00
Lovell Fuller
5ccc2bca97 Warn about liborc 0.4.19 buffer overflows #21 2014-06-02 22:18:00 +01:00
Lovell Fuller
46b701c85c Fail fast when input Buffer is empty #37 2014-06-02 21:11:25 +01:00
Lovell Fuller
7319533969 Add support for Promises/A+ #33 2014-06-01 11:27:30 +01:00
Lovell Fuller
9a05684302 Add withoutEnlargement option #36 2014-05-29 20:01:43 +01:00
Lovell Fuller
906311d403 Replace write() with toFile() to allow streams in the future #30 2014-05-29 19:54:43 +01:00
Lovell Fuller
4de9a2435f Correct typo 2014-05-29 16:05:53 +01:00
Lovell Fuller
a94dd2b354 Add support for image rotation including EXIF auto-orient 2014-05-29 00:48:58 +01:00
Lovell Fuller
bc3311cbad Version bump 2014-05-26 09:23:41 +01:00
Lovell Fuller
7b03eb89d7 Merge pull request #39 from pierreinglebert/update-nan
update to nan 1.1.0
2014-05-26 09:15:35 +01:00
Pierre Inglebert
15a519ebd9 update to nan 1.1.0 2014-05-26 09:51:53 +02:00
Lovell Fuller
3f8e9f6487 Merge pull request #38 from pierreinglebert/fix-max-cropping-29
fix max factor #29
2014-05-24 11:54:59 +01:00
Pierre Inglebert
39688371a8 fix max factor #29 2014-05-24 10:29:33 +02:00
Lovell Fuller
e32faac17a Prevent writing output file over input file - closes #28 2014-05-22 22:27:02 +01:00
Lovell Fuller
6c96bd0d37 Version bumps 2014-05-22 00:18:43 +01:00
Lovell Fuller
6b5f2028b7 Add support for 32 bit Linux #27 2014-05-21 23:46:10 +01:00
Lovell Fuller
276ba5228b Add usage example and further unit test for new max option
Simplify max vs crop logic
2014-05-19 21:52:47 +01:00
Lovell Fuller
ad7735a0a6 Merge pull request #18 from pierreinglebert/master
Add 'max' canvas option to specify the maximum width and/or height.
Approximately equivalent to GraphicsMagick's geometry option.
2014-05-19 21:03:04 +01:00
Pierre Inglebert
88edad3fae add max option #18 2014-05-19 20:18:26 +02:00
Lovell Fuller
308d1971d8 Recalculate residual float value for affine after shrink #26
Switch to static casts for double values

Add unit tests that previously would have failed
2014-05-18 11:39:05 +01:00
Lovell Fuller
6622045172 Merge pull request #25 from pierreinglebert/fix-progressive-typo
Fix progressive typo
2014-05-14 11:17:41 +01:00
Pierre Inglebert
f68ba8ea57 fix progressive typo 2014-05-14 08:56:12 +02:00
Lovell Fuller
2e427bb28a Remove liborc from CI config (to match README) 2014-05-13 16:47:57 +01:00
Lovell Fuller
efc7504961 Merge branch 'pierreinglebert-0.11-wip' 2014-05-13 16:45:32 +01:00
Lovell Fuller
8118613fa0 Merge branch '0.11-wip' of https://github.com/pierreinglebert/sharp into pierreinglebert-0.11-wip
Conflicts:
	package.json
2014-05-13 16:45:12 +01:00
Pierre Inglebert
eb6a221cee use master branch of image-magick-native until the 0.11 compatible is out 2014-05-13 08:03:24 +02:00
Pierre Inglebert
acdfe02502 make travis test on node 0.11 2014-05-12 21:30:44 +02:00
Lovell Fuller
2e106f8e2e Version bumps 2014-05-12 21:15:23 +02:00
Lovell Fuller
10496881f1 Add quality and compressionLevel options for output image. #24 2014-05-12 21:15:23 +02:00
Lovell Fuller
e275f6f5dd Add warning about liborc memory leaks. #21 2014-05-12 21:15:23 +02:00
Lovell Fuller
d635c297a2 Replace use of deprecated libvips conv method.
Ensure unref of mask to fix minor memory leak of ~150 bytes/image.
2014-05-12 21:15:23 +02:00
Pierre Inglebert
817c0a2a5a do not publish tests files 2014-05-12 21:15:23 +02:00
Pierre Inglebert
92fd34c627 remove unnecessary gitignore file 2014-05-12 21:15:23 +02:00
Lovell Fuller
43086cf134 Merge branch 'master' into quality-option 2014-05-11 18:26:16 +01:00
Lovell Fuller
e607bac31c Version bumps 2014-05-10 20:23:08 +01:00
Lovell Fuller
f8338e7c4f Add quality and compressionLevel options for output image. #24 2014-05-10 19:45:12 +01:00
Lovell Fuller
8322b442e0 Add warning about liborc memory leaks. #21 2014-05-10 14:39:34 +01:00
Lovell Fuller
afc51df4d8 Replace use of deprecated libvips conv method.
Ensure unref of mask to fix minor memory leak of ~150 bytes/image.
2014-05-03 22:14:21 +01:00
Lovell Fuller
e3a70c1075 Merge pull request #22 from pierreinglebert/add-npmignore
Add npmignore to minimise published package size
2014-04-24 14:09:42 +01:00
Pierre Inglebert
e3ee2b2976 do not publish tests files 2014-04-24 11:28:02 +02:00
Pierre Inglebert
cb285a6fb3 remove unnecessary gitignore file 2014-04-24 11:27:34 +02:00
Pierre Inglebert
aed3ca63b3 use NanNull, NanNew & NanSymbol for 0.11.11+ compat 2014-04-23 10:03:11 +02:00
Lovell Fuller
cbcf5e0dcc Update instructions for libvips installation on Ubuntu 2014-04-18 13:09:45 +01:00
Lovell Fuller
59f5c2d31b Update async dependency 2014-04-18 09:11:01 +01:00
Lovell Fuller
d1b47ef419 Add details of performance test task - closes #12 2014-04-16 21:55:22 +01:00
Lovell Fuller
0954ca6adf Update performance test results 2014-04-15 22:57:27 +01:00
Lovell Fuller
c9d7f43bd9 Add gm buffer-input perf tests 2014-04-15 22:57:07 +01:00
Lovell Fuller
481741315d Correct README markdown format 2014-04-14 22:09:08 +01:00
Lovell Fuller
cae1dbdb89 Add imagemagick-native comparison to and disable vips caching in perf tests. 2014-04-14 22:03:48 +01:00
Lovell Fuller
200d5a9312 Major version increase ahead of incoming API additions. Add link to Travis CI status. 2014-04-08 23:11:01 +01:00
Lovell Fuller
e9ca25cb45 Correct markdown typo 2014-04-08 22:40:34 +01:00
Lovell Fuller
33f24d41e7 Add Travis CI config
Uses a PPA with back-ported libwebp to work around https://bugs.launchpad.net/ubuntu/+source/libwebp/+bug/1108731
2014-04-08 22:35:42 +01:00
Lovell Fuller
45d5f12a63 Add details of Ubuntu 12.04 oddities. Closes #16. 2014-04-08 22:35:20 +01:00
Lovell Fuller
8785ca4331 Merge pull request #15 from pierreinglebert/feature-movefixtures
Move test fixtures to their own directory.
2014-04-02 22:54:05 +01:00
Pierre Inglebert
1ecdf97bdb Move fixtures to a proper directory
Add test for gif (libmagick)
2014-04-02 23:42:05 +02:00
Lovell Fuller
3703ee41aa Merge pull request #14 from pierreinglebert/feature-magickloader
Add support for many other input files via the *magick libraries
2014-04-02 22:26:38 +01:00
Lovell Fuller
c8f023d8ba Support Node.js v0.10+ only. Add @pierreinglebert credit. 2014-04-02 22:19:53 +01:00
Lovell Fuller
fe773733cd Merge pull request #11 from pierreinglebert/master
Add support for Node.js v0.11+ via nan. Remove epeg test dependency as it fails to compile on v0.11+.
2014-04-02 22:08:21 +01:00
Pierre Inglebert
19bec9346e add libmagick load support 2014-04-02 22:17:41 +02:00
Pierre Inglebert
9bd335079f check input files with vips foreign lib 2014-04-02 21:04:01 +02:00
Pierre Inglebert
b6dc179551 remove all references to epeg 2014-04-02 18:51:19 +02:00
Pierre Inglebert
e96fd8b9de 0.11 compat using nan 2014-04-02 00:10:42 +02:00
Lovell Fuller
6a2816e917 Add help for compiling against the gettext dependency of libvips on Mac OS #9 2014-03-13 22:16:47 +00:00
Lovell Fuller
06d88de5a2 Version bump 2014-03-12 21:11:25 +00:00
Lovell Fuller
a292e1fe8e Add path to homebrew-installed pkgconfig for Mac OS 10.8 (10.9 is symlinked to 10.8) #9 2014-03-12 20:45:35 +00:00
Lovell Fuller
08bb35e7af Add test dependency installation details 2014-03-11 22:44:05 +00:00
Lovell Fuller
0e89a5bbf2 Add summary of JPEG and PNG optimisation features 2014-03-10 22:32:14 +00:00
Lovell Fuller
2a0d79a78b Minor markdown layout fix 2014-03-09 21:54:31 +00:00
Lovell Fuller
764b57022b Minor markdown layout fix 2014-03-09 21:53:09 +00:00
Lovell Fuller
5f61331d1a Breaking change to API to become more expressive (see #8). Add support for upscaling. 2014-03-09 21:44:03 +00:00
Lovell Fuller
d0e6a4c0f3 Support identity transform when both width and height are not specified 2014-03-04 22:44:53 +00:00
Lovell Fuller
f99e42d447 Add support for auto-scaling of width and height 2014-03-03 23:24:09 +00:00
Lovell Fuller
9b4387be97 Improve installation instructions for libvips 2014-02-28 23:11:05 +00:00
Lovell Fuller
9c3631ecb7 Add comparative performance tests using random dimensions 2014-02-28 22:39:02 +00:00
Lovell Fuller
31ca68fb14 Improve documentation of cache method 2014-02-28 22:37:59 +00:00
Lovell Fuller
d5d85a8697 Expose vips internal cache settings and status 2014-02-25 23:31:33 +00:00
Lovell Fuller
ae9a8b0f57 Remove unnecessary temporary copy of input buffer. 2014-02-23 10:52:40 +00:00
Lovell Fuller
0899252a72 Add support for WebP and TIFF image formats. Closes #7. 2014-02-22 21:48:00 +00:00
Lovell Fuller
16551bc058 Take a temporary copy of buffers provided as input. Soak testing suggests this prevents the problems seen in #6. 2014-02-12 22:36:13 +00:00
Lovell Fuller
e9d196f696 Fix (unrelated) memory leak discovered whilst investigating #6 2014-02-06 22:20:48 +00:00
Lovell Fuller
2f97d04dfa Keep shrink-on-load logic DRY. Ensure canvas option (crop vs embed) is always as requested. 2014-02-03 23:20:38 +00:00
Lovell Fuller
e4ca8f44ec Version bump 2014-02-01 23:00:37 +00:00
Lovell Fuller
6b3dc1e350 Ensure crop occurs on y-axis 2014-02-01 22:58:30 +00:00
Lovell Fuller
7ffcdb79e0 Add glib-2.0 path for Mac OS X 2014-01-30 18:28:24 +00:00
Lovell Fuller
10ce7c6693 Add parallel performance test to demonstrate effect of waiting for a worker thread 2014-01-26 20:16:42 +00:00
Lovell Fuller
ccd6012152 Version bump 2014-01-21 22:49:40 +00:00
Lovell Fuller
377662fffc Ensure gm perf tests actually resize. Prevent coredump when embeding pre-shrunk image within same dimensions. 2014-01-21 22:48:33 +00:00
Lovell Fuller
d509458ba1 Major rewrite and therefore API change. Despite the name, image sharpening is now optional, plus there are other new options. Can now handle buffers in and out. Doubled performance in certain cases. Closes #3. Closes #4. 2014-01-19 21:38:37 +00:00
Lovell Fuller
be8f35d830 Use shrink-on-load for JPEG images, partially implementing #4. Switch to new vip_ methods from legacy im_ methods. Large performance gains all round. 2014-01-16 22:51:44 +00:00
Lovell Fuller
7e8af63129 Improve thread/buffer shutdown using new vips_thread_shutdown method - closes #5 2014-01-15 21:38:23 +00:00
Lovell Fuller
f7b8ce1287 Add support for writing image data output to a buffer, boosting ops/sec by ~7%. Partially implements #3. 2014-01-11 21:50:17 +00:00
Lovell Fuller
dde9e94850 From here on in, this module will be using the bleeding edge version of libvips. Why? It just keeps getting faster. 2013-11-23 22:08:51 +00:00
Lovell Fuller
21f12e74ba Add support for PNG format. Close #2. 2013-10-26 16:32:18 +01:00
Lovell Fuller
daeebcc7dc Add epeg module to the perf tests 2013-10-19 13:39:57 +01:00
Lovell Fuller
5546a4f881 Added GraphicsMagick perf stats 2013-10-04 22:01:16 +01:00
Lovell Fuller
4aee725530 README layout clean-up 2013-10-02 23:40:32 +01:00
Lovell Fuller
f3da2284b1 Now requires livbips 7.28 or later 2013-10-02 23:37:29 +01:00
Lovell Fuller
f3cd263cb7 Replaced use of deprecated libvips im_* methods with new, shiny non-deprecated vips_* versions 2013-10-02 21:50:03 +01:00
Lovell Fuller
8443dd5122 Version bump 2013-10-02 19:35:49 +01:00
51 changed files with 4730 additions and 336 deletions

20
.gitignore vendored
View File

@@ -1,17 +1,9 @@
lib-cov
*.seed
*.log
*.csv
*.dat
*.out
*.pid
*.gz
pids
logs
results
build
node_modules
tests/output.jpg
coverage
test/bench/node_modules
test/fixtures/output.*
test/leak/libvips.supp
npm-debug.log
# Mac OS X
.DS_Store

3
.jshintignore Normal file
View File

@@ -0,0 +1,3 @@
node_modules
test/bench/node_modules
coverage

8
.jshintrc Normal file
View File

@@ -0,0 +1,8 @@
{
"strict": true,
"node": true,
"globals": {
"describe": true,
"it": true
}
}

8
.npmignore Normal file
View File

@@ -0,0 +1,8 @@
build
node_modules
coverage
.jshintignore
.jshintrc
.gitignore
test
.travis.yml

View File

@@ -1,8 +1,8 @@
language: node_js
node_js:
- "0.11"
- "0.10"
before_install:
- sudo apt-get update -qq
- sudo apt-get install -qq libvips-dev imagemagick
- sudo ln -s /usr/lib/pkgconfig/vips-7.26.pc /usr/lib/pkgconfig/vips.pc
language: node_js
node_js:
- "0.10"
- "0.11"
before_install:
- curl -s https://raw.githubusercontent.com/lovell/sharp/master/preinstall.sh | sudo bash -
after_success:
- cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js

614
README.md
View File

@@ -1,53 +1,89 @@
# sharp
# sharp
_adj_
* [Installation](https://github.com/lovell/sharp#installation)
* [Usage examples](https://github.com/lovell/sharp#usage-examples)
* [API](https://github.com/lovell/sharp#api)
* [Testing](https://github.com/lovell/sharp#testing)
* [Performance](https://github.com/lovell/sharp#performance)
* [Thanks](https://github.com/lovell/sharp#thanks)
* [Licence](https://github.com/lovell/sharp#licence)
1. clearly defined; distinct: a sharp photographic image.
2. quick, brisk, or spirited.
3. shrewd or astute: a sharp bargainer.
4. (Informal.) very stylish: a sharp dresser; a sharp jacket.
The typical use case for this high speed Node.js module is to convert large images of many formats to smaller, web-friendly JPEG, PNG and WebP images of varying dimensions.
The typical use case for this high speed Node.js module is to convert a large JPEG image to smaller JPEG images of varying dimensions.
The performance of JPEG resizing is typically 8x faster than ImageMagick and GraphicsMagick, based mainly on the number of CPU cores available.
It is somewhat opinionated in that it only deals with JPEG images, always obeys the requested dimensions by either cropping or embedding and insists on a mild sharpen of the resulting image.
Memory usage is kept to a minimum, no child processes are spawned, everything remains non-blocking thanks to _libuv_ and Promises/A+ are supported.
Under the hood you'll find the blazingly fast [libvips](https://github.com/jcupitt/libvips) image processing library, originally created in 1989 at Birkbeck College and currently maintained by the University of Southampton.
This module supports reading and writing JPEG, PNG and WebP images to and from Streams, Buffer objects and the filesystem. It also supports reading images of many other types from the filesystem via libmagick++ or libgraphicsmagick++ if present.
Performance is 4x-8x faster than the imagemagick equivalent, based mainly on the number of CPU cores available.
When generating JPEG output all metadata is removed and Huffman tables optimised without having to use separate command line tools like [jpegoptim](https://github.com/tjko/jpegoptim) and [jpegtran](http://jpegclub.org/jpegtran/).
## Prerequisites
Anyone who has used the Node.js bindings for [GraphicsMagick](https://github.com/aheckmann/gm) will find the API similarly fluent.
Requires Node.js v0.8+, node-gyp and libvips-dev to build.
This module is powered by the blazingly fast [libvips](https://github.com/jcupitt/libvips) image processing library, originally created in 1989 at Birkbeck College and currently maintained by [John Cupitt](https://github.com/jcupitt).
sudo npm install -g node-gyp
sudo apt-get install libvips-dev
Requires vips-7.xx.pc (installed with libvips-dev) to be symlinked as /usr/lib/pkgconfig/vips.pc
Ubuntu 12.04 LTS:
sudo ln -s /usr/lib/pkgconfig/vips-7.26.pc /usr/lib/pkgconfig/vips.pc
Ubuntu 13.04 (64-bit):
sudo ln -s /usr/lib/x86_64-linux-gnu/pkgconfig/vips-7.28.pc /usr/lib/pkgconfig/vips.pc
## Install
## Installation
npm install sharp
## Usage
### Prerequisites
var sharp = require("sharp");
* Node.js v0.10+
* [libvips](https://github.com/jcupitt/libvips) v7.38.5+
### crop(inputPath, outputPath, width, height, callback)
To install the latest version of libvips on the following Operating Systems:
Scale and crop JPEG `inputPath` to `width` x `height` and write JPEG to `outputPath` calling `callback` when complete.
* Mac OS
* Homebrew
* MacPorts
* Debian Linux
* Debian 7, 8
* Ubuntu 12.04, 14.04, 14.10
* Mint 13, 17
* Red Hat Linux
* RHEL/Centos/Scientific 6, 7
* Fedora 21, 22
Example:
run the following as a user with `sudo` access:
curl -s https://raw.githubusercontent.com/lovell/sharp/master/preinstall.sh | sudo bash -
or run the following as `root`:
curl -s https://raw.githubusercontent.com/lovell/sharp/master/preinstall.sh | bash -
The [preinstall.sh](https://github.com/lovell/sharp/blob/master/preinstall.sh) script requires `curl` and `pkg-config`.
### Mac OS tips
Manual install via homebrew:
brew install homebrew/science/vips --with-webp --with-graphicsmagick
A missing or incorrectly configured _Xcode Command Line Tools_ installation [can lead](https://github.com/lovell/sharp/issues/80) to a `library not found for -ljpeg` error. If so, please try:
xcode-select --install
The _gettext_ dependency of _libvips_ [can lead](https://github.com/lovell/sharp/issues/9) to a `library not found for -lintl` error. If so, please try:
brew link gettext --force
### Install libvips on Heroku
[Alessandro Tagliapietra](https://github.com/alex88) maintains an [Heroku buildpack for libvips](https://github.com/alex88/heroku-buildpack-vips) and its dependencies.
### Using with gulp.js
[Mohammad Prabowo](https://github.com/rizalp) maintains a [gulp.js plugin](https://github.com/rizalp/gulp-sharp).
## Usage examples
```javascript
sharp.crop("input.jpg", "output.jpg", 300, 200, function(err) {
var sharp = require('sharp');
```
```javascript
sharp('input.jpg').resize(300, 200).toFile('output.jpg', function(err) {
if (err) {
throw err;
}
@@ -56,62 +92,500 @@ sharp.crop("input.jpg", "output.jpg", 300, 200, function(err) {
});
```
### embedWhite(inputPath, outputPath, width, height, callback)
Scale and embed JPEG `inputPath` to `width` x `height` using a white canvas and write JPEG to `outputPath` calling `callback` when complete.
```javascript
var transformer = sharp().resize(300, 200).crop(sharp.gravity.north);
readableStream.pipe(transformer).pipe(writableStream);
// Read image data from readableStream, resize and write image data to writableStream
```
```javascript
sharp.embedWhite("input.jpg", "output.jpg", 200, 300, function(err) {
if (err) {
throw err;
}
// output.jpg is a 200 pixels wide and 300 pixels high image
// containing a scaled version of input.jpg embedded on a white canvas
var image = sharp(inputJpg);
image.metadata(function(err, metadata) {
image.resize(metadata.width / 2).webp().toBuffer(function(err, outputBuffer, info) {
// outputBuffer contains a WebP image half the width and height of the original JPEG
});
});
```
### embedBlack(inputPath, outputPath, width, height, callback)
Scale and embed JPEG `inputPath` to `width` x `height` using a black canvas and write JPEG to `outputPath` calling `callback` when complete.
```javascript
sharp.embedBlack("input.jpg", "output.jpg", 200, 300, function(err) {
if (err) {
throw err;
}
// output.jpg is a 200 pixels wide and 300 pixels high image
// containing a scaled version of input.jpg embedded on a black canvas
});
var pipeline = sharp()
.rotate()
.resize(null, 200)
.progressive()
.toBuffer(function(err, outputBuffer, info) {
if (err) {
throw err;
}
// outputBuffer contains 200px high progressive JPEG image data,
// auto-rotated using EXIF Orientation tag
// info.width and info.height contain the dimensions of the resized image
});
readableStream.pipe(pipeline);
```
## Testing [![Build Status](https://travis-ci.org/lovell/sharp.png?branch=master)](https://travis-ci.org/lovell/sharp)
```javascript
sharp('input.png')
.rotate(180)
.resize(300)
.flatten()
.background('#ff6600')
.sharpen()
.withMetadata()
.quality(90)
.webp()
.toBuffer()
.then(function(outputBuffer) {
// outputBuffer contains upside down, 300px wide, alpha channel flattened
// onto orange background, sharpened, with metadata, 90% quality WebP image
// data
});
```
npm install --dev sharp
npm test
```javascript
http.createServer(function(request, response) {
response.writeHead(200, {'Content-Type': 'image/webp'});
sharp('input.jpg').rotate().resize(200).webp().pipe(response);
}).listen(8000);
// Create HTTP server that always returns auto-rotated 'input.jpg',
// resized to 200 pixels wide, in WebP format
```
```javascript
sharp(input)
.extract(top, left, width, height)
.toFile(output);
// Extract a region of the input image, saving in the same format.
```
```javascript
sharp(input)
.extract(topOffsetPre, leftOffsetPre, widthPre, heightPre)
.resize(width, height)
.extract(topOffsetPost, leftOffsetPost, widthPost, heightPost)
.toFile(output);
// Extract a region, resize, then extract from the resized image
```
```javascript
sharp(inputBuffer)
.resize(200, 300)
.interpolateWith(sharp.interpolator.nohalo)
.background('white')
.embed()
.toFile('output.tiff')
.then(function() {
// output.tiff is a 200 pixels wide and 300 pixels high image
// containing a bicubic scaled version, embedded on a white canvas,
// of the image data in inputBuffer
});
```
```javascript
sharp('input.gif')
.resize(200, 300)
.background({r: 0, g: 0, b: 0, a: 0})
.embed()
.webp()
.toBuffer(function(err, outputBuffer) {
if (err) {
throw err;
}
// outputBuffer contains WebP image data of a 200 pixels wide and 300 pixels high
// containing a scaled version, embedded on a transparent canvas, of input.gif
});
```
```javascript
sharp(inputBuffer)
.resize(200, 200)
.max()
.jpeg()
.toBuffer().then(function(outputBuffer) {
// outputBuffer contains JPEG image data no wider than 200 pixels and no higher
// than 200 pixels regardless of the inputBuffer image dimensions
});
```
## API
### Input methods
#### sharp([input])
Constructor to which further methods are chained. `input`, if present, can be one of:
* Buffer containing JPEG, PNG or WebP image data, or
* String containing the filename of an image, with most major formats supported.
The object returned implements the [stream.Duplex](http://nodejs.org/api/stream.html#stream_class_stream_duplex) class.
JPEG, PNG or WebP format image data can be streamed into the object when `input` is not provided.
JPEG, PNG or WebP format image data can be streamed out from this object.
#### metadata([callback])
Fast access to image metadata without decoding any compressed image data.
`callback`, if present, gets the arguments `(err, metadata)` where `metadata` has the attributes:
* `format`: Name of decoder to be used to decompress image data e.g. `jpeg`, `png`, `webp` (for file-based input additionally `tiff` and `magick`)
* `width`: Number of pixels wide
* `height`: Number of pixels high
* `space`: Name of colour space interpretation e.g. `srgb`, `rgb`, `scrgb`, `cmyk`, `lab`, `xyz`, `b-w` [...](https://github.com/jcupitt/libvips/blob/master/libvips/iofuncs/enumtypes.c#L502)
* `channels`: Number of bands e.g. `3` for sRGB, `4` for CMYK
* `hasAlpha`: Boolean indicating the presence of an alpha transparency channel
* `orientation`: Number value of the EXIF Orientation header, if present
A Promises/A+ promise is returned when `callback` is not provided.
#### sequentialRead()
An advanced setting that switches the libvips access method to `VIPS_ACCESS_SEQUENTIAL`. This will reduce memory usage and can improve performance on some systems.
### Image transformation options
#### resize(width, [height])
Scale output to `width` x `height`. By default, the resized image is cropped to the exact size specified.
`width` is the Number of pixels wide the resultant image should be. Use `null` or `undefined` to auto-scale the width to match the height.
`height` is the Number of pixels high the resultant image should be. Use `null` or `undefined` to auto-scale the height to match the width.
#### extract(top, left, width, height)
Extract a region of the image. Can be used with or without a `resize` operation.
`top` and `left` are the offset, in pixels, from the top-left corner.
`width` and `height` are the dimensions of the extracted image.
Use `extract` before `resize` for pre-resize extraction. Use `extract` after `resize` for post-resize extraction. Use `extract` before and after for both.
#### crop([gravity])
Crop the resized image to the exact size specified, the default behaviour.
`gravity`, if present, is an attribute of the `sharp.gravity` Object e.g. `sharp.gravity.north`.
Possible values are `north`, `east`, `south`, `west`, `center` and `centre`. The default gravity is `center`/`centre`.
#### max()
Preserving aspect ratio, resize the image to the maximum `width` or `height` specified.
Both `width` and `height` must be provided via `resize` otherwise the behaviour will default to `crop`.
#### background(rgba)
Set the background for the `embed` and `flatten` operations.
`rgba` is parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha.
The alpha value is a float between `0` (transparent) and `1` (opaque).
The default background is `{r: 0, g: 0, b: 0, a: 1}`, black without transparency.
#### embed()
Preserving aspect ratio, resize the image to the maximum `width` or `height` specified then embed on a background of the exact `width` and `height` specified.
If the background contains an alpha value then WebP and PNG format output images will contain an alpha channel, even when the input image does not.
#### flatten()
Merge alpha transparency channel, if any, with `background`.
#### rotate([angle])
Rotate the output image by either an explicit angle or auto-orient based on the EXIF `Orientation` tag.
`angle`, if present, is a Number with a value of `0`, `90`, `180` or `270`.
Use this method without `angle` to determine the angle from EXIF data. Mirroring is supported and may infer the use of a `flip` operation.
#### flip()
Flip the image about the vertical Y axis. This always occurs after rotation, if any.
#### flop()
Flop the image about the horizontal X axis. This always occurs after rotation, if any.
#### withoutEnlargement()
Do not enlarge the output image if the input image width *or* height are already less than the required dimensions.
This is equivalent to GraphicsMagick's `>` geometry option: "change the dimensions of the image only if its width or height exceeds the geometry specification".
#### sharpen()
Perform a mild sharpen of the output image. This typically reduces performance by 10%.
#### interpolateWith(interpolator)
Use the given interpolator for image resizing, where `interpolator` is an attribute of the `sharp.interpolator` Object e.g. `sharp.interpolator.bicubic`.
Possible interpolators, in order of performance, are:
* `nearest`: Use [nearest neighbour interpolation](http://en.wikipedia.org/wiki/Nearest-neighbor_interpolation), suitable for image enlargement only.
* `bilinear`: Use [bilinear interpolation](http://en.wikipedia.org/wiki/Bilinear_interpolation), the default and fastest image reduction interpolation.
* `bicubic`: Use [bicubic interpolation](http://en.wikipedia.org/wiki/Bicubic_interpolation), which typically reduces performance by 5%.
* `vertexSplitQuadraticBasisSpline`: Use [VSQBS interpolation](https://github.com/jcupitt/libvips/blob/master/libvips/resample/vsqbs.cpp#L48), which prevents "staircasing" and typically reduces performance by 5%.
* `locallyBoundedBicubic`: Use [LBB interpolation](https://github.com/jcupitt/libvips/blob/master/libvips/resample/lbb.cpp#L100), which prevents some "[acutance](http://en.wikipedia.org/wiki/Acutance)" and typically reduces performance by a factor of 2.
* `nohalo`: Use [Nohalo interpolation](http://eprints.soton.ac.uk/268086/), which prevents acutance and typically reduces performance by a factor of 3.
#### gamma([gamma])
Apply a gamma correction by reducing the encoding (darken) pre-resize at a factor of `1/gamma` then increasing the encoding (brighten) post-resize at a factor of `gamma`.
`gamma`, if present, is a Number betweem 1 and 3. The default value is `2.2`, a suitable approximation for sRGB images.
This can improve the perceived brightness of a resized image in non-linear colour spaces.
JPEG input images will not take advantage of the shrink-on-load performance optimisation when applying a gamma correction.
#### grayscale() / greyscale()
Convert to 8-bit greyscale; 256 shades of grey.
This is a linear operation. If the input image is in a non-linear colour space such as sRGB, use `gamma()` with `greyscale()` for the best results.
The output image will still be web-friendly sRGB and contain three (identical) channels.
### Output options
#### jpeg()
Use JPEG format for the output image.
#### png()
Use PNG format for the output image.
#### webp()
Use WebP format for the output image.
#### quality(quality)
The output quality to use for lossy JPEG, WebP and TIFF output formats. The default quality is `80`.
`quality` is a Number between 1 and 100.
#### progressive()
Use progressive (interlace) scan for JPEG and PNG output. This typically reduces compression performance by 30% but results in an image that can be rendered sooner when decompressed.
#### withMetadata()
Include all metadata (ICC, EXIF, XMP) from the input image in the output image. The default behaviour is to strip all metadata.
#### compressionLevel(compressionLevel)
An advanced setting for the _zlib_ compression level of the lossless PNG output format. The default level is `6`.
`compressionLevel` is a Number between 0 and 9.
### Output methods
#### toFile(filename, [callback])
`filename` is a String containing the filename to write the image data to. The format is inferred from the extension, with JPEG, PNG, WebP and TIFF supported.
`callback`, if present, is called with two arguments `(err, info)` where:
* `err` contains an error message, if any.
* `info` contains the output image `format`, `width` and `height`.
A Promises/A+ promise is returned when `callback` is not provided.
#### toBuffer([callback])
Write image data to a Buffer, the format of which will match the input image by default. JPEG, PNG and WebP are supported.
`callback`, if present, gets three arguments `(err, buffer, info)` where:
* `err` is an error message, if any.
* `buffer` is the output image data.
* `info` contains the output image `format`, `width` and `height`.
A Promises/A+ promise is returned when `callback` is not provided.
### Utility methods
#### sharp.cache([memory], [items])
If `memory` or `items` are provided, set the limits of _libvips'_ operation cache.
* `memory` is the maximum memory in MB to use for this cache, with a default value of 100
* `items` is the maximum number of operations to cache, with a default value of 500
This method always returns cache statistics, useful for determining how much working memory is required for a particular task.
```javascript
var stats = sharp.cache(); // { current: 75, high: 99, memory: 100, items: 500 }
sharp.cache(200); // { current: 75, high: 99, memory: 200, items: 500 }
sharp.cache(50, 200); // { current: 49, high: 99, memory: 50, items: 200}
```
#### sharp.concurrency([threads])
`threads`, if provided, is the Number of threads _libvips'_ should create for image processing. The default value is the number of CPU cores. A value of `0` will reset to this default.
This method always returns the current concurrency.
```javascript
var threads = sharp.concurrency(); // 4
sharp.concurrency(2); // 2
sharp.concurrency(0); // 4
```
#### sharp.counters()
Provides access to internal task counters.
* `queue` is the number of tasks this module has queued waiting for _libuv_ to provide a worker thread from its pool.
* `process` is the number of resize tasks currently being processed.
```javascript
var counters = sharp.counters(); // { queue: 2, process: 4 }
```
## Testing
### Functional tests
#### Coverage
[![Test Coverage](https://coveralls.io/repos/lovell/sharp/badge.png?branch=master)](https://coveralls.io/r/lovell/sharp?branch=master)
#### Ubuntu 12.04
[![Ubuntu 12.04 Build Status](https://travis-ci.org/lovell/sharp.png?branch=master)](https://travis-ci.org/lovell/sharp)
#### Centos 6.5
[![Centos 6.5 Build Status](https://snap-ci.com/lovell/sharp/branch/master/build_image)](https://snap-ci.com/lovell/sharp/branch/master)
#### It worked on my machine
```
npm test
```
### Memory leak tests
```
cd sharp/test/leak
./leak.sh
```
Requires _valgrind_:
```
brew install valgrind
```
```
sudo apt-get install -qq valgrind
```
### Benchmark tests
```
cd sharp/test/bench
npm install
npm test
```
Requires both _ImageMagick_ and _GraphicsMagick_:
```
brew install imagemagick
brew install graphicsmagick
```
```
sudo apt-get install -qq imagemagick graphicsmagick libmagick++-dev
```
```
sudo yum install ImageMagick
sudo yum install -y http://download.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm
sudo yum install -y --enablerepo=epel GraphicsMagick
```
## Performance
### AMD Athlon 4x core 3.3GHz 512KB L2
### Test environment
* imagemagick x 5.55 ops/sec ±0.68% (31 runs sampled)
* sharp x 24.49 ops/sec ±6.85% (64 runs sampled)
* AWS EC2 [c3.xlarge](http://aws.amazon.com/ec2/instance-types/#Compute_Optimized)
* Ubuntu 14.04
* libvips 7.40.8
* liborc 0.4.22
### AWS t1.micro
### The contenders
* imagemagick x 1.36 ops/sec ±0.96% (11 runs sampled)
* sharp x 12.42 ops/sec ±5.84% (64 runs sampled)
* [imagemagick-native](https://github.com/mash/node-imagemagick-native) v1.2.2 - Supports Buffers only and blocks main V8 thread whilst processing.
* [imagemagick](https://github.com/yourdeveloper/node-imagemagick) v0.1.3 - Supports filesystem only and "has been unmaintained for a long time".
* [gm](https://github.com/aheckmann/gm) v1.16.0 - Fully featured wrapper around GraphicsMagick.
* sharp v0.6.2 - Caching within libvips disabled to ensure a fair comparison.
### AWS m1.medium
### The task
* imagemagick x 1.38 ops/sec ±0.45% (11 runs sampled)
* sharp x 12.66 ops/sec ±5.54% (65 runs sampled)
Decompress a 2725x2225 JPEG image, resize and crop to 720x480, then compress to JPEG.
### AWS c1.medium
### Results
* imagemagick x 2.10 ops/sec ±0.67% (15 runs sampled)
* sharp x 18.97 ops/sec ±10.54% (52 runs sampled)
| Module | Input | Output | Ops/sec | Speed-up |
| :-------------------- | :----- | :----- | ------: | -------: |
| imagemagick-native | buffer | buffer | 1.58 | 1 |
| imagemagick | file | file | 6.23 | 3.9 |
| gm | buffer | file | 5.32 | 3.4 |
| gm | buffer | buffer | 5.32 | 3.4 |
| gm | file | file | 5.36 | 3.4 |
| gm | file | buffer | 5.36 | 3.4 |
| sharp | buffer | file | 22.05 | 14.0 |
| sharp | buffer | buffer | 22.14 | 14.0 |
| sharp | file | file | 21.79 | 13.8 |
| sharp | file | buffer | 21.90 | 13.9 |
| sharp | stream | stream | 20.87 | 13.2 |
| sharp +promise | file | buffer | 21.89 | 13.9 |
| sharp +sharpen | file | buffer | 19.69 | 12.5 |
| sharp +progressive | file | buffer | 16.93 | 10.7 |
| sharp +sequentialRead | file | buffer | 21.60 | 13.7 |
### AWS m3.xlarge
You can expect greater performance with caching enabled (default) and using 8+ core machines.
* imagemagick x 4.46 ops/sec ±0.33% (26 runs sampled)
* sharp x 28.89 ops/sec ±7.75% (74 runs sampled)
## Thanks
This module would never have been possible without the help and code contributions of the following people:
* [John Cupitt](https://github.com/jcupitt)
* [Pierre Inglebert](https://github.com/pierreinglebert)
* [Jonathan Ong](https://github.com/jonathanong)
* [Chanon Sajjamanochai](https://github.com/chanon)
* [Juliano Julio](https://github.com/julianojulio)
* [Daniel Gasienica](https://github.com/gasi)
* [Julian Walker](https://github.com/julianwa)
* [Amit Pitaru](https://github.com/apitaru)
* [Brandon Aaron](https://github.com/brandonaaron)
* [Andreas Lind](https://github.com/papandreou)
Thank you!
## Licence
Copyright 2013, 2014 Lovell Fuller and contributors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0.html)
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -1,17 +1,38 @@
{
'targets': [{
'target_name': 'sharp',
'sources': ['src/sharp.cc'],
'sources': [
'src/common.cc',
'src/utilities.cc',
'src/metadata.cc',
'src/resize.cc',
'src/sharp.cc'
],
'variables': {
'PKG_CONFIG_PATH': '<!(which brew >/dev/null 2>&1 && eval $(brew --env) && echo $PKG_CONFIG_LIBDIR || true):$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig:/usr/lib/pkgconfig'
},
'libraries': [
'<!@(PKG_CONFIG_PATH="/usr/local/lib/pkgconfig" pkg-config --libs vips)',
'<!@(PKG_CONFIG_PATH="/usr/lib/pkgconfig" pkg-config --libs vips)'
'<!(PKG_CONFIG_PATH="<(PKG_CONFIG_PATH)" pkg-config --libs vips)'
],
'include_dirs': [
'/usr/include/glib-2.0',
'/usr/lib/glib-2.0/include',
'/usr/lib/x86_64-linux-gnu/glib-2.0/include'
'<!(PKG_CONFIG_PATH="<(PKG_CONFIG_PATH)" pkg-config --cflags vips glib-2.0)',
'<!(node -e "require(\'nan\')")'
],
'cflags': ['-fexceptions'],
'cflags_cc': ['-fexceptions']
'cflags_cc': [
'-std=c++0x',
'-fexceptions',
'-Wall',
'-O3'
],
'xcode_settings': {
'OTHER_CPLUSPLUSFLAGS': [
'-std=c++11',
'-stdlib=libc++',
'-fexceptions',
'-Wall',
'-O3'
],
'MACOSX_DEPLOYMENT_TARGET': '10.7'
}
}]
}

518
index.js
View File

@@ -1,13 +1,505 @@
var sharp = require("./build/Release/sharp");
module.exports.crop = function(input, output, width, height, callback) {
sharp.resize(input, output, width, height, "c", callback)
}
module.exports.embedWhite = function(input, output, width, height, callback) {
sharp.resize(input, output, width, height, "w", callback)
}
module.exports.embedBlack = function(input, output, width, height, callback) {
sharp.resize(input, output, width, height, "b", callback)
}
'use strict';
var util = require('util');
var stream = require('stream');
var color = require('color');
var BluebirdPromise = require('bluebird');
var sharp = require('./build/Release/sharp');
var Sharp = function(input) {
if (!(this instanceof Sharp)) {
return new Sharp(input);
}
stream.Duplex.call(this);
this.options = {
// input options
streamIn: false,
sequentialRead: false,
// resize options
topOffsetPre: -1,
leftOffsetPre: -1,
widthPre: -1,
heightPre: -1,
topOffsetPost: -1,
leftOffsetPost: -1,
widthPost: -1,
heightPost: -1,
width: -1,
height: -1,
canvas: 'c',
gravity: 0,
angle: 0,
flip: false,
flop: false,
withoutEnlargement: false,
interpolator: 'bilinear',
// operations
background: [0, 0, 0, 255],
flatten: false,
sharpen: false,
gamma: 0,
greyscale: false,
// output options
output: '__input',
progressive: false,
quality: 80,
compressionLevel: 6,
streamOut: false,
withMetadata: false
};
if (typeof input === 'string') {
// input=file
this.options.fileIn = input;
} else if (typeof input === 'object' && input instanceof Buffer) {
// input=buffer
if (
(input.length > 1) &&
(input[0] === 0xff && input[1] === 0xd8) || // JPEG
(input[0] === 0x89 && input[1] === 0x50) || // PNG
(input[0] === 0x52 && input[1] === 0x49) // WebP
) {
this.options.bufferIn = input;
} else {
throw new Error('Buffer contains an unsupported image format. JPEG, PNG and WebP are currently supported.');
}
} else {
// input=stream
this.options.streamIn = true;
}
return this;
};
module.exports = Sharp;
util.inherits(Sharp, stream.Duplex);
/*
Handle incoming chunk on Writable Stream
*/
Sharp.prototype._write = function(chunk, encoding, callback) {
/*jslint unused: false */
if (this.options.streamIn) {
if (typeof chunk === 'object' && chunk instanceof Buffer) {
if (typeof this.options.bufferIn === 'undefined') {
// Create new Buffer
this.options.bufferIn = new Buffer(chunk.length);
chunk.copy(this.options.bufferIn);
} else {
// Append to existing Buffer
this.options.bufferIn = Buffer.concat(
[this.options.bufferIn, chunk],
this.options.bufferIn.length + chunk.length
);
}
callback();
} else {
callback(new Error('Non-Buffer data on Writable Stream'));
}
} else {
callback(new Error('Unexpected data on Writable Stream'));
}
};
// Crop this part of the resized image (Center/Centre, North, East, South, West)
module.exports.gravity = {'center': 0, 'centre': 0, 'north': 1, 'east': 2, 'south': 3, 'west': 4};
Sharp.prototype.crop = function(gravity) {
this.options.canvas = 'c';
if (typeof gravity === 'number' && !Number.isNaN(gravity) && gravity >= 0 && gravity <= 4) {
this.options.gravity = gravity;
} else {
throw new Error('Unsupported crop gravity ' + gravity);
}
return this;
};
Sharp.prototype.extract = function(topOffset, leftOffset, width, height) {
/*jslint unused: false */
var suffix = this.options.width === -1 && this.options.height === -1 ? 'Pre' : 'Post';
var values = arguments;
['topOffset', 'leftOffset', 'width', 'height'].forEach(function(name, index) {
this.options[name + suffix] = values[index];
}.bind(this));
return this;
};
/*
Deprecated embed* methods, to be removed in v0.8.0
*/
Sharp.prototype.embedWhite = util.deprecate(function() {
return this.background('white').embed();
}, "embedWhite() is deprecated, use background('white').embed() instead");
Sharp.prototype.embedBlack = util.deprecate(function() {
return this.background('black').embed();
}, "embedBlack() is deprecated, use background('black').embed() instead");
/*
Set the background colour for embed and flatten operations.
Delegates to the 'Color' module, which can throw an Error
but is liberal in what it accepts, clamping values to sensible min/max.
*/
Sharp.prototype.background = function(rgba) {
var colour = color(rgba);
this.options.background = colour.rgbArray();
this.options.background.push(colour.alpha() * 255);
return this;
};
Sharp.prototype.embed = function() {
this.options.canvas = 'e';
return this;
};
Sharp.prototype.max = function() {
this.options.canvas = 'm';
return this;
};
Sharp.prototype.flatten = function(flatten) {
this.options.flatten = (typeof flatten === 'boolean') ? flatten : true;
return this;
};
/*
Rotate output image by 0, 90, 180 or 270 degrees
Auto-rotation based on the EXIF Orientation tag is represented by an angle of -1
*/
Sharp.prototype.rotate = function(angle) {
if (typeof angle === 'undefined') {
this.options.angle = -1;
} else if (!Number.isNaN(angle) && [0, 90, 180, 270].indexOf(angle) !== -1) {
this.options.angle = angle;
} else {
throw new Error('Unsupported angle (0, 90, 180, 270) ' + angle);
}
return this;
};
/*
Flip the image vertically, about the Y axis
*/
Sharp.prototype.flip = function(flip) {
this.options.flip = (typeof flip === 'boolean') ? flip : true;
return this;
};
/*
Flop the image horizontally, about the X axis
*/
Sharp.prototype.flop = function(flop) {
this.options.flop = (typeof flop === 'boolean') ? flop : true;
return this;
};
/*
Do not enlarge the output if the input width *or* height are already less than the required dimensions
This is equivalent to GraphicsMagick's ">" geometry option:
"change the dimensions of the image only if its width or height exceeds the geometry specification"
*/
Sharp.prototype.withoutEnlargement = function(withoutEnlargement) {
this.options.withoutEnlargement = (typeof withoutEnlargement === 'boolean') ? withoutEnlargement : true;
return this;
};
Sharp.prototype.sharpen = function(sharpen) {
this.options.sharpen = (typeof sharpen === 'boolean') ? sharpen : true;
return this;
};
/*
Set the interpolator to use for the affine transformation
*/
module.exports.interpolator = {
nearest: 'nearest',
bilinear: 'bilinear',
bicubic: 'bicubic',
nohalo: 'nohalo',
locallyBoundedBicubic: 'lbb',
vertexSplitQuadraticBasisSpline: 'vsqbs'
};
Sharp.prototype.interpolateWith = function(interpolator) {
this.options.interpolator = interpolator;
return this;
};
/*
Darken image pre-resize (1/gamma) and brighten post-resize (gamma).
Improves brightness of resized image in non-linear colour spaces.
*/
Sharp.prototype.gamma = function(gamma) {
if (typeof gamma === 'undefined') {
// Default gamma correction of 2.2 (sRGB)
this.options.gamma = 2.2;
} else if (!Number.isNaN(gamma) && gamma >= 1 && gamma <= 3) {
this.options.gamma = gamma;
} else {
throw new Error('Invalid gamma correction (1.0 to 3.0) ' + gamma);
}
return this;
};
/*
Convert to greyscale
*/
Sharp.prototype.greyscale = function(greyscale) {
this.options.greyscale = (typeof greyscale === 'boolean') ? greyscale : true;
return this;
};
Sharp.prototype.grayscale = Sharp.prototype.greyscale;
Sharp.prototype.progressive = function(progressive) {
this.options.progressive = (typeof progressive === 'boolean') ? progressive : true;
return this;
};
Sharp.prototype.sequentialRead = function(sequentialRead) {
this.options.sequentialRead = (typeof sequentialRead === 'boolean') ? sequentialRead : true;
return this;
};
Sharp.prototype.quality = function(quality) {
if (!Number.isNaN(quality) && quality >= 1 && quality <= 100) {
this.options.quality = quality;
} else {
throw new Error('Invalid quality (1 to 100) ' + quality);
}
return this;
};
Sharp.prototype.compressionLevel = function(compressionLevel) {
if (!Number.isNaN(compressionLevel) && compressionLevel >= 0 && compressionLevel <= 9) {
this.options.compressionLevel = compressionLevel;
} else {
throw new Error('Invalid compressionLevel (0 to 9) ' + compressionLevel);
}
return this;
};
Sharp.prototype.withMetadata = function(withMetadata) {
this.options.withMetadata = (typeof withMetadata === 'boolean') ? withMetadata : true;
return this;
};
Sharp.prototype.resize = function(width, height) {
if (!width) {
this.options.width = -1;
} else {
if (typeof width === 'number' && !Number.isNaN(width)) {
this.options.width = width;
} else {
throw new Error('Invalid width ' + width);
}
}
if (!height) {
this.options.height = -1;
} else {
if (typeof height === 'number' && !Number.isNaN(height)) {
this.options.height = height;
} else {
throw new Error('Invalid height ' + height);
}
}
return this;
};
/*
Write output image data to a file
*/
Sharp.prototype.toFile = function(output, callback) {
if (!output || output.length === 0) {
var errOutputInvalid = new Error('Invalid output');
if (typeof callback === 'function') {
callback(errOutputInvalid);
} else {
return BluebirdPromise.reject(errOutputInvalid);
}
} else {
if (this.options.fileIn === output) {
var errOutputIsInput = new Error('Cannot use same file for input and output');
if (typeof callback === 'function') {
callback(errOutputIsInput);
} else {
return BluebirdPromise.reject(errOutputIsInput);
}
} else {
this.options.output = output;
return this._sharp(callback);
}
}
return this;
};
Sharp.prototype.toBuffer = function(callback) {
return this._sharp(callback);
};
Sharp.prototype.jpeg = function() {
this.options.output = '__jpeg';
return this;
};
Sharp.prototype.png = function() {
this.options.output = '__png';
return this;
};
Sharp.prototype.webp = function() {
this.options.output = '__webp';
return this;
};
/*
Used by a Writable Stream to notify that it is ready for data
*/
Sharp.prototype._read = function() {
if (!this.options.streamOut) {
this.options.streamOut = true;
this._sharp();
}
};
/*
Invoke the C++ image processing pipeline
Supports callback, stream and promise variants
*/
Sharp.prototype._sharp = function(callback) {
var that = this;
if (typeof callback === 'function') {
// output=file/buffer
if (this.options.streamIn) {
// output=file/buffer, input=stream
this.on('finish', function() {
sharp.resize(that.options, callback);
});
} else {
// output=file/buffer, input=file/buffer
sharp.resize(this.options, callback);
}
return this;
} else if (this.options.streamOut) {
// output=stream
if (this.options.streamIn) {
// output=stream, input=stream
this.on('finish', function() {
sharp.resize(that.options, function(err, data) {
if (err) {
that.emit('error', new Error(err));
} else {
that.push(data);
}
that.push(null);
});
});
} else {
// output=stream, input=file/buffer
sharp.resize(this.options, function(err, data) {
if (err) {
that.emit('error', new Error(err));
} else {
that.push(data);
}
that.push(null);
});
}
return this;
} else {
// output=promise
if (this.options.streamIn) {
// output=promise, input=stream
return new BluebirdPromise(function(resolve, reject) {
that.on('finish', function() {
sharp.resize(that.options, function(err, data) {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
});
} else {
// output=promise, input=file/buffer
return new BluebirdPromise(function(resolve, reject) {
sharp.resize(that.options, function(err, data) {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
}
};
/*
Reads the image header and returns metadata
Supports callback, stream and promise variants
*/
Sharp.prototype.metadata = function(callback) {
var that = this;
if (typeof callback === 'function') {
if (this.options.streamIn) {
this.on('finish', function() {
sharp.metadata(that.options, callback);
});
} else {
sharp.metadata(this.options, callback);
}
return this;
} else {
if (this.options.streamIn) {
return new BluebirdPromise(function(resolve, reject) {
that.on('finish', function() {
sharp.metadata(that.options, function(err, data) {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
});
} else {
return new BluebirdPromise(function(resolve, reject) {
sharp.metadata(that.options, function(err, data) {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
}
};
/*
Get and set cache memory and item limits
*/
module.exports.cache = function(memory, items) {
if (typeof memory !== 'number' || Number.isNaN(memory)) {
memory = null;
}
if (typeof items !== 'number' || Number.isNaN(items)) {
items = null;
}
return sharp.cache(memory, items);
};
/*
Get and set size of thread pool
*/
module.exports.concurrency = function(concurrency) {
if (typeof concurrency !== 'number' || Number.isNaN(concurrency)) {
concurrency = null;
}
return sharp.concurrency(concurrency);
};
/*
Get internal counters
*/
module.exports.counters = function() {
return sharp.counters();
};

View File

@@ -1,32 +1,58 @@
{
"name": "sharp",
"version": "0.0.3",
"version": "0.7.1",
"author": "Lovell Fuller <npm@lovell.info>",
"contributors": [
"Pierre Inglebert <pierre.inglebert@gmail.com>",
"Jonathan Ong <jonathanrichardong@gmail.com>",
"Chanon Sajjamanochai <chanon.s@gmail.com>",
"Juliano Julio <julianojulio@gmail.com>",
"Daniel Gasienica <daniel@gasienica.ch>",
"Julian Walker <julian@fiftythree.com>",
"Amit Pitaru <pitaru.amit@gmail.com>",
"Brandon Aaron <hello.brandon@aaron.sh>",
"Andreas Lind <andreas@one.com>"
],
"description": "High performance Node.js module to resize JPEG, PNG and WebP images using the libvips library",
"scripts": {
"test": "node ./node_modules/istanbul/lib/cli.js cover ./node_modules/mocha/bin/_mocha -- --slow=5000 --timeout=10000 ./test/unit/*.js"
},
"main": "index.js",
"description": "High performance Node.js module to resize JPEG images using the libvips image processing library",
"repository": {
"type": "git",
"url": "git://github.com/lovell/sharp"
},
"devDependencies": {
"imagemagick": "*",
"benchmark": "*"
},
"scripts": {
"test": "node tests/perf.js"
},
"engines": {
"node": ">=0.8"
},
"keywords": [
"jpeg",
"png",
"webp",
"tiff",
"gif",
"resize",
"thumbnail",
"sharpen",
"crop",
"extract",
"embed",
"libvips",
"fast"
"vips",
"fast",
"buffer",
"stream"
],
"author": "Lovell Fuller",
"license": "Apache 2.0"
}
"dependencies": {
"bluebird": "^2.3.9",
"color": "^0.7.1",
"nan": "^1.3.0"
},
"devDependencies": {
"mocha": "^2.0.1",
"mocha-jshint": "^0.0.9",
"istanbul": "^0.3.2",
"coveralls": "^2.11.2"
},
"license": "Apache 2.0",
"engines": {
"node": ">=0.10"
}
}

145
preinstall.sh Executable file
View File

@@ -0,0 +1,145 @@
#!/bin/sh
# Ensures libvips is installed and attempts to install it if not
# Currently supports:
# * Mac OS
# * Debian Linux
# * Debian 7, 8
# * Ubuntu 12.04, 14.04, 14.10
# * Mint 13, 17
# * Red Hat Linux
# * RHEL/Centos/Scientific 6, 7
# * Fedora 21, 22
vips_version_minimum=7.38.5
vips_version_latest_major=7.40
vips_version_latest_minor=11
install_libvips_from_source() {
echo "Compiling libvips $vips_version_latest_major.$vips_version_latest_minor from source"
curl -O http://www.vips.ecs.soton.ac.uk/supported/$vips_version_latest_major/vips-$vips_version_latest_major.$vips_version_latest_minor.tar.gz
tar zvxf vips-$vips_version_latest_major.$vips_version_latest_minor.tar.gz
cd vips-$vips_version_latest_major.$vips_version_latest_minor
./configure --enable-debug=no --enable-docs=no --enable-cxx=yes --without-python --without-orc --without-fftw $1
make
make install
cd ..
rm -rf vips-$vips_version_latest_major.$vips_version_latest_minor
rm vips-$vips_version_latest_major.$vips_version_latest_minor.tar.gz
ldconfig
echo "Installed libvips $vips_version_latest_major.$vips_version_latest_minor"
}
sorry() {
echo "Sorry, I don't yet know how to install libvips on $1"
exit 1
}
# Is libvips already installed, and is it at least the minimum required version?
if ! type pkg-config >/dev/null; then
sorry "a system without pkg-config"
fi
pkg_config_path_homebrew=`which brew >/dev/null 2>&1 && eval $(brew --env) && echo $PKG_CONFIG_LIBDIR || true`
pkg_config_path="$pkg_config_path_homebrew:$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig:/usr/lib/pkgconfig"
PKG_CONFIG_PATH=$pkg_config_path pkg-config --exists vips
if [ $? -eq 0 ]; then
vips_version_found=$(PKG_CONFIG_PATH=$pkg_config_path pkg-config --modversion vips)
pkg-config --atleast-version=$vips_version_minimum vips
if [ $? -eq 0 ]; then
# Found suitable version of libvips
echo "Found libvips $vips_version_found"
exit 0
fi
echo "Found libvips $vips_version_found but require $vips_version_minimum"
else
echo "Could not find libvips using a PKG_CONFIG_PATH of '$pkg_config_path'"
fi
# Verify root/sudo access
if [ "$(id -u)" -ne "0" ]; then
echo "Sorry, I need root/sudo access to continue"
exit 1
fi
# OS-specific installations of libvips follows
case $(uname -s) in
*[Dd]arwin*)
# Mac OS
echo "Detected Mac OS"
if type "brew" > /dev/null; then
echo "Installing libvips via homebrew"
brew install homebrew/science/vips --with-webp --with-graphicsmagick
elif type "port" > /dev/null; then
echo "Installing libvips via MacPorts"
port install vips
else
sorry "Mac OS without homebrew or MacPorts"
fi
;;
*)
if [ -f /etc/debian_version ]; then
# Debian Linux
DISTRO=$(lsb_release -c -s)
echo "Detected Debian Linux '$DISTRO'"
case "$DISTRO" in
jessie|trusty|utopic|qiana)
# Debian 8, Ubuntu 14, Mint 17
echo "Installing libvips via apt-get"
apt-get install -y libvips-dev
;;
precise|wheezy|maya)
# Debian 7, Ubuntu 12.04, Mint 13
echo "Installing libvips dependencies via apt-get"
add-apt-repository -y ppa:lyrasis/precise-backports
apt-get update
apt-get install -y automake build-essential gobject-introspection gtk-doc-tools libglib2.0-dev libjpeg-turbo8-dev libpng12-dev libwebp-dev libtiff4-dev libexif-dev libxml2-dev swig libmagickwand-dev curl
install_libvips_from_source
;;
*)
# Unsupported Debian-based OS
sorry "Debian-based $DISTRO"
;;
esac
elif [ -f /etc/redhat-release ]; then
# Red Hat Linux
RELEASE=$(cat /etc/redhat-release)
echo "Detected Red Hat Linux '$RELEASE'"
case $RELEASE in
"Red Hat Enterprise Linux release 7."*|"CentOS Linux release 7."*|"Scientific Linux release 7."*)
# RHEL/CentOS 7
echo "Installing libvips dependencies via yum"
yum groupinstall -y "Development Tools"
yum install -y gtk-doc libxml2-devel libjpeg-turbo-devel libpng-devel libtiff-devel libexif-devel ImageMagick-devel gobject-introspection-devel libwebp-devel curl
install_libvips_from_source "--prefix=/usr"
;;
"Red Hat Enterprise Linux release 6."*|"CentOS release 6."*|"Scientific Linux release 6."*)
# RHEL/CentOS 6
echo "Installing libvips dependencies via yum"
yum groupinstall -y "Development Tools"
yum install -y gtk-doc libxml2-devel libjpeg-turbo-devel libpng-devel libtiff-devel libexif-devel ImageMagick-devel curl
yum install -y http://li.nux.ro/download/nux/dextop/el6/x86_64/nux-dextop-release-0-2.el6.nux.noarch.rpm
yum install -y --enablerepo=nux-dextop gobject-introspection-devel
yum install -y http://rpms.famillecollet.com/enterprise/remi-release-6.rpm
yum install -y --enablerepo=remi libwebp-devel
install_libvips_from_source "--prefix=/usr"
;;
"Fedora release 21 "*|"Fedora release 22 "*)
# Fedora 21, 22
echo "Installing libvips via yum"
yum install vips-devel
;;
*)
# Unsupported RHEL-based OS
sorry "$RELEASE"
;;
esac
else
# Unsupported OS
sorry "$(uname -a)"
fi
;;
esac

99
src/common.cc Executable file
View File

@@ -0,0 +1,99 @@
#include <string>
#include <string.h>
#include <vips/vips.h>
#include "common.h"
// How many tasks are in the queue?
volatile int counter_queue = 0;
// How many tasks are being processed?
volatile int counter_process = 0;
// Filename extension checkers
static bool ends_with(std::string const &str, std::string const &end) {
return str.length() >= end.length() && 0 == str.compare(str.length() - end.length(), end.length(), end);
}
bool is_jpeg(std::string const &str) {
return ends_with(str, ".jpg") || ends_with(str, ".jpeg") || ends_with(str, ".JPG") || ends_with(str, ".JPEG");
}
bool is_png(std::string const &str) {
return ends_with(str, ".png") || ends_with(str, ".PNG");
}
bool is_webp(std::string const &str) {
return ends_with(str, ".webp") || ends_with(str, ".WEBP");
}
bool is_tiff(std::string const &str) {
return ends_with(str, ".tif") || ends_with(str, ".tiff") || ends_with(str, ".TIF") || ends_with(str, ".TIFF");
}
unsigned char const MARKER_JPEG[] = {0xff, 0xd8};
unsigned char const MARKER_PNG[] = {0x89, 0x50};
unsigned char const MARKER_WEBP[] = {0x52, 0x49};
/*
Initialise a VipsImage from a buffer. Supports JPEG, PNG and WebP.
Returns the ImageType detected, if any.
*/
ImageType
sharp_init_image_from_buffer(VipsImage **image, void *buffer, size_t const length, VipsAccess const access) {
ImageType imageType = UNKNOWN;
if (memcmp(MARKER_JPEG, buffer, 2) == 0) {
if (!vips_jpegload_buffer(buffer, length, image, "access", access, NULL)) {
imageType = JPEG;
}
} else if(memcmp(MARKER_PNG, buffer, 2) == 0) {
if (!vips_pngload_buffer(buffer, length, image, "access", access, NULL)) {
imageType = PNG;
}
} else if(memcmp(MARKER_WEBP, buffer, 2) == 0) {
if (!vips_webpload_buffer(buffer, length, image, "access", access, NULL)) {
imageType = WEBP;
}
}
return imageType;
}
/*
Initialise a VipsImage from a file.
Returns the ImageType detected, if any.
*/
ImageType
sharp_init_image_from_file(VipsImage **image, char const *file, VipsAccess const access) {
ImageType imageType = UNKNOWN;
if (vips_foreign_is_a("jpegload", file)) {
if (!vips_jpegload(file, image, "access", access, NULL)) {
imageType = JPEG;
}
} else if (vips_foreign_is_a("pngload", file)) {
if (!vips_pngload(file, image, "access", access, NULL)) {
imageType = PNG;
}
} else if (vips_foreign_is_a("webpload", file)) {
if (!vips_webpload(file, image, "access", access, NULL)) {
imageType = WEBP;
}
} else if (vips_foreign_is_a("tiffload", file)) {
if (!vips_tiffload(file, image, "access", access, NULL)) {
imageType = TIFF;
}
} else if(vips_foreign_is_a("magickload", file)) {
if (!vips_magickload(file, image, "access", access, NULL)) {
imageType = MAGICK;
}
}
return imageType;
}
/*
Does this image have an alpha channel?
Uses colour space interpretation with number of channels to guess this.
*/
bool
sharp_image_has_alpha(VipsImage *image) {
return (
(image->Bands == 2 && image->Type == VIPS_INTERPRETATION_B_W) ||
(image->Bands == 4 && image->Type != VIPS_INTERPRETATION_CMYK) ||
(image->Bands == 5 && image->Type == VIPS_INTERPRETATION_CMYK)
);
}

46
src/common.h Executable file
View File

@@ -0,0 +1,46 @@
#ifndef SHARP_COMMON_H
#define SHARP_COMMON_H
typedef enum {
UNKNOWN,
JPEG,
PNG,
WEBP,
TIFF,
MAGICK
} ImageType;
// Filename extension checkers
bool is_jpeg(std::string const &str);
bool is_png(std::string const &str);
bool is_webp(std::string const &str);
bool is_tiff(std::string const &str);
// How many tasks are in the queue?
extern volatile int counter_queue;
// How many tasks are being processed?
extern volatile int counter_process;
/*
Initialise a VipsImage from a buffer. Supports JPEG, PNG and WebP.
Returns the ImageType detected, if any.
*/
ImageType
sharp_init_image_from_buffer(VipsImage **image, void *buffer, size_t const length, VipsAccess const access);
/*
Initialise a VipsImage from a file.
Returns the ImageType detected, if any.
*/
ImageType
sharp_init_image_from_file(VipsImage **image, char const *file, VipsAccess const access);
/*
Does this image have an alpha channel?
Uses colour space interpretation with number of channels to guess this.
*/
bool
sharp_image_has_alpha(VipsImage *image);
#endif

144
src/metadata.cc Executable file
View File

@@ -0,0 +1,144 @@
#include <node.h>
#include <vips/vips.h>
#include "nan.h"
#include "common.h"
#include "metadata.h"
using namespace v8;
struct MetadataBaton {
// Input
std::string fileIn;
void* bufferIn;
size_t bufferInLength;
// Output
std::string format;
int width;
int height;
std::string space;
int channels;
bool hasAlpha;
int orientation;
std::string err;
MetadataBaton():
bufferInLength(0),
orientation(0) {}
};
class MetadataWorker : public NanAsyncWorker {
public:
MetadataWorker(NanCallback *callback, MetadataBaton *baton) : NanAsyncWorker(callback), baton(baton) {}
~MetadataWorker() {}
void Execute() {
// Decrement queued task counter
g_atomic_int_dec_and_test(&counter_queue);
ImageType imageType = UNKNOWN;
VipsImage *image;
if (baton->bufferInLength > 1) {
// From buffer
imageType = sharp_init_image_from_buffer(&image, baton->bufferIn, baton->bufferInLength, VIPS_ACCESS_RANDOM);
if (imageType == UNKNOWN) {
(baton->err).append("Input buffer contains unsupported image format");
}
} else {
// From file
imageType = sharp_init_image_from_file(&image, baton->fileIn.c_str(), VIPS_ACCESS_RANDOM);
if (imageType == UNKNOWN) {
(baton->err).append("File is of an unsupported image format");
}
}
if (imageType != UNKNOWN) {
// Image type
switch (imageType) {
case JPEG: baton->format = "jpeg"; break;
case PNG: baton->format = "png"; break;
case WEBP: baton->format = "webp"; break;
case TIFF: baton->format = "tiff"; break;
case MAGICK: baton->format = "magick"; break;
case UNKNOWN: default: baton->format = "";
}
// VipsImage attributes
baton->width = image->Xsize;
baton->height = image->Ysize;
baton->space = vips_enum_nick(VIPS_TYPE_INTERPRETATION, image->Type);
baton->channels = image->Bands;
baton->hasAlpha = sharp_image_has_alpha(image);
// EXIF Orientation
const char *exif;
if (!vips_image_get_string(image, "exif-ifd0-Orientation", &exif)) {
baton->orientation = atoi(&exif[0]);
}
}
// Clean up
if (imageType != UNKNOWN) {
g_object_unref(image);
}
vips_error_clear();
vips_thread_shutdown();
}
void HandleOKCallback () {
NanScope();
Handle<Value> argv[2] = { NanNull(), NanNull() };
if (!baton->err.empty()) {
// Error
argv[0] = Exception::Error(NanNew<String>(baton->err.data(), baton->err.size()));
} else {
// Metadata Object
Local<Object> info = NanNew<Object>();
info->Set(NanNew<String>("format"), NanNew<String>(baton->format));
info->Set(NanNew<String>("width"), NanNew<Number>(baton->width));
info->Set(NanNew<String>("height"), NanNew<Number>(baton->height));
info->Set(NanNew<String>("space"), NanNew<String>(baton->space));
info->Set(NanNew<String>("channels"), NanNew<Number>(baton->channels));
info->Set(NanNew<String>("hasAlpha"), NanNew<Boolean>(baton->hasAlpha));
if (baton->orientation > 0) {
info->Set(NanNew<String>("orientation"), NanNew<Number>(baton->orientation));
}
argv[1] = info;
}
delete baton;
// Return to JavaScript
callback->Call(2, argv);
}
private:
MetadataBaton* baton;
};
/*
metadata(options, callback)
*/
NAN_METHOD(metadata) {
NanScope();
// V8 objects are converted to non-V8 types held in the baton struct
MetadataBaton *baton = new MetadataBaton;
Local<Object> options = args[0]->ToObject();
// Input filename
baton->fileIn = *String::Utf8Value(options->Get(NanNew<String>("fileIn"))->ToString());
// Input Buffer object
if (options->Get(NanNew<String>("bufferIn"))->IsObject()) {
Local<Object> buffer = options->Get(NanNew<String>("bufferIn"))->ToObject();
baton->bufferInLength = node::Buffer::Length(buffer);
baton->bufferIn = node::Buffer::Data(buffer);
}
// Join queue for worker thread
NanCallback *callback = new NanCallback(args[1].As<v8::Function>());
NanAsyncQueueWorker(new MetadataWorker(callback, baton));
// Increment queued task counter
g_atomic_int_inc(&counter_queue);
NanReturnUndefined();
}

8
src/metadata.h Executable file
View File

@@ -0,0 +1,8 @@
#ifndef SHARP_METADATA_H
#define SHARP_METADATA_H
#include "nan.h"
NAN_METHOD(metadata);
#endif

790
src/resize.cc Executable file
View File

@@ -0,0 +1,790 @@
#include <tuple>
#include <math.h>
#include <node.h>
#include <node_buffer.h>
#include <vips/vips.h>
#include "nan.h"
#include "common.h"
#include "resize.h"
using namespace v8;
typedef enum {
CROP,
MAX,
EMBED
} Canvas;
typedef enum {
ANGLE_0,
ANGLE_90,
ANGLE_180,
ANGLE_270,
ANGLE_LAST
} Angle;
struct ResizeBaton {
std::string fileIn;
void* bufferIn;
size_t bufferInLength;
std::string output;
std::string outputFormat;
void* bufferOut;
size_t bufferOutLength;
int topOffsetPre;
int leftOffsetPre;
int widthPre;
int heightPre;
int topOffsetPost;
int leftOffsetPost;
int widthPost;
int heightPost;
int width;
int height;
Canvas canvas;
int gravity;
std::string interpolator;
double background[4];
bool flatten;
bool sharpen;
double gamma;
bool greyscale;
int angle;
bool flip;
bool flop;
bool progressive;
bool withoutEnlargement;
VipsAccess accessMethod;
int quality;
int compressionLevel;
std::string err;
bool withMetadata;
ResizeBaton():
bufferInLength(0),
outputFormat(""),
bufferOutLength(0),
topOffsetPre(-1),
topOffsetPost(-1),
canvas(CROP),
gravity(0),
background{0.0, 0.0, 0.0, 255.0},
flatten(false),
sharpen(false),
gamma(0.0),
greyscale(false),
flip(false),
flop(false),
progressive(false),
withoutEnlargement(false),
withMetadata(false) {}
};
class ResizeWorker : public NanAsyncWorker {
public:
ResizeWorker(NanCallback *callback, ResizeBaton *baton) : NanAsyncWorker(callback), baton(baton) {}
~ResizeWorker() {}
/*
libuv worker
*/
void Execute() {
// Decrement queued task counter
g_atomic_int_dec_and_test(&counter_queue);
// Increment processing task counter
g_atomic_int_inc(&counter_process);
// Hang image references from this hook object
VipsObject *hook = reinterpret_cast<VipsObject*>(vips_image_new());
// Input
ImageType inputImageType = UNKNOWN;
VipsImage *image = vips_image_new();
vips_object_local(hook, image);
if (baton->bufferInLength > 1) {
// From buffer
inputImageType = sharp_init_image_from_buffer(&image, baton->bufferIn, baton->bufferInLength, baton->accessMethod);
if (inputImageType == UNKNOWN) {
(baton->err).append("Input buffer contains unsupported image format");
}
} else {
// From file
inputImageType = sharp_init_image_from_file(&image, baton->fileIn.c_str(), baton->accessMethod);
if (inputImageType == UNKNOWN) {
(baton->err).append("File is of an unsupported image format");
}
}
if (inputImageType == UNKNOWN) {
return Error(baton, hook);
}
// Pre extraction
if (baton->topOffsetPre != -1) {
VipsImage *extractedPre = vips_image_new();
vips_object_local(hook, extractedPre);
if (vips_extract_area(image, &extractedPre, baton->leftOffsetPre, baton->topOffsetPre, baton->widthPre, baton->heightPre, NULL)) {
return Error(baton, hook);
}
g_object_unref(image);
image = extractedPre;
}
// Get input image width and height
int inputWidth = image->Xsize;
int inputHeight = image->Ysize;
// Calculate angle of rotation, to be carried out later
Angle rotation;
bool flip;
std::tie(rotation, flip) = CalculateRotationAndFlip(baton->angle, image);
if (rotation == ANGLE_90 || rotation == ANGLE_270) {
// Swap input output width and height when rotating by 90 or 270 degrees
int swap = inputWidth;
inputWidth = inputHeight;
inputHeight = swap;
}
if (flip && !baton->flip) {
// Add flip operation due to EXIF mirroring
baton->flip = TRUE;
}
// Scaling calculations
double factor;
if (baton->width > 0 && baton->height > 0) {
// Fixed width and height
double xfactor = static_cast<double>(inputWidth) / static_cast<double>(baton->width);
double yfactor = static_cast<double>(inputHeight) / static_cast<double>(baton->height);
factor = (baton->canvas == CROP) ? std::min(xfactor, yfactor) : std::max(xfactor, yfactor);
// if max is set, we need to compute the real size of the thumb image
if (baton->canvas == MAX) {
if (xfactor > yfactor) {
baton->height = round(static_cast<double>(inputHeight) / xfactor);
} else {
baton->width = round(static_cast<double>(inputWidth) / yfactor);
}
}
} else if (baton->width > 0) {
// Fixed width, auto height
factor = static_cast<double>(inputWidth) / static_cast<double>(baton->width);
baton->height = floor(static_cast<double>(inputHeight) / factor);
} else if (baton->height > 0) {
// Fixed height, auto width
factor = static_cast<double>(inputHeight) / static_cast<double>(baton->height);
baton->width = floor(static_cast<double>(inputWidth) / factor);
} else {
// Identity transform
factor = 1;
baton->width = inputWidth;
baton->height = inputHeight;
}
int shrink = floor(factor);
if (shrink < 1) {
shrink = 1;
}
double residual = static_cast<double>(shrink) / factor;
// Do not enlarge the output if the input width *or* height are already less than the required dimensions
if (baton->withoutEnlargement) {
if (inputWidth < baton->width || inputHeight < baton->height) {
factor = 1;
shrink = 1;
residual = 0;
baton->width = inputWidth;
baton->height = inputHeight;
}
}
// Try to use libjpeg shrink-on-load, but not when applying gamma correction or pre-resize extract
int shrink_on_load = 1;
if (inputImageType == JPEG && baton->gamma == 0 && baton->topOffsetPre == -1) {
if (shrink >= 8) {
factor = factor / 8;
shrink_on_load = 8;
} else if (shrink >= 4) {
factor = factor / 4;
shrink_on_load = 4;
} else if (shrink >= 2) {
factor = factor / 2;
shrink_on_load = 2;
}
}
if (shrink_on_load > 1) {
// Recalculate integral shrink and double residual
factor = std::max(factor, 1.0);
shrink = floor(factor);
residual = static_cast<double>(shrink) / factor;
// Reload input using shrink-on-load
g_object_unref(image);
if (baton->bufferInLength > 1) {
if (vips_jpegload_buffer(baton->bufferIn, baton->bufferInLength, &image, "shrink", shrink_on_load, NULL)) {
return Error(baton, hook);
}
} else {
if (vips_jpegload((baton->fileIn).c_str(), &image, "shrink", shrink_on_load, NULL)) {
return Error(baton, hook);
}
}
}
// Handle colour profile, if any, for non sRGB images
if (image->Type != VIPS_INTERPRETATION_sRGB && vips_image_get_typeof(image, VIPS_META_ICC_NAME)) {
// Import embedded profile
VipsImage *profile = vips_image_new();
vips_object_local(hook, profile);
if (vips_icc_import(image, &profile, NULL, "embedded", TRUE, "pcs", VIPS_PCS_XYZ, NULL)) {
return Error(baton, hook);
}
g_object_unref(image);
image = profile;
// Convert to sRGB colour space
VipsImage *colourspaced = vips_image_new();
vips_object_local(hook, colourspaced);
if (vips_colourspace(image, &colourspaced, VIPS_INTERPRETATION_sRGB, NULL)) {
return Error(baton, hook);
}
g_object_unref(image);
image = colourspaced;
}
// Flatten image to remove alpha channel
if (baton->flatten && sharp_image_has_alpha(image)) {
// Background colour
VipsArrayDouble *background = vips_array_double_newv(
3, // Ignore alpha channel as we're about to remove it
baton->background[0],
baton->background[1],
baton->background[2]
);
VipsImage *flattened = vips_image_new();
vips_object_local(hook, flattened);
if (vips_flatten(image, &flattened, "background", background, NULL)) {
vips_area_unref(reinterpret_cast<VipsArea*>(background));
return Error(baton, hook);
};
vips_area_unref(reinterpret_cast<VipsArea*>(background));
g_object_unref(image);
image = flattened;
}
// Gamma encoding (darken)
if (baton->gamma >= 1 && baton->gamma <= 3) {
VipsImage *gammaEncoded = vips_image_new();
vips_object_local(hook, gammaEncoded);
if (vips_gamma(image, &gammaEncoded, "exponent", 1.0 / baton->gamma, NULL)) {
return Error(baton, hook);
}
g_object_unref(image);
image = gammaEncoded;
}
// Convert to greyscale (linear, therefore after gamma encoding, if any)
if (baton->greyscale) {
VipsImage *greyscale = vips_image_new();
vips_object_local(hook, greyscale);
if (vips_colourspace(image, &greyscale, VIPS_INTERPRETATION_B_W, NULL)) {
return Error(baton, hook);
}
g_object_unref(image);
image = greyscale;
}
if (shrink > 1) {
VipsImage *shrunk = vips_image_new();
vips_object_local(hook, shrunk);
// Use vips_shrink with the integral reduction
if (vips_shrink(image, &shrunk, shrink, shrink, NULL)) {
return Error(baton, hook);
}
g_object_unref(image);
image = shrunk;
// Recalculate residual float based on dimensions of required vs shrunk images
double shrunkWidth = shrunk->Xsize;
double shrunkHeight = shrunk->Ysize;
if (rotation == ANGLE_90 || rotation == ANGLE_270) {
// Swap input output width and height when rotating by 90 or 270 degrees
int swap = shrunkWidth;
shrunkWidth = shrunkHeight;
shrunkHeight = swap;
}
double residualx = static_cast<double>(baton->width) / static_cast<double>(shrunkWidth);
double residualy = static_cast<double>(baton->height) / static_cast<double>(shrunkHeight);
if (baton->canvas == EMBED) {
residual = std::min(residualx, residualy);
} else {
residual = std::max(residualx, residualy);
}
}
// Use vips_affine with the remaining float part
if (residual != 0) {
VipsImage *affined = vips_image_new();
vips_object_local(hook, affined);
// Create interpolator - "bilinear" (default), "bicubic" or "nohalo"
VipsInterpolate *interpolator = vips_interpolate_new(baton->interpolator.c_str());
// Perform affine transformation
if (vips_affine(image, &affined, residual, 0, 0, residual, "interpolate", interpolator, NULL)) {
g_object_unref(interpolator);
return Error(baton, hook);
}
g_object_unref(interpolator);
g_object_unref(image);
image = affined;
}
// Rotate
if (rotation != ANGLE_0) {
VipsImage *rotated = vips_image_new();
vips_object_local(hook, rotated);
if (vips_rot(image, &rotated, static_cast<VipsAngle>(rotation), NULL)) {
return Error(baton, hook);
}
g_object_unref(image);
image = rotated;
}
// Flip (mirror about Y axis)
if (baton->flip) {
VipsImage *flipped = vips_image_new();
vips_object_local(hook, flipped);
if (vips_flip(image, &flipped, VIPS_DIRECTION_VERTICAL, NULL)) {
return Error(baton, hook);
}
g_object_unref(image);
image = flipped;
}
// Flop (mirror about X axis)
if (baton->flop) {
VipsImage *flopped = vips_image_new();
vips_object_local(hook, flopped);
if (vips_flip(image, &flopped, VIPS_DIRECTION_HORIZONTAL, NULL)) {
return Error(baton, hook);
}
g_object_unref(image);
image = flopped;
}
// Crop/embed
if (image->Xsize != baton->width || image->Ysize != baton->height) {
if (baton->canvas == EMBED) {
// Match background colour space, namely sRGB
if (image->Type != VIPS_INTERPRETATION_sRGB) {
// Convert to sRGB colour space
VipsImage *colourspaced = vips_image_new();
vips_object_local(hook, colourspaced);
if (vips_colourspace(image, &colourspaced, VIPS_INTERPRETATION_sRGB, NULL)) {
return Error(baton, hook);
}
g_object_unref(image);
image = colourspaced;
}
// Add non-transparent alpha channel, if required
if (baton->background[3] < 255.0 && !sharp_image_has_alpha(image)) {
// Create single-channel transparency
VipsImage *black = vips_image_new();
vips_object_local(hook, black);
if (vips_black(&black, image->Xsize, image->Ysize, "bands", 1, NULL)) {
return Error(baton, hook);
}
// Invert to become non-transparent
VipsImage *alpha = vips_image_new();
vips_object_local(hook, alpha);
if (vips_invert(black, &alpha, NULL)) {
return Error(baton, hook);
}
g_object_unref(black);
// Append alpha channel to existing image
VipsImage *joined = vips_image_new();
vips_object_local(hook, joined);
if (vips_bandjoin2(image, alpha, &joined, NULL)) {
return Error(baton, hook);
}
g_object_unref(alpha);
g_object_unref(image);
image = joined;
}
// Create background
VipsArrayDouble *background;
if (baton->background[3] < 255.0) {
background = vips_array_double_newv(
4, baton->background[0], baton->background[1], baton->background[2], baton->background[3]
);
} else {
background = vips_array_double_newv(
3, baton->background[0], baton->background[1], baton->background[2]
);
}
// Embed
int left = (baton->width - image->Xsize) / 2;
int top = (baton->height - image->Ysize) / 2;
VipsImage *embedded = vips_image_new();
vips_object_local(hook, embedded);
if (vips_embed(image, &embedded, left, top, baton->width, baton->height,
"extend", VIPS_EXTEND_BACKGROUND, "background", background, NULL
)) {
vips_area_unref(reinterpret_cast<VipsArea*>(background));
return Error(baton, hook);
}
vips_area_unref(reinterpret_cast<VipsArea*>(background));
g_object_unref(image);
image = embedded;
} else {
// Crop/max
int left;
int top;
std::tie(left, top) = CalculateCrop(image->Xsize, image->Ysize, baton->width, baton->height, baton->gravity);
int width = std::min(image->Xsize, baton->width);
int height = std::min(image->Ysize, baton->height);
VipsImage *extracted = vips_image_new();
vips_object_local(hook, extracted);
if (vips_extract_area(image, &extracted, left, top, width, height, NULL)) {
return Error(baton, hook);
}
g_object_unref(image);
image = extracted;
}
}
// Post extraction
if (baton->topOffsetPost != -1) {
VipsImage *extractedPost = vips_image_new();
vips_object_local(hook, extractedPost);
if (vips_extract_area(image, &extractedPost, baton->leftOffsetPost, baton->topOffsetPost, baton->widthPost, baton->heightPost, NULL)) {
return Error(baton, hook);
}
g_object_unref(image);
image = extractedPost;
}
// Mild sharpen
if (baton->sharpen) {
VipsImage *sharpened = vips_image_new();
vips_object_local(hook, sharpened);
VipsImage *sharpen = vips_image_new_matrixv(3, 3,
-1.0, -1.0, -1.0,
-1.0, 32.0, -1.0,
-1.0, -1.0, -1.0);
vips_image_set_double(sharpen, "scale", 24);
vips_object_local(hook, sharpen);
if (vips_conv(image, &sharpened, sharpen, NULL)) {
return Error(baton, hook);
}
g_object_unref(image);
image = sharpened;
}
// Gamma decoding (brighten)
if (baton->gamma >= 1 && baton->gamma <= 3) {
VipsImage *gammaDecoded = vips_image_new();
vips_object_local(hook, gammaDecoded);
if (vips_gamma(image, &gammaDecoded, "exponent", baton->gamma, NULL)) {
return Error(baton, hook);
}
g_object_unref(image);
image = gammaDecoded;
}
// Convert to sRGB colour space, if not already
if (image->Type != VIPS_INTERPRETATION_sRGB) {
VipsImage *colourspaced = vips_image_new();
vips_object_local(hook, colourspaced);
if (vips_colourspace(image, &colourspaced, VIPS_INTERPRETATION_sRGB, NULL)) {
return Error(baton, hook);
}
g_object_unref(image);
image = colourspaced;
}
// Generate image tile cache when interlace output is required
if (baton->progressive) {
VipsImage *cached = vips_image_new();
vips_object_local(hook, cached);
if (vips_tilecache(image, &cached, "threaded", TRUE, "persistent", TRUE, "max_tiles", -1, NULL)) {
return Error(baton, hook);
}
g_object_unref(image);
image = cached;
}
// Output
if (baton->output == "__jpeg" || (baton->output == "__input" && inputImageType == JPEG)) {
// Write JPEG to buffer
if (vips_jpegsave_buffer(image, &baton->bufferOut, &baton->bufferOutLength, "strip", !baton->withMetadata,
"Q", baton->quality, "optimize_coding", TRUE, "interlace", baton->progressive, NULL)) {
return Error(baton, hook);
}
baton->outputFormat = "jpeg";
} else if (baton->output == "__png" || (baton->output == "__input" && inputImageType == PNG)) {
// Write PNG to buffer
if (vips_pngsave_buffer(image, &baton->bufferOut, &baton->bufferOutLength, "strip", !baton->withMetadata,
"compression", baton->compressionLevel, "interlace", baton->progressive, NULL)) {
return Error(baton, hook);
}
baton->outputFormat = "png";
} else if (baton->output == "__webp" || (baton->output == "__input" && inputImageType == WEBP)) {
// Write WEBP to buffer
if (vips_webpsave_buffer(image, &baton->bufferOut, &baton->bufferOutLength, "strip", !baton->withMetadata,
"Q", baton->quality, NULL)) {
return Error(baton, hook);
}
baton->outputFormat = "webp";
} else {
bool output_jpeg = is_jpeg(baton->output);
bool output_png = is_png(baton->output);
bool output_webp = is_webp(baton->output);
bool output_tiff = is_tiff(baton->output);
bool match_input = !(output_jpeg || output_png || output_webp || output_tiff);
if (output_jpeg || (match_input && inputImageType == JPEG)) {
// Write JPEG to file
if (vips_jpegsave(image, baton->output.c_str(), "strip", !baton->withMetadata,
"Q", baton->quality, "optimize_coding", TRUE, "interlace", baton->progressive, NULL)) {
return Error(baton, hook);
}
baton->outputFormat = "jpeg";
} else if (output_png || (match_input && inputImageType == PNG)) {
// Write PNG to file
if (vips_pngsave(image, baton->output.c_str(), "strip", !baton->withMetadata,
"compression", baton->compressionLevel, "interlace", baton->progressive, NULL)) {
return Error(baton, hook);
}
baton->outputFormat = "png";
} else if (output_webp || (match_input && inputImageType == WEBP)) {
// Write WEBP to file
if (vips_webpsave(image, baton->output.c_str(), "strip", !baton->withMetadata,
"Q", baton->quality, NULL)) {
return Error(baton, hook);
}
baton->outputFormat = "webp";
} else if (output_tiff || (match_input && inputImageType == TIFF)) {
// Write TIFF to file
if (vips_tiffsave(image, baton->output.c_str(), "strip", !baton->withMetadata,
"compression", VIPS_FOREIGN_TIFF_COMPRESSION_JPEG, "Q", baton->quality, NULL)) {
return Error(baton, hook);
}
baton->outputFormat = "tiff";
} else {
(baton->err).append("Unsupported output " + baton->output);
g_object_unref(image);
return Error(baton, hook);
}
}
// Clean up any dangling image references
g_object_unref(image);
g_object_unref(hook);
// Clean up libvips' per-request data and threads
vips_error_clear();
vips_thread_shutdown();
}
void HandleOKCallback () {
NanScope();
Handle<Value> argv[3] = { NanNull(), NanNull(), NanNull() };
if (!baton->err.empty()) {
// Error
argv[0] = Exception::Error(NanNew<String>(baton->err.data(), baton->err.size()));
} else {
int width = baton->width;
int height = baton->height;
if (baton->topOffsetPre != -1 && (baton->width == -1 || baton->height == -1)) {
width = baton->widthPre;
height = baton->heightPre;
}
if (baton->topOffsetPost != -1) {
width = baton->widthPost;
height = baton->heightPost;
}
// Info Object
Local<Object> info = NanNew<Object>();
info->Set(NanNew<String>("format"), NanNew<String>(baton->outputFormat));
info->Set(NanNew<String>("width"), NanNew<Number>(width));
info->Set(NanNew<String>("height"), NanNew<Number>(height));
if (baton->bufferOutLength > 0) {
// Buffer
argv[1] = NanNewBufferHandle(static_cast<char*>(baton->bufferOut), baton->bufferOutLength);
g_free(baton->bufferOut);
argv[2] = info;
} else {
// File
argv[1] = info;
}
}
delete baton;
// Decrement processing task counter
g_atomic_int_dec_and_test(&counter_process);
// Return to JavaScript
callback->Call(3, argv);
}
private:
ResizeBaton* baton;
/*
Calculate the angle of rotation and need-to-flip for the output image.
In order of priority:
1. Use explicitly requested angle (supports 90, 180, 270)
2. Use input image EXIF Orientation header - supports mirroring
3. Otherwise default to zero, i.e. no rotation
*/
std::tuple<Angle, bool>
CalculateRotationAndFlip(int const angle, VipsImage const *input) {
Angle rotate = ANGLE_0;
bool flip = FALSE;
if (angle == -1) {
const char *exif;
if (!vips_image_get_string(input, "exif-ifd0-Orientation", &exif)) {
if (exif[0] == 0x36) { // "6"
rotate = ANGLE_90;
} else if (exif[0] == 0x33) { // "3"
rotate = ANGLE_180;
} else if (exif[0] == 0x38) { // "8"
rotate = ANGLE_270;
} else if (exif[0] == 0x32) { // "2" (flip 1)
flip = TRUE;
} else if (exif[0] == 0x37) { // "7" (flip 6)
rotate = ANGLE_90;
flip = TRUE;
} else if (exif[0] == 0x34) { // "4" (flip 3)
rotate = ANGLE_180;
flip = TRUE;
} else if (exif[0] == 0x35) { // "5" (flip 8)
rotate = ANGLE_270;
flip = TRUE;
}
}
} else {
if (angle == 90) {
rotate = ANGLE_90;
} else if (angle == 180) {
rotate = ANGLE_180;
} else if (angle == 270) {
rotate = ANGLE_270;
}
}
return std::make_tuple(rotate, flip);
}
/*
Calculate the (left, top) coordinates of the output image
within the input image, applying the given gravity.
*/
std::tuple<int, int>
CalculateCrop(int const inWidth, int const inHeight, int const outWidth, int const outHeight, int const gravity) {
int left = 0;
int top = 0;
switch (gravity) {
case 1: // North
left = (inWidth - outWidth + 1) / 2;
break;
case 2: // East
left = inWidth - outWidth;
top = (inHeight - outHeight + 1) / 2;
break;
case 3: // South
left = (inWidth - outWidth + 1) / 2;
top = inHeight - outHeight;
break;
case 4: // West
top = (inHeight - outHeight + 1) / 2;
break;
default: // Centre
left = (inWidth - outWidth + 1) / 2;
top = (inHeight - outHeight + 1) / 2;
}
return std::make_tuple(left, top);
}
/*
Copy then clear the error message.
Unref all transitional images on the hook.
Clear all thread-local data.
*/
void Error(ResizeBaton *baton, VipsObject *hook) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
g_object_unref(hook);
vips_thread_shutdown();
}
};
/*
resize(options, output, callback)
*/
NAN_METHOD(resize) {
NanScope();
// V8 objects are converted to non-V8 types held in the baton struct
ResizeBaton *baton = new ResizeBaton;
Local<Object> options = args[0]->ToObject();
// Input filename
baton->fileIn = *String::Utf8Value(options->Get(NanNew<String>("fileIn"))->ToString());
baton->accessMethod = options->Get(NanNew<String>("sequentialRead"))->BooleanValue() ? VIPS_ACCESS_SEQUENTIAL : VIPS_ACCESS_RANDOM;
// Input Buffer object
if (options->Get(NanNew<String>("bufferIn"))->IsObject()) {
Local<Object> buffer = options->Get(NanNew<String>("bufferIn"))->ToObject();
baton->bufferInLength = node::Buffer::Length(buffer);
baton->bufferIn = node::Buffer::Data(buffer);
}
// Extract image options
baton->topOffsetPre = options->Get(NanNew<String>("topOffsetPre"))->Int32Value();
baton->leftOffsetPre = options->Get(NanNew<String>("leftOffsetPre"))->Int32Value();
baton->widthPre = options->Get(NanNew<String>("widthPre"))->Int32Value();
baton->heightPre = options->Get(NanNew<String>("heightPre"))->Int32Value();
baton->topOffsetPost = options->Get(NanNew<String>("topOffsetPost"))->Int32Value();
baton->leftOffsetPost = options->Get(NanNew<String>("leftOffsetPost"))->Int32Value();
baton->widthPost = options->Get(NanNew<String>("widthPost"))->Int32Value();
baton->heightPost = options->Get(NanNew<String>("heightPost"))->Int32Value();
// Output image dimensions
baton->width = options->Get(NanNew<String>("width"))->Int32Value();
baton->height = options->Get(NanNew<String>("height"))->Int32Value();
// Canvas option
Local<String> canvas = options->Get(NanNew<String>("canvas"))->ToString();
if (canvas->Equals(NanNew<String>("c"))) {
baton->canvas = CROP;
} else if (canvas->Equals(NanNew<String>("m"))) {
baton->canvas = MAX;
} else if (canvas->Equals(NanNew<String>("e"))) {
baton->canvas = EMBED;
}
// Background colour
Local<Array> background = Local<Array>::Cast(options->Get(NanNew<String>("background")));
for (int i = 0; i < 4; i++) {
baton->background[i] = background->Get(i)->NumberValue();
}
// Resize options
baton->withoutEnlargement = options->Get(NanNew<String>("withoutEnlargement"))->BooleanValue();
baton->gravity = options->Get(NanNew<String>("gravity"))->Int32Value();
baton->interpolator = *String::Utf8Value(options->Get(NanNew<String>("interpolator"))->ToString());
// Operators
baton->flatten = options->Get(NanNew<String>("flatten"))->BooleanValue();
baton->sharpen = options->Get(NanNew<String>("sharpen"))->BooleanValue();
baton->gamma = options->Get(NanNew<String>("gamma"))->NumberValue();
baton->greyscale = options->Get(NanNew<String>("greyscale"))->BooleanValue();
baton->angle = options->Get(NanNew<String>("angle"))->Int32Value();
baton->flip = options->Get(NanNew<String>("flip"))->BooleanValue();
baton->flop = options->Get(NanNew<String>("flop"))->BooleanValue();
// Output options
baton->progressive = options->Get(NanNew<String>("progressive"))->BooleanValue();
baton->quality = options->Get(NanNew<String>("quality"))->Int32Value();
baton->compressionLevel = options->Get(NanNew<String>("compressionLevel"))->Int32Value();
baton->withMetadata = options->Get(NanNew<String>("withMetadata"))->BooleanValue();
// Output filename or __format for Buffer
baton->output = *String::Utf8Value(options->Get(NanNew<String>("output"))->ToString());
// Join queue for worker thread
NanCallback *callback = new NanCallback(args[1].As<v8::Function>());
NanAsyncQueueWorker(new ResizeWorker(callback, baton));
// Increment queued task counter
g_atomic_int_inc(&counter_queue);
NanReturnUndefined();
}

8
src/resize.h Executable file
View File

@@ -0,0 +1,8 @@
#ifndef SHARP_RESIZE_H
#define SHARP_RESIZE_H
#include "nan.h"
NAN_METHOD(resize);
#endif

View File

@@ -1,173 +1,38 @@
#include <node.h>
#include <math.h>
#include <string>
#include <vector>
#include <vips/vips.h>
#include "nan.h"
#include "common.h"
#include "metadata.h"
#include "resize.h"
#include "utilities.h"
using namespace v8;
// Free VipsImage children when object goes out of scope
// Thanks due to https://github.com/dosx/node-vips
class ImageFreer {
public:
ImageFreer() {}
~ImageFreer() {
for (uint16_t i = 0; i < v_.size(); i++) {
if (v_[i] != NULL) {
g_object_unref(v_[i]);
}
}
v_.clear();
}
void add(VipsImage* i) { v_.push_back(i); }
private:
std::vector<VipsImage*> v_;
};
struct ResizeBaton {
std::string src;
std::string dst;
int cols;
int rows;
bool crop;
int embed;
std::string err;
Persistent<Function> callback;
};
void ResizeAsync(uv_work_t *work) {
ResizeBaton* baton = static_cast<ResizeBaton*>(work->data);
VipsImage *in = vips_image_new_mode((baton->src).c_str(), "p");
im_jpeg2vips((baton->src).c_str(), in);
if (in == NULL) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
return;
}
ImageFreer freer;
freer.add(in);
VipsImage* img = in;
VipsImage* t[4];
if (im_open_local_array(img, t, 4, "temp", "p")) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
return;
}
double xfactor = static_cast<double>(img->Xsize) / std::max(baton->cols, 1);
double yfactor = static_cast<double>(img->Ysize) / std::max(baton->rows, 1);
double factor = baton->crop ? std::min(xfactor, yfactor) : std::max(xfactor, yfactor);
factor = std::max(factor, 1.0);
int shrink = floor(factor);
double residual = shrink / factor;
// Use im_shrink with the integral reduction
if (im_shrink(img, t[0], shrink, shrink)) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
return;
}
// Use im_affinei with the remaining float part using bilinear interpolation
if (im_affinei_all(t[0], t[1], vips_interpolate_bilinear_static(), residual, 0, 0, residual, 0, 0)) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
return;
}
img = t[1];
if (baton->crop) {
int width = std::min(img->Xsize, baton->cols);
int height = std::min(img->Ysize, baton->rows);
int left = (img->Xsize - width + 1) / 2;
int top = (img->Ysize - height + 1) / 2;
if (im_extract_area(img, t[2], left, top, width, height)) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
return;
}
img = t[2];
} else {
int left = (baton->cols - img->Xsize) / 2;
int top = (baton->rows - img->Ysize) / 2;
if (im_embed(img, t[2], baton->embed, left, top, baton->cols, baton->rows)) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
return;
}
img = t[2];
}
// Mild sharpen
INTMASK* sharpen = im_create_imaskv("sharpen", 3, 3,
-1, -1, -1,
-1, 32, -1,
-1, -1, -1);
sharpen->scale = 24;
if (im_conv(img, t[3], sharpen)) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
return;
}
img = t[3];
if (im_vips2jpeg(img, baton->dst.c_str())) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
}
}
void ResizeAsyncAfter(uv_work_t *work, int status) {
HandleScope scope;
ResizeBaton *baton = static_cast<ResizeBaton*>(work->data);
Local<Value> argv[1];
if (!baton->err.empty()) {
argv[0] = String::New(baton->err.data(), baton->err.size());
} else {
argv[0] = Local<Value>::New(Null());
}
baton->callback->Call(Context::GetCurrent()->Global(), 1, argv);
baton->callback.Dispose();
delete baton;
delete work;
}
Handle<Value> Resize(const Arguments& args) {
HandleScope scope;
ResizeBaton *baton = new ResizeBaton;
baton->src = *String::Utf8Value(args[0]->ToString());
baton->dst = *String::Utf8Value(args[1]->ToString());
baton->cols = args[2]->Int32Value();
baton->rows = args[3]->Int32Value();
Local<String> canvas = args[4]->ToString();
if (canvas->Equals(String::NewSymbol("c"))) {
baton->crop = true;
} else if (canvas->Equals(String::NewSymbol("w"))) {
baton->crop = false;
baton->embed = 4;
} else if (canvas->Equals(String::NewSymbol("b"))) {
baton->crop = false;
baton->embed = 0;
}
baton->callback = Persistent<Function>::New(Local<Function>::Cast(args[5]));
uv_work_t *work = new uv_work_t;
work->data = baton;
uv_queue_work(uv_default_loop(), work, ResizeAsync, (uv_after_work_cb)ResizeAsyncAfter);
return Undefined();
static void at_exit(void* arg) {
NanScope();
vips_shutdown();
}
extern "C" void init(Handle<Object> target) {
HandleScope scope;
vips_init("");
NODE_SET_METHOD(target, "resize", Resize);
};
NanScope();
vips_init("sharp");
node::AtExit(at_exit);
// Set libvips operation cache limits
vips_cache_set_max_mem(100 * 1048576); // 100 MB
vips_cache_set_max(500); // 500 operations
// Notify the V8 garbage collector of max cache size
NanAdjustExternalMemory(vips_cache_get_max_mem());
// Methods available to JavaScript
NODE_SET_METHOD(target, "metadata", metadata);
NODE_SET_METHOD(target, "resize", resize);
NODE_SET_METHOD(target, "cache", cache);
NODE_SET_METHOD(target, "concurrency", concurrency);
NODE_SET_METHOD(target, "counters", counters);
}
NODE_MODULE(sharp, init)

64
src/utilities.cc Executable file
View File

@@ -0,0 +1,64 @@
#include <node.h>
#include <vips/vips.h>
#include "nan.h"
#include "common.h"
#include "utilities.h"
using namespace v8;
/*
Get and set cache memory and item limits
*/
NAN_METHOD(cache) {
NanScope();
// Set cache memory limit
if (args[0]->IsInt32()) {
int newMax = args[0]->Int32Value() * 1048576;
int oldMax = vips_cache_get_max_mem();
vips_cache_set_max_mem(newMax);
// Notify the V8 garbage collector of delta in max cache size
NanAdjustExternalMemory(newMax - oldMax);
}
// Set cache items limit
if (args[1]->IsInt32()) {
vips_cache_set_max(args[1]->Int32Value());
}
// Get cache statistics
Local<Object> cache = NanNew<Object>();
cache->Set(NanNew<String>("current"), NanNew<Number>(vips_tracked_get_mem() / 1048576));
cache->Set(NanNew<String>("high"), NanNew<Number>(vips_tracked_get_mem_highwater() / 1048576));
cache->Set(NanNew<String>("memory"), NanNew<Number>(vips_cache_get_max_mem() / 1048576));
cache->Set(NanNew<String>("items"), NanNew<Number>(vips_cache_get_max()));
NanReturnValue(cache);
}
/*
Get and set size of thread pool
*/
NAN_METHOD(concurrency) {
NanScope();
// Set concurrency
if (args[0]->IsInt32()) {
vips_concurrency_set(args[0]->Int32Value());
}
// Get concurrency
NanReturnValue(NanNew<Number>(vips_concurrency_get()));
}
/*
Get internal counters (queued tasks, processing tasks)
*/
NAN_METHOD(counters) {
NanScope();
Local<Object> counters = NanNew<Object>();
counters->Set(NanNew<String>("queue"), NanNew<Number>(counter_queue));
counters->Set(NanNew<String>("process"), NanNew<Number>(counter_process));
NanReturnValue(counters);
}

10
src/utilities.h Executable file
View File

@@ -0,0 +1,10 @@
#ifndef SHARP_UTILITIES_H
#define SHARP_UTILITIES_H
#include "nan.h"
NAN_METHOD(cache);
NAN_METHOD(concurrency);
NAN_METHOD(counters);
#endif

21
test/bench/package.json Executable file
View File

@@ -0,0 +1,21 @@
{
"name": "sharp-benchmark",
"version": "0.0.1",
"private": true,
"author": "Lovell Fuller <npm@lovell.info>",
"description": "Benchmark and performance tests for sharp",
"scripts": {
"test": "node perf && node random && node parallel"
},
"devDependencies": {
"imagemagick": "^0.1.3",
"imagemagick-native": "^1.4.0",
"gm": "^1.16.0",
"async": "^0.9.0",
"benchmark": "^1.0.0"
},
"license": "Apache 2.0",
"engines": {
"node": ">=0.10"
}
}

41
test/bench/parallel.js Executable file
View File

@@ -0,0 +1,41 @@
'use strict';
var assert = require('assert');
var async = require('async');
var sharp = require('../../index');
var fixtures = require('../fixtures');
var width = 720;
var height = 480;
sharp.concurrency(1);
var timer = setInterval(function() {
console.dir(sharp.counters());
}, 100);
async.mapSeries([1, 1, 2, 4, 8, 16, 32, 64, 128], function(parallelism, next) {
var start = new Date().getTime();
async.times(parallelism,
function(id, callback) {
/*jslint unused: false */
sharp(fixtures.inputJpg).resize(width, height).toBuffer(function(err, buffer) {
buffer = null;
callback(err, new Date().getTime() - start);
});
},
function(err, ids) {
assert(!err);
assert(ids.length === parallelism);
var mean = ids.reduce(function(a, b) {
return a + b;
}) / ids.length;
console.log(parallelism + ' parallel calls: fastest=' + ids[0] + 'ms slowest=' + ids[ids.length - 1] + 'ms mean=' + mean + 'ms');
next();
}
);
}, function() {
clearInterval(timer);
console.dir(sharp.counters());
});

493
test/bench/perf.js Executable file
View File

@@ -0,0 +1,493 @@
'use strict';
var fs = require('fs');
var async = require('async');
var assert = require('assert');
var Benchmark = require('benchmark');
var imagemagick = require('imagemagick');
var imagemagickNative = require('imagemagick-native');
var gm = require('gm');
var sharp = require('../../index');
var fixtures = require('../fixtures');
var width = 720;
var height = 480;
// Disable libvips cache to ensure tests are as fair as they can be
sharp.cache(0);
async.series({
jpeg: function(callback) {
var inputJpgBuffer = fs.readFileSync(fixtures.inputJpg);
(new Benchmark.Suite('jpeg')).add('imagemagick-file-file', {
defer: true,
fn: function(deferred) {
imagemagick.resize({
srcPath: fixtures.inputJpg,
dstPath: fixtures.outputJpg,
quality: 0.8,
width: width,
height: height
}, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add('imagemagick-native-buffer-buffer', {
defer: true,
fn: function(deferred) {
imagemagickNative.convert({
srcData: inputJpgBuffer,
quality: 80,
width: width,
height: height,
format: 'JPEG'
});
deferred.resolve();
}
}).add('gm-buffer-file', {
defer: true,
fn: function(deferred) {
gm(inputJpgBuffer).resize(width, height).quality(80).write(fixtures.outputJpg, function (err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add('gm-buffer-buffer', {
defer: true,
fn: function(deferred) {
gm(inputJpgBuffer).resize(width, height).quality(80).toBuffer(function (err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add('gm-file-file', {
defer: true,
fn: function(deferred) {
gm(fixtures.inputJpg).resize(width, height).quality(80).write(fixtures.outputJpg, function (err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add('gm-file-buffer', {
defer: true,
fn: function(deferred) {
gm(fixtures.inputJpg).resize(width, height).quality(80).toBuffer(function (err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add('sharp-buffer-file', {
defer: true,
fn: function(deferred) {
sharp(inputJpgBuffer).resize(width, height).toFile(fixtures.outputJpg, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add('sharp-buffer-buffer', {
defer: true,
fn: function(deferred) {
sharp(inputJpgBuffer).resize(width, height).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add('sharp-file-file', {
defer: true,
fn: function(deferred) {
sharp(fixtures.inputJpg).resize(width, height).toFile(fixtures.outputJpg, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add('sharp-stream-stream', {
defer: true,
fn: function(deferred) {
var readable = fs.createReadStream(fixtures.inputJpg);
var writable = fs.createWriteStream(fixtures.outputJpg);
writable.on('finish', function() {
deferred.resolve();
});
var pipeline = sharp().resize(width, height);
readable.pipe(pipeline).pipe(writable);
}
}).add('sharp-file-buffer', {
defer: true,
fn: function(deferred) {
sharp(fixtures.inputJpg).resize(width, height).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add('sharp-promise', {
defer: true,
fn: function(deferred) {
sharp(inputJpgBuffer).resize(width, height).toBuffer().then(function(buffer) {
assert.notStrictEqual(null, buffer);
deferred.resolve();
});
}
}).add('sharp-sharpen', {
defer: true,
fn: function(deferred) {
sharp(inputJpgBuffer).resize(width, height).sharpen().toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add('sharp-nearest-neighbour', {
defer: true,
fn: function(deferred) {
sharp(inputJpgBuffer).resize(width, height).interpolateWith(sharp.interpolator.nearest).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add('sharp-bicubic', {
defer: true,
fn: function(deferred) {
sharp(inputJpgBuffer).resize(width, height).interpolateWith(sharp.interpolator.bicubic).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add('sharp-nohalo', {
defer: true,
fn: function(deferred) {
sharp(inputJpgBuffer).resize(width, height).interpolateWith(sharp.interpolator.nohalo).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add('sharp-locallyBoundedBicubic', {
defer: true,
fn: function(deferred) {
sharp(inputJpgBuffer).resize(width, height).interpolateWith(sharp.interpolator.locallyBoundedBicubic).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add('sharp-vertexSplitQuadraticBasisSpline', {
defer: true,
fn: function(deferred) {
sharp(inputJpgBuffer).resize(width, height).interpolateWith(sharp.interpolator.vertexSplitQuadraticBasisSpline).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add('sharp-gamma', {
defer: true,
fn: function(deferred) {
sharp(inputJpgBuffer).resize(width, height).gamma().toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add('sharp-greyscale', {
defer: true,
fn: function(deferred) {
sharp(inputJpgBuffer).resize(width, height).greyscale().toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add('sharp-greyscale-gamma', {
defer: true,
fn: function(deferred) {
sharp(inputJpgBuffer).resize(width, height).gamma().greyscale().toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add('sharp-progressive', {
defer: true,
fn: function(deferred) {
sharp(inputJpgBuffer).resize(width, height).progressive().toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add('sharp-rotate', {
defer: true,
fn: function(deferred) {
sharp(inputJpgBuffer).rotate(90).resize(width, height).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add('sharp-sequentialRead', {
defer: true,
fn: function(deferred) {
sharp(inputJpgBuffer).resize(width, height).sequentialRead().toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).on('cycle', function(event) {
console.log('jpeg ' + String(event.target));
}).on('complete', function() {
callback(null, this.filter('fastest').pluck('name'));
}).run();
},
png: function(callback) {
var inputPngBuffer = fs.readFileSync(fixtures.inputPng);
(new Benchmark.Suite('png')).add('imagemagick-file-file', {
defer: true,
fn: function(deferred) {
imagemagick.resize({
srcPath: fixtures.inputPng,
dstPath: fixtures.outputPng,
width: width,
height: height
}, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add('imagemagick-native-buffer-buffer', {
defer: true,
fn: function(deferred) {
imagemagickNative.convert({
srcData: inputPngBuffer,
width: width,
height: height,
format: 'PNG'
});
deferred.resolve();
}
}).add('gm-file-file', {
defer: true,
fn: function(deferred) {
gm(fixtures.inputPng).resize(width, height).write(fixtures.outputPng, function (err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add('gm-file-buffer', {
defer: true,
fn: function(deferred) {
gm(fixtures.inputPng).resize(width, height).quality(80).toBuffer(function (err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add('sharp-buffer-file', {
defer: true,
fn: function(deferred) {
sharp(inputPngBuffer).resize(width, height).toFile(fixtures.outputPng, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add('sharp-buffer-buffer', {
defer: true,
fn: function(deferred) {
sharp(inputPngBuffer).resize(width, height).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add('sharp-file-file', {
defer: true,
fn: function(deferred) {
sharp(fixtures.inputPng).resize(width, height).toFile(fixtures.outputPng, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add('sharp-file-buffer', {
defer: true,
fn: function(deferred) {
sharp(fixtures.inputPng).resize(width, height).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add('sharp-progressive', {
defer: true,
fn: function(deferred) {
sharp(inputPngBuffer).resize(width, height).progressive().toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).on('cycle', function(event) {
console.log(' png ' + String(event.target));
}).on('complete', function() {
callback(null, this.filter('fastest').pluck('name'));
}).run();
},
webp: function(callback) {
var inputWebPBuffer = fs.readFileSync(fixtures.inputWebP);
(new Benchmark.Suite('webp')).add('sharp-buffer-file', {
defer: true,
fn: function(deferred) {
sharp(inputWebPBuffer).resize(width, height).toFile(fixtures.outputWebP, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add('sharp-buffer-buffer', {
defer: true,
fn: function(deferred) {
sharp(inputWebPBuffer).resize(width, height).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add('sharp-file-file', {
defer: true,
fn: function(deferred) {
sharp(fixtures.inputWebP).resize(width, height).toFile(fixtures.outputWebP, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add('sharp-file-buffer', {
defer: true,
fn: function(deferred) {
sharp(fixtures.inputWebp).resize(width, height).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).on('cycle', function(event) {
console.log('webp ' + String(event.target));
}).on('complete', function() {
callback(null, this.filter('fastest').pluck('name'));
}).run();
}
}, function(err, results) {
assert(!err, err);
Object.keys(results).forEach(function(format) {
if (results[format].toString().substr(0, 5) !== 'sharp') {
console.log('sharp was slower than ' + results[format] + ' for ' + format);
}
});
console.dir(sharp.cache());
});

65
test/bench/random.js Executable file
View File

@@ -0,0 +1,65 @@
'use strict';
var imagemagick = require('imagemagick');
var gm = require('gm');
var assert = require('assert');
var Benchmark = require('benchmark');
var sharp = require('../../index');
var fixtures = require('../fixtures');
var min = 320;
var max = 960;
var randomDimension = function() {
return Math.random() * (max - min) + min;
};
new Benchmark.Suite('random').add('imagemagick', {
defer: true,
fn: function(deferred) {
imagemagick.resize({
srcPath: fixtures.inputJpg,
dstPath: fixtures.outputJpg,
quality: 0.8,
width: randomDimension(),
height: randomDimension()
}, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add('gm', {
defer: true,
fn: function(deferred) {
gm(fixtures.inputJpg).resize(randomDimension(), randomDimension()).quality(80).toBuffer(function (err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add('sharp', {
defer: true,
fn: function(deferred) {
sharp(fixtures.inputJpg).resize(randomDimension(), randomDimension()).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).on('cycle', function(event) {
console.log(String(event.target));
}).on('complete', function() {
var winner = this.filter('fastest').pluck('name');
assert.strictEqual('sharp', String(winner), 'sharp was slower than ' + winner);
console.dir(sharp.cache());
}).run();

View File

Before

Width:  |  Height:  |  Size: 813 KiB

After

Width:  |  Height:  |  Size: 813 KiB

BIN
test/fixtures/4.webp vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

BIN
test/fixtures/50020484-00001.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 KiB

BIN
test/fixtures/Crash_test.gif vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

BIN
test/fixtures/G31D.TIF vendored Normal file

Binary file not shown.

BIN
test/fixtures/Landscape_5.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

BIN
test/fixtures/Landscape_8.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

BIN
test/fixtures/blackbug.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

BIN
test/fixtures/gamma_dalai_lama_gray.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

31
test/fixtures/index.js vendored Executable file
View File

@@ -0,0 +1,31 @@
'use strict';
var path = require('path');
var getPath = function(filename) {
return path.join(__dirname, filename);
};
module.exports = {
inputJpg: getPath('2569067123_aca715a2ee_o.jpg'), // http://www.flickr.com/photos/grizdave/2569067123/
inputJpgWithExif: getPath('Landscape_8.jpg'), // https://github.com/recurser/exif-orientation-examples/blob/master/Landscape_8.jpg
inputJpgWithExifMirroring: getPath('Landscape_5.jpg'), // https://github.com/recurser/exif-orientation-examples/blob/master/Landscape_5.jpg
inputJpgWithGammaHoliness: getPath('gamma_dalai_lama_gray.jpg'), // http://www.4p8.com/eric.brasseur/gamma.html
inputJpgWithCmykProfile: getPath('Channel_digital_image_CMYK_color.jpg'), // http://en.wikipedia.org/wiki/File:Channel_digital_image_CMYK_color.jpg
inputPng: getPath('50020484-00001.png'), // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png
inputPngWithTransparency: getPath('blackbug.png'), // public domain
inputWebP: getPath('4.webp'), // http://www.gstatic.com/webp/gallery/4.webp
inputTiff: getPath('G31D.TIF'), // http://www.fileformat.info/format/tiff/sample/e6c9a6e5253348f4aef6d17b534360ab/index.htm
inputGif: getPath('Crash_test.gif'), // http://upload.wikimedia.org/wikipedia/commons/e/e3/Crash_test.gif
outputJpg: getPath('output.jpg'),
outputPng: getPath('output.png'),
outputWebP: getPath('output.webp'),
outputZoinks: getPath('output.zoinks'), // an 'unknown' file extension
path: getPath // allows tests to write files to fixtures directory (for testing with human eyes)
};

8
test/leak/leak.sh Executable file
View File

@@ -0,0 +1,8 @@
if ! type valgrind >/dev/null; then
echo "Please install valgrind before running memory leak tests"
exit 1
fi
curl -O https://raw.githubusercontent.com/jcupitt/libvips/master/libvips.supp test/leak/libvips.supp
cd ../../
G_SLICE=always-malloc G_DEBUG=gc-friendly valgrind --suppressions=test/leak/libvips.supp --suppressions=test/leak/sharp.supp --leak-check=full --show-leak-kinds=definite,indirect,possible --num-callers=20 npm test

111
test/leak/sharp.supp Executable file
View File

@@ -0,0 +1,111 @@
# libjpeg warnings
{
cond_jpeg_read_scanlines
Memcheck:Cond
...
fun:jpeg_read_scanlines
}
{
value_jpeg_read_scanlines
Memcheck:Value8
...
fun:jpeg_read_scanlines
}
{
cond_jpeg_write_scanlines
Memcheck:Cond
...
fun:jpeg_write_scanlines
}
{
cond_jpeg_finish_compress
Memcheck:Cond
...
fun:jpeg_finish_compress
}
{
value_jpeg_finish_compress
Memcheck:Value8
...
fun:jpeg_finish_compress
}
# libvips interpolator warnings
{
cond_libvips_interpolate_lbb
Memcheck:Cond
...
fun:_ZL32vips_interpolate_lbb_interpolateP16_VipsInterpolatePvP11_VipsRegiondd
fun:vips_affine_gen
}
# libuv warnings
{
free_libuv
Memcheck:Free
...
fun:uv__work_done
}
# nodejs warnings
{
param_nodejs_write_buffer
Memcheck:Param
write(buf)
...
obj:/usr/bin/nodejs
}
{
leak_nodejs_ImmutableAsciiSource_CreateFromLiteral
Memcheck:Leak
match-leak-kinds: definite
...
fun:_ZN4node20ImmutableAsciiSource17CreateFromLiteralEPKcm
}
{
leak_nodejs_Buffer_New
Memcheck:Leak
match-leak-kinds: definite
...
fun:_ZN4node6Buffer3NewERKN2v89ArgumentsE
}
{
leak_nodejs_Buffer_Replace
Memcheck:Leak
match-leak-kinds: indirect,possible
...
fun:_ZN4node6Buffer7ReplaceEPcmPFvS1_PvES2_
}
{
leak_nodejs_SignalWrap_New
Memcheck:Leak
match-leak-kinds: possible
...
fun:_ZN4node10SignalWrap3NewERKN2v89ArgumentsE
}
{
leak_nodejs_TTYWrap_New
Memcheck:Leak
match-leak-kinds: possible
...
fun:_ZN4node7TTYWrap3NewERKN2v89ArgumentsE
}
{
leak_nodejs_ares_init_options
Memcheck:Leak
match-leak-kinds: reachable
fun:malloc
fun:strdup
...
fun:ares_init_options
}
# vips__init warnings
{
leak_libvips_init
Memcheck:Leak
match-leak-kinds: reachable
fun:malloc
...
fun:vips__init
}

62
test/unit/alpha.js Executable file
View File

@@ -0,0 +1,62 @@
'use strict';
var assert = require('assert');
var sharp = require('../../index');
var fixtures = require('../fixtures');
describe('Alpha transparency', function() {
it('Flatten to black', function(done) {
sharp(fixtures.inputPngWithTransparency)
.flatten()
.resize(400, 300)
.toFile(fixtures.path('output.flatten-black.jpg'), done);
});
it('Flatten to RGB orange', function(done) {
sharp(fixtures.inputPngWithTransparency)
.flatten()
.background({r: 255, g: 102, b: 0})
.resize(400, 300)
.toFile(fixtures.path('output.flatten-rgb-orange.jpg'), done);
});
it('Flatten to CSS/hex orange', function(done) {
sharp(fixtures.inputPngWithTransparency)
.flatten()
.background('#ff6600')
.resize(400, 300)
.toFile(fixtures.path('output.flatten-hex-orange.jpg'), done);
});
it('Do not flatten', function(done) {
sharp(fixtures.inputPngWithTransparency)
.flatten(false)
.toBuffer(function(err, data) {
if (err) throw err;
sharp(data).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('png', metadata.format);
assert.strictEqual(4, metadata.channels);
done();
});
});
});
it('Ignored for JPEG', function(done) {
sharp(fixtures.inputJpg)
.background('#ff0000')
.flatten()
.toBuffer(function(err, data) {
if (err) throw err;
sharp(data).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('jpeg', metadata.format);
assert.strictEqual(3, metadata.channels);
done();
});
});
});
});

69
test/unit/colourspace.js Executable file
View File

@@ -0,0 +1,69 @@
'use strict';
var assert = require('assert');
var sharp = require('../../index');
var fixtures = require('../fixtures');
describe('Colour space conversion', function() {
it('To greyscale', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 240)
.greyscale()
.toFile(fixtures.path('output.greyscale-gamma-0.0.jpg'), done);
});
it('To greyscale with gamma correction', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 240)
.gamma()
.greyscale()
.toFile(fixtures.path('output.greyscale-gamma-2.2.jpg'), done);
});
it('Not to greyscale', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 240)
.greyscale(false)
.toFile(fixtures.path('output.greyscale-not.jpg'), done);
});
it('From 1-bit TIFF to sRGB WebP [slow]', function(done) {
sharp(fixtures.inputTiff)
.webp()
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('webp', info.format);
done();
});
});
it('From CMYK to sRGB', function(done) {
sharp(fixtures.inputJpgWithCmykProfile)
.resize(320)
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
done();
});
});
it('From CMYK to sRGB with white background, not yellow', function(done) {
sharp(fixtures.inputJpgWithCmykProfile)
.resize(320, 240)
.background('white')
.embed()
.toFile(fixtures.path('output.cmyk2srgb.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
});

93
test/unit/crop.js Executable file
View File

@@ -0,0 +1,93 @@
'use strict';
var assert = require('assert');
var sharp = require('../../index');
var fixtures = require('../fixtures');
describe('Crop gravities', function() {
it('North', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 80)
.crop(sharp.gravity.north)
.toFile(fixtures.path('output.gravity-north.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height);
done();
});
});
it('East', function(done) {
sharp(fixtures.inputJpg)
.resize(80, 320)
.crop(sharp.gravity.east)
.toFile(fixtures.path('output.gravity-east.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(80, info.width);
assert.strictEqual(320, info.height);
done();
});
});
it('South', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 80)
.crop(sharp.gravity.south)
.toFile(fixtures.path('output.gravity-south.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height);
done();
});
});
it('West', function(done) {
sharp(fixtures.inputJpg)
.resize(80, 320)
.crop(sharp.gravity.west)
.toFile(fixtures.path('output.gravity-west.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(80, info.width);
assert.strictEqual(320, info.height);
done();
});
});
it('Center', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 80)
.crop(sharp.gravity.center)
.toFile(fixtures.path('output.gravity-center.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height);
done();
});
});
it('Centre', function(done) {
sharp(fixtures.inputJpg)
.resize(80, 320)
.crop(sharp.gravity.centre)
.toFile(fixtures.path('output.gravity-centre.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(80, info.width);
assert.strictEqual(320, info.height);
done();
});
});
it('Invalid', function(done) {
var isValid = true;
try {
sharp(fixtures.inputJpg).crop(5);
} catch (err) {
isValid = false;
}
assert.strictEqual(false, isValid);
done();
});
});

49
test/unit/embed.js Executable file
View File

@@ -0,0 +1,49 @@
'use strict';
var assert = require('assert');
var sharp = require('../../index');
var fixtures = require('../fixtures');
describe('Embed', function() {
it('JPEG within PNG, no alpha channel', function(done) {
sharp(fixtures.inputJpg)
.embed()
.resize(320, 240)
.png()
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
sharp(data).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual(3, metadata.channels);
done();
});
});
});
it('JPEG within WebP, to include alpha channel', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 240)
.background({r: 0, g: 0, b: 0, a: 0})
.embed()
.webp()
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('webp', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
sharp(data).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual(4, metadata.channels);
done();
});
});
});
});

93
test/unit/extract.js Executable file
View File

@@ -0,0 +1,93 @@
'use strict';
var assert = require('assert');
var sharp = require('../../index');
var fixtures = require('../fixtures');
describe('Partial image extraction', function() {
it('JPEG', function(done) {
sharp(fixtures.inputJpg)
.extract(2, 2, 20, 20)
.toFile(fixtures.path('output.extract.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(20, info.width);
assert.strictEqual(20, info.height);
done();
});
});
it('PNG', function(done) {
sharp(fixtures.inputPng)
.extract(300, 200, 400, 200)
.toFile(fixtures.path('output.extract.png'), function(err, info) {
if (err) throw err;
assert.strictEqual(400, info.width);
assert.strictEqual(200, info.height);
done();
});
});
it('WebP', function(done) {
sharp(fixtures.inputWebP)
.extract(50, 100, 125, 200)
.toFile(fixtures.path('output.extract.webp'), function(err, info) {
if (err) throw err;
assert.strictEqual(125, info.width);
assert.strictEqual(200, info.height);
done();
});
});
it('TIFF', function(done) {
sharp(fixtures.inputTiff)
.extract(63, 34, 341, 529)
.toFile(fixtures.path('output.extract.tiff'), function(err, info) {
if (err) throw err;
assert.strictEqual(341, info.width);
assert.strictEqual(529, info.height);
done();
});
});
it('Before resize', function(done) {
sharp(fixtures.inputJpg)
.extract(10, 10, 10, 500, 500)
.resize(100, 100)
.toFile(fixtures.path('output.extract.resize.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(100, info.width);
assert.strictEqual(100, info.height);
done();
});
});
it('After resize and crop', function(done) {
sharp(fixtures.inputJpg)
.resize(500, 500)
.crop(sharp.gravity.north)
.extract(10, 10, 100, 100)
.toFile(fixtures.path('output.resize.crop.extract.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(100, info.width);
assert.strictEqual(100, info.height);
done();
});
});
it('Before and after resize and crop', function(done) {
sharp(fixtures.inputJpg)
.extract(0, 0, 700, 700)
.resize(500, 500)
.crop(sharp.gravity.north)
.extract(10, 10, 100, 100)
.toFile(fixtures.path('output.extract.resize.crop.extract.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(100, info.width);
assert.strictEqual(100, info.height);
done();
});
});
});

41
test/unit/gamma.js Executable file
View File

@@ -0,0 +1,41 @@
'use strict';
var assert = require('assert');
var sharp = require('../../index');
var fixtures = require('../fixtures');
describe('Gamma correction', function() {
it('value of 0.0 (disabled)', function(done) {
sharp(fixtures.inputJpgWithGammaHoliness)
.resize(129, 111)
.toFile(fixtures.path('output.gamma-0.0.jpg'), done);
});
it('value of 2.2 (default)', function(done) {
sharp(fixtures.inputJpgWithGammaHoliness)
.resize(129, 111)
.gamma()
.toFile(fixtures.path('output.gamma-2.2.jpg'), done);
});
it('value of 3.0', function(done) {
sharp(fixtures.inputJpgWithGammaHoliness)
.resize(129, 111)
.gamma(3)
.toFile(fixtures.path('output.gamma-3.0.jpg'), done);
});
it('invalid value', function(done) {
var isValid = true;
try {
sharp(fixtures.inputJpgWithGammaHoliness).gamma(4);
} catch (err) {
isValid = false;
}
assert.strictEqual(false, isValid);
done();
});
});

94
test/unit/interpolation.js Executable file
View File

@@ -0,0 +1,94 @@
'use strict';
var assert = require('assert');
var sharp = require('../../index');
var fixtures = require('../fixtures');
describe('Interpolation', function() {
it('nearest neighbour', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 240)
.interpolateWith(sharp.interpolator.nearest)
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
it('bilinear', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 240)
.interpolateWith(sharp.interpolator.bilinear)
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
it('bicubic', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 240)
.interpolateWith(sharp.interpolator.bicubic)
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
it('nohalo', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 240)
.interpolateWith(sharp.interpolator.nohalo)
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
it('locally bounded bicubic (LBB)', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 240)
.interpolateWith(sharp.interpolator.locallyBoundedBicubic)
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
it('vertex split quadratic basis spline (VSQBS)', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 240)
.interpolateWith(sharp.interpolator.vertexSplitQuadraticBasisSpline)
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
});

370
test/unit/io.js Executable file
View File

@@ -0,0 +1,370 @@
'use strict';
var fs = require('fs');
var assert = require('assert');
var sharp = require('../../index');
var fixtures = require('../fixtures');
describe('Input/output', function() {
it('Read from File and write to Stream', function(done) {
var writable = fs.createWriteStream(fixtures.outputJpg);
writable.on('finish', function() {
sharp(fixtures.outputJpg).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
fs.unlinkSync(fixtures.outputJpg);
done();
});
});
sharp(fixtures.inputJpg).resize(320, 240).pipe(writable);
});
it('Read from Buffer and write to Stream', function(done) {
var inputJpgBuffer = fs.readFileSync(fixtures.inputJpg);
var writable = fs.createWriteStream(fixtures.outputJpg);
writable.on('finish', function() {
sharp(fixtures.outputJpg).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
fs.unlinkSync(fixtures.outputJpg);
done();
});
});
sharp(inputJpgBuffer).resize(320, 240).pipe(writable);
});
it('Read from Stream and write to File', function(done) {
var readable = fs.createReadStream(fixtures.inputJpg);
var pipeline = sharp().resize(320, 240).toFile(fixtures.outputJpg, function(err, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
fs.unlinkSync(fixtures.outputJpg);
done();
});
readable.pipe(pipeline);
});
it('Read from Stream and write to Buffer', function(done) {
var readable = fs.createReadStream(fixtures.inputJpg);
var pipeline = sharp().resize(320, 240).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
readable.pipe(pipeline);
});
it('Read from Stream and write to Buffer via Promise', function(done) {
var readable = fs.createReadStream(fixtures.inputJpg);
var pipeline = sharp().resize(1, 1);
pipeline.toBuffer().then(function(data) {
assert.strictEqual(true, data.length > 0);
done();
}).catch(function(err) {
throw err;
});
readable.pipe(pipeline);
});
it('Read from Stream and write to Stream', function(done) {
var readable = fs.createReadStream(fixtures.inputJpg);
var writable = fs.createWriteStream(fixtures.outputJpg);
writable.on('finish', function() {
sharp(fixtures.outputJpg).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
fs.unlinkSync(fixtures.outputJpg);
done();
});
});
var pipeline = sharp().resize(320, 240);
readable.pipe(pipeline).pipe(writable);
});
it('Handle Stream to Stream error ', function(done) {
var pipeline = sharp().resize(320, 240);
var anErrorWasEmitted = false;
pipeline.on('error', function(err) {
anErrorWasEmitted = !!err;
}).on('end', function() {
assert(anErrorWasEmitted);
fs.unlinkSync(fixtures.outputJpg);
done();
});
var readableButNotAnImage = fs.createReadStream(__filename);
var writable = fs.createWriteStream(fixtures.outputJpg);
readableButNotAnImage.pipe(pipeline).pipe(writable);
});
it('Handle File to Stream error', function(done) {
var readableButNotAnImage = sharp(__filename).resize(320, 240);
var anErrorWasEmitted = false;
readableButNotAnImage.on('error', function(err) {
anErrorWasEmitted = !!err;
}).on('end', function() {
assert(anErrorWasEmitted);
fs.unlinkSync(fixtures.outputJpg);
done();
});
var writable = fs.createWriteStream(fixtures.outputJpg);
readableButNotAnImage.pipe(writable);
});
it('Sequential read', function(done) {
sharp(fixtures.inputJpg)
.sequentialRead()
.resize(320, 240)
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
it('Not sequential read', function(done) {
sharp(fixtures.inputJpg)
.sequentialRead(false)
.resize(320, 240)
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
it('Fail when output File is input File', function(done) {
sharp(fixtures.inputJpg).toFile(fixtures.inputJpg, function(err) {
assert(!!err);
done();
});
});
it('Fail when output File is input File via Promise', function(done) {
sharp(fixtures.inputJpg).toFile(fixtures.inputJpg).then(function(data) {
assert(false);
done();
}).catch(function(err) {
assert(!!err);
done();
});
});
it('Fail when output File is empty', function(done) {
sharp(fixtures.inputJpg).toFile('', function(err) {
assert(!!err);
done();
});
});
it('Fail when output File is empty via Promise', function(done) {
sharp(fixtures.inputJpg).toFile('').then(function(data) {
assert(false);
done();
}).catch(function(err) {
assert(!!err);
done();
});
});
it('Fail when input is empty Buffer', function(done) {
var failed = true;
try {
sharp(new Buffer(0));
failed = false;
} catch (err) {
assert(err instanceof Error);
}
assert(failed);
done();
});
it('Fail when input is invalid Buffer', function(done) {
var failed = true;
try {
sharp(new Buffer([0x1, 0x2, 0x3, 0x4]));
failed = false;
} catch (err) {
assert(err instanceof Error);
}
assert(failed);
done();
});
it('Promises/A+', function(done) {
sharp(fixtures.inputJpg).resize(320, 240).toBuffer().then(function(data) {
sharp(data).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
}).catch(function(err) {
throw err;
});
});
it('JPEG quality', function(done) {
sharp(fixtures.inputJpg).resize(320, 240).quality(70).toBuffer(function(err, buffer70) {
if (err) throw err;
sharp(fixtures.inputJpg).resize(320, 240).toBuffer(function(err, buffer80) {
if (err) throw err;
sharp(fixtures.inputJpg).resize(320, 240).quality(90).toBuffer(function(err, buffer90) {
if (err) throw err;
assert(buffer70.length < buffer80.length);
assert(buffer80.length < buffer90.length);
done();
});
});
});
});
it('Invalid quality', function(done) {
var isValid = true;
try {
sharp(fixtures.inputJpg).quality(-1);
} catch (err) {
isValid = false;
}
assert.strictEqual(false, isValid);
done();
});
it('Progressive image', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 240)
.png()
.progressive(false)
.toBuffer(function(err, nonProgressive, info) {
if (err) throw err;
assert.strictEqual(true, nonProgressive.length > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
sharp(nonProgressive)
.progressive()
.toBuffer(function(err, progressive, info) {
if (err) throw err;
assert.strictEqual(true, progressive.length > 0);
assert.strictEqual(true, progressive.length > nonProgressive.length);
assert.strictEqual('png', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
});
describe('Output filename without extension uses input format', function() {
it('JPEG', function(done) {
sharp(fixtures.inputJpg).resize(320, 80).toFile(fixtures.outputZoinks, function(err, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height);
fs.unlinkSync(fixtures.outputZoinks);
done();
});
});
it('PNG', function(done) {
sharp(fixtures.inputPng).resize(320, 80).toFile(fixtures.outputZoinks, function(err, info) {
if (err) throw err;
assert.strictEqual('png', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height);
fs.unlinkSync(fixtures.outputZoinks);
done();
});
});
it('Transparent PNG', function(done) {
sharp(fixtures.inputPngWithTransparency).resize(320, 80).toFile(fixtures.outputZoinks, function(err, info) {
if (err) throw err;
assert.strictEqual('png', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height);
done();
});
});
it('WebP', function(done) {
sharp(fixtures.inputWebP).resize(320, 80).toFile(fixtures.outputZoinks, function(err, info) {
if (err) throw err;
assert.strictEqual('webp', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height);
fs.unlinkSync(fixtures.outputZoinks);
done();
});
});
it('TIFF', function(done) {
sharp(fixtures.inputTiff).resize(320, 80).toFile(fixtures.outputZoinks, function(err, info) {
if (err) throw err;
assert.strictEqual('tiff', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height);
fs.unlinkSync(fixtures.outputZoinks);
done();
});
});
it('Fail with GIF', function(done) {
sharp(fixtures.inputGif).resize(320, 80).toFile(fixtures.outputZoinks, function(err) {
assert(!!err);
done();
});
});
});
describe('PNG compression level', function() {
it('valid', function(done) {
var isValid = false;
try {
sharp().compressionLevel(0);
isValid = true;
} catch (e) {}
assert(isValid);
done();
});
it('invalid', function(done) {
var isValid = false;
try {
sharp().compressionLevel(-1);
isValid = true;
} catch (e) {}
assert(!isValid);
done();
});
});
});

1
test/unit/jshint.js Executable file
View File

@@ -0,0 +1 @@
require('mocha-jshint')();

188
test/unit/metadata.js Executable file
View File

@@ -0,0 +1,188 @@
'use strict';
var fs = require('fs');
var assert = require('assert');
var sharp = require('../../index');
var fixtures = require('../fixtures');
describe('Image metadata', function() {
it('JPEG', function(done) {
sharp(fixtures.inputJpg).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('jpeg', metadata.format);
assert.strictEqual(2725, metadata.width);
assert.strictEqual(2225, metadata.height);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels);
assert.strictEqual('undefined', typeof metadata.orientation);
done();
});
});
it('JPEG with EXIF', function(done) {
sharp(fixtures.inputJpgWithExif).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('jpeg', metadata.format);
assert.strictEqual(450, metadata.width);
assert.strictEqual(600, metadata.height);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels);
assert.strictEqual(false, metadata.hasAlpha);
assert.strictEqual(8, metadata.orientation);
done();
});
});
it('TIFF', function(done) {
sharp(fixtures.inputTiff).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('tiff', metadata.format);
assert.strictEqual(2464, metadata.width);
assert.strictEqual(3248, metadata.height);
assert.strictEqual('b-w', metadata.space);
assert.strictEqual(1, metadata.channels);
assert.strictEqual(false, metadata.hasAlpha);
done();
});
});
it('PNG', function(done) {
sharp(fixtures.inputPng).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('png', metadata.format);
assert.strictEqual(2809, metadata.width);
assert.strictEqual(2074, metadata.height);
assert.strictEqual('b-w', metadata.space);
assert.strictEqual(1, metadata.channels);
assert.strictEqual(false, metadata.hasAlpha);
done();
});
});
it('Transparent PNG', function(done) {
sharp(fixtures.inputPngWithTransparency).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('png', metadata.format);
assert.strictEqual(2048, metadata.width);
assert.strictEqual(1536, metadata.height);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(4, metadata.channels);
assert.strictEqual(true, metadata.hasAlpha);
done();
});
});
it('WebP', function(done) {
sharp(fixtures.inputWebP).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('webp', metadata.format);
assert.strictEqual(1024, metadata.width);
assert.strictEqual(772, metadata.height);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels);
assert.strictEqual(false, metadata.hasAlpha);
done();
});
});
it('GIF via libmagick', function(done) {
sharp(fixtures.inputGif).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('magick', metadata.format);
assert.strictEqual(800, metadata.width);
assert.strictEqual(533, metadata.height);
assert.strictEqual(3, metadata.channels);
assert.strictEqual(false, metadata.hasAlpha);
done();
});
});
it('File in, Promise out', function(done) {
sharp(fixtures.inputJpg).metadata().then(function(metadata) {
assert.strictEqual('jpeg', metadata.format);
assert.strictEqual(2725, metadata.width);
assert.strictEqual(2225, metadata.height);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels);
assert.strictEqual(false, metadata.hasAlpha);
done();
});
});
it('Stream in, Promise out', function(done) {
var readable = fs.createReadStream(fixtures.inputJpg);
var pipeline = sharp();
pipeline.metadata().then(function(metadata) {
assert.strictEqual('jpeg', metadata.format);
assert.strictEqual(2725, metadata.width);
assert.strictEqual(2225, metadata.height);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels);
assert.strictEqual(false, metadata.hasAlpha);
done();
}).catch(function(err) {
throw err;
});
readable.pipe(pipeline);
});
it('Stream', function(done) {
var readable = fs.createReadStream(fixtures.inputJpg);
var pipeline = sharp().metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('jpeg', metadata.format);
assert.strictEqual(2725, metadata.width);
assert.strictEqual(2225, metadata.height);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels);
assert.strictEqual(false, metadata.hasAlpha);
done();
});
readable.pipe(pipeline);
});
it('Resize to half width using metadata', function(done) {
var image = sharp(fixtures.inputJpg);
image.metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('jpeg', metadata.format);
assert.strictEqual(2725, metadata.width);
assert.strictEqual(2225, metadata.height);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels);
assert.strictEqual(false, metadata.hasAlpha);
image.resize(metadata.width / 2).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual(1362, info.width);
assert.strictEqual(1112, info.height);
done();
});
});
});
it('Keep EXIF metadata after a resize', function(done) {
sharp(fixtures.inputJpgWithExif).resize(320, 240).withMetadata().toBuffer(function(err, buffer) {
if (err) throw err;
sharp(buffer).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual(8, metadata.orientation);
done();
});
});
});
it('Remove EXIF metadata after a resize', function(done) {
sharp(fixtures.inputJpgWithExif).resize(320, 240).withMetadata(false).toBuffer(function(err, buffer) {
if (err) throw err;
sharp(buffer).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('undefined', typeof metadata.orientation);
done();
});
});
});
});

184
test/unit/resize.js Executable file
View File

@@ -0,0 +1,184 @@
'use strict';
var assert = require('assert');
var sharp = require('../../index');
var fixtures = require('../fixtures');
describe('Resize dimensions', function() {
it('Exact crop', function(done) {
sharp(fixtures.inputJpg).resize(320, 240).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
it('Fixed width', function(done) {
sharp(fixtures.inputJpg).resize(320).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(261, info.height);
done();
});
});
it('Fixed height', function(done) {
sharp(fixtures.inputJpg).resize(null, 320).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(391, info.width);
assert.strictEqual(320, info.height);
done();
});
});
it('Identity transform', function(done) {
sharp(fixtures.inputJpg).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(2725, info.width);
assert.strictEqual(2225, info.height);
done();
});
});
it('Upscale', function(done) {
sharp(fixtures.inputJpg).resize(3000).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3000, info.width);
assert.strictEqual(2449, info.height);
done();
});
});
it('Invalid width', function(done) {
var isValid = true;
try {
sharp(fixtures.inputJpg).resize('spoons', 240);
} catch (err) {
isValid = false;
}
assert.strictEqual(false, isValid);
done();
});
it('Invalid height', function(done) {
var isValid = true;
try {
sharp(fixtures.inputJpg).resize(320, 'spoons');
} catch (err) {
isValid = false;
}
assert.strictEqual(false, isValid);
done();
});
it('TIFF embed known to cause rounding errors', function(done) {
sharp(fixtures.inputTiff).resize(240, 320).embed().jpeg().toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(240, info.width);
assert.strictEqual(320, info.height);
done();
});
});
it('TIFF known to cause rounding errors', function(done) {
sharp(fixtures.inputTiff).resize(240, 320).jpeg().toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(240, info.width);
assert.strictEqual(320, info.height);
done();
});
});
it('Max width or height considering ratio (landscape)', function(done) {
sharp(fixtures.inputJpg).resize(320, 320).max().toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(261, info.height);
done();
});
});
it('Max width or height considering ratio (portrait)', function(done) {
sharp(fixtures.inputTiff).resize(320, 320).max().jpeg().toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(243, info.width);
assert.strictEqual(320, info.height);
done();
});
});
it('Provide only one dimension with max, should default to crop', function(done) {
sharp(fixtures.inputJpg).resize(320).max().toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(261, info.height);
done();
});
});
it('Do not enlarge when input width is already less than output width', function(done) {
sharp(fixtures.inputJpg)
.resize(2800)
.withoutEnlargement()
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(2725, info.width);
assert.strictEqual(2225, info.height);
done();
});
});
it('Do not enlarge when input height is already less than output height', function(done) {
sharp(fixtures.inputJpg)
.resize(null, 2300)
.withoutEnlargement()
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(2725, info.width);
assert.strictEqual(2225, info.height);
done();
});
});
it('Do enlarge when input width is less than output width', function(done) {
sharp(fixtures.inputJpg)
.resize(2800)
.withoutEnlargement(false)
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(2800, info.width);
assert.strictEqual(2286, info.height);
done();
});
});
});

134
test/unit/rotate.js Executable file
View File

@@ -0,0 +1,134 @@
'use strict';
var assert = require('assert');
var sharp = require('../../index');
var fixtures = require('../fixtures');
describe('Rotation', function() {
it('Rotate by 90 degrees, respecting output input size', function(done) {
sharp(fixtures.inputJpg).rotate(90).resize(320, 240).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
it('Input image has Orientation EXIF tag but do not rotate output', function(done) {
sharp(fixtures.inputJpgWithExif)
.resize(320)
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(426, info.height);
done();
});
});
it('Input image has Orientation EXIF tag value of 8 (270 degrees), auto-rotate', function(done) {
sharp(fixtures.inputJpgWithExif)
.rotate()
.resize(320)
.toFile(fixtures.path('output.exif.8.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
it('Input image has Orientation EXIF tag value of 5 (270 degrees + flip), auto-rotate', function(done) {
sharp(fixtures.inputJpgWithExifMirroring)
.rotate()
.resize(320)
.toFile(fixtures.path('output.exif.5.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
it('Attempt to auto-rotate using image that has no EXIF', function(done) {
sharp(fixtures.inputJpg).rotate().resize(320).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(261, info.height);
done();
});
});
it('Rotate to an invalid angle, should fail', function(done) {
var fail = false;
try {
sharp(fixtures.inputJpg).rotate(1);
fail = true;
} catch (e) {}
assert(!fail);
done();
});
it('Flip - vertical', function(done) {
sharp(fixtures.inputJpg)
.resize(320)
.flip()
.toFile(fixtures.path('output.flip.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(261, info.height);
done();
});
});
it('Flop - horizontal', function(done) {
sharp(fixtures.inputJpg)
.resize(320)
.flop()
.toFile(fixtures.path('output.flop.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(261, info.height);
done();
});
});
it('Flip and flop', function(done) {
sharp(fixtures.inputJpg)
.resize(320)
.flop()
.toFile(fixtures.path('output.flip.flop.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(261, info.height);
done();
});
});
it('Neither flip nor flop', function(done) {
sharp(fixtures.inputJpg)
.resize(320)
.flip(false)
.flop(false)
.toFile(fixtures.path('output.flip.flop.nope.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(261, info.height);
done();
});
});
});

34
test/unit/sharpen.js Executable file
View File

@@ -0,0 +1,34 @@
'use strict';
var assert = require('assert');
var sharp = require('../../index');
var fixtures = require('../fixtures');
describe('Sharpen', function() {
it('sharpen image is larger than non-sharpen', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 240)
.sharpen(false)
.toBuffer(function(err, notSharpened, info) {
if (err) throw err;
assert.strictEqual(true, notSharpened.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
sharp(notSharpened)
.sharpen()
.toBuffer(function(err, sharpened, info) {
if (err) throw err;
assert.strictEqual(true, sharpened.length > 0);
assert.strictEqual(true, sharpened.length > notSharpened.length);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
});
});

53
test/unit/util.js Executable file
View File

@@ -0,0 +1,53 @@
'use strict';
var assert = require('assert');
var sharp = require('../../index');
var defaultConcurrency = sharp.concurrency();
describe('Utilities', function() {
describe('Cache', function() {
it('Can be disabled', function() {
var cache = sharp.cache(0, 0);
assert.strictEqual(0, cache.memory);
assert.strictEqual(0, cache.items);
});
it('Can be set to a maximum of 50MB and 500 items', function() {
var cache = sharp.cache(50, 500);
assert.strictEqual(50, cache.memory);
assert.strictEqual(500, cache.items);
});
it('Ignores invalid values', function() {
sharp.cache(50, 500);
var cache = sharp.cache('spoons');
assert.strictEqual(50, cache.memory);
assert.strictEqual(500, cache.items);
});
});
describe('Concurrency', function() {
it('Can be set to use 16 threads', function() {
sharp.concurrency(16);
assert.strictEqual(16, sharp.concurrency());
});
it('Can be reset to default', function() {
sharp.concurrency(0);
assert.strictEqual(defaultConcurrency, sharp.concurrency());
});
it('Ignores invalid values', function() {
sharp.concurrency(0);
sharp.concurrency('spoons');
assert.strictEqual(defaultConcurrency, sharp.concurrency());
});
});
describe('Counters', function() {
it('Have zero value at rest', function() {
var counters = sharp.counters();
assert.strictEqual(0, counters.queue);
assert.strictEqual(0, counters.process);
});
});
});

View File

@@ -1,44 +0,0 @@
var sharp = require("../index");
var imagemagick = require("imagemagick");
var assert = require("assert");
var Benchmark = require("benchmark");
var input = __dirname + "/2569067123_aca715a2ee_o.jpg"; // http://www.flickr.com/photos/grizdave/2569067123/
var output = __dirname + "/output.jpg";
var width = 640;
var height = 480;
var suite = new Benchmark.Suite;
suite.add("imagemagick", {
"defer": true,
"fn": function(deferred) {
imagemagick.resize({
srcPath: input,
dstPath: output,
quality: 0.75,
width: width,
height: height
}, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add("sharp", {
"defer": true,
"fn": function(deferred) {
sharp.crop(input, output, width, height, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).on("cycle", function(event) {
console.log(String(event.target));
}).on("complete", function() {
assert(this.filter("fastest").pluck("name") == "sharp");
}).run();