Compare commits

...

43 Commits

Author SHA1 Message Date
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
Lovell Fuller
18afcf5f90 Release v0.22.0 2019-03-18 23:26:39 +00:00
Lovell Fuller
87a422942d Pin prebuild due to breaking change in 8.2.0 2019-03-18 23:10:33 +00:00
Lovell Fuller
ac515121e5 Release v0.22.0 2019-03-18 21:31:46 +00:00
Lovell Fuller
2bfea0ad76 Docs: refresh usage examples 2019-03-18 21:29:17 +00:00
Lovell Fuller
83cdb558f6 Allow Stream-based input of raw pixel data #1579 2019-03-18 20:15:18 +00:00
Lovell Fuller
9ee377963e Improve error message if libvips tarball is corrupt 2019-03-17 23:07:58 +00:00
Lovell Fuller
9cc06c887b Add support for pages option for multi-page input #1566 2019-03-17 16:37:27 +00:00
Lovell Fuller
7457b50373 Remove unused shared library 2019-03-15 15:58:25 +00:00
Lovell Fuller
6387fb79b1 Small improvements to input and install docs, bump deps 2019-03-15 15:48:55 +00:00
Lovell Fuller
54e5514b9a Bump dependencies to latest 2019-03-10 18:14:43 +00:00
Lovell Fuller
1e4597c284 Changelog entry for #1595 (plus add GIF) 2019-03-10 17:26:26 +00:00
Lovell Fuller
7cafd4386c Add composite op, supporting multiple images and blend modes #728 2019-03-09 22:46:23 +00:00
Lovell Fuller
e3549ba28c Remove functions previously deprecated in v0.21.0
background, crop, embed, ignoreAspectRatio, max, min, withoutEnlargement
2019-03-01 23:43:35 +00:00
Lovell Fuller
d1bbe62e52 Rename armv8 as arm64v8 to match Node's process.arch 2019-03-01 23:43:35 +00:00
Lovell Fuller
36af74a09b Upgrade to libvips v8.7.4 2019-03-01 23:43:35 +00:00
Fabrizio Ruggeri
5afe02be60 Allow page input option to be set for PDF (#1595) 2019-03-01 23:29:34 +00:00
Jack Cross
2262959673 Docs: add missing comma to extend example (#1588) 2019-02-27 11:03:02 +00:00
Lovell Fuller
ba3f914445 Document support for animated WebP in metadata pages 2019-01-27 21:01:49 +00:00
Lovell Fuller
770be35c44 Tests: add a couple of extra leak suppressions for Node 2019-01-27 20:48:17 +00:00
Lovell Fuller
cc9f2b90fd Docs: use absolute URL for logo 2019-01-19 15:15:22 +00:00
75 changed files with 1297 additions and 1685 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.23.0 | vision | | v0.23.0 | vision |
| v0.24.0 | wit |
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

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?

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

@@ -0,0 +1,14 @@
---
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?

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?

View File

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

View File

@@ -18,6 +18,12 @@ matrix:
sudo: false sudo: false
language: node_js language: node_js
node_js: "10" node_js: "10"
- name: "Linux (glibc) - Node 12"
os: linux
dist: trusty
sudo: false
language: node_js
node_js: "12"
after_success: after_success:
- npm install coveralls - npm install coveralls
- cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js
@@ -57,6 +63,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 12"
os: linux
dist: trusty
sudo: true
language: minimal
before_install:
- 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
install: sudo docker exec sharp sh -c "npm install --unsafe-perm"
script: sudo docker exec sharp sh -c "npm test"
- name: "OS X - Node 6" - name: "OS X - Node 6"
os: osx os: osx
osx_image: xcode9.2 osx_image: xcode9.2
@@ -77,3 +93,8 @@ matrix:
osx_image: xcode9.2 osx_image: xcode9.2
language: node_js language: node_js
node_js: "11" node_js: "11"
- name: "OS X - Node 12"
os: osx
osx_image: xcode9.2
language: node_js
node_js: "12"

View File

@@ -1,15 +1,11 @@
# sharp # sharp
<img src="docs/image/sharp-logo.svg" width="160" height="160" alt="sharp logo" align="right"> <img src="https://raw.githubusercontent.com/lovell/sharp/master/docs/image/sharp-logo.svg?sanitize=true" width="160" height="160" alt="sharp logo" align="right">
```sh ```sh
npm install sharp npm install sharp
``` ```
```sh
yarn add sharp
```
The typical use case for this high speed Node.js module The typical use case for this high speed Node.js module
is to convert large images in common formats to is to convert large images in common formats to
smaller, web-friendly JPEG, PNG and WebP images of varying dimensions. smaller, web-friendly JPEG, PNG and WebP images of varying dimensions.
@@ -24,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 6, 8, 10, 11 and 12
do not require any additional install or runtime dependencies. do not require any additional install or runtime dependencies.
## Examples ## Examples
@@ -33,22 +29,42 @@ do not require any additional install or runtime dependencies.
const sharp = require('sharp'); const sharp = require('sharp');
``` ```
### Callback
```javascript ```javascript
sharp(inputBuffer) sharp(inputBuffer)
.resize(320, 240) .resize(320, 240)
.toFile('output.webp', (err, info) => ... ); .toFile('output.webp', (err, info) => { ... });
// A Promises/A+ promise is returned when callback is not provided.
``` ```
### Promise
```javascript ```javascript
sharp('input.jpg') sharp('input.jpg')
.rotate() .rotate()
.resize(200) .resize(200)
.toBuffer() .toBuffer()
.then( data => ... ) .then( data => { ... })
.catch( err => ... ); .catch( err => { ... });
``` ```
### Async/await
```javascript
const semiTransparentRedPng = await sharp({
create: {
width: 48,
height: 48,
channels: 4,
background: { r: 255, g: 0, b: 0, alpha: 0.5 }
}
})
.png()
.toBuffer();
```
### Stream
```javascript ```javascript
const roundedCorners = Buffer.from( const roundedCorners = Buffer.from(
'<svg><rect x="0" y="0" width="200" height="200" rx="50" ry="50"/></svg>' '<svg><rect x="0" y="0" width="200" height="200" rx="50" ry="50"/></svg>'
@@ -57,7 +73,10 @@ const roundedCorners = Buffer.from(
const roundedCornerResizer = const roundedCornerResizer =
sharp() sharp()
.resize(200, 200) .resize(200, 200)
.overlayWith(roundedCorners, { cutout: true }) .composite([{
input: roundedCorners,
blend: 'dest-in'
}])
.png(); .png();
readableStream readableStream
@@ -77,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

@@ -8,9 +8,10 @@ environment:
- nodejs_version: "8" - nodejs_version: "8"
- nodejs_version: "10" - nodejs_version: "10"
- nodejs_version: "11" - 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

@@ -140,7 +140,6 @@
'../vendor/lib/libgsf-1.so', '../vendor/lib/libgsf-1.so',
'../vendor/lib/libgthread-2.0.so', '../vendor/lib/libgthread-2.0.so',
'../vendor/lib/libharfbuzz.so', '../vendor/lib/libharfbuzz.so',
'../vendor/lib/libharfbuzz-subset.so.0',
'../vendor/lib/libjpeg.so', '../vendor/lib/libjpeg.so',
'../vendor/lib/liblcms2.so', '../vendor/lib/liblcms2.so',
'../vendor/lib/liborc-0.4.so', '../vendor/lib/liborc-0.4.so',

View File

@@ -1,33 +1,41 @@
<!-- Generated by documentation.js. Update this documentation by updating the source code. --> <!-- Generated by documentation.js. Update this documentation by updating the source code. -->
## overlayWith ## composite
Overlay (composite) an image over the processed (resized, extracted etc.) image. Composite image(s) over the processed (resized, extracted etc.) image.
The overlay image must be the same size or smaller than the processed image. The images to composite must be the same size or smaller than the processed image.
If both `top` and `left` options are provided, they take precedence over `gravity`. If both `top` and `left` options are provided, they take precedence over `gravity`.
If the overlay image contains an alpha channel then composition with premultiplication will occur. The `blend` option can be one of `clear`, `source`, `over`, `in`, `out`, `atop`,
`dest`, `dest-over`, `dest-in`, `dest-out`, `dest-atop`,
`xor`, `add`, `saturate`, `multiply`, `screen`, `overlay`, `darken`, `lighten`,
`colour-dodge`, `color-dodge`, `colour-burn`,`color-burn`,
`hard-light`, `soft-light`, `difference`, `exclusion`.
More information about blend modes can be found at
[https://libvips.github.io/libvips/API/current/libvips-conversion.html#VipsBlendMode][1]
and [https://www.cairographics.org/operators/][2]
### Parameters ### Parameters
- `overlay` **([Buffer][1] \| [String][2])?** Buffer containing image data or String containing the path to an image file. - `images` **[Array][3]&lt;[Object][4]>** Ordered list of images to composite
- `options` **[Object][3]?** - `images[].input` **([Buffer][5] \| [String][6])?** Buffer containing image data, String containing the path to an image file, or Create object (see bellow)
- `options.gravity` **[String][2]** gravity at which to place the overlay. (optional, default `'centre'`) - `images[].input.create` **[Object][4]?** describes a blank overlay to be created.
- `options.top` **[Number][4]?** the pixel offset from the top edge. - `images[].input.create.width` **[Number][7]?**
- `options.left` **[Number][4]?** the pixel offset from the left edge. - `images[].input.create.height` **[Number][7]?**
- `options.tile` **[Boolean][5]** set to true to repeat the overlay image across the entire image with the given `gravity`. (optional, default `false`) - `images[].input.create.channels` **[Number][7]?** 3-4
- `options.cutout` **[Boolean][5]** set to true to apply only the alpha channel of the overlay image to the input image, giving the appearance of one image being cut out of another. (optional, default `false`) - `images[].input.create.background` **([String][6] \| [Object][4])?** parsed by the [color][8] module to extract values for red, green, blue and alpha.
- `options.density` **[Number][4]** number representing the DPI for vector overlay image. (optional, default `72`) - `images[].blend` **[String][6]** how to blend this image with the image below. (optional, default `'over'`)
- `options.raw` **[Object][3]?** describes overlay when using raw pixel data. - `images[].gravity` **[String][6]** gravity at which to place the overlay. (optional, default `'centre'`)
- `options.raw.width` **[Number][4]?** - `images[].top` **[Number][7]?** the pixel offset from the top edge.
- `options.raw.height` **[Number][4]?** - `images[].left` **[Number][7]?** the pixel offset from the left edge.
- `options.raw.channels` **[Number][4]?** - `images[].tile` **[Boolean][9]** set to true to repeat the overlay image across the entire image with the given `gravity`. (optional, default `false`)
- `options.create` **[Object][3]?** describes a blank overlay to be created. - `images[].density` **[Number][7]** number representing the DPI for vector overlay image. (optional, default `72`)
- `options.create.width` **[Number][4]?** - `images[].raw` **[Object][4]?** describes overlay when using raw pixel data.
- `options.create.height` **[Number][4]?** - `images[].raw.width` **[Number][7]?**
- `options.create.channels` **[Number][4]?** 3-4 - `images[].raw.height` **[Number][7]?**
- `options.create.background` **([String][2] \| [Object][3])?** parsed by the [color][6] module to extract values for red, green, blue and alpha. - `images[].raw.channels` **[Number][7]?**
### Examples ### Examples
@@ -36,7 +44,7 @@ sharp('input.png')
.rotate(180) .rotate(180)
.resize(300) .resize(300)
.flatten( { background: '#ff6600' } ) .flatten( { background: '#ff6600' } )
.overlayWith('overlay.png', { gravity: sharp.gravity.southeast } ) .composite([{ input: 'overlay.png', gravity: 'southeast' }])
.sharpen() .sharpen()
.withMetadata() .withMetadata()
.webp( { quality: 90 } ) .webp( { quality: 90 } )
@@ -48,20 +56,26 @@ sharp('input.png')
}); });
``` ```
- Throws **[Error][7]** Invalid parameters - Throws **[Error][10]** Invalid parameters
Returns **Sharp** Returns **Sharp**
[1]: https://nodejs.org/api/buffer.html [1]: https://libvips.github.io/libvips/API/current/libvips-conversion.html#VipsBlendMode
[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String [2]: https://www.cairographics.org/operators/
[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object [3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number [4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[5]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean [5]: https://nodejs.org/api/buffer.html
[6]: https://www.npmjs.org/package/color [6]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error [7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
[8]: 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

View File

@@ -12,7 +12,8 @@
- `options.failOnError` **[Boolean][4]** by default halt processing and raise an error when loading invalid images. - `options.failOnError` **[Boolean][4]** by default halt processing and raise an error when loading invalid images.
Set this flag to `false` if you'd rather apply a "best effort" to decode images, even if the data is corrupt or invalid. (optional, default `true`) Set this flag to `false` if you'd rather apply a "best effort" to decode images, even if the data is corrupt or invalid. (optional, default `true`)
- `options.density` **[Number][5]** number representing the DPI for vector images. (optional, default `72`) - `options.density` **[Number][5]** number representing the DPI for vector images. (optional, default `72`)
- `options.page` **[Number][5]** page number to extract for multi-page input (GIF, TIFF) (optional, default `0`) - `options.pages` **[Number][5]** number of pages to extract for multi-page input (GIF, TIFF, PDF), use -1 for all pages. (optional, default `1`)
- `options.page` **[Number][5]** page number to start extracting from for multi-page input (GIF, TIFF, PDF), zero based. (optional, default `0`)
- `options.raw` **[Object][3]?** describes raw pixel input image data. See `raw()` for pixel ordering. - `options.raw` **[Object][3]?** describes raw pixel input image data. See `raw()` for pixel ordering.
- `options.raw.width` **[Number][5]?** - `options.raw.width` **[Number][5]?**
- `options.raw.height` **[Number][5]?** - `options.raw.height` **[Number][5]?**

View File

@@ -22,7 +22,7 @@ Returns **Sharp**
## metadata ## metadata
Fast access to (uncached) image metadata without decoding any compressed image data. Fast access to (uncached) image metadata without decoding any compressed image data.
A Promises/A+ promise is returned when `callback` is not provided. A `Promise` is returned when `callback` is not provided.
- `format`: Name of decoder used to decompress image data e.g. `jpeg`, `png`, `webp`, `gif`, `svg` - `format`: Name of decoder used to decompress image data e.g. `jpeg`, `png`, `webp`, `gif`, `svg`
- `size`: Total size of image in bytes, for Stream and Buffer input only - `size`: Total size of image in bytes, for Stream and Buffer input only
@@ -34,7 +34,7 @@ A Promises/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 this TIFF, GIF or PDF image contains. - `pages`: Number of pages/frames contained within the image, with support for TIFF, 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.
- `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
@@ -70,7 +70,7 @@ Returns **([Promise][5]&lt;[Object][6]> | Sharp)**
## stats ## stats
Access to pixel-derived image statistics for every channel in the image. Access to pixel-derived image statistics for every channel in the image.
A Promise is returned when `callback` is not provided. A `Promise` is returned when `callback` is not provided.
- `channels`: Array of channel statistics for each channel in the image. Each channel statistic contains - `channels`: Array of channel statistics for each channel in the image. Each channel statistic contains
- `min` (minimum value in the channel) - `min` (minimum value in the channel)
@@ -105,9 +105,9 @@ Returns **[Promise][5]&lt;[Object][6]>**
## limitInputPixels ## limitInputPixels
Do not process input images where the number of pixels (width _ height) exceeds this limit. Do not process input images where the number of pixels (width x height) exceeds this limit.
Assumes image dimensions contained in the input metadata can be trusted. Assumes image dimensions contained in the input metadata can be trusted.
The default limit is 268402689 (0x3FFF _ 0x3FFF) pixels. The default limit is 268402689 (0x3FFF x 0x3FFF) pixels.
### Parameters ### Parameters

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

@@ -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

View File

@@ -143,7 +143,7 @@ sharp(input)
top: 10, top: 10,
bottom: 20, bottom: 20,
left: 10, left: 10,
right: 10 right: 10,
background: { r: 0, g: 0, b: 0, alpha: 0 } background: { r: 0, g: 0, b: 0, alpha: 0 }
}) })
... ...
@@ -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,5 +1,40 @@
# Changelog # Changelog
### v0.22 - "*uptake*"
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
* Remove functions previously deprecated in v0.21.0:
`background`, `crop`, `embed`, `ignoreAspectRatio`, `max`, `min` and `withoutEnlargement`.
* Add `composite` operation supporting multiple images and blend modes; deprecate `overlayWith`.
[#728](https://github.com/lovell/sharp/issues/728)
* Add support for `pages` input option for multi-page input.
[#1566](https://github.com/lovell/sharp/issues/1566)
* Allow Stream-based input of raw pixel data.
[#1579](https://github.com/lovell/sharp/issues/1579)
* Add support for `page` input option to GIF and PDF.
[#1595](https://github.com/lovell/sharp/pull/1595)
[@ramiel](https://github.com/ramiel)
### v0.21 - "*teeth*" ### v0.21 - "*teeth*"
Requires libvips v8.7.0. Requires libvips v8.7.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 6, 8, 10, 11 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,7 @@ 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)
Thank you! Thank you!

View File

@@ -10,12 +10,12 @@ yarn add sharp
## Prerequisites ## Prerequisites
* Node v4.5.0+ * Node.js v6+
### 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 6, 8, 10, 11 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,14 +36,14 @@ 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 8MB. This involves an automated HTTPS download of approximately 9MB.
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 7+
* Ubuntu 14.04+ * Ubuntu 14.04+
* Centos 7+ * Centos 7+
* Alpine 3.8+ (Node 8 and 10) * Alpine 3.8+ (Node 8+)
* Fedora * Fedora
* openSUSE 13.2+ * openSUSE 13.2+
* Archlinux * Archlinux
@@ -86,7 +86,7 @@ via `sharp.cache(false)` to avoid a stack overflow.
[![OS X 10.12 Build Status](https://travis-ci.org/lovell/sharp.png?branch=master)](https://travis-ci.org/lovell/sharp) [![OS X 10.12 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 7MB. This involves an automated HTTPS download of approximately 8MB.
To use your own version of libvips instead of the provided binaries, make sure it is To use your own version of libvips instead of the provided binaries, make sure it is
at least the version listed under `config.libvips` in the `package.json` file and at least the version listed under `config.libvips` in the `package.json` file and
@@ -97,8 +97,9 @@ 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 13MB. If you are having issues during This involves an automated HTTPS download of approximately 14MB.
installation consider removing the directory ```C:\Users\[user]\AppData\Roaming\npm-cache\_libvips```. If you are having issues during installation consider removing the directory
`C:\Users\[user]\AppData\Roaming\npm-cache\_libvips`.
Only 64-bit (x64) `node.exe` is supported. Only 64-bit (x64) `node.exe` is supported.

View File

@@ -34,7 +34,12 @@ const extractTarball = function (tarPath) {
cwd: vendorPath, cwd: vendorPath,
strict: true strict: true
}) })
.catch(fail); .catch(function (err) {
if (/unexpected end of file/.test(err.message)) {
npmLog.error('sharp', `Please delete ${tarPath} as it is not a valid tarball`);
}
fail(err);
});
}; };
try { try {

View File

@@ -1,7 +1,5 @@
'use strict'; 'use strict';
const deprecate = require('util').deprecate;
const color = require('color'); const color = require('color');
const is = require('./is'); const is = require('./is');
@@ -17,24 +15,6 @@ const colourspace = {
srgb: 'srgb' srgb: 'srgb'
}; };
/**
* @deprecated
* @private
*/
function background (rgba) {
const colour = color(rgba);
const background = [
colour.red(),
colour.green(),
colour.blue(),
Math.round(colour.alpha() * 255)
];
this.options.resizeBackground = background;
this.options.extendBackground = background;
this.options.flattenBackground = background.slice(0, 3);
return this;
}
/** /**
* Tint the image using the provided chroma while preserving the image luminance. * Tint the image using the provided chroma while preserving the image luminance.
* An alpha channel may be present and will be unchanged by the operation. * An alpha channel may be present and will be unchanged by the operation.
@@ -136,6 +116,4 @@ module.exports = function (Sharp) {
// Class attributes // Class attributes
Sharp.colourspace = colourspace; Sharp.colourspace = colourspace;
Sharp.colorspace = colourspace; Sharp.colorspace = colourspace;
// Deprecated
Sharp.prototype.background = deprecate(background, 'background(background) is deprecated, use resize({ background }), extend({ background }) or flatten({ background }) instead');
}; };

View File

@@ -1,21 +1,66 @@
'use strict'; 'use strict';
const deprecate = require('util').deprecate;
const is = require('./is'); const is = require('./is');
/** /**
* Overlay (composite) an image over the processed (resized, extracted etc.) image. * Blend modes.
* @member
* @private
*/
const blend = {
clear: 'clear',
source: 'source',
over: 'over',
in: 'in',
out: 'out',
atop: 'atop',
dest: 'dest',
'dest-over': 'dest-over',
'dest-in': 'dest-in',
'dest-out': 'dest-out',
'dest-atop': 'dest-atop',
xor: 'xor',
add: 'add',
saturate: 'saturate',
multiply: 'multiply',
screen: 'screen',
overlay: 'overlay',
darken: 'darken',
lighten: 'lighten',
'colour-dodge': 'colour-dodge',
'color-dodge': 'colour-dodge',
'colour-burn': 'colour-burn',
'color-burn': 'colour-burn',
'hard-light': 'hard-light',
'soft-light': 'soft-light',
difference: 'difference',
exclusion: 'exclusion'
};
/**
* Composite image(s) over the processed (resized, extracted etc.) image.
* *
* The overlay image must be the same size or smaller than the processed image. * The images to composite must be the same size or smaller than the processed image.
* If both `top` and `left` options are provided, they take precedence over `gravity`. * If both `top` and `left` options are provided, they take precedence over `gravity`.
* *
* If the overlay image contains an alpha channel then composition with premultiplication will occur. * The `blend` option can be one of `clear`, `source`, `over`, `in`, `out`, `atop`,
* `dest`, `dest-over`, `dest-in`, `dest-out`, `dest-atop`,
* `xor`, `add`, `saturate`, `multiply`, `screen`, `overlay`, `darken`, `lighten`,
* `colour-dodge`, `color-dodge`, `colour-burn`,`color-burn`,
* `hard-light`, `soft-light`, `difference`, `exclusion`.
*
* More information about blend modes can be found at
* https://libvips.github.io/libvips/API/current/libvips-conversion.html#VipsBlendMode
* and https://www.cairographics.org/operators/
* *
* @example * @example
* sharp('input.png') * sharp('input.png')
* .rotate(180) * .rotate(180)
* .resize(300) * .resize(300)
* .flatten( { background: '#ff6600' } ) * .flatten( { background: '#ff6600' } )
* .overlayWith('overlay.png', { gravity: sharp.gravity.southeast } ) * .composite([{ input: 'overlay.png', gravity: 'southeast' }])
* .sharpen() * .sharpen()
* .withMetadata() * .withMetadata()
* .webp( { quality: 90 } ) * .webp( { quality: 90 } )
@@ -26,70 +71,104 @@ const is = require('./is');
* // sharpened, with metadata, 90% quality WebP image data. Phew! * // sharpened, with metadata, 90% quality WebP image data. Phew!
* }); * });
* *
* @param {(Buffer|String)} [overlay] - Buffer containing image data or String containing the path to an image file. * @param {Object[]} images - Ordered list of images to composite
* @param {Object} [options] * @param {Buffer|String} [images[].input] - Buffer containing image data, String containing the path to an image file, or Create object (see bellow)
* @param {String} [options.gravity='centre'] - gravity at which to place the overlay. * @param {Object} [images[].input.create] - describes a blank overlay to be created.
* @param {Number} [options.top] - the pixel offset from the top edge. * @param {Number} [images[].input.create.width]
* @param {Number} [options.left] - the pixel offset from the left edge. * @param {Number} [images[].input.create.height]
* @param {Boolean} [options.tile=false] - set to true to repeat the overlay image across the entire image with the given `gravity`. * @param {Number} [images[].input.create.channels] - 3-4
* @param {Boolean} [options.cutout=false] - set to true to apply only the alpha channel of the overlay image to the input image, giving the appearance of one image being cut out of another. * @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 {Number} [options.density=72] - number representing the DPI for vector overlay image. * @param {String} [images[].blend='over'] - how to blend this image with the image below.
* @param {Object} [options.raw] - describes overlay when using raw pixel data. * @param {String} [images[].gravity='centre'] - gravity at which to place the overlay.
* @param {Number} [options.raw.width] * @param {Number} [images[].top] - the pixel offset from the top edge.
* @param {Number} [options.raw.height] * @param {Number} [images[].left] - the pixel offset from the left edge.
* @param {Number} [options.raw.channels] * @param {Boolean} [images[].tile=false] - set to true to repeat the overlay image across the entire image with the given `gravity`.
* @param {Object} [options.create] - describes a blank overlay to be created. * @param {Number} [images[].density=72] - number representing the DPI for vector overlay image.
* @param {Number} [options.create.width] * @param {Object} [images[].raw] - describes overlay when using raw pixel data.
* @param {Number} [options.create.height] * @param {Number} [images[].raw.width]
* @param {Number} [options.create.channels] - 3-4 * @param {Number} [images[].raw.height]
* @param {String|Object} [options.create.background] - parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha. * @param {Number} [images[].raw.channels]
* @returns {Sharp} * @returns {Sharp}
* @throws {Error} Invalid parameters * @throws {Error} Invalid parameters
*/ */
function overlayWith (overlay, options) { function composite (images) {
this.options.overlay = this._createInputDescriptor(overlay, options, { if (!Array.isArray(images)) {
allowStream: false throw is.invalidParameterError('images to composite', 'array', images);
});
if (is.object(options)) {
if (is.defined(options.tile)) {
if (is.bool(options.tile)) {
this.options.overlayTile = options.tile;
} else {
throw new Error('Invalid overlay tile ' + options.tile);
}
}
if (is.defined(options.cutout)) {
if (is.bool(options.cutout)) {
this.options.overlayCutout = options.cutout;
} else {
throw new Error('Invalid overlay cutout ' + options.cutout);
}
}
if (is.defined(options.left) || is.defined(options.top)) {
if (is.integer(options.left) && options.left >= 0 && is.integer(options.top) && options.top >= 0) {
this.options.overlayXOffset = options.left;
this.options.overlayYOffset = options.top;
} else {
throw new Error('Invalid overlay left ' + options.left + ' and/or top ' + options.top);
}
}
if (is.defined(options.gravity)) {
if (is.integer(options.gravity) && is.inRange(options.gravity, 0, 8)) {
this.options.overlayGravity = options.gravity;
} else if (is.string(options.gravity) && is.integer(this.constructor.gravity[options.gravity])) {
this.options.overlayGravity = this.constructor.gravity[options.gravity];
} else {
throw new Error('Unsupported overlay gravity ' + options.gravity);
}
}
} }
this.options.composite = images.map(image => {
if (!is.object(image)) {
throw is.invalidParameterError('image to composite', 'object', image);
}
const { raw, density } = image;
const inputOptions = (raw || density) ? { raw, density } : undefined;
const composite = {
input: this._createInputDescriptor(image.input, inputOptions, { allowStream: false }),
blend: 'over',
tile: false,
left: -1,
top: -1,
gravity: 0
};
if (is.defined(image.blend)) {
if (is.string(blend[image.blend])) {
composite.blend = blend[image.blend];
} else {
throw is.invalidParameterError('blend', 'valid blend name', image.blend);
}
}
if (is.defined(image.tile)) {
if (is.bool(image.tile)) {
composite.tile = image.tile;
} else {
throw is.invalidParameterError('tile', 'boolean', image.tile);
}
}
if (is.defined(image.left)) {
if (is.integer(image.left) && image.left >= 0) {
composite.left = image.left;
} else {
throw is.invalidParameterError('left', 'positive integer', image.left);
}
}
if (is.defined(image.top)) {
if (is.integer(image.top) && image.top >= 0) {
composite.top = image.top;
} else {
throw is.invalidParameterError('top', 'positive integer', image.top);
}
}
if (composite.left !== composite.top && Math.min(composite.left, composite.top) === -1) {
throw new Error('Expected both left and top to be set');
}
if (is.defined(image.gravity)) {
if (is.integer(image.gravity) && is.inRange(image.gravity, 0, 8)) {
composite.gravity = image.gravity;
} else if (is.string(image.gravity) && is.integer(this.constructor.gravity[image.gravity])) {
composite.gravity = this.constructor.gravity[image.gravity];
} else {
throw is.invalidParameterError('gravity', 'valid gravity', image.gravity);
}
}
return composite;
});
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.overlayWith = overlayWith; Sharp.prototype.composite = composite;
Sharp.prototype.overlayWith = deprecate(overlayWith, 'overlayWith(input, options) is deprecated, use composite([{ input, ...options }]) instead');
Sharp.blend = blend;
}; };

View File

@@ -7,7 +7,27 @@ 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;
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');
@@ -64,7 +84,8 @@ const debuglog = util.debuglog('sharp');
* @param {Boolean} [options.failOnError=true] - by default halt processing and raise an error when loading invalid images. * @param {Boolean} [options.failOnError=true] - by default halt processing and raise an error when loading invalid images.
* Set this flag to `false` if you'd rather apply a "best effort" to decode images, even if the data is corrupt or invalid. * Set this flag to `false` if you'd rather apply a "best effort" to decode images, even if the data is corrupt or invalid.
* @param {Number} [options.density=72] - number representing the DPI for vector images. * @param {Number} [options.density=72] - number representing the DPI for vector images.
* @param {Number} [options.page=0] - page number to extract for multi-page input (GIF, TIFF) * @param {Number} [options.pages=1] - number of pages to extract for multi-page input (GIF, TIFF, PDF), use -1 for all pages.
* @param {Number} [options.page=0] - page number to start extracting from for multi-page input (GIF, TIFF, PDF), zero based.
* @param {Object} [options.raw] - describes raw pixel input image data. See `raw()` for pixel ordering. * @param {Object} [options.raw] - describes raw pixel input image data. See `raw()` for pixel ordering.
* @param {Number} [options.raw.width] * @param {Number} [options.raw.width]
* @param {Number} [options.raw.height] * @param {Number} [options.raw.height]
@@ -138,6 +159,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: [],
@@ -145,12 +169,7 @@ const Sharp = function (input, options) {
removeAlpha: false, removeAlpha: false,
ensureAlpha: false, ensureAlpha: false,
colourspace: 'srgb', colourspace: 'srgb',
// overlay composite: [],
overlayGravity: 0,
overlayXOffset: -1,
overlayYOffset: -1,
overlayTile: false,
overlayCutout: false,
// output // output
fileOut: '', fileOut: '',
formatOut: 'input', formatOut: 'input',

View File

@@ -19,6 +19,10 @@ 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)) {
// Raw Stream
inputDescriptor.buffer = [];
}
} else if (!is.defined(input) && is.object(containerOptions) && containerOptions.allowStream) { } else if (!is.defined(input) && is.object(containerOptions) && containerOptions.allowStream) {
// Stream // Stream
inputDescriptor.buffer = []; inputDescriptor.buffer = [];
@@ -57,7 +61,12 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
throw new Error('Expected width, height and channels for raw pixel input'); throw new Error('Expected width, height and channels for raw pixel input');
} }
} }
// Page input for multi-page TIFF // Multi-page input (GIF, TIFF, PDF)
if (is.defined(inputOptions.pages)) {
if (is.integer(inputOptions.pages) && is.inRange(inputOptions.pages, -1, 100000)) {
inputDescriptor.pages = inputOptions.pages;
}
}
if (is.defined(inputOptions.page)) { if (is.defined(inputOptions.page)) {
if (is.integer(inputOptions.page) && is.inRange(inputOptions.page, 0, 100000)) { if (is.integer(inputOptions.page) && is.inRange(inputOptions.page, 0, 100000)) {
inputDescriptor.page = inputOptions.page; inputDescriptor.page = inputOptions.page;
@@ -174,7 +183,7 @@ function clone () {
/** /**
* Fast access to (uncached) image metadata without decoding any compressed image data. * Fast access to (uncached) image metadata without decoding any compressed image data.
* A Promises/A+ promise is returned when `callback` is not provided. * A `Promise` is returned when `callback` is not provided.
* *
* - `format`: Name of decoder used to decompress image data e.g. `jpeg`, `png`, `webp`, `gif`, `svg` * - `format`: Name of decoder used to decompress image data e.g. `jpeg`, `png`, `webp`, `gif`, `svg`
* - `size`: Total size of image in bytes, for Stream and Buffer input only * - `size`: Total size of image in bytes, for Stream and Buffer input only
@@ -186,7 +195,7 @@ 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 this TIFF, GIF or PDF image contains. * - `pages`: Number of pages/frames contained within the image, with support for TIFF, 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.
* - `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
@@ -255,7 +264,7 @@ function metadata (callback) {
/** /**
* Access to pixel-derived image statistics for every channel in the image. * Access to pixel-derived image statistics for every channel in the image.
* A Promise is returned when `callback` is not provided. * A `Promise` is returned when `callback` is not provided.
* *
* - `channels`: Array of channel statistics for each channel in the image. Each channel statistic contains * - `channels`: Array of channel statistics for each channel in the image. Each channel statistic contains
* - `min` (minimum value in the channel) * - `min` (minimum value in the channel)
@@ -323,9 +332,9 @@ function stats (callback) {
} }
/** /**
* Do not process input images where the number of pixels (width * height) exceeds this limit. * Do not process input images where the number of pixels (width x height) exceeds this limit.
* Assumes image dimensions contained in the input metadata can be trusted. * Assumes image dimensions contained in the input metadata can be trusted.
* The default limit is 268402689 (0x3FFF * 0x3FFF) pixels. * The default limit is 268402689 (0x3FFF x 0x3FFF) pixels.
* @param {(Number|Boolean)} limit - an integral Number of pixels, zero or false to remove limit, true to use default limit. * @param {(Number|Boolean)} limit - an integral Number of pixels, zero or false to remove limit, true to use default limit.
* @returns {Sharp} * @returns {Sharp}
* @throws {Error} Invalid limit * @throws {Error} Invalid limit

View File

@@ -415,6 +415,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 +492,7 @@ module.exports = function (Sharp) {
threshold, threshold,
boolean, boolean,
linear, linear,
recomb recomb,
modulate
}); });
}; };

View File

@@ -144,13 +144,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}
@@ -221,11 +221,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

View File

@@ -2,17 +2,22 @@
const detectLibc = require('detect-libc'); const detectLibc = require('detect-libc');
const env = process.env;
module.exports = function () { module.exports = function () {
const arch = process.env.npm_config_arch || process.arch; const arch = env.npm_config_arch || process.arch;
const platform = process.env.npm_config_platform || process.platform; const platform = env.npm_config_platform || process.platform;
const libc = (platform === 'linux' && detectLibc.isNonGlibcLinux) ? detectLibc.family : ''; const libc = (platform === 'linux' && detectLibc.isNonGlibcLinux) ? detectLibc.family : '';
const platformId = [`${platform}${libc}`]; const platformId = [`${platform}${libc}`];
if (arch === 'arm' || arch === 'armhf' || arch === 'arm64') {
const armVersion = (arch === 'arm64') ? '8' : process.env.npm_config_armv || process.config.variables.arm_version || '6'; if (arch === 'arm') {
platformId.push(`armv${armVersion}`); platformId.push(`armv${env.npm_config_arm_version || process.config.variables.arm_version || '6'}`);
} else if (arch === 'arm64') {
platformId.push(`arm64v${env.npm_config_arm_version || '8'}`);
} else { } else {
platformId.push(arch); platformId.push(arch);
} }
return platformId.join('-'); return platformId.join('-');
}; };

View File

@@ -1,6 +1,5 @@
'use strict'; 'use strict';
const deprecate = require('util').deprecate;
const is = require('./is'); const is = require('./is');
/** /**
@@ -275,7 +274,7 @@ function resize (width, height, options) {
* top: 10, * top: 10,
* bottom: 20, * bottom: 20,
* left: 10, * left: 10,
* right: 10 * right: 10,
* background: { r: 0, g: 0, b: 0, alpha: 0 } * background: { r: 0, g: 0, b: 0, alpha: 0 }
* }) * })
* ... * ...
@@ -335,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
*/ */
@@ -378,92 +377,6 @@ function trim (threshold) {
return this; return this;
} }
// Deprecated functions
/**
* @deprecated
* @private
*/
function crop (crop) {
this.options.canvas = 'crop';
if (!is.defined(crop)) {
// Default
this.options.position = gravity.center;
} else if (is.integer(crop) && is.inRange(crop, 0, 8)) {
// Gravity (numeric)
this.options.position = crop;
} else if (is.string(crop) && is.integer(gravity[crop])) {
// Gravity (string)
this.options.position = gravity[crop];
} else if (is.integer(crop) && crop >= strategy.entropy) {
// Strategy
this.options.position = crop;
} else if (is.string(crop) && is.integer(strategy[crop])) {
// Strategy (string)
this.options.position = strategy[crop];
} else {
throw is.invalidParameterError('crop', 'valid crop id/name/strategy', crop);
}
return this;
}
/**
* @deprecated
* @private
*/
function embed (embed) {
this.options.canvas = 'embed';
if (!is.defined(embed)) {
// Default
this.options.position = gravity.center;
} else if (is.integer(embed) && is.inRange(embed, 0, 8)) {
// Gravity (numeric)
this.options.position = embed;
} else if (is.string(embed) && is.integer(gravity[embed])) {
// Gravity (string)
this.options.position = gravity[embed];
} else {
throw is.invalidParameterError('embed', 'valid embed id/name', embed);
}
return this;
}
/**
* @deprecated
* @private
*/
function max () {
this.options.canvas = 'max';
return this;
}
/**
* @deprecated
* @private
*/
function min () {
this.options.canvas = 'min';
return this;
}
/**
* @deprecated
* @private
*/
function ignoreAspectRatio () {
this.options.canvas = 'ignore_aspect';
return this;
}
/**
* @deprecated
* @private
*/
function withoutEnlargement (withoutEnlargement) {
this.options.withoutEnlargement = is.bool(withoutEnlargement) ? withoutEnlargement : true;
return this;
}
/** /**
* Decorate the Sharp prototype with resize-related functions. * Decorate the Sharp prototype with resize-related functions.
* @private * @private
@@ -481,11 +394,4 @@ module.exports = function (Sharp) {
Sharp.kernel = kernel; Sharp.kernel = kernel;
Sharp.fit = fit; Sharp.fit = fit;
Sharp.position = position; Sharp.position = position;
// Deprecated functions, to be removed in v0.22.0
Sharp.prototype.crop = deprecate(crop, 'crop(position) is deprecated, use resize({ fit: "cover", position }) instead');
Sharp.prototype.embed = deprecate(embed, 'embed(position) is deprecated, use resize({ fit: "contain", position }) instead');
Sharp.prototype.max = deprecate(max, 'max() is deprecated, use resize({ fit: "inside" }) instead');
Sharp.prototype.min = deprecate(min, 'min() is deprecated, use resize({ fit: "outside" }) instead');
Sharp.prototype.ignoreAspectRatio = deprecate(ignoreAspectRatio, 'ignoreAspectRatio() is deprecated, use resize({ fit: "fill" }) instead');
Sharp.prototype.withoutEnlargement = deprecate(withoutEnlargement, 'withoutEnlargement() is deprecated, use resize({ withoutEnlargement: true }) instead');
}; };

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.21.3", "version": "0.22.1",
"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": [
@@ -93,37 +93,36 @@
"vips" "vips"
], ],
"dependencies": { "dependencies": {
"bindings": "^1.3.1", "color": "^3.1.1",
"color": "^3.1.0",
"detect-libc": "^1.0.3", "detect-libc": "^1.0.3",
"fs-copy-file-sync": "^1.1.1", "fs-copy-file-sync": "^1.1.1",
"nan": "^2.12.1", "nan": "^2.13.2",
"npmlog": "^4.1.2", "npmlog": "^4.1.2",
"prebuild-install": "^5.2.2", "prebuild-install": "^5.3.0",
"semver": "^5.6.0", "semver": "^6.0.0",
"simple-get": "^3.0.3", "simple-get": "^3.0.3",
"tar": "^4.4.8", "tar": "^4.4.8",
"tunnel-agent": "^0.6.0" "tunnel-agent": "^0.6.0"
}, },
"devDependencies": { "devDependencies": {
"async": "^2.6.1", "async": "^2.6.2",
"cc": "^1.0.2", "cc": "^1.0.2",
"decompress-zip": "^0.3.2", "decompress-zip": "^0.3.2",
"documentation": "^9.1.1", "documentation": "^10.0.0",
"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": "^5.2.0", "mocha": "^6.1.4",
"mock-fs": "^4.7.0", "mock-fs": "^4.9.0",
"nyc": "^13.1.0", "nyc": "^14.0.0",
"prebuild": "^8.1.2", "prebuild": "^8.2.1",
"prebuild-ci": "^2.3.0", "prebuild-ci": "^3.0.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.0" "libvips": "8.7.4"
}, },
"engines": { "engines": {
"node": ">=6" "node": ">=6"

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;
@@ -71,7 +71,10 @@ namespace sharp {
descriptor->rawWidth = AttrTo<uint32_t>(input, "rawWidth"); descriptor->rawWidth = AttrTo<uint32_t>(input, "rawWidth");
descriptor->rawHeight = AttrTo<uint32_t>(input, "rawHeight"); descriptor->rawHeight = AttrTo<uint32_t>(input, "rawHeight");
} }
// Page input for multi-page TIFF // Multi-page input (GIF, TIFF, PDF)
if (HasAttr(input, "pages")) {
descriptor->pages = AttrTo<int32_t>(input, "pages");
}
if (HasAttr(input, "page")) { if (HasAttr(input, "page")) {
descriptor->page = AttrTo<uint32_t>(input, "page"); descriptor->page = AttrTo<uint32_t>(input, "page");
} }
@@ -212,6 +215,16 @@ namespace sharp {
return imageType; return imageType;
} }
/*
Does this image type support multiple pages?
*/
bool ImageTypeSupportsPage(ImageType imageType) {
return
imageType == ImageType::GIF ||
imageType == ImageType::TIFF ||
imageType == ImageType::PDF;
}
/* /*
Open an image from the given InputDescriptor (filesystem, compressed buffer, raw pixel data) Open an image from the given InputDescriptor (filesystem, compressed buffer, raw pixel data)
*/ */
@@ -243,8 +256,9 @@ namespace sharp {
if (imageType == ImageType::MAGICK) { if (imageType == ImageType::MAGICK) {
option->set("density", std::to_string(descriptor->density).data()); option->set("density", std::to_string(descriptor->density).data());
} }
if (imageType == ImageType::TIFF) { if (ImageTypeSupportsPage(imageType)) {
option->set("page", descriptor->page); option->set("n", descriptor->pages);
option->set("page", descriptor->page);
} }
image = VImage::new_from_buffer(descriptor->buffer, descriptor->bufferLength, nullptr, option); image = VImage::new_from_buffer(descriptor->buffer, descriptor->bufferLength, nullptr, option);
if (imageType == ImageType::SVG || imageType == ImageType::PDF || imageType == ImageType::MAGICK) { if (imageType == ImageType::SVG || imageType == ImageType::PDF || imageType == ImageType::MAGICK) {
@@ -288,8 +302,9 @@ namespace sharp {
if (imageType == ImageType::MAGICK) { if (imageType == ImageType::MAGICK) {
option->set("density", std::to_string(descriptor->density).data()); option->set("density", std::to_string(descriptor->density).data());
} }
if (imageType == ImageType::TIFF) { if (ImageTypeSupportsPage(imageType)) {
option->set("page", descriptor->page); option->set("n", descriptor->pages);
option->set("page", descriptor->page);
} }
image = VImage::new_from_file(descriptor->file.data(), option); image = VImage::new_from_file(descriptor->file.data(), option);
if (imageType == ImageType::SVG || imageType == ImageType::PDF || imageType == ImageType::MAGICK) { if (imageType == ImageType::SVG || imageType == ImageType::PDF || imageType == ImageType::MAGICK) {

View File

@@ -53,6 +53,7 @@ namespace sharp {
int rawChannels; int rawChannels;
int rawWidth; int rawWidth;
int rawHeight; int rawHeight;
int pages;
int page; int page;
int createChannels; int createChannels;
int createWidth; int createWidth;
@@ -67,6 +68,7 @@ namespace sharp {
rawChannels(0), rawChannels(0),
rawWidth(0), rawWidth(0),
rawHeight(0), rawHeight(0),
pages(1),
page(0), page(0),
createChannels(0), createChannels(0),
createWidth(0), createWidth(0),
@@ -75,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,
@@ -140,6 +142,11 @@ namespace sharp {
*/ */
ImageType DetermineImageType(char const *file); ImageType DetermineImageType(char const *file);
/*
Does this image type support multiple pages?
*/
bool ImageTypeSupportsPage(ImageType imageType);
/* /*
Open an image from the given InputDescriptor (filesystem, compressed buffer, raw pixel data) Open an image from the given InputDescriptor (filesystem, compressed buffer, raw pixel data)
*/ */

View File

@@ -50,130 +50,6 @@ namespace sharp {
return image; return image;
} }
/*
Composite overlayImage over image at given position
Assumes alpha channels are already premultiplied and will be unpremultiplied after
*/
VImage Composite(VImage image, VImage overlayImage, int const left, int const top) {
if (HasAlpha(overlayImage)) {
// Alpha composite
if (overlayImage.width() < image.width() || overlayImage.height() < image.height()) {
// Enlarge overlay
std::vector<double> const background { 0.0, 0.0, 0.0, 0.0 };
overlayImage = overlayImage.embed(left, top, image.width(), image.height(), VImage::option()
->set("extend", VIPS_EXTEND_BACKGROUND)
->set("background", background));
}
return AlphaComposite(image, overlayImage);
} else {
if (HasAlpha(image)) {
// Add alpha channel to overlayImage so channels match
double const multiplier = sharp::Is16Bit(overlayImage.interpretation()) ? 256.0 : 1.0;
overlayImage = overlayImage.bandjoin(
VImage::new_matrix(overlayImage.width(), overlayImage.height()).new_from_image(255 * multiplier));
}
return image.insert(overlayImage, left, top);
}
}
VImage AlphaComposite(VImage dst, VImage src) {
// Split src into non-alpha and alpha channels
VImage srcWithoutAlpha = src.extract_band(0, VImage::option()->set("n", src.bands() - 1));
VImage srcAlpha = src[src.bands() - 1] * (1.0 / 255.0);
// Split dst into non-alpha and alpha channels
VImage dstWithoutAlpha = dst.extract_band(0, VImage::option()->set("n", dst.bands() - 1));
VImage dstAlpha = dst[dst.bands() - 1] * (1.0 / 255.0);
//
// Compute normalized output alpha channel:
//
// References:
// - http://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending
// - https://github.com/libvips/ruby-vips/issues/28#issuecomment-9014826
//
// out_a = src_a + dst_a * (1 - src_a)
// ^^^^^^^^^^^
// t0
VImage t0 = srcAlpha.linear(-1.0, 1.0);
VImage outAlphaNormalized = srcAlpha + dstAlpha * t0;
//
// Compute output RGB channels:
//
// Wikipedia:
// out_rgb = (src_rgb * src_a + dst_rgb * dst_a * (1 - src_a)) / out_a
// ^^^^^^^^^^^
// t0
//
// Omit division by `out_a` since `Compose` is supposed to output a
// premultiplied RGBA image as reversal of premultiplication is handled
// externally.
//
VImage outRGBPremultiplied = srcWithoutAlpha + dstWithoutAlpha * t0;
// Combine RGB and alpha channel into output image:
return outRGBPremultiplied.bandjoin(outAlphaNormalized * 255.0);
}
/*
Cutout src over dst with given gravity.
*/
VImage Cutout(VImage mask, VImage dst, const int gravity) {
using sharp::CalculateCrop;
using sharp::HasAlpha;
using sharp::MaximumImageAlpha;
bool maskHasAlpha = HasAlpha(mask);
if (!maskHasAlpha && mask.bands() > 1) {
throw VError("Overlay image must have an alpha channel or one band");
}
if (!HasAlpha(dst)) {
throw VError("Image to be overlaid must have an alpha channel");
}
if (mask.width() > dst.width() || mask.height() > dst.height()) {
throw VError("Overlay image must have same dimensions or smaller");
}
// Enlarge overlay mask, if required
if (mask.width() < dst.width() || mask.height() < dst.height()) {
// Calculate the (left, top) coordinates of the output image within the input image, applying the given gravity.
int left;
int top;
std::tie(left, top) = CalculateCrop(dst.width(), dst.height(), mask.width(), mask.height(), gravity);
// Embed onto transparent background
std::vector<double> background { 0.0, 0.0, 0.0, 0.0 };
mask = mask.embed(left, top, dst.width(), dst.height(), VImage::option()
->set("extend", VIPS_EXTEND_BACKGROUND)
->set("background", background));
}
// we use the mask alpha if it has alpha
if (maskHasAlpha) {
mask = mask.extract_band(mask.bands() - 1, VImage::option()->set("n", 1));;
}
// Split dst into an optional alpha
VImage dstAlpha = dst.extract_band(dst.bands() - 1, VImage::option()->set("n", 1));
// we use the dst non-alpha
dst = dst.extract_band(0, VImage::option()->set("n", dst.bands() - 1));
// the range of the mask and the image need to match .. one could be
// 16-bit, one 8-bit
double const dstMax = MaximumImageAlpha(dst.interpretation());
double const maskMax = MaximumImageAlpha(mask.interpretation());
// combine the new mask and the existing alpha ... there are
// many ways of doing this, mult is the simplest
mask = dstMax * ((mask / maskMax) * (dstAlpha / dstMax));
// append the mask to the image data ... the mask might be float now,
// we must cast the format down to match the image data
return dst.bandjoin(mask.cast(dst.format()));
}
/* /*
* Tint an image using the specified chroma, preserving the original image luminance * Tint an image using the specified chroma, preserving the original image luminance
*/ */
@@ -309,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

@@ -35,27 +35,6 @@ namespace sharp {
*/ */
VImage EnsureAlpha(VImage image); VImage EnsureAlpha(VImage image);
/*
Alpha composite src over dst with given gravity.
Assumes alpha channels are already premultiplied and will be unpremultiplied after.
*/
VImage Composite(VImage src, VImage dst, const int gravity);
/*
Composite overlayImage over image at given position
*/
VImage Composite(VImage image, VImage overlayImage, int const x, int const y);
/*
Alpha composite overlayImage over image, assumes matching dimensions
*/
VImage AlphaComposite(VImage image, VImage overlayImage);
/*
Cutout src over dst with given gravity.
*/
VImage Cutout(VImage src, VImage dst, const int gravity);
/* /*
* Tint an image using the specified chroma, preserving the original image luminance * Tint an image using the specified chroma, preserving the original image luminance
*/ */
@@ -118,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

@@ -75,7 +75,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();
} }
@@ -343,30 +344,20 @@ class PipelineWorker : public Nan::AsyncWorker {
image = image.colourspace(VIPS_INTERPRETATION_B_W); image = image.colourspace(VIPS_INTERPRETATION_B_W);
} }
// Ensure image has an alpha channel when there is an overlay with an alpha channel
VImage overlayImage;
ImageType overlayImageType = ImageType::UNKNOWN;
bool shouldOverlayWithAlpha = FALSE;
if (baton->overlay != nullptr) {
std::tie(overlayImage, overlayImageType) = OpenInput(baton->overlay, baton->accessMethod);
if (HasAlpha(overlayImage)) {
shouldOverlayWithAlpha = !baton->overlayCutout;
if (!HasAlpha(image)) {
double const multiplier = sharp::Is16Bit(image.interpretation()) ? 256.0 : 1.0;
image = image.bandjoin(
VImage::new_matrix(image.width(), image.height()).new_from_image(255 * multiplier));
}
}
}
bool const shouldResize = xfactor != 1.0 || yfactor != 1.0; bool const shouldResize = xfactor != 1.0 || yfactor != 1.0;
bool const shouldBlur = baton->blurSigma != 0.0; bool const shouldBlur = baton->blurSigma != 0.0;
bool const shouldConv = baton->convKernelWidth * baton->convKernelHeight > 0; bool const shouldConv = baton->convKernelWidth * baton->convKernelHeight > 0;
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 shouldModulate = baton->brightness != 1.0 || baton->saturation != 1.0 || baton->hue != 0.0;
if (shouldComposite && !HasAlpha(image)) {
image = sharp::EnsureAlpha(image);
}
bool const shouldPremultiplyAlpha = HasAlpha(image) && bool const shouldPremultiplyAlpha = HasAlpha(image) &&
(shouldResize || shouldBlur || shouldConv || shouldSharpen || shouldOverlayWithAlpha); (shouldResize || shouldBlur || shouldConv || shouldSharpen || shouldComposite);
// Premultiply image alpha channel before all transformations to avoid // Premultiply image alpha channel before all transformations to avoid
// dark fringing around bright pixels // dark fringing around bright pixels
@@ -539,77 +530,76 @@ 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);
} }
// Composite with overlay, if present // Composite
if (baton->overlay != nullptr) { if (shouldComposite) {
// Verify overlay image is within current dimensions for (Composite *composite : baton->composite) {
if (overlayImage.width() > image.width() || overlayImage.height() > image.height()) { VImage compositeImage;
throw vips::VError("Overlay image must have same dimensions or smaller"); ImageType compositeImageType = ImageType::UNKNOWN;
} std::tie(compositeImage, compositeImageType) = OpenInput(composite->input, baton->accessMethod);
// Check if overlay is tiled // Verify within current dimensions
if (baton->overlayTile) { if (compositeImage.width() > image.width() || compositeImage.height() > image.height()) {
int const overlayImageWidth = overlayImage.width(); throw vips::VError("Image to composite must have same dimensions or smaller");
int const overlayImageHeight = overlayImage.height();
int across = 0;
int down = 0;
// Use gravity in overlay
if (overlayImageWidth <= baton->width) {
across = static_cast<int>(ceil(static_cast<double>(image.width()) / overlayImageWidth));
} }
if (overlayImageHeight <= baton->height) { // Check if overlay is tiled
down = static_cast<int>(ceil(static_cast<double>(image.height()) / overlayImageHeight)); if (composite->tile) {
} int across = 0;
if (across != 0 || down != 0) { int down = 0;
int left; // Use gravity in overlay
int top; if (compositeImage.width() <= baton->width) {
overlayImage = overlayImage.replicate(across, down); across = static_cast<int>(ceil(static_cast<double>(image.width()) / compositeImage.width()));
if (baton->overlayXOffset >= 0 && baton->overlayYOffset >= 0) {
// the overlayX/YOffsets will now be used to CalculateCrop for extract_area
std::tie(left, top) = sharp::CalculateCrop(
overlayImage.width(), overlayImage.height(), image.width(), image.height(),
baton->overlayXOffset, baton->overlayYOffset);
} else {
// the overlayGravity will now be used to CalculateCrop for extract_area
std::tie(left, top) = sharp::CalculateCrop(
overlayImage.width(), overlayImage.height(), image.width(), image.height(), baton->overlayGravity);
} }
overlayImage = overlayImage.extract_area(left, top, image.width(), image.height()); if (compositeImage.height() <= baton->height) {
} down = static_cast<int>(ceil(static_cast<double>(image.height()) / compositeImage.height()));
// the overlayGravity was used for extract_area, therefore set it back to its default value of 0
baton->overlayGravity = 0;
}
if (baton->overlayCutout) {
// 'cut out' the image, premultiplication is not required
image = sharp::Cutout(overlayImage, image, baton->overlayGravity);
} else {
// Ensure overlay is sRGB
overlayImage = overlayImage.colourspace(VIPS_INTERPRETATION_sRGB);
// Ensure overlay matches premultiplication state
if (shouldPremultiplyAlpha) {
// Ensure overlay has alpha channel
if (!HasAlpha(overlayImage)) {
double const multiplier = sharp::Is16Bit(overlayImage.interpretation()) ? 256.0 : 1.0;
overlayImage = overlayImage.bandjoin(
VImage::new_matrix(overlayImage.width(), overlayImage.height()).new_from_image(255 * multiplier));
} }
overlayImage = overlayImage.premultiply(); if (across != 0 || down != 0) {
int left;
int top;
compositeImage = compositeImage.replicate(across, down);
if (composite->left >= 0 && composite->top >= 0) {
std::tie(left, top) = sharp::CalculateCrop(
compositeImage.width(), compositeImage.height(), image.width(), image.height(),
composite->left, composite->top);
} else {
std::tie(left, top) = sharp::CalculateCrop(
compositeImage.width(), compositeImage.height(), image.width(), image.height(), composite->gravity);
}
compositeImage = compositeImage.extract_area(left, top, image.width(), image.height());
}
// gravity was used for extract_area, set it back to its default value of 0
composite->gravity = 0;
} }
// Ensure image to composite is sRGB with premultiplied alpha
compositeImage = compositeImage.colourspace(VIPS_INTERPRETATION_sRGB);
if (!HasAlpha(compositeImage)) {
compositeImage = sharp::EnsureAlpha(compositeImage);
}
compositeImage = compositeImage.premultiply();
// Calculate position
int left; int left;
int top; int top;
if (baton->overlayXOffset >= 0 && baton->overlayYOffset >= 0) { if (composite->left >= 0 && composite->top >= 0) {
// Composite images at given offsets // Composite image at given offsets
std::tie(left, top) = sharp::CalculateCrop(image.width(), image.height(), std::tie(left, top) = sharp::CalculateCrop(image.width(), image.height(),
overlayImage.width(), overlayImage.height(), baton->overlayXOffset, baton->overlayYOffset); compositeImage.width(), compositeImage.height(), composite->left, composite->top);
} else { } else {
// Composite images with given gravity // Composite image with given gravity
std::tie(left, top) = sharp::CalculateCrop(image.width(), image.height(), std::tie(left, top) = sharp::CalculateCrop(image.width(), image.height(),
overlayImage.width(), overlayImage.height(), baton->overlayGravity); compositeImage.width(), compositeImage.height(), composite->gravity);
} }
image = sharp::Composite(image, overlayImage, left, top); // Composite
image = image.composite2(compositeImage, composite->mode, VImage::option()
->set("premultiplied", TRUE)
->set("x", left)
->set("y", top));
} }
} }
@@ -1029,13 +1019,17 @@ class PipelineWorker : public Nan::AsyncWorker {
GetFromPersistent(index); GetFromPersistent(index);
return index + 1; return index + 1;
}); });
// Delete baton
delete baton->input; delete baton->input;
delete baton->overlay;
delete baton->boolean; delete baton->boolean;
for_each(baton->joinChannelIn.begin(), baton->joinChannelIn.end(), for (Composite *composite : baton->composite) {
[this](sharp::InputDescriptor *joinChannelIn) { delete composite->input;
delete joinChannelIn; delete composite;
}); }
for (sharp::InputDescriptor *input : baton->joinChannelIn) {
delete input;
}
delete baton; delete baton;
// Handle warnings // Handle warnings
@@ -1182,14 +1176,21 @@ NAN_METHOD(pipeline) {
// Tint chroma // Tint chroma
baton->tintA = AttrTo<double>(options, "tintA"); baton->tintA = AttrTo<double>(options, "tintA");
baton->tintB = AttrTo<double>(options, "tintB"); baton->tintB = AttrTo<double>(options, "tintB");
// Overlay options // Composite
if (HasAttr(options, "overlay")) { v8::Local<v8::Array> compositeArray = Nan::Get(options, Nan::New("composite").ToLocalChecked())
baton->overlay = CreateInputDescriptor(AttrAs<v8::Object>(options, "overlay"), buffersToPersist); .ToLocalChecked().As<v8::Array>();
baton->overlayGravity = AttrTo<int32_t>(options, "overlayGravity"); int const compositeArrayLength = AttrTo<uint32_t>(compositeArray, "length");
baton->overlayXOffset = AttrTo<int32_t>(options, "overlayXOffset"); for (int i = 0; i < compositeArrayLength; i++) {
baton->overlayYOffset = AttrTo<int32_t>(options, "overlayYOffset"); v8::Local<v8::Object> compositeObject = Nan::Get(compositeArray, i).ToLocalChecked().As<v8::Object>();
baton->overlayTile = AttrTo<bool>(options, "overlayTile"); Composite *composite = new Composite;
baton->overlayCutout = AttrTo<bool>(options, "overlayCutout"); composite->input = CreateInputDescriptor(AttrAs<v8::Object>(compositeObject, "input"), buffersToPersist);
composite->mode = static_cast<VipsBlendMode>(
vips_enum_from_nick(nullptr, VIPS_TYPE_BLEND_MODE, AttrAsStr(compositeObject, "blend").data()));
composite->gravity = AttrTo<uint32_t>(compositeObject, "gravity");
composite->left = AttrTo<int32_t>(compositeObject, "left");
composite->top = AttrTo<int32_t>(compositeObject, "top");
composite->tile = AttrTo<bool>(compositeObject, "tile");
baton->composite.push_back(composite);
} }
// Resize options // Resize options
baton->withoutEnlargement = AttrTo<bool>(options, "withoutEnlargement"); baton->withoutEnlargement = AttrTo<bool>(options, "withoutEnlargement");
@@ -1215,6 +1216,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");

View File

@@ -34,6 +34,23 @@ enum class Canvas {
IGNORE_ASPECT IGNORE_ASPECT
}; };
struct Composite {
sharp::InputDescriptor *input;
VipsBlendMode mode;
int gravity;
int left;
int top;
bool tile;
Composite():
input(nullptr),
mode(VIPS_BLEND_MODE_OVER),
gravity(0),
left(-1),
top(-1),
tile(false) {}
};
struct PipelineBaton { struct PipelineBaton {
sharp::InputDescriptor *input; sharp::InputDescriptor *input;
std::string iccProfilePath; std::string iccProfilePath;
@@ -42,12 +59,7 @@ struct PipelineBaton {
std::string fileOut; std::string fileOut;
void *bufferOut; void *bufferOut;
size_t bufferOutLength; size_t bufferOutLength;
sharp::InputDescriptor *overlay; std::vector<Composite *> composite;
int overlayGravity;
int overlayXOffset;
int overlayYOffset;
bool overlayTile;
bool overlayCutout;
std::vector<sharp::InputDescriptor *> joinChannelIn; std::vector<sharp::InputDescriptor *> joinChannelIn;
int topOffsetPre; int topOffsetPre;
int leftOffsetPre; int leftOffsetPre;
@@ -75,6 +87,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;
@@ -161,12 +176,6 @@ struct PipelineBaton {
input(nullptr), input(nullptr),
limitInputPixels(0), limitInputPixels(0),
bufferOutLength(0), bufferOutLength(0),
overlay(nullptr),
overlayGravity(0),
overlayXOffset(-1),
overlayYOffset(-1),
overlayTile(false),
overlayCutout(false),
topOffsetPre(-1), topOffsetPre(-1),
topOffsetPost(-1), topOffsetPost(-1),
channels(0), channels(0),
@@ -183,6 +192,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.0),
medianSize(0), medianSize(0),
sharpenSigma(0.0), sharpenSigma(0.0),
sharpenFlat(1.0), sharpenFlat(1.0),

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 B

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

View File

@@ -100,6 +100,7 @@ module.exports = {
inputTiff8BitDepth: getPath('8bit_depth.tiff'), inputTiff8BitDepth: getPath('8bit_depth.tiff'),
inputGif: getPath('Crash_test.gif'), // http://upload.wikimedia.org/wikipedia/commons/e/e3/Crash_test.gif inputGif: getPath('Crash_test.gif'), // http://upload.wikimedia.org/wikipedia/commons/e/e3/Crash_test.gif
inputGifGreyPlusAlpha: getPath('grey-plus-alpha.gif'), // http://i.imgur.com/gZ5jlmE.gif inputGifGreyPlusAlpha: getPath('grey-plus-alpha.gif'), // http://i.imgur.com/gZ5jlmE.gif
inputGifAnimated: getPath('rotating-squares.gif'), // CC0 https://loading.io/spinner/blocks/-rotating-squares-preloader-gif
inputSvg: getPath('check.svg'), // http://dev.w3.org/SVG/tools/svgweb/samples/svg-files/check.svg inputSvg: getPath('check.svg'), // http://dev.w3.org/SVG/tools/svgweb/samples/svg-files/check.svg
inputSvgWithEmbeddedImages: getPath('struct-image-04-t.svg'), // https://dev.w3.org/SVG/profiles/1.2T/test/svg/struct-image-04-t.svg inputSvgWithEmbeddedImages: getPath('struct-image-04-t.svg'), // https://dev.w3.org/SVG/profiles/1.2T/test/svg/struct-image-04-t.svg
@@ -119,6 +120,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/rotating-squares.gif vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

View File

@@ -484,6 +484,20 @@
... ...
fun:_ZN4node9inspector5Agent5StartERKSsSt10shared_ptrINS_12DebugOptionsEEb fun:_ZN4node9inspector5Agent5StartERKSsSt10shared_ptrINS_12DebugOptionsEEb
} }
{
leak_nodejs_start
Memcheck:Leak
match-leak-kinds: definite
fun:_Znwm
fun:_ZN4node5StartEiPPc
}
{
leak_nodejs_start_background_task_runner
Memcheck:Leak
match-leak-kinds: possible
...
fun:_ZN4node20BackgroundTaskRunnerC1Ei
}
{ {
leak_nan_FunctionCallbackInfo leak_nan_FunctionCallbackInfo
Memcheck:Leak Memcheck:Leak

View File

@@ -5,8 +5,10 @@ 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);
}); });

298
test/unit/composite.js Normal file
View File

@@ -0,0 +1,298 @@
'use strict';
const assert = require('assert');
const fixtures = require('../fixtures');
const sharp = require('../../');
const red = { r: 255, g: 0, b: 0, alpha: 0.5 };
const green = { r: 0, g: 255, b: 0, alpha: 0.5 };
const blue = { r: 0, g: 0, b: 255, alpha: 0.5 };
const redRect = {
create: {
width: 80,
height: 60,
channels: 4,
background: red
}
};
const greenRect = {
create: {
width: 40,
height: 40,
channels: 4,
background: green
}
};
const blueRect = {
create: {
width: 60,
height: 40,
channels: 4,
background: blue
}
};
const blends = [
'over',
'xor',
'saturate',
'dest-over'
];
// Test
describe('composite', () => {
it('blend', () => Promise.all(
blends.map(blend => {
const filename = `composite.blend.${blend}.png`;
const actual = fixtures.path(`output.${filename}`);
const expected = fixtures.expected(filename);
return sharp(redRect)
.composite([{
input: blueRect,
blend
}])
.toFile(actual)
.then(() => {
fixtures.assertMaxColourDistance(actual, expected);
});
})
));
it('multiple', () => {
const filename = 'composite-multiple.png';
const actual = fixtures.path(`output.${filename}`);
const expected = fixtures.expected(filename);
return sharp(redRect)
.composite([{
input: blueRect,
gravity: 'northeast'
}, {
input: greenRect,
gravity: 'southwest'
}])
.toFile(actual)
.then(() => {
fixtures.assertMaxColourDistance(actual, expected);
});
});
it('zero offset', done => {
sharp(fixtures.inputJpg)
.resize(400)
.composite([{
input: fixtures.inputPngWithTransparency16bit,
top: 0,
left: 0
}])
.toBuffer((err, data, info) => {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3, info.channels);
fixtures.assertSimilar(fixtures.expected('overlay-offset-0.jpg'), data, done);
});
});
it('offset and gravity', done => {
sharp(fixtures.inputJpg)
.resize(400)
.composite([{
input: fixtures.inputPngWithTransparency16bit,
left: 10,
top: 10,
gravity: 4
}])
.toBuffer((err, data, info) => {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3, info.channels);
fixtures.assertSimilar(fixtures.expected('overlay-offset-with-gravity.jpg'), data, done);
});
});
it('offset, gravity and tile', done => {
sharp(fixtures.inputJpg)
.resize(400)
.composite([{
input: fixtures.inputPngWithTransparency16bit,
left: 10,
top: 10,
gravity: 4,
tile: true
}])
.toBuffer((err, data, info) => {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3, info.channels);
fixtures.assertSimilar(fixtures.expected('overlay-offset-with-gravity-tile.jpg'), data, done);
});
});
it('offset and tile', done => {
sharp(fixtures.inputJpg)
.resize(400)
.composite([{
input: fixtures.inputPngWithTransparency16bit,
left: 10,
top: 10,
tile: true
}])
.toBuffer((err, data, info) => {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3, info.channels);
fixtures.assertSimilar(fixtures.expected('overlay-offset-with-tile.jpg'), data, done);
});
});
it('cutout via dest-in', done => {
sharp(fixtures.inputJpg)
.resize(300, 300)
.composite([{
input: Buffer.from('<svg><rect x="0" y="0" width="200" height="200" rx="50" ry="50"/></svg>'),
density: 96,
blend: 'dest-in',
cutout: true
}])
.png()
.toBuffer((err, data, info) => {
if (err) throw err;
assert.strictEqual('png', info.format);
assert.strictEqual(300, info.width);
assert.strictEqual(300, info.height);
assert.strictEqual(4, info.channels);
fixtures.assertSimilar(fixtures.expected('composite-cutout.png'), data, done);
});
});
describe('numeric gravity', () => {
Object.keys(sharp.gravity).forEach(gravity => {
it(gravity, done => {
sharp(fixtures.inputJpg)
.resize(80)
.composite([{
input: fixtures.inputPngWithTransparency16bit,
gravity: gravity
}])
.toBuffer((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(fixtures.expected(`overlay-gravity-${gravity}.jpg`), data, done);
});
});
});
});
describe('string gravity', () => {
Object.keys(sharp.gravity).forEach(gravity => {
it(gravity, done => {
const expected = fixtures.expected('overlay-gravity-' + gravity + '.jpg');
sharp(fixtures.inputJpg)
.resize(80)
.composite([{
input: fixtures.inputPngWithTransparency16bit,
gravity: sharp.gravity[gravity]
}])
.toBuffer((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('tile and gravity', () => {
Object.keys(sharp.gravity).forEach(gravity => {
it(gravity, done => {
const expected = fixtures.expected('overlay-tile-gravity-' + gravity + '.jpg');
sharp(fixtures.inputJpg)
.resize(80)
.composite([{
input: fixtures.inputPngWithTransparency16bit,
tile: true,
gravity: gravity
}])
.toBuffer((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('validation', () => {
it('missing images', () => {
assert.throws(() => {
sharp().composite();
}, /Expected array for images to composite but received undefined of type undefined/);
});
it('invalid images', () => {
assert.throws(() => {
sharp().composite(['invalid']);
}, /Expected object for image to composite but received invalid of type string/);
});
it('missing input', () => {
assert.throws(() => {
sharp().composite([{}]);
}, /Unsupported input/);
});
it('invalid blend', () => {
assert.throws(() => {
sharp().composite([{ input: 'test', blend: 'invalid' }]);
}, /Expected valid blend name for blend but received invalid of type string/);
});
it('invalid tile', () => {
assert.throws(() => {
sharp().composite([{ input: 'test', tile: 'invalid' }]);
}, /Expected boolean for tile but received invalid of type string/);
});
it('invalid left', () => {
assert.throws(() => {
sharp().composite([{ input: 'test', left: 0.5 }]);
}, /Expected positive integer for left but received 0.5 of type number/);
});
it('invalid top', () => {
assert.throws(() => {
sharp().composite([{ input: 'test', top: -1 }]);
}, /Expected positive integer for top but received -1 of type number/);
});
it('left but no top', () => {
assert.throws(() => {
sharp().composite([{ input: 'test', left: 1 }]);
}, /Expected both left and top to be set/);
});
it('top but no left', () => {
assert.throws(() => {
sharp().composite([{ input: 'test', top: 1 }]);
}, /Expected both left and top to be set/);
});
it('invalid gravity', () => {
assert.throws(() => {
sharp().composite([{ input: 'test', gravity: 'invalid' }]);
}, /Expected valid gravity for gravity but received invalid of type string/);
});
});
});

View File

@@ -1,73 +0,0 @@
'use strict';
const assert = require('assert');
const fixtures = require('../fixtures');
const sharp = require('../../');
describe('Deprecated background', function () {
it('Flatten to RGB orange', function (done) {
sharp(fixtures.inputPngWithTransparency)
.flatten()
.background({ r: 255, g: 102, b: 0 })
.resize(400, 300)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(400, info.width);
assert.strictEqual(300, info.height);
fixtures.assertSimilar(fixtures.expected('flatten-orange.jpg'), data, done);
});
});
it('Flatten to CSS/hex orange', function (done) {
sharp(fixtures.inputPngWithTransparency)
.flatten()
.background('#ff6600')
.resize(400, 300)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(400, info.width);
assert.strictEqual(300, info.height);
fixtures.assertSimilar(fixtures.expected('flatten-orange.jpg'), data, done);
});
});
it('Flatten 16-bit PNG with transparency to orange', function (done) {
const output = fixtures.path('output.flatten-rgb16-orange.jpg');
sharp(fixtures.inputPngWithTransparency16bit)
.flatten()
.background({ r: 255, g: 102, b: 0 })
.toFile(output, function (err, info) {
if (err) throw err;
assert.strictEqual(true, info.size > 0);
assert.strictEqual(32, info.width);
assert.strictEqual(32, info.height);
fixtures.assertMaxColourDistance(output, fixtures.expected('flatten-rgb16-orange.jpg'), 25);
done();
});
});
it('Ignored for JPEG', function (done) {
sharp(fixtures.inputJpg)
.background('#ff0000')
.flatten()
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3, info.channels);
done();
});
});
it('extend all sides equally with RGB', function (done) {
sharp(fixtures.inputJpg)
.resize(120)
.background({ r: 255, g: 0, b: 0 })
.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.jpg'), data, done);
});
});
});

View File

@@ -1,279 +0,0 @@
'use strict';
const assert = require('assert');
const sharp = require('../../');
const fixtures = require('../fixtures');
describe('Deprecated crop', function () {
[
{
name: 'North',
width: 320,
height: 80,
gravity: sharp.gravity.north,
fixture: 'gravity-north.jpg'
},
{
name: 'East',
width: 80,
height: 320,
gravity: sharp.gravity.east,
fixture: 'gravity-east.jpg'
},
{
name: 'South',
width: 320,
height: 80,
gravity: sharp.gravity.south,
fixture: 'gravity-south.jpg'
},
{
name: 'West',
width: 80,
height: 320,
gravity: sharp.gravity.west,
fixture: 'gravity-west.jpg'
},
{
name: 'Center',
width: 320,
height: 80,
gravity: sharp.gravity.center,
fixture: 'gravity-center.jpg'
},
{
name: 'Centre',
width: 80,
height: 320,
gravity: sharp.gravity.centre,
fixture: 'gravity-centre.jpg'
},
{
name: 'Default (centre)',
width: 80,
height: 320,
gravity: undefined,
fixture: 'gravity-centre.jpg'
},
{
name: 'Northeast',
width: 320,
height: 80,
gravity: sharp.gravity.northeast,
fixture: 'gravity-north.jpg'
},
{
name: 'Northeast',
width: 80,
height: 320,
gravity: sharp.gravity.northeast,
fixture: 'gravity-east.jpg'
},
{
name: 'Southeast',
width: 320,
height: 80,
gravity: sharp.gravity.southeast,
fixture: 'gravity-south.jpg'
},
{
name: 'Southeast',
width: 80,
height: 320,
gravity: sharp.gravity.southeast,
fixture: 'gravity-east.jpg'
},
{
name: 'Southwest',
width: 320,
height: 80,
gravity: sharp.gravity.southwest,
fixture: 'gravity-south.jpg'
},
{
name: 'Southwest',
width: 80,
height: 320,
gravity: sharp.gravity.southwest,
fixture: 'gravity-west.jpg'
},
{
name: 'Northwest',
width: 320,
height: 80,
gravity: sharp.gravity.northwest,
fixture: 'gravity-north.jpg'
},
{
name: 'Northwest',
width: 80,
height: 320,
gravity: sharp.gravity.northwest,
fixture: 'gravity-west.jpg'
}
].forEach(function (settings) {
it(settings.name + ' gravity', function (done) {
sharp(fixtures.inputJpg)
.resize(settings.width, settings.height)
.crop(settings.gravity)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(settings.width, info.width);
assert.strictEqual(settings.height, info.height);
fixtures.assertSimilar(fixtures.expected(settings.fixture), data, done);
});
});
});
it('Allows specifying the gravity as a string', function (done) {
sharp(fixtures.inputJpg)
.resize(80, 320)
.crop('east')
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(80, info.width);
assert.strictEqual(320, info.height);
fixtures.assertSimilar(fixtures.expected('gravity-east.jpg'), data, done);
});
});
it('Invalid values fail', function () {
assert.throws(function () {
sharp().crop(9);
}, /Expected valid crop id\/name\/strategy for crop but received 9 of type number/);
assert.throws(function () {
sharp().crop(1.1);
}, /Expected valid crop id\/name\/strategy for crop but received 1.1 of type number/);
assert.throws(function () {
sharp().crop(-1);
}, /Expected valid crop id\/name\/strategy for crop but received -1 of type number/);
assert.throws(function () {
sharp().crop('zoinks');
}, /Expected valid crop id\/name\/strategy for crop but received zoinks of type string/);
});
it('Uses default value when none specified', function () {
assert.doesNotThrow(function () {
sharp().crop();
});
});
it('Skip crop when post-resize dimensions are at target', function () {
return sharp(fixtures.inputJpg)
.resize(1600, 1200)
.toBuffer()
.then(function (input) {
return sharp(input)
.resize(1110)
.crop(sharp.strategy.attention)
.toBuffer({ resolveWithObject: true })
.then(function (result) {
assert.strictEqual(1110, result.info.width);
assert.strictEqual(832, result.info.height);
assert.strictEqual(undefined, result.info.cropOffsetLeft);
assert.strictEqual(undefined, result.info.cropOffsetTop);
});
});
});
describe('Entropy-based strategy', function () {
it('JPEG', function (done) {
sharp(fixtures.inputJpg)
.resize(80, 320)
.crop(sharp.strategy.entropy)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3, info.channels);
assert.strictEqual(80, info.width);
assert.strictEqual(320, info.height);
assert.strictEqual(-117, info.cropOffsetLeft);
assert.strictEqual(0, info.cropOffsetTop);
fixtures.assertSimilar(fixtures.expected('crop-strategy-entropy.jpg'), data, done);
});
});
it('PNG', function (done) {
sharp(fixtures.inputPngWithTransparency)
.resize(320, 80)
.crop(sharp.strategy.entropy)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('png', info.format);
assert.strictEqual(4, info.channels);
assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height);
assert.strictEqual(0, info.cropOffsetLeft);
assert.strictEqual(-80, info.cropOffsetTop);
fixtures.assertSimilar(fixtures.expected('crop-strategy.png'), data, done);
});
});
it('supports the strategy passed as a string', function (done) {
sharp(fixtures.inputPngWithTransparency)
.resize(320, 80)
.crop('entropy')
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('png', info.format);
assert.strictEqual(4, info.channels);
assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height);
assert.strictEqual(0, info.cropOffsetLeft);
assert.strictEqual(-80, info.cropOffsetTop);
fixtures.assertSimilar(fixtures.expected('crop-strategy.png'), data, done);
});
});
});
describe('Attention strategy', function () {
it('JPEG', function (done) {
sharp(fixtures.inputJpg)
.resize(80, 320)
.crop(sharp.strategy.attention)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3, info.channels);
assert.strictEqual(80, info.width);
assert.strictEqual(320, info.height);
assert.strictEqual(-143, info.cropOffsetLeft);
assert.strictEqual(0, info.cropOffsetTop);
fixtures.assertSimilar(fixtures.expected('crop-strategy-attention.jpg'), data, done);
});
});
it('PNG', function (done) {
sharp(fixtures.inputPngWithTransparency)
.resize(320, 80)
.crop(sharp.strategy.attention)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('png', info.format);
assert.strictEqual(4, info.channels);
assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height);
assert.strictEqual(0, info.cropOffsetLeft);
assert.strictEqual(0, info.cropOffsetTop);
fixtures.assertSimilar(fixtures.expected('crop-strategy.png'), data, done);
});
});
it('supports the strategy passed as a string', function (done) {
sharp(fixtures.inputPngWithTransparency)
.resize(320, 80)
.crop('attention')
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('png', info.format);
assert.strictEqual(4, info.channels);
assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height);
assert.strictEqual(0, info.cropOffsetLeft);
assert.strictEqual(0, info.cropOffsetTop);
fixtures.assertSimilar(fixtures.expected('crop-strategy.png'), data, done);
});
});
});
});

