Compare commits

...

46 Commits

Author SHA1 Message Date
Lovell Fuller
c610e306df Release v0.23.0 2019-07-29 14:45:46 +01:00
Lovell Fuller
417cca6e0d Use libvips built-in ICC profiles when required #1619 2019-07-29 14:16:21 +01:00
Lovell Fuller
2ed4b5ae83 Provide guidance on (mis)use of sudo with npm install 2019-07-29 11:48:18 +01:00
Lovell Fuller
16e429cf2c Add a few new leak test suppressions 2019-07-29 11:34:20 +01:00
Lovell Fuller
6b7ce8a605 Force V8 GC after each test during leak checks 2019-07-29 11:33:45 +01:00
Lovell Fuller
ba4ce75377 Ensure all WebP tests wait until Promises resolve 2019-07-29 11:32:31 +01:00
Lovell Fuller
76ded7fd28 Changelog entry and credit for #1755 2019-07-28 17:39:30 +01:00
Lovell Fuller
a0d1a7be50 Upgrade to libvips v8.8.1 2019-07-28 10:51:09 +01:00
Ilya Ovdin
690bc43abe Fix rotate/extract ordering for non-90 angles (#1755) 2019-07-26 20:28:45 +01:00
Lovell Fuller
50b461024d Add test coverage for single value extend operation 2019-07-26 19:31:14 +01:00
Lovell Fuller
6cf0b3240d Simplify 'this' in IO pipeline using arrow functions 2019-07-26 19:19:21 +01:00
Lovell Fuller
233b015d77 Improve consistency of validation error handling
Utilises common path of existing invalidParameterError
2019-07-26 14:58:54 +01:00
Lovell Fuller
28de243c11 Bump dependencies 2019-07-14 23:01:34 +01:00
Lovell Fuller
36e8a3da88 Expose libwebp smartSubsample and reductionEffort #1545 2019-07-14 22:52:38 +01:00
Lovell Fuller
119d16cad3 Ignore test coverage on more esoteric code paths 2019-07-14 21:52:29 +01:00
Lovell Fuller
38402d3185 Changelog entry for #1687 2019-07-12 12:15:24 +01:00
RaboliotTheGrey
6c02949fc1 Add skipBlanks support for tile layout (#1687) 2019-07-12 12:02:51 +01:00
Lovell Fuller
b737d4601e Add experimental support for HEIF images #1105
Requires a custom, globally-installed libvips compiled with libheif
2019-07-04 13:21:32 +01:00
jwater7
3ff3353550 Docs: vips is now available via the Alpine community repo (#1769) 2019-07-01 12:08:11 +01:00
Lovell Fuller
946d3c81a5 Add experimental support for Worker Threads #1558 2019-06-26 21:15:04 +01:00
Lovell Fuller
628996846d Allow use of failOnError with Stream-based input #1691 2019-06-26 19:37:27 +01:00
Lovell Fuller
631a3597c7 Upgrade to libvips v8.8.0, remove deprecated overlayWith 2019-06-26 18:32:53 +01:00
Marc Bornträger
cfa4f7d45c Docs: Alpine now provides vips package via community repo (#1730) 2019-05-30 09:02:32 +01:00
Lovell Fuller
05d76eeadf Release v0.22.1 2019-04-25 12:14:45 +01:00
Lovell Fuller
28a6c53da0 Prevent issue templates being published to npm 2019-04-25 12:14:17 +01:00
Lovell Fuller
6fcd2153c5 Docs: add support for Node 12 #1668 2019-04-25 12:01:14 +01:00
Lovell Fuller
7ae0512b9b Docs: clarify some output features require custom libvips 2019-04-25 11:39:30 +01:00
Nicolas Stepien
0890b59c32 Add Node 12 to Windows CI (#1669) 2019-04-25 11:24:35 +01:00
Lovell Fuller
df3ce450d9 Bump all dependencies 2019-04-25 09:51:59 +01:00
Lovell Fuller
bb0257b318 Remove not-yet-available Node 12 from Windows CI 2019-04-25 09:51:03 +01:00
Lovell Fuller
9c3597670d Add Node 12 to CI 2019-04-25 08:42:13 +01:00
Lovell Fuller
aa9b328778 Remove use of deprecated V8 API, swap v8::Handle for v8::Local #1668 2019-04-23 20:16:31 +01:00
tyankatsu
159e8dace2 docs: Fix path to CONTRIBUTING.md (#1666) 2019-04-20 19:19:14 +01:00
Lovell Fuller
3be4d5bb45 Ensure limitInputPixels check uses 64-bit unsigned type 2019-04-20 17:51:19 +01:00
Lovell Fuller
af7caa7b25 Docs: modernise contributing guide 2019-04-02 22:33:24 +01:00
Lovell Fuller
b4ede75522 Add issue templates (#1639) 2019-04-02 22:32:06 +01:00
Lovell Fuller
9d98114074 Move contributing info to GitHub-specific subdirectory,
which then allows for future possible issue/PR templates etc.
2019-04-02 21:03:57 +01:00
Lovell Fuller
4ac51899c3 Docs: minimum supported version of Node.js is 6 2019-04-02 20:41:23 +01:00
Lovell Fuller
90a0382317 Tests: use a concurrency of 1 on musl-based Linux
Should reduce a bit of pressure on the stack
2019-04-02 20:40:36 +01:00
Lovell Fuller
687795c801 Enhancement to and changelog entry for #1638
Remove bindings dependency as this isn't really... required
2019-04-02 20:39:06 +01:00
Sidhartha Chatterjee
2e0fbbb942 Add warning if sharp bindings aren't built correctly (#1638) 2019-04-02 17:00:55 +01:00
Lovell Fuller
0a3512d066 Unpin prebuild as 8.2.x is no longer breaking 2019-04-01 20:51:45 +01:00
Lovell Fuller
6032171f91 Docs: clarify use of integral pixel values for extract 2019-04-01 20:06:46 +01:00
Lovell Fuller
fc178de309 Changelog entry for #1601 2019-03-25 08:32:20 +00:00
Jakub Michálek
3f4398457f Change docs for composite to reflect how create works (#1623) 2019-03-25 07:45:46 +00:00
Jakub Michálek
b494b2e872 Add brightness, saturation and hue modulation #609 (#1601) 2019-03-25 07:44:07 +00:00
104 changed files with 4267 additions and 3855 deletions

View File

@@ -12,9 +12,12 @@ New bugs are assigned a `triage` label whilst under investigation.
## Submit a new feature request ## Submit a new feature request
If a [similar request](https://github.com/lovell/sharp/labels/enhancement) exists, it's probably fastest to add a comment to it about your requirement. If a [similar request](https://github.com/lovell/sharp/labels/enhancement) exists,
it's probably fastest to add a comment to it about your requirement.
Implementation is usually straightforward if _libvips_ [already supports](https://libvips.github.io/libvips/API/current/) the feature you need. Implementation is usually straightforward if libvips
[already supports](https://libvips.github.io/libvips/API/current/func-list.html)
the feature you need.
## Submit a Pull Request to fix a bug ## Submit a Pull Request to fix a bug
@@ -41,18 +44,18 @@ Any change that modifies the existing public API should be added to the relevant
| Release | WIP branch | | Release | WIP branch |
| ------: | :--------- | | ------: | :--------- |
| v0.22.0 | uptake | | v0.24.0 | wit |
| v0.23.0 | vision | | v0.25.0 | yield |
Please squash your changes into a single commit using a command like `git rebase -i upstream/<wip-branch>`. Please squash your changes into a single commit using a command like `git rebase -i upstream/<wip-branch>`.
### Add a new public method ### Add a new public method
The API tries to be as fluent as possible. Image processing concepts follow the naming conventions from _libvips_ and, to a lesser extent, _ImageMagick_. The API tries to be as fluent as possible.
Image processing concepts follow the naming conventions from libvips and, to a lesser extent, ImageMagick.
Most methods have optional parameters and assume sensible defaults. Methods with mandatory parameters often have names like `doSomethingWith(X)`. Most methods have optional parameters and assume sensible defaults.
Please ensure backwards compatibility where possible.
Please ensure backwards compatibility where possible. Methods to modify previously default behaviour often have names like `withoutOptionY()` or `withExtraZ()`.
Feel free to create a [new issue](https://github.com/lovell/sharp/issues/new) to gather feedback on a potential API change. Feel free to create a [new issue](https://github.com/lovell/sharp/issues/new) to gather feedback on a potential API change.
@@ -60,7 +63,7 @@ Feel free to create a [new issue](https://github.com/lovell/sharp/issues/new) to
A method to be removed should be deprecated in the next major version then removed in the following major version. A method to be removed should be deprecated in the next major version then removed in the following major version.
By way of example, the [bilinearInterpolation method](https://github.com/lovell/sharp/blob/v0.6.0/index.js#L155) present in v0.5.0 was deprecated in v0.6.0 and removed in v0.7.0. By way of example, the `background()` method present in v0.20.0 was deprecated in v0.21.0 and removed in v0.22.0.
## Documentation ## Documentation
@@ -95,5 +98,5 @@ Please feel free to ask any questions via a
[new issue](https://github.com/lovell/sharp/issues/new). [new issue](https://github.com/lovell/sharp/issues/new).
If you're unable to post details publicly, please If you're unable to post details publicly, please
[e-mail](https://github.com/lovell/sharp/blob/master/package.json#L4) [e-mail](https://github.com/lovell/sharp/blob/master/package.json#L5)
for private, paid consulting. for private, paid consulting.

View File

@@ -0,0 +1,18 @@
---
name: Feature request
about: Suggest an idea
title: ''
labels: enhancement
assignees: ''
---
What are you trying to achieve?
Have you searched for similar feature requests?
What would you expect the API to look like?
What alternatives have you considered?
Is there a sample image that helps explain?

16
.github/ISSUE_TEMPLATE/installation.md vendored Normal file
View File

@@ -0,0 +1,16 @@
---
name: Installation
about: For help if something went wrong installing sharp
title: ''
labels: ''
assignees: ''
---
What is the output of running `npm install --verbose sharp`?
What is the output of running `npx envinfo --binaries --languages --system --utilities`?
Have you ensured the platform and version of Node.js used for `npm install` is the same as the platform and version of Node.js used at runtime?
If you're using `sudo npm install` have you tried with the `sudo npm install --unsafe-perm` flag?

18
.github/ISSUE_TEMPLATE/possible-bug.md vendored Normal file
View File

@@ -0,0 +1,18 @@
---
name: Possible bug
about: Please provide steps to reproduce
title: ''
labels: ''
assignees: ''
---
What is the output of running `npx envinfo --binaries --languages --system --utilities`?
What are the steps to reproduce?
What is the expected behaviour?
Are you able to provide a standalone code sample, without other dependencies, that demonstrates this problem?
Are you able to provide a sample image that helps explain the problem?

16
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View File

@@ -0,0 +1,16 @@
---
name: Question
about: For help with an existing feature
title: ''
labels: question
assignees: ''
---
What are you trying to achieve?
Have you searched for similar questions?
Are you able to provide a standalone code sample that demonstrates this question?
Are you able to provide a sample image that helps explain the question?

1
.gitignore vendored
View File

@@ -12,4 +12,5 @@ vendor
.gitattributes .gitattributes
.DS_Store .DS_Store
.nyc_output .nyc_output
.vscode/
package-lock.json package-lock.json

View File

@@ -12,4 +12,5 @@ docs/css/
vendor vendor
.prebuildrc .prebuildrc
.nyc_output .nyc_output
CONTRIBUTING.md .github/
.vscode/

View File

@@ -1,11 +1,5 @@
matrix: matrix:
include: include:
- name: "Linux (glibc) - Node 6"
os: linux
dist: trusty
sudo: false
language: node_js
node_js: "6"
- name: "Linux (glibc) - Node 8" - name: "Linux (glibc) - Node 8"
os: linux os: linux
dist: trusty dist: trusty
@@ -18,15 +12,15 @@ matrix:
sudo: false sudo: false
language: node_js language: node_js
node_js: "10" node_js: "10"
after_success: - name: "Linux (glibc) - Node 12"
- npm install coveralls
- cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js
- name: "Linux (glibc) - Node 11"
os: linux os: linux
dist: trusty dist: trusty
sudo: false sudo: false
language: node_js language: node_js
node_js: "11" node_js: "12"
after_success:
- npm install coveralls
- cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js
- name: "Linux (musl) - Node 8" - name: "Linux (musl) - Node 8"
os: linux os: linux
dist: trusty dist: trusty
@@ -47,21 +41,16 @@ matrix:
- sudo docker exec sharp apk add build-base git python2 --update-cache - sudo docker exec sharp apk add build-base git python2 --update-cache
install: sudo docker exec sharp sh -c "npm install --unsafe-perm" install: sudo docker exec sharp sh -c "npm install --unsafe-perm"
script: sudo docker exec sharp sh -c "npm test" script: sudo docker exec sharp sh -c "npm test"
- name: "Linux (musl) - Node 11" - name: "Linux (musl) - Node 12"
os: linux os: linux
dist: trusty dist: trusty
sudo: true sudo: true
language: minimal language: minimal
before_install: before_install:
- sudo docker run -dit --name sharp --env CI --env PREBUILD_TOKEN --volume "${PWD}:/mnt/sharp" --workdir /mnt/sharp node:11-alpine - sudo docker run -dit --name sharp --env CI --env PREBUILD_TOKEN --volume "${PWD}:/mnt/sharp" --workdir /mnt/sharp node:12.0-alpine
- sudo docker exec sharp apk add build-base git python2 --update-cache - sudo docker exec sharp apk add build-base git python2 --update-cache
install: sudo docker exec sharp sh -c "npm install --unsafe-perm" install: sudo docker exec sharp sh -c "npm install --unsafe-perm"
script: sudo docker exec sharp sh -c "npm test" script: sudo docker exec sharp sh -c "npm test"
- name: "OS X - Node 6"
os: osx
osx_image: xcode9.2
language: node_js
node_js: "6"
- name: "OS X - Node 8" - name: "OS X - Node 8"
os: osx os: osx
osx_image: xcode9.2 osx_image: xcode9.2
@@ -72,8 +61,8 @@ matrix:
osx_image: xcode9.2 osx_image: xcode9.2
language: node_js language: node_js
node_js: "10" node_js: "10"
- name: "OS X - Node 11" - name: "OS X - Node 12"
os: osx os: osx
osx_image: xcode9.2 osx_image: xcode9.2
language: node_js language: node_js
node_js: "11" node_js: "12"

View File

@@ -20,7 +20,7 @@ As well as image resizing, operations such as
rotation, extraction, compositing and gamma correction are available. rotation, extraction, compositing and gamma correction are available.
Most modern 64-bit OS X, Windows and Linux systems running Most modern 64-bit OS X, Windows and Linux systems running
Node versions 6, 8, 10 and 11 Node versions 8, 10 and 12
do not require any additional install or runtime dependencies. do not require any additional install or runtime dependencies.
## Examples ## Examples
@@ -96,7 +96,7 @@ Visit [sharp.pixelplumbing.com](https://sharp.pixelplumbing.com/) for complete
### Contributing ### Contributing
A [guide for contributors](https://github.com/lovell/sharp/blob/master/CONTRIBUTING.md) A [guide for contributors](https://github.com/lovell/sharp/blob/master/.github/CONTRIBUTING.md)
covers reporting bugs, requesting features and submitting code changes. covers reporting bugs, requesting features and submitting code changes.
### Licensing ### Licensing

View File

@@ -4,13 +4,12 @@ build: off
platform: x64 platform: x64
environment: environment:
matrix: matrix:
- nodejs_version: "6"
- nodejs_version: "8" - nodejs_version: "8"
- nodejs_version: "10" - nodejs_version: "10"
- nodejs_version: "11" - nodejs_version: "12"
install: install:
- ps: Install-Product node $env:nodejs_version x64 - ps: Update-NodeJsInstallation (Get-NodeJsLatestBuild $env:nodejs_version) x64
- npm install -g npm@5 - npm install -g npm@6
- npm install - npm install
test_script: test_script:
- npm test - npm test

View File

@@ -97,7 +97,8 @@
'conditions': [ 'conditions': [
['OS == "win"', { ['OS == "win"', {
'defines': [ 'defines': [
'_ALLOW_KEYWORD_MACROS' '_ALLOW_KEYWORD_MACROS',
'_FILE_OFFSET_BITS=64'
], ],
'libraries': [ 'libraries': [
'../vendor/lib/libvips.lib', '../vendor/lib/libvips.lib',
@@ -183,22 +184,15 @@
'configurations': { 'configurations': {
'Release': { 'Release': {
'cflags_cc': [ 'cflags_cc': [
'-Wno-cast-function-type', '-Wno-cast-function-type'
'-Wno-deprecated-declarations'
], ],
'xcode_settings': {
'OTHER_CPLUSPLUSFLAGS': [
'-Wno-deprecated-declarations'
]
},
'msvs_settings': { 'msvs_settings': {
'VCCLCompilerTool': { 'VCCLCompilerTool': {
'ExceptionHandling': 1 'ExceptionHandling': 1
} }
}, },
'msvs_disabled_warnings': [ 'msvs_disabled_warnings': [
4275, 4275
4996
] ]
} }
}, },

View File

@@ -20,22 +20,22 @@ and [https://www.cairographics.org/operators/][2]
### Parameters ### Parameters
- `images` **[Array][3]&lt;[Object][4]>** Ordered list of images to composite - `images` **[Array][3]&lt;[Object][4]>** Ordered list of images to composite
- `images[].input` **([Buffer][5] \| [String][6])?** Buffer containing image data or String containing the path to an image file. - `images[].input` **([Buffer][5] \| [String][6])?** Buffer containing image data, String containing the path to an image file, or Create object (see bellow)
- `images[].input.create` **[Object][4]?** describes a blank overlay to be created.
- `images[].input.create.width` **[Number][7]?**
- `images[].input.create.height` **[Number][7]?**
- `images[].input.create.channels` **[Number][7]?** 3-4
- `images[].input.create.background` **([String][6] \| [Object][4])?** parsed by the [color][8] module to extract values for red, green, blue and alpha.
- `images[].blend` **[String][6]** how to blend this image with the image below. (optional, default `'over'`) - `images[].blend` **[String][6]** how to blend this image with the image below. (optional, default `'over'`)
- `images[].gravity` **[String][6]** gravity at which to place the overlay. (optional, default `'centre'`) - `images[].gravity` **[String][6]** gravity at which to place the overlay. (optional, default `'centre'`)
- `images[].top` **[Number][7]?** the pixel offset from the top edge. - `images[].top` **[Number][7]?** the pixel offset from the top edge.
- `images[].left` **[Number][7]?** the pixel offset from the left edge. - `images[].left` **[Number][7]?** the pixel offset from the left edge.
- `images[].tile` **[Boolean][8]** set to true to repeat the overlay image across the entire image with the given `gravity`. (optional, default `false`) - `images[].tile` **[Boolean][9]** set to true to repeat the overlay image across the entire image with the given `gravity`. (optional, default `false`)
- `images[].density` **[Number][7]** number representing the DPI for vector overlay image. (optional, default `72`) - `images[].density` **[Number][7]** number representing the DPI for vector overlay image. (optional, default `72`)
- `images[].raw` **[Object][4]?** describes overlay when using raw pixel data. - `images[].raw` **[Object][4]?** describes overlay when using raw pixel data.
- `images[].raw.width` **[Number][7]?** - `images[].raw.width` **[Number][7]?**
- `images[].raw.height` **[Number][7]?** - `images[].raw.height` **[Number][7]?**
- `images[].raw.channels` **[Number][7]?** - `images[].raw.channels` **[Number][7]?**
- `images[].create` **[Object][4]?** describes a blank overlay to be created.
- `images[].create.width` **[Number][7]?**
- `images[].create.height` **[Number][7]?**
- `images[].create.channels` **[Number][7]?** 3-4
- `images[].create.background` **([String][6] \| [Object][4])?** parsed by the [color][9] module to extract values for red, green, blue and alpha.
### Examples ### Examples
@@ -74,8 +74,8 @@ Returns **Sharp**
[7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number [7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean [8]: https://www.npmjs.org/package/color
[9]: https://www.npmjs.org/package/color [9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error [10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error

View File

@@ -34,8 +34,9 @@ A `Promise` is returned when `callback` is not provided.
- `density`: Number of pixels per inch (DPI), if present - `density`: Number of pixels per inch (DPI), if present
- `chromaSubsampling`: String containing JPEG chroma subsampling, `4:2:0` or `4:4:4` for RGB, `4:2:0:4` or `4:4:4:4` for CMYK - `chromaSubsampling`: String containing JPEG chroma subsampling, `4:2:0` or `4:4:4` for RGB, `4:2:0:4` or `4:4:4:4` for CMYK
- `isProgressive`: Boolean indicating whether the image is interlaced using a progressive scan - `isProgressive`: Boolean indicating whether the image is interlaced using a progressive scan
- `pages`: Number of pages/frames contained within the image, with support for TIFF, PDF, animated GIF and animated WebP - `pages`: Number of pages/frames contained within the image, with support for TIFF, HEIF, PDF, animated GIF and animated WebP
- `pageHeight`: Number of pixels high each page in this PDF image will be. - `pageHeight`: Number of pixels high each page in this PDF image will be.
- `pagePrimary`: Number of the primary page in a HEIF image
- `hasProfile`: Boolean indicating the presence of an embedded ICC profile - `hasProfile`: Boolean indicating the presence of an embedded ICC profile
- `hasAlpha`: Boolean indicating the presence of an alpha transparency channel - `hasAlpha`: Boolean indicating the presence of an alpha transparency channel
- `orientation`: Number value of the EXIF Orientation header, if present - `orientation`: Number value of the EXIF Orientation header, if present

View File

@@ -287,6 +287,41 @@ sharp(input)
Returns **Sharp** Returns **Sharp**
## modulate
Transforms the image using brightness, saturation and hue rotation.
### Parameters
- `options` **[Object][2]?**
- `options.brightness` **[Number][1]?** Brightness multiplier
- `options.saturation` **[Number][1]?** Saturation multiplier
- `options.hue` **[Number][1]?** Degrees for hue rotation
### Examples
```javascript
sharp(input)
.modulate({
brightness: 2 // increase lightness by a factor of 2
});
sharp(input)
.modulate({
hue: 180 // hue-rotate by 180 degrees
});
// decreate brightness and saturation while also hue-rotating by 90 degrees
sharp(input)
.modulate({
brightness: 0.5,
saturation: 0.5,
hue: 90
});
```
Returns **Sharp**
[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number [1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object [2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object

View File

@@ -89,8 +89,8 @@ This will also convert to and add a web-friendly sRGB ICC profile.
### Parameters ### Parameters
- `withMetadata` **[Object][5]?** - `options` **[Object][5]?**
- `withMetadata.orientation` **[Number][8]?** value between 1 and 8, used to update the EXIF `Orientation` tag. - `options.orientation` **[Number][8]?** value between 1 and 8, used to update the EXIF `Orientation` tag.
### Examples ### Examples
@@ -115,13 +115,13 @@ Use these JPEG options for output image.
- `options.quality` **[Number][8]** quality, integer 1-100 (optional, default `80`) - `options.quality` **[Number][8]** quality, integer 1-100 (optional, default `80`)
- `options.progressive` **[Boolean][6]** use progressive (interlace) scan (optional, default `false`) - `options.progressive` **[Boolean][6]** use progressive (interlace) scan (optional, default `false`)
- `options.chromaSubsampling` **[String][1]** set to '4:4:4' to prevent chroma subsampling when quality &lt;= 90 (optional, default `'4:2:0'`) - `options.chromaSubsampling` **[String][1]** set to '4:4:4' to prevent chroma subsampling when quality &lt;= 90 (optional, default `'4:2:0'`)
- `options.trellisQuantisation` **[Boolean][6]** apply trellis quantisation, requires mozjpeg (optional, default `false`) - `options.trellisQuantisation` **[Boolean][6]** apply trellis quantisation, requires libvips compiled with support for mozjpeg (optional, default `false`)
- `options.overshootDeringing` **[Boolean][6]** apply overshoot deringing, requires mozjpeg (optional, default `false`) - `options.overshootDeringing` **[Boolean][6]** apply overshoot deringing, requires libvips compiled with support for mozjpeg (optional, default `false`)
- `options.optimiseScans` **[Boolean][6]** optimise progressive scans, forces progressive, requires mozjpeg (optional, default `false`) - `options.optimiseScans` **[Boolean][6]** optimise progressive scans, forces progressive, requires libvips compiled with support for mozjpeg (optional, default `false`)
- `options.optimizeScans` **[Boolean][6]** alternative spelling of optimiseScans (optional, default `false`) - `options.optimizeScans` **[Boolean][6]** alternative spelling of optimiseScans (optional, default `false`)
- `options.optimiseCoding` **[Boolean][6]** optimise Huffman coding tables (optional, default `true`) - `options.optimiseCoding` **[Boolean][6]** optimise Huffman coding tables (optional, default `true`)
- `options.optimizeCoding` **[Boolean][6]** alternative spelling of optimiseCoding (optional, default `true`) - `options.optimizeCoding` **[Boolean][6]** alternative spelling of optimiseCoding (optional, default `true`)
- `options.quantisationTable` **[Number][8]** quantization table to use, integer 0-8, requires mozjpeg (optional, default `0`) - `options.quantisationTable` **[Number][8]** quantization table to use, integer 0-8, requires libvips compiled with support for mozjpeg (optional, default `0`)
- `options.quantizationTable` **[Number][8]** alternative spelling of quantisationTable (optional, default `0`) - `options.quantizationTable` **[Number][8]** alternative spelling of quantisationTable (optional, default `0`)
- `options.force` **[Boolean][6]** force JPEG output, otherwise attempt to use input format (optional, default `true`) - `options.force` **[Boolean][6]** force JPEG output, otherwise attempt to use input format (optional, default `true`)
@@ -154,11 +154,11 @@ Indexed PNG input at 1, 2 or 4 bits per pixel is converted to 8 bits per pixel.
- `options.progressive` **[Boolean][6]** use progressive (interlace) scan (optional, default `false`) - `options.progressive` **[Boolean][6]** use progressive (interlace) scan (optional, default `false`)
- `options.compressionLevel` **[Number][8]** zlib compression level, 0-9 (optional, default `9`) - `options.compressionLevel` **[Number][8]** zlib compression level, 0-9 (optional, default `9`)
- `options.adaptiveFiltering` **[Boolean][6]** use adaptive row filtering (optional, default `false`) - `options.adaptiveFiltering` **[Boolean][6]** use adaptive row filtering (optional, default `false`)
- `options.palette` **[Boolean][6]** quantise to a palette-based image with alpha transparency support, requires libimagequant (optional, default `false`) - `options.palette` **[Boolean][6]** quantise to a palette-based image with alpha transparency support, requires libvips compiled with support for libimagequant (optional, default `false`)
- `options.quality` **[Number][8]** use the lowest number of colours needed to achieve given quality, requires libimagequant (optional, default `100`) - `options.quality` **[Number][8]** use the lowest number of colours needed to achieve given quality, requires libvips compiled with support for libimagequant (optional, default `100`)
- `options.colours` **[Number][8]** maximum number of palette entries, requires libimagequant (optional, default `256`) - `options.colours` **[Number][8]** maximum number of palette entries, requires libvips compiled with support for libimagequant (optional, default `256`)
- `options.colors` **[Number][8]** alternative spelling of `options.colours`, requires libimagequant (optional, default `256`) - `options.colors` **[Number][8]** alternative spelling of `options.colours`, requires libvips compiled with support for libimagequant (optional, default `256`)
- `options.dither` **[Number][8]** level of Floyd-Steinberg error diffusion, requires libimagequant (optional, default `1.0`) - `options.dither` **[Number][8]** level of Floyd-Steinberg error diffusion, requires libvips compiled with support for libimagequant (optional, default `1.0`)
- `options.force` **[Boolean][6]** force PNG output, otherwise attempt to use input format (optional, default `true`) - `options.force` **[Boolean][6]** force PNG output, otherwise attempt to use input format (optional, default `true`)
### Examples ### Examples
@@ -185,6 +185,8 @@ Use these WebP options for output image.
- `options.alphaQuality` **[Number][8]** quality of alpha layer, integer 0-100 (optional, default `100`) - `options.alphaQuality` **[Number][8]** quality of alpha layer, integer 0-100 (optional, default `100`)
- `options.lossless` **[Boolean][6]** use lossless compression mode (optional, default `false`) - `options.lossless` **[Boolean][6]** use lossless compression mode (optional, default `false`)
- `options.nearLossless` **[Boolean][6]** use near_lossless compression mode (optional, default `false`) - `options.nearLossless` **[Boolean][6]** use near_lossless compression mode (optional, default `false`)
- `options.smartSubsample` **[Boolean][6]** use high quality chroma subsampling (optional, default `false`)
- `options.reductionEffort` **[Number][8]** level of CPU effort to reduce file size, integer 0-6 (optional, default `4`)
- `options.force` **[Boolean][6]** force WebP output, otherwise attempt to use input format (optional, default `true`) - `options.force` **[Boolean][6]** force WebP output, otherwise attempt to use input format (optional, default `true`)
### Examples ### Examples
@@ -232,6 +234,29 @@ sharp('input.svg')
.then(info => { ... }); .then(info => { ... });
``` ```
- Throws **[Error][3]** Invalid options
Returns **Sharp**
## heif
Use these HEIF options for output image.
Support for HEIF (HEIC/AVIF) is experimental.
Do not use this in production systems.
Requires a custom, globally-installed libvips compiled with support for libheif.
Most versions of libheif support only the patent-encumbered HEVC compression format.
### Parameters
- `options` **[Object][5]?** output options
- `options.quality` **[Number][8]** quality, integer 1-100 (optional, default `80`)
- `options.compression` **[Boolean][6]** compression format: hevc, avc, jpeg, av1 (optional, default `'hevc'`)
- `options.lossless` **[Boolean][6]** use lossless compression (optional, default `false`)
- Throws **[Error][3]** Invalid options - Throws **[Error][3]** Invalid options
Returns **Sharp** Returns **Sharp**
@@ -283,13 +308,14 @@ Warning: multiple sharp instances concurrently producing tile output can expose
### Parameters ### Parameters
- `tile` **[Object][5]?** - `options` **[Object][5]?**
- `tile.size` **[Number][8]** tile size in pixels, a value between 1 and 8192. (optional, default `256`) - `options.size` **[Number][8]** tile size in pixels, a value between 1 and 8192. (optional, default `256`)
- `tile.overlap` **[Number][8]** tile overlap in pixels, a value between 0 and 8192. (optional, default `0`) - `options.overlap` **[Number][8]** tile overlap in pixels, a value between 0 and 8192. (optional, default `0`)
- `tile.angle` **[Number][8]** tile angle of rotation, must be a multiple of 90. (optional, default `0`) - `options.angle` **[Number][8]** tile angle of rotation, must be a multiple of 90. (optional, default `0`)
- `tile.depth` **[String][1]?** how deep to make the pyramid, possible values are `onepixel`, `onetile` or `one`, default based on layout. - `options.depth` **[String][1]?** how deep to make the pyramid, possible values are `onepixel`, `onetile` or `one`, default based on layout.
- `tile.container` **[String][1]** tile container, with value `fs` (filesystem) or `zip` (compressed file). (optional, default `'fs'`) - `options.skipBlanks` **[Number][8]** threshold to skip tile generation, a value 0 - 255 for 8-bit images or 0 - 65535 for 16-bit images (optional, default `-1`)
- `tile.layout` **[String][1]** filesystem layout, possible values are `dz`, `zoomify` or `google`. (optional, default `'dz'`) - `options.container` **[String][1]** tile container, with value `fs` (filesystem) or `zip` (compressed file). (optional, default `'fs'`)
- `options.layout` **[String][1]** filesystem layout, possible values are `dz`, `zoomify` or `google`. (optional, default `'dz'`)
### Examples ### Examples

View File

@@ -163,11 +163,11 @@ Extract a region of the image.
### Parameters ### Parameters
- `options` **[Object][9]** - `options` **[Object][9]** describes the region to extract using integral pixel values
- `options.left` **[Number][8]** zero-indexed offset from left edge - `options.left` **[Number][8]** zero-indexed offset from left edge
- `options.top` **[Number][8]** zero-indexed offset from top edge - `options.top` **[Number][8]** zero-indexed offset from top edge
- `options.width` **[Number][8]** dimension of extracted image - `options.width` **[Number][8]** width of region to extract
- `options.height` **[Number][8]** dimension of extracted image - `options.height` **[Number][8]** height of region to extract
### Examples ### Examples

View File

@@ -1,9 +1,56 @@
# Changelog # Changelog
### v0.23 - "*vision*"
Requires libvips v8.8.1.
#### v0.23.0 - 29<sup>th</sup> July 2019
* Remove `overlayWith` previously deprecated in v0.22.0.
* Add experimental support for HEIF images. Requires libvips compiled with libheif.
[#1105](https://github.com/lovell/sharp/issues/1105)
* Expose libwebp `smartSubsample` and `reductionEffort` options.
[#1545](https://github.com/lovell/sharp/issues/1545)
* Add experimental support for Worker Threads.
[#1558](https://github.com/lovell/sharp/issues/1558)
* Use libvips' built-in CMYK and sRGB profiles when required.
[#1619](https://github.com/lovell/sharp/issues/1619)
* Drop support for Node.js versions 6 and 11.
[#1674](https://github.com/lovell/sharp/issues/1674)
* Expose `skipBlanks` option for tile-based output.
[#1687](https://github.com/lovell/sharp/pull/1687)
[@RaboliotTheGrey](https://github.com/RaboliotTheGrey)
* Allow use of `failOnError` option with Stream-based input.
[#1691](https://github.com/lovell/sharp/issues/1691)
* Fix rotate/extract ordering for non-90 angles.
[#1755](https://github.com/lovell/sharp/pull/1755)
[@iovdin](https://github.com/iovdin)
### v0.22 - "*uptake*" ### v0.22 - "*uptake*"
Requires libvips v8.7.4. Requires libvips v8.7.4.
#### v0.22.1 - 25<sup>th</sup> April 2019
* Add `modulate` operation for brightness, saturation and hue.
[#1601](https://github.com/lovell/sharp/pull/1601)
[@Goues](https://github.com/Goues)
* Improve help messaging should `require("sharp")` fail.
[#1638](https://github.com/lovell/sharp/pull/1638)
[@sidharthachatterjee](https://github.com/sidharthachatterjee)
* Add support for Node 12.
[#1668](https://github.com/lovell/sharp/issues/1668)
#### v0.22.0 - 18<sup>th</sup> March 2019 #### v0.22.0 - 18<sup>th</sup> March 2019
* Remove functions previously deprecated in v0.21.0: * Remove functions previously deprecated in v0.21.0:

View File

@@ -16,7 +16,7 @@ As well as image resizing, operations such as
rotation, extraction, compositing and gamma correction are available. rotation, extraction, compositing and gamma correction are available.
Most modern 64-bit OS X, Windows and Linux systems running Most modern 64-bit OS X, Windows and Linux systems running
Node versions 6, 8 and 10 Node versions 8, 10 and 12
do not require any additional install or runtime dependencies. do not require any additional install or runtime dependencies.
[![Test Coverage](https://coveralls.io/repos/lovell/sharp/badge.png?branch=master)](https://coveralls.io/r/lovell/sharp?branch=master) [![Test Coverage](https://coveralls.io/repos/lovell/sharp/badge.png?branch=master)](https://coveralls.io/r/lovell/sharp?branch=master)
@@ -64,7 +64,7 @@ as [pngcrush](https://pmt.sourceforge.io/pngcrush/).
### Contributing ### Contributing
A [guide for contributors](https://github.com/lovell/sharp/blob/master/CONTRIBUTING.md) A [guide for contributors](https://github.com/lovell/sharp/blob/master/.github/CONTRIBUTING.md)
covers reporting bugs, requesting features and submitting code changes. covers reporting bugs, requesting features and submitting code changes.
### Credits ### Credits
@@ -124,6 +124,8 @@ the help and code contributions of the following people:
* [Julian Aubourg](https://github.com/jaubourg) * [Julian Aubourg](https://github.com/jaubourg)
* [Keith Belovay](https://github.com/fromkeith) * [Keith Belovay](https://github.com/fromkeith)
* [Michael B. Klein](https://github.com/mbklein) * [Michael B. Klein](https://github.com/mbklein)
* [Jakub Michálek](https://github.com/Goues)
* [Ilya Ovdin](https://github.com/iovdin)
Thank you! Thank you!

View File

@@ -10,12 +10,12 @@ yarn add sharp
## Prerequisites ## Prerequisites
* Node v4.5.0+ * Node.js v8.5.0+
### Building from source ### Building from source
Pre-compiled binaries for sharp are provided for use with Pre-compiled binaries for sharp are provided for use with
Node versions 6, 8, 10 and 11 on Node versions 8, 10 and 12 on
64-bit Windows, OS X and Linux platforms. 64-bit Windows, OS X and Linux platforms.
Sharp will be built from source at install time when: Sharp will be built from source at install time when:
@@ -36,15 +36,16 @@ Building from source requires:
[![Ubuntu 16.04 Build Status](https://travis-ci.org/lovell/sharp.png?branch=master)](https://travis-ci.org/lovell/sharp) [![Ubuntu 16.04 Build Status](https://travis-ci.org/lovell/sharp.png?branch=master)](https://travis-ci.org/lovell/sharp)
libvips and its dependencies are fetched and stored within `node_modules/sharp/vendor` during `npm install`. libvips and its dependencies are fetched and stored within `node_modules/sharp/vendor` during `npm install`.
This involves an automated HTTPS download of approximately 9MB. This involves an automated HTTPS download of approximately 10MB.
Most Linux-based (glibc, musl) operating systems running on x64 and ARMv6+ CPUs should "just work", e.g.: Most Linux-based (glibc, musl) operating systems running on x64 and ARMv6+ CPUs should "just work", e.g.:
* Debian 7+ * Debian 8+
* Ubuntu 14.04+ * Ubuntu 14.04+
* Centos 7+ * Red Hat Enterprise 7+
* Alpine 3.8+ (Node 8 and 10) * CentOS 7+
* Fedora * Alpine 3.10+
* Fedora 21+
* openSUSE 13.2+ * openSUSE 13.2+
* Archlinux * Archlinux
* Raspbian Jessie * Raspbian Jessie
@@ -61,7 +62,8 @@ and `LD_LIBRARY_PATH` at runtime.
This allows the use of newer versions of libvips with older versions of sharp. This allows the use of newer versions of libvips with older versions of sharp.
For 32-bit Intel CPUs and older Linux-based operating systems such as Centos 6, For 32-bit Intel CPUs and older Linux-based operating systems such as
those based on Red Hat Enterprise 6 (e.g. CentOS 6)
compiling libvips from source is recommended. compiling libvips from source is recommended.
[https://libvips.github.io/libvips/install.html#building-libvips-from-a-source-tarball](https://libvips.github.io/libvips/install.html#building-libvips-from-a-source-tarball) [https://libvips.github.io/libvips/install.html#building-libvips-from-a-source-tarball](https://libvips.github.io/libvips/install.html#building-libvips-from-a-source-tarball)
@@ -69,11 +71,11 @@ compiling libvips from source is recommended.
#### Alpine Linux #### Alpine Linux
libvips is available in the libvips is available in the
[testing repository](https://pkgs.alpinelinux.org/packages?name=vips-dev): [community repository](https://pkgs.alpinelinux.org/packages?name=vips-dev):
```sh ```sh
apk add vips-dev fftw-dev build-base --update-cache \ apk add vips-dev fftw-dev build-base --update-cache \
--repository https://alpine.global.ssl.fastly.net/alpine/edge/testing/ \ --repository https://alpine.global.ssl.fastly.net/alpine/edge/community/ \
--repository https://alpine.global.ssl.fastly.net/alpine/edge/main --repository https://alpine.global.ssl.fastly.net/alpine/edge/main
``` ```
@@ -97,7 +99,7 @@ that it can be located using `pkg-config --modversion vips-cpp`.
[![Windows x64 Build Status](https://ci.appveyor.com/api/projects/status/pgtul704nkhhg6sg)](https://ci.appveyor.com/project/lovell/sharp) [![Windows x64 Build Status](https://ci.appveyor.com/api/projects/status/pgtul704nkhhg6sg)](https://ci.appveyor.com/project/lovell/sharp)
libvips and its dependencies are fetched and stored within `node_modules\sharp\vendor` during `npm install`. libvips and its dependencies are fetched and stored within `node_modules\sharp\vendor` during `npm install`.
This involves an automated HTTPS download of approximately 14MB. This involves an automated HTTPS download of approximately 10MB.
If you are having issues during installation consider removing the directory If you are having issues during installation consider removing the directory
`C:\Users\[user]\AppData\Roaming\npm-cache\_libvips`. `C:\Users\[user]\AppData\Roaming\npm-cache\_libvips`.
@@ -150,7 +152,7 @@ docker pull tailor/docker-libvips
### AWS Lambda ### AWS Lambda
Set the Lambda runtime to Node.js 8.10. Set the Lambda runtime to `nodejs10.x`.
The binaries in the `node_modules` directory of the The binaries in the `node_modules` directory of the
[deployment package](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-create-deployment-pkg.html) [deployment package](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-create-deployment-pkg.html)
@@ -160,14 +162,14 @@ On non-Linux machines such as OS X and Windows run the following:
```sh ```sh
rm -rf node_modules/sharp rm -rf node_modules/sharp
npm install --arch=x64 --platform=linux --target=8.10.0 sharp npm install --arch=x64 --platform=linux --target=10.15.0 sharp
``` ```
Alternatively a Docker container closely matching the Lambda runtime can be used: Alternatively a Docker container closely matching the Lambda runtime can be used:
```sh ```sh
rm -rf node_modules/sharp rm -rf node_modules/sharp
docker run -v "$PWD":/var/task lambci/lambda:build-nodejs8.10 npm install sharp docker run -v "$PWD":/var/task lambci/lambda:build-nodejs10.x npm install sharp
``` ```
To get the best performance select the largest memory available. To get the best performance select the largest memory available.

View File

@@ -3,7 +3,6 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const copyFileSync = require('fs-copy-file-sync');
const libvips = require('../lib/libvips'); const libvips = require('../lib/libvips');
const npmLog = require('npmlog'); const npmLog = require('npmlog');
@@ -24,7 +23,7 @@ if (process.platform === 'win32') {
return /\.dll$/.test(filename); return /\.dll$/.test(filename);
}) })
.forEach(function (filename) { .forEach(function (filename) {
copyFileSync( fs.copyFileSync(
path.join(vendorLibDir, filename), path.join(vendorLibDir, filename),
path.join(buildReleaseDir, filename) path.join(buildReleaseDir, filename)
); );

View File

@@ -9,7 +9,6 @@ const npmLog = require('npmlog');
const semver = require('semver'); const semver = require('semver');
const simpleGet = require('simple-get'); const simpleGet = require('simple-get');
const tar = require('tar'); const tar = require('tar');
const copyFileSync = require('fs-copy-file-sync');
const agent = require('../lib/agent'); const agent = require('../lib/agent');
const libvips = require('../lib/libvips'); const libvips = require('../lib/libvips');
@@ -64,7 +63,7 @@ try {
if (platformAndArch === 'freebsd-x64' || platformAndArch === 'openbsd-x64' || platformAndArch === 'sunos-x64') { if (platformAndArch === 'freebsd-x64' || platformAndArch === 'openbsd-x64' || platformAndArch === 'sunos-x64') {
throw new Error(`BSD/SunOS systems require manual installation of libvips >= ${minimumLibvipsVersion}`); throw new Error(`BSD/SunOS systems require manual installation of libvips >= ${minimumLibvipsVersion}`);
} }
if (detectLibc.family === detectLibc.GLIBC && detectLibc.version && semver.lt(`${detectLibc.version}.0`, '2.13.0')) { if (detectLibc.family === detectLibc.GLIBC && detectLibc.version && semver.lt(`${detectLibc.version}.0`, '2.17.0')) {
throw new Error(`Use with glibc version ${detectLibc.version} requires manual installation of libvips >= ${minimumLibvipsVersion}`); throw new Error(`Use with glibc version ${detectLibc.version} requires manual installation of libvips >= ${minimumLibvipsVersion}`);
} }
// Download to per-process temporary file // Download to per-process temporary file
@@ -97,7 +96,7 @@ try {
fs.renameSync(tarPathTemp, tarPathCache); fs.renameSync(tarPathTemp, tarPathCache);
} catch (err) { } catch (err) {
// Fall back to copy and unlink // Fall back to copy and unlink
copyFileSync(tarPathTemp, tarPathCache); fs.copyFileSync(tarPathTemp, tarPathCache);
fs.unlinkSync(tarPathTemp); fs.unlinkSync(tarPathTemp);
} }
extractTarball(tarPathCache); extractTarball(tarPathCache);

View File

@@ -72,7 +72,7 @@ function extractChannel (channel) {
if (is.integer(channel) && is.inRange(channel, 0, 4)) { if (is.integer(channel) && is.inRange(channel, 0, 4)) {
this.options.extractChannel = channel; this.options.extractChannel = channel;
} else { } else {
throw new Error('Cannot extract invalid channel ' + channel); throw is.invalidParameterError('channel', 'integer or one of: red, green, blue', channel);
} }
return this; return this;
} }
@@ -124,7 +124,7 @@ function bandbool (boolOp) {
if (is.string(boolOp) && is.inArray(boolOp, ['and', 'or', 'eor'])) { if (is.string(boolOp) && is.inArray(boolOp, ['and', 'or', 'eor'])) {
this.options.bandBoolOp = boolOp; this.options.bandBoolOp = boolOp;
} else { } else {
throw new Error('Invalid bandbool operation ' + boolOp); throw is.invalidParameterError('boolOp', 'one of: and, or, eor', boolOp);
} }
return this; return this;
} }

View File

@@ -63,7 +63,7 @@ function grayscale (grayscale) {
*/ */
function toColourspace (colourspace) { function toColourspace (colourspace) {
if (!is.string(colourspace)) { if (!is.string(colourspace)) {
throw new Error('Invalid output colourspace ' + colourspace); throw is.invalidParameterError('colourspace', 'string', colourspace);
} }
this.options.colourspace = colourspace; this.options.colourspace = colourspace;
return this; return this;

View File

@@ -1,7 +1,5 @@
'use strict'; 'use strict';
const deprecate = require('util').deprecate;
const is = require('./is'); const is = require('./is');
/** /**
@@ -72,7 +70,12 @@ const blend = {
* }); * });
* *
* @param {Object[]} images - Ordered list of images to composite * @param {Object[]} images - Ordered list of images to composite
* @param {Buffer|String} [images[].input] - Buffer containing image data or String containing the path to an image file. * @param {Buffer|String} [images[].input] - Buffer containing image data, String containing the path to an image file, or Create object (see bellow)
* @param {Object} [images[].input.create] - describes a blank overlay to be created.
* @param {Number} [images[].input.create.width]
* @param {Number} [images[].input.create.height]
* @param {Number} [images[].input.create.channels] - 3-4
* @param {String|Object} [images[].input.create.background] - parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha.
* @param {String} [images[].blend='over'] - how to blend this image with the image below. * @param {String} [images[].blend='over'] - how to blend this image with the image below.
* @param {String} [images[].gravity='centre'] - gravity at which to place the overlay. * @param {String} [images[].gravity='centre'] - gravity at which to place the overlay.
* @param {Number} [images[].top] - the pixel offset from the top edge. * @param {Number} [images[].top] - the pixel offset from the top edge.
@@ -83,11 +86,6 @@ const blend = {
* @param {Number} [images[].raw.width] * @param {Number} [images[].raw.width]
* @param {Number} [images[].raw.height] * @param {Number} [images[].raw.height]
* @param {Number} [images[].raw.channels] * @param {Number} [images[].raw.channels]
* @param {Object} [images[].create] - describes a blank overlay to be created.
* @param {Number} [images[].create.width]
* @param {Number} [images[].create.height]
* @param {Number} [images[].create.channels] - 3-4
* @param {String|Object} [images[].create.background] - parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha.
* @returns {Sharp} * @returns {Sharp}
* @throws {Error} Invalid parameters * @throws {Error} Invalid parameters
*/ */
@@ -154,21 +152,11 @@ function composite (images) {
return this; return this;
} }
/**
* @deprecated
* @private
*/
function overlayWith (input, options) {
const blend = (is.object(options) && options.cutout) ? 'dest-in' : 'over';
return this.composite([Object.assign({ input, blend }, options)]);
}
/** /**
* Decorate the Sharp prototype with composite-related functions. * Decorate the Sharp prototype with composite-related functions.
* @private * @private
*/ */
module.exports = function (Sharp) { module.exports = function (Sharp) {
Sharp.prototype.composite = composite; Sharp.prototype.composite = composite;
Sharp.prototype.overlayWith = deprecate(overlayWith, 'overlayWith(input, options) is deprecated, use composite([{ input, ...options }]) instead');
Sharp.blend = blend; Sharp.blend = blend;
}; };

View File

@@ -1,13 +1,33 @@
'use strict'; 'use strict';
const path = require('path');
const util = require('util'); const util = require('util');
const stream = require('stream'); const stream = require('stream');
const events = require('events'); const events = require('events');
const is = require('./is'); const is = require('./is');
require('./libvips').hasVendoredLibvips(); require('./libvips').hasVendoredLibvips();
const sharp = require('bindings')('sharp.node');
let sharp;
/* istanbul ignore next */
try {
sharp = require('../build/Release/sharp.node');
} catch (err) {
// Bail early if bindings aren't available
const help = ['', 'Something went wrong installing the "sharp" module', '', err.message, ''];
if (/NODE_MODULE_VERSION/.test(err.message)) {
help.push('- Ensure the version of Node.js used at install time matches that used at runtime');
} else if (/invalid ELF header/.test(err.message)) {
help.push(`- Ensure "${process.platform}" is used at install time as well as runtime`);
} else {
help.push('- Remove the "node_modules/sharp" directory, run "npm install" and look for errors');
}
help.push(
'- Consult the installation documentation at https://sharp.pixelplumbing.com/en/stable/install/',
'- Search for this error at https://github.com/lovell/sharp/issues', ''
);
console.error(help.join('\n'));
process.exit(1);
}
// Use NODE_DEBUG=sharp to enable libvips warnings // Use NODE_DEBUG=sharp to enable libvips warnings
const debuglog = util.debuglog('sharp'); const debuglog = util.debuglog('sharp');
@@ -90,8 +110,6 @@ const Sharp = function (input, options) {
// input options // input options
sequentialRead: false, sequentialRead: false,
limitInputPixels: Math.pow(0x3FFF, 2), limitInputPixels: Math.pow(0x3FFF, 2),
// ICC profiles
iccProfilePath: path.join(__dirname, 'icc') + path.sep,
// resize options // resize options
topOffsetPre: -1, topOffsetPre: -1,
leftOffsetPre: -1, leftOffsetPre: -1,
@@ -139,6 +157,9 @@ const Sharp = function (input, options) {
gammaOut: 0, gammaOut: 0,
greyscale: false, greyscale: false,
normalise: 0, normalise: 0,
brightness: 1,
saturation: 1,
hue: 0,
booleanBufferIn: null, booleanBufferIn: null,
booleanFileIn: '', booleanFileIn: '',
joinChannelIn: [], joinChannelIn: [],
@@ -174,6 +195,8 @@ const Sharp = function (input, options) {
webpAlphaQuality: 100, webpAlphaQuality: 100,
webpLossless: false, webpLossless: false,
webpNearLossless: false, webpNearLossless: false,
webpSmartSubsample: false,
webpReductionEffort: 4,
tiffQuality: 80, tiffQuality: 80,
tiffCompression: 'jpeg', tiffCompression: 'jpeg',
tiffPredictor: 'horizontal', tiffPredictor: 'horizontal',
@@ -184,8 +207,12 @@ const Sharp = function (input, options) {
tiffTileWidth: 256, tiffTileWidth: 256,
tiffXres: 1.0, tiffXres: 1.0,
tiffYres: 1.0, tiffYres: 1.0,
heifQuality: 80,
heifLossless: false,
heifCompression: 'hevc',
tileSize: 256, tileSize: 256,
tileOverlap: 0, tileOverlap: 0,
tileSkipBlanks: -1,
linearA: 1, linearA: 1,
linearB: 0, linearB: 0,
// Function to notify of libvips warnings // Function to notify of libvips warnings

Binary file not shown.

Binary file not shown.

View File

@@ -19,7 +19,7 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
} else if (is.plainObject(input) && !is.defined(inputOptions)) { } else if (is.plainObject(input) && !is.defined(inputOptions)) {
// Plain Object descriptor, e.g. create // Plain Object descriptor, e.g. create
inputOptions = input; inputOptions = input;
if (is.plainObject(inputOptions.raw)) { if (is.plainObject(inputOptions.raw) || is.bool(inputOptions.failOnError)) {
// Raw Stream // Raw Stream
inputDescriptor.buffer = []; inputDescriptor.buffer = [];
} }
@@ -35,7 +35,7 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
if (is.bool(inputOptions.failOnError)) { if (is.bool(inputOptions.failOnError)) {
inputDescriptor.failOnError = inputOptions.failOnError; inputDescriptor.failOnError = inputOptions.failOnError;
} else { } else {
throw new Error('Invalid failOnError (boolean) ' + inputOptions.failOnError); throw is.invalidParameterError('failOnError', 'boolean', inputOptions.failOnError);
} }
} }
// Density // Density
@@ -43,7 +43,7 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
if (is.inRange(inputOptions.density, 1, 2400)) { if (is.inRange(inputOptions.density, 1, 2400)) {
inputDescriptor.density = inputOptions.density; inputDescriptor.density = inputOptions.density;
} else { } else {
throw new Error('Invalid density (1 to 2400) ' + inputOptions.density); throw is.invalidParameterError('density', 'number between 1 and 2400', inputOptions.density);
} }
} }
// Raw pixel input // Raw pixel input
@@ -115,9 +115,8 @@ function _write (chunk, encoding, callback) {
/* istanbul ignore else */ /* istanbul ignore else */
if (is.buffer(chunk)) { if (is.buffer(chunk)) {
if (this.options.input.buffer.length === 0) { if (this.options.input.buffer.length === 0) {
const that = this; this.on('finish', () => {
this.on('finish', function () { this.streamInFinished = true;
that.streamInFinished = true;
}); });
} }
this.options.input.buffer.push(chunk); this.options.input.buffer.push(chunk);
@@ -165,16 +164,15 @@ function _isStreamInput () {
* @returns {Sharp} * @returns {Sharp}
*/ */
function clone () { function clone () {
const that = this;
// Clone existing options // Clone existing options
const clone = this.constructor.call(); const clone = this.constructor.call();
clone.options = Object.assign({}, this.options); clone.options = Object.assign({}, this.options);
// Pass 'finish' event to clone for Stream-based input // Pass 'finish' event to clone for Stream-based input
if (this._isStreamInput()) { if (this._isStreamInput()) {
this.on('finish', function () { this.on('finish', () => {
// Clone inherits input data // Clone inherits input data
that._flattenBufferIn(); this._flattenBufferIn();
clone.options.bufferIn = that.options.bufferIn; clone.options.bufferIn = this.options.bufferIn;
clone.emit('finish'); clone.emit('finish');
}); });
} }
@@ -195,8 +193,9 @@ function clone () {
* - `density`: Number of pixels per inch (DPI), if present * - `density`: Number of pixels per inch (DPI), if present
* - `chromaSubsampling`: String containing JPEG chroma subsampling, `4:2:0` or `4:4:4` for RGB, `4:2:0:4` or `4:4:4:4` for CMYK * - `chromaSubsampling`: String containing JPEG chroma subsampling, `4:2:0` or `4:4:4` for RGB, `4:2:0:4` or `4:4:4:4` for CMYK
* - `isProgressive`: Boolean indicating whether the image is interlaced using a progressive scan * - `isProgressive`: Boolean indicating whether the image is interlaced using a progressive scan
* - `pages`: Number of pages/frames contained within the image, with support for TIFF, PDF, animated GIF and animated WebP * - `pages`: Number of pages/frames contained within the image, with support for TIFF, HEIF, PDF, animated GIF and animated WebP
* - `pageHeight`: Number of pixels high each page in this PDF image will be. * - `pageHeight`: Number of pixels high each page in this PDF image will be.
* - `pagePrimary`: Number of the primary page in a HEIF image
* - `hasProfile`: Boolean indicating the presence of an embedded ICC profile * - `hasProfile`: Boolean indicating the presence of an embedded ICC profile
* - `hasAlpha`: Boolean indicating the presence of an alpha transparency channel * - `hasAlpha`: Boolean indicating the presence of an alpha transparency channel
* - `orientation`: Number value of the EXIF Orientation header, if present * - `orientation`: Number value of the EXIF Orientation header, if present
@@ -223,12 +222,11 @@ function clone () {
* @returns {Promise<Object>|Sharp} * @returns {Promise<Object>|Sharp}
*/ */
function metadata (callback) { function metadata (callback) {
const that = this;
if (is.fn(callback)) { if (is.fn(callback)) {
if (this._isStreamInput()) { if (this._isStreamInput()) {
this.on('finish', function () { this.on('finish', () => {
that._flattenBufferIn(); this._flattenBufferIn();
sharp.metadata(that.options, callback); sharp.metadata(this.options, callback);
}); });
} else { } else {
sharp.metadata(this.options, callback); sharp.metadata(this.options, callback);
@@ -236,10 +234,10 @@ function metadata (callback) {
return this; return this;
} else { } else {
if (this._isStreamInput()) { if (this._isStreamInput()) {
return new Promise(function (resolve, reject) { return new Promise((resolve, reject) => {
that.on('finish', function () { this.on('finish', () => {
that._flattenBufferIn(); this._flattenBufferIn();
sharp.metadata(that.options, function (err, metadata) { sharp.metadata(this.options, (err, metadata) => {
if (err) { if (err) {
reject(err); reject(err);
} else { } else {
@@ -249,8 +247,8 @@ function metadata (callback) {
}); });
}); });
} else { } else {
return new Promise(function (resolve, reject) { return new Promise((resolve, reject) => {
sharp.metadata(that.options, function (err, metadata) { sharp.metadata(this.options, (err, metadata) => {
if (err) { if (err) {
reject(err); reject(err);
} else { } else {
@@ -292,12 +290,11 @@ function metadata (callback) {
* @returns {Promise<Object>} * @returns {Promise<Object>}
*/ */
function stats (callback) { function stats (callback) {
const that = this;
if (is.fn(callback)) { if (is.fn(callback)) {
if (this._isStreamInput()) { if (this._isStreamInput()) {
this.on('finish', function () { this.on('finish', () => {
that._flattenBufferIn(); this._flattenBufferIn();
sharp.stats(that.options, callback); sharp.stats(this.options, callback);
}); });
} else { } else {
sharp.stats(this.options, callback); sharp.stats(this.options, callback);
@@ -305,10 +302,10 @@ function stats (callback) {
return this; return this;
} else { } else {
if (this._isStreamInput()) { if (this._isStreamInput()) {
return new Promise(function (resolve, reject) { return new Promise((resolve, reject) => {
that.on('finish', function () { this.on('finish', function () {
that._flattenBufferIn(); this._flattenBufferIn();
sharp.stats(that.options, function (err, stats) { sharp.stats(this.options, (err, stats) => {
if (err) { if (err) {
reject(err); reject(err);
} else { } else {
@@ -318,8 +315,8 @@ function stats (callback) {
}); });
}); });
} else { } else {
return new Promise(function (resolve, reject) { return new Promise((resolve, reject) => {
sharp.stats(that.options, function (err, stats) { sharp.stats(this.options, (err, stats) => {
if (err) { if (err) {
reject(err); reject(err);
} else { } else {

View File

@@ -8,7 +8,8 @@ const semver = require('semver');
const platform = require('./platform'); const platform = require('./platform');
const env = process.env; const env = process.env;
const minimumLibvipsVersion = env.npm_package_config_libvips || require('../package.json').config.libvips; const minimumLibvipsVersion = env.npm_package_config_libvips || /* istanbul ignore next */
require('../package.json').config.libvips;
const spawnSyncOptions = { const spawnSyncOptions = {
encoding: 'utf8', encoding: 'utf8',
@@ -19,6 +20,7 @@ const mkdirSync = function (dirPath) {
try { try {
fs.mkdirSync(dirPath); fs.mkdirSync(dirPath);
} catch (err) { } catch (err) {
/* istanbul ignore if */
if (err.code !== 'EEXIST') { if (err.code !== 'EEXIST') {
throw err; throw err;
} }
@@ -26,7 +28,8 @@ const mkdirSync = function (dirPath) {
}; };
const cachePath = function () { const cachePath = function () {
const npmCachePath = env.npm_config_cache || (env.APPDATA ? path.join(env.APPDATA, 'npm-cache') : path.join(os.homedir(), '.npm')); const npmCachePath = env.npm_config_cache || /* istanbul ignore next */
(env.APPDATA ? path.join(env.APPDATA, 'npm-cache') : path.join(os.homedir(), '.npm'));
mkdirSync(npmCachePath); mkdirSync(npmCachePath);
const libvipsCachePath = path.join(npmCachePath, '_libvips'); const libvipsCachePath = path.join(npmCachePath, '_libvips');
mkdirSync(libvipsCachePath); mkdirSync(libvipsCachePath);
@@ -51,17 +54,21 @@ const hasVendoredLibvips = function () {
vendorVersionId = require(path.join(vendorPath, 'versions.json')).vips; vendorVersionId = require(path.join(vendorPath, 'versions.json')).vips;
vendorPlatformId = require(path.join(vendorPath, 'platform.json')); vendorPlatformId = require(path.join(vendorPath, 'platform.json'));
} catch (err) {} } catch (err) {}
/* istanbul ignore if */
if (vendorVersionId && vendorVersionId !== minimumLibvipsVersion) { if (vendorVersionId && vendorVersionId !== minimumLibvipsVersion) {
throw new Error(`Found vendored libvips v${vendorVersionId} but require v${minimumLibvipsVersion}. Please remove the 'node_modules/sharp/vendor' directory and run 'npm install'.`); throw new Error(`Found vendored libvips v${vendorVersionId} but require v${minimumLibvipsVersion}. Please remove the 'node_modules/sharp/vendor' directory and run 'npm install'.`);
} }
/* istanbul ignore else */
if (vendorPlatformId) { if (vendorPlatformId) {
/* istanbul ignore else */
if (currentPlatformId === vendorPlatformId) { if (currentPlatformId === vendorPlatformId) {
return true; return true;
} else { } else {
throw new Error(`'${vendorPlatformId}' binaries cannot be used on the '${currentPlatformId}' platform. Please remove the 'node_modules/sharp/vendor' directory and run 'npm install'.`); throw new Error(`'${vendorPlatformId}' binaries cannot be used on the '${currentPlatformId}' platform. Please remove the 'node_modules/sharp/vendor' directory and run 'npm install'.`);
} }
} else {
return false;
} }
return false;
}; };
const pkgConfigPath = function () { const pkgConfigPath = function () {
@@ -81,7 +88,8 @@ const useGlobalLibvips = function () {
} }
const globalVipsVersion = globalLibvipsVersion(); const globalVipsVersion = globalLibvipsVersion();
return !!globalVipsVersion && semver.gte(globalVipsVersion, minimumLibvipsVersion); return !!globalVipsVersion && /* istanbul ignore next */
semver.gte(globalVipsVersion, minimumLibvipsVersion);
}; };
module.exports = { module.exports = {

View File

@@ -55,7 +55,7 @@ function rotate (angle, options) {
]; ];
} }
} else { } else {
throw new Error('Unsupported angle: must be a number.'); throw is.invalidParameterError('angle', 'numeric', angle);
} }
return this; return this;
} }
@@ -109,7 +109,7 @@ function sharpen (sigma, flat, jagged) {
if (is.number(flat) && is.inRange(flat, 0, 10000)) { if (is.number(flat) && is.inRange(flat, 0, 10000)) {
this.options.sharpenFlat = flat; this.options.sharpenFlat = flat;
} else { } else {
throw new Error('Invalid sharpen level for flat areas (0.0 - 10000.0) ' + flat); throw is.invalidParameterError('flat', 'number between 0 and 10000', flat);
} }
} }
// Control over jagged areas // Control over jagged areas
@@ -117,11 +117,11 @@ function sharpen (sigma, flat, jagged) {
if (is.number(jagged) && is.inRange(jagged, 0, 10000)) { if (is.number(jagged) && is.inRange(jagged, 0, 10000)) {
this.options.sharpenJagged = jagged; this.options.sharpenJagged = jagged;
} else { } else {
throw new Error('Invalid sharpen level for jagged areas (0.0 - 10000.0) ' + jagged); throw is.invalidParameterError('jagged', 'number between 0 and 10000', jagged);
} }
} }
} else { } else {
throw new Error('Invalid sharpen sigma (0.01 - 10000) ' + sigma); throw is.invalidParameterError('sigma', 'number between 0.01 and 10000', sigma);
} }
return this; return this;
} }
@@ -141,7 +141,7 @@ function median (size) {
// Numeric argument: specific sigma // Numeric argument: specific sigma
this.options.medianSize = size; this.options.medianSize = size;
} else { } else {
throw new Error('Invalid median size ' + size); throw is.invalidParameterError('size', 'integer between 1 and 1000', size);
} }
return this; return this;
} }
@@ -165,7 +165,7 @@ function blur (sigma) {
// Numeric argument: specific sigma // Numeric argument: specific sigma
this.options.blurSigma = sigma; this.options.blurSigma = sigma;
} else { } else {
throw new Error('Invalid blur sigma (0.3 - 1000.0) ' + sigma); throw is.invalidParameterError('sigma', 'number between 0.3 and 1000', sigma);
} }
return this; return this;
} }
@@ -205,7 +205,7 @@ function gamma (gamma, gammaOut) {
} else if (is.number(gamma) && is.inRange(gamma, 1, 3)) { } else if (is.number(gamma) && is.inRange(gamma, 1, 3)) {
this.options.gamma = gamma; this.options.gamma = gamma;
} else { } else {
throw new Error('Invalid gamma correction (1.0 to 3.0) ' + gamma); throw is.invalidParameterError('gamma', 'number between 1.0 and 3.0', gamma);
} }
if (!is.defined(gammaOut)) { if (!is.defined(gammaOut)) {
// Default gamma correction for output is same as input // Default gamma correction for output is same as input
@@ -213,7 +213,7 @@ function gamma (gamma, gammaOut) {
} else if (is.number(gammaOut) && is.inRange(gammaOut, 1, 3)) { } else if (is.number(gammaOut) && is.inRange(gammaOut, 1, 3)) {
this.options.gammaOut = gammaOut; this.options.gammaOut = gammaOut;
} else { } else {
throw new Error('Invalid output gamma correction (1.0 to 3.0) ' + gammaOut); throw is.invalidParameterError('gammaOut', 'number between 1.0 and 3.0', gammaOut);
} }
return this; return this;
} }
@@ -315,7 +315,7 @@ function threshold (threshold, options) {
} else if (is.integer(threshold) && is.inRange(threshold, 0, 255)) { } else if (is.integer(threshold) && is.inRange(threshold, 0, 255)) {
this.options.threshold = threshold; this.options.threshold = threshold;
} else { } else {
throw new Error('Invalid threshold (0 to 255) ' + threshold); throw is.invalidParameterError('threshold', 'integer between 0 and 255', threshold);
} }
if (!is.object(options) || options.greyscale === true || options.grayscale === true) { if (!is.object(options) || options.greyscale === true || options.grayscale === true) {
this.options.thresholdGrayscale = true; this.options.thresholdGrayscale = true;
@@ -346,7 +346,7 @@ function boolean (operand, operator, options) {
if (is.string(operator) && is.inArray(operator, ['and', 'or', 'eor'])) { if (is.string(operator) && is.inArray(operator, ['and', 'or', 'eor'])) {
this.options.booleanOp = operator; this.options.booleanOp = operator;
} else { } else {
throw new Error('Invalid boolean operator ' + operator); throw is.invalidParameterError('operator', 'one of: and, or, eor', operator);
} }
return this; return this;
} }
@@ -364,17 +364,15 @@ function linear (a, b) {
} else if (is.number(a)) { } else if (is.number(a)) {
this.options.linearA = a; this.options.linearA = a;
} else { } else {
throw new Error('Invalid linear transform multiplier ' + a); throw is.invalidParameterError('a', 'numeric', a);
} }
if (!is.defined(b)) { if (!is.defined(b)) {
this.options.linearB = 0.0; this.options.linearB = 0.0;
} else if (is.number(b)) { } else if (is.number(b)) {
this.options.linearB = b; this.options.linearB = b;
} else { } else {
throw new Error('Invalid linear transform offset ' + b); throw is.invalidParameterError('b', 'numeric', b);
} }
return this; return this;
} }
@@ -405,7 +403,7 @@ function recomb (inputMatrix) {
inputMatrix[2].length !== 3 inputMatrix[2].length !== 3
) { ) {
// must pass in a kernel // must pass in a kernel
throw new Error('Invalid Recomb Matrix'); throw new Error('Invalid recombination matrix');
} }
this.options.recombMatrix = [ this.options.recombMatrix = [
inputMatrix[0][0], inputMatrix[0][1], inputMatrix[0][2], inputMatrix[0][0], inputMatrix[0][1], inputMatrix[0][2],
@@ -415,6 +413,62 @@ function recomb (inputMatrix) {
return this; return this;
} }
/**
* Transforms the image using brightness, saturation and hue rotation.
*
* @example
* sharp(input)
* .modulate({
* brightness: 2 // increase lightness by a factor of 2
* });
*
* sharp(input)
* .modulate({
* hue: 180 // hue-rotate by 180 degrees
* });
*
* // decreate brightness and saturation while also hue-rotating by 90 degrees
* sharp(input)
* .modulate({
* brightness: 0.5,
* saturation: 0.5,
* hue: 90
* });
*
* @param {Object} [options]
* @param {Number} [options.brightness] Brightness multiplier
* @param {Number} [options.saturation] Saturation multiplier
* @param {Number} [options.hue] Degrees for hue rotation
* @returns {Sharp}
*/
function modulate (options) {
if (!is.plainObject(options)) {
throw is.invalidParameterError('options', 'plain object', options);
}
if ('brightness' in options) {
if (is.number(options.brightness) && options.brightness >= 0) {
this.options.brightness = options.brightness;
} else {
throw is.invalidParameterError('brightness', 'number above zero', options.brightness);
}
}
if ('saturation' in options) {
if (is.number(options.saturation) && options.saturation >= 0) {
this.options.saturation = options.saturation;
} else {
throw is.invalidParameterError('saturation', 'number above zero', options.saturation);
}
}
if ('hue' in options) {
if (is.integer(options.hue)) {
this.options.hue = options.hue % 360;
} else {
throw is.invalidParameterError('hue', 'number', options.hue);
}
}
return this;
}
/** /**
* Decorate the Sharp prototype with operation-related functions. * Decorate the Sharp prototype with operation-related functions.
* @private * @private
@@ -436,6 +490,7 @@ module.exports = function (Sharp) {
threshold, threshold,
boolean, boolean,
linear, linear,
recomb recomb,
modulate
}); });
}; };

View File

@@ -91,9 +91,7 @@ function toFile (fileOut, callback) {
*/ */
function toBuffer (options, callback) { function toBuffer (options, callback) {
if (is.object(options)) { if (is.object(options)) {
if (is.bool(options.resolveWithObject)) { this._setBooleanOption('resolveWithObject', options.resolveWithObject);
this.options.resolveWithObject = options.resolveWithObject;
}
} }
return this._pipeline(is.fn(options) ? options : callback); return this._pipeline(is.fn(options) ? options : callback);
} }
@@ -109,19 +107,19 @@ function toBuffer (options, callback) {
* .toFile('output-with-metadata.jpg') * .toFile('output-with-metadata.jpg')
* .then(info => { ... }); * .then(info => { ... });
* *
* @param {Object} [withMetadata] * @param {Object} [options]
* @param {Number} [withMetadata.orientation] value between 1 and 8, used to update the EXIF `Orientation` tag. * @param {Number} [options.orientation] value between 1 and 8, used to update the EXIF `Orientation` tag.
* @returns {Sharp} * @returns {Sharp}
* @throws {Error} Invalid parameters * @throws {Error} Invalid parameters
*/ */
function withMetadata (withMetadata) { function withMetadata (options) {
this.options.withMetadata = is.bool(withMetadata) ? withMetadata : true; this.options.withMetadata = is.bool(options) ? options : true;
if (is.object(withMetadata)) { if (is.object(options)) {
if (is.defined(withMetadata.orientation)) { if (is.defined(options.orientation)) {
if (is.integer(withMetadata.orientation) && is.inRange(withMetadata.orientation, 1, 8)) { if (is.integer(options.orientation) && is.inRange(options.orientation, 1, 8)) {
this.options.withMetadataOrientation = withMetadata.orientation; this.options.withMetadataOrientation = options.orientation;
} else { } else {
throw new Error('Invalid orientation (1 to 8) ' + withMetadata.orientation); throw is.invalidParameterError('orientation', 'integer between 1 and 8', options.orientation);
} }
} }
} }
@@ -144,13 +142,13 @@ function withMetadata (withMetadata) {
* @param {Number} [options.quality=80] - quality, integer 1-100 * @param {Number} [options.quality=80] - quality, integer 1-100
* @param {Boolean} [options.progressive=false] - use progressive (interlace) scan * @param {Boolean} [options.progressive=false] - use progressive (interlace) scan
* @param {String} [options.chromaSubsampling='4:2:0'] - set to '4:4:4' to prevent chroma subsampling when quality <= 90 * @param {String} [options.chromaSubsampling='4:2:0'] - set to '4:4:4' to prevent chroma subsampling when quality <= 90
* @param {Boolean} [options.trellisQuantisation=false] - apply trellis quantisation, requires mozjpeg * @param {Boolean} [options.trellisQuantisation=false] - apply trellis quantisation, requires libvips compiled with support for mozjpeg
* @param {Boolean} [options.overshootDeringing=false] - apply overshoot deringing, requires mozjpeg * @param {Boolean} [options.overshootDeringing=false] - apply overshoot deringing, requires libvips compiled with support for mozjpeg
* @param {Boolean} [options.optimiseScans=false] - optimise progressive scans, forces progressive, requires mozjpeg * @param {Boolean} [options.optimiseScans=false] - optimise progressive scans, forces progressive, requires libvips compiled with support for mozjpeg
* @param {Boolean} [options.optimizeScans=false] - alternative spelling of optimiseScans * @param {Boolean} [options.optimizeScans=false] - alternative spelling of optimiseScans
* @param {Boolean} [options.optimiseCoding=true] - optimise Huffman coding tables * @param {Boolean} [options.optimiseCoding=true] - optimise Huffman coding tables
* @param {Boolean} [options.optimizeCoding=true] - alternative spelling of optimiseCoding * @param {Boolean} [options.optimizeCoding=true] - alternative spelling of optimiseCoding
* @param {Number} [options.quantisationTable=0] - quantization table to use, integer 0-8, requires mozjpeg * @param {Number} [options.quantisationTable=0] - quantization table to use, integer 0-8, requires libvips compiled with support for mozjpeg
* @param {Number} [options.quantizationTable=0] - alternative spelling of quantisationTable * @param {Number} [options.quantizationTable=0] - alternative spelling of quantisationTable
* @param {Boolean} [options.force=true] - force JPEG output, otherwise attempt to use input format * @param {Boolean} [options.force=true] - force JPEG output, otherwise attempt to use input format
* @returns {Sharp} * @returns {Sharp}
@@ -162,7 +160,7 @@ function jpeg (options) {
if (is.integer(options.quality) && is.inRange(options.quality, 1, 100)) { if (is.integer(options.quality) && is.inRange(options.quality, 1, 100)) {
this.options.jpegQuality = options.quality; this.options.jpegQuality = options.quality;
} else { } else {
throw new Error('Invalid quality (integer, 1-100) ' + options.quality); throw is.invalidParameterError('quality', 'integer between 1 and 100', options.quality);
} }
} }
if (is.defined(options.progressive)) { if (is.defined(options.progressive)) {
@@ -172,7 +170,7 @@ function jpeg (options) {
if (is.string(options.chromaSubsampling) && is.inArray(options.chromaSubsampling, ['4:2:0', '4:4:4'])) { if (is.string(options.chromaSubsampling) && is.inArray(options.chromaSubsampling, ['4:2:0', '4:4:4'])) {
this.options.jpegChromaSubsampling = options.chromaSubsampling; this.options.jpegChromaSubsampling = options.chromaSubsampling;
} else { } else {
throw new Error('Invalid chromaSubsampling (4:2:0, 4:4:4) ' + options.chromaSubsampling); throw is.invalidParameterError('chromaSubsampling', 'one of: 4:2:0, 4:4:4', options.chromaSubsampling);
} }
} }
const trellisQuantisation = is.bool(options.trellisQuantization) ? options.trellisQuantization : options.trellisQuantisation; const trellisQuantisation = is.bool(options.trellisQuantization) ? options.trellisQuantization : options.trellisQuantisation;
@@ -198,7 +196,7 @@ function jpeg (options) {
if (is.integer(quantisationTable) && is.inRange(quantisationTable, 0, 8)) { if (is.integer(quantisationTable) && is.inRange(quantisationTable, 0, 8)) {
this.options.jpegQuantisationTable = quantisationTable; this.options.jpegQuantisationTable = quantisationTable;
} else { } else {
throw new Error('Invalid quantisation table (integer, 0-8) ' + quantisationTable); throw is.invalidParameterError('quantisationTable', 'integer between 0 and 8', quantisationTable);
} }
} }
} }
@@ -221,11 +219,11 @@ function jpeg (options) {
* @param {Boolean} [options.progressive=false] - use progressive (interlace) scan * @param {Boolean} [options.progressive=false] - use progressive (interlace) scan
* @param {Number} [options.compressionLevel=9] - zlib compression level, 0-9 * @param {Number} [options.compressionLevel=9] - zlib compression level, 0-9
* @param {Boolean} [options.adaptiveFiltering=false] - use adaptive row filtering * @param {Boolean} [options.adaptiveFiltering=false] - use adaptive row filtering
* @param {Boolean} [options.palette=false] - quantise to a palette-based image with alpha transparency support, requires libimagequant * @param {Boolean} [options.palette=false] - quantise to a palette-based image with alpha transparency support, requires libvips compiled with support for libimagequant
* @param {Number} [options.quality=100] - use the lowest number of colours needed to achieve given quality, requires libimagequant * @param {Number} [options.quality=100] - use the lowest number of colours needed to achieve given quality, requires libvips compiled with support for libimagequant
* @param {Number} [options.colours=256] - maximum number of palette entries, requires libimagequant * @param {Number} [options.colours=256] - maximum number of palette entries, requires libvips compiled with support for libimagequant
* @param {Number} [options.colors=256] - alternative spelling of `options.colours`, requires libimagequant * @param {Number} [options.colors=256] - alternative spelling of `options.colours`, requires libvips compiled with support for libimagequant
* @param {Number} [options.dither=1.0] - level of Floyd-Steinberg error diffusion, requires libimagequant * @param {Number} [options.dither=1.0] - level of Floyd-Steinberg error diffusion, requires libvips compiled with support for libimagequant
* @param {Boolean} [options.force=true] - force PNG output, otherwise attempt to use input format * @param {Boolean} [options.force=true] - force PNG output, otherwise attempt to use input format
* @returns {Sharp} * @returns {Sharp}
* @throws {Error} Invalid options * @throws {Error} Invalid options
@@ -239,7 +237,7 @@ function png (options) {
if (is.integer(options.compressionLevel) && is.inRange(options.compressionLevel, 0, 9)) { if (is.integer(options.compressionLevel) && is.inRange(options.compressionLevel, 0, 9)) {
this.options.pngCompressionLevel = options.compressionLevel; this.options.pngCompressionLevel = options.compressionLevel;
} else { } else {
throw new Error('Invalid compressionLevel (integer, 0-9) ' + options.compressionLevel); throw is.invalidParameterError('compressionLevel', 'integer between 0 and 9', options.compressionLevel);
} }
} }
if (is.defined(options.adaptiveFiltering)) { if (is.defined(options.adaptiveFiltering)) {
@@ -290,6 +288,8 @@ function png (options) {
* @param {Number} [options.alphaQuality=100] - quality of alpha layer, integer 0-100 * @param {Number} [options.alphaQuality=100] - quality of alpha layer, integer 0-100
* @param {Boolean} [options.lossless=false] - use lossless compression mode * @param {Boolean} [options.lossless=false] - use lossless compression mode
* @param {Boolean} [options.nearLossless=false] - use near_lossless compression mode * @param {Boolean} [options.nearLossless=false] - use near_lossless compression mode
* @param {Boolean} [options.smartSubsample=false] - use high quality chroma subsampling
* @param {Number} [options.reductionEffort=4] - level of CPU effort to reduce file size, integer 0-6
* @param {Boolean} [options.force=true] - force WebP output, otherwise attempt to use input format * @param {Boolean} [options.force=true] - force WebP output, otherwise attempt to use input format
* @returns {Sharp} * @returns {Sharp}
* @throws {Error} Invalid options * @throws {Error} Invalid options
@@ -299,14 +299,14 @@ function webp (options) {
if (is.integer(options.quality) && is.inRange(options.quality, 1, 100)) { if (is.integer(options.quality) && is.inRange(options.quality, 1, 100)) {
this.options.webpQuality = options.quality; this.options.webpQuality = options.quality;
} else { } else {
throw new Error('Invalid quality (integer, 1-100) ' + options.quality); throw is.invalidParameterError('quality', 'integer between 1 and 100', options.quality);
} }
} }
if (is.object(options) && is.defined(options.alphaQuality)) { if (is.object(options) && is.defined(options.alphaQuality)) {
if (is.integer(options.alphaQuality) && is.inRange(options.alphaQuality, 0, 100)) { if (is.integer(options.alphaQuality) && is.inRange(options.alphaQuality, 0, 100)) {
this.options.webpAlphaQuality = options.alphaQuality; this.options.webpAlphaQuality = options.alphaQuality;
} else { } else {
throw new Error('Invalid webp alpha quality (integer, 0-100) ' + options.alphaQuality); throw is.invalidParameterError('alphaQuality', 'integer between 0 and 100', options.alphaQuality);
} }
} }
if (is.object(options) && is.defined(options.lossless)) { if (is.object(options) && is.defined(options.lossless)) {
@@ -315,6 +315,16 @@ function webp (options) {
if (is.object(options) && is.defined(options.nearLossless)) { if (is.object(options) && is.defined(options.nearLossless)) {
this._setBooleanOption('webpNearLossless', options.nearLossless); this._setBooleanOption('webpNearLossless', options.nearLossless);
} }
if (is.object(options) && is.defined(options.smartSubsample)) {
this._setBooleanOption('webpSmartSubsample', options.smartSubsample);
}
if (is.object(options) && is.defined(options.reductionEffort)) {
if (is.integer(options.reductionEffort) && is.inRange(options.reductionEffort, 0, 6)) {
this.options.webpReductionEffort = options.reductionEffort;
} else {
throw is.invalidParameterError('reductionEffort', 'integer between 0 and 6', options.reductionEffort);
}
}
return this._updateFormatOut('webp', options); return this._updateFormatOut('webp', options);
} }
@@ -352,59 +362,47 @@ function tiff (options) {
if (is.integer(options.quality) && is.inRange(options.quality, 1, 100)) { if (is.integer(options.quality) && is.inRange(options.quality, 1, 100)) {
this.options.tiffQuality = options.quality; this.options.tiffQuality = options.quality;
} else { } else {
throw new Error('Invalid quality (integer, 1-100) ' + options.quality); throw is.invalidParameterError('quality', 'integer between 1 and 100', options.quality);
} }
} }
if (is.defined(options.squash)) { if (is.defined(options.squash)) {
if (is.bool(options.squash)) { this._setBooleanOption('tiffSquash', options.squash);
this.options.tiffSquash = options.squash;
} else {
throw new Error('Invalid Value for squash ' + options.squash + ' Only Boolean Values allowed for options.squash.');
}
} }
// tiling // tiling
if (is.defined(options.tile)) { if (is.defined(options.tile)) {
if (is.bool(options.tile)) { this._setBooleanOption('tiffTile', options.tile);
this.options.tiffTile = options.tile;
} else {
throw new Error('Invalid Value for tile ' + options.tile + ' Only Boolean values allowed for options.tile');
}
} }
if (is.defined(options.tileWidth)) { if (is.defined(options.tileWidth)) {
if (is.number(options.tileWidth) && options.tileWidth > 0) { if (is.integer(options.tileWidth) && options.tileWidth > 0) {
this.options.tiffTileWidth = options.tileWidth; this.options.tiffTileWidth = options.tileWidth;
} else { } else {
throw new Error('Invalid Value for tileWidth ' + options.tileWidth + ' Only positive numeric values allowed for options.tileWidth'); throw is.invalidParameterError('tileWidth', 'integer greater than zero', options.tileWidth);
} }
} }
if (is.defined(options.tileHeight)) { if (is.defined(options.tileHeight)) {
if (is.number(options.tileHeight) && options.tileHeight > 0) { if (is.integer(options.tileHeight) && options.tileHeight > 0) {
this.options.tiffTileHeight = options.tileHeight; this.options.tiffTileHeight = options.tileHeight;
} else { } else {
throw new Error('Invalid Value for tileHeight ' + options.tileHeight + ' Only positive numeric values allowed for options.tileHeight'); throw is.invalidParameterError('tileHeight', 'integer greater than zero', options.tileHeight);
} }
} }
// pyramid // pyramid
if (is.defined(options.pyramid)) { if (is.defined(options.pyramid)) {
if (is.bool(options.pyramid)) { this._setBooleanOption('tiffPyramid', options.pyramid);
this.options.tiffPyramid = options.pyramid;
} else {
throw new Error('Invalid Value for pyramid ' + options.pyramid + ' Only Boolean values allowed for options.pyramid');
}
} }
// resolution // resolution
if (is.defined(options.xres)) { if (is.defined(options.xres)) {
if (is.number(options.xres)) { if (is.number(options.xres) && options.xres > 0) {
this.options.tiffXres = options.xres; this.options.tiffXres = options.xres;
} else { } else {
throw new Error('Invalid Value for xres ' + options.xres + ' Only numeric values allowed for options.xres'); throw is.invalidParameterError('xres', 'number greater than zero', options.xres);
} }
} }
if (is.defined(options.yres)) { if (is.defined(options.yres)) {
if (is.number(options.yres)) { if (is.number(options.yres) && options.yres > 0) {
this.options.tiffYres = options.yres; this.options.tiffYres = options.yres;
} else { } else {
throw new Error('Invalid Value for yres ' + options.yres + ' Only numeric values allowed for options.yres'); throw is.invalidParameterError('yres', 'number greater than zero', options.yres);
} }
} }
// compression // compression
@@ -412,8 +410,7 @@ function tiff (options) {
if (is.string(options.compression) && is.inArray(options.compression, ['lzw', 'deflate', 'jpeg', 'ccittfax4', 'none'])) { if (is.string(options.compression) && is.inArray(options.compression, ['lzw', 'deflate', 'jpeg', 'ccittfax4', 'none'])) {
this.options.tiffCompression = options.compression; this.options.tiffCompression = options.compression;
} else { } else {
const message = `Invalid compression option "${options.compression}". Should be one of: lzw, deflate, jpeg, ccittfax4, none`; throw is.invalidParameterError('compression', 'one of: lzw, deflate, jpeg, ccittfax4, none', options.compression);
throw new Error(message);
} }
} }
// predictor // predictor
@@ -421,14 +418,60 @@ function tiff (options) {
if (is.string(options.predictor) && is.inArray(options.predictor, ['none', 'horizontal', 'float'])) { if (is.string(options.predictor) && is.inArray(options.predictor, ['none', 'horizontal', 'float'])) {
this.options.tiffPredictor = options.predictor; this.options.tiffPredictor = options.predictor;
} else { } else {
const message = `Invalid predictor option "${options.predictor}". Should be one of: none, horizontal, float`; throw is.invalidParameterError('predictor', 'one of: none, horizontal, float', options.predictor);
throw new Error(message);
} }
} }
} }
return this._updateFormatOut('tiff', options); return this._updateFormatOut('tiff', options);
} }
/**
* Use these HEIF options for output image.
*
* Support for HEIF (HEIC/AVIF) is experimental.
* Do not use this in production systems.
*
* Requires a custom, globally-installed libvips compiled with support for libheif.
*
* Most versions of libheif support only the patent-encumbered HEVC compression format.
*
* @param {Object} [options] - output options
* @param {Number} [options.quality=80] - quality, integer 1-100
* @param {Boolean} [options.compression='hevc'] - compression format: hevc, avc, jpeg, av1
* @param {Boolean} [options.lossless=false] - use lossless compression
* @returns {Sharp}
* @throws {Error} Invalid options
*/
function heif (options) {
if (!this.constructor.format.heif.output.buffer) {
throw new Error('The heif operation requires libvips to have been installed with support for libheif');
}
if (is.object(options)) {
if (is.defined(options.quality)) {
if (is.integer(options.quality) && is.inRange(options.quality, 1, 100)) {
this.options.heifQuality = options.quality;
} else {
throw is.invalidParameterError('quality', 'integer between 1 and 100', options.quality);
}
}
if (is.defined(options.lossless)) {
if (is.bool(options.lossless)) {
this.options.heifLossless = options.lossless;
} else {
throw is.invalidParameterError('lossless', 'boolean', options.lossless);
}
}
if (is.defined(options.compression)) {
if (is.string(options.compression) && is.inArray(options.compression, ['hevc', 'avc', 'jpeg', 'av1'])) {
this.options.heifCompression = options.compression;
} else {
throw is.invalidParameterError('compression', 'one of: hevc, avc, jpeg, av1', options.compression);
}
}
}
return this._updateFormatOut('heif', options);
}
/** /**
* Force output to be raw, uncompressed uint8 pixel data. * Force output to be raw, uncompressed uint8 pixel data.
* *
@@ -464,7 +507,7 @@ function toFormat (format, options) {
} }
if (format === 'jpg') format = 'jpeg'; if (format === 'jpg') format = 'jpeg';
if (!is.inArray(format, ['jpeg', 'png', 'webp', 'tiff', 'raw'])) { if (!is.inArray(format, ['jpeg', 'png', 'webp', 'tiff', 'raw'])) {
throw new Error('Unsupported output format ' + format); throw is.invalidParameterError('format', 'one of: jpeg, png, webp, tiff, raw', format);
} }
return this[format](options); return this[format](options);
} }
@@ -487,79 +530,87 @@ function toFormat (format, options) {
* // output_files contains 512x512 tiles grouped by zoom level * // output_files contains 512x512 tiles grouped by zoom level
* }); * });
* *
* @param {Object} [tile] * @param {Object} [options]
* @param {Number} [tile.size=256] tile size in pixels, a value between 1 and 8192. * @param {Number} [options.size=256] tile size in pixels, a value between 1 and 8192.
* @param {Number} [tile.overlap=0] tile overlap in pixels, a value between 0 and 8192. * @param {Number} [options.overlap=0] tile overlap in pixels, a value between 0 and 8192.
* @param {Number} [tile.angle=0] tile angle of rotation, must be a multiple of 90. * @param {Number} [options.angle=0] tile angle of rotation, must be a multiple of 90.
* @param {String} [tile.depth] how deep to make the pyramid, possible values are `onepixel`, `onetile` or `one`, default based on layout. * @param {String} [options.depth] how deep to make the pyramid, possible values are `onepixel`, `onetile` or `one`, default based on layout.
* @param {String} [tile.container='fs'] tile container, with value `fs` (filesystem) or `zip` (compressed file). * @param {Number} [options.skipBlanks=-1] threshold to skip tile generation, a value 0 - 255 for 8-bit images or 0 - 65535 for 16-bit images
* @param {String} [tile.layout='dz'] filesystem layout, possible values are `dz`, `zoomify` or `google`. * @param {String} [options.container='fs'] tile container, with value `fs` (filesystem) or `zip` (compressed file).
* @param {String} [options.layout='dz'] filesystem layout, possible values are `dz`, `zoomify` or `google`.
* @returns {Sharp} * @returns {Sharp}
* @throws {Error} Invalid parameters * @throws {Error} Invalid parameters
*/ */
function tile (tile) { function tile (options) {
if (is.object(tile)) { if (is.object(options)) {
// Size of square tiles, in pixels // Size of square tiles, in pixels
if (is.defined(tile.size)) { if (is.defined(options.size)) {
if (is.integer(tile.size) && is.inRange(tile.size, 1, 8192)) { if (is.integer(options.size) && is.inRange(options.size, 1, 8192)) {
this.options.tileSize = tile.size; this.options.tileSize = options.size;
} else { } else {
throw new Error('Invalid tile size (1 to 8192) ' + tile.size); throw is.invalidParameterError('size', 'integer between 1 and 8192', options.size);
} }
} }
// Overlap of tiles, in pixels // Overlap of tiles, in pixels
if (is.defined(tile.overlap)) { if (is.defined(options.overlap)) {
if (is.integer(tile.overlap) && is.inRange(tile.overlap, 0, 8192)) { if (is.integer(options.overlap) && is.inRange(options.overlap, 0, 8192)) {
if (tile.overlap > this.options.tileSize) { if (options.overlap > this.options.tileSize) {
throw new Error('Tile overlap ' + tile.overlap + ' cannot be larger than tile size ' + this.options.tileSize); throw is.invalidParameterError('overlap', `<= size (${this.options.tileSize})`, options.overlap);
} }
this.options.tileOverlap = tile.overlap; this.options.tileOverlap = tile.overlap;
} else { } else {
throw new Error('Invalid tile overlap (0 to 8192) ' + tile.overlap); throw is.invalidParameterError('overlap', 'integer between 0 and 8192', options.overlap);
} }
} }
// Container // Container
if (is.defined(tile.container)) { if (is.defined(options.container)) {
if (is.string(tile.container) && is.inArray(tile.container, ['fs', 'zip'])) { if (is.string(options.container) && is.inArray(options.container, ['fs', 'zip'])) {
this.options.tileContainer = tile.container; this.options.tileContainer = options.container;
} else { } else {
throw new Error('Invalid tile container ' + tile.container); throw is.invalidParameterError('container', 'one of: fs, zip', options.container);
} }
} }
// Layout // Layout
if (is.defined(tile.layout)) { if (is.defined(options.layout)) {
if (is.string(tile.layout) && is.inArray(tile.layout, ['dz', 'google', 'zoomify'])) { if (is.string(options.layout) && is.inArray(options.layout, ['dz', 'google', 'zoomify'])) {
this.options.tileLayout = tile.layout; this.options.tileLayout = options.layout;
} else { } else {
throw new Error('Invalid tile layout ' + tile.layout); throw is.invalidParameterError('layout', 'one of: dz, google, zoomify', options.layout);
} }
} }
// Angle of rotation, // Angle of rotation,
if (is.defined(tile.angle)) { if (is.defined(options.angle)) {
if (is.integer(tile.angle) && !(tile.angle % 90)) { if (is.integer(options.angle) && !(options.angle % 90)) {
this.options.tileAngle = tile.angle; this.options.tileAngle = options.angle;
} else { } else {
throw new Error('Unsupported angle: angle must be a positive/negative multiple of 90 ' + tile.angle); throw is.invalidParameterError('angle', 'positive/negative multiple of 90', options.angle);
} }
} }
// Depth of tiles // Depth of tiles
if (is.defined(tile.depth)) { if (is.defined(options.depth)) {
if (is.string(tile.depth) && is.inArray(tile.depth, ['onepixel', 'onetile', 'one'])) { if (is.string(options.depth) && is.inArray(options.depth, ['onepixel', 'onetile', 'one'])) {
this.options.tileDepth = tile.depth; this.options.tileDepth = options.depth;
} else { } else {
throw new Error("Invalid tile depth '" + tile.depth + "', should be one of 'onepixel', 'onetile' or 'one'"); throw is.invalidParameterError('depth', 'one of: onepixel, onetile, one', options.depth);
} }
} }
// Threshold to skip blank tiles
if (is.defined(options.skipBlanks)) {
if (is.integer(options.skipBlanks) && is.inRange(options.skipBlanks, -1, 65535)) {
this.options.tileSkipBlanks = options.skipBlanks;
} else {
throw is.invalidParameterError('skipBlanks', 'integer between -1 and 255/65535', options.skipBlanks);
}
} else if (is.defined(options.layout) && options.layout === 'google') {
this.options.tileSkipBlanks = 5;
}
} }
// Format // Format
if (is.inArray(this.options.formatOut, ['jpeg', 'png', 'webp'])) { if (is.inArray(this.options.formatOut, ['jpeg', 'png', 'webp'])) {
this.options.tileFormat = this.options.formatOut; this.options.tileFormat = this.options.formatOut;
} else if (this.options.formatOut !== 'input') { } else if (this.options.formatOut !== 'input') {
throw new Error('Invalid tile format ' + this.options.formatOut); throw is.invalidParameterError('format', 'one of: jpeg, png, webp', this.options.formatOut);
} }
return this._updateFormatOut('dz'); return this._updateFormatOut('dz');
} }
@@ -590,7 +641,7 @@ function _setBooleanOption (key, val) {
if (is.bool(val)) { if (is.bool(val)) {
this.options[key] = val; this.options[key] = val;
} else { } else {
throw new Error('Invalid ' + key + ' (boolean) ' + val); throw is.invalidParameterError(key, 'boolean', val);
} }
} }
@@ -599,6 +650,7 @@ function _setBooleanOption (key, val) {
* @private * @private
*/ */
function _read () { function _read () {
/* istanbul ignore else */
if (!this.options.streamOut) { if (!this.options.streamOut) {
this.options.streamOut = true; this.options.streamOut = true;
this._pipeline(); this._pipeline();
@@ -611,14 +663,13 @@ function _read () {
* @private * @private
*/ */
function _pipeline (callback) { function _pipeline (callback) {
const that = this;
if (typeof callback === 'function') { if (typeof callback === 'function') {
// output=file/buffer // output=file/buffer
if (this._isStreamInput()) { if (this._isStreamInput()) {
// output=file/buffer, input=stream // output=file/buffer, input=stream
this.on('finish', function () { this.on('finish', () => {
that._flattenBufferIn(); this._flattenBufferIn();
sharp.pipeline(that.options, callback); sharp.pipeline(this.options, callback);
}); });
} else { } else {
// output=file/buffer, input=file/buffer // output=file/buffer, input=file/buffer
@@ -629,41 +680,31 @@ function _pipeline (callback) {
// output=stream // output=stream
if (this._isStreamInput()) { if (this._isStreamInput()) {
// output=stream, input=stream // output=stream, input=stream
if (this.streamInFinished) { this.once('finish', () => {
this._flattenBufferIn(); this._flattenBufferIn();
sharp.pipeline(this.options, function (err, data, info) { sharp.pipeline(this.options, (err, data, info) => {
if (err) { if (err) {
that.emit('error', err); this.emit('error', err);
} else { } else {
that.emit('info', info); this.emit('info', info);
that.push(data); this.push(data);
} }
that.push(null); this.push(null);
});
} else {
this.on('finish', function () {
that._flattenBufferIn();
sharp.pipeline(that.options, function (err, data, info) {
if (err) {
that.emit('error', err);
} else {
that.emit('info', info);
that.push(data);
}
that.push(null);
});
}); });
});
if (this.streamInFinished) {
this.emit('finish');
} }
} else { } else {
// output=stream, input=file/buffer // output=stream, input=file/buffer
sharp.pipeline(this.options, function (err, data, info) { sharp.pipeline(this.options, (err, data, info) => {
if (err) { if (err) {
that.emit('error', err); this.emit('error', err);
} else { } else {
that.emit('info', info); this.emit('info', info);
that.push(data); this.push(data);
} }
that.push(null); this.push(null);
}); });
} }
return this; return this;
@@ -671,15 +712,15 @@ function _pipeline (callback) {
// output=promise // output=promise
if (this._isStreamInput()) { if (this._isStreamInput()) {
// output=promise, input=stream // output=promise, input=stream
return new Promise(function (resolve, reject) { return new Promise((resolve, reject) => {
that.on('finish', function () { this.once('finish', () => {
that._flattenBufferIn(); this._flattenBufferIn();
sharp.pipeline(that.options, function (err, data, info) { sharp.pipeline(this.options, (err, data, info) => {
if (err) { if (err) {
reject(err); reject(err);
} else { } else {
if (that.options.resolveWithObject) { if (this.options.resolveWithObject) {
resolve({ data: data, info: info }); resolve({ data, info });
} else { } else {
resolve(data); resolve(data);
} }
@@ -689,12 +730,12 @@ function _pipeline (callback) {
}); });
} else { } else {
// output=promise, input=file/buffer // output=promise, input=file/buffer
return new Promise(function (resolve, reject) { return new Promise((resolve, reject) => {
sharp.pipeline(that.options, function (err, data, info) { sharp.pipeline(this.options, (err, data, info) => {
if (err) { if (err) {
reject(err); reject(err);
} else { } else {
if (that.options.resolveWithObject) { if (this.options.resolveWithObject) {
resolve({ data: data, info: info }); resolve({ data: data, info: info });
} else { } else {
resolve(data); resolve(data);
@@ -720,6 +761,7 @@ module.exports = function (Sharp) {
png, png,
webp, webp,
tiff, tiff,
heif,
raw, raw,
toFormat, toFormat,
tile, tile,

View File

@@ -307,7 +307,7 @@ function extend (extend) {
this.options.extendRight = extend.right; this.options.extendRight = extend.right;
this._setColourOption('extendBackground', extend.background); this._setColourOption('extendBackground', extend.background);
} else { } else {
throw new Error('Invalid edge extension ' + extend); throw is.invalidParameterError('extend', 'integer or object', extend);
} }
return this; return this;
} }
@@ -334,11 +334,11 @@ function extend (extend) {
* // Extract a region, resize, then extract from the resized image * // Extract a region, resize, then extract from the resized image
* }); * });
* *
* @param {Object} options * @param {Object} options - describes the region to extract using integral pixel values
* @param {Number} options.left - zero-indexed offset from left edge * @param {Number} options.left - zero-indexed offset from left edge
* @param {Number} options.top - zero-indexed offset from top edge * @param {Number} options.top - zero-indexed offset from top edge
* @param {Number} options.width - dimension of extracted image * @param {Number} options.width - width of region to extract
* @param {Number} options.height - dimension of extracted image * @param {Number} options.height - height of region to extract
* @returns {Sharp} * @returns {Sharp}
* @throws {Error} Invalid parameters * @throws {Error} Invalid parameters
*/ */
@@ -349,11 +349,11 @@ function extract (options) {
if (is.integer(value) && value >= 0) { if (is.integer(value) && value >= 0) {
this.options[name + (name === 'left' || name === 'top' ? 'Offset' : '') + suffix] = value; this.options[name + (name === 'left' || name === 'top' ? 'Offset' : '') + suffix] = value;
} else { } else {
throw new Error('Non-integer value for ' + name + ' of ' + value); throw is.invalidParameterError(name, 'integer', value);
} }
}, this); }, this);
// Ensure existing rotation occurs before pre-resize extraction // Ensure existing rotation occurs before pre-resize extraction
if (suffix === 'Pre' && ((this.options.angle % 360) !== 0 || this.options.useExifOrientation === true)) { if (suffix === 'Pre' && ((this.options.angle % 360) !== 0 || this.options.useExifOrientation === true || this.options.rotationAngle !== 0)) {
this.options.rotateBeforePreExtract = true; this.options.rotateBeforePreExtract = true;
} }
return this; return this;

View File

@@ -1,7 +1,7 @@
{ {
"name": "sharp", "name": "sharp",
"description": "High performance Node.js image processing, the fastest module to resize JPEG, PNG, WebP and TIFF images", "description": "High performance Node.js image processing, the fastest module to resize JPEG, PNG, WebP and TIFF images",
"version": "0.22.0", "version": "0.23.0",
"author": "Lovell Fuller <npm@lovell.info>", "author": "Lovell Fuller <npm@lovell.info>",
"homepage": "https://github.com/lovell/sharp", "homepage": "https://github.com/lovell/sharp",
"contributors": [ "contributors": [
@@ -59,7 +59,9 @@
"Daiz <taneli.vatanen@gmail.com>", "Daiz <taneli.vatanen@gmail.com>",
"Julian Aubourg <j@ubourg.net>", "Julian Aubourg <j@ubourg.net>",
"Keith Belovay <keith@picthrive.com>", "Keith Belovay <keith@picthrive.com>",
"Michael B. Klein <mbklein@gmail.com>" "Michael B. Klein <mbklein@gmail.com>",
"Jordan Prudhomme <jordan@raboland.fr>",
"Ilya Ovdin <iovdin@gmail.com>"
], ],
"scripts": { "scripts": {
"install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node-gyp rebuild && node install/dll-copy)", "install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node-gyp rebuild && node install/dll-copy)",
@@ -93,40 +95,38 @@
"vips" "vips"
], ],
"dependencies": { "dependencies": {
"bindings": "^1.5.0", "color": "^3.1.2",
"color": "^3.1.0",
"detect-libc": "^1.0.3", "detect-libc": "^1.0.3",
"fs-copy-file-sync": "^1.1.1", "nan": "^2.14.0",
"nan": "^2.13.1",
"npmlog": "^4.1.2", "npmlog": "^4.1.2",
"prebuild-install": "^5.2.5", "prebuild-install": "^5.3.0",
"semver": "^5.6.0", "semver": "^6.3.0",
"simple-get": "^3.0.3", "simple-get": "^3.0.3",
"tar": "^4.4.8", "tar": "^4.4.10",
"tunnel-agent": "^0.6.0" "tunnel-agent": "^0.6.0"
}, },
"devDependencies": { "devDependencies": {
"async": "^2.6.2", "async": "^3.1.0",
"cc": "^1.0.2", "cc": "^1.0.2",
"decompress-zip": "^0.3.2", "decompress-zip": "^0.3.2",
"documentation": "^9.3.1", "documentation": "^12.0.3",
"exif-reader": "^1.0.2", "exif-reader": "^1.0.2",
"icc": "^1.0.0", "icc": "^1.0.0",
"license-checker": "^25.0.1", "license-checker": "^25.0.1",
"mocha": "^6.0.2", "mocha": "^6.2.0",
"mock-fs": "^4.8.0", "mock-fs": "^4.10.1",
"nyc": "^13.3.0", "nyc": "^14.1.1",
"prebuild": "8.1.0", "prebuild": "^9.0.1",
"prebuild-ci": "^2.3.0", "prebuild-ci": "^3.1.0",
"rimraf": "^2.6.3", "rimraf": "^2.6.3",
"semistandard": "^13.0.1" "semistandard": "^13.0.1"
}, },
"license": "Apache-2.0", "license": "Apache-2.0",
"config": { "config": {
"libvips": "8.7.4" "libvips": "8.8.1"
}, },
"engines": { "engines": {
"node": ">=6" "node": ">=8.5.0"
}, },
"semistandard": { "semistandard": {
"env": [ "env": [

View File

@@ -31,13 +31,13 @@ using vips::VImage;
namespace sharp { namespace sharp {
// Convenience methods to access the attributes of a v8::Object // Convenience methods to access the attributes of a v8::Object
bool HasAttr(v8::Handle<v8::Object> obj, std::string attr) { bool HasAttr(v8::Local<v8::Object> obj, std::string attr) {
return Nan::Has(obj, Nan::New(attr).ToLocalChecked()).FromJust(); return Nan::Has(obj, Nan::New(attr).ToLocalChecked()).FromJust();
} }
std::string AttrAsStr(v8::Handle<v8::Object> obj, std::string attr) { std::string AttrAsStr(v8::Local<v8::Object> obj, std::string attr) {
return *Nan::Utf8String(Nan::Get(obj, Nan::New(attr).ToLocalChecked()).ToLocalChecked()); return *Nan::Utf8String(Nan::Get(obj, Nan::New(attr).ToLocalChecked()).ToLocalChecked());
} }
std::vector<double> AttrAsRgba(v8::Handle<v8::Object> obj, std::string attr) { std::vector<double> AttrAsRgba(v8::Local<v8::Object> obj, std::string attr) {
v8::Local<v8::Object> background = AttrAs<v8::Object>(obj, attr); v8::Local<v8::Object> background = AttrAs<v8::Object>(obj, attr);
std::vector<double> rgba(4); std::vector<double> rgba(4);
for (unsigned int i = 0; i < 4; i++) { for (unsigned int i = 0; i < 4; i++) {
@@ -48,7 +48,7 @@ namespace sharp {
// Create an InputDescriptor instance from a v8::Object describing an input image // Create an InputDescriptor instance from a v8::Object describing an input image
InputDescriptor* CreateInputDescriptor( InputDescriptor* CreateInputDescriptor(
v8::Handle<v8::Object> input, std::vector<v8::Local<v8::Object>> &buffersToPersist v8::Local<v8::Object> input, std::vector<v8::Local<v8::Object>> &buffersToPersist
) { ) {
Nan::HandleScope(); Nan::HandleScope();
InputDescriptor *descriptor = new InputDescriptor; InputDescriptor *descriptor = new InputDescriptor;
@@ -110,6 +110,15 @@ namespace sharp {
bool IsTiff(std::string const &str) { bool IsTiff(std::string const &str) {
return EndsWith(str, ".tif") || EndsWith(str, ".tiff") || EndsWith(str, ".TIF") || EndsWith(str, ".TIFF"); return EndsWith(str, ".tif") || EndsWith(str, ".tiff") || EndsWith(str, ".TIF") || EndsWith(str, ".TIFF");
} }
bool IsHeic(std::string const &str) {
return EndsWith(str, ".heic") || EndsWith(str, ".HEIC");
}
bool IsHeif(std::string const &str) {
return EndsWith(str, ".heif") || EndsWith(str, ".HEIF") || IsHeic(str) || IsAvif(str);
}
bool IsAvif(std::string const &str) {
return EndsWith(str, ".avif") || EndsWith(str, ".AVIF");
}
bool IsDz(std::string const &str) { bool IsDz(std::string const &str) {
return EndsWith(str, ".dzi") || EndsWith(str, ".DZI"); return EndsWith(str, ".dzi") || EndsWith(str, ".DZI");
} }
@@ -132,6 +141,7 @@ namespace sharp {
case ImageType::TIFF: id = "tiff"; break; case ImageType::TIFF: id = "tiff"; break;
case ImageType::GIF: id = "gif"; break; case ImageType::GIF: id = "gif"; break;
case ImageType::SVG: id = "svg"; break; case ImageType::SVG: id = "svg"; break;
case ImageType::HEIF: id = "heif"; break;
case ImageType::PDF: id = "pdf"; break; case ImageType::PDF: id = "pdf"; break;
case ImageType::MAGICK: id = "magick"; break; case ImageType::MAGICK: id = "magick"; break;
case ImageType::OPENSLIDE: id = "openslide"; break; case ImageType::OPENSLIDE: id = "openslide"; break;
@@ -165,6 +175,8 @@ namespace sharp {
imageType = ImageType::GIF; imageType = ImageType::GIF;
} else if (EndsWith(loader, "SvgBuffer")) { } else if (EndsWith(loader, "SvgBuffer")) {
imageType = ImageType::SVG; imageType = ImageType::SVG;
} else if (EndsWith(loader, "HeifBuffer")) {
imageType = ImageType::HEIF;
} else if (EndsWith(loader, "PdfBuffer")) { } else if (EndsWith(loader, "PdfBuffer")) {
imageType = ImageType::PDF; imageType = ImageType::PDF;
} else if (EndsWith(loader, "MagickBuffer")) { } else if (EndsWith(loader, "MagickBuffer")) {
@@ -196,6 +208,8 @@ namespace sharp {
imageType = ImageType::GIF; imageType = ImageType::GIF;
} else if (EndsWith(loader, "SvgFile")) { } else if (EndsWith(loader, "SvgFile")) {
imageType = ImageType::SVG; imageType = ImageType::SVG;
} else if (EndsWith(loader, "HeifFile")) {
imageType = ImageType::HEIF;
} else if (EndsWith(loader, "PdfFile")) { } else if (EndsWith(loader, "PdfFile")) {
imageType = ImageType::PDF; imageType = ImageType::PDF;
} else if (EndsWith(loader, "Ppm")) { } else if (EndsWith(loader, "Ppm")) {
@@ -222,6 +236,7 @@ namespace sharp {
return return
imageType == ImageType::GIF || imageType == ImageType::GIF ||
imageType == ImageType::TIFF || imageType == ImageType::TIFF ||
imageType == ImageType::HEIF ||
imageType == ImageType::PDF; imageType == ImageType::PDF;
} }

View File

@@ -25,8 +25,8 @@
// Verify platform and compiler compatibility // Verify platform and compiler compatibility
#if (VIPS_MAJOR_VERSION < 8 || (VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION < 7)) #if (VIPS_MAJOR_VERSION < 8 || (VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION < 8))
#error libvips version 8.7.0+ is required - see sharp.pixelplumbing.com/page/install #error libvips version 8.8.0+ is required - see sharp.pixelplumbing.com/page/install
#endif #endif
#if ((!defined(__clang__)) && defined(__GNUC__) && (__GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 6))) #if ((!defined(__clang__)) && defined(__GNUC__) && (__GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 6)))
@@ -77,22 +77,22 @@ namespace sharp {
}; };
// Convenience methods to access the attributes of a v8::Object // Convenience methods to access the attributes of a v8::Object
bool HasAttr(v8::Handle<v8::Object> obj, std::string attr); bool HasAttr(v8::Local<v8::Object> obj, std::string attr);
std::string AttrAsStr(v8::Handle<v8::Object> obj, std::string attr); std::string AttrAsStr(v8::Local<v8::Object> obj, std::string attr);
std::vector<double> AttrAsRgba(v8::Handle<v8::Object> obj, std::string attr); std::vector<double> AttrAsRgba(v8::Local<v8::Object> obj, std::string attr);
template<typename T> v8::Local<T> AttrAs(v8::Handle<v8::Object> obj, std::string attr) { template<typename T> v8::Local<T> AttrAs(v8::Local<v8::Object> obj, std::string attr) {
return Nan::Get(obj, Nan::New(attr).ToLocalChecked()).ToLocalChecked().As<T>(); return Nan::Get(obj, Nan::New(attr).ToLocalChecked()).ToLocalChecked().As<T>();
} }
template<typename T> T AttrTo(v8::Handle<v8::Object> obj, std::string attr) { template<typename T> T AttrTo(v8::Local<v8::Object> obj, std::string attr) {
return Nan::To<T>(Nan::Get(obj, Nan::New(attr).ToLocalChecked()).ToLocalChecked()).FromJust(); return Nan::To<T>(Nan::Get(obj, Nan::New(attr).ToLocalChecked()).ToLocalChecked()).FromJust();
} }
template<typename T> T AttrTo(v8::Handle<v8::Object> obj, int attr) { template<typename T> T AttrTo(v8::Local<v8::Object> obj, int attr) {
return Nan::To<T>(Nan::Get(obj, attr).ToLocalChecked()).FromJust(); return Nan::To<T>(Nan::Get(obj, attr).ToLocalChecked()).FromJust();
} }
// Create an InputDescriptor instance from a v8::Object describing an input image // Create an InputDescriptor instance from a v8::Object describing an input image
InputDescriptor* CreateInputDescriptor( InputDescriptor* CreateInputDescriptor(
v8::Handle<v8::Object> input, std::vector<v8::Local<v8::Object>> &buffersToPersist); v8::Local<v8::Object> input, std::vector<v8::Local<v8::Object>> &buffersToPersist);
enum class ImageType { enum class ImageType {
JPEG, JPEG,
@@ -101,6 +101,7 @@ namespace sharp {
TIFF, TIFF,
GIF, GIF,
SVG, SVG,
HEIF,
PDF, PDF,
MAGICK, MAGICK,
OPENSLIDE, OPENSLIDE,
@@ -123,6 +124,9 @@ namespace sharp {
bool IsPng(std::string const &str); bool IsPng(std::string const &str);
bool IsWebp(std::string const &str); bool IsWebp(std::string const &str);
bool IsTiff(std::string const &str); bool IsTiff(std::string const &str);
bool IsHeic(std::string const &str);
bool IsHeif(std::string const &str);
bool IsAvif(std::string const &str);
bool IsDz(std::string const &str); bool IsDz(std::string const &str);
bool IsDzZip(std::string const &str); bool IsDzZip(std::string const &str);
bool IsV(std::string const &str); bool IsV(std::string const &str);

View File

@@ -32,8 +32,6 @@
#endif /*HAVE_CONFIG_H*/ #endif /*HAVE_CONFIG_H*/
#include <vips/intl.h> #include <vips/intl.h>
#include <iostream>
#include <vips/vips8> #include <vips/vips8>
VIPS_NAMESPACE_START VIPS_NAMESPACE_START

View File

@@ -563,7 +563,7 @@ VImage::new_from_file( const char *name, VOption *options )
} }
VImage VImage
VImage::new_from_buffer( void *buf, size_t len, const char *option_string, VImage::new_from_buffer( const void *buf, size_t len, const char *option_string,
VOption *options ) VOption *options )
{ {
const char *operation_name; const char *operation_name;
@@ -588,6 +588,13 @@ VImage::new_from_buffer( void *buf, size_t len, const char *option_string,
return( out ); return( out );
} }
VImage
VImage::new_from_buffer( const std::string &buf, const char *option_string,
VOption *options )
{
return( new_from_buffer( buf.c_str(), buf.size(), option_string, options ) );
}
VImage VImage
VImage::new_matrix( int width, int height ) VImage::new_matrix( int width, int height )
{ {

File diff suppressed because it is too large Load Diff

View File

@@ -77,6 +77,9 @@ class MetadataWorker : public Nan::AsyncWorker {
if (image.get_typeof(VIPS_META_PAGE_HEIGHT) == G_TYPE_INT) { if (image.get_typeof(VIPS_META_PAGE_HEIGHT) == G_TYPE_INT) {
baton->pageHeight = image.get_int(VIPS_META_PAGE_HEIGHT); baton->pageHeight = image.get_int(VIPS_META_PAGE_HEIGHT);
} }
if (image.get_typeof("heif-primary") == G_TYPE_INT) {
baton->pagePrimary = image.get_int("heif-primary");
}
baton->hasProfile = sharp::HasProfile(image); baton->hasProfile = sharp::HasProfile(image);
// Derived attributes // Derived attributes
baton->hasAlpha = sharp::HasAlpha(image); baton->hasAlpha = sharp::HasAlpha(image);
@@ -158,6 +161,9 @@ class MetadataWorker : public Nan::AsyncWorker {
if (baton->pageHeight > 0) { if (baton->pageHeight > 0) {
Set(info, New("pageHeight").ToLocalChecked(), New<v8::Uint32>(baton->pageHeight)); Set(info, New("pageHeight").ToLocalChecked(), New<v8::Uint32>(baton->pageHeight));
} }
if (baton->pagePrimary > -1) {
Set(info, New("pagePrimary").ToLocalChecked(), New<v8::Uint32>(baton->pagePrimary));
}
Set(info, New("hasProfile").ToLocalChecked(), New<v8::Boolean>(baton->hasProfile)); Set(info, New("hasProfile").ToLocalChecked(), New<v8::Boolean>(baton->hasProfile));
Set(info, New("hasAlpha").ToLocalChecked(), New<v8::Boolean>(baton->hasAlpha)); Set(info, New("hasAlpha").ToLocalChecked(), New<v8::Boolean>(baton->hasAlpha));
if (baton->orientation > 0) { if (baton->orientation > 0) {

View File

@@ -36,6 +36,7 @@ struct MetadataBaton {
int paletteBitDepth; int paletteBitDepth;
int pages; int pages;
int pageHeight; int pageHeight;
int pagePrimary;
bool hasProfile; bool hasProfile;
bool hasAlpha; bool hasAlpha;
int orientation; int orientation;
@@ -59,6 +60,7 @@ struct MetadataBaton {
paletteBitDepth(0), paletteBitDepth(0),
pages(0), pages(0),
pageHeight(0), pageHeight(0),
pagePrimary(-1),
hasProfile(false), hasProfile(false),
hasAlpha(false), hasAlpha(false),
orientation(0), orientation(0),

View File

@@ -185,6 +185,21 @@ namespace sharp {
0.0, 0.0, 0.0, 1.0)); 0.0, 0.0, 0.0, 1.0));
} }
VImage Modulate(VImage image, double const brightness, double const saturation, int const hue) {
if (HasAlpha(image)) {
// Separate alpha channel
VImage alpha = image[image.bands() - 1];
return RemoveAlpha(image)
.colourspace(VIPS_INTERPRETATION_LCH)
.linear({brightness, saturation, 1}, {0, 0, static_cast<double>(hue)})
.bandjoin(alpha);
} else {
return image
.colourspace(VIPS_INTERPRETATION_LCH)
.linear({brightness, saturation, 1}, {0, 0, static_cast<double>(hue)});
}
}
/* /*
* Sharpen flat and jagged areas. Use sigma of -1.0 for fast sharpen. * Sharpen flat and jagged areas. Use sigma of -1.0 for fast sharpen.
*/ */

View File

@@ -97,6 +97,11 @@ namespace sharp {
*/ */
VImage Recomb(VImage image, std::unique_ptr<double[]> const &matrix); VImage Recomb(VImage image, std::unique_ptr<double[]> const &matrix);
/*
* Modulate brightness, saturation and hue
*/
VImage Modulate(VImage image, double const brightness, double const saturation, int const hue);
} // namespace sharp } // namespace sharp
#endif // SRC_OPERATIONS_H_ #endif // SRC_OPERATIONS_H_

View File

@@ -21,6 +21,8 @@
#include <tuple> #include <tuple>
#include <utility> #include <utility>
#include <vector> #include <vector>
#include <sys/types.h>
#include <sys/stat.h>
#include <vips/vips8> #include <vips/vips8>
#include <node.h> #include <node.h>
@@ -30,6 +32,17 @@
#include "operations.h" #include "operations.h"
#include "pipeline.h" #include "pipeline.h"
#if defined(WIN32)
#define STAT64_STRUCT __stat64
#define STAT64_FUNCTION _stat64
#elif defined(__APPLE__)
#define STAT64_STRUCT stat
#define STAT64_FUNCTION stat
#else
#define STAT64_STRUCT stat64
#define STAT64_FUNCTION stat64
#endif
class PipelineWorker : public Nan::AsyncWorker { class PipelineWorker : public Nan::AsyncWorker {
public: public:
PipelineWorker( PipelineWorker(
@@ -57,16 +70,6 @@ class PipelineWorker : public Nan::AsyncWorker {
// Increment processing task counter // Increment processing task counter
g_atomic_int_inc(&sharp::counterProcess); g_atomic_int_inc(&sharp::counterProcess);
std::map<VipsInterpretation, std::string> profileMap;
// Default sRGB ICC profile from https://packages.debian.org/sid/all/icc-profiles-free/filelist
profileMap.insert(
std::pair<VipsInterpretation, std::string>(VIPS_INTERPRETATION_sRGB,
baton->iccProfilePath + "sRGB.icc"));
// Convert to sRGB using default CMYK profile from http://www.argyllcms.com/cmyk.icm
profileMap.insert(
std::pair<VipsInterpretation, std::string>(VIPS_INTERPRETATION_CMYK,
baton->iccProfilePath + "cmyk.icm"));
try { try {
// Open input // Open input
vips::VImage image; vips::VImage image;
@@ -75,7 +78,8 @@ class PipelineWorker : public Nan::AsyncWorker {
// Limit input images to a given number of pixels, where pixels = width * height // Limit input images to a given number of pixels, where pixels = width * height
// Ignore if 0 // Ignore if 0
if (baton->limitInputPixels > 0 && image.width() * image.height() > baton->limitInputPixels) { if (baton->limitInputPixels > 0 &&
static_cast<uint64_t>(image.width() * image.height()) > static_cast<uint64_t>(baton->limitInputPixels)) {
(baton->err).append("Input image exceeds pixel limit"); (baton->err).append("Input image exceeds pixel limit");
return Error(); return Error();
} }
@@ -94,9 +98,16 @@ class PipelineWorker : public Nan::AsyncWorker {
} }
// Rotate pre-extract // Rotate pre-extract
if (baton->rotateBeforePreExtract && rotation != VIPS_ANGLE_D0) { if (baton->rotateBeforePreExtract) {
image = image.rot(rotation); if (rotation != VIPS_ANGLE_D0) {
sharp::RemoveExifOrientation(image); image = image.rot(rotation);
sharp::RemoveExifOrientation(image);
}
if (baton->rotationAngle != 0.0) {
std::vector<double> background;
std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground);
image = image.rotate(baton->rotationAngle, VImage::option()->set("background", background));
}
} }
// Trim // Trim
@@ -300,17 +311,15 @@ class PipelineWorker : public Nan::AsyncWorker {
if (sharp::HasProfile(image) && image.interpretation() != VIPS_INTERPRETATION_LABS) { if (sharp::HasProfile(image) && image.interpretation() != VIPS_INTERPRETATION_LABS) {
// Convert to sRGB using embedded profile // Convert to sRGB using embedded profile
try { try {
image = image.icc_transform( image = image.icc_transform("srgb", VImage::option()
const_cast<char*>(profileMap[VIPS_INTERPRETATION_sRGB].data()), VImage::option()
->set("embedded", TRUE) ->set("embedded", TRUE)
->set("intent", VIPS_INTENT_PERCEPTUAL)); ->set("intent", VIPS_INTENT_PERCEPTUAL));
} catch(...) { } catch(...) {
// Ignore failure of embedded profile // Ignore failure of embedded profile
} }
} else if (image.interpretation() == VIPS_INTERPRETATION_CMYK) { } else if (image.interpretation() == VIPS_INTERPRETATION_CMYK) {
image = image.icc_transform( image = image.icc_transform("srgb", VImage::option()
const_cast<char*>(profileMap[VIPS_INTERPRETATION_sRGB].data()), VImage::option() ->set("input_profile", "cmyk")
->set("input_profile", profileMap[VIPS_INTERPRETATION_CMYK].data())
->set("intent", VIPS_INTENT_PERCEPTUAL)); ->set("intent", VIPS_INTENT_PERCEPTUAL));
} }
@@ -349,6 +358,7 @@ class PipelineWorker : public Nan::AsyncWorker {
bool const shouldSharpen = baton->sharpenSigma != 0.0; bool const shouldSharpen = baton->sharpenSigma != 0.0;
bool const shouldApplyMedian = baton->medianSize > 0; bool const shouldApplyMedian = baton->medianSize > 0;
bool const shouldComposite = !baton->composite.empty(); bool const shouldComposite = !baton->composite.empty();
bool const shouldModulate = baton->brightness != 1.0 || baton->saturation != 1.0 || baton->hue != 0.0;
if (shouldComposite && !HasAlpha(image)) { if (shouldComposite && !HasAlpha(image)) {
image = sharp::EnsureAlpha(image); image = sharp::EnsureAlpha(image);
@@ -388,12 +398,13 @@ class PipelineWorker : public Nan::AsyncWorker {
->set("kernel", kernel)); ->set("kernel", kernel));
} }
// Rotate // Rotate post-extract 90-angle
if (!baton->rotateBeforePreExtract && rotation != VIPS_ANGLE_D0) { if (!baton->rotateBeforePreExtract && rotation != VIPS_ANGLE_D0) {
image = image.rot(rotation); image = image.rot(rotation);
sharp::RemoveExifOrientation(image); sharp::RemoveExifOrientation(image);
} }
// Flip (mirror about Y axis) // Flip (mirror about Y axis)
if (baton->flip) { if (baton->flip) {
image = image.flip(VIPS_DIRECTION_VERTICAL); image = image.flip(VIPS_DIRECTION_VERTICAL);
@@ -476,8 +487,8 @@ class PipelineWorker : public Nan::AsyncWorker {
} }
} }
// Rotate by degree // Rotate post-extract non-90 angle
if (baton->rotationAngle != 0.0) { if (!baton->rotateBeforePreExtract && baton->rotationAngle != 0.0) {
std::vector<double> background; std::vector<double> background;
std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground); std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground);
image = image.rotate(baton->rotationAngle, VImage::option()->set("background", background)); image = image.rotate(baton->rotationAngle, VImage::option()->set("background", background));
@@ -528,6 +539,10 @@ class PipelineWorker : public Nan::AsyncWorker {
image = sharp::Recomb(image, baton->recombMatrix); image = sharp::Recomb(image, baton->recombMatrix);
} }
if (shouldModulate) {
image = sharp::Modulate(image, baton->brightness, baton->saturation, baton->hue);
}
// Sharpen // Sharpen
if (shouldSharpen) { if (shouldSharpen) {
image = sharp::Sharpen(image, baton->sharpenSigma, baton->sharpenFlat, baton->sharpenJagged); image = sharp::Sharpen(image, baton->sharpenSigma, baton->sharpenFlat, baton->sharpenJagged);
@@ -674,8 +689,8 @@ class PipelineWorker : public Nan::AsyncWorker {
// Convert colourspace, pass the current known interpretation so libvips doesn't have to guess // Convert colourspace, pass the current known interpretation so libvips doesn't have to guess
image = image.colourspace(baton->colourspace, VImage::option()->set("source_space", image.interpretation())); image = image.colourspace(baton->colourspace, VImage::option()->set("source_space", image.interpretation()));
// Transform colours from embedded profile to output profile // Transform colours from embedded profile to output profile
if (baton->withMetadata && sharp::HasProfile(image) && profileMap[baton->colourspace] != std::string()) { if (baton->withMetadata && sharp::HasProfile(image)) {
image = image.icc_transform(const_cast<char*>(profileMap[baton->colourspace].data()), image = image.icc_transform(vips_enum_nick(VIPS_TYPE_INTERPRETATION, baton->colourspace),
VImage::option()->set("embedded", TRUE)); VImage::option()->set("embedded", TRUE));
} }
} }
@@ -741,6 +756,8 @@ class PipelineWorker : public Nan::AsyncWorker {
->set("Q", baton->webpQuality) ->set("Q", baton->webpQuality)
->set("lossless", baton->webpLossless) ->set("lossless", baton->webpLossless)
->set("near_lossless", baton->webpNearLossless) ->set("near_lossless", baton->webpNearLossless)
->set("smart_subsample", baton->webpSmartSubsample)
->set("reduction_effort", baton->webpReductionEffort)
->set("alpha_q", baton->webpAlphaQuality))); ->set("alpha_q", baton->webpAlphaQuality)));
baton->bufferOut = static_cast<char*>(area->data); baton->bufferOut = static_cast<char*>(area->data);
baton->bufferOutLength = area->length; baton->bufferOutLength = area->length;
@@ -774,6 +791,18 @@ class PipelineWorker : public Nan::AsyncWorker {
vips_area_unref(area); vips_area_unref(area);
baton->formatOut = "tiff"; baton->formatOut = "tiff";
baton->channels = std::min(baton->channels, 3); baton->channels = std::min(baton->channels, 3);
} else if (baton->formatOut == "heif" || (baton->formatOut == "input" && inputImageType == ImageType::HEIF)) {
// Write HEIF to buffer
VipsArea *area = VIPS_AREA(image.heifsave_buffer(VImage::option()
->set("strip", !baton->withMetadata)
->set("compression", baton->heifCompression)
->set("Q", baton->heifQuality)
->set("lossless", baton->heifLossless)));
baton->bufferOut = static_cast<char*>(area->data);
baton->bufferOutLength = area->length;
area->free_fn = nullptr;
vips_area_unref(area);
baton->formatOut = "heif";
} else if (baton->formatOut == "raw" || (baton->formatOut == "input" && inputImageType == ImageType::RAW)) { } else if (baton->formatOut == "raw" || (baton->formatOut == "input" && inputImageType == ImageType::RAW)) {
// Write raw, uncompressed image data to buffer // Write raw, uncompressed image data to buffer
if (baton->greyscale || image.interpretation() == VIPS_INTERPRETATION_B_W) { if (baton->greyscale || image.interpretation() == VIPS_INTERPRETATION_B_W) {
@@ -808,6 +837,7 @@ class PipelineWorker : public Nan::AsyncWorker {
bool const isPng = sharp::IsPng(baton->fileOut); bool const isPng = sharp::IsPng(baton->fileOut);
bool const isWebp = sharp::IsWebp(baton->fileOut); bool const isWebp = sharp::IsWebp(baton->fileOut);
bool const isTiff = sharp::IsTiff(baton->fileOut); bool const isTiff = sharp::IsTiff(baton->fileOut);
bool const isHeif = sharp::IsHeif(baton->fileOut);
bool const isDz = sharp::IsDz(baton->fileOut); bool const isDz = sharp::IsDz(baton->fileOut);
bool const isDzZip = sharp::IsDzZip(baton->fileOut); bool const isDzZip = sharp::IsDzZip(baton->fileOut);
bool const isV = sharp::IsV(baton->fileOut); bool const isV = sharp::IsV(baton->fileOut);
@@ -852,6 +882,8 @@ class PipelineWorker : public Nan::AsyncWorker {
->set("Q", baton->webpQuality) ->set("Q", baton->webpQuality)
->set("lossless", baton->webpLossless) ->set("lossless", baton->webpLossless)
->set("near_lossless", baton->webpNearLossless) ->set("near_lossless", baton->webpNearLossless)
->set("smart_subsample", baton->webpSmartSubsample)
->set("reduction_effort", baton->webpReductionEffort)
->set("alpha_q", baton->webpAlphaQuality)); ->set("alpha_q", baton->webpAlphaQuality));
baton->formatOut = "webp"; baton->formatOut = "webp";
} else if (baton->formatOut == "tiff" || (mightMatchInput && isTiff) || } else if (baton->formatOut == "tiff" || (mightMatchInput && isTiff) ||
@@ -874,6 +906,20 @@ class PipelineWorker : public Nan::AsyncWorker {
->set("yres", baton->tiffYres)); ->set("yres", baton->tiffYres));
baton->formatOut = "tiff"; baton->formatOut = "tiff";
baton->channels = std::min(baton->channels, 3); baton->channels = std::min(baton->channels, 3);
} else if (baton->formatOut == "heif" || (mightMatchInput && isHeif) ||
(willMatchInput && inputImageType == ImageType::HEIF)) {
// Write HEIF to file
#ifdef VIPS_TYPE_FOREIGN_HEIF_COMPRESSION
if (sharp::IsAvif(baton->fileOut)) {
baton->heifCompression = VIPS_FOREIGN_HEIF_COMPRESSION_AV1;
}
#endif
image.heifsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
->set("strip", !baton->withMetadata)
->set("Q", baton->heifQuality)
->set("compression", baton->heifCompression)
->set("lossless", baton->heifLossless));
baton->formatOut = "heif";
} else if (baton->formatOut == "dz" || isDz || isDzZip) { } else if (baton->formatOut == "dz" || isDz || isDzZip) {
if (isDzZip) { if (isDzZip) {
baton->tileContainer = VIPS_FOREIGN_DZ_CONTAINER_ZIP; baton->tileContainer = VIPS_FOREIGN_DZ_CONTAINER_ZIP;
@@ -892,7 +938,9 @@ class PipelineWorker : public Nan::AsyncWorker {
{"Q", std::to_string(baton->webpQuality)}, {"Q", std::to_string(baton->webpQuality)},
{"alpha_q", std::to_string(baton->webpAlphaQuality)}, {"alpha_q", std::to_string(baton->webpAlphaQuality)},
{"lossless", baton->webpLossless ? "TRUE" : "FALSE"}, {"lossless", baton->webpLossless ? "TRUE" : "FALSE"},
{"near_lossless", baton->webpNearLossless ? "TRUE" : "FALSE"} {"near_lossless", baton->webpNearLossless ? "TRUE" : "FALSE"},
{"smart_subsample", baton->webpSmartSubsample ? "TRUE" : "FALSE"},
{"reduction_effort", std::to_string(baton->webpReductionEffort)}
}; };
suffix = AssembleSuffixString(".webp", options); suffix = AssembleSuffixString(".webp", options);
} else { } else {
@@ -919,7 +967,8 @@ class PipelineWorker : public Nan::AsyncWorker {
->set("container", baton->tileContainer) ->set("container", baton->tileContainer)
->set("layout", baton->tileLayout) ->set("layout", baton->tileLayout)
->set("suffix", const_cast<char*>(suffix.data())) ->set("suffix", const_cast<char*>(suffix.data()))
->set("angle", CalculateAngleRotation(baton->tileAngle)); ->set("angle", CalculateAngleRotation(baton->tileAngle))
->set("skip_blanks", baton->tileSkipBlanks);
// libvips chooses a default depth based on layout. Instead of replicating that logic here by // libvips chooses a default depth based on layout. Instead of replicating that logic here by
// not passing anything - libvips will handle choice // not passing anything - libvips will handle choice
@@ -999,8 +1048,8 @@ class PipelineWorker : public Nan::AsyncWorker {
argv[2] = info; argv[2] = info;
} else { } else {
// Add file size to info // Add file size to info
GStatBuf st; struct STAT64_STRUCT st;
if (g_stat(baton->fileOut.data(), &st) == 0) { if (STAT64_FUNCTION(baton->fileOut.data(), &st) == 0) {
Set(info, New("size").ToLocalChecked(), New<v8::Uint32>(static_cast<uint32_t>(st.st_size))); Set(info, New("size").ToLocalChecked(), New<v8::Uint32>(static_cast<uint32_t>(st.st_size)));
} }
argv[1] = info; argv[1] = info;
@@ -1135,9 +1184,6 @@ NAN_METHOD(pipeline) {
// Input // Input
baton->input = CreateInputDescriptor(AttrAs<v8::Object>(options, "input"), buffersToPersist); baton->input = CreateInputDescriptor(AttrAs<v8::Object>(options, "input"), buffersToPersist);
// ICC profile to use when input CMYK image has no embedded profile
baton->iccProfilePath = AttrAsStr(options, "iccProfilePath");
baton->accessMethod = AttrTo<bool>(options, "sequentialRead") ? baton->accessMethod = AttrTo<bool>(options, "sequentialRead") ?
VIPS_ACCESS_SEQUENTIAL : VIPS_ACCESS_RANDOM; VIPS_ACCESS_SEQUENTIAL : VIPS_ACCESS_RANDOM;
// Limit input images to a given number of pixels, where pixels = width * height // Limit input images to a given number of pixels, where pixels = width * height
@@ -1210,6 +1256,9 @@ NAN_METHOD(pipeline) {
baton->flattenBackground = AttrAsRgba(options, "flattenBackground"); baton->flattenBackground = AttrAsRgba(options, "flattenBackground");
baton->negate = AttrTo<bool>(options, "negate"); baton->negate = AttrTo<bool>(options, "negate");
baton->blurSigma = AttrTo<double>(options, "blurSigma"); baton->blurSigma = AttrTo<double>(options, "blurSigma");
baton->brightness = AttrTo<double>(options, "brightness");
baton->saturation = AttrTo<double>(options, "saturation");
baton->hue = AttrTo<int32_t>(options, "hue");
baton->medianSize = AttrTo<uint32_t>(options, "medianSize"); baton->medianSize = AttrTo<uint32_t>(options, "medianSize");
baton->sharpenSigma = AttrTo<double>(options, "sharpenSigma"); baton->sharpenSigma = AttrTo<double>(options, "sharpenSigma");
baton->sharpenFlat = AttrTo<double>(options, "sharpenFlat"); baton->sharpenFlat = AttrTo<double>(options, "sharpenFlat");
@@ -1295,6 +1344,8 @@ NAN_METHOD(pipeline) {
baton->webpAlphaQuality = AttrTo<uint32_t>(options, "webpAlphaQuality"); baton->webpAlphaQuality = AttrTo<uint32_t>(options, "webpAlphaQuality");
baton->webpLossless = AttrTo<bool>(options, "webpLossless"); baton->webpLossless = AttrTo<bool>(options, "webpLossless");
baton->webpNearLossless = AttrTo<bool>(options, "webpNearLossless"); baton->webpNearLossless = AttrTo<bool>(options, "webpNearLossless");
baton->webpSmartSubsample = AttrTo<bool>(options, "webpSmartSubsample");
baton->webpReductionEffort = AttrTo<uint32_t>(options, "webpReductionEffort");
baton->tiffQuality = AttrTo<uint32_t>(options, "tiffQuality"); baton->tiffQuality = AttrTo<uint32_t>(options, "tiffQuality");
baton->tiffPyramid = AttrTo<bool>(options, "tiffPyramid"); baton->tiffPyramid = AttrTo<bool>(options, "tiffPyramid");
baton->tiffSquash = AttrTo<bool>(options, "tiffSquash"); baton->tiffSquash = AttrTo<bool>(options, "tiffSquash");
@@ -1310,12 +1361,19 @@ NAN_METHOD(pipeline) {
baton->tiffPredictor = static_cast<VipsForeignTiffPredictor>( baton->tiffPredictor = static_cast<VipsForeignTiffPredictor>(
vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_TIFF_PREDICTOR, vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_TIFF_PREDICTOR,
AttrAsStr(options, "tiffPredictor").data())); AttrAsStr(options, "tiffPredictor").data()));
baton->heifQuality = AttrTo<uint32_t>(options, "heifQuality");
baton->heifLossless = AttrTo<bool>(options, "heifLossless");
#ifdef VIPS_TYPE_FOREIGN_HEIF_COMPRESSION
baton->heifCompression = static_cast<VipsForeignHeifCompression>(
vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_HEIF_COMPRESSION,
AttrAsStr(options, "heifCompression").data()));
#endif
// Tile output // Tile output
baton->tileSize = AttrTo<uint32_t>(options, "tileSize"); baton->tileSize = AttrTo<uint32_t>(options, "tileSize");
baton->tileOverlap = AttrTo<uint32_t>(options, "tileOverlap"); baton->tileOverlap = AttrTo<uint32_t>(options, "tileOverlap");
std::string tileContainer = AttrAsStr(options, "tileContainer"); std::string tileContainer = AttrAsStr(options, "tileContainer");
baton->tileAngle = AttrTo<int32_t>(options, "tileAngle"); baton->tileAngle = AttrTo<int32_t>(options, "tileAngle");
baton->tileSkipBlanks = AttrTo<int32_t>(options, "tileSkipBlanks");
if (tileContainer == "zip") { if (tileContainer == "zip") {
baton->tileContainer = VIPS_FOREIGN_DZ_CONTAINER_ZIP; baton->tileContainer = VIPS_FOREIGN_DZ_CONTAINER_ZIP;
} else { } else {

View File

@@ -53,7 +53,6 @@ struct Composite {
struct PipelineBaton { struct PipelineBaton {
sharp::InputDescriptor *input; sharp::InputDescriptor *input;
std::string iccProfilePath;
int limitInputPixels; int limitInputPixels;
std::string formatOut; std::string formatOut;
std::string fileOut; std::string fileOut;
@@ -87,6 +86,9 @@ struct PipelineBaton {
std::vector<double> flattenBackground; std::vector<double> flattenBackground;
bool negate; bool negate;
double blurSigma; double blurSigma;
double brightness;
double saturation;
int hue;
int medianSize; int medianSize;
double sharpenSigma; double sharpenSigma;
double sharpenFlat; double sharpenFlat;
@@ -135,6 +137,8 @@ struct PipelineBaton {
int webpAlphaQuality; int webpAlphaQuality;
bool webpNearLossless; bool webpNearLossless;
bool webpLossless; bool webpLossless;
bool webpSmartSubsample;
int webpReductionEffort;
int tiffQuality; int tiffQuality;
VipsForeignTiffCompression tiffCompression; VipsForeignTiffCompression tiffCompression;
VipsForeignTiffPredictor tiffPredictor; VipsForeignTiffPredictor tiffPredictor;
@@ -145,6 +149,9 @@ struct PipelineBaton {
int tiffTileWidth; int tiffTileWidth;
double tiffXres; double tiffXres;
double tiffYres; double tiffYres;
int heifQuality;
int heifCompression; // TODO(libvips 8.9.0): VipsForeignHeifCompression
bool heifLossless;
std::string err; std::string err;
bool withMetadata; bool withMetadata;
int withMetadataOrientation; int withMetadataOrientation;
@@ -166,6 +173,7 @@ struct PipelineBaton {
VipsForeignDzLayout tileLayout; VipsForeignDzLayout tileLayout;
std::string tileFormat; std::string tileFormat;
int tileAngle; int tileAngle;
int tileSkipBlanks;
VipsForeignDzDepth tileDepth; VipsForeignDzDepth tileDepth;
std::unique_ptr<double[]> recombMatrix; std::unique_ptr<double[]> recombMatrix;
@@ -189,6 +197,9 @@ struct PipelineBaton {
flattenBackground{ 0.0, 0.0, 0.0 }, flattenBackground{ 0.0, 0.0, 0.0 },
negate(false), negate(false),
blurSigma(0.0), blurSigma(0.0),
brightness(1.0),
saturation(1.0),
hue(0),
medianSize(0), medianSize(0),
sharpenSigma(0.0), sharpenSigma(0.0),
sharpenFlat(1.0), sharpenFlat(1.0),
@@ -231,6 +242,11 @@ struct PipelineBaton {
pngColours(256), pngColours(256),
pngDither(1.0), pngDither(1.0),
webpQuality(80), webpQuality(80),
webpAlphaQuality(100),
webpNearLossless(false),
webpLossless(false),
webpSmartSubsample(false),
webpReductionEffort(4),
tiffQuality(80), tiffQuality(80),
tiffCompression(VIPS_FOREIGN_TIFF_COMPRESSION_JPEG), tiffCompression(VIPS_FOREIGN_TIFF_COMPRESSION_JPEG),
tiffPredictor(VIPS_FOREIGN_TIFF_PREDICTOR_HORIZONTAL), tiffPredictor(VIPS_FOREIGN_TIFF_PREDICTOR_HORIZONTAL),
@@ -241,6 +257,9 @@ struct PipelineBaton {
tiffTileWidth(256), tiffTileWidth(256),
tiffXres(1.0), tiffXres(1.0),
tiffYres(1.0), tiffYres(1.0),
heifQuality(80),
heifCompression(1), // TODO(libvips 8.9.0): VIPS_FOREIGN_HEIF_COMPRESSION_HEVC
heifLossless(false),
withMetadata(false), withMetadata(false),
withMetadataOrientation(-1), withMetadataOrientation(-1),
convKernelWidth(0), convKernelWidth(0),
@@ -259,6 +278,7 @@ struct PipelineBaton {
tileContainer(VIPS_FOREIGN_DZ_CONTAINER_FS), tileContainer(VIPS_FOREIGN_DZ_CONTAINER_FS),
tileLayout(VIPS_FOREIGN_DZ_LAYOUT_DZ), tileLayout(VIPS_FOREIGN_DZ_LAYOUT_DZ),
tileAngle(0), tileAngle(0),
tileSkipBlanks(-1),
tileDepth(VIPS_FOREIGN_DZ_DEPTH_LAST) {} tileDepth(VIPS_FOREIGN_DZ_DEPTH_LAST) {}
}; };

View File

@@ -51,4 +51,4 @@ NAN_MODULE_INIT(init) {
Nan::GetFunction(Nan::New<v8::FunctionTemplate>(stats)).ToLocalChecked()); Nan::GetFunction(Nan::New<v8::FunctionTemplate>(stats)).ToLocalChecked());
} }
NODE_MODULE(sharp, init) NAN_MODULE_WORKER_ENABLED(sharp, init)

View File

@@ -115,7 +115,7 @@ class StatsWorker : public Nan::AsyncWorker {
std::vector<ChannelStats>::iterator it; std::vector<ChannelStats>::iterator it;
int i = 0; int i = 0;
for (it=baton->channelStats.begin() ; it < baton->channelStats.end(); it++, i++) { for (it = baton->channelStats.begin(); it < baton->channelStats.end(); it++, i++) {
v8::Local<v8::Object> channelStat = New<v8::Object>(); v8::Local<v8::Object> channelStat = New<v8::Object>();
Set(channelStat, New("min").ToLocalChecked(), New<v8::Number>(it->min)); Set(channelStat, New("min").ToLocalChecked(), New<v8::Number>(it->min));
Set(channelStat, New("max").ToLocalChecked(), New<v8::Number>(it->max)); Set(channelStat, New("max").ToLocalChecked(), New<v8::Number>(it->max));
@@ -127,7 +127,7 @@ class StatsWorker : public Nan::AsyncWorker {
Set(channelStat, New("minY").ToLocalChecked(), New<v8::Number>(it->minY)); Set(channelStat, New("minY").ToLocalChecked(), New<v8::Number>(it->minY));
Set(channelStat, New("maxX").ToLocalChecked(), New<v8::Number>(it->maxX)); Set(channelStat, New("maxX").ToLocalChecked(), New<v8::Number>(it->maxX));
Set(channelStat, New("maxY").ToLocalChecked(), New<v8::Number>(it->maxY)); Set(channelStat, New("maxY").ToLocalChecked(), New<v8::Number>(it->maxY));
channels->Set(i, channelStat); Set(channels, i, channelStat);
} }
Set(info, New("channels").ToLocalChecked(), channels); Set(info, New("channels").ToLocalChecked(), channels);

View File

@@ -151,7 +151,7 @@ NAN_METHOD(format) {
// Which load/save operations are available for each compressed format? // Which load/save operations are available for each compressed format?
Local<Object> format = New<Object>(); Local<Object> format = New<Object>();
for (std::string f : { for (std::string f : {
"jpeg", "png", "webp", "tiff", "magick", "openslide", "dz", "ppm", "fits", "gif", "svg", "pdf", "v" "jpeg", "png", "webp", "tiff", "magick", "openslide", "dz", "ppm", "fits", "gif", "svg", "heif", "pdf", "v"
}) { }) {
// Input // Input
Local<Boolean> hasInputFile = Local<Boolean> hasInputFile =

View File

@@ -8,17 +8,17 @@
"test": "node perf && node random && node parallel" "test": "node perf && node random && node parallel"
}, },
"devDependencies": { "devDependencies": {
"async": "^2.6.1", "async": "^3.1.0",
"benchmark": "^2.1.4", "benchmark": "^2.1.4",
"gm": "^1.23.1", "gm": "^1.23.1",
"imagemagick": "^0.1.3", "imagemagick": "^0.1.3",
"imagemagick-native": "^1.9.3", "imagemagick-native": "^1.9.3",
"jimp": "^0.5.3", "jimp": "^0.6.4",
"mapnik": "^4.0.1", "mapnik": "^4.2.1",
"semver": "^5.5.1" "semver": "^6.1.2"
}, },
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
"node": ">=6" "node": ">=8.5.0"
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
test/fixtures/expected/modulate-all.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 84 KiB

View File

@@ -80,9 +80,6 @@ module.exports = {
inputPngWithTransparency16bit: getPath('tbgn2c16.png'), // http://www.schaik.com/pngsuite/tbgn2c16.png inputPngWithTransparency16bit: getPath('tbgn2c16.png'), // http://www.schaik.com/pngsuite/tbgn2c16.png
inputPngOverlayLayer0: getPath('alpha-layer-0-background.png'), inputPngOverlayLayer0: getPath('alpha-layer-0-background.png'),
inputPngOverlayLayer1: getPath('alpha-layer-1-fill.png'), inputPngOverlayLayer1: getPath('alpha-layer-1-fill.png'),
inputPngOverlayLayer2: getPath('alpha-layer-2-ink.png'),
inputPngOverlayLayer1LowAlpha: getPath('alpha-layer-1-fill-low-alpha.png'),
inputPngOverlayLayer2LowAlpha: getPath('alpha-layer-2-ink-low-alpha.png'),
inputPngAlphaPremultiplicationSmall: getPath('alpha-premultiply-1024x768-paper.png'), inputPngAlphaPremultiplicationSmall: getPath('alpha-premultiply-1024x768-paper.png'),
inputPngAlphaPremultiplicationLarge: getPath('alpha-premultiply-2048x1536-paper.png'), inputPngAlphaPremultiplicationLarge: getPath('alpha-premultiply-2048x1536-paper.png'),
inputPngBooleanNoAlpha: getPath('bandbool.png'), inputPngBooleanNoAlpha: getPath('bandbool.png'),
@@ -120,6 +117,8 @@ module.exports = {
outputTiff: getPath('output.tiff'), outputTiff: getPath('output.tiff'),
outputZoinks: getPath('output.zoinks'), // an 'unknown' file extension outputZoinks: getPath('output.zoinks'), // an 'unknown' file extension
testPattern: getPath('test-pattern.png'),
// Path for tests requiring human inspection // Path for tests requiring human inspection
path: getPath, path: getPath,

BIN
test/fixtures/test-pattern.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

View File

@@ -16,5 +16,5 @@ for test in ./test/unit/*.js; do
--show-leak-kinds=definite,indirect,possible \ --show-leak-kinds=definite,indirect,possible \
--num-callers=20 \ --num-callers=20 \
--trace-children=yes \ --trace-children=yes \
node node_modules/.bin/mocha --slow=60000 --timeout=120000 --file test/unit/beforeEach.js "$test"; node --expose-gc node_modules/.bin/mocha --slow=60000 --timeout=120000 --file test/unit/beforeEach.js "$test";
done done

View File

@@ -150,7 +150,7 @@
{ {
cond_libwebp_generic cond_libwebp_generic
Memcheck:Cond Memcheck:Cond
obj:/usr/lib/x86_64-linux-gnu/libwebp.so.6.0.2 obj:*/libwebp.so.*
} }
# tiff # tiff
@@ -188,6 +188,14 @@
... ...
fun:FcConfigSubstituteWithPat fun:FcConfigSubstituteWithPat
} }
{
leak_fontconfig_init
Memcheck:Leak
match-leak-kinds: indirect
fun:calloc
...
fun:FcInitLoadConfigAndFonts
}
# libvips # libvips
{ {
@@ -220,6 +228,13 @@
fun:write_webp.constprop.1 fun:write_webp.constprop.1
fun:vips__webp_write_buffer fun:vips__webp_write_buffer
} }
{
value_libvips_start_thread
Memcheck:Value8
obj:*/libvips.so.*
fun:start_thread
fun:clone
}
{ {
cond_libvips_vips_cast_gen cond_libvips_vips_cast_gen
Memcheck:Cond Memcheck:Cond
@@ -261,6 +276,14 @@
... ...
fun:write_webp_image fun:write_webp_image
} }
{
param_libvips_write_buf
Memcheck:Param
write(buf)
fun:write
...
fun:start_thread
}
{ {
leak_libvips_init leak_libvips_init
Memcheck:Leak Memcheck:Leak
@@ -284,6 +307,13 @@
... ...
fun:uv__fs_work fun:uv__fs_work
} }
{
param_libuv_epoll_ctl
Memcheck:Param
epoll_ctl(event)
fun:epoll_ctl
fun:uv__io_poll
}
{ {
cond_libuv_work_done cond_libuv_work_done
Memcheck:Cond Memcheck:Cond
@@ -434,6 +464,30 @@
... ...
fun:_ZN4node12NodePlatformC1EiPN2v817TracingControllerE fun:_ZN4node12NodePlatformC1EiPN2v817TracingControllerE
} }
{
leak_nodejs_start_isolate_data
Memcheck:Leak
match-leak-kinds: possible
fun:_Znwm
...
fun:_ZN4node5StartEPN2v87IsolateEPNS_11IsolateDataERKSt6vectorISsSaISsEES9_
}
{
leak_nodejs_runtime_stackguard_object_isolate
Memcheck:Leak
match-leak-kinds: possible
fun:_Znwm
...
fun:_ZN2v88internal18Runtime_StackGuardEiPPNS0_6ObjectEPNS0_7IsolateE
}
{
leak_nodejs_builtin_handleapicall_object_isolate
Memcheck:Leak
match-leak-kinds: possible
fun:_Znwm
...
fun:_ZN2v88internal21Builtin_HandleApiCallEiPPNS0_6ObjectEPNS0_7IsolateE
}
{ {
param_nodejs_delayed_task_scheduler param_nodejs_delayed_task_scheduler
Memcheck:Param Memcheck:Param

View File

@@ -5,8 +5,16 @@ const sharp = require('../../');
const usingCache = detectLibc.family !== detectLibc.MUSL; const usingCache = detectLibc.family !== detectLibc.MUSL;
const usingSimd = !process.env.G_DEBUG; const usingSimd = !process.env.G_DEBUG;
const concurrency = detectLibc.family === detectLibc.MUSL ? 1 : undefined;
beforeEach(function () { beforeEach(function () {
sharp.cache(usingCache); sharp.cache(usingCache);
sharp.simd(usingSimd); sharp.simd(usingSimd);
sharp.concurrency(concurrency);
});
afterEach(function () {
if (global.gc) {
global.gc();
}
}); });

View File

@@ -6,6 +6,18 @@ const sharp = require('../../');
const fixtures = require('../fixtures'); const fixtures = require('../fixtures');
describe('Extend', function () { describe('Extend', function () {
it('extend all sides equally via a single value', function (done) {
sharp(fixtures.inputJpg)
.resize(120)
.extend(10)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(140, info.width);
assert.strictEqual(118, info.height);
fixtures.assertSimilar(fixtures.expected('extend-equal-single.jpg'), data, done);
});
});
it('extend all sides equally with RGB', function (done) { it('extend all sides equally with RGB', function (done) {
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)
.resize(120) .resize(120)

View File

@@ -121,6 +121,32 @@ describe('Partial image extraction', function () {
}); });
}); });
it('Extract then rotate non-90 anagle', function (done) {
sharp(fixtures.inputPngWithGreyAlpha)
.extract({ left: 20, top: 10, width: 380, height: 280 })
.rotate(45)
.jpeg()
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(467, info.width);
assert.strictEqual(467, info.height);
fixtures.assertSimilar(fixtures.expected('extract-rotate-45.jpg'), data, done);
});
});
it('Rotate then extract non-90 angle', function (done) {
sharp(fixtures.inputPngWithGreyAlpha)
.rotate(45)
.extract({ left: 20, top: 10, width: 380, height: 280 })
.jpeg()
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(380, info.width);
assert.strictEqual(280, info.height);
fixtures.assertSimilar(fixtures.expected('rotate-extract-45.jpg'), data, { threshold: 7 }, done);
});
});
describe('Invalid parameters', function () { describe('Invalid parameters', function () {
describe('using the legacy extract(top,left,width,height) syntax', function () { describe('using the legacy extract(top,left,width,height) syntax', function () {
it('String top', function () { it('String top', function () {

View File

@@ -1,6 +1,7 @@
'use strict'; 'use strict';
const assert = require('assert'); const assert = require('assert');
const fs = require('fs');
const sharp = require('../../'); const sharp = require('../../');
const fixtures = require('../fixtures'); const fixtures = require('../fixtures');
@@ -72,4 +73,10 @@ describe('failOnError', function () {
done(err.message.includes('VipsJpeg: Premature end of JPEG file') ? undefined : err); done(err.message.includes('VipsJpeg: Premature end of JPEG file') ? undefined : err);
}); });
}); });
it('handles stream-based input', function () {
const writable = sharp({ failOnError: false });
fs.createReadStream(fixtures.inputJpgTruncated).pipe(writable);
return writable.toBuffer();
});
}); });

79
test/unit/heif.js Normal file
View File

@@ -0,0 +1,79 @@
'use strict';
const assert = require('assert');
const sharp = require('../../');
const formatHeifOutputBuffer = sharp.format.heif.output.buffer;
describe('HEIF (experimental)', () => {
describe('Stubbed without support for HEIF', () => {
before(() => {
sharp.format.heif.output.buffer = false;
});
after(() => {
sharp.format.heif.output.buffer = formatHeifOutputBuffer;
});
it('should throw an error', () => {
assert.throws(() => {
sharp().heif();
});
});
});
describe('Stubbed with support for HEIF', () => {
before(() => {
sharp.format.heif.output.buffer = true;
});
after(() => {
sharp.format.heif.output.buffer = formatHeifOutputBuffer;
});
it('called without options does not throw an error', () => {
assert.doesNotThrow(() => {
sharp().heif();
});
});
it('valid quality does not throw an error', () => {
assert.doesNotThrow(() => {
sharp().heif({ quality: 50 });
});
});
it('invalid quality should throw an error', () => {
assert.throws(() => {
sharp().heif({ quality: 101 });
});
});
it('non-numeric quality should throw an error', () => {
assert.throws(() => {
sharp().heif({ quality: 'fail' });
});
});
it('valid lossless does not throw an error', () => {
assert.doesNotThrow(() => {
sharp().heif({ lossless: true });
});
});
it('non-boolean lossless should throw an error', () => {
assert.throws(() => {
sharp().heif({ lossless: 'fail' });
});
});
it('valid compression does not throw an error', () => {
assert.doesNotThrow(() => {
sharp().heif({ compression: 'avc' });
});
});
it('unknown compression should throw an error', () => {
assert.throws(() => {
sharp().heif({ compression: 'fail' });
});
});
it('invalid compression should throw an error', () => {
assert.throws(() => {
sharp().heif({ compression: 1 });
});
});
});
});

View File

@@ -392,6 +392,22 @@ describe('Input/output', function () {
}); });
}); });
it('Stream input with corrupt header fails gracefully', function (done) {
const transformer = sharp();
transformer
.toBuffer()
.then(function () {
done(new Error('Unexpectedly resolved Promise'));
})
.catch(function (err) {
assert.strictEqual(true, !!err);
done();
});
fs
.createReadStream(fixtures.inputJpgWithCorruptHeader)
.pipe(transformer);
});
describe('Output filename with unknown extension', function () { describe('Output filename with unknown extension', function () {
it('Match JPEG input', function (done) { it('Match JPEG input', function (done) {
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)

View File

@@ -75,8 +75,8 @@ describe('Image metadata', function () {
// XMP // XMP
assert.strictEqual('object', typeof metadata.xmp); assert.strictEqual('object', typeof metadata.xmp);
assert.strictEqual(true, metadata.xmp instanceof Buffer); assert.strictEqual(true, metadata.xmp instanceof Buffer);
assert.strictEqual(12495, metadata.xmp.byteLength); assert.strictEqual(12466, metadata.xmp.byteLength);
assert.strictEqual(metadata.xmp.indexOf(Buffer.from('http://ns.adobe.com/xap/1.0')), 0); assert.strictEqual(metadata.xmp.indexOf(Buffer.from('<?xpacket begin="')), 0);
done(); done();
}); });
}); });

125
test/unit/modulate.js Normal file
View File

@@ -0,0 +1,125 @@
'use strict';
const sharp = require('../../');
const assert = require('assert');
const fixtures = require('../fixtures');
describe('Modulate', function () {
describe('Invalid options', function () {
[
null,
undefined,
10,
{ brightness: -1 },
{ brightness: '50%' },
{ brightness: null },
{ saturation: -1 },
{ saturation: '50%' },
{ saturation: null },
{ hue: '50deg' },
{ hue: 1.5 },
{ hue: null }
].forEach(function (options) {
it('should throw', function () {
assert.throws(function () {
sharp(fixtures.inputJpg).modulate(options);
});
});
});
});
it('should be able to hue-rotate', function () {
const base = 'modulate-hue-120.jpg';
const actual = fixtures.path('output.' + base);
const expected = fixtures.expected(base);
return sharp(fixtures.inputJpg)
.modulate({ hue: 120 })
.toFile(actual)
.then(function () {
fixtures.assertMaxColourDistance(actual, expected, 25);
});
});
it('should be able to brighten', function () {
const base = 'modulate-brightness-2.jpg';
const actual = fixtures.path('output.' + base);
const expected = fixtures.expected(base);
return sharp(fixtures.inputJpg)
.modulate({ brightness: 2 })
.toFile(actual)
.then(function () {
fixtures.assertMaxColourDistance(actual, expected, 25);
});
});
it('should be able to unbrighten', function () {
const base = 'modulate-brightness-0-5.jpg';
const actual = fixtures.path('output.' + base);
const expected = fixtures.expected(base);
return sharp(fixtures.inputJpg)
.modulate({ brightness: 0.5 })
.toFile(actual)
.then(function () {
fixtures.assertMaxColourDistance(actual, expected, 25);
});
});
it('should be able to saturate', function () {
const base = 'modulate-saturation-2.jpg';
const actual = fixtures.path('output.' + base);
const expected = fixtures.expected(base);
return sharp(fixtures.inputJpg)
.modulate({ saturation: 2 })
.toFile(actual)
.then(function () {
fixtures.assertMaxColourDistance(actual, expected, 30);
});
});
it('should be able to desaturate', function () {
const base = 'modulate-saturation-0.5.jpg';
const actual = fixtures.path('output.' + base);
const expected = fixtures.expected(base);
return sharp(fixtures.inputJpg)
.modulate({ saturation: 0.5 })
.toFile(actual)
.then(function () {
fixtures.assertMaxColourDistance(actual, expected, 25);
});
});
it('should be able to modulate all channels', function () {
const base = 'modulate-all.jpg';
const actual = fixtures.path('output.' + base);
const expected = fixtures.expected(base);
return sharp(fixtures.inputJpg)
.modulate({ brightness: 2, saturation: 0.5, hue: 180 })
.toFile(actual)
.then(function () {
fixtures.assertMaxColourDistance(actual, expected, 25);
});
});
describe('hue-rotate', function (done) {
[30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330, 360].forEach(function (angle) {
it('should properly hue rotate by ' + angle + 'deg', function () {
const base = 'modulate-hue-angle-' + angle + '.png';
const actual = fixtures.path('output.' + base);
const expected = fixtures.expected(base);
return sharp(fixtures.testPattern)
.modulate({ hue: angle })
.toFile(actual)
.then(function () {
fixtures.assertMaxColourDistance(actual, expected, 25);
});
});
});
});
});

View File

@@ -1,589 +0,0 @@
'use strict';
const fs = require('fs');
const assert = require('assert');
const fixtures = require('../fixtures');
const sharp = require('../../');
// Helpers
const getPaths = function (baseName, extension) {
if (typeof extension === 'undefined') {
extension = 'png';
}
return {
actual: fixtures.path('output.' + baseName + '.' + extension),
expected: fixtures.expected(baseName + '.' + extension)
};
};
// Test
describe('Overlays', function () {
it('Overlay transparent PNG file on solid background', function (done) {
const paths = getPaths('alpha-layer-01');
sharp(fixtures.inputPngOverlayLayer0)
.overlayWith(fixtures.inputPngOverlayLayer1)
.toFile(paths.actual, function (error) {
if (error) return done(error);
fixtures.assertMaxColourDistance(paths.actual, paths.expected);
done();
});
});
it('Overlay transparent PNG Buffer on solid background', function (done) {
const paths = getPaths('alpha-layer-01');
sharp(fixtures.inputPngOverlayLayer0)
.overlayWith(fs.readFileSync(fixtures.inputPngOverlayLayer1))
.toFile(paths.actual, function (error) {
if (error) return done(error);
fixtures.assertMaxColourDistance(paths.actual, paths.expected);
done();
});
});
it('Overlay low-alpha transparent PNG on solid background', function (done) {
const paths = getPaths('alpha-layer-01-low-alpha');
sharp(fixtures.inputPngOverlayLayer0)
.overlayWith(fixtures.inputPngOverlayLayer1LowAlpha)
.toFile(paths.actual, function (error) {
if (error) return done(error);
fixtures.assertMaxColourDistance(paths.actual, paths.expected);
done();
});
});
it('Composite three transparent PNGs into one', function (done) {
const paths = getPaths('alpha-layer-012');
sharp(fixtures.inputPngOverlayLayer0)
.overlayWith(fixtures.inputPngOverlayLayer1)
.toBuffer(function (error, data) {
if (error) return done(error);
sharp(data)
.overlayWith(fixtures.inputPngOverlayLayer2)
.toFile(paths.actual, function (error) {
if (error) return done(error);
fixtures.assertMaxColourDistance(paths.actual, paths.expected);
done();
});
});
});
it('Composite two transparent PNGs into one', function (done) {
const paths = getPaths('alpha-layer-12');
sharp(fixtures.inputPngOverlayLayer1)
.overlayWith(fixtures.inputPngOverlayLayer2)
.toFile(paths.actual, function (error) {
if (error) return done(error);
fixtures.assertMaxColourDistance(paths.actual, paths.expected);
done();
});
});
it('Composite two low-alpha transparent PNGs into one', function (done) {
const paths = getPaths('alpha-layer-12-low-alpha');
sharp(fixtures.inputPngOverlayLayer1LowAlpha)
.overlayWith(fixtures.inputPngOverlayLayer2LowAlpha)
.toFile(paths.actual, function (error) {
if (error) return done(error);
fixtures.assertMaxColourDistance(paths.actual, paths.expected, 2);
done();
});
});
it('Composite three low-alpha transparent PNGs into one', function (done) {
const paths = getPaths('alpha-layer-012-low-alpha');
sharp(fixtures.inputPngOverlayLayer0)
.overlayWith(fixtures.inputPngOverlayLayer1LowAlpha)
.toBuffer(function (error, data) {
if (error) return done(error);
sharp(data)
.overlayWith(fixtures.inputPngOverlayLayer2LowAlpha)
.toFile(paths.actual, function (error) {
if (error) return done(error);
fixtures.assertMaxColourDistance(paths.actual, paths.expected);
done();
});
});
});
it('Composite rgb+alpha PNG onto JPEG', function (done) {
const paths = getPaths('overlay-jpeg-with-rgb', 'jpg');
sharp(fixtures.inputJpg)
.resize(2048, 1536)
.overlayWith(fixtures.inputPngOverlayLayer1)
.toFile(paths.actual, function (error, info) {
if (error) return done(error);
fixtures.assertMaxColourDistance(paths.actual, paths.expected, 102);
done();
});
});
it('Composite greyscale+alpha PNG onto JPEG', function (done) {
const paths = getPaths('overlay-jpeg-with-greyscale', 'jpg');
sharp(fixtures.inputJpg)
.resize(400, 300)
.overlayWith(fixtures.inputPngWithGreyAlpha)
.toFile(paths.actual, function (error, info) {
if (error) return done(error);
fixtures.assertMaxColourDistance(paths.actual, paths.expected, 102);
done();
});
});
it('Composite WebP onto JPEG', function (done) {
const paths = getPaths('overlay-jpeg-with-webp', 'jpg');
sharp(fixtures.inputJpg)
.resize(300, 300)
.overlayWith(fixtures.inputWebPWithTransparency)
.toFile(paths.actual, function (error, info) {
if (error) return done(error);
fixtures.assertMaxColourDistance(paths.actual, paths.expected, 102);
done();
});
});
it('Composite JPEG onto PNG, ensure premultiply', function (done) {
sharp(fixtures.inputPngOverlayLayer1)
.overlayWith(fixtures.inputJpgWithLandscapeExif1)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, info.premultiplied);
done();
});
});
it('Composite opaque JPEG onto JPEG, ensure premultiply', function (done) {
sharp(fixtures.inputJpg)
.overlayWith(fixtures.inputJpgWithLandscapeExif1)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, info.premultiplied);
done();
});
});
it('Fail when overlay is larger', function (done) {
sharp(fixtures.inputJpg)
.resize(320)
.overlayWith(fixtures.inputPngOverlayLayer1)
.toBuffer(function (error) {
assert.strictEqual(true, error instanceof Error);
done();
});
});
it('Fail with empty String parameter', function () {
assert.throws(function () {
sharp().overlayWith('');
});
});
it('Fail with non-String parameter', function () {
assert.throws(function () {
sharp().overlayWith(1);
});
});
it('Fail with unsupported gravity', function () {
assert.throws(function () {
sharp()
.overlayWith(fixtures.inputPngOverlayLayer1, {
gravity: 9
});
});
});
it('Empty options', function () {
assert.doesNotThrow(function () {
sharp().overlayWith(fixtures.inputPngOverlayLayer1, {});
});
});
describe('Overlay with numeric gravity', function () {
Object.keys(sharp.gravity).forEach(function (gravity) {
it(gravity, function (done) {
const expected = fixtures.expected('overlay-gravity-' + gravity + '.jpg');
sharp(fixtures.inputJpg)
.resize(80)
.overlayWith(fixtures.inputPngWithTransparency16bit, {
gravity: gravity
})
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(80, info.width);
assert.strictEqual(65, info.height);
assert.strictEqual(3, info.channels);
fixtures.assertSimilar(expected, data, done);
});
});
});
});
describe('Overlay with string-based gravity', function () {
Object.keys(sharp.gravity).forEach(function (gravity) {
it(gravity, function (done) {
const expected = fixtures.expected('overlay-gravity-' + gravity + '.jpg');
sharp(fixtures.inputJpg)
.resize(80)
.overlayWith(fixtures.inputPngWithTransparency16bit, {
gravity: sharp.gravity[gravity]
})
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(80, info.width);
assert.strictEqual(65, info.height);
assert.strictEqual(3, info.channels);
fixtures.assertSimilar(expected, data, done);
});
});
});
});
describe('Overlay with tile enabled and gravity', function () {
Object.keys(sharp.gravity).forEach(function (gravity) {
it(gravity, function (done) {
const expected = fixtures.expected('overlay-tile-gravity-' + gravity + '.jpg');
sharp(fixtures.inputJpg)
.resize(80)
.overlayWith(fixtures.inputPngWithTransparency16bit, {
tile: true,
gravity: gravity
})
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(80, info.width);
assert.strictEqual(65, info.height);
assert.strictEqual(3, info.channels);
fixtures.assertSimilar(expected, data, done);
});
});
});
});
describe('Overlay with top-left offsets', function () {
it('Overlay with 10px top & 10px left offsets', function (done) {
const expected = fixtures.expected('overlay-valid-offsets-10-10.jpg');
sharp(fixtures.inputJpg)
.resize(400)
.overlayWith(fixtures.inputPngWithTransparency16bit, {
top: 10,
left: 10
})
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3, info.channels);
fixtures.assertSimilar(expected, data, done);
});
});
it('Overlay with 100px top & 300px left offsets', function (done) {
const expected = fixtures.expected('overlay-valid-offsets-100-300.jpg');
sharp(fixtures.inputJpg)
.resize(400)
.overlayWith(fixtures.inputPngWithTransparency16bit, {
top: 100,
left: 300
})
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3, info.channels);
fixtures.assertSimilar(expected, data, done);
});
});
it('Overlay with only top offset', function () {
assert.throws(function () {
sharp(fixtures.inputJpg)
.resize(400)
.overlayWith(fixtures.inputPngWithTransparency16bit, {
top: 1000
});
});
});
it('Overlay with only left offset', function () {
assert.throws(function () {
sharp(fixtures.inputJpg)
.resize(400)
.overlayWith(fixtures.inputPngWithTransparency16bit, {
left: 1000
});
});
});
it('Overlay with negative offsets', function () {
assert.throws(function () {
sharp(fixtures.inputJpg)
.resize(400)
.overlayWith(fixtures.inputPngWithTransparency16bit, {
top: -1000,
left: -1000
});
});
});
it('Overlay with 0 offset', function (done) {
const expected = fixtures.expected('overlay-offset-0.jpg');
sharp(fixtures.inputJpg)
.resize(400)
.overlayWith(fixtures.inputPngWithTransparency16bit, {
top: 0,
left: 0
})
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3, info.channels);
fixtures.assertSimilar(expected, data, done);
});
});
it('Overlay with offset and gravity', function (done) {
const expected = fixtures.expected('overlay-offset-with-gravity.jpg');
sharp(fixtures.inputJpg)
.resize(400)
.overlayWith(fixtures.inputPngWithTransparency16bit, {
left: 10,
top: 10,
gravity: 4
})
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3, info.channels);
fixtures.assertSimilar(expected, data, done);
});
});
it('Overlay with offset and gravity and tile', function (done) {
const expected = fixtures.expected('overlay-offset-with-gravity-tile.jpg');
sharp(fixtures.inputJpg)
.resize(400)
.overlayWith(fixtures.inputPngWithTransparency16bit, {
left: 10,
top: 10,
gravity: 4,
tile: true
})
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3, info.channels);
fixtures.assertSimilar(expected, data, done);
});
});
it('Overlay with offset and tile', function (done) {
const expected = fixtures.expected('overlay-offset-with-tile.jpg');
sharp(fixtures.inputJpg)
.resize(400)
.overlayWith(fixtures.inputPngWithTransparency16bit, {
left: 10,
top: 10,
tile: true
})
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3, info.channels);
fixtures.assertSimilar(expected, data, done);
});
});
it('Overlay with invalid tile option', function () {
assert.throws(function () {
sharp().overlayWith('ignore', { tile: 1 });
});
});
it('Overlay with very large offset', function (done) {
const expected = fixtures.expected('overlay-very-large-offset.jpg');
sharp(fixtures.inputJpg)
.resize(400)
.overlayWith(fixtures.inputPngWithTransparency16bit, {
left: 10000,
top: 10000
})
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3, info.channels);
fixtures.assertSimilar(expected, data, done);
});
});
it('Overlay 100x100 with 50x50 so bottom edges meet', function (done) {
sharp(fixtures.inputJpg)
.resize(50, 50)
.toBuffer(function (err, overlay) {
if (err) throw err;
sharp(fixtures.inputJpgWithLandscapeExif1)
.resize(100, 100)
.overlayWith(overlay, {
top: 50,
left: 40
})
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(100, info.width);
assert.strictEqual(100, info.height);
fixtures.assertSimilar(fixtures.expected('overlay-bottom-edges-meet.jpg'), data, done);
});
});
});
});
it('With tile enabled and image rotated 90 degrees', function (done) {
const expected = fixtures.expected('overlay-tile-rotated90.jpg');
sharp(fixtures.inputJpg)
.rotate(90)
.resize(80)
.overlayWith(fixtures.inputPngWithTransparency16bit, {
tile: true
})
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(80, info.width);
assert.strictEqual(98, info.height);
assert.strictEqual(3, info.channels);
fixtures.assertSimilar(expected, data, done);
});
});
it('With tile enabled and image rotated 90 degrees and gravity northwest', function (done) {
const expected = fixtures.expected('overlay-tile-rotated90-gravity-northwest.jpg');
sharp(fixtures.inputJpg)
.rotate(90)
.resize(80)
.overlayWith(fixtures.inputPngWithTransparency16bit, {
tile: true,
gravity: 'northwest'
})
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(80, info.width);
assert.strictEqual(98, info.height);
assert.strictEqual(3, info.channels);
fixtures.assertSimilar(expected, data, done);
});
});
describe('Overlay with cutout enabled and gravity', function () {
Object.keys(sharp.gravity).forEach(function (gravity) {
it(gravity, function (done) {
const expected = fixtures.expected('overlay-cutout-gravity-' + gravity + '.jpg');
sharp(fixtures.inputJpg)
.resize(80)
.overlayWith(fixtures.inputPngWithTransparency16bit, {
cutout: true,
gravity: gravity
})
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(80, info.width);
assert.strictEqual(65, info.height);
assert.strictEqual(3, info.channels);
fixtures.assertSimilar(expected, data, done);
});
});
});
});
it('With cutout enabled and image rotated 90 degrees', function (done) {
const expected = fixtures.expected('overlay-cutout-rotated90.jpg');
sharp(fixtures.inputJpg)
.rotate(90)
.resize(80)
.overlayWith(fixtures.inputPngWithTransparency16bit, {
cutout: true
})
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(80, info.width);
assert.strictEqual(98, info.height);
assert.strictEqual(3, info.channels);
fixtures.assertSimilar(expected, data, done);
});
});
it('With cutout enabled and image rotated 90 degrees and gravity northwest', function (done) {
const expected = fixtures.expected('overlay-cutout-rotated90-gravity-northwest.jpg');
sharp(fixtures.inputJpg)
.rotate(90)
.resize(80)
.overlayWith(fixtures.inputPngWithTransparency16bit, {
cutout: true,
gravity: 'northwest'
})
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(80, info.width);
assert.strictEqual(98, info.height);
assert.strictEqual(3, info.channels);
fixtures.assertSimilar(expected, data, done);
});
});
it('Composite RGBA raw buffer onto JPEG', function (done) {
sharp(fixtures.inputPngOverlayLayer1)
.raw()
.toBuffer(function (err, data, info) {
if (err) throw err;
sharp(fixtures.inputJpg)
.resize(2048, 1536)
.overlayWith(data, { raw: info })
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, info.premultiplied);
fixtures.assertSimilar(fixtures.expected('overlay-jpeg-with-rgb.jpg'), data, done);
});
});
});
it('Returns an error when called with an invalid file', function (done) {
sharp(fixtures.inputJpg)
.overlayWith('notfound.png')
.toBuffer(function (err) {
assert(err instanceof Error);
done();
});
});
it('Composite JPEG onto JPEG', function (done) {
sharp(fixtures.inputJpg)
.resize(480, 320)
.overlayWith(fixtures.inputJpgBooleanTest)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(480, info.width);
assert.strictEqual(320, info.height);
assert.strictEqual(3, info.channels);
assert.strictEqual(true, info.premultiplied);
fixtures.assertSimilar(fixtures.expected('overlay-jpeg-with-jpeg.jpg'), data, done);
});
});
});

View File

@@ -338,7 +338,7 @@ describe('Resize fit=cover', function () {
assert.strictEqual(3, info.channels); assert.strictEqual(3, info.channels);
assert.strictEqual(80, info.width); assert.strictEqual(80, info.width);
assert.strictEqual(320, info.height); assert.strictEqual(320, info.height);
assert.strictEqual(-143, info.cropOffsetLeft); assert.strictEqual(-107, info.cropOffsetLeft);
assert.strictEqual(0, info.cropOffsetTop); assert.strictEqual(0, info.cropOffsetTop);
fixtures.assertSimilar(fixtures.expected('crop-strategy-attention.jpg'), data, done); fixtures.assertSimilar(fixtures.expected('crop-strategy-attention.jpg'), data, done);
}); });

Some files were not shown because too many files have changed in this diff Show More