Compare commits

...

22 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
49 changed files with 444 additions and 221 deletions

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

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

@@ -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]<[Buffer][8]>** when no callback is provided Returns **[Promise][5]<[Buffer][8]>** when no callback is provided
@@ -423,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

@@ -4,7 +4,7 @@
Requires libvips v8.10.6 Requires libvips v8.10.6
### v0.28.0 - TBD ### v0.28.0 - 29th March 2021
* Prebuilt binaries now include mozjpeg and libimagequant (BSD 2-Clause). * Prebuilt binaries now include mozjpeg and libimagequant (BSD 2-Clause).
@@ -16,10 +16,24 @@ Requires libvips v8.10.6
* Reduce concurrency on glibc-based Linux when using the default memory allocator to help prevent fragmentation. * 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. * Ensure `@id` attribute can be set for IIIF tile-based output.
[#2612](https://github.com/lovell/sharp/issues/2612) [#2612](https://github.com/lovell/sharp/issues/2612)
[@edsilv](https://github.com/edsilv) [@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

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.2/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.2/docs', sourcePath: 'https://cdn.jsdelivr.net/gh/lovell/sharp@v0.28.0/docs',
nav: [ nav: [
{ {
title: 'Funding', title: 'Funding',

View File

@@ -23,7 +23,7 @@ Node.js v10+ on the most common platforms:
* Windows x64 * Windows x64
* Windows x86 * Windows x86
An ~8MB 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
@@ -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

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) =>

View File

@@ -4,14 +4,19 @@ 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',
@@ -30,7 +35,10 @@ module.exports = [
'have', 'have',
'how', 'how',
'image', 'image',
'involve',
'its', 'its',
'least',
'lots',
'may', 'may',
'more', 'more',
'most', 'most',
@@ -40,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',
@@ -59,6 +74,9 @@ module.exports = [
'therefore', 'therefore',
'these', 'these',
'this', 'this',
'under',
'unless',
'until',
'use', 'use',
'used', 'used',
'using', 'using',
@@ -69,5 +87,6 @@ module.exports = [
'while', 'while',
'will', 'will',
'with', 'with',
'without' 'without',
'you'
]; ];

View File

@@ -268,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}
@@ -396,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

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

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,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.28.0-alpha1", "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 --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",
@@ -121,7 +121,7 @@
"detect-libc": "^1.0.3", "detect-libc": "^1.0.3",
"node-addon-api": "^3.1.0", "node-addon-api": "^3.1.0",
"prebuild-install": "^6.0.1", "prebuild-install": "^6.0.1",
"semver": "^7.3.4", "semver": "^7.3.5",
"simple-get": "^3.1.0", "simple-get": "^3.1.0",
"tar-fs": "^2.1.1", "tar-fs": "^2.1.1",
"tunnel-agent": "^0.6.0" "tunnel-agent": "^0.6.0"
@@ -130,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.3.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",
@@ -143,7 +143,7 @@
}, },
"license": "Apache-2.0", "license": "Apache-2.0",
"config": { "config": {
"libvips": "8.10.6-alpha3", "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 },

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(),

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

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

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

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

View File

@@ -5,22 +5,23 @@ const sharp = require('../../');
const fixtures = require('../fixtures'); const fixtures = require('../fixtures');
describe('toFormat', () => { describe('toFormat', () => {
it('accepts upper case characters as format parameter (string)', function (done) { it('accepts upper case characters as format parameter (string)', async () => {
sharp(fixtures.inputJpg) const data = await sharp(fixtures.inputJpg)
.resize(8, 8)
.toFormat('PNG') .toFormat('PNG')
.toBuffer(function (err, data, info) { .toBuffer();
if (err) throw err;
assert.strictEqual('png', info.format); const { format } = await sharp(data).metadata();
done(); assert.strictEqual(format, 'png');
});
}); });
it('accepts upper case characters as format parameter (object)', function (done) {
sharp(fixtures.inputJpg) it('accepts upper case characters as format parameter (object)', async () => {
const data = await sharp(fixtures.inputJpg)
.resize(8, 8)
.toFormat({ id: 'PNG' }) .toFormat({ id: 'PNG' })
.toBuffer(function (err, data, info) { .toBuffer();
if (err) throw err;
assert.strictEqual('png', info.format); const { format } = await sharp(data).metadata();
done(); assert.strictEqual(format, 'png');
});
}); });
}); });