View File

@@ -1,440 +0,0 @@
'use strict';
const assert = require('assert');
const sharp = require('../../');
const fixtures = require('../fixtures');
describe('Deprecated embed', function () {
it('Allows specifying the gravity as a string', function (done) {
sharp(fixtures.inputJpg)
.resize(320, 240)
.embed('center')
.png()
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
fixtures.assertSimilar(fixtures.expected('embed-3-into-3.png'), data, done);
});
});
it('JPEG within PNG, no alpha channel', function (done) {
sharp(fixtures.inputJpg)
.embed()
.resize(320, 240)
.png()
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
assert.strictEqual(3, info.channels);
fixtures.assertSimilar(fixtures.expected('embed-3-into-3.png'), data, done);
});
});
it('JPEG within WebP, to include alpha channel', function (done) {
sharp(fixtures.inputJpg)
.resize(320, 240)
.background({ r: 0, g: 0, b: 0, alpha: 0 })
.embed()
.webp()
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('webp', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
assert.strictEqual(4, info.channels);
fixtures.assertSimilar(fixtures.expected('embed-3-into-4.webp'), data, done);
});
});
it('PNG with alpha channel', function (done) {
sharp(fixtures.inputPngWithTransparency)
.resize(50, 50)
.embed()
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(50, info.width);
assert.strictEqual(50, info.height);
assert.strictEqual(4, info.channels);
fixtures.assertSimilar(fixtures.expected('embed-4-into-4.png'), data, done);
});
});
it('16-bit PNG with alpha channel', function (done) {
sharp(fixtures.inputPngWithTransparency16bit)
.resize(32, 16)
.embed()
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(32, info.width);
assert.strictEqual(16, info.height);
assert.strictEqual(4, info.channels);
fixtures.assertSimilar(fixtures.expected('embed-16bit.png'), data, done);
});
});
it('16-bit PNG with alpha channel onto RGBA', function (done) {
sharp(fixtures.inputPngWithTransparency16bit)
.resize(32, 16)
.embed()
.background({ r: 0, g: 0, b: 0, alpha: 0 })
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(32, info.width);
assert.strictEqual(16, info.height);
assert.strictEqual(4, info.channels);
fixtures.assertSimilar(fixtures.expected('embed-16bit-rgba.png'), data, done);
});
});
it('PNG with 2 channels', function (done) {
sharp(fixtures.inputPngWithGreyAlpha)
.resize(32, 16)
.embed()
.background({ r: 0, g: 0, b: 0, alpha: 0 })
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(32, info.width);
assert.strictEqual(16, info.height);
assert.strictEqual(4, info.channels);
fixtures.assertSimilar(fixtures.expected('embed-2channel.png'), data, done);
});
});
it('Enlarge and embed', function (done) {
sharp(fixtures.inputPngWithOneColor)
.embed()
.resize(320, 240)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
assert.strictEqual(3, info.channels);
fixtures.assertSimilar(fixtures.expected('embed-enlarge.png'), data, done);
});
});
it('Embed invalid param values should fail', function () {
assert.throws(function () {
sharp().embed(-1);
});
assert.throws(function () {
sharp().embed(8.1);
});
assert.throws(function () {
sharp().embed(9);
});
assert.throws(function () {
sharp().embed(1000000);
});
assert.throws(function () {
sharp().embed(false);
});
assert.throws(function () {
sharp().embed('vallejo');
});
});
it('Embed gravity horizontal northwest', function (done) {
sharp(fixtures.inputPngEmbed)
.resize(200, 100)
.background({ r: 0, g: 0, b: 0, alpha: 0 })
.embed(sharp.gravity.northwest)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(200, info.width);
assert.strictEqual(100, info.height);
assert.strictEqual(4, info.channels);
fixtures.assertSimilar(fixtures.expected('./embedgravitybird/a1-nw.png'), data, done);
});
});
it('Embed gravity horizontal north', function (done) {
sharp(fixtures.inputPngEmbed)
.resize(200, 100)
.background({ r: 0, g: 0, b: 0, alpha: 0 })
.embed(sharp.gravity.north)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(200, info.width);
assert.strictEqual(100, info.height);
assert.strictEqual(4, info.channels);
fixtures.assertSimilar(fixtures.expected('./embedgravitybird/a2-n.png'), data, done);
});
});
it('Embed gravity horizontal northeast', function (done) {
sharp(fixtures.inputPngEmbed)
.resize(200, 100)
.background({ r: 0, g: 0, b: 0, alpha: 0 })
.embed(sharp.gravity.northeast)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(200, info.width);
assert.strictEqual(100, info.height);
assert.strictEqual(4, info.channels);
fixtures.assertSimilar(fixtures.expected('./embedgravitybird/a3-ne.png'), data, done);
});
});
it('Embed gravity horizontal east', function (done) {
sharp(fixtures.inputPngEmbed)
.resize(200, 100)
.background({ r: 0, g: 0, b: 0, alpha: 0 })
.embed(sharp.gravity.east)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(200, info.width);
assert.strictEqual(100, info.height);
assert.strictEqual(4, info.channels);
fixtures.assertSimilar(fixtures.expected('./embedgravitybird/a4-e.png'), data, done);
});
});
it('Embed gravity horizontal southeast', function (done) {
sharp(fixtures.inputPngEmbed)
.resize(200, 100)
.background({ r: 0, g: 0, b: 0, alpha: 0 })
.embed(sharp.gravity.southeast)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(200, info.width);
assert.strictEqual(100, info.height);
assert.strictEqual(4, info.channels);
fixtures.assertSimilar(fixtures.expected('./embedgravitybird/a5-se.png'), data, done);
});
});
it('Embed gravity horizontal south', function (done) {
sharp(fixtures.inputPngEmbed)
.resize(200, 100)
.background({ r: 0, g: 0, b: 0, alpha: 0 })
.embed(sharp.gravity.south)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(200, info.width);
assert.strictEqual(100, info.height);
assert.strictEqual(4, info.channels);
fixtures.assertSimilar(fixtures.expected('./embedgravitybird/a6-s.png'), data, done);
});
});
it('Embed gravity horizontal southwest', function (done) {
sharp(fixtures.inputPngEmbed)
.resize(200, 100)
.background({ r: 0, g: 0, b: 0, alpha: 0 })
.embed(sharp.gravity.southwest)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(200, info.width);
assert.strictEqual(100, info.height);
assert.strictEqual(4, info.channels);
fixtures.assertSimilar(fixtures.expected('./embedgravitybird/a7-sw.png'), data, done);
});
});
it('Embed gravity horizontal west', function (done) {
sharp(fixtures.inputPngEmbed)
.resize(200, 100)
.background({ r: 0, g: 0, b: 0, alpha: 0 })
.embed(sharp.gravity.west)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(200, info.width);
assert.strictEqual(100, info.height);
assert.strictEqual(4, info.channels);
fixtures.assertSimilar(fixtures.expected('./embedgravitybird/a8-w.png'), data, done);
});
});
it('Embed gravity horizontal center', function (done) {
sharp(fixtures.inputPngEmbed)
.resize(200, 100)
.background({ r: 0, g: 0, b: 0, alpha: 0 })
.embed(sharp.gravity.center)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(200, info.width);
assert.strictEqual(100, info.height);
assert.strictEqual(4, info.channels);
fixtures.assertSimilar(fixtures.expected('./embedgravitybird/a9-c.png'), data, done);
});
});
it('Embed gravity vertical northwest', function (done) {
sharp(fixtures.inputPngEmbed)
.resize(200, 200)
.background({ r: 0, g: 0, b: 0, alpha: 0 })
.embed(sharp.gravity.northwest)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(200, info.width);
assert.strictEqual(200, info.height);
assert.strictEqual(4, info.channels);
fixtures.assertSimilar(fixtures.expected('./embedgravitybird/1-nw.png'), data, done);
});
});
it('Embed gravity vertical north', function (done) {
sharp(fixtures.inputPngEmbed)
.resize(200, 200)
.background({ r: 0, g: 0, b: 0, alpha: 0 })
.embed(sharp.gravity.north)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(200, info.width);
assert.strictEqual(200, info.height);
assert.strictEqual(4, info.channels);
fixtures.assertSimilar(fixtures.expected('./embedgravitybird/2-n.png'), data, done);
});
});
it('Embed gravity vertical northeast', function (done) {
sharp(fixtures.inputPngEmbed)
.resize(200, 200)
.background({ r: 0, g: 0, b: 0, alpha: 0 })
.embed(sharp.gravity.northeast)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(200, info.width);
assert.strictEqual(200, info.height);
assert.strictEqual(4, info.channels);
fixtures.assertSimilar(fixtures.expected('./embedgravitybird/3-ne.png'), data, done);
});
});
it('Embed gravity vertical east', function (done) {
sharp(fixtures.inputPngEmbed)
.resize(200, 200)
.background({ r: 0, g: 0, b: 0, alpha: 0 })
.embed(sharp.gravity.east)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(200, info.width);
assert.strictEqual(200, info.height);
assert.strictEqual(4, info.channels);
fixtures.assertSimilar(fixtures.expected('./embedgravitybird/4-e.png'), data, done);
});
});
it('Embed gravity vertical southeast', function (done) {
sharp(fixtures.inputPngEmbed)
.resize(200, 200)
.background({ r: 0, g: 0, b: 0, alpha: 0 })
.embed(sharp.gravity.southeast)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(200, info.width);
assert.strictEqual(200, info.height);
assert.strictEqual(4, info.channels);
fixtures.assertSimilar(fixtures.expected('./embedgravitybird/5-se.png'), data, done);
});
});
it('Embed gravity vertical south', function (done) {
sharp(fixtures.inputPngEmbed)
.resize(200, 200)
.background({ r: 0, g: 0, b: 0, alpha: 0 })
.embed(sharp.gravity.south)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(200, info.width);
assert.strictEqual(200, info.height);
assert.strictEqual(4, info.channels);
fixtures.assertSimilar(fixtures.expected('./embedgravitybird/6-s.png'), data, done);
});
});
it('Embed gravity vertical southwest', function (done) {
sharp(fixtures.inputPngEmbed)
.resize(200, 200)
.background({ r: 0, g: 0, b: 0, alpha: 0 })
.embed(sharp.gravity.southwest)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(200, info.width);
assert.strictEqual(200, info.height);
assert.strictEqual(4, info.channels);
fixtures.assertSimilar(fixtures.expected('./embedgravitybird/7-sw.png'), data, done);
});
});
it('Embed gravity vertical west', function (done) {
sharp(fixtures.inputPngEmbed)
.resize(200, 200)
.background({ r: 0, g: 0, b: 0, alpha: 0 })
.embed(sharp.gravity.west)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(200, info.width);
assert.strictEqual(200, info.height);
assert.strictEqual(4, info.channels);
fixtures.assertSimilar(fixtures.expected('./embedgravitybird/8-w.png'), data, done);
});
});
it('Embed gravity vertical center', function (done) {
sharp(fixtures.inputPngEmbed)
.resize(200, 200)
.background({ r: 0, g: 0, b: 0, alpha: 0 })
.embed(sharp.gravity.center)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(200, info.width);
assert.strictEqual(200, info.height);
assert.strictEqual(4, info.channels);
fixtures.assertSimilar(fixtures.expected('./embedgravitybird/9-c.png'), data, done);
});
});
});

