Compare commits

..

30 Commits

Author SHA1 Message Date
Lovell Fuller
16cf9f0ef2 Release v0.28.1 2021-04-05 12:28:16 +01:00
Lovell Fuller
133f69d66c Bump prebuild-install (for sharp_local_prebuilds) 2021-04-05 12:26:28 +01:00
Lovell Fuller
bc60daff9e Allow EXIF metadata to be set/update #650 2021-04-05 11:39:53 +01:00
Lovell Fuller
43a085d1ae Add support for OME-TIFF subIFDs #2557 2021-04-02 08:04:21 +01:00
Lovell Fuller
8c33d0aa56 Allow ensureAlpha to set alpha transparency level #2634 2021-04-01 21:14:06 +01:00
Lovell Fuller
fe0767df13 Install: log errors with more obvious prefix 2021-04-01 16:20:58 +01:00
Lovell Fuller
08a25a0c8f Docs: add animated WebP example #2648 2021-04-01 16:04:46 +01:00
Lovell Fuller
cd410080bd Docs: remove reference to specific Lambda runtime 2021-03-29 20:16:07 +01:00
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
65 changed files with 721 additions and 263 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+
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
```sh
@@ -43,6 +51,7 @@ sharp(inputBuffer)
sharp('input.jpg')
.rotate()
.resize(200)
.jpeg({ mozjpeg: true })
.toBuffer()
.then( data => { ... })
.catch( err => { ... });
@@ -84,23 +93,15 @@ readableStream
.pipe(writableStream);
```
[![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)
### 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
## Contributing
A [guide for contributors](https://github.com/lovell/sharp/blob/master/.github/CONTRIBUTING.md)
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.

View File

@@ -50,6 +50,10 @@ no child processes are spawned and Promises/async/await are supported.
### 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
without having to use separate command line tools like
[jpegoptim](https://github.com/tjko/jpegoptim) and

View File

@@ -18,18 +18,33 @@ Returns **Sharp**
## ensureAlpha
Ensure alpha channel, if missing. The added alpha channel will be fully opaque. This is a no-op if the image already has an alpha channel.
Ensure the output image has an alpha transparency channel.
If missing, the added alpha channel will have the specified
transparency level, defaulting to fully-opaque (1).
This is a no-op if the image already has an alpha channel.
### Parameters
- `alpha` **[number][1]** alpha transparency level (0=fully-transparent, 1=fully-opaque) (optional, default `1`)
### Examples
```javascript
sharp('rgb.jpg')
// rgba.png will be a 4 channel image with a fully-opaque alpha channel
await sharp('rgb.jpg')
.ensureAlpha()
.toFile('rgba.png', function(err, info) {
// rgba.png is a 4 channel image with a fully opaque alpha channel
});
.toFile('rgba.png')
```
```javascript
// rgba is a 4 channel image with a fully-transparent alpha channel
const rgba = await sharp(rgb)
.ensureAlpha(0)
.toBuffer();
```
- Throws **[Error][2]** Invalid alpha transparency level
Returns **Sharp**
**Meta**
@@ -42,7 +57,7 @@ Extract a single channel from a multi-channel image.
### Parameters
- `channel` **([number][1] \| [string][2])** zero-indexed channel/band number to extract, or `red`, `green`, `blue` or `alpha`.
- `channel` **([number][1] \| [string][3])** zero-indexed channel/band number to extract, or `red`, `green`, `blue` or `alpha`.
### Examples
@@ -56,7 +71,7 @@ sharp(input)
});
```
- Throws **[Error][3]** Invalid channel
- Throws **[Error][2]** Invalid channel
Returns **Sharp**
@@ -75,11 +90,11 @@ For raw pixel input, the `options` object should contain a `raw` attribute, whic
### Parameters
- `images` **([Array][4]<([string][2] \| [Buffer][5])> | [string][2] \| [Buffer][5])** one or more images (file paths, Buffers).
- `images` **([Array][4]<([string][3] \| [Buffer][5])> | [string][3] \| [Buffer][5])** one or more images (file paths, Buffers).
- `options` **[Object][6]** image options, see `sharp()` constructor.
- Throws **[Error][3]** Invalid parameters
- Throws **[Error][2]** Invalid parameters
Returns **Sharp**
@@ -89,7 +104,7 @@ Perform a bitwise boolean operation on all input image channels (bands) to produ
### Parameters
- `boolOp` **[string][2]** one of `and`, `or` or `eor` to perform that bitwise operation, like the C logic operators `&`, `|` and `^` respectively.
- `boolOp` **[string][3]** one of `and`, `or` or `eor` to perform that bitwise operation, like the C logic operators `&`, `|` and `^` respectively.
### Examples
@@ -103,15 +118,15 @@ sharp('3-channel-rgb-input.png')
});
```
- Throws **[Error][3]** Invalid parameters
- Throws **[Error][2]** Invalid parameters
Returns **Sharp**
[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error
[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error
[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array

View File

@@ -28,6 +28,7 @@ Implements the [stream.Duplex][1] class.
- `options.density` **[number][8]** number representing the DPI for vector images in the range 1 to 100000. (optional, default `72`)
- `options.pages` **[number][8]** number of pages to extract for multi-page input (GIF, WebP, AVIF, TIFF, PDF), use -1 for all pages. (optional, default `1`)
- `options.page` **[number][8]** page number to start extracting from for multi-page input (GIF, WebP, AVIF, TIFF, PDF), zero based. (optional, default `0`)
- `options.subifd` **[number][8]** subIFD (Sub Image File Directory) to extract for OME-TIFF, defaults to main image. (optional, default `-1`)
- `options.level` **[number][8]** level to extract from a multi-level input (OpenSlide), zero based. (optional, default `0`)
- `options.animated` **[boolean][7]** Set to `true` to read all frames/pages of an animated image (equivalent of setting `pages` to `-1`). (optional, default `false`)
- `options.raw` **[Object][6]?** describes raw pixel input image data. See `raw()` for pixel ordering.

View File

@@ -21,6 +21,7 @@ A `Promise` is returned when `callback` is not provided.
- `delay`: Delay in ms between each page in an animated image, provided as an array of integers.
- `pagePrimary`: Number of the primary page in a HEIF image
- `levels`: Details of each level in a multi-level image provided as an array of objects, requires libvips compiled with support for OpenSlide
- `subifds`: Number of Sub Image File Directories in an OME-TIFF image
- `hasProfile`: Boolean indicating the presence of an embedded ICC profile
- `hasAlpha`: Boolean indicating the presence of an alpha transparency channel
- `orientation`: Number value of the EXIF Orientation header, if present

View File

@@ -169,13 +169,21 @@ Returns **Sharp**
## 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
- `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}`)
### Examples
```javascript
await sharp(rgbaInput)
.flatten('#F0A703')
.toBuffer();
```
Returns **Sharp**
## gamma
@@ -263,7 +271,7 @@ Returns **Sharp**
## 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

View File

@@ -87,10 +87,10 @@ sharp(input)
```
```javascript
const data = await sharp('my-image.jpg')
const { data, info } = await sharp('my-image.jpg')
// output the raw pixels
.raw()
.toBuffer();
.toBuffer({ resolveWithObject: true });
// 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
@@ -98,7 +98,9 @@ const data = await sharp('my-image.jpg')
const pixelArray = new Uint8ClampedArray(data.buffer);
// 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
@@ -117,6 +119,7 @@ sRGB colour space and strip all metadata, including the removal of any ICC profi
- `options` **[Object][6]?**
- `options.orientation` **[number][9]?** value between 1 and 8, used to update the EXIF `Orientation` tag.
- `options.icc` **[string][2]?** filesystem path to output ICC profile, defaults to sRGB.
- `options.exif` **[Object][6]<[Object][6]>** Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data. (optional, default `{}`)
### Examples
@@ -127,6 +130,19 @@ sharp('input.jpg')
.then(info => { ... });
```
```javascript
// Set "IFD0-Copyright" in output EXIF metadata
await sharp(input)
.withMetadata({
exif: {
IFD0: {
Copyright: 'Wernham Hogg'
}
}
})
.toBuffer();
```
- Throws **[Error][4]** Invalid parameters
Returns **Sharp**
@@ -265,6 +281,13 @@ const data = await sharp(input)
.toBuffer();
```
```javascript
// Optimise the file size of an animated WebP
const outputWebp = await sharp(inputWebp, { animated: true })
.webp({ reductionEffort: 6 })
.toBuffer();
```
- Throws **[Error][4]** Invalid options
Returns **Sharp**
@@ -423,6 +446,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.centre` **[boolean][7]** centre image in tile. (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

View File

@@ -137,10 +137,10 @@ This operation will always occur after resizing and extraction, if any.
### Parameters
- `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.left` **[number][8]?**
- `extend.bottom` **[number][8]?**
- `extend.right` **[number][8]?**
- `extend.top` **[number][8]** (optional, default `0`)
- `extend.left` **[number][8]** (optional, default `0`)
- `extend.bottom` **[number][8]** (optional, default `0`)
- `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}`)
### 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
Returns **Sharp**

View File

@@ -84,8 +84,12 @@ Returns **[Object][1]**
Gets or, when a concurrency is provided, sets
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
is limited by libuv's `UV_THREADPOOL_SIZE` environment variable.

View File

@@ -4,7 +4,20 @@
Requires libvips v8.10.6
### v0.28.0 - TBD
### v0.28.1 - 5th April 2021
* Ensure all installation errors are logged with a more obvious prefix.
* Allow `withMetadata` to set and update EXIF metadata.
[#650](https://github.com/lovell/sharp/issues/650)
* Add support for OME-TIFF Sub Image File Directories (subIFD).
[#2557](https://github.com/lovell/sharp/issues/2557)
* Allow `ensureAlpha` to set the alpha transparency level.
[#2634](https://github.com/lovell/sharp/issues/2634)
### v0.28.0 - 29th March 2021
* Prebuilt binaries now include mozjpeg and libimagequant (BSD 2-Clause).
@@ -16,10 +29,24 @@ Requires libvips v8.10.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*
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" href="https://pixel.plumbing/px/57x57/sharp-logo.svg">
<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.1/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="dns-prefetch" href="https://pixel.plumbing">
<link rel="dns-prefetch" href="https://www.google-analytics.com">
@@ -139,7 +139,7 @@
docuteApiTitlePlugin,
docuteApiSearchPlugin
],
sourcePath: 'https://cdn.jsdelivr.net/gh/lovell/sharp@v0.27.2/docs',
sourcePath: 'https://cdn.jsdelivr.net/gh/lovell/sharp@v0.28.1/docs',
nav: [
{
title: 'Funding',

View File

@@ -23,7 +23,7 @@ Node.js v10+ on the most common platforms:
* Windows x64
* 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`.
This provides support for the
@@ -102,6 +102,10 @@ To install the prebuilt sharp binaries from a custom URL,
set the `sharp_binary_host` npm config option
or the `npm_config_sharp_binary_host` environment variable.
To install the prebuilt sharp binaries from a directory on the local filesystem,
set the `sharp_local_prebuilds` npm config option
or the `npm_config_sharp_local_prebuilds` environment variable.
To install the prebuilt libvips binaries from a custom URL,
set the `sharp_libvips_binary_host` npm config option
or the `npm_config_sharp_libvips_binary_host` environment variable.
@@ -149,6 +153,23 @@ pkg install -y pkgconf vips
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
Add the
@@ -161,8 +182,6 @@ to `false` when using the `yarn` package manager.
## AWS Lambda
Set the Lambda runtime to `nodejs12.x`.
The binaries in the `node_modules` directory of the
[deployment package](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-package.html)
must be for the Linux x64 platform.

View File

@@ -5,10 +5,10 @@ A test to benchmark the performance of this module relative to alternatives.
## The contenders
* [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*".
* [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
@@ -18,25 +18,25 @@ then compress to JPEG at a "quality" setting of 80.
## Test environment
* AWS EC2 eu-west-1 [c5d.large](https://aws.amazon.com/ec2/instance-types/c5/) (2x Xeon Platinum 8275CL CPU @ 3.00GHz)
* Ubuntu 20.10 (ami-046cdbcee95cdd75c)
* Node.js v14.15.3
* AWS EC2 eu-west-1 [c5ad.xlarge](https://aws.amazon.com/ec2/instance-types/c5/) (4x AMD EPYC 7R32)
* Ubuntu 20.10 (ami-03f10415e8b0bfb86)
* Node.js v14.16.0
## Results
| Module | Input | Output | Ops/sec | Speed-up |
| :----------------- | :----- | :----- | ------: | -------: |
| jimp | buffer | buffer | 0.77 | 1.0 |
| mapnik | buffer | buffer | 3.39 | 4.4 |
| gm | buffer | buffer | 4.30 | 5.6 |
| gm | file | file | 4.33 | 5.6 |
| imagemagick | file | file | 4.39 | 5.7 |
| sharp | stream | stream | 23.81 | 30.9 |
| sharp | file | file | 25.09 | 32.6 |
| sharp | buffer | buffer | 25.60 | 33.2 |
| jimp | buffer | buffer | 0.78 | 1.0 |
| mapnik | buffer | buffer | 3.39 | 4.3 |
| gm | buffer | buffer | 7.84 | 10.1 |
| gm | file | file | 9.24 | 11.8 |
| imagemagick | file | file | 9.37 | 12.0 |
| sharp | stream | stream | 26.84 | 34.4 |
| sharp | file | file | 29.76 | 38.2 |
| sharp | buffer | buffer | 31.60 | 40.5 |
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.
@@ -51,7 +51,7 @@ brew install mapnik
```
```sh
sudo apt-get install imagemagick libmagick++-dev graphicsmagick libmapnik-dev
sudo apt-get install build-essential imagemagick libmagick++-dev graphicsmagick libmapnik-dev
```
```sh
@@ -61,7 +61,7 @@ sudo yum install ImageMagick-devel ImageMagick-c++-devel GraphicsMagick mapnik-d
```sh
git clone https://github.com/lovell/sharp.git
cd sharp
npm install
npm install --build-from-source
cd test/bench
npm install
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(/[^A-Za-z0-9_/\-,. ]/g, '')
.replace(/\s+/g, ' ')
.substr(0, 140)
.substr(0, 180)
.trim();
const extractKeywords = (str) =>

View File

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

View File

@@ -145,7 +145,9 @@ try {
tmpFileStream
.on('error', function (err) {
// Clean up temporary file
fs.unlinkSync(tarPathTemp);
try {
fs.unlinkSync(tarPathTemp);
} catch (e) {}
fail(err);
})
.on('close', function () {
@@ -157,7 +159,7 @@ try {
fs.copyFileSync(tarPathTemp, tarPathCache);
fs.unlinkSync(tarPathTemp);
}
extractTarball(tarPathCache);
extractTarball(tarPathCache, platformAndArch);
});
}
});

View File

@@ -30,21 +30,39 @@ function removeAlpha () {
}
/**
* Ensure alpha channel, if missing. The added alpha channel will be fully opaque. This is a no-op if the image already has an alpha channel.
* Ensure the output image has an alpha transparency channel.
* If missing, the added alpha channel will have the specified
* transparency level, defaulting to fully-opaque (1).
* This is a no-op if the image already has an alpha channel.
*
* @since 0.21.2
*
* @example
* sharp('rgb.jpg')
* // rgba.png will be a 4 channel image with a fully-opaque alpha channel
* await sharp('rgb.jpg')
* .ensureAlpha()
* .toFile('rgba.png', function(err, info) {
* // rgba.png is a 4 channel image with a fully opaque alpha channel
* });
* .toFile('rgba.png')
*
* @example
* // rgba is a 4 channel image with a fully-transparent alpha channel
* const rgba = await sharp(rgb)
* .ensureAlpha(0)
* .toBuffer();
*
* @param {number} [alpha=1] - alpha transparency level (0=fully-transparent, 1=fully-opaque)
* @returns {Sharp}
* @throws {Error} Invalid alpha transparency level
*/
function ensureAlpha () {
this.options.ensureAlpha = true;
function ensureAlpha (alpha) {
if (is.defined(alpha)) {
if (is.number(alpha) && is.inRange(alpha, 0, 1)) {
this.options.ensureAlpha = alpha;
} else {
throw is.invalidParameterError('alpha', 'number between 0 and 1', alpha);
}
} else {
this.options.ensureAlpha = 1;
}
return this;
}

View File

@@ -132,6 +132,7 @@ const debuglog = util.debuglog('sharp');
* @param {number} [options.density=72] - number representing the DPI for vector images in the range 1 to 100000.
* @param {number} [options.pages=1] - number of pages to extract for multi-page input (GIF, WebP, AVIF, TIFF, PDF), use -1 for all pages.
* @param {number} [options.page=0] - page number to start extracting from for multi-page input (GIF, WebP, AVIF, TIFF, PDF), zero based.
* @param {number} [options.subifd=-1] - subIFD (Sub Image File Directory) to extract for OME-TIFF, defaults to main image.
* @param {number} [options.level=0] - level to extract from a multi-level input (OpenSlide), zero based.
* @param {boolean} [options.animated=false] - Set to `true` to read all frames/pages of an animated image (equivalent of setting `pages` to `-1`).
* @param {Object} [options.raw] - describes raw pixel input image data. See `raw()` for pixel ordering.
@@ -221,7 +222,7 @@ const Sharp = function (input, options) {
joinChannelIn: [],
extractChannel: -1,
removeAlpha: false,
ensureAlpha: false,
ensureAlpha: -1,
colourspace: 'srgb',
composite: [],
// output
@@ -231,6 +232,7 @@ const Sharp = function (input, options) {
withMetadata: false,
withMetadataOrientation: -1,
withMetadataIcc: '',
withMetadataStrs: {},
resolveWithObject: false,
// output format
jpegQuality: 80,

View File

@@ -9,9 +9,9 @@ const sharp = require('../build/Release/sharp.node');
* @private
*/
function _inputOptionsFromObject (obj) {
const { raw, density, limitInputPixels, sequentialRead, failOnError, animated, page, pages } = obj;
return [raw, density, limitInputPixels, sequentialRead, failOnError, animated, page, pages].some(is.defined)
? { raw, density, limitInputPixels, sequentialRead, failOnError, animated, page, pages }
const { raw, density, limitInputPixels, sequentialRead, failOnError, animated, page, pages, subifd } = obj;
return [raw, density, limitInputPixels, sequentialRead, failOnError, animated, page, pages, subifd].some(is.defined)
? { raw, density, limitInputPixels, sequentialRead, failOnError, animated, page, pages, subifd }
: undefined;
}
@@ -131,6 +131,14 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
throw is.invalidParameterError('level', 'integer between 0 and 256', inputOptions.level);
}
}
// Sub Image File Directory (TIFF)
if (is.defined(inputOptions.subifd)) {
if (is.integer(inputOptions.subifd) && is.inRange(inputOptions.subifd, -1, 100000)) {
inputDescriptor.subifd = inputOptions.subifd;
} else {
throw is.invalidParameterError('subifd', 'integer between -1 and 100000', inputOptions.subifd);
}
}
// Create new image
if (is.defined(inputOptions.create)) {
if (
@@ -255,6 +263,7 @@ function _isStreamInput () {
* - `delay`: Delay in ms between each page in an animated image, provided as an array of integers.
* - `pagePrimary`: Number of the primary page in a HEIF image
* - `levels`: Details of each level in a multi-level image provided as an array of objects, requires libvips compiled with support for OpenSlide
* - `subifds`: Number of Sub Image File Directories in an OME-TIFF image
* - `hasProfile`: Boolean indicating the presence of an embedded ICC profile
* - `hasAlpha`: Boolean indicating the presence of an alpha transparency channel
* - `orientation`: Number value of the EXIF Orientation header, if present

View File

@@ -39,7 +39,7 @@ const cachePath = function () {
const log = function (item) {
if (item instanceof Error) {
console.error(`sharp: ${item.message}`);
console.error(`sharp: Installation error: ${item.message}`);
} else {
console.log(`sharp: ${item}`);
}

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 {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}
@@ -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 {Object} [options]
* @param {Boolean} [options.greyscale=true] - convert to single channel greyscale.

View File

@@ -105,10 +105,10 @@ function toFile (fileOut, callback) {
* .catch(err => { ... });
*
* @example
* const data = await sharp('my-image.jpg')
* const { data, info } = await sharp('my-image.jpg')
* // output the raw pixels
* .raw()
* .toBuffer();
* .toBuffer({ resolveWithObject: true });
*
* // 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
@@ -116,7 +116,9 @@ function toFile (fileOut, callback) {
* const pixelArray = new Uint8ClampedArray(data.buffer);
*
* // 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 {boolean} [options.resolveWithObject] Resolve the Promise with an Object containing `data` and `info` properties instead of resolving only with `data`.
@@ -146,9 +148,22 @@ function toBuffer (options, callback) {
* .toFile('output-with-metadata.jpg')
* .then(info => { ... });
*
* @example
* // Set "IFD0-Copyright" in output EXIF metadata
* await sharp(input)
* .withMetadata({
* exif: {
* IFD0: {
* Copyright: 'Wernham Hogg'
* }
* }
* })
* .toBuffer();
*
* @param {Object} [options]
* @param {number} [options.orientation] value between 1 and 8, used to update the EXIF `Orientation` tag.
* @param {string} [options.icc] filesystem path to output ICC profile, defaults to sRGB.
* @param {Object<Object>} [options.exif={}] Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data.
* @returns {Sharp}
* @throws {Error} Invalid parameters
*/
@@ -169,6 +184,25 @@ function withMetadata (options) {
throw is.invalidParameterError('icc', 'string filesystem path to ICC profile', options.icc);
}
}
if (is.defined(options.exif)) {
if (is.object(options.exif)) {
for (const [ifd, entries] of Object.entries(options.exif)) {
if (is.object(entries)) {
for (const [k, v] of Object.entries(entries)) {
if (is.string(v)) {
this.options.withMetadataStrs[`exif-${ifd.toLowerCase()}-${k}`] = v;
} else {
throw is.invalidParameterError(`exif.${ifd}.${k}`, 'string', v);
}
}
} else {
throw is.invalidParameterError(`exif.${ifd}`, 'object', entries);
}
}
} else {
throw is.invalidParameterError('exif', 'object', options.exif);
}
}
}
return this;
}
@@ -381,6 +415,12 @@ function png (options) {
* .webp({ lossless: true })
* .toBuffer();
*
* @example
* // Optimise the file size of an animated WebP
* const outputWebp = await sharp(inputWebp, { animated: true })
* .webp({ reductionEffort: 6 })
* .toBuffer();
*
* @param {Object} [options] - output options
* @param {number} [options.quality=80] - quality, integer 1-100
* @param {number} [options.alphaQuality=100] - quality of alpha layer, integer 0-100

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} [extend.top]
* @param {number} [extend.left]
* @param {number} [extend.bottom]
* @param {number} [extend.right]
* @param {number} [extend.top=0]
* @param {number} [extend.left=0]
* @param {number} [extend.bottom=0]
* @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.
* @returns {Sharp}
* @throws {Error} Invalid parameters
@@ -317,17 +326,35 @@ function extend (extend) {
this.options.extendBottom = extend;
this.options.extendLeft = extend;
this.options.extendRight = extend;
} else if (
is.object(extend) &&
is.integer(extend.top) && extend.top >= 0 &&
is.integer(extend.bottom) && extend.bottom >= 0 &&
is.integer(extend.left) && extend.left >= 0 &&
is.integer(extend.right) && extend.right >= 0
) {
this.options.extendTop = extend.top;
this.options.extendBottom = extend.bottom;
this.options.extendLeft = extend.left;
this.options.extendRight = extend.right;
} else if (is.object(extend)) {
if (is.defined(extend.top)) {
if (is.integer(extend.top) && extend.top >= 0) {
this.options.extendTop = extend.top;
} else {
throw is.invalidParameterError('top', 'positive integer', extend.top);
}
}
if (is.defined(extend.bottom)) {
if (is.integer(extend.bottom) && extend.bottom >= 0) {
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);
} else {
throw is.invalidParameterError('extend', 'integer or object', extend);

View File

@@ -1,7 +1,7 @@
{
"name": "sharp",
"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.1",
"author": "Lovell Fuller <npm@lovell.info>",
"homepage": "https://github.com/lovell/sharp",
"contributors": [
@@ -80,7 +80,7 @@
"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.*",
"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-coverage": "./test/coverage/report.sh",
"test-leak": "./test/leak/leak.sh",
@@ -120,8 +120,8 @@
"color": "^3.1.3",
"detect-libc": "^1.0.3",
"node-addon-api": "^3.1.0",
"prebuild-install": "^6.0.1",
"semver": "^7.3.4",
"prebuild-install": "^6.1.1",
"semver": "^7.3.5",
"simple-get": "^3.1.0",
"tar-fs": "^2.1.1",
"tunnel-agent": "^0.6.0"
@@ -130,11 +130,11 @@
"async": "^3.2.0",
"cc": "^3.0.1",
"decompress-zip": "^0.3.3",
"documentation": "^13.1.1",
"documentation": "^13.2.0",
"exif-reader": "^1.0.3",
"icc": "^2.0.0",
"license-checker": "^25.0.1",
"mocha": "^8.3.1",
"mocha": "^8.3.2",
"mock-fs": "^4.13.0",
"nyc": "^15.1.0",
"prebuild": "^10.0.1",
@@ -143,7 +143,7 @@
},
"license": "Apache-2.0",
"config": {
"libvips": "8.10.6-alpha3",
"libvips": "8.10.6",
"runtime": "napi",
"target": 3
},

View File

@@ -36,6 +36,9 @@ namespace sharp {
std::string AttrAsStr(Napi::Object obj, std::string attr) {
return obj.Get(attr).As<Napi::String>();
}
std::string AttrAsStr(Napi::Object obj, unsigned int const attr) {
return obj.Get(attr).As<Napi::String>();
}
uint32_t AttrAsUint32(Napi::Object obj, std::string attr) {
return obj.Get(attr).As<Napi::Number>().Uint32Value();
}
@@ -104,6 +107,10 @@ namespace sharp {
if (HasAttr(input, "level")) {
descriptor->level = AttrAsUint32(input, "level");
}
// subIFD (OME-TIFF)
if (HasAttr(input, "subifd")) {
descriptor->subifd = AttrAsInt32(input, "subifd");
}
// Create new image
if (HasAttr(input, "createChannels")) {
descriptor->createChannels = AttrAsUint32(input, "createChannels");
@@ -213,6 +220,8 @@ namespace sharp {
{ "VipsForeignLoadTiffBuffer", ImageType::TIFF },
{ "VipsForeignLoadGifFile", ImageType::GIF },
{ "VipsForeignLoadGifBuffer", ImageType::GIF },
{ "VipsForeignLoadNsgifFile", ImageType::GIF },
{ "VipsForeignLoadNsgifBuffer", ImageType::GIF },
{ "VipsForeignLoadSvgFile", ImageType::SVG },
{ "VipsForeignLoadSvgBuffer", ImageType::SVG },
{ "VipsForeignLoadHeifFile", ImageType::HEIF },
@@ -317,6 +326,9 @@ namespace sharp {
if (imageType == ImageType::OPENSLIDE) {
option->set("level", descriptor->level);
}
if (imageType == ImageType::TIFF) {
option->set("subifd", descriptor->subifd);
}
image = VImage::new_from_buffer(descriptor->buffer, descriptor->bufferLength, nullptr, option);
if (imageType == ImageType::SVG || imageType == ImageType::PDF || imageType == ImageType::MAGICK) {
image = SetDensity(image, descriptor->density);
@@ -389,6 +401,9 @@ namespace sharp {
if (imageType == ImageType::OPENSLIDE) {
option->set("level", descriptor->level);
}
if (imageType == ImageType::TIFF) {
option->set("subifd", descriptor->subifd);
}
image = VImage::new_from_file(descriptor->file.data(), option);
if (imageType == ImageType::SVG || imageType == ImageType::PDF || imageType == ImageType::MAGICK) {
image = SetDensity(image, descriptor->density);
@@ -797,10 +812,10 @@ namespace sharp {
/*
Ensures alpha channel, if missing.
*/
VImage EnsureAlpha(VImage image) {
VImage EnsureAlpha(VImage image, double const value) {
if (!HasAlpha(image)) {
std::vector<double> alpha;
alpha.push_back(sharp::MaximumImageAlpha(image.interpretation()));
alpha.push_back(value * sharp::MaximumImageAlpha(image.interpretation()));
image = image.bandjoin_const(alpha);
}
return image;

View File

@@ -60,6 +60,7 @@ namespace sharp {
int pages;
int page;
int level;
int subifd;
int createChannels;
int createWidth;
int createHeight;
@@ -82,6 +83,7 @@ namespace sharp {
pages(1),
page(0),
level(0),
subifd(-1),
createChannels(0),
createWidth(0),
createHeight(0),
@@ -93,6 +95,7 @@ namespace sharp {
// Convenience methods to access the attributes of a Napi::Object
bool HasAttr(Napi::Object obj, std::string attr);
std::string AttrAsStr(Napi::Object obj, std::string attr);
std::string AttrAsStr(Napi::Object obj, unsigned int const attr);
uint32_t AttrAsUint32(Napi::Object obj, std::string attr);
int32_t AttrAsInt32(Napi::Object obj, std::string attr);
int32_t AttrAsInt32(Napi::Object obj, unsigned int const attr);
@@ -296,7 +299,7 @@ namespace sharp {
/*
Ensures alpha channel, if missing.
*/
VImage EnsureAlpha(VImage image);
VImage EnsureAlpha(VImage image, double const value);
} // namespace sharp

View File

@@ -86,6 +86,9 @@ class MetadataWorker : public Napi::AsyncWorker {
baton->levels.push_back(std::pair<int, int>(width, height));
}
}
if (image.get_typeof(VIPS_META_N_SUBIFDS) == G_TYPE_INT) {
baton->subifds = image.get_int(VIPS_META_N_SUBIFDS);
}
baton->hasProfile = sharp::HasProfile(image);
// Derived attributes
baton->hasAlpha = sharp::HasAlpha(image);
@@ -203,6 +206,9 @@ class MetadataWorker : public Napi::AsyncWorker {
}
info.Set("levels", levels);
}
if (baton->subifds > 0) {
info.Set("subifds", baton->subifds);
}
info.Set("hasProfile", baton->hasProfile);
info.Set("hasAlpha", baton->hasAlpha);
if (baton->orientation > 0) {

View File

@@ -41,6 +41,7 @@ struct MetadataBaton {
int pagePrimary;
std::string compression;
std::vector<std::pair<int, int>> levels;
int subifds;
bool hasProfile;
bool hasAlpha;
int orientation;
@@ -68,6 +69,7 @@ struct MetadataBaton {
pageHeight(0),
loop(-1),
pagePrimary(-1),
subifds(0),
hasProfile(false),
hasAlpha(false),
orientation(0),

View File

@@ -346,7 +346,7 @@ class PipelineWorker : public Napi::AsyncWorker {
bool const shouldModulate = baton->brightness != 1.0 || baton->saturation != 1.0 || baton->hue != 0.0;
if (shouldComposite && !sharp::HasAlpha(image)) {
image = sharp::EnsureAlpha(image);
image = sharp::EnsureAlpha(image, 1);
}
bool const shouldPremultiplyAlpha = sharp::HasAlpha(image) &&
@@ -562,9 +562,17 @@ class PipelineWorker : public Napi::AsyncWorker {
// Use gravity in overlay
if (compositeImage.width() <= baton->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) {
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) {
int left;
@@ -586,7 +594,7 @@ class PipelineWorker : public Napi::AsyncWorker {
// Ensure image to composite is sRGB with premultiplied alpha
compositeImage = compositeImage.colourspace(VIPS_INTERPRETATION_sRGB);
if (!sharp::HasAlpha(compositeImage)) {
compositeImage = sharp::EnsureAlpha(compositeImage);
compositeImage = sharp::EnsureAlpha(compositeImage, 1);
}
if (!composite->premultiplied) compositeImage = compositeImage.premultiply();
// Calculate position
@@ -594,8 +602,13 @@ class PipelineWorker : public Napi::AsyncWorker {
int top;
if (composite->hasOffset) {
// Composite image at given offsets
std::tie(left, top) = sharp::CalculateCrop(image.width(), image.height(),
compositeImage.width(), compositeImage.height(), composite->left, composite->top);
if (composite->tile) {
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 {
// Composite image with given gravity
std::tie(left, top) = sharp::CalculateCrop(image.width(), image.height(),
@@ -678,8 +691,8 @@ class PipelineWorker : public Napi::AsyncWorker {
}
// Ensure alpha channel, if missing
if (baton->ensureAlpha) {
image = sharp::EnsureAlpha(image);
if (baton->ensureAlpha != -1) {
image = sharp::EnsureAlpha(image, baton->ensureAlpha);
}
// Convert image to sRGB, if not already
@@ -704,11 +717,17 @@ class PipelineWorker : public Napi::AsyncWorker {
->set("input_profile", "srgb")
->set("intent", VIPS_INTENT_PERCEPTUAL));
}
// Override EXIF Orientation tag
if (baton->withMetadata && baton->withMetadataOrientation != -1) {
image = sharp::SetExifOrientation(image, baton->withMetadataOrientation);
}
// Metadata key/value pairs, e.g. EXIF
if (!baton->withMetadataStrs.empty()) {
image = image.copy();
for (const auto& s : baton->withMetadataStrs) {
image.set(s.first.data(), s.second.data());
}
}
// Number of channels used in output image
baton->channels = image.bands();
@@ -1328,7 +1347,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
baton->affineInterpolator = vips::VInterpolate::new_from_name(sharp::AttrAsStr(options, "affineInterpolator").data());
baton->removeAlpha = sharp::AttrAsBool(options, "removeAlpha");
baton->ensureAlpha = sharp::AttrAsBool(options, "ensureAlpha");
baton->ensureAlpha = sharp::AttrAsDouble(options, "ensureAlpha");
if (options.Has("boolean")) {
baton->boolean = sharp::CreateInputDescriptor(options.Get("boolean").As<Napi::Object>());
baton->booleanOp = sharp::GetBooleanOperation(sharp::AttrAsStr(options, "booleanOp"));
@@ -1366,6 +1385,12 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
baton->withMetadata = sharp::AttrAsBool(options, "withMetadata");
baton->withMetadataOrientation = sharp::AttrAsUint32(options, "withMetadataOrientation");
baton->withMetadataIcc = sharp::AttrAsStr(options, "withMetadataIcc");
Napi::Object mdStrs = options.Get("withMetadataStrs").As<Napi::Object>();
Napi::Array mdStrKeys = mdStrs.GetPropertyNames();
for (unsigned int i = 0; i < mdStrKeys.Length(); i++) {
std::string k = sharp::AttrAsStr(mdStrKeys, i);
baton->withMetadataStrs.insert(std::make_pair(k, sharp::AttrAsStr(mdStrs, k)));
}
// Format-specific
baton->jpegQuality = sharp::AttrAsUint32(options, "jpegQuality");
baton->jpegProgressive = sharp::AttrAsBool(options, "jpegProgressive");

View File

@@ -18,6 +18,7 @@
#include <memory>
#include <string>
#include <vector>
#include <unordered_map>
#include <napi.h>
#include <vips/vips8>
@@ -168,6 +169,7 @@ struct PipelineBaton {
bool withMetadata;
int withMetadataOrientation;
std::string withMetadataIcc;
std::unordered_map<std::string, std::string> withMetadataStrs;
std::unique_ptr<double[]> convKernel;
int convKernelWidth;
int convKernelHeight;
@@ -178,7 +180,7 @@ struct PipelineBaton {
VipsOperationBoolean bandBoolOp;
int extractChannel;
bool removeAlpha;
bool ensureAlpha;
double ensureAlpha;
VipsInterpretation colourspace;
int pageHeight;
std::vector<int> delay;
@@ -297,7 +299,7 @@ struct PipelineBaton {
bandBoolOp(VIPS_OPERATION_BOOLEAN_LAST),
extractChannel(-1),
removeAlpha(false),
ensureAlpha(false),
ensureAlpha(-1.0),
colourspace(VIPS_INTERPRETATION_LAST),
pageHeight(0),
delay{-1},

View File

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

View File

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

@@ -155,4 +155,27 @@ describe('Alpha transparency', function () {
});
}));
});
it('Valid ensureAlpha value used for alpha channel', async () => {
const background = { r: 255, g: 0, b: 0 };
const [r, g, b, alpha] = await sharp({
create: {
width: 8,
height: 8,
channels: 3,
background
}
})
.ensureAlpha(0.5)
.raw()
.toBuffer();
assert.deepStrictEqual({ r, g, b, alpha }, { ...background, alpha: 127 });
});
it('Invalid ensureAlpha value throws', async () => {
assert.throws(() => {
sharp().ensureAlpha('fail');
});
});
});

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 [slow]', function (done) {
sharp(fixtures.inputTiff)
.webp()
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('webp', info.format);
done();
});
});
}
it('From 1-bit TIFF to sRGB WebP', async () => {
const data = await sharp(fixtures.inputTiff)
.resize(8, 8)
.webp()
.toBuffer();
const { format } = await sharp(data).metadata();
assert.strictEqual(format, 'webp');
});
it('From CMYK to sRGB', function (done) {
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 => {
sharp(fixtures.inputJpg)
.resize(300, 300)
@@ -390,4 +408,20 @@ describe('composite', () => {
}, /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)
.extend({
top: 50,
bottom: 0,
left: 10,
right: 35,
background: { r: 0, g: 0, b: 0, alpha: 0 }
@@ -64,18 +63,38 @@ describe('Extend', function () {
sharp().extend(-1);
});
});
it('partial object fails', function () {
assert.throws(function () {
sharp().extend({ top: 1 });
});
it('invalid top fails', () => {
assert.throws(
() => 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) {
sharp(fixtures.inputJpgWithLandscapeExif1)
.extend({
top: 0,
bottom: 10,
left: 0,
right: 10,
background: { r: 0, g: 0, b: 0, alpha: 0 }
})
@@ -91,9 +110,7 @@ describe('Extend', function () {
it('PNG with 2 channels', function (done) {
sharp(fixtures.inputPngWithGreyAlpha)
.extend({
top: 0,
bottom: 20,
left: 0,
right: 20,
background: 'transparent'
})

View File

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

View File

@@ -718,6 +718,19 @@ describe('Input/output', function () {
sharp({ level: -1 });
}, /Expected integer between 0 and 256 for level but received -1 of type number/);
});
it('Valid subifd property', function () {
sharp({ subifd: 1 });
});
it('Invalid subifd property (string) throws', function () {
assert.throws(function () {
sharp({ subifd: '1' });
}, /Expected integer between -1 and 100000 for subifd but received 1 of type string/);
});
it('Invalid subifd property (float) throws', function () {
assert.throws(function () {
sharp({ subifd: 1.2 });
}, /Expected integer between -1 and 100000 for subifd but received 1.2 of type number/);
});
});
describe('create new image', function () {

View File

@@ -125,7 +125,7 @@ describe('libvips binaries', function () {
it('logs an error message', function (done) {
console.error = function (msg) {
assert.strictEqual(msg, 'sharp: problem');
assert.strictEqual(msg, 'sharp: Installation error: problem');
done();
};
libvips.log(new Error('problem'));

View File

@@ -599,6 +599,30 @@ describe('Image metadata', function () {
});
});
it('Add EXIF metadata to JPEG', async () => {
const data = await sharp({
create: {
width: 8,
height: 8,
channels: 3,
background: 'red'
}
})
.jpeg()
.withMetadata({
exif: {
IFD0: { Software: 'sharp' },
IFD2: { ExposureTime: '0.2' }
}
})
.toBuffer();
const { exif } = await sharp(data).metadata();
const parsedExif = exifReader(exif);
assert.strictEqual(parsedExif.image.Software, 'sharp');
assert.strictEqual(parsedExif.exif.ExposureTime, 0.2);
});
it('chromaSubsampling 4:4:4:4 CMYK JPEG', function () {
return sharp(fixtures.inputJpgWithCmykProfile)
.metadata()
@@ -717,5 +741,20 @@ describe('Image metadata', function () {
sharp().withMetadata({ icc: true });
});
});
it('Non object exif', function () {
assert.throws(function () {
sharp().withMetadata({ exif: false });
});
});
it('Non string value in object exif', function () {
assert.throws(function () {
sharp().withMetadata({ exif: { ifd0: false } });
});
});
it('Non string value in nested object exif', function () {
assert.throws(function () {
sharp().withMetadata({ exif: { ifd0: { fail: false } } });
});
});
});
});

View File

@@ -28,115 +28,138 @@ describe('Modulate', function () {
});
});
it('should be able to hue-rotate', function () {
const base = 'modulate-hue-120.jpg';
const actual = fixtures.path('output.' + base);
const expected = fixtures.expected(base);
return sharp(fixtures.inputJpg)
it('should be able to hue-rotate', async () => {
const [r, g, b] = await sharp({
create: {
width: 1,
height: 1,
channels: 3,
background: { r: 153, g: 68, b: 68 }
}
})
.modulate({ hue: 120 })
.toFile(actual)
.then(function () {
fixtures.assertMaxColourDistance(actual, expected, 25);
});
.raw()
.toBuffer();
assert.deepStrictEqual({ r: 41, g: 107, b: 57 }, { r, g, b });
});
it('should be able to brighten', function () {
const base = 'modulate-brightness-2.jpg';
const actual = fixtures.path('output.' + base);
const expected = fixtures.expected(base);
return sharp(fixtures.inputJpg)
it('should be able to brighten', async () => {
const [r, g, b] = await sharp({
create: {
width: 1,
height: 1,
channels: 3,
background: { r: 153, g: 68, b: 68 }
}
})
.modulate({ brightness: 2 })
.toFile(actual)
.then(function () {
fixtures.assertMaxColourDistance(actual, expected, 25);
});
.raw()
.toBuffer();
assert.deepStrictEqual({ r: 255, g: 173, b: 168 }, { r, g, b });
});
it('should be able to unbrighten', function () {
const base = 'modulate-brightness-0-5.jpg';
const actual = fixtures.path('output.' + base);
const expected = fixtures.expected(base);
return sharp(fixtures.inputJpg)
it('should be able to darken', async () => {
const [r, g, b] = await sharp({
create: {
width: 1,
height: 1,
channels: 3,
background: { r: 153, g: 68, b: 68 }
}
})
.modulate({ brightness: 0.5 })
.toFile(actual)
.then(function () {
fixtures.assertMaxColourDistance(actual, expected, 25);
});
.raw()
.toBuffer();
assert.deepStrictEqual({ r: 97, g: 17, b: 25 }, { r, g, b });
});
it('should be able to saturate', function () {
const base = 'modulate-saturation-2.jpg';
const actual = fixtures.path('output.' + base);
const expected = fixtures.expected(base);
return sharp(fixtures.inputJpg)
it('should be able to saturate', async () => {
const [r, g, b] = await sharp({
create: {
width: 1,
height: 1,
channels: 3,
background: { r: 153, g: 68, b: 68 }
}
})
.modulate({ saturation: 2 })
.toFile(actual)
.then(function () {
fixtures.assertMaxColourDistance(actual, expected, 30);
});
.raw()
.toBuffer();
assert.deepStrictEqual({ r: 198, g: 0, b: 43 }, { r, g, b });
});
it('should be able to desaturate', function () {
const base = 'modulate-saturation-0.5.jpg';
const actual = fixtures.path('output.' + base);
const expected = fixtures.expected(base);
return sharp(fixtures.inputJpg)
it('should be able to desaturate', async () => {
const [r, g, b] = await sharp({
create: {
width: 1,
height: 1,
channels: 3,
background: { r: 153, g: 68, b: 68 }
}
})
.modulate({ saturation: 0.5 })
.toFile(actual)
.then(function () {
fixtures.assertMaxColourDistance(actual, expected, 25);
});
.raw()
.toBuffer();
assert.deepStrictEqual({ r: 127, g: 83, b: 81 }, { r, g, b });
});
it('should be able to modulate all channels', function () {
const base = 'modulate-all.jpg';
const actual = fixtures.path('output.' + base);
const expected = fixtures.expected(base);
return sharp(fixtures.inputJpg)
it('should be able to modulate all channels', async () => {
const [r, g, b] = await sharp({
create: {
width: 1,
height: 1,
channels: 3,
background: { r: 153, g: 68, b: 68 }
}
})
.modulate({ brightness: 2, saturation: 0.5, hue: 180 })
.toFile(actual)
.then(function () {
fixtures.assertMaxColourDistance(actual, expected, 25);
});
.raw()
.toBuffer();
assert.deepStrictEqual({ r: 149, g: 209, b: 214 }, { r, g, b });
});
describe('hue-rotate', function (done) {
[30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330, 360].forEach(function (angle) {
it('should properly hue rotate by ' + angle + 'deg', function () {
const base = 'modulate-hue-angle-' + angle + '.png';
const actual = fixtures.path('output.' + base);
it('should be able to use linear and modulate together', async () => {
const contrast = 1.5;
const brightness = 0.5;
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);
return sharp(fixtures.testPattern)
await sharp(fixtures.testPattern)
.resize(320)
.modulate({ hue: angle })
.png({ compressionLevel: 0 })
.toFile(actual)
.then(function () {
fixtures.assertMaxColourDistance(actual, expected, 25);
.then(() => {
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) {
sharp(fixtures.inputPngWithTransparency)
.resize(8, 8)
.normalize()
.toBuffer(function (err, data) {
if (err) throw err;
@@ -69,6 +70,7 @@ describe('Normalization', function () {
it('keeps the alpha channel of greyscale images intact', function (done) {
sharp(fixtures.inputPngWithGreyAlpha)
.resize(8, 8)
.normalise()
.toBuffer(function (err, data) {
if (err) throw err;

View File

@@ -1,5 +1,6 @@
'use strict';
const fs = require('fs');
const assert = require('assert');
const sharp = require('../../');
@@ -99,4 +100,18 @@ describe('SVG input', function () {
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', () =>
sharp(fixtures.inputJpgWithLandscapeExif1)
.resize(320, 240)
.tiff({ quality: 40 })
.toBuffer()
.then(tiff40 => sharp(fixtures.inputJpgWithLandscapeExif1)
.resize(320, 240)
.tiff({ quality: 90 })
.toBuffer()
.then(tiff90 =>
@@ -155,6 +157,7 @@ describe('TIFF', function () {
it('TIFF setting xres and yres on file', () =>
sharp(fixtures.inputTiff)
.resize(8, 8)
.tiff({
xres: 1000,
yres: 1000
@@ -171,6 +174,7 @@ describe('TIFF', function () {
it('TIFF setting xres and yres on buffer', () =>
sharp(fixtures.inputTiff)
.resize(8, 8)
.tiff({
xres: 1000,
yres: 1000

View File

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