Compare commits

...

58 Commits

Author SHA1 Message Date
Lovell Fuller
7555378e3b Release v0.28.0 2021-03-29 14:10:34 +01:00
Lovell Fuller
80c95ee66a Docs: libvips tarballs are a bit smaller now 2021-03-29 12:16:48 +01:00
Lovell Fuller
31563b210d Ensure GIF input will work with future libvips v8.11.0 2021-03-29 12:16:10 +01:00
Lovell Fuller
861cd93324 Pre-release v0.28.0-beta1 2021-03-27 07:11:34 +00:00
Lovell Fuller
abb344bb1a Upgrade to libvips v8.10.6 2021-03-26 21:57:12 +00:00
Lovell Fuller
6147491d9e Extend: default missing edge props to zero #2578 2021-03-25 16:34:02 +00:00
Lovell Fuller
f1f18fbb4a Docs: clarify that flatten removes alpha channel #2601 2021-03-25 14:38:55 +00:00
Lovell Fuller
9fc611f257 Docs: changelog entries for #2594 #2608 2021-03-22 20:30:46 +00:00
SHG42
34a2e14a14 Fix erroneous top/left clipping in composite #2571
Fixes bug where certain input values for top/left parameters
in composite can conflict with clipping logic, resulting in
inaccurate alignment in output.
2021-03-22 18:27:49 +00:00
Lovell Fuller
83fe65b9e9 Docs: include more relevant content in search index 2021-03-21 20:59:05 +00:00
Lovell Fuller
ec26c8aa49 Docs: ensure toBuffer pixel example works #2624 2021-03-21 20:54:09 +00:00
Lovell Fuller
da43a3055f Docs: correct typo in description of threshold operation 2021-03-21 20:51:30 +00:00
Lovell Fuller
a38126c82f Ensure composite replicates correct tiles with centre gravity #2626 2021-03-20 13:24:04 +00:00
Lovell Fuller
cb592ce588 Tests: add case for SVG with truncated embedded PNG 2021-03-18 19:34:56 +00:00
Lovell Fuller
d69c58a6da Docs: add section about Linux memory allocators 2021-03-18 19:34:07 +00:00
Lovell Fuller
bdb1986e08 Tests: run in parallel again 2021-03-17 23:25:34 +00:00
Lovell Fuller
55356c78a8 Docs: refresh markdown 2021-03-15 20:24:53 +00:00
Lovell Fuller
a0f55252b1 Tests: a few more speed improvements 2021-03-15 20:24:13 +00:00
Lovell Fuller
013f5cffa9 Tests: refactor modulate suite, ~20x faster 2021-03-15 18:20:06 +00:00
Lovell Fuller
d5d008f568 Docs: reorder readme sections 2021-03-15 13:07:16 +00:00
Lovell Fuller
3b02134cdc Tests: update latest benchmark test results 2021-03-14 21:10:26 +00:00
Lovell Fuller
a57d7b51b1 Tests: match concurrency with CPU count 2021-03-14 19:51:45 +00:00
Lovell Fuller
1a3c38d35f Pre-release v0.28.0-alpha1 2021-03-14 11:50:33 +00:00
Lovell Fuller
00aece0538 Ensure id attr can be set for IIIF tile output #2612 2021-03-14 11:19:53 +00:00
Lovell Fuller
5a9cc835b3 Reduce concurrency when using glibc-based Linux
to help prevent memory fragmentation
2021-03-14 11:19:53 +00:00
Lovell Fuller
58526cc849 Upgrade to libvips v8.10.6-alpha3 2021-03-14 11:19:53 +00:00
Lovell Fuller
955b5f43a5 Tests: small speed up to a couple of tile-related tests 2021-03-14 11:19:53 +00:00
Lovell Fuller
447aec3fde Tests: update leak suppressions 2021-03-14 11:19:53 +00:00
Lovell Fuller
473260a836 Docs: update with install-time improvements 2021-03-14 11:19:53 +00:00
Lovell Fuller
4d2784c10c Prebuilt libvips v8.10.6 binaries work with musl 1.1.x and 1.2.x 2021-03-14 11:19:53 +00:00
Lovell Fuller
d9af897595 Tests: ensure AVIF order is read, write, read+write 2021-03-14 11:19:53 +00:00
Lovell Fuller
23a48be315 Upgrade to libvips v8.10.6-alpha2
- Prebuilt Linux libvips binaries now use 'new' C++11 ABI
2021-03-14 11:19:53 +00:00
Lovell Fuller
ce8f48e5d1 CI: Add linuxmusl-arm64v8 environment 2021-03-14 11:19:53 +00:00
Kleis Auke Wolthuizen
6aaf839662 Use a single shared library 2021-03-14 11:19:53 +00:00
Lovell Fuller
984a9e653e Upgrade to libvips 8.10.6-alpha1
- Prebuilt binaries now include mozjpeg and libimagequant (BSD 2-Clause)
- Prebuilt binaries limit AVIF support to the most common 8-bit depth
- Add `mozjpeg` option to `jpeg` method, sets mozjpeg defaults
- Reduce the default PNG `compressionLevel` to the more commonly used 6
2021-03-14 11:19:53 +00:00
Lovell Fuller
8dffa28b4d Remove npmlog as a direct dependency
It remains a transitive dependency via prebuild-install
2021-03-14 11:19:53 +00:00
Lovell Fuller
b05a4bdadd Use same version of simple-get as prebuild-install
to prevent two different versions being installed
2021-03-14 11:19:53 +00:00
Lovell Fuller
36087fe518 Remove array-flatten dependency 2021-03-14 11:19:53 +00:00
Lovell Fuller
5eed87ec4d Install: skip header files when using prebuilds 2021-03-14 11:19:53 +00:00
Tobias Nießen
af66a73225 Tests: fix unit test description (#2619) 2021-03-13 16:18:40 +00:00
Alex Bradley
dcf913c17e Install: fail on incomplete download and clean up tempfile (#2608)
- Fail when the connection closes before the response is complete
- Create tempfile only when needed, and clean it up on failure
2021-03-05 15:21:34 +00:00
Lovell Fuller
68ccba8f74 Docs: refresh search index 2021-02-22 21:17:31 +00:00
Lovell Fuller
956f7e29db Release v0.27.2 2021-02-22 20:30:42 +00:00
Lovell Fuller
4264c0577e Improve experience for those using Apple M1 devices #2460
- For Rosetta x64, prevent use of global ARM64 libvips
- For ARM64, improve error message when global libvips not found
2021-02-22 13:49:31 +00:00
Lovell Fuller
cc37b59309 Switch to libvips' recently-exposed has_alpha #2569 2021-02-22 12:32:20 +00:00
Lovell Fuller
9f2f92095d Skip prebuilt binaries for musl >=1.2.0 #2570 2021-02-20 19:40:40 +00:00
Lovell Fuller
0c1075c089 Docs: local compilation requires --build-from-source flag 2021-02-20 15:43:48 +00:00
allx
9c64710c8b Allow code bundling of utility functions (#2586) 2021-02-20 15:39:25 +00:00
Lovell Fuller
f6f16b91db Allow use of recomb op with 1/2 channel input #2584 2021-02-19 16:37:29 +00:00
Lovell Fuller
1986b5cfe6 Bump deps 2021-02-19 15:49:21 +00:00
Lovell Fuller
6445b72d41 Docs: Changelog entry and credit for #2581 2021-02-19 15:48:59 +00:00
Florian Busch
df7b8ba738 Add support for non lower case extensions with toFormat 2021-02-17 20:46:13 +00:00
Pedro Poveda
202083999e Docs: add closing parenthesis so code example runs 2021-02-14 15:57:06 +00:00
aprat84
315f519e1d Docs: correct type for AVIF speed output option (#2568) 2021-02-08 20:37:56 +00:00
Lovell Fuller
d7d580ae6f Tests: using parallel fails on latest Node.js 15.8.0 2021-02-08 13:12:35 +00:00
Lovell Fuller
7017af303d Improve error message when attempting toFile/GIF without magick 2021-02-08 11:46:13 +00:00
Lovell Fuller
0dc325daa4 Docs: add section about Webpack configuration 2021-01-29 11:29:24 +00:00
Lovell Fuller
6dffb47973 Docs: small search index improvements 2021-01-29 11:28:48 +00:00
79 changed files with 965 additions and 420 deletions

View File

@@ -27,7 +27,7 @@ Please select the `master` branch as the destination for your Pull Request so yo
Please squash your changes into a single commit using a command like `git rebase -i upstream/master`. Please squash your changes into a single commit using a command like `git rebase -i upstream/master`.
To test C++ changes, you can compile the module using `npm install` and then run the tests using `npm test`. To test C++ changes, you can compile the module using `npm install --build-from-source` and then run the tests using `npm test`.
## Submit a Pull Request with a new feature ## Submit a Pull Request with a new feature

View File

@@ -30,6 +30,8 @@ jobs:
container: node:12-alpine3.11 container: node:12-alpine3.11
- os: ubuntu-20.04 - os: ubuntu-20.04
container: node:14-alpine3.11 container: node:14-alpine3.11
- os: ubuntu-20.04
container: node:14-alpine3.13
- os: ubuntu-20.04 - os: ubuntu-20.04
container: node:15-alpine3.11 container: node:15-alpine3.11
- os: macos-10.15 - os: macos-10.15

View File

@@ -58,5 +58,51 @@ jobs:
install: sudo docker exec sharp sh -c "npm install --build-from-source --unsafe-perm" install: sudo docker exec sharp sh -c "npm install --build-from-source --unsafe-perm"
script: sudo docker exec sharp sh -c "npm test" script: sudo docker exec sharp sh -c "npm test"
- name: "Linux ARM64v8 (Alpine 3.11, musl 1.1.24) - Node.js 10"
arch: arm64
os: linux
dist: focal
language: shell
before_install:
- sudo docker run -dit --name sharp --volume "${PWD}:/mnt/sharp" --workdir /mnt/sharp node:10-alpine3.11
- sudo docker exec sharp sh -c "apk add build-base git python3 --update-cache"
install: sudo docker exec sharp sh -c "npm install --build-from-source --unsafe-perm"
script: sudo docker exec sharp sh -c "npm test"
after_success: "[[ -n $TRAVIS_TAG ]] && sudo docker exec --env prebuild_upload sharp sh -c \"npx prebuild --runtime napi --target 3\""
- name: "Linux ARM64v8 (Alpine 3.11, musl 1.1.24) - Node.js 12"
arch: arm64
os: linux
dist: focal
language: shell
before_install:
- sudo docker run -dit --name sharp --volume "${PWD}:/mnt/sharp" --workdir /mnt/sharp node:12-alpine3.11
- sudo docker exec sharp sh -c "apk add build-base git python3 --update-cache"
install: sudo docker exec sharp sh -c "npm install --build-from-source --unsafe-perm"
script: sudo docker exec sharp sh -c "npm test"
- name: "Linux ARM64v8 (Alpine 3.11, musl 1.1.24) - Node.js 14"
arch: arm64
os: linux
dist: focal
language: shell
before_install:
- sudo docker run -dit --name sharp --volume "${PWD}:/mnt/sharp" --workdir /mnt/sharp node:14-alpine3.11
- sudo docker exec sharp sh -c "apk add build-base git python3 --update-cache"
install: sudo docker exec sharp sh -c "npm install --build-from-source --unsafe-perm"
script: sudo docker exec sharp sh -c "npm test"
- name: "Linux ARM64v8 (Alpine 3.11, musl 1.1.24) - Node.js 15"
arch: arm64
os: linux
dist: focal
language: shell
before_install:
- sudo chown 0.0 ${PWD}
- sudo docker run -dit --name sharp --volume "${PWD}:/mnt/sharp" --workdir /mnt/sharp node:15-alpine3.11
- sudo docker exec sharp sh -c "apk add build-base git python3 --update-cache"
install: sudo docker exec sharp sh -c "npm install --build-from-source --unsafe-perm"
script: sudo docker exec sharp sh -c "npm test"
cache: cache:
npm: false npm: false

View File

@@ -19,6 +19,14 @@ rotation, extraction, compositing and gamma correction are available.
Most modern macOS, Windows and Linux systems running Node.js v10+ Most modern macOS, Windows and Linux systems running Node.js v10+
do not require any additional install or runtime dependencies. do not require any additional install or runtime dependencies.
## Documentation
Visit [sharp.pixelplumbing.com](https://sharp.pixelplumbing.com/) for complete
[installation instructions](https://sharp.pixelplumbing.com/install),
[API documentation](https://sharp.pixelplumbing.com/api-constructor),
[benchmark tests](https://sharp.pixelplumbing.com/performance) and
[changelog](https://sharp.pixelplumbing.com/changelog).
## Examples ## Examples
```sh ```sh
@@ -43,6 +51,7 @@ sharp(inputBuffer)
sharp('input.jpg') sharp('input.jpg')
.rotate() .rotate()
.resize(200) .resize(200)
.jpeg({ mozjpeg: true })
.toBuffer() .toBuffer()
.then( data => { ... }) .then( data => { ... })
.catch( err => { ... }); .catch( err => { ... });
@@ -84,23 +93,15 @@ readableStream
.pipe(writableStream); .pipe(writableStream);
``` ```
[![Test Coverage](https://coveralls.io/repos/lovell/sharp/badge.svg?branch=master)](https://coveralls.io/r/lovell/sharp?branch=master) ## Contributing
[![N-API v3](https://img.shields.io/badge/N--API-v3-green.svg)](https://nodejs.org/dist/latest/docs/api/n-api.html#n_api_n_api_version_matrix)
### Documentation
Visit [sharp.pixelplumbing.com](https://sharp.pixelplumbing.com/) for complete
[installation instructions](https://sharp.pixelplumbing.com/install),
[API documentation](https://sharp.pixelplumbing.com/api-constructor),
[benchmark tests](https://sharp.pixelplumbing.com/performance) and
[changelog](https://sharp.pixelplumbing.com/changelog).
### Contributing
A [guide for contributors](https://github.com/lovell/sharp/blob/master/.github/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 [![Test Coverage](https://coveralls.io/repos/lovell/sharp/badge.svg?branch=master)](https://coveralls.io/r/lovell/sharp?branch=master)
[![N-API v3](https://img.shields.io/badge/N--API-v3-green.svg)](https://nodejs.org/dist/latest/docs/api/n-api.html#n_api_n_api_version_matrix)
## Licensing
Copyright 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021 Lovell Fuller and contributors. Copyright 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021 Lovell Fuller and contributors.

View File

@@ -140,8 +140,7 @@
'link_settings': { 'link_settings': {
'library_dirs': ['<(sharp_vendor_dir)/lib'], 'library_dirs': ['<(sharp_vendor_dir)/lib'],
'libraries': [ 'libraries': [
'libvips-cpp.42.dylib', 'libvips-cpp.42.dylib'
'libvips.42.dylib'
] ]
}, },
'xcode_settings': { 'xcode_settings': {
@@ -153,13 +152,12 @@
}], }],
['OS == "linux"', { ['OS == "linux"', {
'defines': [ 'defines': [
'_GLIBCXX_USE_CXX11_ABI=0' '_GLIBCXX_USE_CXX11_ABI=1'
], ],
'link_settings': { 'link_settings': {
'library_dirs': ['<(sharp_vendor_dir)/lib'], 'library_dirs': ['<(sharp_vendor_dir)/lib'],
'libraries': [ 'libraries': [
'-l:libvips-cpp.so.42', '-l:libvips-cpp.so.42'
'-l:libvips.so.42'
], ],
'ldflags': [ 'ldflags': [
# Ensure runtime linking is relative to sharp.node # Ensure runtime linking is relative to sharp.node

View File

@@ -50,6 +50,10 @@ no child processes are spawned and Promises/async/await are supported.
### Optimal ### Optimal
The features of `mozjpeg` and `pngquant` can be used
to optimise the file size of JPEG and PNG images respectively,
without having to invoke separate `imagemin` processes.
Huffman tables are optimised when generating JPEG output images Huffman tables are optimised when generating JPEG output images
without having to use separate command line tools like without having to use separate command line tools like
[jpegoptim](https://github.com/tjko/jpegoptim) and [jpegoptim](https://github.com/tjko/jpegoptim) and

View File

@@ -116,7 +116,7 @@ await sharp({
sigma: 30 sigma: 30
} }
} }
}.toFile('noise.png'); }).toFile('noise.png');
``` ```
- Throws **[Error][10]** Invalid parameters - Throws **[Error][10]** Invalid parameters

View File

@@ -169,13 +169,21 @@ Returns **Sharp**
## flatten ## flatten
Merge alpha transparency channel, if any, with a background. Merge alpha transparency channel, if any, with a background, then remove the alpha channel.
### Parameters ### Parameters
- `options` **[Object][2]?** - `options` **[Object][2]?**
- `options.background` **([string][3] \| [Object][2])** background colour, parsed by the [color][4] module, defaults to black. (optional, default `{r:0,g:0,b:0}`) - `options.background` **([string][3] \| [Object][2])** background colour, parsed by the [color][4] module, defaults to black. (optional, default `{r:0,g:0,b:0}`)
### Examples
```javascript
await sharp(rgbaInput)
.flatten('#F0A703')
.toBuffer();
```
Returns **Sharp** Returns **Sharp**
## gamma ## gamma
@@ -263,7 +271,7 @@ Returns **Sharp**
## threshold ## threshold
Any pixel value greather than or equal to the threshold value will be set to 255, otherwise it will be set to 0. Any pixel value greater than or equal to the threshold value will be set to 255, otherwise it will be set to 0.
### Parameters ### Parameters

View File

@@ -87,10 +87,10 @@ sharp(input)
``` ```
```javascript ```javascript
const data = await sharp('my-image.jpg') const { data, info } = await sharp('my-image.jpg')
// output the raw pixels // output the raw pixels
.raw() .raw()
.toBuffer(); .toBuffer({ resolveWithObject: true });
// create a more type safe way to work with the raw pixel data // create a more type safe way to work with the raw pixel data
// this will not copy the data, instead it will change `data`s underlying ArrayBuffer // this will not copy the data, instead it will change `data`s underlying ArrayBuffer
@@ -98,7 +98,9 @@ const data = await sharp('my-image.jpg')
const pixelArray = new Uint8ClampedArray(data.buffer); const pixelArray = new Uint8ClampedArray(data.buffer);
// When you are done changing the pixelArray, sharp takes the `pixelArray` as an input // When you are done changing the pixelArray, sharp takes the `pixelArray` as an input
await sharp(pixelArray).toFile('my-changed-image.jpg'); const { width, height, channels } = info;
await sharp(pixelArray, { raw: { width, height, channels } })
.toFile('my-changed-image.jpg');
``` ```
Returns **[Promise][5]&lt;[Buffer][8]>** when no callback is provided Returns **[Promise][5]&lt;[Buffer][8]>** when no callback is provided
@@ -157,8 +159,6 @@ Returns **Sharp**
Use these JPEG options for output image. Use these JPEG options for output image.
Some of these options require the use of a globally-installed libvips compiled with support for mozjpeg.
### Parameters ### Parameters
- `options` **[Object][6]?** output options - `options` **[Object][6]?** output options
@@ -167,12 +167,13 @@ Some of these options require the use of a globally-installed libvips compiled w
- `options.chromaSubsampling` **[string][2]** set to '4:4:4' to prevent chroma subsampling otherwise defaults to '4:2:0' chroma subsampling (optional, default `'4:2:0'`) - `options.chromaSubsampling` **[string][2]** set to '4:4:4' to prevent chroma subsampling otherwise defaults to '4:2:0' chroma subsampling (optional, default `'4:2:0'`)
- `options.optimiseCoding` **[boolean][7]** optimise Huffman coding tables (optional, default `true`) - `options.optimiseCoding` **[boolean][7]** optimise Huffman coding tables (optional, default `true`)
- `options.optimizeCoding` **[boolean][7]** alternative spelling of optimiseCoding (optional, default `true`) - `options.optimizeCoding` **[boolean][7]** alternative spelling of optimiseCoding (optional, default `true`)
- `options.trellisQuantisation` **[boolean][7]** apply trellis quantisation, requires libvips compiled with support for mozjpeg (optional, default `false`) - `options.mozjpeg` **[boolean][7]** use mozjpeg defaults, equivalent to `{ trellisQuantisation: true, overshootDeringing: true, optimiseScans: true, quantisationTable: 3 }` (optional, default `false`)
- `options.overshootDeringing` **[boolean][7]** apply overshoot deringing, requires libvips compiled with support for mozjpeg (optional, default `false`) - `options.trellisQuantisation` **[boolean][7]** apply trellis quantisation (optional, default `false`)
- `options.optimiseScans` **[boolean][7]** optimise progressive scans, forces progressive, requires libvips compiled with support for mozjpeg (optional, default `false`) - `options.overshootDeringing` **[boolean][7]** apply overshoot deringing (optional, default `false`)
- `options.optimizeScans` **[boolean][7]** alternative spelling of optimiseScans, requires libvips compiled with support for mozjpeg (optional, default `false`) - `options.optimiseScans` **[boolean][7]** optimise progressive scans, forces progressive (optional, default `false`)
- `options.quantisationTable` **[number][9]** quantization table to use, integer 0-8, requires libvips compiled with support for mozjpeg (optional, default `0`) - `options.optimizeScans` **[boolean][7]** alternative spelling of optimiseScans (optional, default `false`)
- `options.quantizationTable` **[number][9]** alternative spelling of quantisationTable, requires libvips compiled with support for mozjpeg (optional, default `0`) - `options.quantisationTable` **[number][9]** quantization table to use, integer 0-8 (optional, default `0`)
- `options.quantizationTable` **[number][9]** alternative spelling of quantisationTable (optional, default `0`)
- `options.force` **[boolean][7]** force JPEG output, otherwise attempt to use input format (optional, default `true`) - `options.force` **[boolean][7]** force JPEG output, otherwise attempt to use input format (optional, default `true`)
### Examples ### Examples
@@ -187,6 +188,13 @@ const data = await sharp(input)
.toBuffer(); .toBuffer();
``` ```
```javascript
// Use mozjpeg to reduce output JPEG file size (slower)
const data = await sharp(input)
.jpeg({ mozjpeg: true })
.toBuffer();
```
- Throws **[Error][4]** Invalid options - Throws **[Error][4]** Invalid options
Returns **Sharp** Returns **Sharp**
@@ -195,33 +203,39 @@ Returns **Sharp**
Use these PNG options for output image. Use these PNG options for output image.
PNG output is always full colour at 8 or 16 bits per pixel. By default, PNG output is full colour at 8 or 16 bits per pixel.
Indexed PNG input at 1, 2 or 4 bits per pixel is converted to 8 bits per pixel. Indexed PNG input at 1, 2 or 4 bits per pixel is converted to 8 bits per pixel.
Set `palette` to `true` for slower, indexed PNG output.
Some of these options require the use of a globally-installed libvips compiled with support for libimagequant (GPL).
### Parameters ### Parameters
- `options` **[Object][6]?** - `options` **[Object][6]?**
- `options.progressive` **[boolean][7]** use progressive (interlace) scan (optional, default `false`) - `options.progressive` **[boolean][7]** use progressive (interlace) scan (optional, default `false`)
- `options.compressionLevel` **[number][9]** zlib compression level, 0-9 (optional, default `9`) - `options.compressionLevel` **[number][9]** zlib compression level, 0 (fastest, largest) to 9 (slowest, smallest) (optional, default `6`)
- `options.adaptiveFiltering` **[boolean][7]** use adaptive row filtering (optional, default `false`) - `options.adaptiveFiltering` **[boolean][7]** use adaptive row filtering (optional, default `false`)
- `options.palette` **[boolean][7]** quantise to a palette-based image with alpha transparency support, requires libvips compiled with support for libimagequant (optional, default `false`) - `options.palette` **[boolean][7]** quantise to a palette-based image with alpha transparency support (optional, default `false`)
- `options.quality` **[number][9]** use the lowest number of colours needed to achieve given quality, sets `palette` to `true`, requires libvips compiled with support for libimagequant (optional, default `100`) - `options.quality` **[number][9]** use the lowest number of colours needed to achieve given quality, sets `palette` to `true` (optional, default `100`)
- `options.colours` **[number][9]** maximum number of palette entries, sets `palette` to `true`, requires libvips compiled with support for libimagequant (optional, default `256`) - `options.colours` **[number][9]** maximum number of palette entries, sets `palette` to `true` (optional, default `256`)
- `options.colors` **[number][9]** alternative spelling of `options.colours`, sets `palette` to `true`, requires libvips compiled with support for libimagequant (optional, default `256`) - `options.colors` **[number][9]** alternative spelling of `options.colours`, sets `palette` to `true` (optional, default `256`)
- `options.dither` **[number][9]** level of Floyd-Steinberg error diffusion, sets `palette` to `true`, requires libvips compiled with support for libimagequant (optional, default `1.0`) - `options.dither` **[number][9]** level of Floyd-Steinberg error diffusion, sets `palette` to `true` (optional, default `1.0`)
- `options.force` **[boolean][7]** force PNG output, otherwise attempt to use input format (optional, default `true`) - `options.force` **[boolean][7]** force PNG output, otherwise attempt to use input format (optional, default `true`)
### Examples ### Examples
```javascript ```javascript
// Convert any input to PNG output // Convert any input to full colour PNG output
const data = await sharp(input) const data = await sharp(input)
.png() .png()
.toBuffer(); .toBuffer();
``` ```
```javascript
// Convert any input to indexed PNG output (slower)
const data = await sharp(input)
.png({ palette: true })
.toBuffer();
```
- Throws **[Error][4]** Invalid options - Throws **[Error][4]** Invalid options
Returns **Sharp** Returns **Sharp**
@@ -326,7 +340,7 @@ most web browsers do not display these properly.
- `options` **[Object][6]?** output options - `options` **[Object][6]?** output options
- `options.quality` **[number][9]** quality, integer 1-100 (optional, default `50`) - `options.quality` **[number][9]** quality, integer 1-100 (optional, default `50`)
- `options.lossless` **[boolean][7]** use lossless compression (optional, default `false`) - `options.lossless` **[boolean][7]** use lossless compression (optional, default `false`)
- `options.speed` **[boolean][7]** CPU effort vs file size, 0 (slowest/smallest) to 8 (fastest/largest) (optional, default `5`) - `options.speed` **[number][9]** CPU effort vs file size, 0 (slowest/smallest) to 8 (fastest/largest) (optional, default `5`)
- `options.chromaSubsampling` **[string][2]** set to '4:4:4' to prevent chroma subsampling otherwise defaults to '4:2:0' chroma subsampling, requires libvips v8.11.0 (optional, default `'4:2:0'`) - `options.chromaSubsampling` **[string][2]** set to '4:4:4' to prevent chroma subsampling otherwise defaults to '4:2:0' chroma subsampling, requires libvips v8.11.0 (optional, default `'4:2:0'`)
@@ -411,6 +425,7 @@ Warning: multiple sharp instances concurrently producing tile output can expose
- `options.layout` **[string][2]** filesystem layout, possible values are `dz`, `iiif`, `zoomify` or `google`. (optional, default `'dz'`) - `options.layout` **[string][2]** filesystem layout, possible values are `dz`, `iiif`, `zoomify` or `google`. (optional, default `'dz'`)
- `options.centre` **[boolean][7]** centre image in tile. (optional, default `false`) - `options.centre` **[boolean][7]** centre image in tile. (optional, default `false`)
- `options.center` **[boolean][7]** alternative spelling of centre. (optional, default `false`) - `options.center` **[boolean][7]** alternative spelling of centre. (optional, default `false`)
- `options.id` **[string][2]** when `layout` is `iiif`, sets the `@id` attribute of `info.json` (optional, default `'https://example.com/iiif'`)
### Examples ### Examples

View File

@@ -137,10 +137,10 @@ This operation will always occur after resizing and extraction, if any.
### Parameters ### Parameters
- `extend` **([number][8] \| [Object][9])** single pixel count to add to all edges or an Object with per-edge counts - `extend` **([number][8] \| [Object][9])** single pixel count to add to all edges or an Object with per-edge counts
- `extend.top` **[number][8]?** - `extend.top` **[number][8]** (optional, default `0`)
- `extend.left` **[number][8]?** - `extend.left` **[number][8]** (optional, default `0`)
- `extend.bottom` **[number][8]?** - `extend.bottom` **[number][8]** (optional, default `0`)
- `extend.right` **[number][8]?** - `extend.right` **[number][8]** (optional, default `0`)
- `extend.background` **([String][10] \| [Object][9])** background colour, parsed by the [color][11] module, defaults to black without transparency. (optional, default `{r:0,g:0,b:0,alpha:1}`) - `extend.background` **([String][10] \| [Object][9])** background colour, parsed by the [color][11] module, defaults to black without transparency. (optional, default `{r:0,g:0,b:0,alpha:1}`)
### Examples ### Examples
@@ -160,6 +160,16 @@ sharp(input)
... ...
``` ```
```javascript
// Add a row of 10 red pixels to the bottom
sharp(input)
.extend({
bottom: 10,
background: 'red'
})
...
```
- Throws **[Error][13]** Invalid parameters - Throws **[Error][13]** Invalid parameters
Returns **Sharp** Returns **Sharp**

View File

@@ -84,8 +84,12 @@ Returns **[Object][1]**
Gets or, when a concurrency is provided, sets Gets or, when a concurrency is provided, sets
the number of threads _libvips'_ should create to process each image. the number of threads _libvips'_ should create to process each image.
The default value is the number of CPU cores.
A value of `0` will reset to this default. The default value is the number of CPU cores,
except when using glibc-based Linux without jemalloc,
where the default is `1` to help reduce memory fragmentation.
A value of `0` will reset this to the number of CPU cores.
The maximum number of images that can be processed in parallel The maximum number of images that can be processed in parallel
is limited by libuv's `UV_THREADPOOL_SIZE` environment variable. is limited by libuv's `UV_THREADPOOL_SIZE` environment variable.

View File

@@ -1,9 +1,61 @@
# Changelog # Changelog
## v0.28 - *bijou*
Requires libvips v8.10.6
### v0.28.0 - 29th March 2021
* Prebuilt binaries now include mozjpeg and libimagequant (BSD 2-Clause).
* Prebuilt binaries limit AVIF support to the most common 8-bit depth.
* Add `mozjpeg` option to `jpeg` method, sets mozjpeg defaults.
* Reduce the default PNG `compressionLevel` to the more commonly used 6.
* Reduce concurrency on glibc-based Linux when using the default memory allocator to help prevent fragmentation.
* Default missing edge properties of extend operation to zero.
[#2578](https://github.com/lovell/sharp/issues/2578)
* Ensure composite does not clip top and left offsets.
[#2594](https://github.com/lovell/sharp/pull/2594)
[@SHG42](https://github.com/SHG42)
* Improve error handling of network failure at install time.
[#2608](https://github.com/lovell/sharp/pull/2608)
[@abradley](https://github.com/abradley)
* Ensure `@id` attribute can be set for IIIF tile-based output.
[#2612](https://github.com/lovell/sharp/issues/2612)
[@edsilv](https://github.com/edsilv)
* Ensure composite replicates the correct number of tiles for centred gravities.
[#2626](https://github.com/lovell/sharp/issues/2626)
## v0.27 - *avif* ## v0.27 - *avif*
Requires libvips v8.10.5 Requires libvips v8.10.5
### v0.27.2 - 22nd February 2021
* macOS: Prevent use of globally-installed ARM64 libvips with Rosetta x64 emulation.
[#2460](https://github.com/lovell/sharp/issues/2460)
* Linux (musl): Prevent use of prebuilt linuxmusl-x64 binaries with musl >= 1.2.0.
[#2570](https://github.com/lovell/sharp/issues/2570)
* Improve 16-bit grey+alpha support by using libvips' `has_alpha` detection.
[#2569](https://github.com/lovell/sharp/issues/2569)
* Allow the use of non lower case extensions with `toFormat`.
[#2581](https://github.com/lovell/sharp/pull/2581)
[@florian-busch](https://github.com/florian-busch)
* Allow use of `recomb` operation with single channel input.
[#2584](https://github.com/lovell/sharp/issues/2584)
### v0.27.1 - 27th January 2021 ### v0.27.1 - 27th January 2021
* Ensure TIFF is cast when using float predictor. * Ensure TIFF is cast when using float predictor.

View File

@@ -206,3 +206,6 @@ GitHub: https://github.com/stefanprobst
Name: Thomas Beiganz Name: Thomas Beiganz
GitHub: https://github.com/beig GitHub: https://github.com/beig
Name: Florian Busch
GitHub: https://github.com/florian-busch

View File

@@ -19,7 +19,7 @@
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="https://pixel.plumbing/px/72x72/sharp-logo.svg"> <link rel="apple-touch-icon-precomposed" sizes="72x72" href="https://pixel.plumbing/px/72x72/sharp-logo.svg">
<link rel="apple-touch-icon-precomposed" href="https://pixel.plumbing/px/57x57/sharp-logo.svg"> <link rel="apple-touch-icon-precomposed" href="https://pixel.plumbing/px/57x57/sharp-logo.svg">
<link rel="author" href="/humans.txt" type="text/plain"> <link rel="author" href="/humans.txt" type="text/plain">
<link rel="preload" href="https://cdn.jsdelivr.net/gh/lovell/sharp@v0.27.1/docs/README.md" as="fetch" type="text/markdown" crossorigin> <link rel="preload" href="https://cdn.jsdelivr.net/gh/lovell/sharp@v0.28.0/docs/README.md" as="fetch" type="text/markdown" crossorigin>
<link rel="preload" href="https://cdn.jsdelivr.net/gh/lovell/sharp@master/docs/image/sharp-logo.svg" as="image" type="image/svg+xml" crossorigin> <link rel="preload" href="https://cdn.jsdelivr.net/gh/lovell/sharp@master/docs/image/sharp-logo.svg" as="image" type="image/svg+xml" crossorigin>
<link rel="dns-prefetch" href="https://pixel.plumbing"> <link rel="dns-prefetch" href="https://pixel.plumbing">
<link rel="dns-prefetch" href="https://www.google-analytics.com"> <link rel="dns-prefetch" href="https://www.google-analytics.com">
@@ -139,7 +139,7 @@
docuteApiTitlePlugin, docuteApiTitlePlugin,
docuteApiSearchPlugin docuteApiSearchPlugin
], ],
sourcePath: 'https://cdn.jsdelivr.net/gh/lovell/sharp@v0.27.1/docs', sourcePath: 'https://cdn.jsdelivr.net/gh/lovell/sharp@v0.28.0/docs',
nav: [ nav: [
{ {
title: 'Funding', title: 'Funding',

View File

@@ -19,11 +19,11 @@ Node.js v10+ on the most common platforms:
* macOS x64 (>= 10.13) * macOS x64 (>= 10.13)
* Linux x64 (glibc >= 2.17, musl >= 1.1.24) * Linux x64 (glibc >= 2.17, musl >= 1.1.24)
* Linux ARM64 (glibc >= 2.29) * Linux ARM64 (glibc >= 2.29, musl >= 1.1.24)
* Windows x64 * Windows x64
* Windows x86 * Windows x86
A ~9MB tarball containing libvips and its most commonly used dependencies An ~7.5MB tarball containing libvips and its most commonly used dependencies
is downloaded via HTTPS and stored within `node_modules/sharp/vendor` during `npm install`. is downloaded via HTTPS and stored within `node_modules/sharp/vendor` during `npm install`.
This provides support for the This provides support for the
@@ -31,16 +31,16 @@ JPEG, PNG, WebP, AVIF, TIFF, GIF (input) and SVG (input) image formats.
The following platforms have prebuilt libvips but not sharp: The following platforms have prebuilt libvips but not sharp:
* macOS ARM64
* Linux ARMv6 * Linux ARMv6
* Linux ARMv7 (glibc >= 2.28) * Linux ARMv7 (glibc >= 2.28)
* Windows ARM64 * Windows ARM64
The following platforms require compilation of both libvips and sharp from source: The following platforms require compilation of both libvips and sharp from source:
* macOS ARM64
* Linux x86 * Linux x86
* Linux x64 (glibc <= 2.16, includes RHEL/CentOS 6) * Linux x64 (glibc <= 2.16, includes RHEL/CentOS 6)
* Linux ARM64 (glibc <= 2.28, musl) * Linux ARM64 (glibc <= 2.28)
* Linux PowerPC * Linux PowerPC
* FreeBSD * FreeBSD
* OpenBSD * OpenBSD
@@ -60,15 +60,15 @@ Check the output of running `npm install --verbose sharp` for useful error messa
## Apple M1 ## Apple M1
libvips must currently be installed via Homebrew before installing sharp. Prebuilt libvips binaries are provided for macOS on ARM64 (since sharp v0.28.0).
```sh During `npm install` sharp will be built locally,
brew install vips which requires Xcode and Python - see
``` [building from source](#building-from-source).
When this new ARM64 CPU is made freely available When this new ARM64 CPU is made freely available
to open source projects via a CI service to open source projects via a CI service
then prebuilt binaries can be provided. then prebuilt sharp binaries can also be provided.
## Custom libvips ## Custom libvips
@@ -149,6 +149,23 @@ pkg install -y pkgconf vips
cd /usr/ports/graphics/vips/ && make install clean cd /usr/ports/graphics/vips/ && make install clean
``` ```
## Linux memory allocator
The default memory allocator on most glibc-based Linux systems
(e.g. Debian, Red Hat) is unsuitable for long-running, multi-threaded
processes that involve lots of small memory allocations.
For this reason, by default, sharp will limit the use of thread-based
[concurrency](api-utility#concurrency) when the glibc allocator is
detected at runtime.
To help avoid fragmentation and improve performance on these systems,
the use of an alternative memory allocator such as
[jemalloc](https://github.com/jemalloc/jemalloc) is recommended.
Those using musl-based Linux (e.g. Alpine) and non-Linux systems are
unaffected.
## Heroku ## Heroku
Add the Add the
@@ -192,6 +209,18 @@ docker run -v "$PWD":/var/task lambci/lambda:build-nodejs12.x npm install sharp
To get the best performance select the largest memory available. To get the best performance select the largest memory available.
A 1536 MB function provides ~12x more CPU time than a 128 MB function. A 1536 MB function provides ~12x more CPU time than a 128 MB function.
## Webpack
Ensure sharp is added to the
[externals](https://webpack.js.org/configuration/externals/)
configuration.
```js
externals: {
'sharp': 'commonjs sharp'
}
```
## Worker threads ## Worker threads
The main thread must call `require('sharp')` The main thread must call `require('sharp')`
@@ -201,22 +230,6 @@ until after all threads are complete.
## Known conflicts ## Known conflicts
### Electron and Linux
The prebuilt binaries provided by Electron for Linux depend on many shared system libraries.
One of these, `libgobject-2.0.so`,
is known to conflict with the statically-linked binaries provided by sharp
and the following error can occur:
```
basic_string::_S_construct null not valid
```
To workaround this, set the `LD_PRELOAD` environment variable before the `electron` binary is run.
```sh
LD_PRELOAD=node_modules/sharp/vendor/8.10.5/lib/libvips.so.42 electron script.js
```
### Canvas and Windows ### Canvas and Windows
The prebuilt binaries provided by `canvas` for Windows depend on the unmaintained GTK 2, last updated in 2011. The prebuilt binaries provided by `canvas` for Windows depend on the unmaintained GTK 2, last updated in 2011.

View File

@@ -5,10 +5,10 @@ A test to benchmark the performance of this module relative to alternatives.
## The contenders ## The contenders
* [jimp](https://www.npmjs.com/package/jimp) v0.16.1 - Image processing in pure JavaScript. Provides bicubic interpolation. * [jimp](https://www.npmjs.com/package/jimp) v0.16.1 - Image processing in pure JavaScript. Provides bicubic interpolation.
* [mapnik](https://www.npmjs.org/package/mapnik) v4.5.5 - Whilst primarily a map renderer, Mapnik contains bitmap image utilities. * [mapnik](https://www.npmjs.org/package/mapnik) v4.5.6 - Whilst primarily a map renderer, Mapnik contains bitmap image utilities.
* [imagemagick](https://www.npmjs.com/package/imagemagick) v0.1.3 - Supports filesystem only and "*has been unmaintained for a long time*". * [imagemagick](https://www.npmjs.com/package/imagemagick) v0.1.3 - Supports filesystem only and "*has been unmaintained for a long time*".
* [gm](https://www.npmjs.com/package/gm) v1.23.1 - Fully featured wrapper around GraphicsMagick's `gm` command line utility. * [gm](https://www.npmjs.com/package/gm) v1.23.1 - Fully featured wrapper around GraphicsMagick's `gm` command line utility.
* sharp v0.27.0 / libvips v8.10.5 - Caching within libvips disabled to ensure a fair comparison. * sharp v0.28.0 / libvips v8.10.6 - Caching within libvips disabled to ensure a fair comparison.
## The task ## The task
@@ -18,25 +18,25 @@ then compress to JPEG at a "quality" setting of 80.
## Test environment ## Test environment
* AWS EC2 eu-west-1 [c5d.large](https://aws.amazon.com/ec2/instance-types/c5/) (2x Xeon Platinum 8275CL CPU @ 3.00GHz) * AWS EC2 eu-west-1 [c5ad.xlarge](https://aws.amazon.com/ec2/instance-types/c5/) (4x AMD EPYC 7R32)
* Ubuntu 20.10 (ami-046cdbcee95cdd75c) * Ubuntu 20.10 (ami-03f10415e8b0bfb86)
* Node.js v14.15.3 * Node.js v14.16.0
## Results ## Results
| Module | Input | Output | Ops/sec | Speed-up | | Module | Input | Output | Ops/sec | Speed-up |
| :----------------- | :----- | :----- | ------: | -------: | | :----------------- | :----- | :----- | ------: | -------: |
| jimp | buffer | buffer | 0.77 | 1.0 | | jimp | buffer | buffer | 0.78 | 1.0 |
| mapnik | buffer | buffer | 3.39 | 4.4 | | mapnik | buffer | buffer | 3.39 | 4.3 |
| gm | buffer | buffer | 4.30 | 5.6 | | gm | buffer | buffer | 7.84 | 10.1 |
| gm | file | file | 4.33 | 5.6 | | gm | file | file | 9.24 | 11.8 |
| imagemagick | file | file | 4.39 | 5.7 | | imagemagick | file | file | 9.37 | 12.0 |
| sharp | stream | stream | 23.81 | 30.9 | | sharp | stream | stream | 26.84 | 34.4 |
| sharp | file | file | 25.09 | 32.6 | | sharp | file | file | 29.76 | 38.2 |
| sharp | buffer | buffer | 25.60 | 33.2 | | sharp | buffer | buffer | 31.60 | 40.5 |
Greater libvips performance can be expected with caching enabled (default) Greater libvips performance can be expected with caching enabled (default)
and using 4+ core machines, especially those with larger L1/L2 CPU caches. and using 8+ core machines, especially those with larger L1/L2 CPU caches.
The I/O limits of the relevant (de)compression library will generally determine maximum throughput. The I/O limits of the relevant (de)compression library will generally determine maximum throughput.
@@ -51,7 +51,7 @@ brew install mapnik
``` ```
```sh ```sh
sudo apt-get install imagemagick libmagick++-dev graphicsmagick libmapnik-dev sudo apt-get install build-essential imagemagick libmagick++-dev graphicsmagick libmapnik-dev
``` ```
```sh ```sh
@@ -61,7 +61,7 @@ sudo yum install ImageMagick-devel ImageMagick-c++-devel GraphicsMagick mapnik-d
```sh ```sh
git clone https://github.com/lovell/sharp.git git clone https://github.com/lovell/sharp.git
cd sharp cd sharp
npm install npm install --build-from-source
cd test/bench cd test/bench
npm install npm install
npm test npm test

File diff suppressed because one or more lines are too long

View File

@@ -8,7 +8,7 @@ const extractDescription = (str) =>
.replace(/\s+/g, ' ') .replace(/\s+/g, ' ')
.replace(/[^A-Za-z0-9_/\-,. ]/g, '') .replace(/[^A-Za-z0-9_/\-,. ]/g, '')
.replace(/\s+/g, ' ') .replace(/\s+/g, ' ')
.substr(0, 140) .substr(0, 180)
.trim(); .trim();
const extractKeywords = (str) => const extractKeywords = (str) =>
@@ -17,7 +17,7 @@ const extractKeywords = (str) =>
str str
.split(/[ -/]/) .split(/[ -/]/)
.map((word) => word.toLowerCase().replace(/[^a-z]/g, '')) .map((word) => word.toLowerCase().replace(/[^a-z]/g, ''))
.filter((word) => word.length > 2 && !stopWords.includes(word)) .filter((word) => word.length > 2 && word.length < 15 && !stopWords.includes(word))
) )
].join(' '); ].join(' ');

View File

@@ -4,21 +4,28 @@ module.exports = [
'about', 'about',
'after', 'after',
'all', 'all',
'allows',
'already', 'already',
'also',
'alternative', 'alternative',
'always', 'always',
'and', 'and',
'any', 'any',
'are', 'are',
'based',
'been', 'been',
'before', 'before',
'both',
'call',
'can', 'can',
'containing', 'containing',
'default', 'default',
'does', 'does',
'each', 'each',
'either', 'either',
'ensure',
'etc', 'etc',
'every',
'for', 'for',
'from', 'from',
'get', 'get',
@@ -28,7 +35,10 @@ module.exports = [
'have', 'have',
'how', 'how',
'image', 'image',
'involve',
'its', 'its',
'least',
'lots',
'may', 'may',
'more', 'more',
'most', 'most',
@@ -38,17 +48,24 @@ module.exports = [
'not', 'not',
'occur', 'occur',
'occurs', 'occurs',
'options',
'over', 'over',
'perform', 'perform',
'performs', 'performs',
'provide', 'provide',
'provided', 'provided',
'ready',
'same',
'see',
'set', 'set',
'sets', 'sets',
'should', 'should',
'since',
'spelling', 'spelling',
'such',
'support', 'support',
'supported', 'supported',
'sure',
'take', 'take',
'that', 'that',
'the', 'the',
@@ -57,6 +74,9 @@ module.exports = [
'therefore', 'therefore',
'these', 'these',
'this', 'this',
'under',
'unless',
'until',
'use', 'use',
'used', 'used',
'using', 'using',
@@ -67,5 +87,6 @@ module.exports = [
'while', 'while',
'will', 'will',
'with', 'with',
'without' 'without',
'you'
]; ];

View File

@@ -4,7 +4,6 @@ const fs = require('fs');
const path = require('path'); const path = require('path');
const libvips = require('../lib/libvips'); const libvips = require('../lib/libvips');
const npmLog = require('npmlog');
const minimumLibvipsVersion = libvips.minimumLibvipsVersion; const minimumLibvipsVersion = libvips.minimumLibvipsVersion;
@@ -12,13 +11,13 @@ const platform = process.env.npm_config_platform || process.platform;
if (platform === 'win32') { if (platform === 'win32') {
const buildDir = path.join(__dirname, '..', 'build'); const buildDir = path.join(__dirname, '..', 'build');
const buildReleaseDir = path.join(buildDir, 'Release'); const buildReleaseDir = path.join(buildDir, 'Release');
npmLog.info('sharp', `Creating ${buildReleaseDir}`); libvips.log(`Creating ${buildReleaseDir}`);
try { try {
libvips.mkdirSync(buildDir); libvips.mkdirSync(buildDir);
libvips.mkdirSync(buildReleaseDir); libvips.mkdirSync(buildReleaseDir);
} catch (err) {} } catch (err) {}
const vendorLibDir = path.join(__dirname, '..', 'vendor', minimumLibvipsVersion, 'lib'); const vendorLibDir = path.join(__dirname, '..', 'vendor', minimumLibvipsVersion, 'lib');
npmLog.info('sharp', `Copying DLLs from ${vendorLibDir} to ${buildReleaseDir}`); libvips.log(`Copying DLLs from ${vendorLibDir} to ${buildReleaseDir}`);
try { try {
fs fs
.readdirSync(vendorLibDir) .readdirSync(vendorLibDir)
@@ -32,6 +31,7 @@ if (platform === 'win32') {
); );
}); });
} catch (err) { } catch (err) {
npmLog.error('sharp', err.message); libvips.log(err);
process.exit(1);
} }
} }

View File

@@ -7,7 +7,6 @@ const stream = require('stream');
const zlib = require('zlib'); const zlib = require('zlib');
const detectLibc = require('detect-libc'); const detectLibc = require('detect-libc');
const npmLog = require('npmlog');
const semver = require('semver'); const semver = require('semver');
const simpleGet = require('simple-get'); const simpleGet = require('simple-get');
const tarFs = require('tar-fs'); const tarFs = require('tar-fs');
@@ -22,34 +21,50 @@ const minimumGlibcVersionByArch = {
x64: '2.17' x64: '2.17'
}; };
const hasSharpPrebuild = [
'darwin-x64',
'linux-arm64',
'linux-x64',
'linuxmusl-x64',
'linuxmusl-arm64',
'win32-ia32',
'win32-x64'
];
const { minimumLibvipsVersion, minimumLibvipsVersionLabelled } = libvips; const { minimumLibvipsVersion, minimumLibvipsVersionLabelled } = libvips;
const distHost = process.env.npm_config_sharp_libvips_binary_host || 'https://github.com/lovell/sharp-libvips/releases/download'; const distHost = process.env.npm_config_sharp_libvips_binary_host || 'https://github.com/lovell/sharp-libvips/releases/download';
const distBaseUrl = process.env.npm_config_sharp_dist_base_url || process.env.SHARP_DIST_BASE_URL || `${distHost}/v${minimumLibvipsVersionLabelled}/`; const distBaseUrl = process.env.npm_config_sharp_dist_base_url || process.env.SHARP_DIST_BASE_URL || `${distHost}/v${minimumLibvipsVersionLabelled}/`;
const supportsBrotli = ('BrotliDecompress' in zlib); const supportsBrotli = ('BrotliDecompress' in zlib);
const fail = function (err) { const fail = function (err) {
npmLog.error('sharp', err.message); libvips.log(err);
if (err.code === 'EACCES') { if (err.code === 'EACCES') {
npmLog.info('sharp', 'Are you trying to install as a root or sudo user? Try again with the --unsafe-perm flag'); libvips.log('Are you trying to install as a root or sudo user? Try again with the --unsafe-perm flag');
} }
npmLog.info('sharp', 'Attempting to build from source via node-gyp but this may fail due to the above error'); libvips.log('Attempting to build from source via node-gyp but this may fail due to the above error');
npmLog.info('sharp', 'Please see https://sharp.pixelplumbing.com/install for required dependencies'); libvips.log('Please see https://sharp.pixelplumbing.com/install for required dependencies');
process.exit(1); process.exit(1);
}; };
const extractTarball = function (tarPath) { const extractTarball = function (tarPath, platformAndArch) {
const vendorPath = path.join(__dirname, '..', 'vendor'); const vendorPath = path.join(__dirname, '..', 'vendor');
libvips.mkdirSync(vendorPath); libvips.mkdirSync(vendorPath);
const versionedVendorPath = path.join(vendorPath, minimumLibvipsVersion); const versionedVendorPath = path.join(vendorPath, minimumLibvipsVersion);
libvips.mkdirSync(versionedVendorPath); libvips.mkdirSync(versionedVendorPath);
const ignoreVendorInclude = hasSharpPrebuild.includes(platformAndArch) && !process.env.npm_config_build_from_source;
const ignore = function (name) {
return ignoreVendorInclude && name.includes('include/');
};
stream.pipeline( stream.pipeline(
fs.createReadStream(tarPath), fs.createReadStream(tarPath),
supportsBrotli ? new zlib.BrotliDecompress() : new zlib.Gunzip(), supportsBrotli ? new zlib.BrotliDecompress() : new zlib.Gunzip(),
tarFs.extract(versionedVendorPath), tarFs.extract(versionedVendorPath, { ignore }),
function (err) { function (err) {
if (err) { if (err) {
if (/unexpected end of file/.test(err.message)) { if (/unexpected end of file/.test(err.message)) {
npmLog.error('sharp', `Please delete ${tarPath} as it is not a valid tarball`); fail(new Error(`Please delete ${tarPath} as it is not a valid tarball`));
} }
fail(err); fail(err);
} }
@@ -62,11 +77,11 @@ try {
if (useGlobalLibvips) { if (useGlobalLibvips) {
const globalLibvipsVersion = libvips.globalLibvipsVersion(); const globalLibvipsVersion = libvips.globalLibvipsVersion();
npmLog.info('sharp', `Detected globally-installed libvips v${globalLibvipsVersion}`); libvips.log(`Detected globally-installed libvips v${globalLibvipsVersion}`);
npmLog.info('sharp', 'Building from source via node-gyp'); libvips.log('Building from source via node-gyp');
process.exit(1); process.exit(1);
} else if (libvips.hasVendoredLibvips()) { } else if (libvips.hasVendoredLibvips()) {
npmLog.info('sharp', `Using existing vendored libvips v${minimumLibvipsVersion}`); libvips.log(`Using existing vendored libvips v${minimumLibvipsVersion}`);
} else { } else {
// Is this arch/platform supported? // Is this arch/platform supported?
const arch = process.env.npm_config_arch || process.arch; const arch = process.env.npm_config_arch || process.arch;
@@ -74,6 +89,9 @@ try {
if (arch === 'ia32' && !platformAndArch.startsWith('win32')) { if (arch === 'ia32' && !platformAndArch.startsWith('win32')) {
throw new Error(`Intel Architecture 32-bit systems require manual installation of libvips >= ${minimumLibvipsVersion}`); throw new Error(`Intel Architecture 32-bit systems require manual installation of libvips >= ${minimumLibvipsVersion}`);
} }
if (platformAndArch === 'darwin-arm64') {
throw new Error("Please run 'brew install vips' to install libvips on Apple M1 (ARM64) systems");
}
if (platformAndArch === 'freebsd-x64' || platformAndArch === 'openbsd-x64' || platformAndArch === 'sunos-x64') { if (platformAndArch === 'freebsd-x64' || platformAndArch === 'openbsd-x64' || platformAndArch === 'sunos-x64') {
throw new Error(`BSD/SunOS systems require manual installation of libvips >= ${minimumLibvipsVersion}`); throw new Error(`BSD/SunOS systems require manual installation of libvips >= ${minimumLibvipsVersion}`);
} }
@@ -82,6 +100,11 @@ try {
throw new Error(`Use with glibc ${detectLibc.version} requires manual installation of libvips >= ${minimumLibvipsVersion}`); throw new Error(`Use with glibc ${detectLibc.version} requires manual installation of libvips >= ${minimumLibvipsVersion}`);
} }
} }
if (detectLibc.family === detectLibc.MUSL && detectLibc.version) {
if (semver.lt(detectLibc.version, '1.1.24')) {
throw new Error(`Use with musl ${detectLibc.version} requires manual installation of libvips >= ${minimumLibvipsVersion}`);
}
}
const supportedNodeVersion = process.env.npm_package_engines_node || require('../package.json').engines.node; const supportedNodeVersion = process.env.npm_package_engines_node || require('../package.json').engines.node;
if (!semver.satisfies(process.versions.node, supportedNodeVersion)) { if (!semver.satisfies(process.versions.node, supportedNodeVersion)) {
@@ -94,13 +117,11 @@ try {
const tarFilename = ['libvips', minimumLibvipsVersion, platformAndArch].join('-') + '.tar.' + extension; const tarFilename = ['libvips', minimumLibvipsVersion, platformAndArch].join('-') + '.tar.' + extension;
const tarPathCache = path.join(libvips.cachePath(), tarFilename); const tarPathCache = path.join(libvips.cachePath(), tarFilename);
if (fs.existsSync(tarPathCache)) { if (fs.existsSync(tarPathCache)) {
npmLog.info('sharp', `Using cached ${tarPathCache}`); libvips.log(`Using cached ${tarPathCache}`);
extractTarball(tarPathCache); extractTarball(tarPathCache, platformAndArch);
} else { } else {
const tarPathTemp = path.join(os.tmpdir(), `${process.pid}-${tarFilename}`);
const tmpFile = fs.createWriteStream(tarPathTemp);
const url = distBaseUrl + tarFilename; const url = distBaseUrl + tarFilename;
npmLog.info('sharp', `Downloading ${url}`); libvips.log(`Downloading ${url}`);
simpleGet({ url: url, agent: agent() }, function (err, response) { simpleGet({ url: url, agent: agent() }, function (err, response) {
if (err) { if (err) {
fail(err); fail(err);
@@ -109,24 +130,37 @@ try {
} else if (response.statusCode !== 200) { } else if (response.statusCode !== 200) {
fail(new Error(`Status ${response.statusCode} ${response.statusMessage}`)); fail(new Error(`Status ${response.statusCode} ${response.statusMessage}`));
} else { } else {
const tarPathTemp = path.join(os.tmpdir(), `${process.pid}-${tarFilename}`);
const tmpFileStream = fs.createWriteStream(tarPathTemp);
response response
.on('error', fail) .on('error', function (err) {
.pipe(tmpFile); tmpFileStream.destroy(err);
})
.on('close', function () {
if (!response.complete) {
tmpFileStream.destroy(new Error('Download incomplete (connection was terminated)'));
}
})
.pipe(tmpFileStream);
tmpFileStream
.on('error', function (err) {
// Clean up temporary file
fs.unlinkSync(tarPathTemp);
fail(err);
})
.on('close', function () {
try {
// Attempt to rename
fs.renameSync(tarPathTemp, tarPathCache);
} catch (err) {
// Fall back to copy and unlink
fs.copyFileSync(tarPathTemp, tarPathCache);
fs.unlinkSync(tarPathTemp);
}
extractTarball(tarPathCache);
});
} }
}); });
tmpFile
.on('error', fail)
.on('close', function () {
try {
// Attempt to rename
fs.renameSync(tarPathTemp, tarPathCache);
} catch (err) {
// Fall back to copy and unlink
fs.copyFileSync(tarPathTemp, tarPathCache);
fs.unlinkSync(tarPathTemp);
}
extractTarball(tarPathCache);
});
} }
} }
} catch (err) { } catch (err) {

View File

@@ -18,12 +18,10 @@ try {
help.push(`- Ensure "${process.platform}" is used at install time as well as runtime`); help.push(`- Ensure "${process.platform}" is used at install time as well as runtime`);
} else if (/dylib/.test(err.message) && /Incompatible library version/.test(err.message)) { } else if (/dylib/.test(err.message) && /Incompatible library version/.test(err.message)) {
help.push('- Run "brew update && brew upgrade vips"'); help.push('- Run "brew update && brew upgrade vips"');
} else if (/Cannot find module/.test(err.message)) {
help.push('- Run "npm rebuild --verbose sharp" and look for errors');
} else { } else {
help.push( help.push(
'- Remove the "node_modules/sharp" directory then run', '- Remove the "node_modules/sharp" directory then run',
' "npm install --ignore-scripts=false --verbose" and look for errors' ' "npm install --ignore-scripts=false --verbose sharp" and look for errors'
); );
} }
help.push( help.push(
@@ -117,7 +115,7 @@ const debuglog = util.debuglog('sharp');
* sigma: 30 * sigma: 30
* } * }
* } * }
* }.toFile('noise.png'); * }).toFile('noise.png');
* *
* @param {(Buffer|Uint8Array|Uint8ClampedArray|string)} [input] - if present, can be * @param {(Buffer|Uint8Array|Uint8ClampedArray|string)} [input] - if present, can be
* a Buffer / Uint8Array / Uint8ClampedArray containing JPEG, PNG, WebP, AVIF, GIF, SVG, TIFF or raw pixel image data, or * a Buffer / Uint8Array / Uint8ClampedArray containing JPEG, PNG, WebP, AVIF, GIF, SVG, TIFF or raw pixel image data, or
@@ -244,7 +242,7 @@ const Sharp = function (input, options) {
jpegOptimiseCoding: true, jpegOptimiseCoding: true,
jpegQuantisationTable: 0, jpegQuantisationTable: 0,
pngProgressive: false, pngProgressive: false,
pngCompressionLevel: 9, pngCompressionLevel: 6,
pngAdaptiveFiltering: false, pngAdaptiveFiltering: false,
pngPalette: false, pngPalette: false,
pngQuality: 100, pngQuality: 100,
@@ -281,6 +279,7 @@ const Sharp = function (input, options) {
tileSkipBlanks: -1, tileSkipBlanks: -1,
tileBackground: [255, 255, 255, 255], tileBackground: [255, 255, 255, 255],
tileCentre: false, tileCentre: false,
tileId: 'https://example.com/iiif',
linearA: 1, linearA: 1,
linearB: 0, linearB: 0,
// Function to notify of libvips warnings // Function to notify of libvips warnings

View File

@@ -37,6 +37,23 @@ const cachePath = function () {
return libvipsCachePath; return libvipsCachePath;
}; };
const log = function (item) {
if (item instanceof Error) {
console.error(`sharp: ${item.message}`);
} else {
console.log(`sharp: ${item}`);
}
};
const isRosetta = function () {
/* istanbul ignore next */
if (process.platform === 'darwin' && process.arch === 'x64') {
const translated = spawnSync('sysctl sysctl.proc_translated', spawnSyncOptions).stdout;
return (translated || '').trim() === 'sysctl.proc_translated: 1';
}
return false;
};
const globalLibvipsVersion = function () { const globalLibvipsVersion = function () {
if (process.platform !== 'win32') { if (process.platform !== 'win32') {
const globalLibvipsVersion = spawnSync(`PKG_CONFIG_PATH="${pkgConfigPath()}" pkg-config --modversion vips-cpp`, spawnSyncOptions).stdout; const globalLibvipsVersion = spawnSync(`PKG_CONFIG_PATH="${pkgConfigPath()}" pkg-config --modversion vips-cpp`, spawnSyncOptions).stdout;
@@ -82,7 +99,10 @@ const useGlobalLibvips = function () {
if (Boolean(env.SHARP_IGNORE_GLOBAL_LIBVIPS) === true) { if (Boolean(env.SHARP_IGNORE_GLOBAL_LIBVIPS) === true) {
return false; return false;
} }
/* istanbul ignore next */
if (isRosetta()) {
return false;
}
const globalVipsVersion = globalLibvipsVersion(); const globalVipsVersion = globalLibvipsVersion();
return !!globalVipsVersion && /* istanbul ignore next */ return !!globalVipsVersion && /* istanbul ignore next */
semver.gte(globalVipsVersion, minimumLibvipsVersion); semver.gte(globalVipsVersion, minimumLibvipsVersion);
@@ -92,6 +112,7 @@ module.exports = {
minimumLibvipsVersion, minimumLibvipsVersion,
minimumLibvipsVersionLabelled, minimumLibvipsVersionLabelled,
cachePath, cachePath,
log,
globalLibvipsVersion, globalLibvipsVersion,
hasVendoredLibvips, hasVendoredLibvips,
pkgConfigPath, pkgConfigPath,

View File

@@ -1,6 +1,5 @@
'use strict'; 'use strict';
const { flatten: flattenArray } = require('array-flatten');
const color = require('color'); const color = require('color');
const is = require('./is'); const is = require('./is');
@@ -127,7 +126,7 @@ function flop (flop) {
* @throws {Error} Invalid parameters * @throws {Error} Invalid parameters
*/ */
function affine (matrix, options) { function affine (matrix, options) {
const flatMatrix = flattenArray(matrix); const flatMatrix = [].concat(...matrix);
if (flatMatrix.length === 4 && flatMatrix.every(is.number)) { if (flatMatrix.length === 4 && flatMatrix.every(is.number)) {
this.options.affineMatrix = flatMatrix; this.options.affineMatrix = flatMatrix;
} else { } else {
@@ -269,7 +268,13 @@ function blur (sigma) {
} }
/** /**
* Merge alpha transparency channel, if any, with a background. * Merge alpha transparency channel, if any, with a background, then remove the alpha channel.
*
* @example
* await sharp(rgbaInput)
* .flatten('#F0A703')
* .toBuffer();
*
* @param {Object} [options] * @param {Object} [options]
* @param {string|Object} [options.background={r: 0, g: 0, b: 0}] - background colour, parsed by the [color](https://www.npmjs.org/package/color) module, defaults to black. * @param {string|Object} [options.background={r: 0, g: 0, b: 0}] - background colour, parsed by the [color](https://www.npmjs.org/package/color) module, defaults to black.
* @returns {Sharp} * @returns {Sharp}
@@ -397,7 +402,7 @@ function convolve (kernel) {
} }
/** /**
* Any pixel value greather than or equal to the threshold value will be set to 255, otherwise it will be set to 0. * Any pixel value greater than or equal to the threshold value will be set to 255, otherwise it will be set to 0.
* @param {number} [threshold=128] - a value in the range 0-255 representing the level at which the threshold will be applied. * @param {number} [threshold=128] - a value in the range 0-255 representing the level at which the threshold will be applied.
* @param {Object} [options] * @param {Object} [options]
* @param {Boolean} [options.greyscale=true] - convert to single channel greyscale. * @param {Boolean} [options.greyscale=true] - convert to single channel greyscale.

View File

@@ -16,6 +16,8 @@ const formats = new Map([
['gif', 'gif'] ['gif', 'gif']
]); ]);
const errMagickSave = new Error('GIF output requires libvips with support for ImageMagick');
/** /**
* Write output image data to a file. * Write output image data to a file.
* *
@@ -47,25 +49,23 @@ const formats = new Map([
* @throws {Error} Invalid parameters * @throws {Error} Invalid parameters
*/ */
function toFile (fileOut, callback) { function toFile (fileOut, callback) {
if (!fileOut || fileOut.length === 0) { let err;
const errOutputInvalid = new Error('Missing output file path'); if (!is.string(fileOut)) {
err = new Error('Missing output file path');
} else if (this.options.input.file === fileOut) {
err = new Error('Cannot use same file for input and output');
} else if (this.options.formatOut === 'input' && fileOut.toLowerCase().endsWith('.gif') && !this.constructor.format.magick.output.file) {
err = errMagickSave;
}
if (err) {
if (is.fn(callback)) { if (is.fn(callback)) {
callback(errOutputInvalid); callback(err);
} else { } else {
return Promise.reject(errOutputInvalid); return Promise.reject(err);
} }
} else { } else {
if (this.options.input.file === fileOut) { this.options.fileOut = fileOut;
const errOutputIsInput = new Error('Cannot use same file for input and output'); return this._pipeline(callback);
if (is.fn(callback)) {
callback(errOutputIsInput);
} else {
return Promise.reject(errOutputIsInput);
}
} else {
this.options.fileOut = fileOut;
return this._pipeline(callback);
}
} }
return this; return this;
} }
@@ -105,10 +105,10 @@ function toFile (fileOut, callback) {
* .catch(err => { ... }); * .catch(err => { ... });
* *
* @example * @example
* const data = await sharp('my-image.jpg') * const { data, info } = await sharp('my-image.jpg')
* // output the raw pixels * // output the raw pixels
* .raw() * .raw()
* .toBuffer(); * .toBuffer({ resolveWithObject: true });
* *
* // create a more type safe way to work with the raw pixel data * // create a more type safe way to work with the raw pixel data
* // this will not copy the data, instead it will change `data`s underlying ArrayBuffer * // this will not copy the data, instead it will change `data`s underlying ArrayBuffer
@@ -116,7 +116,9 @@ function toFile (fileOut, callback) {
* const pixelArray = new Uint8ClampedArray(data.buffer); * const pixelArray = new Uint8ClampedArray(data.buffer);
* *
* // When you are done changing the pixelArray, sharp takes the `pixelArray` as an input * // When you are done changing the pixelArray, sharp takes the `pixelArray` as an input
* await sharp(pixelArray).toFile('my-changed-image.jpg'); * const { width, height, channels } = info;
* await sharp(pixelArray, { raw: { width, height, channels } })
* .toFile('my-changed-image.jpg');
* *
* @param {Object} [options] * @param {Object} [options]
* @param {boolean} [options.resolveWithObject] Resolve the Promise with an Object containing `data` and `info` properties instead of resolving only with `data`. * @param {boolean} [options.resolveWithObject] Resolve the Promise with an Object containing `data` and `info` properties instead of resolving only with `data`.
@@ -188,7 +190,7 @@ function withMetadata (options) {
* @throws {Error} unsupported format or options * @throws {Error} unsupported format or options
*/ */
function toFormat (format, options) { function toFormat (format, options) {
const actualFormat = formats.get(is.object(format) && is.string(format.id) ? format.id : format); const actualFormat = formats.get((is.object(format) && is.string(format.id) ? format.id : format).toLowerCase());
if (!actualFormat) { if (!actualFormat) {
throw is.invalidParameterError('format', `one of: ${[...formats.keys()].join(', ')}`, format); throw is.invalidParameterError('format', `one of: ${[...formats.keys()].join(', ')}`, format);
} }
@@ -198,8 +200,6 @@ function toFormat (format, options) {
/** /**
* Use these JPEG options for output image. * Use these JPEG options for output image.
* *
* Some of these options require the use of a globally-installed libvips compiled with support for mozjpeg.
*
* @example * @example
* // Convert any input to very high quality JPEG output * // Convert any input to very high quality JPEG output
* const data = await sharp(input) * const data = await sharp(input)
@@ -209,18 +209,25 @@ function toFormat (format, options) {
* }) * })
* .toBuffer(); * .toBuffer();
* *
* @example
* // Use mozjpeg to reduce output JPEG file size (slower)
* const data = await sharp(input)
* .jpeg({ mozjpeg: true })
* .toBuffer();
*
* @param {Object} [options] - output options * @param {Object} [options] - output options
* @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 otherwise defaults to '4:2:0' chroma subsampling * @param {string} [options.chromaSubsampling='4:2:0'] - set to '4:4:4' to prevent chroma subsampling otherwise defaults to '4:2:0' chroma subsampling
* @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 {boolean} [options.trellisQuantisation=false] - apply trellis quantisation, requires libvips compiled with support for mozjpeg * @param {boolean} [options.mozjpeg=false] - use mozjpeg defaults, equivalent to `{ trellisQuantisation: true, overshootDeringing: true, optimiseScans: true, quantisationTable: 3 }`
* @param {boolean} [options.overshootDeringing=false] - apply overshoot deringing, requires libvips compiled with support for mozjpeg * @param {boolean} [options.trellisQuantisation=false] - apply trellis quantisation
* @param {boolean} [options.optimiseScans=false] - optimise progressive scans, forces progressive, requires libvips compiled with support for mozjpeg * @param {boolean} [options.overshootDeringing=false] - apply overshoot deringing
* @param {boolean} [options.optimizeScans=false] - alternative spelling of optimiseScans, requires libvips compiled with support for mozjpeg * @param {boolean} [options.optimiseScans=false] - optimise progressive scans, forces progressive
* @param {number} [options.quantisationTable=0] - quantization table to use, integer 0-8, requires libvips compiled with support for mozjpeg * @param {boolean} [options.optimizeScans=false] - alternative spelling of optimiseScans
* @param {number} [options.quantizationTable=0] - alternative spelling of quantisationTable, requires libvips compiled with support for mozjpeg * @param {number} [options.quantisationTable=0] - quantization table to use, integer 0-8
* @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}
* @throws {Error} Invalid options * @throws {Error} Invalid options
@@ -244,6 +251,23 @@ function jpeg (options) {
throw is.invalidParameterError('chromaSubsampling', 'one of: 4:2:0, 4:4:4', options.chromaSubsampling); throw is.invalidParameterError('chromaSubsampling', 'one of: 4:2:0, 4:4:4', options.chromaSubsampling);
} }
} }
const optimiseCoding = is.bool(options.optimizeCoding) ? options.optimizeCoding : options.optimiseCoding;
if (is.defined(optimiseCoding)) {
this._setBooleanOption('jpegOptimiseCoding', optimiseCoding);
}
if (is.defined(options.mozjpeg)) {
if (is.bool(options.mozjpeg)) {
if (options.mozjpeg) {
this.options.jpegTrellisQuantisation = true;
this.options.jpegOvershootDeringing = true;
this.options.jpegOptimiseScans = true;
this.options.jpegProgressive = true;
this.options.jpegQuantisationTable = 3;
}
} else {
throw is.invalidParameterError('mozjpeg', 'boolean', options.mozjpeg);
}
}
const trellisQuantisation = is.bool(options.trellisQuantization) ? options.trellisQuantization : options.trellisQuantisation; const trellisQuantisation = is.bool(options.trellisQuantization) ? options.trellisQuantization : options.trellisQuantisation;
if (is.defined(trellisQuantisation)) { if (is.defined(trellisQuantisation)) {
this._setBooleanOption('jpegTrellisQuantisation', trellisQuantisation); this._setBooleanOption('jpegTrellisQuantisation', trellisQuantisation);
@@ -258,10 +282,6 @@ function jpeg (options) {
this.options.jpegProgressive = true; this.options.jpegProgressive = true;
} }
} }
const optimiseCoding = is.bool(options.optimizeCoding) ? options.optimizeCoding : options.optimiseCoding;
if (is.defined(optimiseCoding)) {
this._setBooleanOption('jpegOptimiseCoding', optimiseCoding);
}
const quantisationTable = is.number(options.quantizationTable) ? options.quantizationTable : options.quantisationTable; const quantisationTable = is.number(options.quantizationTable) ? options.quantizationTable : options.quantisationTable;
if (is.defined(quantisationTable)) { if (is.defined(quantisationTable)) {
if (is.integer(quantisationTable) && is.inRange(quantisationTable, 0, 8)) { if (is.integer(quantisationTable) && is.inRange(quantisationTable, 0, 8)) {
@@ -277,26 +297,31 @@ function jpeg (options) {
/** /**
* Use these PNG options for output image. * Use these PNG options for output image.
* *
* PNG output is always full colour at 8 or 16 bits per pixel. * By default, PNG output is full colour at 8 or 16 bits per pixel.
* Indexed PNG input at 1, 2 or 4 bits per pixel is converted to 8 bits per pixel. * Indexed PNG input at 1, 2 or 4 bits per pixel is converted to 8 bits per pixel.
* * Set `palette` to `true` for slower, indexed PNG output.
* Some of these options require the use of a globally-installed libvips compiled with support for libimagequant (GPL).
* *
* @example * @example
* // Convert any input to PNG output * // Convert any input to full colour PNG output
* const data = await sharp(input) * const data = await sharp(input)
* .png() * .png()
* .toBuffer(); * .toBuffer();
* *
* @example
* // Convert any input to indexed PNG output (slower)
* const data = await sharp(input)
* .png({ palette: true })
* .toBuffer();
*
* @param {Object} [options] * @param {Object} [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=6] - zlib compression level, 0 (fastest, largest) to 9 (slowest, smallest)
* @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 libvips compiled with support for libimagequant * @param {boolean} [options.palette=false] - quantise to a palette-based image with alpha transparency support
* @param {number} [options.quality=100] - use the lowest number of colours needed to achieve given quality, sets `palette` to `true`, requires libvips compiled with support for libimagequant * @param {number} [options.quality=100] - use the lowest number of colours needed to achieve given quality, sets `palette` to `true`
* @param {number} [options.colours=256] - maximum number of palette entries, sets `palette` to `true`, requires libvips compiled with support for libimagequant * @param {number} [options.colours=256] - maximum number of palette entries, sets `palette` to `true`
* @param {number} [options.colors=256] - alternative spelling of `options.colours`, sets `palette` to `true`, requires libvips compiled with support for libimagequant * @param {number} [options.colors=256] - alternative spelling of `options.colours`, sets `palette` to `true`
* @param {number} [options.dither=1.0] - level of Floyd-Steinberg error diffusion, sets `palette` to `true`, requires libvips compiled with support for libimagequant * @param {number} [options.dither=1.0] - level of Floyd-Steinberg error diffusion, sets `palette` to `true`
* @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
@@ -426,7 +451,7 @@ function webp (options) {
/* istanbul ignore next */ /* istanbul ignore next */
function gif (options) { function gif (options) {
if (!this.constructor.format.magick.output.buffer) { if (!this.constructor.format.magick.output.buffer) {
throw new Error('The gif operation requires libvips to have been installed with support for ImageMagick'); throw errMagickSave;
} }
trySetAnimationOptions(options, this.options); trySetAnimationOptions(options, this.options);
return this._updateFormatOut('gif', options); return this._updateFormatOut('gif', options);
@@ -582,7 +607,7 @@ function tiff (options) {
* @param {Object} [options] - output options * @param {Object} [options] - output options
* @param {number} [options.quality=50] - quality, integer 1-100 * @param {number} [options.quality=50] - quality, integer 1-100
* @param {boolean} [options.lossless=false] - use lossless compression * @param {boolean} [options.lossless=false] - use lossless compression
* @param {boolean} [options.speed=5] - CPU effort vs file size, 0 (slowest/smallest) to 8 (fastest/largest) * @param {number} [options.speed=5] - CPU effort vs file size, 0 (slowest/smallest) to 8 (fastest/largest)
* @param {string} [options.chromaSubsampling='4:2:0'] - set to '4:4:4' to prevent chroma subsampling otherwise defaults to '4:2:0' chroma subsampling, requires libvips v8.11.0 * @param {string} [options.chromaSubsampling='4:2:0'] - set to '4:4:4' to prevent chroma subsampling otherwise defaults to '4:2:0' chroma subsampling, requires libvips v8.11.0
* @returns {Sharp} * @returns {Sharp}
* @throws {Error} Invalid options * @throws {Error} Invalid options
@@ -704,6 +729,7 @@ function raw () {
* @param {string} [options.layout='dz'] filesystem layout, possible values are `dz`, `iiif`, `zoomify` or `google`. * @param {string} [options.layout='dz'] filesystem layout, possible values are `dz`, `iiif`, `zoomify` or `google`.
* @param {boolean} [options.centre=false] centre image in tile. * @param {boolean} [options.centre=false] centre image in tile.
* @param {boolean} [options.center=false] alternative spelling of centre. * @param {boolean} [options.center=false] alternative spelling of centre.
* @param {string} [options.id='https://example.com/iiif'] when `layout` is `iiif`, sets the `@id` attribute of `info.json`
* @returns {Sharp} * @returns {Sharp}
* @throws {Error} Invalid parameters * @throws {Error} Invalid parameters
*/ */
@@ -777,6 +803,14 @@ function tile (options) {
if (is.defined(centre)) { if (is.defined(centre)) {
this._setBooleanOption('tileCentre', centre); this._setBooleanOption('tileCentre', centre);
} }
// @id attribute for IIIF layout
if (is.defined(options.id)) {
if (is.string(options.id)) {
this.options.tileId = options.id;
} else {
throw is.invalidParameterError('id', 'string', options.id);
}
}
} }
// Format // Format
if (is.inArray(this.options.formatOut, ['jpeg', 'png', 'webp'])) { if (is.inArray(this.options.formatOut, ['jpeg', 'png', 'webp'])) {

View File

@@ -302,11 +302,20 @@ function resize (width, height, options) {
* }) * })
* ... * ...
* *
* @example
* // Add a row of 10 red pixels to the bottom
* sharp(input)
* .extend({
* bottom: 10,
* background: 'red'
* })
* ...
*
* @param {(number|Object)} extend - single pixel count to add to all edges or an Object with per-edge counts * @param {(number|Object)} extend - single pixel count to add to all edges or an Object with per-edge counts
* @param {number} [extend.top] * @param {number} [extend.top=0]
* @param {number} [extend.left] * @param {number} [extend.left=0]
* @param {number} [extend.bottom] * @param {number} [extend.bottom=0]
* @param {number} [extend.right] * @param {number} [extend.right=0]
* @param {String|Object} [extend.background={r: 0, g: 0, b: 0, alpha: 1}] - background colour, parsed by the [color](https://www.npmjs.org/package/color) module, defaults to black without transparency. * @param {String|Object} [extend.background={r: 0, g: 0, b: 0, alpha: 1}] - background colour, parsed by the [color](https://www.npmjs.org/package/color) module, defaults to black without transparency.
* @returns {Sharp} * @returns {Sharp}
* @throws {Error} Invalid parameters * @throws {Error} Invalid parameters
@@ -317,17 +326,35 @@ function extend (extend) {
this.options.extendBottom = extend; this.options.extendBottom = extend;
this.options.extendLeft = extend; this.options.extendLeft = extend;
this.options.extendRight = extend; this.options.extendRight = extend;
} else if ( } else if (is.object(extend)) {
is.object(extend) && if (is.defined(extend.top)) {
is.integer(extend.top) && extend.top >= 0 && if (is.integer(extend.top) && extend.top >= 0) {
is.integer(extend.bottom) && extend.bottom >= 0 && this.options.extendTop = extend.top;
is.integer(extend.left) && extend.left >= 0 && } else {
is.integer(extend.right) && extend.right >= 0 throw is.invalidParameterError('top', 'positive integer', extend.top);
) { }
this.options.extendTop = extend.top; }
this.options.extendBottom = extend.bottom; if (is.defined(extend.bottom)) {
this.options.extendLeft = extend.left; if (is.integer(extend.bottom) && extend.bottom >= 0) {
this.options.extendRight = extend.right; this.options.extendBottom = extend.bottom;
} else {
throw is.invalidParameterError('bottom', 'positive integer', extend.bottom);
}
}
if (is.defined(extend.left)) {
if (is.integer(extend.left) && extend.left >= 0) {
this.options.extendLeft = extend.left;
} else {
throw is.invalidParameterError('left', 'positive integer', extend.left);
}
}
if (is.defined(extend.right)) {
if (is.integer(extend.right) && extend.right >= 0) {
this.options.extendRight = extend.right;
} else {
throw is.invalidParameterError('right', 'positive integer', extend.right);
}
}
this._setBackgroundColourOption('extendBackground', extend.background); this._setBackgroundColourOption('extendBackground', extend.background);
} else { } else {
throw is.invalidParameterError('extend', 'integer or object', extend); throw is.invalidParameterError('extend', 'integer or object', extend);

View File

@@ -1,6 +1,8 @@
'use strict'; 'use strict';
const events = require('events'); const events = require('events');
const detectLibc = require('detect-libc');
const is = require('./is'); const is = require('./is');
const sharp = require('../build/Release/sharp.node'); const sharp = require('../build/Release/sharp.node');
@@ -84,8 +86,12 @@ cache(true);
/** /**
* Gets or, when a concurrency is provided, sets * Gets or, when a concurrency is provided, sets
* the number of threads _libvips'_ should create to process each image. * the number of threads _libvips'_ should create to process each image.
* The default value is the number of CPU cores. *
* A value of `0` will reset to this default. * The default value is the number of CPU cores,
* except when using glibc-based Linux without jemalloc,
* where the default is `1` to help reduce memory fragmentation.
*
* A value of `0` will reset this to the number of CPU cores.
* *
* The maximum number of images that can be processed in parallel * The maximum number of images that can be processed in parallel
* is limited by libuv's `UV_THREADPOOL_SIZE` environment variable. * is limited by libuv's `UV_THREADPOOL_SIZE` environment variable.
@@ -103,6 +109,11 @@ cache(true);
function concurrency (concurrency) { function concurrency (concurrency) {
return sharp.concurrency(is.integer(concurrency) ? concurrency : null); return sharp.concurrency(is.integer(concurrency) ? concurrency : null);
} }
/* istanbul ignore next */
if (detectLibc.family === detectLibc.GLIBC && !sharp._isUsingJemalloc()) {
// Reduce default concurrency to 1 when using glibc memory allocator
sharp.concurrency(1);
}
/** /**
* An EventEmitter that emits a `change` event when a task is either: * An EventEmitter that emits a `change` event when a task is either:
@@ -157,14 +168,10 @@ simd(true);
* @private * @private
*/ */
module.exports = function (Sharp) { module.exports = function (Sharp) {
[ Sharp.cache = cache;
cache, Sharp.concurrency = concurrency;
concurrency, Sharp.counters = counters;
counters, Sharp.simd = simd;
simd
].forEach(function (f) {
Sharp[f.name] = f;
});
Sharp.format = format; Sharp.format = format;
Sharp.interpolators = interpolators; Sharp.interpolators = interpolators;
Sharp.versions = versions; Sharp.versions = versions;

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, AVIF and TIFF images", "description": "High performance Node.js image processing, the fastest module to resize JPEG, PNG, WebP, AVIF and TIFF images",
"version": "0.27.1", "version": "0.28.0",
"author": "Lovell Fuller <npm@lovell.info>", "author": "Lovell Fuller <npm@lovell.info>",
"homepage": "https://github.com/lovell/sharp", "homepage": "https://github.com/lovell/sharp",
"contributors": [ "contributors": [
@@ -80,7 +80,7 @@
"install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node-gyp rebuild && node install/dll-copy)", "install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node-gyp rebuild && node install/dll-copy)",
"clean": "rm -rf node_modules/ build/ vendor/ .nyc_output/ coverage/ test/fixtures/output.*", "clean": "rm -rf node_modules/ build/ vendor/ .nyc_output/ coverage/ test/fixtures/output.*",
"test": "semistandard && cpplint && npm run test-unit && npm run test-licensing", "test": "semistandard && cpplint && npm run test-unit && npm run test-licensing",
"test-unit": "nyc --reporter=lcov --branches=99 mocha --parallel --slow=5000 --timeout=60000 ./test/unit/*.js", "test-unit": "nyc --reporter=lcov --branches=99 mocha --slow=1000 --timeout=60000 ./test/unit/*.js",
"test-licensing": "license-checker --production --summary --onlyAllow=\"Apache-2.0;BSD;ISC;MIT\"", "test-licensing": "license-checker --production --summary --onlyAllow=\"Apache-2.0;BSD;ISC;MIT\"",
"test-coverage": "./test/coverage/report.sh", "test-coverage": "./test/coverage/report.sh",
"test-leak": "./test/leak/leak.sh", "test-leak": "./test/leak/leak.sh",
@@ -117,14 +117,12 @@
"vips" "vips"
], ],
"dependencies": { "dependencies": {
"array-flatten": "^3.0.0",
"color": "^3.1.3", "color": "^3.1.3",
"detect-libc": "^1.0.3", "detect-libc": "^1.0.3",
"node-addon-api": "^3.1.0", "node-addon-api": "^3.1.0",
"npmlog": "^4.1.2", "prebuild-install": "^6.0.1",
"prebuild-install": "^6.0.0", "semver": "^7.3.5",
"semver": "^7.3.4", "simple-get": "^3.1.0",
"simple-get": "^4.0.0",
"tar-fs": "^2.1.1", "tar-fs": "^2.1.1",
"tunnel-agent": "^0.6.0" "tunnel-agent": "^0.6.0"
}, },
@@ -132,11 +130,11 @@
"async": "^3.2.0", "async": "^3.2.0",
"cc": "^3.0.1", "cc": "^3.0.1",
"decompress-zip": "^0.3.3", "decompress-zip": "^0.3.3",
"documentation": "^13.1.1", "documentation": "^13.2.0",
"exif-reader": "^1.0.3", "exif-reader": "^1.0.3",
"icc": "^2.0.0", "icc": "^2.0.0",
"license-checker": "^25.0.1", "license-checker": "^25.0.1",
"mocha": "^8.2.1", "mocha": "^8.3.2",
"mock-fs": "^4.13.0", "mock-fs": "^4.13.0",
"nyc": "^15.1.0", "nyc": "^15.1.0",
"prebuild": "^10.0.1", "prebuild": "^10.0.1",
@@ -145,7 +143,7 @@
}, },
"license": "Apache-2.0", "license": "Apache-2.0",
"config": { "config": {
"libvips": "8.10.5", "libvips": "8.10.6",
"runtime": "napi", "runtime": "napi",
"target": 3 "target": 3
}, },

View File

@@ -213,6 +213,8 @@ namespace sharp {
{ "VipsForeignLoadTiffBuffer", ImageType::TIFF }, { "VipsForeignLoadTiffBuffer", ImageType::TIFF },
{ "VipsForeignLoadGifFile", ImageType::GIF }, { "VipsForeignLoadGifFile", ImageType::GIF },
{ "VipsForeignLoadGifBuffer", ImageType::GIF }, { "VipsForeignLoadGifBuffer", ImageType::GIF },
{ "VipsForeignLoadNsgifFile", ImageType::GIF },
{ "VipsForeignLoadNsgifBuffer", ImageType::GIF },
{ "VipsForeignLoadSvgFile", ImageType::SVG }, { "VipsForeignLoadSvgFile", ImageType::SVG },
{ "VipsForeignLoadSvgBuffer", ImageType::SVG }, { "VipsForeignLoadSvgBuffer", ImageType::SVG },
{ "VipsForeignLoadHeifFile", ImageType::HEIF }, { "VipsForeignLoadHeifFile", ImageType::HEIF },
@@ -421,12 +423,7 @@ namespace sharp {
Uses colour space interpretation with number of channels to guess this. Uses colour space interpretation with number of channels to guess this.
*/ */
bool HasAlpha(VImage image) { bool HasAlpha(VImage image) {
int const bands = image.bands(); return image.has_alpha();
VipsInterpretation const interpretation = image.interpretation();
return (
(bands == 2 && interpretation == VIPS_INTERPRETATION_B_W) ||
(bands == 4 && interpretation != VIPS_INTERPRETATION_CMYK) ||
(bands == 5 && interpretation == VIPS_INTERPRETATION_CMYK));
} }
/* /*

View File

@@ -26,8 +26,8 @@
#if (VIPS_MAJOR_VERSION < 8) || \ #if (VIPS_MAJOR_VERSION < 8) || \
(VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION < 10) || \ (VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION < 10) || \
(VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION == 10 && VIPS_MICRO_VERSION < 5) (VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION == 10 && VIPS_MICRO_VERSION < 6)
#error "libvips version 8.10.5+ is required - please see https://sharp.pixelplumbing.com/install" #error "libvips version 8.10.6+ is required - please see https://sharp.pixelplumbing.com/install"
#endif #endif
#if ((!defined(__clang__)) && defined(__GNUC__) && (__GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 6))) #if ((!defined(__clang__)) && defined(__GNUC__) && (__GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 6)))

View File

@@ -149,8 +149,8 @@ namespace sharp {
*/ */
VImage Recomb(VImage image, std::unique_ptr<double[]> const &matrix) { VImage Recomb(VImage image, std::unique_ptr<double[]> const &matrix) {
double *m = matrix.get(); double *m = matrix.get();
image = image.colourspace(VIPS_INTERPRETATION_sRGB);
return image return image
.colourspace(VIPS_INTERPRETATION_sRGB)
.recomb(image.bands() == 3 .recomb(image.bands() == 3
? VImage::new_from_memory( ? VImage::new_from_memory(
m, 9 * sizeof(double), 3, 3, 1, VIPS_FORMAT_DOUBLE m, 9 * sizeof(double), 3, 3, 1, VIPS_FORMAT_DOUBLE

View File

@@ -562,9 +562,17 @@ class PipelineWorker : public Napi::AsyncWorker {
// Use gravity in overlay // Use gravity in overlay
if (compositeImage.width() <= baton->width) { if (compositeImage.width() <= baton->width) {
across = static_cast<int>(ceil(static_cast<double>(image.width()) / compositeImage.width())); across = static_cast<int>(ceil(static_cast<double>(image.width()) / compositeImage.width()));
// Ensure odd number of tiles across when gravity is centre, north or south
if (composite->gravity == 0 || composite->gravity == 1 || composite->gravity == 3) {
across |= 1;
}
} }
if (compositeImage.height() <= baton->height) { if (compositeImage.height() <= baton->height) {
down = static_cast<int>(ceil(static_cast<double>(image.height()) / compositeImage.height())); down = static_cast<int>(ceil(static_cast<double>(image.height()) / compositeImage.height()));
// Ensure odd number of tiles down when gravity is centre, east or west
if (composite->gravity == 0 || composite->gravity == 2 || composite->gravity == 4) {
down |= 1;
}
} }
if (across != 0 || down != 0) { if (across != 0 || down != 0) {
int left; int left;
@@ -594,8 +602,13 @@ class PipelineWorker : public Napi::AsyncWorker {
int top; int top;
if (composite->hasOffset) { if (composite->hasOffset) {
// Composite image at given offsets // Composite image at given offsets
std::tie(left, top) = sharp::CalculateCrop(image.width(), image.height(), if (composite->tile) {
compositeImage.width(), compositeImage.height(), composite->left, composite->top); std::tie(left, top) = sharp::CalculateCrop(image.width(), image.height(),
compositeImage.width(), compositeImage.height(), composite->left, composite->top);
} else {
left = composite->left;
top = composite->top;
}
} else { } else {
// Composite image 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(),
@@ -1038,6 +1051,7 @@ class PipelineWorker : public Napi::AsyncWorker {
->set("angle", CalculateAngleRotation(baton->tileAngle)) ->set("angle", CalculateAngleRotation(baton->tileAngle))
->set("background", baton->tileBackground) ->set("background", baton->tileBackground)
->set("centre", baton->tileCentre) ->set("centre", baton->tileCentre)
->set("id", const_cast<char*>(baton->tileId.data()))
->set("skip_blanks", baton->tileSkipBlanks); ->set("skip_blanks", baton->tileSkipBlanks);
// libvips chooses a default depth based on layout. Instead of replicating that logic here by // libvips chooses a default depth based on layout. Instead of replicating that logic here by
// not passing anything - libvips will handle choice // not passing anything - libvips will handle choice
@@ -1438,6 +1452,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_DZ_DEPTH, vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_DZ_DEPTH,
sharp::AttrAsStr(options, "tileDepth").data())); sharp::AttrAsStr(options, "tileDepth").data()));
baton->tileCentre = sharp::AttrAsBool(options, "tileCentre"); baton->tileCentre = sharp::AttrAsBool(options, "tileCentre");
baton->tileId = sharp::AttrAsStr(options, "tileId");
// Force random access for certain operations // Force random access for certain operations
if (baton->input->access == VIPS_ACCESS_SEQUENTIAL) { if (baton->input->access == VIPS_ACCESS_SEQUENTIAL) {

View File

@@ -192,6 +192,7 @@ struct PipelineBaton {
std::vector<double> tileBackground; std::vector<double> tileBackground;
int tileSkipBlanks; int tileSkipBlanks;
VipsForeignDzDepth tileDepth; VipsForeignDzDepth tileDepth;
std::string tileId;
std::unique_ptr<double[]> recombMatrix; std::unique_ptr<double[]> recombMatrix;
PipelineBaton(): PipelineBaton():
@@ -258,7 +259,7 @@ struct PipelineBaton {
jpegOptimiseScans(false), jpegOptimiseScans(false),
jpegOptimiseCoding(true), jpegOptimiseCoding(true),
pngProgressive(false), pngProgressive(false),
pngCompressionLevel(9), pngCompressionLevel(6),
pngAdaptiveFiltering(false), pngAdaptiveFiltering(false),
pngPalette(false), pngPalette(false),
pngQuality(100), pngQuality(100),

View File

@@ -44,6 +44,7 @@ Napi::Object init(Napi::Env env, Napi::Object exports) {
exports.Set("libvipsVersion", Napi::Function::New(env, libvipsVersion)); exports.Set("libvipsVersion", Napi::Function::New(env, libvipsVersion));
exports.Set("format", Napi::Function::New(env, format)); exports.Set("format", Napi::Function::New(env, format));
exports.Set("_maxColourDistance", Napi::Function::New(env, _maxColourDistance)); exports.Set("_maxColourDistance", Napi::Function::New(env, _maxColourDistance));
exports.Set("_isUsingJemalloc", Napi::Function::New(env, _isUsingJemalloc));
exports.Set("stats", Napi::Function::New(env, stats)); exports.Set("stats", Napi::Function::New(env, stats));
return exports; return exports;
} }

View File

@@ -225,3 +225,19 @@ Napi::Value _maxColourDistance(const Napi::CallbackInfo& info) {
return Napi::Number::New(env, maxColourDistance); return Napi::Number::New(env, maxColourDistance);
} }
#if defined(__GNUC__)
// mallctl will be resolved by the runtime linker when jemalloc is being used
extern "C" {
int mallctl(const char *name, void *oldp, size_t *oldlenp, void *newp, size_t newlen) __attribute__((weak));
}
Napi::Value _isUsingJemalloc(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
return Napi::Boolean::New(env, mallctl != nullptr);
}
#else
Napi::Value _isUsingJemalloc(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
return Napi::Boolean::New(env, false);
}
#endif

View File

@@ -24,5 +24,6 @@ Napi::Value simd(const Napi::CallbackInfo& info);
Napi::Value libvipsVersion(const Napi::CallbackInfo& info); Napi::Value libvipsVersion(const Napi::CallbackInfo& info);
Napi::Value format(const Napi::CallbackInfo& info); Napi::Value format(const Napi::CallbackInfo& info);
Napi::Value _maxColourDistance(const Napi::CallbackInfo& info); Napi::Value _maxColourDistance(const Napi::CallbackInfo& info);
Napi::Value _isUsingJemalloc(const Napi::CallbackInfo& info);
#endif // SRC_UTILITIES_H_ #endif // SRC_UTILITIES_H_

View File

@@ -13,7 +13,7 @@
"gm": "1.23.1", "gm": "1.23.1",
"imagemagick": "0.1.3", "imagemagick": "0.1.3",
"jimp": "0.16.1", "jimp": "0.16.1",
"mapnik": "4.5.5", "mapnik": "4.5.6",
"semver": "7.3.4" "semver": "7.3.4"
}, },
"license": "Apache-2.0", "license": "Apache-2.0",

View File

@@ -1,5 +1,6 @@
'use strict'; 'use strict';
const os = require('os');
const fs = require('fs'); const fs = require('fs');
const async = require('async'); const async = require('async');
@@ -25,6 +26,9 @@ const height = 588;
// Disable libvips cache to ensure tests are as fair as they can be // Disable libvips cache to ensure tests are as fair as they can be
sharp.cache(false); sharp.cache(false);
// Spawn one thread per CPU
sharp.concurrency(os.cpus().length);
async.series({ async.series({
jpeg: function (callback) { jpeg: function (callback) {
const inputJpgBuffer = fs.readFileSync(fixtures.inputJpg); const inputJpgBuffer = fs.readFileSync(fixtures.inputJpg);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 664 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 426 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 692 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 653 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 606 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 672 KiB

View File

@@ -109,7 +109,7 @@ module.exports = {
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
inputSvgSmallViewBox: getPath('circle.svg'), inputSvgSmallViewBox: getPath('circle.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
inputAvif: getPath('cosmos_frame12924_yuv420_10bpc_bt2020_pq_q50.avif'), // CC by-nc-nd https://github.com/AOMediaCodec/av1-avif/tree/master/testFiles/Netflix inputAvif: getPath('sdr_cosmos12920_cicp1-13-6_yuv444_full_qp10.avif'), // CC by-nc-nd https://github.com/AOMediaCodec/av1-avif/tree/master/testFiles/Netflix
inputJPGBig: getPath('flowers.jpeg'), inputJPGBig: getPath('flowers.jpeg'),

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

View File

@@ -426,6 +426,16 @@
... ...
fun:vips_target_finish fun:vips_target_finish
} }
{
value8_libvips_static
Memcheck:Value8
obj:*/libvips-cpp.so.*
}
{
cond_libvips_static
Memcheck:Cond
obj:*/libvips-cpp.so.*
}
{ {
leak_libvips_init leak_libvips_init
Memcheck:Leak Memcheck:Leak
@@ -440,7 +450,7 @@
match-leak-kinds: definite match-leak-kinds: definite
fun:malloc fun:malloc
... ...
fun:rsvg_rust_handle_new_from_stream_sync fun:rsvg_handle_new_from_stream_sync
} }
{ {
leak_rsvg_rsvg_rust_handle_new_from_gfile_sync leak_rsvg_rsvg_rust_handle_new_from_gfile_sync
@@ -448,7 +458,7 @@
match-leak-kinds: definite match-leak-kinds: definite
fun:malloc fun:malloc
... ...
fun:rsvg_rust_handle_new_from_gfile_sync fun:rsvg_handle_new_from_gfile_sync
} }
{ {
leak_rsvg_rust_handle_new_from_stream_sync leak_rsvg_rust_handle_new_from_stream_sync
@@ -458,7 +468,7 @@
... ...
fun:xmlParseElement fun:xmlParseElement
... ...
fun:rsvg_rust_handle_new_from_stream_sync fun:rsvg_handle_new_from_stream_sync
} }
{ {
leak_rsvg_rust_handle_new_from_gfile_sync leak_rsvg_rust_handle_new_from_gfile_sync
@@ -468,7 +478,7 @@
... ...
fun:xmlParseElement fun:xmlParseElement
... ...
fun:rsvg_rust_handle_new_from_gfile_sync fun:rsvg_handle_new_from_gfile_sync
} }
# libuv warnings # libuv warnings

View File

@@ -12,29 +12,6 @@ describe('AVIF', () => {
}); });
}); });
it('can passthrough AVIF', async () => {
const data = await sharp(inputAvif)
.resize(32)
.toBuffer();
const metadata = await sharp(data)
.metadata();
const { compression, size, ...metadataWithoutSize } = metadata;
assert.deepStrictEqual(metadataWithoutSize, {
channels: 3,
depth: 'uchar',
format: 'heif',
hasAlpha: false,
hasProfile: false,
height: 12,
isProgressive: false,
pageHeight: 12,
pagePrimary: 0,
pages: 1,
space: 'srgb',
width: 32
});
});
it('can convert AVIF to JPEG', async () => { it('can convert AVIF to JPEG', async () => {
const data = await sharp(inputAvif) const data = await sharp(inputAvif)
.resize(32) .resize(32)
@@ -81,4 +58,27 @@ describe('AVIF', () => {
width: 32 width: 32
}); });
}); });
it('can passthrough AVIF', async () => {
const data = await sharp(inputAvif)
.resize(32)
.toBuffer();
const metadata = await sharp(data)
.metadata();
const { compression, size, ...metadataWithoutSize } = metadata;
assert.deepStrictEqual(metadataWithoutSize, {
channels: 3,
depth: 'uchar',
format: 'heif',
hasAlpha: false,
hasProfile: false,
height: 12,
isProgressive: false,
pageHeight: 12,
pagePrimary: 0,
pages: 1,
space: 'srgb',
width: 32
});
});
}); });

View File

@@ -42,18 +42,15 @@ describe('Colour space conversion', function () {
}); });
}); });
if (sharp.format.tiff.input.file && sharp.format.webp.output.buffer) { it('From 1-bit TIFF to sRGB WebP', async () => {
it('From 1-bit TIFF to sRGB WebP [slow]', function (done) { const data = await sharp(fixtures.inputTiff)
sharp(fixtures.inputTiff) .resize(8, 8)
.webp() .webp()
.toBuffer(function (err, data, info) { .toBuffer();
if (err) throw err;
assert.strictEqual(true, data.length > 0); const { format } = await sharp(data).metadata();
assert.strictEqual('webp', info.format); assert.strictEqual(format, 'webp');
done(); });
});
});
}
it('From CMYK to sRGB', function (done) { it('From CMYK to sRGB', function (done) {
sharp(fixtures.inputJpgWithCmykProfile) sharp(fixtures.inputJpgWithCmykProfile)

View File

@@ -225,6 +225,24 @@ describe('composite', () => {
}); });
}); });
it('centre gravity should replicate correct number of tiles', async () => {
const red = { r: 255, g: 0, b: 0 };
const [r, g, b] = await sharp({
create: {
width: 40, height: 40, channels: 4, background: red
}
})
.composite([{
input: fixtures.inputPngWithTransparency16bit,
gravity: 'centre',
tile: true
}])
.raw()
.toBuffer();
assert.deepStrictEqual({ r, g, b }, red);
});
it('cutout via dest-in', done => { it('cutout via dest-in', done => {
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)
.resize(300, 300) .resize(300, 300)
@@ -390,4 +408,20 @@ describe('composite', () => {
}, /Expected valid gravity for gravity but received invalid of type string/); }, /Expected valid gravity for gravity but received invalid of type string/);
}); });
}); });
it('Allow offset beyond bottom/right edge', async () => {
const red = { r: 255, g: 0, b: 0 };
const blue = { r: 0, g: 0, b: 255 };
const [r, g, b] = await sharp({ create: { width: 2, height: 2, channels: 4, background: red } })
.composite([{
input: { create: { width: 2, height: 2, channels: 4, background: blue } },
top: 1,
left: 1
}])
.raw()
.toBuffer();
assert.deepStrictEqual(red, { r, g, b });
});
}); });

View File

@@ -41,7 +41,6 @@ describe('Extend', function () {
.resize(120) .resize(120)
.extend({ .extend({
top: 50, top: 50,
bottom: 0,
left: 10, left: 10,
right: 35, right: 35,
background: { r: 0, g: 0, b: 0, alpha: 0 } background: { r: 0, g: 0, b: 0, alpha: 0 }
@@ -64,18 +63,38 @@ describe('Extend', function () {
sharp().extend(-1); sharp().extend(-1);
}); });
}); });
it('partial object fails', function () { it('invalid top fails', () => {
assert.throws(function () { assert.throws(
sharp().extend({ top: 1 }); () => sharp().extend({ top: 'fail' }),
}); /Expected positive integer for top but received fail of type string/
);
});
it('invalid bottom fails', () => {
assert.throws(
() => sharp().extend({ bottom: -1 }),
/Expected positive integer for bottom but received -1 of type number/
);
});
it('invalid left fails', () => {
assert.throws(
() => sharp().extend({ left: 0.1 }),
/Expected positive integer for left but received 0.1 of type number/
);
});
it('invalid right fails', () => {
assert.throws(
() => sharp().extend({ right: {} }),
/Expected positive integer for right but received \[object Object\] of type object/
);
});
it('can set all edges apart from right', () => {
assert.doesNotThrow(() => sharp().extend({ top: 1, left: 2, bottom: 3 }));
}); });
it('should add alpha channel before extending with a transparent Background', function (done) { it('should add alpha channel before extending with a transparent Background', function (done) {
sharp(fixtures.inputJpgWithLandscapeExif1) sharp(fixtures.inputJpgWithLandscapeExif1)
.extend({ .extend({
top: 0,
bottom: 10, bottom: 10,
left: 0,
right: 10, right: 10,
background: { r: 0, g: 0, b: 0, alpha: 0 } background: { r: 0, g: 0, b: 0, alpha: 0 }
}) })
@@ -91,9 +110,7 @@ describe('Extend', function () {
it('PNG with 2 channels', function (done) { it('PNG with 2 channels', function (done) {
sharp(fixtures.inputPngWithGreyAlpha) sharp(fixtures.inputPngWithGreyAlpha)
.extend({ .extend({
top: 0,
bottom: 20, bottom: 20,
left: 0,
right: 20, right: 20,
background: 'transparent' background: 'transparent'
}) })

View File

@@ -28,32 +28,28 @@ describe('Partial image extraction', function () {
}); });
}); });
if (sharp.format.webp.output.file) { it('WebP', function (done) {
it('WebP', function (done) { sharp(fixtures.inputWebP)
sharp(fixtures.inputWebP) .extract({ left: 100, top: 50, width: 125, height: 200 })
.extract({ left: 100, top: 50, width: 125, height: 200 }) .toBuffer(function (err, data, info) {
.toBuffer(function (err, data, info) { if (err) throw err;
if (err) throw err; assert.strictEqual(125, info.width);
assert.strictEqual(125, info.width); assert.strictEqual(200, info.height);
assert.strictEqual(200, info.height); fixtures.assertSimilar(fixtures.expected('extract.webp'), data, done);
fixtures.assertSimilar(fixtures.expected('extract.webp'), data, done); });
}); });
});
}
if (sharp.format.tiff.output.file) { it('TIFF', function (done) {
it('TIFF', function (done) { sharp(fixtures.inputTiff)
sharp(fixtures.inputTiff) .extract({ left: 34, top: 63, width: 341, height: 529 })
.extract({ left: 34, top: 63, width: 341, height: 529 }) .jpeg()
.jpeg() .toBuffer(function (err, data, info) {
.toBuffer(function (err, data, info) { if (err) throw err;
if (err) throw err; assert.strictEqual(341, info.width);
assert.strictEqual(341, info.width); assert.strictEqual(529, info.height);
assert.strictEqual(529, info.height); fixtures.assertSimilar(fixtures.expected('extract.tiff'), data, done);
fixtures.assertSimilar(fixtures.expected('extract.tiff'), data, done); });
}); });
});
}
it('Before resize', function (done) { it('Before resize', function (done) {
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)

View File

@@ -107,7 +107,7 @@ describe('Image channel extraction', function () {
}); });
}); });
it('Non-existant channel', function (done) { it('Non-existent channel', function (done) {
sharp(fixtures.inputPng) sharp(fixtures.inputPng)
.extractChannel(1) .extractChannel(1)
.toBuffer(function (err) { .toBuffer(function (err) {

View File

@@ -63,12 +63,17 @@ describe('GIF input', () => {
); );
if (!sharp.format.magick.output.buffer) { if (!sharp.format.magick.output.buffer) {
it('GIF output should fail due to missing ImageMagick', () => { it('GIF buffer output should fail due to missing ImageMagick', () => {
assert.throws( assert.throws(
() => { () => sharp().gif(),
sharp().gif(); /GIF output requires libvips with support for ImageMagick/
}, );
/The gif operation requires libvips to have been installed with support for ImageMagick/ });
it('GIF file output should fail due to missing ImageMagick', () => {
assert.rejects(
async () => await sharp().toFile('test.gif'),
/GIF output requires libvips with support for ImageMagick/
); );
}); });
} }

View File

@@ -290,4 +290,24 @@ describe('JPEG', function () {
}); });
}); });
}); });
it('Can use mozjpeg defaults', async () => {
const withoutData = await sharp(fixtures.inputJpg)
.resize(32, 24)
.jpeg({ mozjpeg: false })
.toBuffer();
const withoutMeta = await sharp(withoutData).metadata();
assert.strictEqual(false, withoutMeta.isProgressive);
const withData = await sharp(fixtures.inputJpg)
.resize(32, 24)
.jpeg({ mozjpeg: true })
.toBuffer();
const withMeta = await sharp(withData).metadata();
assert.strictEqual(true, withMeta.isProgressive);
});
it('Invalid mozjpeg value throws error', () => {
assert.throws(() => sharp().jpeg({ mozjpeg: 'fail' }));
});
}); });

View File

@@ -105,4 +105,30 @@ describe('libvips binaries', function () {
assert.strictEqual(true, fs.existsSync(nestedDirPath)); assert.strictEqual(true, fs.existsSync(nestedDirPath));
}); });
}); });
describe('logger', function () {
const consoleLog = console.log;
const consoleError = console.error;
after(function () {
console.log = consoleLog;
console.error = consoleError;
});
it('logs an info message', function (done) {
console.log = function (msg) {
assert.strictEqual(msg, 'sharp: progress');
done();
};
libvips.log('progress');
});
it('logs an error message', function (done) {
console.error = function (msg) {
assert.strictEqual(msg, 'sharp: problem');
done();
};
libvips.log(new Error('problem'));
});
});
}); });

View File

@@ -28,115 +28,138 @@ describe('Modulate', function () {
}); });
}); });
it('should be able to hue-rotate', function () { it('should be able to hue-rotate', async () => {
const base = 'modulate-hue-120.jpg'; const [r, g, b] = await sharp({
const actual = fixtures.path('output.' + base); create: {
const expected = fixtures.expected(base); width: 1,
height: 1,
return sharp(fixtures.inputJpg) channels: 3,
background: { r: 153, g: 68, b: 68 }
}
})
.modulate({ hue: 120 }) .modulate({ hue: 120 })
.toFile(actual) .raw()
.then(function () { .toBuffer();
fixtures.assertMaxColourDistance(actual, expected, 25);
}); assert.deepStrictEqual({ r: 41, g: 107, b: 57 }, { r, g, b });
}); });
it('should be able to brighten', function () { it('should be able to brighten', async () => {
const base = 'modulate-brightness-2.jpg'; const [r, g, b] = await sharp({
const actual = fixtures.path('output.' + base); create: {
const expected = fixtures.expected(base); width: 1,
height: 1,
return sharp(fixtures.inputJpg) channels: 3,
background: { r: 153, g: 68, b: 68 }
}
})
.modulate({ brightness: 2 }) .modulate({ brightness: 2 })
.toFile(actual) .raw()
.then(function () { .toBuffer();
fixtures.assertMaxColourDistance(actual, expected, 25);
}); assert.deepStrictEqual({ r: 255, g: 173, b: 168 }, { r, g, b });
}); });
it('should be able to unbrighten', function () { it('should be able to darken', async () => {
const base = 'modulate-brightness-0-5.jpg'; const [r, g, b] = await sharp({
const actual = fixtures.path('output.' + base); create: {
const expected = fixtures.expected(base); width: 1,
height: 1,
return sharp(fixtures.inputJpg) channels: 3,
background: { r: 153, g: 68, b: 68 }
}
})
.modulate({ brightness: 0.5 }) .modulate({ brightness: 0.5 })
.toFile(actual) .raw()
.then(function () { .toBuffer();
fixtures.assertMaxColourDistance(actual, expected, 25);
}); assert.deepStrictEqual({ r: 97, g: 17, b: 25 }, { r, g, b });
}); });
it('should be able to saturate', function () { it('should be able to saturate', async () => {
const base = 'modulate-saturation-2.jpg'; const [r, g, b] = await sharp({
const actual = fixtures.path('output.' + base); create: {
const expected = fixtures.expected(base); width: 1,
height: 1,
return sharp(fixtures.inputJpg) channels: 3,
background: { r: 153, g: 68, b: 68 }
}
})
.modulate({ saturation: 2 }) .modulate({ saturation: 2 })
.toFile(actual) .raw()
.then(function () { .toBuffer();
fixtures.assertMaxColourDistance(actual, expected, 30);
}); assert.deepStrictEqual({ r: 198, g: 0, b: 43 }, { r, g, b });
}); });
it('should be able to desaturate', function () { it('should be able to desaturate', async () => {
const base = 'modulate-saturation-0.5.jpg'; const [r, g, b] = await sharp({
const actual = fixtures.path('output.' + base); create: {
const expected = fixtures.expected(base); width: 1,
height: 1,
return sharp(fixtures.inputJpg) channels: 3,
background: { r: 153, g: 68, b: 68 }
}
})
.modulate({ saturation: 0.5 }) .modulate({ saturation: 0.5 })
.toFile(actual) .raw()
.then(function () { .toBuffer();
fixtures.assertMaxColourDistance(actual, expected, 25);
}); assert.deepStrictEqual({ r: 127, g: 83, b: 81 }, { r, g, b });
}); });
it('should be able to modulate all channels', function () { it('should be able to modulate all channels', async () => {
const base = 'modulate-all.jpg'; const [r, g, b] = await sharp({
const actual = fixtures.path('output.' + base); create: {
const expected = fixtures.expected(base); width: 1,
height: 1,
return sharp(fixtures.inputJpg) channels: 3,
background: { r: 153, g: 68, b: 68 }
}
})
.modulate({ brightness: 2, saturation: 0.5, hue: 180 }) .modulate({ brightness: 2, saturation: 0.5, hue: 180 })
.toFile(actual) .raw()
.then(function () { .toBuffer();
fixtures.assertMaxColourDistance(actual, expected, 25);
}); assert.deepStrictEqual({ r: 149, g: 209, b: 214 }, { r, g, b });
}); });
describe('hue-rotate', function (done) { it('should be able to use linear and modulate together', async () => {
[30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330, 360].forEach(function (angle) { const contrast = 1.5;
it('should properly hue rotate by ' + angle + 'deg', function () { const brightness = 0.5;
const base = 'modulate-hue-angle-' + angle + '.png';
const actual = fixtures.path('output.' + base); const [r, g, b] = await sharp({
create: {
width: 1,
height: 1,
channels: 3,
background: { r: 153, g: 68, b: 68 }
}
})
.linear(contrast, -(128 * contrast) + 128)
.modulate({ brightness })
.raw()
.toBuffer();
assert.deepStrictEqual({ r: 81, g: 0, b: 0 }, { r, g, b });
});
describe('hue-rotate', () => {
[30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330, 360].forEach(angle => {
it(`should hue rotate by ${angle} deg`, async () => {
const base = `modulate-hue-angle-${angle}.png`;
const actual = fixtures.path(`output.${base}`);
const expected = fixtures.expected(base); const expected = fixtures.expected(base);
return sharp(fixtures.testPattern) await sharp(fixtures.testPattern)
.resize(320)
.modulate({ hue: angle }) .modulate({ hue: angle })
.png({ compressionLevel: 0 })
.toFile(actual) .toFile(actual)
.then(function () { .then(() => {
fixtures.assertMaxColourDistance(actual, expected, 25); fixtures.assertMaxColourDistance(actual, expected);
}); });
}); });
}); });
}); });
it('should be able to use linear and modulate together', function () {
const base = 'modulate-linear.jpg';
const actual = fixtures.path('output.' + base);
const expected = fixtures.expected(base);
const contrast = 1.5;
const brightness = 0.5;
return sharp(fixtures.testPattern)
.linear(contrast, -(128 * contrast) + 128)
.modulate({ brightness })
.toFile(actual)
.then(function () {
fixtures.assertMaxColourDistance(actual, expected);
});
});
}); });

View File

@@ -54,6 +54,7 @@ describe('Normalization', function () {
it('keeps an existing alpha channel', function (done) { it('keeps an existing alpha channel', function (done) {
sharp(fixtures.inputPngWithTransparency) sharp(fixtures.inputPngWithTransparency)
.resize(8, 8)
.normalize() .normalize()
.toBuffer(function (err, data) { .toBuffer(function (err, data) {
if (err) throw err; if (err) throw err;
@@ -69,6 +70,7 @@ describe('Normalization', function () {
it('keeps the alpha channel of greyscale images intact', function (done) { it('keeps the alpha channel of greyscale images intact', function (done) {
sharp(fixtures.inputPngWithGreyAlpha) sharp(fixtures.inputPngWithGreyAlpha)
.resize(8, 8)
.normalise() .normalise()
.toBuffer(function (err, data) { .toBuffer(function (err, data) {
if (err) throw err; if (err) throw err;

View File

@@ -19,7 +19,7 @@ describe('PNG', function () {
}); });
}); });
it('default compressionLevel generates smaller file than compressionLevel=6', function (done) { it('default compressionLevel generates smaller file than compressionLevel=0', function (done) {
// First generate with default compressionLevel // First generate with default compressionLevel
sharp(fixtures.inputPng) sharp(fixtures.inputPng)
.resize(320, 240) .resize(320, 240)
@@ -31,7 +31,7 @@ describe('PNG', function () {
// Then generate with compressionLevel=6 // Then generate with compressionLevel=6
sharp(fixtures.inputPng) sharp(fixtures.inputPng)
.resize(320, 240) .resize(320, 240)
.png({ compressionLevel: 6 }) .png({ compressionLevel: 0 })
.toBuffer(function (err, largerData, largerInfo) { .toBuffer(function (err, largerData, largerInfo) {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, largerData.length > 0); assert.strictEqual(true, largerData.length > 0);

View File

@@ -5,15 +5,17 @@ const assert = require('assert');
const sharp = require('../../'); const sharp = require('../../');
const fixtures = require('../fixtures'); const fixtures = require('../fixtures');
const sepia = [
[0.3588, 0.7044, 0.1368],
[0.299, 0.587, 0.114],
[0.2392, 0.4696, 0.0912]
];
describe('Recomb', function () { describe('Recomb', function () {
it('applies a sepia filter using recomb', function (done) { it('applies a sepia filter using recomb', function (done) {
const output = fixtures.path('output.recomb-sepia.jpg'); const output = fixtures.path('output.recomb-sepia.jpg');
sharp(fixtures.inputJpgWithLandscapeExif1) sharp(fixtures.inputJpgWithLandscapeExif1)
.recomb([ .recomb(sepia)
[0.3588, 0.7044, 0.1368],
[0.299, 0.587, 0.114],
[0.2392, 0.4696, 0.0912]
])
.toFile(output, function (err, info) { .toFile(output, function (err, info) {
if (err) throw err; if (err) throw err;
assert.strictEqual('jpeg', info.format); assert.strictEqual('jpeg', info.format);
@@ -31,11 +33,7 @@ describe('Recomb', function () {
it('applies a sepia filter using recomb to an PNG with Alpha', function (done) { it('applies a sepia filter using recomb to an PNG with Alpha', function (done) {
const output = fixtures.path('output.recomb-sepia.png'); const output = fixtures.path('output.recomb-sepia.png');
sharp(fixtures.inputPngAlphaPremultiplicationSmall) sharp(fixtures.inputPngAlphaPremultiplicationSmall)
.recomb([ .recomb(sepia)
[0.3588, 0.7044, 0.1368],
[0.299, 0.587, 0.114],
[0.2392, 0.4696, 0.0912]
])
.toFile(output, function (err, info) { .toFile(output, function (err, info) {
if (err) throw err; if (err) throw err;
assert.strictEqual('png', info.format); assert.strictEqual('png', info.format);
@@ -50,6 +48,20 @@ describe('Recomb', function () {
}); });
}); });
it('recomb with a single channel input', async () => {
const { info } = await sharp(Buffer.alloc(64), {
raw: {
width: 8,
height: 8,
channels: 1
}
})
.recomb(sepia)
.toBuffer({ resolveWithObject: true });
assert.strictEqual(3, info.channels);
});
it('applies a different sepia filter using recomb', function (done) { it('applies a different sepia filter using recomb', function (done) {
const output = fixtures.path('output.recomb-sepia2.jpg'); const output = fixtures.path('output.recomb-sepia2.jpg');
sharp(fixtures.inputJpgWithLandscapeExif1) sharp(fixtures.inputJpgWithLandscapeExif1)

View File

@@ -1,5 +1,6 @@
'use strict'; 'use strict';
const fs = require('fs');
const assert = require('assert'); const assert = require('assert');
const sharp = require('../../'); const sharp = require('../../');
@@ -99,4 +100,18 @@ describe('SVG input', function () {
fixtures.assertSimilar(fixtures.expected('svg-embedded.png'), data, done); fixtures.assertSimilar(fixtures.expected('svg-embedded.png'), data, done);
}); });
}); });
it('Converts SVG with truncated embedded PNG', async () => {
const truncatedPng = fs.readFileSync(fixtures.inputPngTruncated).toString('base64');
const svg = `<?xml version="1.0" encoding="UTF-8"?>
<svg width="294" height="240" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<image width="294" height="240" xlink:href="data:image/png;base64,${truncatedPng}"/>
</svg>`;
const { info } = await sharp(Buffer.from(svg)).toBuffer({ resolveWithObject: true });
assert.strictEqual(info.format, 'png');
assert.strictEqual(info.width, 294);
assert.strictEqual(info.height, 240);
assert.strictEqual(info.channels, 4);
});
}); });

View File

@@ -90,9 +90,11 @@ describe('TIFF', function () {
it('Increasing TIFF quality increases file size', () => it('Increasing TIFF quality increases file size', () =>
sharp(fixtures.inputJpgWithLandscapeExif1) sharp(fixtures.inputJpgWithLandscapeExif1)
.resize(320, 240)
.tiff({ quality: 40 }) .tiff({ quality: 40 })
.toBuffer() .toBuffer()
.then(tiff40 => sharp(fixtures.inputJpgWithLandscapeExif1) .then(tiff40 => sharp(fixtures.inputJpgWithLandscapeExif1)
.resize(320, 240)
.tiff({ quality: 90 }) .tiff({ quality: 90 })
.toBuffer() .toBuffer()
.then(tiff90 => .then(tiff90 =>
@@ -155,6 +157,7 @@ describe('TIFF', function () {
it('TIFF setting xres and yres on file', () => it('TIFF setting xres and yres on file', () =>
sharp(fixtures.inputTiff) sharp(fixtures.inputTiff)
.resize(8, 8)
.tiff({ .tiff({
xres: 1000, xres: 1000,
yres: 1000 yres: 1000
@@ -171,6 +174,7 @@ describe('TIFF', function () {
it('TIFF setting xres and yres on buffer', () => it('TIFF setting xres and yres on buffer', () =>
sharp(fixtures.inputTiff) sharp(fixtures.inputTiff)
.resize(8, 8)
.tiff({ .tiff({
xres: 1000, xres: 1000,
yres: 1000 yres: 1000
@@ -279,7 +283,7 @@ describe('TIFF', function () {
}); });
}); });
it('TIFF deflate compression of integral input with float predictor increases file size', function (done) { it('TIFF deflate compression with float predictor shrinks test file', function (done) {
const startSize = fs.statSync(fixtures.inputTiffUncompressed).size; const startSize = fs.statSync(fixtures.inputTiffUncompressed).size;
sharp(fixtures.inputTiffUncompressed) sharp(fixtures.inputTiffUncompressed)
.tiff({ .tiff({
@@ -289,7 +293,7 @@ describe('TIFF', function () {
.toFile(outputTiff, (err, info) => { .toFile(outputTiff, (err, info) => {
if (err) throw err; if (err) throw err;
assert.strictEqual('tiff', info.format); assert.strictEqual('tiff', info.format);
assert(info.size > startSize); assert(startSize > info.size);
rimraf(outputTiff, done); rimraf(outputTiff, done);
}); });
}); });

View File

@@ -297,6 +297,22 @@ describe('Tile', function () {
}); });
}); });
it('Valid id parameter value passes', function () {
assert.doesNotThrow(function () {
sharp().tile({
id: 'test'
});
});
});
it('Invalid id parameter value fails', function () {
assert.throws(function () {
sharp().tile({
id: true
});
});
});
it('Deep Zoom layout', function (done) { it('Deep Zoom layout', function (done) {
const directory = fixtures.path('output.dzi_files'); const directory = fixtures.path('output.dzi_files');
rimraf(directory, function () { rimraf(directory, function () {
@@ -613,7 +629,7 @@ describe('Tile', function () {
rimraf(directory, function () { rimraf(directory, function () {
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)
.png({ .png({
compressionLevel: 1 compressionLevel: 0
}) })
.tile({ .tile({
layout: 'google' layout: 'google'
@@ -650,7 +666,8 @@ describe('Tile', function () {
rimraf(directory, function () { rimraf(directory, function () {
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)
.webp({ .webp({
quality: 1 quality: 1,
reductionEffort: 0
}) })
.tile({ .tile({
layout: 'google' layout: 'google'
@@ -814,11 +831,14 @@ describe('Tile', function () {
}); });
it('IIIF layout', function (done) { it('IIIF layout', function (done) {
const directory = fixtures.path('output.iiif.info'); const name = 'output.iiif.info';
const directory = fixtures.path(name);
rimraf(directory, function () { rimraf(directory, function () {
const id = 'https://sharp.test.com/iiif';
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)
.tile({ .tile({
layout: 'iiif' layout: 'iiif',
id
}) })
.toFile(directory, function (err, info) { .toFile(directory, function (err, info) {
if (err) throw err; if (err) throw err;
@@ -827,6 +847,8 @@ describe('Tile', function () {
assert.strictEqual(2225, info.height); assert.strictEqual(2225, info.height);
assert.strictEqual(3, info.channels); assert.strictEqual(3, info.channels);
assert.strictEqual('number', typeof info.size); assert.strictEqual('number', typeof info.size);
const infoJson = require(path.join(directory, 'info.json'));
assert.strictEqual(`${id}/${name}`, infoJson['@id']);
fs.stat(path.join(directory, '0,0,256,256', '256,', '0', 'default.jpg'), function (err, stat) { fs.stat(path.join(directory, '0,0,256,256', '256,', '0', 'default.jpg'), function (err, stat) {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, stat.isFile()); assert.strictEqual(true, stat.isFile());

27
test/unit/toFormat.js Normal file
View File

@@ -0,0 +1,27 @@
'use strict';
const assert = require('assert');
const sharp = require('../../');
const fixtures = require('../fixtures');
describe('toFormat', () => {
it('accepts upper case characters as format parameter (string)', async () => {
const data = await sharp(fixtures.inputJpg)
.resize(8, 8)
.toFormat('PNG')
.toBuffer();
const { format } = await sharp(data).metadata();
assert.strictEqual(format, 'png');
});
it('accepts upper case characters as format parameter (object)', async () => {
const data = await sharp(fixtures.inputJpg)
.resize(8, 8)
.toFormat({ id: 'PNG' })
.toBuffer();
const { format } = await sharp(data).metadata();
assert.strictEqual(format, 'png');
});
});

View File

@@ -3,8 +3,6 @@
const assert = require('assert'); const assert = require('assert');
const sharp = require('../../'); const sharp = require('../../');
const defaultConcurrency = sharp.concurrency();
describe('Utilities', function () { describe('Utilities', function () {
describe('Cache', function () { describe('Cache', function () {
it('Can be disabled', function () { it('Can be disabled', function () {
@@ -60,10 +58,10 @@ describe('Utilities', function () {
}); });
it('Can be reset to default', function () { it('Can be reset to default', function () {
sharp.concurrency(0); sharp.concurrency(0);
assert.strictEqual(defaultConcurrency, sharp.concurrency()); assert.strictEqual(true, sharp.concurrency() > 0);
}); });
it('Ignores invalid values', function () { it('Ignores invalid values', function () {
sharp.concurrency(0); const defaultConcurrency = sharp.concurrency();
sharp.concurrency('spoons'); sharp.concurrency('spoons');
assert.strictEqual(defaultConcurrency, sharp.concurrency()); assert.strictEqual(defaultConcurrency, sharp.concurrency());
}); });