View File

@@ -1,261 +0,0 @@
'use strict';
const assert = require('assert');
const sharp = require('../../');
const fixtures = require('../fixtures');
describe('Deprecated resize-related functions', function () {
it('Max width or height considering ratio (portrait)', function (done) {
sharp(fixtures.inputTiff)
.resize(320, 320)
.max()
.jpeg()
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(243, info.width);
assert.strictEqual(320, info.height);
done();
});
});
it('Min width or height considering ratio (portrait)', function (done) {
sharp(fixtures.inputTiff)
.resize(320, 320)
.min()
.jpeg()
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(422, info.height);
done();
});
});
it('Max width or height considering ratio (landscape)', function (done) {
sharp(fixtures.inputJpg)
.resize(320, 320)
.max()
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(261, info.height);
done();
});
});
it('Provide only one dimension with max, should default to crop', function (done) {
sharp(fixtures.inputJpg)
.resize(320)
.max()
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(261, info.height);
done();
});
});
it('Min width or height considering ratio (landscape)', function (done) {
sharp(fixtures.inputJpg)
.resize(320, 320)
.min()
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(392, info.width);
assert.strictEqual(320, info.height);
done();
});
});
it('Provide only one dimension with min, should default to crop', function (done) {
sharp(fixtures.inputJpg)
.resize(320)
.min()
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(261, info.height);
done();
});
});
it('Do not enlarge when input width is already less than output width', function (done) {
sharp(fixtures.inputJpg)
.resize(2800)
.withoutEnlargement()
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(2725, info.width);
assert.strictEqual(2225, info.height);
done();
});
});
it('Do not enlarge when input height is already less than output height', function (done) {
sharp(fixtures.inputJpg)
.resize(null, 2300)
.withoutEnlargement()
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(2725, info.width);
assert.strictEqual(2225, info.height);
done();
});
});
it('Do enlarge when input width is less than output width', function (done) {
sharp(fixtures.inputJpg)
.resize(2800)
.withoutEnlargement(false)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(2800, info.width);
assert.strictEqual(2286, info.height);
done();
});
});
it('Downscale width and height, ignoring aspect ratio', function (done) {
sharp(fixtures.inputJpg)
.resize(320, 320)
.ignoreAspectRatio()
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(320, info.height);
done();
});
});
it('Downscale width, ignoring aspect ratio', function (done) {
sharp(fixtures.inputJpg)
.resize(320)
.ignoreAspectRatio()
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(2225, info.height);
done();
});
});
it('Downscale height, ignoring aspect ratio', function (done) {
sharp(fixtures.inputJpg)
.resize(null, 320)
.ignoreAspectRatio()
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(2725, info.width);
assert.strictEqual(320, info.height);
done();
});
});
it('Upscale width and height, ignoring aspect ratio', function (done) {
sharp(fixtures.inputJpg)
.resize(3000, 3000)
.ignoreAspectRatio()
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3000, info.width);
assert.strictEqual(3000, info.height);
done();
});
});
it('Upscale width, ignoring aspect ratio', function (done) {
sharp(fixtures.inputJpg)
.resize(3000)
.ignoreAspectRatio()
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3000, info.width);
assert.strictEqual(2225, info.height);
done();
});
});
it('Upscale height, ignoring aspect ratio', function (done) {
sharp(fixtures.inputJpg)
.resize(null, 3000)
.ignoreAspectRatio()
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(2725, info.width);
assert.strictEqual(3000, info.height);
done();
});
});
it('Downscale width, upscale height, ignoring aspect ratio', function (done) {
sharp(fixtures.inputJpg)
.resize(320, 3000)
.ignoreAspectRatio()
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(3000, info.height);
done();
});
});
it('Upscale width, downscale height, ignoring aspect ratio', function (done) {
sharp(fixtures.inputJpg)
.resize(3000, 320)
.ignoreAspectRatio()
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3000, info.width);
assert.strictEqual(320, info.height);
done();
});
});
it('Identity transform, ignoring aspect ratio', function (done) {
sharp(fixtures.inputJpg)
.ignoreAspectRatio()
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(2725, info.width);
assert.strictEqual(2225, info.height);
done();
});
});
});

64
test/unit/gif.js Normal file
View File

@@ -0,0 +1,64 @@
'use strict';
const fs = require('fs');
const assert = require('assert');
const sharp = require('../../');
const fixtures = require('../fixtures');
describe('GIF input', () => {
it('GIF Buffer to JPEG Buffer', () =>
sharp(fs.readFileSync(fixtures.inputGif))
.resize(8, 4)
.jpeg()
.toBuffer({ resolveWithObject: true })
.then(({ data, info }) => {
assert.strictEqual(true, data.length > 0);
assert.strictEqual(data.length, info.size);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(8, info.width);
assert.strictEqual(4, info.height);
})
);
it('2 channel GIF file to PNG Buffer', () =>
sharp(fixtures.inputGifGreyPlusAlpha)
.resize(8, 4)
.png()
.toBuffer({ resolveWithObject: true })
.then(({ data, info }) => {
assert.strictEqual(true, data.length > 0);
assert.strictEqual(data.length, info.size);
assert.strictEqual('png', info.format);
assert.strictEqual(8, info.width);
assert.strictEqual(4, info.height);
assert.strictEqual(4, info.channels);
})
);
it('Animated GIF first page to PNG', () =>
sharp(fixtures.inputGifAnimated)
.toBuffer({ resolveWithObject: true })
.then(({ data, info }) => {
assert.strictEqual(true, data.length > 0);
assert.strictEqual(data.length, info.size);
assert.strictEqual('png', info.format);
assert.strictEqual(80, info.width);
assert.strictEqual(80, info.height);
assert.strictEqual(4, info.channels);
})
);
it('Animated GIF all pages to PNG "toilet roll"', () =>
sharp(fixtures.inputGifAnimated, { pages: -1 })
.toBuffer({ resolveWithObject: true })
.then(({ data, info }) => {
assert.strictEqual(true, data.length > 0);
assert.strictEqual(data.length, info.size);
assert.strictEqual('png', info.format);
assert.strictEqual(80, info.width);
assert.strictEqual(2400, info.height);
assert.strictEqual(4, info.channels);
})
);
});

View File

@@ -517,37 +517,6 @@ describe('Input/output', function () {
}); });
}); });
it('Load GIF from Buffer', function (done) {
const inputGifBuffer = fs.readFileSync(fixtures.inputGif);
sharp(inputGifBuffer)
.resize(320, 240)
.jpeg()
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual(data.length, info.size);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
it('Load GIF grey+alpha from file, auto convert to PNG', function (done) {
sharp(fixtures.inputGifGreyPlusAlpha)
.resize(8, 4)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual(data.length, info.size);
assert.strictEqual('png', info.format);
assert.strictEqual(8, info.width);
assert.strictEqual(4, info.height);
assert.strictEqual(4, info.channels);
done();
});
});
it('Load Vips V file', function (done) { it('Load Vips V file', function (done) {
sharp(fixtures.inputV) sharp(fixtures.inputV)
.jpeg() .jpeg()

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

@@ -140,37 +140,35 @@ describe('Overlays', function () {
}); });
}); });
if (sharp.format.webp.input.file) { it('Composite WebP onto JPEG', function (done) {
it('Composite WebP onto JPEG', function (done) { const paths = getPaths('overlay-jpeg-with-webp', 'jpg');
const paths = getPaths('overlay-jpeg-with-webp', 'jpg');
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)
.resize(300, 300) .resize(300, 300)
.overlayWith(fixtures.inputWebPWithTransparency) .overlayWith(fixtures.inputWebPWithTransparency)
.toFile(paths.actual, function (error, info) { .toFile(paths.actual, function (error, info) {
if (error) return done(error); if (error) return done(error);
fixtures.assertMaxColourDistance(paths.actual, paths.expected, 102); fixtures.assertMaxColourDistance(paths.actual, paths.expected, 102);
done();
});
});
}
it('Composite JPEG onto PNG, no premultiply', function (done) {
sharp(fixtures.inputPngOverlayLayer1)
.overlayWith(fixtures.inputJpgWithLandscapeExif1)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(false, info.premultiplied);
done(); done();
}); });
}); });
it('Composite opaque JPEG onto JPEG, no premultiply', function (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) sharp(fixtures.inputJpg)
.overlayWith(fixtures.inputJpgWithLandscapeExif1) .overlayWith(fixtures.inputJpgWithLandscapeExif1)
.toBuffer(function (err, data, info) { .toBuffer(function (err, data, info) {
if (err) throw err; if (err) throw err;
assert.strictEqual(false, info.premultiplied); assert.strictEqual(true, info.premultiplied);
done(); done();
}); });
}); });
@@ -409,12 +407,6 @@ describe('Overlays', function () {
}); });
}); });
it('Overlay with invalid cutout option', function () {
assert.throws(function () {
sharp().overlayWith('ignore', { cutout: 1 });
});
});
it('Overlay with invalid tile option', function () { it('Overlay with invalid tile option', function () {
assert.throws(function () { assert.throws(function () {
sharp().overlayWith('ignore', { tile: 1 }); sharp().overlayWith('ignore', { tile: 1 });
@@ -580,18 +572,17 @@ describe('Overlays', function () {
}); });
}); });
it('Composite JPEG onto JPEG, no premultiply', function (done) { it('Composite JPEG onto JPEG', function (done) {
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)
.resize(480, 320) .resize(480, 320)
.overlayWith(fixtures.inputJpgBooleanTest) .overlayWith(fixtures.inputJpgBooleanTest)
.png()
.toBuffer(function (err, data, info) { .toBuffer(function (err, data, info) {
if (err) throw err; if (err) throw err;
assert.strictEqual('png', info.format); assert.strictEqual('jpeg', info.format);
assert.strictEqual(480, info.width); assert.strictEqual(480, info.width);
assert.strictEqual(320, info.height); assert.strictEqual(320, info.height);
assert.strictEqual(3, info.channels); assert.strictEqual(3, info.channels);
assert.strictEqual(false, info.premultiplied); assert.strictEqual(true, info.premultiplied);
fixtures.assertSimilar(fixtures.expected('overlay-jpeg-with-jpeg.jpg'), data, done); fixtures.assertSimilar(fixtures.expected('overlay-jpeg-with-jpeg.jpg'), data, done);
}); });
}); });

View File

@@ -16,16 +16,24 @@ describe('Platform-detection', function () {
delete process.env.npm_config_platform; delete process.env.npm_config_platform;
}); });
it('Can override ARM version via npm_config_armv', function () { it('Can override ARM version via --arm-version', function () {
process.env.npm_config_arch = 'arm'; process.env.npm_config_arch = 'arm';
process.env.npm_config_armv = 'test'; process.env.npm_config_arm_version = 'test';
assert.strictEqual('armvtest', platform().split('-')[1]); assert.strictEqual('armvtest', platform().split('-')[1]);
delete process.env.npm_config_armv; delete process.env.npm_config_arm_version;
delete process.env.npm_config_arch;
});
it('Can override ARM64 version via --arm-version', function () {
process.env.npm_config_arch = 'arm64';
process.env.npm_config_arm_version = 'test';
assert.strictEqual('arm64vtest', platform().split('-')[1]);
delete process.env.npm_config_arm_version;
delete process.env.npm_config_arch; delete process.env.npm_config_arch;
}); });
it('Can detect ARM version via process.config', function () { it('Can detect ARM version via process.config', function () {
process.env.npm_config_arch = 'armhf'; process.env.npm_config_arch = 'arm';
const armVersion = process.config.variables.arm_version; const armVersion = process.config.variables.arm_version;
process.config.variables.arm_version = 'test'; process.config.variables.arm_version = 'test';
assert.strictEqual('armvtest', platform().split('-')[1]); assert.strictEqual('armvtest', platform().split('-')[1]);
@@ -41,7 +49,7 @@ describe('Platform-detection', function () {
it('Defaults to ARMv8 for 64-bit', function () { it('Defaults to ARMv8 for 64-bit', function () {
process.env.npm_config_arch = 'arm64'; process.env.npm_config_arch = 'arm64';
assert.strictEqual('armv8', platform().split('-')[1]); assert.strictEqual('arm64v8', platform().split('-')[1]);
delete process.env.npm_config_arch; delete process.env.npm_config_arch;
}); });
}); });

View File

@@ -92,6 +92,31 @@ describe('Raw pixel data', function () {
}); });
}); });
}); });
it('JPEG to raw Stream and back again', function (done) {
const width = 32;
const height = 24;
const writable = sharp({
raw: {
width,
height,
channels: 3
}
});
writable
.jpeg()
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(32, info.width);
assert.strictEqual(24, info.height);
done();
});
sharp(fixtures.inputJpg)
.resize(width, height)
.raw()
.pipe(writable);
});
}); });
describe('Ouput raw, uncompressed image data', function () { describe('Ouput raw, uncompressed image data', function () {