Compare commits

...

63 Commits

Author SHA1 Message Date
Lovell Fuller
b2a0b8c0f0 Release v0.27.0 2020-12-22 11:50:23 +00:00
Lovell Fuller
4debc46d0e Docs: add AVIF to supported formats 2020-12-22 11:47:54 +00:00
Lovell Fuller
f4e259d10f Pre-release v0.27.0-beta1 (prebuild test) 2020-12-21 21:14:15 +00:00
Lovell Fuller
774d78228e Docs: update performance test results 2020-12-21 21:00:44 +00:00
Lovell Fuller
0e62bde5c3 Update (and pin) benchmark module versions 2020-12-21 20:26:57 +00:00
Lovell Fuller
2bbd9b23e6 Add new leak test suppressions (fontconfig, gsf, rsvg) 2020-12-21 11:24:34 +00:00
Manan Jadhav
02676140e8 Allow for negative top/left offsets in composite overlays
A top or left offset value of -1 will no longer mean that the
value is not set, but will now be an actual offset of -1.

INT_MIN for left & top will mean that the values are not set.

Co-authored-by: Christian Flintrup <chr@gigahost.dk>
2020-12-20 17:36:39 +00:00
Lovell Fuller
182beaa4a1 Docs: add note about AVIF images smaller than 16x16 2020-12-20 17:17:49 +00:00
Lovell Fuller
7c08a09529 Add new leak test suppressions (rsvg, vips, heif, aom) 2020-12-20 17:05:37 +00:00
Lovell Fuller
ef964b5472 Ensure all platforms use fontconfig to render #2399 2020-12-20 10:23:26 +00:00
Lovell Fuller
ee54ce9913 Upgrade to stable libvips v8.10.5 prebuild 2020-12-20 09:51:33 +00:00
Lovell Fuller
e59e146887 CI: migrate x64 Linux, macOS and Windows to GitHub Actions 2020-12-18 17:39:11 +00:00
Lovell Fuller
103ec0d58f Upgrade to libvips 8.10.5, AVIF support in prebuilt binaries
Remove experimental status from HEIF, changing defaults
to prefer royalty-free AV1 over patent-encumbered HEVC
2020-12-18 17:32:16 +00:00
Lovell Fuller
a0d89ed514 Add link to documentation on new issue page 2020-12-09 15:01:14 +00:00
Lovell Fuller
c10888e6fe Release v0.26.3 2020-11-16 16:44:18 +00:00
Lovell Fuller
93455f8eb5 Docs: changelog entry for #2412 2020-11-16 15:26:27 +00:00
Matt Kane
65acd96c8d Install: conditionally use Brotli or gzip prebuilt libvips (#2412) 2020-11-16 15:22:13 +00:00
Lovell Fuller
fabe720b9b Docs: keyword search improvements 2020-11-16 15:10:28 +00:00
Lovell Fuller
53dd313e97 CI: force build from source, req of prebuild-install v6+ 2020-11-16 13:55:55 +00:00
Lovell Fuller
2678d7a660 Bump dev dependencies 2020-11-16 12:52:03 +00:00
Lovell Fuller
46718102c6 Docs: changelog for #2336 2020-11-16 12:35:19 +00:00
Guillermo Varela
0f473fe3b1 Expose libvips affine operation (#2336) 2020-11-16 12:27:38 +00:00
Nick Jones
2872602c9e Deps: upgrade to prebuild-install 6.0.0 (#2419) 2020-11-16 12:22:35 +00:00
Lovell Fuller
ab653cae33 Docs: use of N-API removes Electron-specific binaries
Clarify Lambda deployment for Windows/macOS users
2020-11-16 12:20:29 +00:00
Nicolas Stepien
e6a035e575 CI: add Node.js 15 (#2415) 2020-10-23 10:01:29 +01:00
Lovell Fuller
fbe48d75dd Release v0.26.2 2020-10-14 17:59:55 +01:00
Lovell Fuller
20ba0f49dd Changelog entry and doc refresh for #2397 2020-10-08 10:05:39 +01:00
beig
c213e9878d Add centre/center option to tile-based output (#2397) 2020-10-08 09:51:54 +01:00
Lovell Fuller
9704ca4c18 Add support for libvips compiled with OpenEXR #698 2020-10-07 15:12:06 +01:00
Lovell Fuller
49dce6219e Docs: changelog entry for #2379 2020-10-07 14:43:13 +01:00
Adam Coster
260ff6c94f Docs: clarify response object description for trim (#2400) 2020-10-06 17:08:32 +01:00
Adam Lovatt
3ec281d104 Ensure support for yarn v2 (#2379) 2020-09-23 21:41:14 +01:00
Lovell Fuller
c4c43d525b Release v0.26.1 2020-09-20 09:29:50 +01:00
Lovell Fuller
6c5cde363a Ensure animated GIF output is optimised #2376 2020-09-19 20:53:15 +01:00
Lovell Fuller
d46b4d950f Allow spaces in installation directory #2279
Uses new include_dir property of node-addon-api
2020-09-19 14:26:30 +01:00
Lovell Fuller
b369c4bb8a Ensure stats can be calculated for 1x1 input #2372 2020-09-17 11:08:52 +01:00
Lovell Fuller
c3898487c4 CI: name the FreeBSD task 2020-09-17 11:02:16 +01:00
Lovell Fuller
ca3c5b400f CI: install ARM64 nodejs package with dependency on python3-minimal
Node.js 14 will work but 10 and 12 will fail until
https://github.com/nodesource/distributions/issues/1100 is fixed
2020-09-15 19:21:32 +01:00
Lovell Fuller
97772b176c Docs: changelog entry for #2369 2020-09-15 18:05:22 +01:00
AcrylicShrimp
08a2965f1c Ensure animation-related properties work with stream-based input 2020-09-15 15:12:26 +01:00
Lovell Fuller
76dcddfa3d Changelog, credit, doc update for #2348 2020-09-02 09:06:15 +01:00
stefanprobst
79f476ae4d Allow input density range up to 100000 DPI (#2348) 2020-09-02 08:56:12 +01:00
Lovell Fuller
d406cb619c Improve error messaging for unsupported Node.js versions 2020-08-28 21:39:11 +01:00
Lovell Fuller
4f3890f1e4 Issue template: ask about installation architecture and platform 2020-08-28 13:59:44 +01:00
Lovell Fuller
7a9d58cc51 Docs: changelog entry for #2343 2020-08-28 13:56:35 +01:00
Lovell Fuller
eef87da0e1 Docs: CSP allow inline JS 2020-08-28 13:53:19 +01:00
Denis Soldatov
00e65f6f14 Ensure correct pageHeight when verifying image dimensions (#2343) 2020-08-28 13:39:19 +01:00
Lovell Fuller
866e9824d1 Release v0.26.0 2020-08-25 18:42:06 +01:00
Lovell Fuller
482e6078e2 Tests: Update leak suppressions for librsvg static data 2020-08-25 18:40:12 +01:00
Lovell Fuller
bc7ab296ef Tests: Update leak suppressions for static libs 2020-08-24 21:10:20 +01:00
Lovell Fuller
a5f4f53b56 Tests: remove no-longer-used conditional assertions 2020-08-23 20:11:23 +01:00
Lovell Fuller
b1227f526d Verify minimum libvips version at compile time 2020-08-23 20:10:27 +01:00
Lovell Fuller
78b42c8306 Docs: update performance test results 2020-08-23 16:48:46 +01:00
Lovell Fuller
4b6d45ab8e Prerelease v0.26.0-beta1 2020-08-23 15:56:25 +01:00
Lovell Fuller
7980341923 Upgrade libvips to v8.10.0 2020-08-23 15:36:03 +01:00
Lovell Fuller
3917efdebd Benchmarks: ensure PNG tests use consistent settings 2020-08-21 21:10:27 +01:00
Robert O'Rourke
eaecb7347b Add support to withMetadata for custom ICC profile #2271 2020-08-19 21:32:15 +01:00
Lovell Fuller
05ca7d3129 CI: further attempts to get Windows to play nicely 2020-08-18 20:53:18 +01:00
Lovell Fuller
4beae0de71 Add 'animated' constructor property as shortcut for 'pages'
Provides easier-to-understand API when handling animated images
2020-08-18 20:28:35 +01:00
Lovell Fuller
b711661784 CI: improve cross-platform (i.e. Windows) process spawning 2020-08-18 17:11:42 +01:00
Lovell Fuller
0c4a45b1f4 CI: workaround Appveyor ignoring v-prefixed tag names 2020-08-18 14:31:33 +01:00
Lovell Fuller
ec2beb0039 Prerelease v0.26.0-alpha2 2020-08-18 14:01:19 +01:00
Lovell Fuller
cafb3e8bac CI: ensure git tag is available inside prebuild containers 2020-08-18 14:00:14 +01:00
82 changed files with 1677 additions and 487 deletions

View File

@@ -2,6 +2,7 @@ freebsd_instance:
image_family: freebsd-13-0-snap
task:
name: FreeBSD 13.0
env:
IGNORE_OSVERSION: yes
prerequisites_script:
@@ -9,6 +10,6 @@ task:
- pkg upgrade -y
- pkg install -y pkgconf vips node npm
install_script:
- npm install --unsafe-perm
- npm install --build-from-source --unsafe-perm
test_script:
- npm test

View File

@@ -42,10 +42,6 @@ You deserve to add your details to the [list of contributors](https://github.com
Any change that modifies the existing public API should be added to the relevant work-in-progress branch for inclusion in the next major release.
| Release | WIP branch |
| ------: | :--------- |
| v0.26.0 | zoom |
Please squash your changes into a single commit using a command like `git rebase -i upstream/<wip-branch>`.
### Add a new public method

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Documentation
url: https://sharp.pixelplumbing.com/
about: Installation instructions, complete API documentation with examples, changelog

View File

@@ -7,7 +7,7 @@ labels: installation
Did you see the [documentation relating to installation](https://sharp.pixelplumbing.com/install)?
Have you ensured the platform and version of Node.js used for `npm install` is the same as the platform and version of Node.js used at runtime?
Have you ensured the architecture and platform of Node.js used for `npm install` is the same as the architecture and platform of Node.js used at runtime?
Are you using the latest version? Is the version currently in use as reported by `npm ls sharp` the same as the latest version as reported by `npm view sharp dist-tags.latest`?

85
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,85 @@
on:
- push
- pull_request
jobs:
CI:
runs-on: ${{ matrix.os }}
container: ${{ matrix.container }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-20.04
container: centos:7
nodejs_version: 10
coverage: true
prebuild: true
- os: ubuntu-20.04
container: centos:7
nodejs_version: 12
- os: ubuntu-20.04
container: centos:7
nodejs_version: 14
- os: ubuntu-20.04
container: centos:7
nodejs_version: 15
- os: ubuntu-20.04
container: node:10-alpine3.11
prebuild: true
- os: ubuntu-20.04
container: node:12-alpine3.11
- os: ubuntu-20.04
container: node:14-alpine3.11
- os: ubuntu-20.04
container: node:15-alpine3.11
- os: macos-10.15
nodejs_version: 10
prebuild: true
- os: macos-10.15
nodejs_version: 12
- os: macos-10.15
nodejs_version: 14
- os: macos-10.15
nodejs_version: 15
- os: windows-2019
nodejs_version: 10
prebuild: true
- os: windows-2019
nodejs_version: 12
- os: windows-2019
nodejs_version: 14
- os: windows-2019
nodejs_version: 15
steps:
- name: Dependencies (Linux glibc)
if: contains(matrix.container, 'centos')
run: |
curl -sL https://rpm.nodesource.com/setup_${{ matrix.nodejs_version }}.x | bash -
yum install -y gcc-c++ make git python3 nodejs
- name: Dependencies (Linux musl)
if: contains(matrix.container, 'alpine')
run: apk add build-base git python3 --update-cache
- name: Dependencies (macOS, Windows)
if: contains(matrix.os, 'macos') || contains(matrix.os, 'windows')
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.nodejs_version }}
- name: Checkout
uses: actions/checkout@v2
- name: Fix working directory ownership
if: matrix.container
run: chown root.root .
- name: Install
run: npm install --build-from-source --unsafe-perm
- name: Test
run: npm test
- name: Coverage
if: matrix.coverage
uses: coverallsapp/github-action@v1.1.2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Prebuild
if: matrix.prebuild && startsWith(github.ref, 'refs/tags/')
env:
prebuild_upload: ${{ secrets.GITHUB_TOKEN }}
run: npx prebuild --runtime napi --target 3

View File

@@ -1,81 +1,19 @@
jobs:
include:
- name: "Linux x64 (CentOS 7, glibc 2.17) - Node.js 10"
os: linux
dist: bionic
language: shell
before_install:
- sudo docker run -dit --name sharp --env CI --env prebuild_upload --volume "${PWD}:/mnt/sharp" --workdir /mnt/sharp centos:7
- sudo docker exec sharp bash -c "curl -sL https://rpm.nodesource.com/setup_10.x | bash -"
- sudo docker exec sharp yum install -y gcc-c++ make git nodejs
install: sudo docker exec sharp bash -c "npm install --unsafe-perm"
script: sudo docker exec sharp bash -c "npm test"
- name: "Linux x64 (CentOS 7, glibc 2.17) - Node.js 12"
os: linux
dist: bionic
language: shell
before_install:
- sudo docker run -dit --name sharp --env CI --volume "${PWD}:/mnt/sharp" --workdir /mnt/sharp centos:7
- sudo docker exec sharp bash -c "curl -sL https://rpm.nodesource.com/setup_12.x | bash -"
- sudo docker exec sharp yum install -y gcc-c++ make git nodejs
install: sudo docker exec sharp bash -c "npm install --unsafe-perm"
script: sudo docker exec sharp bash -c "npm test"
- name: "Linux x64 (CentOS 7, glibc 2.17) - Node.js 14"
os: linux
dist: bionic
language: shell
before_install:
- sudo docker run -dit --name sharp --env CI --volume "${PWD}:/mnt/sharp" --workdir /mnt/sharp centos:7
- sudo docker exec sharp bash -c "curl -sL https://rpm.nodesource.com/setup_14.x | bash -"
- sudo docker exec sharp yum install -y gcc-c++ make git nodejs
install: sudo docker exec sharp bash -c "npm install --unsafe-perm"
script: sudo docker exec sharp bash -c "npm test"
- name: "Linux x64 (Alpine 3.9, musl 1.1.20) - Node.js 10"
os: linux
dist: bionic
language: shell
before_install:
- sudo docker run -dit --name sharp --env CI --env prebuild_upload --volume "${PWD}:/mnt/sharp" --workdir /mnt/sharp node:10.17.0-alpine3.9 # https://github.com/nodejs/docker-node/issues/1158
- sudo docker exec sharp apk add build-base git python2 --update-cache
install: sudo docker exec sharp sh -c "npm install --unsafe-perm"
script: sudo docker exec sharp sh -c "npm test"
- name: "Linux x64 (Alpine 3.11, musl 1.1.20) - Node.js 12"
os: linux
dist: bionic
language: shell
before_install:
- sudo docker run -dit --name sharp --env CI --volume "${PWD}:/mnt/sharp" --workdir /mnt/sharp node:12.0-alpine
- sudo docker exec sharp apk add build-base git python2 --update-cache
install: sudo docker exec sharp sh -c "npm install --unsafe-perm"
script: sudo docker exec sharp sh -c "npm test"
- name: "Linux x64 (Alpine 3.11, musl 1.1.20) - Node.js 14"
os: linux
dist: bionic
language: shell
before_install:
- sudo docker run -dit --name sharp --env CI --volume "${PWD}:/mnt/sharp" --workdir /mnt/sharp node:14.0-alpine
- sudo docker exec sharp apk add build-base git python2 --update-cache
install: sudo docker exec sharp sh -c "npm install --unsafe-perm"
script: sudo docker exec sharp sh -c "npm test"
- name: "Linux ARM64v8 (Debian 11, glibc 2.29) - Node.js 10"
arch: arm64
os: linux
dist: bionic
language: shell
before_install:
- sudo docker run -dit --name sharp --env CI --env prebuild_upload --volume "${PWD}:/mnt/sharp" --workdir /mnt/sharp arm64v8/debian:bullseye
- sudo docker exec sharp sh -c "apt-get update && apt-get install -y build-essential git python2 curl"
- sudo docker run -dit --name sharp --volume "${PWD}:/mnt/sharp" --workdir /mnt/sharp arm64v8/debian:bullseye
- sudo docker exec sharp sh -c "apt-get update && apt-get install -y build-essential git python3 curl"
- sudo docker exec sharp sh -c "curl -s https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add -"
- sudo docker exec sharp sh -c "echo 'deb https://deb.nodesource.com/node_10.x buster main' >/etc/apt/sources.list.d/nodesource.list"
- sudo docker exec sharp sh -c "echo 'deb https://deb.nodesource.com/node_10.x sid main' >/etc/apt/sources.list.d/nodesource.list"
- sudo docker exec sharp sh -c "apt-get update && apt-get install -y nodejs=10.*"
install: sudo docker exec sharp sh -c "npm install --unsafe-perm"
install: sudo docker exec sharp sh -c "npm install --build-from-source --unsafe-perm"
script: sudo docker exec sharp sh -c "npm test"
after_success: "[[ -n $TRAVIS_TAG ]] && sudo docker exec --env prebuild_upload sharp sh -c \"npx prebuild --runtime napi --target 3\""
- name: "Linux ARM64v8 (Debian 11, glibc 2.29) - Node.js 12"
arch: arm64
@@ -83,12 +21,12 @@ jobs:
dist: bionic
language: shell
before_install:
- sudo docker run -dit --name sharp --env CI --volume "${PWD}:/mnt/sharp" --workdir /mnt/sharp arm64v8/debian:bullseye
- sudo docker exec sharp sh -c "apt-get update && apt-get install -y build-essential git python2 curl"
- sudo docker run -dit --name sharp --volume "${PWD}:/mnt/sharp" --workdir /mnt/sharp arm64v8/debian:bullseye
- sudo docker exec sharp sh -c "apt-get update && apt-get install -y build-essential git python3 curl"
- sudo docker exec sharp sh -c "curl -s https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add -"
- sudo docker exec sharp sh -c "echo 'deb https://deb.nodesource.com/node_12.x buster main' >/etc/apt/sources.list.d/nodesource.list"
- sudo docker exec sharp sh -c "echo 'deb https://deb.nodesource.com/node_12.x sid main' >/etc/apt/sources.list.d/nodesource.list"
- sudo docker exec sharp sh -c "apt-get update && apt-get install -y nodejs"
install: sudo docker exec sharp sh -c "npm install --unsafe-perm"
install: sudo docker exec sharp sh -c "npm install --build-from-source --unsafe-perm"
script: sudo docker exec sharp sh -c "npm test"
- name: "Linux ARM64v8 (Debian 11, glibc 2.29) - Node.js 14"
@@ -97,43 +35,28 @@ jobs:
dist: bionic
language: shell
before_install:
- sudo docker run -dit --name sharp --env CI --volume "${PWD}:/mnt/sharp" --workdir /mnt/sharp arm64v8/debian:bullseye
- sudo docker exec sharp sh -c "apt-get update && apt-get install -y build-essential git python2 curl"
- sudo docker run -dit --name sharp --volume "${PWD}:/mnt/sharp" --workdir /mnt/sharp arm64v8/debian:bullseye
- sudo docker exec sharp sh -c "apt-get update && apt-get install -y build-essential git python3 curl"
- sudo docker exec sharp sh -c "curl -s https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add -"
- sudo docker exec sharp sh -c "echo 'deb https://deb.nodesource.com/node_14.x buster main' >/etc/apt/sources.list.d/nodesource.list"
- sudo docker exec sharp sh -c "echo 'deb https://deb.nodesource.com/node_14.x sid main' >/etc/apt/sources.list.d/nodesource.list"
- sudo docker exec sharp sh -c "apt-get update && apt-get install -y nodejs"
install: sudo docker exec sharp sh -c "npm install --unsafe-perm"
install: sudo docker exec sharp sh -c "npm install --build-from-source --unsafe-perm"
script: sudo docker exec sharp sh -c "npm test"
- name: "macOS (10.13) - Node.js 10"
os: osx
osx_image: xcode10.1
language: node_js
node_js: "10"
- name: "macOS (10.13) - Node.js 12"
os: osx
osx_image: xcode10.1
language: node_js
node_js: "12"
before_install: unset prebuild_upload
- name: "macOS (10.13) - Node.js 14"
os: osx
osx_image: xcode10.1
language: node_js
node_js: "14"
before_install: unset prebuild_upload
- name: "Unit test coverage report"
- name: "Linux ARM64v8 (Debian 11, glibc 2.29) - Node.js 15"
arch: arm64
os: linux
dist: bionic
language: node_js
node_js: "13"
before_install: unset prebuild_upload
after_success:
- npm install coveralls
- cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js
language: shell
before_install:
- sudo chown 0.0 ${PWD}
- sudo docker run -dit --name sharp --volume "${PWD}:/mnt/sharp" --workdir /mnt/sharp arm64v8/debian:bullseye
- sudo docker exec sharp sh -c "apt-get update && apt-get install -y build-essential git python3 curl"
- sudo docker exec sharp sh -c "curl -s https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add -"
- sudo docker exec sharp sh -c "echo 'deb https://deb.nodesource.com/node_15.x sid main' >/etc/apt/sources.list.d/nodesource.list"
- sudo docker exec sharp sh -c "apt-get update && apt-get install -y nodejs"
install: sudo docker exec sharp sh -c "npm install --build-from-source --unsafe-perm"
script: sudo docker exec sharp sh -c "npm test"
cache:
npm: false

View File

@@ -4,7 +4,7 @@
The typical use case for this high speed Node.js module
is to convert large images in common formats to
smaller, web-friendly JPEG, PNG and WebP images of varying dimensions.
smaller, web-friendly JPEG, PNG, WebP and AVIF images of varying dimensions.
Resizing an image is typically 4x-5x faster than using the
quickest ImageMagick and GraphicsMagick settings
@@ -16,7 +16,7 @@ Lanczos resampling ensures quality is not sacrificed for speed.
As well as image resizing, operations such as
rotation, extraction, compositing and gamma correction are available.
Most modern macOS, Windows and Linux systems running Node.js v10.16.0+
Most modern macOS, Windows and Linux systems running Node.js v10+
do not require any additional install or runtime dependencies.
## Examples

View File

@@ -1,26 +1,18 @@
os: Visual Studio 2017
os: Visual Studio 2019
version: "{build}"
build: off
platform: x86
environment:
matrix:
- nodejs_version: "10"
platform: x86
- nodejs_version: "10"
platform: x64
prebuild: true
- nodejs_version: "12"
platform: x86
prebuild_upload: ""
- nodejs_version: "12"
platform: x64
prebuild_upload: ""
- nodejs_version: "14.2.0"
platform: x86
prebuild_upload: ""
- nodejs_version: "14"
platform: x64
prebuild_upload: ""
- nodejs_version: "15"
install:
- ps: Update-NodeJsInstallation (Get-NodeJsLatestBuild $env:nodejs_version) $env:platform
- npm install
- ps: Update-NodeJsInstallation (Get-NodeJsLatestBuild $env:nodejs_version)
- npm install --build-from-source
test_script:
- npm test
on_success:
- if [%prebuild%] == [true] if [%APPVEYOR_REPO_TAG%] == [true] npx prebuild --runtime napi --target 3

View File

@@ -95,7 +95,7 @@
'src/sharp.cc'
],
'include_dirs': [
'<!@(node -p "require(\'node-addon-api\').include")',
'<!(node -p "require(\'node-addon-api\').include_dir")',
],
'conditions': [
['use_global_libvips == "true"', {

View File

@@ -4,7 +4,7 @@
The typical use case for this high speed Node.js module
is to convert large images in common formats to
smaller, web-friendly JPEG, PNG and WebP images of varying dimensions.
smaller, web-friendly JPEG, PNG, AVIF and WebP images of varying dimensions.
Resizing an image is typically 4x-5x faster than using the
quickest ImageMagick and GraphicsMagick settings
@@ -16,14 +16,14 @@ Lanczos resampling ensures quality is not sacrificed for speed.
As well as image resizing, operations such as
rotation, extraction, compositing and gamma correction are available.
Most modern macOS, Windows and Linux systems running Node.js v10.16.0+
Most modern macOS, Windows and Linux systems running Node.js v10+
do not require any additional install or runtime dependencies.
### Formats
This module supports reading JPEG, PNG, WebP, TIFF, GIF and SVG images.
This module supports reading JPEG, PNG, WebP, AVIF, TIFF, GIF and SVG images.
Output images can be in JPEG, PNG, WebP and TIFF formats as well as uncompressed raw pixel data.
Output images can be in JPEG, PNG, WebP, AVIF and TIFF formats as well as uncompressed raw pixel data.
Streams, Buffer objects and the filesystem can be used for input and output.

View File

@@ -70,7 +70,7 @@ Channel ordering follows vips convention:
- sRGB: 0: Red, 1: Green, 2: Blue, 3: Alpha.
- CMYK: 0: Magenta, 1: Cyan, 2: Yellow, 3: Black, 4: Alpha.
Buffers may be any of the image formats supported by sharp: JPEG, PNG, WebP, GIF, SVG, TIFF or raw pixel image data.
Buffers may be any of the image formats supported by sharp.
For raw pixel input, the `options` object should contain a `raw` attribute, which follows the format of the attribute of the same name in the `sharp()` constructor.
### Parameters

View File

@@ -4,7 +4,7 @@
Constructor factory to create an instance of `sharp`, to which further methods are chained.
JPEG, PNG, WebP or TIFF format image data can be streamed out from this object.
JPEG, PNG, WebP, AVIF or TIFF format image data can be streamed out from this object.
When using Stream based output, derived attributes are available from the `info` event.
Non-critical problems encountered during processing are emitted as `warning` events.
@@ -14,9 +14,9 @@ Implements the [stream.Duplex][1] class.
### Parameters
- `input` **([Buffer][2] \| [string][3])?** if present, can be
a Buffer containing JPEG, PNG, WebP, GIF, SVG, TIFF or raw pixel image data, or
a String containing the filesystem path to an JPEG, PNG, WebP, GIF, SVG or TIFF image file.
JPEG, PNG, WebP, GIF, SVG, TIFF or raw pixel image data can be streamed into the object when not present.
a Buffer containing JPEG, PNG, WebP, AVIF, GIF, SVG, TIFF or raw pixel image data, or
a String containing the filesystem path to an JPEG, PNG, WebP, AVIF, GIF, SVG or TIFF image file.
JPEG, PNG, WebP, AVIF, GIF, SVG, TIFF or raw pixel image data can be streamed into the object when not present.
- `options` **[Object][4]?** if present, is an Object with optional attributes.
- `options.failOnError` **[boolean][5]** by default halt processing and raise an error when loading invalid images.
Set this flag to `false` if you'd rather apply a "best effort" to decode images, even if the data is corrupt or invalid. (optional, default `true`)
@@ -25,10 +25,11 @@ Implements the [stream.Duplex][1] class.
An integral Number of pixels, zero or false to remove limit, true to use default limit of 268402689 (0x3FFF x 0x3FFF). (optional, default `268402689`)
- `options.sequentialRead` **[boolean][5]** Set this to `true` to use sequential rather than random access where possible.
This can reduce memory usage and might improve performance on some systems. (optional, default `false`)
- `options.density` **[number][6]** number representing the DPI for vector images. (optional, default `72`)
- `options.pages` **[number][6]** number of pages to extract for multi-page input (GIF, TIFF, PDF), use -1 for all pages. (optional, default `1`)
- `options.page` **[number][6]** page number to start extracting from for multi-page input (GIF, TIFF, PDF), zero based. (optional, default `0`)
- `options.density` **[number][6]** number representing the DPI for vector images in the range 1 to 100000. (optional, default `72`)
- `options.pages` **[number][6]** 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][6]** page number to start extracting from for multi-page input (GIF, WebP, AVIF, TIFF, PDF), zero based. (optional, default `0`)
- `options.level` **[number][6]** level to extract from a multi-level input (OpenSlide), zero based. (optional, default `0`)
- `options.animated` **[boolean][5]** Set to `true` to read all frames/pages of an animated image (equivalent of setting `pages` to `-1`). (optional, default `false`)
- `options.raw` **[Object][4]?** describes raw pixel input image data. See `raw()` for pixel ordering.
- `options.raw.width` **[number][6]?**
- `options.raw.height` **[number][6]?**
@@ -78,6 +79,11 @@ sharp({
.then( ... );
```
```javascript
// Convert an animated GIF to an animated WebP
await sharp('in.gif', { animated: true }).toFile('out.webp');
```
- Throws **[Error][8]** Invalid parameters
Returns **[Sharp][9]**

View File

@@ -65,6 +65,61 @@ The use of `flop` implies the removal of the EXIF `Orientation` tag, if any.
Returns **Sharp**
## affine
Perform an affine transform on an image. This operation will always occur after resizing, extraction and rotation, if any.
You must provide an array of length 4 or a 2x2 affine transformation matrix.
By default, new pixels are filled with a black background. You can provide a background color with the `background` option.
A particular interpolator may also be specified. Set the `interpolator` option to an attribute of the `sharp.interpolator` Object e.g. `sharp.interpolator.nohalo`.
In the case of a 2x2 matrix, the transform is:
- X = `matrix[0, 0]` \* (x + `idx`) + `matrix[0, 1]` \* (y + `idy`) + `odx`
- Y = `matrix[1, 0]` \* (x + `idx`) + `matrix[1, 1]` \* (y + `idy`) + `ody`
where:
- x and y are the coordinates in input image.
- X and Y are the coordinates in output image.
- (0,0) is the upper left corner.
### Parameters
- `matrix` **([Array][7]&lt;[Array][7]&lt;[number][1]>> | [Array][7]&lt;[number][1]>)** affine transformation matrix
- `options` **[Object][2]?** if present, is an Object with optional attributes.
- `options.background` **([String][3] \| [Object][2])** parsed by the [color][4] module to extract values for red, green, blue and alpha. (optional, default `"#000000"`)
- `options.idx` **[Number][1]** input horizontal offset (optional, default `0`)
- `options.idy` **[Number][1]** input vertical offset (optional, default `0`)
- `options.odx` **[Number][1]** output horizontal offset (optional, default `0`)
- `options.ody` **[Number][1]** output vertical offset (optional, default `0`)
- `options.interpolator` **[String][3]** interpolator (optional, default `sharp.interpolators.bicubic`)
### Examples
```javascript
const pipeline = sharp()
.affine([[1, 0.3], [0.1, 0.7]], {
background: 'white',
interpolate: sharp.interpolators.nohalo
})
.toBuffer((err, outputBuffer, info) => {
// outputBuffer contains the transformed image
// info.width and info.height contain the new dimensions
});
inputStream
.pipe(pipeline);
```
- Throws **[Error][5]** Invalid parameters
Returns **Sharp**
**Meta**
- **since**: 0.27.0
## sharpen
Sharpen the image.

View File

@@ -5,7 +5,7 @@
Write output image data to a file.
If an explicit output format is not selected, it will be inferred from the extension,
with JPEG, PNG, WebP, TIFF, DZI, and libvips' V format supported.
with JPEG, PNG, WebP, AVIF, TIFF, DZI, and libvips' V format supported.
Note that raw pixel data is only supported for buffer output.
By default all metadata will be removed, which includes EXIF-based orientation.
@@ -42,7 +42,7 @@ Returns **[Promise][5]&lt;[Object][6]>** when no callback is provided
## toBuffer
Write output to a Buffer.
JPEG, PNG, WebP, TIFF and RAW output are supported.
JPEG, PNG, WebP, AVIF, TIFF and raw pixel data output are supported.
If no explicit format is set, the output format will match the input image, except GIF and SVG input which become PNG output.
@@ -91,7 +91,8 @@ Returns **[Promise][5]&lt;[Buffer][8]>** when no callback is provided
## withMetadata
Include all metadata (EXIF, XMP, IPTC) from the input image in the output image.
This will also convert to and add a web-friendly sRGB ICC profile.
This will also convert to and add a web-friendly sRGB ICC profile unless a custom
output profile is provided.
The default behaviour, when `withMetadata` is not used, is to convert to the device-independent
sRGB colour space and strip all metadata, including the removal of any ICC profile.
@@ -100,6 +101,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.
### Examples
@@ -297,23 +299,43 @@ sharp('input.svg')
Returns **Sharp**
## heif
## avif
Use these HEIF options for output image.
Use these AVIF options for output image.
Support for HEIF (HEIC/AVIF) is experimental.
Do not use this in production systems.
Requires a custom, globally-installed libvips compiled with support for libheif.
Most versions of libheif support only the patent-encumbered HEVC compression format.
Whilst it is possible to create AVIF images smaller than 16x16 pixels,
most web browsers do not display these properly.
### Parameters
- `options` **[Object][6]?** output options
- `options.quality` **[number][9]** quality, integer 1-100 (optional, default `80`)
- `options.compression` **[boolean][7]** compression format: hevc, avc, jpeg, av1 (optional, default `'hevc'`)
- `options.quality` **[number][9]** quality, integer 1-100 (optional, default `50`)
- `options.lossless` **[boolean][7]** use lossless compression (optional, default `false`)
- `options.speed` **[boolean][7]** CPU effort vs file size, 0 (slowest/smallest) to 8 (fastest/largest) (optional, default `5`)
- Throws **[Error][4]** Invalid options
Returns **Sharp**
**Meta**
- **since**: 0.27.0
## heif
Use these HEIF options for output image.
Support for patent-encumbered HEIC images requires the use of a
globally-installed libvips compiled with support for libheif, libde265 and x265.
### Parameters
- `options` **[Object][6]?** output options
- `options.quality` **[number][9]** quality, integer 1-100 (optional, default `50`)
- `options.compression` **[boolean][7]** compression format: av1, hevc (optional, default `'av1'`)
- `options.lossless` **[boolean][7]** use lossless compression (optional, default `false`)
- `options.speed` **[boolean][7]** CPU effort vs file size, 0 (slowest/smallest) to 8 (fastest/largest) (optional, default `5`)
- Throws **[Error][4]** Invalid options
@@ -370,6 +392,8 @@ Warning: multiple sharp instances concurrently producing tile output can expose
- `options.skipBlanks` **[number][9]** threshold to skip tile generation, a value 0 - 255 for 8-bit images or 0 - 65535 for 16-bit images (optional, default `-1`)
- `options.container` **[string][2]** tile container, with value `fs` (filesystem) or `zip` (compressed file). (optional, default `'fs'`)
- `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`)
### Examples

View File

@@ -209,7 +209,8 @@ Returns **Sharp**
Trim "boring" pixels from all edges that contain values similar to the top-left pixel.
Images consisting entirely of a single colour will calculate "boring" using the alpha channel, if any.
The `info` response Object will contain `trimOffsetLeft` and `trimOffsetTop` properties.
The `info` response Object, obtained from callback of `.toFile()` or `.toBuffer()`,
will contain `trimOffsetLeft` and `trimOffsetTop` properties.
### Parameters

View File

@@ -12,6 +12,36 @@ console.log(sharp.format);
Returns **[Object][1]**
## interpolators
An Object containing the available interpolators and their proper values
Type: [string][2]
### nearest
[Nearest neighbour interpolation][3]. Suitable for image enlargement only.
### bilinear
[Bilinear interpolation][4]. Faster than bicubic but with less smooth results.
### bicubic
[Bicubic interpolation][5] (the default).
### locallyBoundedBicubic
[LBB interpolation][6]. Prevents some "[acutance][7]" but typically reduces performance by a factor of 2.
### nohalo
[Nohalo interpolation][8]. Prevents acutance but typically reduces performance by a factor of 3.
### vertexSplitQuadraticBasisSpline
[VSQBS interpolation][9]. Prevents "staircasing" when enlarging.
## versions
An Object containing the version numbers of libvips and its dependencies.
@@ -31,10 +61,10 @@ useful for determining how much working memory is required for a particular task
### Parameters
- `options` **([Object][1] \| [boolean][2])** Object with the following attributes, or boolean where true uses default cache settings and false removes all caching (optional, default `true`)
- `options.memory` **[number][3]** is the maximum memory in MB to use for this cache (optional, default `50`)
- `options.files` **[number][3]** is the maximum number of files to hold open (optional, default `20`)
- `options.items` **[number][3]** is the maximum number of operations to cache (optional, default `100`)
- `options` **([Object][1] \| [boolean][10])** Object with the following attributes, or boolean where true uses default cache settings and false removes all caching (optional, default `true`)
- `options.memory` **[number][11]** is the maximum memory in MB to use for this cache (optional, default `50`)
- `options.files` **[number][11]** is the maximum number of files to hold open (optional, default `20`)
- `options.items` **[number][11]** is the maximum number of operations to cache (optional, default `100`)
### Examples
@@ -64,7 +94,7 @@ This method always returns the current concurrency.
### Parameters
- `concurrency` **[number][3]?**
- `concurrency` **[number][11]?**
### Examples
@@ -74,7 +104,7 @@ sharp.concurrency(2); // 2
sharp.concurrency(0); // 4
```
Returns **[number][3]** concurrency
Returns **[number][11]** concurrency
## queue
@@ -116,7 +146,7 @@ by taking advantage of the SIMD vector unit of the CPU, e.g. Intel SSE and ARM N
### Parameters
- `simd` **[boolean][2]** (optional, default `true`)
- `simd` **[boolean][10]** (optional, default `true`)
### Examples
@@ -130,10 +160,26 @@ const simd = sharp.simd(false);
// prevent libvips from using liborc at runtime
```
Returns **[boolean][2]**
Returns **[boolean][10]**
[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
[3]: http://en.wikipedia.org/wiki/Nearest-neighbor_interpolation
[4]: http://en.wikipedia.org/wiki/Bilinear_interpolation
[5]: http://en.wikipedia.org/wiki/Bicubic_interpolation
[6]: https://github.com/jcupitt/libvips/blob/master/libvips/resample/lbb.cpp#L100
[7]: http://en.wikipedia.org/wiki/Acutance
[8]: http://eprints.soton.ac.uk/268086/
[9]: https://github.com/jcupitt/libvips/blob/master/libvips/resample/vsqbs.cpp#L48
[10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[11]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number

View File

@@ -1,10 +1,70 @@
# Changelog
## v0.27 - *avif*
Requires libvips v8.10.5
### v0.27.0 - 22nd December 2020
* Add support for AVIF to prebuilt binaries.
* Remove experimental status from `heif` output, defaults are now AVIF-centric.
* Allow negative top/left offsets for composite operation.
[#2391](https://github.com/lovell/sharp/pull/2391)
[@CurosMJ](https://github.com/CurosMJ)
* Ensure all platforms use fontconfig for font rendering.
[#2399](https://github.com/lovell/sharp/issues/2399)
## v0.26 - *zoom*
Requires libvips v8.10.0
### v0.26.0 - TBD
### v0.26.3 - 16th November 2020
* Expose libvips' affine operation.
[#2336](https://github.com/lovell/sharp/pull/2336)
[@guillevc](https://github.com/guillevc)
* Fallback to tar.gz for prebuilt libvips when Brotli not available.
[#2412](https://github.com/lovell/sharp/pull/2412)
[@ascorbic](https://github.com/ascorbic)
### v0.26.2 - 14th October 2020
* Add support for EXR input. Requires libvips compiled with OpenEXR.
[#698](https://github.com/lovell/sharp/issues/698)
* Ensure support for yarn v2.
[#2379](https://github.com/lovell/sharp/pull/2379)
[@jalovatt](https://github.com/jalovatt)
* Add centre/center option to tile-based output.
[#2397](https://github.com/lovell/sharp/pull/2397)
[@beig](https://github.com/beig)
### v0.26.1 - 20th September 2020
* Ensure correct pageHeight when verifying multi-page image dimensions.
[#2343](https://github.com/lovell/sharp/pull/2343)
[@derom](https://github.com/derom)
* Allow input density range up to 100000 DPI.
[#2348](https://github.com/lovell/sharp/pull/2348)
[@stefanprobst](https://github.com/stefanprobst)
* Ensure animation-related properties can be set for Stream-based input.
[#2369](https://github.com/lovell/sharp/pull/2369)
[@AcrylicShrimp](https://github.com/AcrylicShrimp)
* Ensure `stats` can be calculated for 1x1 input.
[#2372](https://github.com/lovell/sharp/issues/2372)
* Ensure animated GIF output is optimised.
[#2376](https://github.com/lovell/sharp/issues/2376)
### v0.26.0 - 25th August 2020
* Prebuilt libvips binaries are now statically-linked and Brotli-compressed, requiring Node.js 10.16.0+.
@@ -27,6 +87,10 @@ Requires libvips v8.10.0
[#2259](https://github.com/lovell/sharp/pull/2259)
[@vouillon](https://github.com/vouillon)
* Add support to `withMetadata` for custom ICC profile.
[#2271](https://github.com/lovell/sharp/pull/2271)
[@roborourke](https://github.com/roborourke)
* Ensure prebuilt binaries for ARM default to v7 when using Electron.
[#2292](https://github.com/lovell/sharp/pull/2292)
[@diegodev3](https://github.com/diegodev3)

View File

@@ -194,3 +194,15 @@ GitHub: https://github.com/vouillon
Name: Tomáš Szabo
GitHub: https://github.com/deftomat
Name: Robert O'Rourke
GitHub: https://github.com/roborourke
Name: Denis Soldatov
GitHub: https://github.com/derom
Name: Stefan Probst
GitHub: https://github.com/stefanprobst
Name: Thomas Beiganz
GitHub: https://github.com/beig

View File

@@ -4,11 +4,11 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="Resize large images in common formats to smaller, web-friendly JPEG, PNG and WebP images of varying dimensions">
<meta name="description" content="Resize large images in common formats to smaller, web-friendly JPEG, PNG, WebP and AVIF images of varying dimensions">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; object-src 'none'; style-src 'unsafe-inline';
img-src 'unsafe-inline' data: https://pixel.plumbing/px/ https://cdn.jsdelivr.net/gh/lovell/ https://www.google-analytics.com;
connect-src 'self' https://cdn.jsdelivr.net/gh/lovell/ https://www.google-analytics.com;
script-src 'sha256-iXDHv+t2aGdJcmEvwHfqmGH4SrOSx+P8rCF6+WLLwXA=' 'unsafe-eval'
script-src 'unsafe-inline' 'unsafe-eval'
https://cdn.jsdelivr.net/npm/docute@4/dist/docute.min.js
https://www.google-analytics.com/analytics.js;">
<link rel="icon" type="image/png" href="https://pixel.plumbing/px/32x32/sharp-logo.svg">
@@ -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.25.4/docs/README.md" as="fetch" type="text/markdown" crossorigin>
<link rel="preload" href="https://cdn.jsdelivr.net/gh/lovell/sharp@v0.27.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="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.25.4/docs',
sourcePath: 'https://cdn.jsdelivr.net/gh/lovell/sharp@v0.27.0/docs',
nav: [
{
title: 'Funding',

View File

@@ -10,31 +10,34 @@ yarn add sharp
## Prerequisites
* Node.js v10.16.0+
* Node.js v10+
## Prebuilt binaries
Ready-compiled sharp and libvips binaries are provided for use with
Node.js v10.16.0+ on the most common platforms:
Node.js v10+ on the most common platforms:
* macOS x64 (>= 10.13)
* Linux x64 (glibc >= 2.17, musl >= 1.1.24)
* Linux ARM64 (glibc >= 2.29)
* Windows
* Windows x64
* Windows x86
A ~7MB tarball containing libvips and its most commonly used dependencies
A ~9MB 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
JPEG, PNG, WebP, TIFF, GIF (input) and SVG (input) image formats.
JPEG, PNG, WebP, AVIF, TIFF, GIF (input) and SVG (input) image formats.
The following platforms have prebuilt libvips but not sharp:
* Linux ARMv6
* Linux ARMv7 (glibc >= 2.28)
* Windows ARM64
The following platforms require compilation of both libvips and sharp from source:
* macOS ARM64
* Linux x86
* Linux x64 (glibc <= 2.16, includes RHEL/CentOS 6)
* Linux ARM64 (glibc <= 2.28, musl)
@@ -69,7 +72,7 @@ The use of a globally-installed libvips is unsupported on Windows.
This module will be compiled from source at `npm install` time when:
* a globally-installed libvips is detected (set the `SHARP_IGNORE_GLOBAL_LIBVIPS` environment variable to skip this),
* prebuilt binaries do not exist for the current platform and Node.js version, or
* prebuilt sharp binaries do not exist for the current platform, or
* when the `npm install --build-from-source` flag is used.
Building from source requires:
@@ -89,11 +92,14 @@ 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.
The version subpath and file name are appended to these.
The version subpath and file name are appended to these. There should be tarballs available
that are compressed with both gzip and Brotli, as the format downloaded will vary depending
on whether the user's version of Node supports Brotli decompression (Node.js v10.16.0+)
For example, if `sharp_libvips_binary_host` is set to `https://hostname/path`
and the libvips version is `1.2.3` then the resultant URL will be
`https://hostname/path/v1.2.3/libvips-1.2.3-platform-arch.tar.br`.
`https://hostname/path/v1.2.3/libvips-1.2.3-platform-arch.tar.br` or
`https://hostname/path/v1.2.3/libvips-1.2.3-platform-arch.tar.gz`.
See the Chinese mirror below for a further example.
@@ -147,10 +153,18 @@ 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.
On machines other than Linux x64, such as macOS and Windows, run the following:
When building your deployment package on machines other than Linux x64 (glibc),
run the following commands:
macOS:
```sh
rm -rf node_modules/sharp
SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install --arch=x64 --platform=linux sharp
```
Windows:
```sh
rmdir /s /q node_modules/sharp
npm install --arch=x64 --platform=linux sharp
```
@@ -164,20 +178,6 @@ docker run -v "$PWD":/var/task lambci/lambda:build-nodejs12.x npm install sharp
To get the best performance select the largest memory available.
A 1536 MB function provides ~12x more CPU time than a 128 MB function.
## Electron
Electron provides versions of the V8 JavaScript engine
that are incompatible with Node.js.
To ensure the correct binaries are used, run the following:
```sh
npm install
npx electron-rebuild
```
Further help can be found at
[https://electronjs.org/docs/tutorial/using-native-node-modules](https://electronjs.org/docs/tutorial/using-native-node-modules)
## Worker threads
The main thread must call `require('sharp')`

View File

@@ -4,11 +4,11 @@ A test to benchmark the performance of this module relative to alternatives.
## The contenders
* [jimp](https://www.npmjs.com/package/jimp) v0.9.3 - Image processing in pure JavaScript. Provides bicubic interpolation.
* [mapnik](https://www.npmjs.org/package/mapnik) v4.3.1 - Whilst primarily a map renderer, Mapnik contains bitmap image utilities.
* [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.
* [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.24.0 / libvips v8.9.0 - Caching within libvips disabled to ensure a fair comparison.
* sharp v0.27.0 / libvips v8.10.5 - 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 [c5.large](https://aws.amazon.com/ec2/instance-types/c5/) (2x Xeon Platinum 8124M CPU @ 3.00GHz)
* Ubuntu 18.04 (hvm-ssd/ubuntu-bionic-18.04-amd64-server-20180912 ami-00035f41c82244dab)
* Node.js v12.14.1
* 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
## Results
| Module | Input | Output | Ops/sec | Speed-up |
| :----------------- | :----- | :----- | ------: | -------: |
| jimp | buffer | buffer | 0.72 | 1.0 |
| mapnik | buffer | buffer | 3.02 | 4.2 |
| gm | buffer | buffer | 3.90 | 5.4 |
| gm | file | file | 3.94 | 5.5 |
| imagemagick | file | file | 4.30 | 6.0 |
| sharp | stream | stream | 23.65 | 32.8 |
| sharp | file | file | 24.66 | 34.3 |
| sharp | buffer | buffer | 25.14 | 34.9 |
| 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 |
Greater libvips performance can be expected with caching enabled (default)
and using 8+ core machines, especially those with larger L1/L2 CPU caches.
and using 4+ 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.

File diff suppressed because one or more lines are too long

View File

@@ -1,12 +1,13 @@
'use strict';
const fs = require('fs');
const path = require('path');
const { extractDescription, extractKeywords } = require('./extract');
const searchIndex = [];
// Install
const contents = fs.readFileSync(`${__dirname}/../install.md`, 'utf8');
const contents = fs.readFileSync(path.join(__dirname, '..', 'install.md'), 'utf8');
const matches = contents.matchAll(
/## (?<title>[A-Za-z ]+)\n\n(?<body>[^#]+)/gs
);
@@ -34,7 +35,7 @@ for (const match of matches) {
'colour',
'utility'
].forEach((section) => {
const contents = fs.readFileSync(`${__dirname}/../api-${section}.md`, 'utf8');
const contents = fs.readFileSync(path.join(__dirname, '..', `api-${section}.md`), 'utf8');
const matches = contents.matchAll(
/\n## (?<title>[A-Za-z]+)\n\n(?<firstparagraph>.+?)\n\n/gs
);
@@ -54,6 +55,6 @@ for (const match of matches) {
});
fs.writeFileSync(
`${__dirname}/../search-index.json`,
path.join(__dirname, '..', 'search-index.json'),
JSON.stringify(searchIndex)
);

View File

@@ -1,65 +1,6 @@
'use strict';
const stopWords = [
'a',
'about',
'all',
'already',
'always',
'an',
'and',
'any',
'are',
'as',
'at',
'be',
'been',
'by',
'can',
'do',
'does',
'each',
'either',
'etc',
'for',
'from',
'get',
'gets',
'has',
'have',
'how',
'if',
'in',
'is',
'it',
'its',
'may',
'more',
'much',
'no',
'not',
'of',
'on',
'or',
'over',
'set',
'sets',
'should',
'that',
'the',
'their',
'there',
'therefore',
'these',
'this',
'to',
'use',
'using',
'when',
'which',
'will',
'with'
];
const stopWords = require('./stop-words');
const extractDescription = (str) =>
str
@@ -71,10 +12,13 @@ const extractDescription = (str) =>
.trim();
const extractKeywords = (str) =>
str
.split(/[ -/]/)
.map((word) => word.toLowerCase().replace(/[^a-z]/g, ''))
.filter((word) => word.length > 2 && !stopWords.includes(word))
.join(' ');
[
...new Set(
str
.split(/[ -/]/)
.map((word) => word.toLowerCase().replace(/[^a-z]/g, ''))
.filter((word) => word.length > 2 && !stopWords.includes(word))
)
].join(' ');
module.exports = { extractDescription, extractKeywords };

View File

@@ -0,0 +1,71 @@
'use strict';
module.exports = [
'about',
'after',
'all',
'already',
'alternative',
'always',
'and',
'any',
'are',
'been',
'before',
'can',
'containing',
'default',
'does',
'each',
'either',
'etc',
'for',
'from',
'get',
'gets',
'given',
'has',
'have',
'how',
'image',
'its',
'may',
'more',
'most',
'much',
'must',
'non',
'not',
'occur',
'occurs',
'over',
'perform',
'performs',
'provide',
'provided',
'set',
'sets',
'should',
'spelling',
'support',
'supported',
'take',
'that',
'the',
'their',
'there',
'therefore',
'these',
'this',
'use',
'used',
'using',
'value',
'values',
'when',
'which',
'while',
'will',
'with',
'without'
];

View File

@@ -25,6 +25,7 @@ const minimumGlibcVersionByArch = {
const { minimumLibvipsVersion, minimumLibvipsVersionLabelled } = libvips;
const distHost = process.env.npm_config_sharp_libvips_binary_host || 'https://github.com/lovell/sharp-libvips/releases/download';
const distBaseUrl = process.env.npm_config_sharp_dist_base_url || process.env.SHARP_DIST_BASE_URL || `${distHost}/v${minimumLibvipsVersionLabelled}/`;
const supportsBrotli = ('BrotliDecompress' in zlib);
const fail = function (err) {
npmLog.error('sharp', err.message);
@@ -43,7 +44,7 @@ const extractTarball = function (tarPath) {
libvips.mkdirSync(versionedVendorPath);
stream.pipeline(
fs.createReadStream(tarPath),
new zlib.BrotliDecompress(),
supportsBrotli ? new zlib.BrotliDecompress() : new zlib.Gunzip(),
tarFs.extract(versionedVendorPath),
function (err) {
if (err) {
@@ -58,6 +59,7 @@ const extractTarball = function (tarPath) {
try {
const useGlobalLibvips = libvips.useGlobalLibvips();
if (useGlobalLibvips) {
const globalLibvipsVersion = libvips.globalLibvipsVersion();
npmLog.info('sharp', `Detected globally-installed libvips v${globalLibvipsVersion}`);
@@ -80,8 +82,16 @@ try {
throw new Error(`Use with glibc ${detectLibc.version} requires manual installation of libvips >= ${minimumLibvipsVersion}`);
}
}
const supportedNodeVersion = process.env.npm_package_engines_node || require('../package.json').engines.node;
if (!semver.satisfies(process.versions.node, supportedNodeVersion)) {
throw new Error(`Expected Node.js version ${supportedNodeVersion} but found ${process.versions.node}`);
}
const extension = supportsBrotli ? 'br' : 'gz';
// Download to per-process temporary file
const tarFilename = ['libvips', minimumLibvipsVersion, platformAndArch].join('-') + '.tar.br';
const tarFilename = ['libvips', minimumLibvipsVersion, platformAndArch].join('-') + '.tar.' + extension;
const tarPathCache = path.join(libvips.cachePath(), tarFilename);
if (fs.existsSync(tarPathCache)) {
npmLog.info('sharp', `Using cached ${tarPathCache}`);

View File

@@ -1,9 +0,0 @@
'use strict';
const { execFileSync } = require('child_process');
const { prebuild_upload: hasToken, APPVEYOR_REPO_TAG_NAME, TRAVIS_TAG } = process.env;
if (hasToken && (APPVEYOR_REPO_TAG_NAME || TRAVIS_TAG)) {
execFileSync('prebuild', ['--runtime', 'napi', '--target', '3'], { stdio: 'inherit' });
}

View File

@@ -85,7 +85,7 @@ function extractChannel (channel) {
* - sRGB: 0: Red, 1: Green, 2: Blue, 3: Alpha.
* - CMYK: 0: Magenta, 1: Cyan, 2: Yellow, 3: Black, 4: Alpha.
*
* Buffers may be any of the image formats supported by sharp: JPEG, PNG, WebP, GIF, SVG, TIFF or raw pixel image data.
* Buffers may be any of the image formats supported by sharp.
* For raw pixel input, the `options` object should contain a `raw` attribute, which follows the format of the attribute of the same name in the `sharp()` constructor.
*
* @param {Array<string|Buffer>|string|Buffer} images - one or more images (file paths, Buffers).

View File

@@ -105,8 +105,9 @@ function composite (images) {
input: this._createInputDescriptor(image.input, inputOptions, { allowStream: false }),
blend: 'over',
tile: false,
left: -1,
top: -1,
left: 0,
top: 0,
hasOffset: false,
gravity: 0,
premultiplied: false
};
@@ -125,21 +126,23 @@ function composite (images) {
}
}
if (is.defined(image.left)) {
if (is.integer(image.left) && image.left >= 0) {
if (is.integer(image.left)) {
composite.left = image.left;
} else {
throw is.invalidParameterError('left', 'positive integer', image.left);
throw is.invalidParameterError('left', 'integer', image.left);
}
}
if (is.defined(image.top)) {
if (is.integer(image.top) && image.top >= 0) {
if (is.integer(image.top)) {
composite.top = image.top;
} else {
throw is.invalidParameterError('top', 'positive integer', image.top);
throw is.invalidParameterError('top', 'integer', image.top);
}
}
if (composite.left !== composite.top && Math.min(composite.left, composite.top) === -1) {
if (is.defined(image.top) !== is.defined(image.left)) {
throw new Error('Expected both left and top to be set');
} else {
composite.hasOffset = is.integer(image.top) && is.integer(image.left);
}
if (is.defined(image.gravity)) {
if (is.integer(image.gravity) && is.inRange(image.gravity, 0, 8)) {

View File

@@ -40,7 +40,7 @@ const debuglog = util.debuglog('sharp');
/**
* Constructor factory to create an instance of `sharp`, to which further methods are chained.
*
* JPEG, PNG, WebP or TIFF format image data can be streamed out from this object.
* JPEG, PNG, WebP, AVIF or TIFF format image data can be streamed out from this object.
* When using Stream based output, derived attributes are available from the `info` event.
*
* Non-critical problems encountered during processing are emitted as `warning` events.
@@ -86,10 +86,14 @@ const debuglog = util.debuglog('sharp');
* .toBuffer()
* .then( ... );
*
* @example
* // Convert an animated GIF to an animated WebP
* await sharp('in.gif', { animated: true }).toFile('out.webp');
*
* @param {(Buffer|string)} [input] - if present, can be
* a Buffer containing JPEG, PNG, WebP, GIF, SVG, TIFF or raw pixel image data, or
* a String containing the filesystem path to an JPEG, PNG, WebP, GIF, SVG or TIFF image file.
* JPEG, PNG, WebP, GIF, SVG, TIFF or raw pixel image data can be streamed into the object when not present.
* a Buffer containing JPEG, PNG, WebP, AVIF, GIF, SVG, TIFF or raw pixel image data, or
* a String containing the filesystem path to an JPEG, PNG, WebP, AVIF, GIF, SVG or TIFF image file.
* JPEG, PNG, WebP, AVIF, GIF, SVG, TIFF or raw pixel image data can be streamed into the object when not present.
* @param {Object} [options] - if present, is an Object with optional attributes.
* @param {boolean} [options.failOnError=true] - by default halt processing and raise an error when loading invalid images.
* Set this flag to `false` if you'd rather apply a "best effort" to decode images, even if the data is corrupt or invalid.
@@ -98,10 +102,11 @@ const debuglog = util.debuglog('sharp');
* An integral Number of pixels, zero or false to remove limit, true to use default limit of 268402689 (0x3FFF x 0x3FFF).
* @param {boolean} [options.sequentialRead=false] - Set this to `true` to use sequential rather than random access where possible.
* This can reduce memory usage and might improve performance on some systems.
* @param {number} [options.density=72] - number representing the DPI for vector images.
* @param {number} [options.pages=1] - number of pages to extract for multi-page input (GIF, TIFF, PDF), use -1 for all pages.
* @param {number} [options.page=0] - page number to start extracting from for multi-page input (GIF, TIFF, PDF), zero based.
* @param {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.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.
* @param {number} [options.raw.width]
* @param {number} [options.raw.height]
@@ -150,6 +155,13 @@ const Sharp = function (input, options) {
extendRight: 0,
extendBackground: [0, 0, 0, 255],
withoutEnlargement: false,
affineMatrix: [],
affineBackground: [0, 0, 0, 255],
affineIdx: 0,
affineIdy: 0,
affineOdx: 0,
affineOdy: 0,
affineInterpolator: this.constructor.interpolators.bilinear,
kernel: 'lanczos3',
fastShrinkOnLoad: true,
// operations
@@ -187,6 +199,7 @@ const Sharp = function (input, options) {
streamOut: false,
withMetadata: false,
withMetadataOrientation: -1,
withMetadataIcc: '',
resolveWithObject: false,
// output format
jpegQuality: 80,
@@ -220,9 +233,10 @@ const Sharp = function (input, options) {
tiffTileWidth: 256,
tiffXres: 1.0,
tiffYres: 1.0,
heifQuality: 80,
heifQuality: 50,
heifLossless: false,
heifCompression: 'hevc',
heifCompression: 'av1',
heifSpeed: 5,
tileSize: 256,
tileOverlap: 0,
tileContainer: 'fs',
@@ -232,6 +246,7 @@ const Sharp = function (input, options) {
tileAngle: 0,
tileSkipBlanks: -1,
tileBackground: [255, 255, 255, 255],
tileCentre: false,
linearA: 1,
linearB: 0,
// Function to notify of libvips warnings

View File

@@ -9,9 +9,9 @@ const sharp = require('../build/Release/sharp.node');
* @private
*/
function _inputOptionsFromObject (obj) {
const { raw, density, limitInputPixels, sequentialRead, failOnError } = obj;
return [raw, density, limitInputPixels, sequentialRead, failOnError].some(is.defined)
? { raw, density, limitInputPixels, sequentialRead, failOnError }
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 }
: undefined;
}
@@ -57,10 +57,10 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
}
// Density
if (is.defined(inputOptions.density)) {
if (is.inRange(inputOptions.density, 1, 2400)) {
if (is.inRange(inputOptions.density, 1, 100000)) {
inputDescriptor.density = inputOptions.density;
} else {
throw is.invalidParameterError('density', 'number between 1 and 2400', inputOptions.density);
throw is.invalidParameterError('density', 'number between 1 and 100000', inputOptions.density);
}
}
// limitInputPixels
@@ -99,6 +99,13 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
}
}
// Multi-page input (GIF, TIFF, PDF)
if (is.defined(inputOptions.animated)) {
if (is.bool(inputOptions.animated)) {
inputDescriptor.pages = inputOptions.animated ? -1 : 1;
} else {
throw is.invalidParameterError('animated', 'boolean', inputOptions.animated);
}
}
if (is.defined(inputOptions.pages)) {
if (is.integer(inputOptions.pages) && is.inRange(inputOptions.pages, -1, 100000)) {
inputDescriptor.pages = inputOptions.pages;

View File

@@ -1,5 +1,6 @@
'use strict';
const { flatten: flattenArray } = require('array-flatten');
const color = require('color');
const is = require('./is');
@@ -82,6 +83,103 @@ function flop (flop) {
return this;
}
/**
* Perform an affine transform on an image. This operation will always occur after resizing, extraction and rotation, if any.
*
* You must provide an array of length 4 or a 2x2 affine transformation matrix.
* By default, new pixels are filled with a black background. You can provide a background color with the `background` option.
* A particular interpolator may also be specified. Set the `interpolator` option to an attribute of the `sharp.interpolator` Object e.g. `sharp.interpolator.nohalo`.
*
* In the case of a 2x2 matrix, the transform is:
* - X = `matrix[0, 0]` \* (x + `idx`) + `matrix[0, 1]` \* (y + `idy`) + `odx`
* - Y = `matrix[1, 0]` \* (x + `idx`) + `matrix[1, 1]` \* (y + `idy`) + `ody`
*
* where:
* - x and y are the coordinates in input image.
* - X and Y are the coordinates in output image.
* - (0,0) is the upper left corner.
*
* @since 0.27.0
*
* @example
* const pipeline = sharp()
* .affine([[1, 0.3], [0.1, 0.7]], {
* background: 'white',
* interpolate: sharp.interpolators.nohalo
* })
* .toBuffer((err, outputBuffer, info) => {
* // outputBuffer contains the transformed image
* // info.width and info.height contain the new dimensions
* });
*
* inputStream
* .pipe(pipeline);
*
* @param {Array<Array<number>>|Array<number>} matrix - affine transformation matrix
* @param {Object} [options] - if present, is an Object with optional attributes.
* @param {String|Object} [options.background="#000000"] - parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha.
* @param {Number} [options.idx=0] - input horizontal offset
* @param {Number} [options.idy=0] - input vertical offset
* @param {Number} [options.odx=0] - output horizontal offset
* @param {Number} [options.ody=0] - output vertical offset
* @param {String} [options.interpolator=sharp.interpolators.bicubic] - interpolator
* @returns {Sharp}
* @throws {Error} Invalid parameters
*/
function affine (matrix, options) {
const flatMatrix = flattenArray(matrix);
if (flatMatrix.length === 4 && flatMatrix.every(is.number)) {
this.options.affineMatrix = flatMatrix;
} else {
throw is.invalidParameterError('matrix', '1x4 or 2x2 array', matrix);
}
if (is.defined(options)) {
if (is.object(options)) {
this._setBackgroundColourOption('affineBackground', options.background);
if (is.defined(options.idx)) {
if (is.number(options.idx)) {
this.options.affineIdx = options.idx;
} else {
throw is.invalidParameterError('options.idx', 'number', options.idx);
}
}
if (is.defined(options.idy)) {
if (is.number(options.idy)) {
this.options.affineIdy = options.idy;
} else {
throw is.invalidParameterError('options.idy', 'number', options.idy);
}
}
if (is.defined(options.odx)) {
if (is.number(options.odx)) {
this.options.affineOdx = options.odx;
} else {
throw is.invalidParameterError('options.odx', 'number', options.odx);
}
}
if (is.defined(options.ody)) {
if (is.number(options.ody)) {
this.options.affineOdy = options.ody;
} else {
throw is.invalidParameterError('options.ody', 'number', options.ody);
}
}
if (is.defined(options.interpolator)) {
if (is.inArray(options.interpolator, Object.values(this.constructor.interpolators))) {
this.options.affineInterpolator = options.interpolator;
} else {
throw is.invalidParameterError('options.interpolator', 'valid interpolator name', options.interpolator);
}
}
} else {
throw is.invalidParameterError('options', 'object', options);
}
}
return this;
}
/**
* Sharpen the image.
* When used without parameters, performs a fast, mild sharpen of the output image.
@@ -482,6 +580,7 @@ module.exports = function (Sharp) {
rotate,
flip,
flop,
affine,
sharpen,
median,
blur,

View File

@@ -6,6 +6,7 @@ const sharp = require('../build/Release/sharp.node');
const formats = new Map([
['heic', 'heif'],
['heif', 'heif'],
['avif', 'avif'],
['jpeg', 'jpeg'],
['jpg', 'jpeg'],
['png', 'png'],
@@ -19,7 +20,7 @@ const formats = new Map([
* Write output image data to a file.
*
* If an explicit output format is not selected, it will be inferred from the extension,
* with JPEG, PNG, WebP, TIFF, DZI, and libvips' V format supported.
* with JPEG, PNG, WebP, AVIF, TIFF, DZI, and libvips' V format supported.
* Note that raw pixel data is only supported for buffer output.
*
* By default all metadata will be removed, which includes EXIF-based orientation.
@@ -71,7 +72,7 @@ function toFile (fileOut, callback) {
/**
* Write output to a Buffer.
* JPEG, PNG, WebP, TIFF and RAW output are supported.
* JPEG, PNG, WebP, AVIF, TIFF and raw pixel data output are supported.
*
* If no explicit format is set, the output format will match the input image, except GIF and SVG input which become PNG output.
*
@@ -119,7 +120,8 @@ function toBuffer (options, callback) {
/**
* Include all metadata (EXIF, XMP, IPTC) from the input image in the output image.
* This will also convert to and add a web-friendly sRGB ICC profile.
* This will also convert to and add a web-friendly sRGB ICC profile unless a custom
* output profile is provided.
*
* The default behaviour, when `withMetadata` is not used, is to convert to the device-independent
* sRGB colour space and strip all metadata, including the removal of any ICC profile.
@@ -132,6 +134,7 @@ function toBuffer (options, callback) {
*
* @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.
* @returns {Sharp}
* @throws {Error} Invalid parameters
*/
@@ -145,6 +148,13 @@ function withMetadata (options) {
throw is.invalidParameterError('orientation', 'integer between 1 and 8', options.orientation);
}
}
if (is.defined(options.icc)) {
if (is.string(options.icc)) {
this.options.withMetadataIcc = options.icc;
} else {
throw is.invalidParameterError('icc', 'string filesystem path to ICC profile', options.icc);
}
}
}
return this;
}
@@ -399,6 +409,7 @@ function webp (options) {
* @returns {Sharp}
* @throws {Error} Invalid options
*/
/* istanbul ignore next */
function gif (options) {
if (!this.constructor.format.magick.output.buffer) {
throw new Error('The gif operation requires libvips to have been installed with support for ImageMagick');
@@ -546,29 +557,42 @@ function tiff (options) {
return this._updateFormatOut('tiff', options);
}
/**
* Use these AVIF options for output image.
*
* Whilst it is possible to create AVIF images smaller than 16x16 pixels,
* most web browsers do not display these properly.
*
* @since 0.27.0
*
* @param {Object} [options] - output options
* @param {number} [options.quality=50] - quality, integer 1-100
* @param {boolean} [options.lossless=false] - use lossless compression
* @param {boolean} [options.speed=5] - CPU effort vs file size, 0 (slowest/smallest) to 8 (fastest/largest)
* @returns {Sharp}
* @throws {Error} Invalid options
*/
function avif (options) {
return this.heif({ ...options, compression: 'av1' });
}
/**
* Use these HEIF options for output image.
*
* Support for HEIF (HEIC/AVIF) is experimental.
* Do not use this in production systems.
*
* Requires a custom, globally-installed libvips compiled with support for libheif.
*
* Most versions of libheif support only the patent-encumbered HEVC compression format.
* Support for patent-encumbered HEIC images requires the use of a
* globally-installed libvips compiled with support for libheif, libde265 and x265.
*
* @since 0.23.0
*
* @param {Object} [options] - output options
* @param {number} [options.quality=80] - quality, integer 1-100
* @param {boolean} [options.compression='hevc'] - compression format: hevc, avc, jpeg, av1
* @param {number} [options.quality=50] - quality, integer 1-100
* @param {boolean} [options.compression='av1'] - compression format: av1, hevc
* @param {boolean} [options.lossless=false] - use lossless compression
* @param {boolean} [options.speed=5] - CPU effort vs file size, 0 (slowest/smallest) to 8 (fastest/largest)
* @returns {Sharp}
* @throws {Error} Invalid options
*/
function heif (options) {
if (!this.constructor.format.heif.output.buffer) {
throw new Error('The heif operation requires libvips to have been installed with support for libheif');
}
if (is.object(options)) {
if (is.defined(options.quality)) {
if (is.integer(options.quality) && is.inRange(options.quality, 1, 100)) {
@@ -585,10 +609,17 @@ function heif (options) {
}
}
if (is.defined(options.compression)) {
if (is.string(options.compression) && is.inArray(options.compression, ['hevc', 'avc', 'jpeg', 'av1'])) {
if (is.string(options.compression) && is.inArray(options.compression, ['av1', 'hevc'])) {
this.options.heifCompression = options.compression;
} else {
throw is.invalidParameterError('compression', 'one of: hevc, avc, jpeg, av1', options.compression);
throw is.invalidParameterError('compression', 'one of: av1, hevc', options.compression);
}
}
if (is.defined(options.speed)) {
if (is.integer(options.speed) && is.inRange(options.speed, 0, 8)) {
this.options.heifSpeed = options.speed;
} else {
throw is.invalidParameterError('speed', 'integer between 0 and 8', options.speed);
}
}
}
@@ -648,6 +679,8 @@ function raw () {
* @param {number} [options.skipBlanks=-1] threshold to skip tile generation, a value 0 - 255 for 8-bit images or 0 - 65535 for 16-bit images
* @param {string} [options.container='fs'] tile container, with value `fs` (filesystem) or `zip` (compressed file).
* @param {string} [options.layout='dz'] filesystem layout, possible values are `dz`, `iiif`, `zoomify` or `google`.
* @param {boolean} [options.centre=false] centre image in tile.
* @param {boolean} [options.center=false] alternative spelling of centre.
* @returns {Sharp}
* @throws {Error} Invalid parameters
*/
@@ -716,6 +749,11 @@ function tile (options) {
} else if (is.defined(options.layout) && options.layout === 'google') {
this.options.tileSkipBlanks = 5;
}
// Center image in tile
const centre = is.bool(options.center) ? options.center : options.centre;
if (is.defined(centre)) {
this._setBooleanOption('tileCentre', centre);
}
}
// Format
if (is.inArray(this.options.formatOut, ['jpeg', 'png', 'webp'])) {
@@ -874,6 +912,7 @@ module.exports = function (Sharp) {
png,
webp,
tiff,
avif,
heif,
gif,
raw,

View File

@@ -386,7 +386,8 @@ function extract (options) {
* Trim "boring" pixels from all edges that contain values similar to the top-left pixel.
* Images consisting entirely of a single colour will calculate "boring" using the alpha channel, if any.
*
* The `info` response Object will contain `trimOffsetLeft` and `trimOffsetTop` properties.
* The `info` response Object, obtained from callback of `.toFile()` or `.toBuffer()`,
* will contain `trimOffsetLeft` and `trimOffsetTop` properties.
*
* @param {number} [threshold=10] the allowed difference from the top-left pixel, a number greater than zero.
* @returns {Sharp}

View File

@@ -13,6 +13,26 @@ const sharp = require('../build/Release/sharp.node');
*/
const format = sharp.format();
/**
* An Object containing the available interpolators and their proper values
* @readonly
* @enum {string}
*/
const interpolators = {
/** [Nearest neighbour interpolation](http://en.wikipedia.org/wiki/Nearest-neighbor_interpolation). Suitable for image enlargement only. */
nearest: 'nearest',
/** [Bilinear interpolation](http://en.wikipedia.org/wiki/Bilinear_interpolation). Faster than bicubic but with less smooth results. */
bilinear: 'bilinear',
/** [Bicubic interpolation](http://en.wikipedia.org/wiki/Bicubic_interpolation) (the default). */
bicubic: 'bicubic',
/** [LBB interpolation](https://github.com/jcupitt/libvips/blob/master/libvips/resample/lbb.cpp#L100). Prevents some "[acutance](http://en.wikipedia.org/wiki/Acutance)" but typically reduces performance by a factor of 2. */
locallyBoundedBicubic: 'lbb',
/** [Nohalo interpolation](http://eprints.soton.ac.uk/268086/). Prevents acutance but typically reduces performance by a factor of 3. */
nohalo: 'nohalo',
/** [VSQBS interpolation](https://github.com/jcupitt/libvips/blob/master/libvips/resample/vsqbs.cpp#L48). Prevents "staircasing" when enlarging. */
vertexSplitQuadraticBasisSpline: 'vsqbs'
};
/**
* An Object containing the version numbers of libvips and its dependencies.
* @member
@@ -146,6 +166,7 @@ module.exports = function (Sharp) {
Sharp[f.name] = f;
});
Sharp.format = format;
Sharp.interpolators = interpolators;
Sharp.versions = versions;
Sharp.queue = queue;
};

View File

@@ -1,7 +1,7 @@
{
"name": "sharp",
"description": "High performance Node.js image processing, the fastest module to resize JPEG, PNG, WebP and TIFF images",
"version": "0.26.0-alpha1",
"description": "High performance Node.js image processing, the fastest module to resize JPEG, PNG, WebP, AVIF and TIFF images",
"version": "0.27.0",
"author": "Lovell Fuller <npm@lovell.info>",
"homepage": "https://github.com/lovell/sharp",
"contributors": [
@@ -68,12 +68,16 @@
"Brychan Bennett-Odlum <git@brychan.io>",
"Edward Silverton <e.silverton@gmail.com>",
"Roman Malieiev <aromaleev@gmail.com>",
"Tomas Szabo <tomas.szabo@deftomat.com>"
"Tomas Szabo <tomas.szabo@deftomat.com>",
"Robert O'Rourke <robert@o-rourke.org>",
"Guillermo Alfonso Varela Chouciño <guillevch@gmail.com>",
"Christian Flintrup <chr@gigahost.dk>",
"Manan Jadhav <manan@motionden.com>"
],
"scripts": {
"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 && node install/prebuild-ci",
"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-licensing": "license-checker --production --summary --onlyAllow=\"Apache-2.0;BSD;ISC;MIT\"",
"test-coverage": "./test/coverage/report.sh",
@@ -86,7 +90,6 @@
"files": [
"binding.gyp",
"install/**",
"!install/prebuild-ci.js",
"lib/**",
"src/**"
],
@@ -98,6 +101,7 @@
"jpeg",
"png",
"webp",
"avif",
"tiff",
"gif",
"svg",
@@ -111,39 +115,40 @@
"vips"
],
"dependencies": {
"color": "^3.1.2",
"array-flatten": "^3.0.0",
"color": "^3.1.3",
"detect-libc": "^1.0.3",
"node-addon-api": "^3.0.0",
"node-addon-api": "^3.1.0",
"npmlog": "^4.1.2",
"prebuild-install": "^5.3.5",
"semver": "^7.3.2",
"prebuild-install": "^6.0.0",
"semver": "^7.3.4",
"simple-get": "^4.0.0",
"tar-fs": "^2.1.0",
"tar-fs": "^2.1.1",
"tunnel-agent": "^0.6.0"
},
"devDependencies": {
"async": "^3.2.0",
"cc": "^2.0.1",
"cc": "^3.0.1",
"decompress-zip": "^0.3.2",
"documentation": "^13.0.2",
"documentation": "^13.1.0",
"exif-reader": "^1.0.3",
"icc": "^2.0.0",
"license-checker": "^25.0.1",
"mocha": "^8.1.1",
"mock-fs": "^4.12.0",
"mocha": "^8.2.1",
"mock-fs": "^4.13.0",
"nyc": "^15.1.0",
"prebuild": "^10.0.0",
"prebuild": "^10.0.1",
"rimraf": "^3.0.2",
"semistandard": "^14.2.3"
"semistandard": "^16.0.0"
},
"license": "Apache-2.0",
"config": {
"libvips": "8.10.0-rc4",
"libvips": "8.10.5",
"runtime": "napi",
"target": 3
},
"engines": {
"node": ">=10.16.0"
"node": ">=10"
},
"funding": {
"url": "https://opencollective.com/libvips"

View File

@@ -54,13 +54,13 @@ namespace sharp {
bool AttrAsBool(Napi::Object obj, std::string attr) {
return obj.Get(attr).As<Napi::Boolean>().Value();
}
std::vector<double> AttrAsRgba(Napi::Object obj, std::string attr) {
Napi::Array background = obj.Get(attr).As<Napi::Array>();
std::vector<double> rgba(background.Length());
for (unsigned int i = 0; i < background.Length(); i++) {
rgba[i] = AttrAsDouble(background, i);
std::vector<double> AttrAsVectorOfDouble(Napi::Object obj, std::string attr) {
Napi::Array napiArray = obj.Get(attr).As<Napi::Array>();
std::vector<double> vectorOfDouble(napiArray.Length());
for (unsigned int i = 0; i < napiArray.Length(); i++) {
vectorOfDouble[i] = AttrAsDouble(napiArray, i);
}
return rgba;
return vectorOfDouble;
}
std::vector<int32_t> AttrAsInt32Vector(Napi::Object obj, std::string attr) {
Napi::Array array = obj.Get(attr).As<Napi::Array>();
@@ -109,7 +109,7 @@ namespace sharp {
descriptor->createChannels = AttrAsUint32(input, "createChannels");
descriptor->createWidth = AttrAsUint32(input, "createWidth");
descriptor->createHeight = AttrAsUint32(input, "createHeight");
descriptor->createBackground = AttrAsRgba(input, "createBackground");
descriptor->createBackground = AttrAsVectorOfDouble(input, "createBackground");
}
// Limit input images to a given number of pixels, where pixels = width * height
descriptor->limitInputPixels = AttrAsUint32(input, "limitInputPixels");
@@ -180,6 +180,7 @@ namespace sharp {
case ImageType::OPENSLIDE: id = "openslide"; break;
case ImageType::PPM: id = "ppm"; break;
case ImageType::FITS: id = "fits"; break;
case ImageType::EXR: id = "exr"; break;
case ImageType::VIPS: id = "vips"; break;
case ImageType::RAW: id = "raw"; break;
case ImageType::UNKNOWN: id = "unknown"; break;
@@ -210,6 +211,7 @@ namespace sharp {
{ "openslideload", ImageType::OPENSLIDE },
{ "ppmload", ImageType::PPM },
{ "fitsload", ImageType::FITS },
{ "openexrload", ImageType::EXR },
{ "vipsload", ImageType::VIPS },
{ "rawload", ImageType::RAW }
};
@@ -485,8 +487,8 @@ namespace sharp {
Check the proposed format supports the current dimensions.
*/
void AssertImageTypeDimensions(VImage image, ImageType const imageType) {
const int height = image.get_typeof("pageHeight") == G_TYPE_INT
? image.get_int("pageHeight")
const int height = image.get_typeof(VIPS_META_PAGE_HEIGHT) == G_TYPE_INT
? image.get_int(VIPS_META_PAGE_HEIGHT)
: image.height();
if (imageType == ImageType::JPEG) {
if (image.width() > 65535 || height > 65535) {
@@ -656,26 +658,18 @@ namespace sharp {
int top = 0;
// assign only if valid
if (x >= 0 && x < (inWidth - outWidth)) {
if (x < (inWidth - outWidth)) {
left = x;
} else if (x >= (inWidth - outWidth)) {
left = inWidth - outWidth;
}
if (y >= 0 && y < (inHeight - outHeight)) {
if (y < (inHeight - outHeight)) {
top = y;
} else if (y >= (inHeight - outHeight)) {
top = inHeight - outHeight;
}
// the resulting left and top could have been outside the image after calculation from bottom/right edges
if (left < 0) {
left = 0;
}
if (top < 0) {
top = 0;
}
return std::make_tuple(left, top);
}

View File

@@ -24,8 +24,10 @@
// Verify platform and compiler compatibility
#if (VIPS_MAJOR_VERSION < 8 || (VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION < 9))
#error "libvips version 8.9.2+ is required - please see https://sharp.pixelplumbing.com/install"
#if (VIPS_MAJOR_VERSION < 8) || \
(VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION < 10) || \
(VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION == 10 && VIPS_MICRO_VERSION < 5)
#error "libvips version 8.10.5+ is required - please see https://sharp.pixelplumbing.com/install"
#endif
#if ((!defined(__clang__)) && defined(__GNUC__) && (__GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 6)))
@@ -92,7 +94,7 @@ namespace sharp {
double AttrAsDouble(Napi::Object obj, std::string attr);
double AttrAsDouble(Napi::Object obj, unsigned int const attr);
bool AttrAsBool(Napi::Object obj, std::string attr);
std::vector<double> AttrAsRgba(Napi::Object obj, std::string attr);
std::vector<double> AttrAsVectorOfDouble(Napi::Object obj, std::string attr);
std::vector<int32_t> AttrAsInt32Vector(Napi::Object obj, std::string attr);
// Create an InputDescriptor instance from a Napi::Object describing an input image
@@ -111,6 +113,7 @@ namespace sharp {
OPENSLIDE,
PPM,
FITS,
EXR,
VIPS,
RAW,
UNKNOWN,

View File

@@ -485,6 +485,18 @@ class PipelineWorker : public Napi::AsyncWorker {
baton->leftOffsetPost, baton->topOffsetPost, baton->widthPost, baton->heightPost);
}
// Affine transform
if (baton->affineMatrix.size() > 0) {
std::vector<double> background;
std::tie(image, background) = sharp::ApplyAlpha(image, baton->affineBackground);
image = image.affine(baton->affineMatrix, VImage::option()->set("background", background)
->set("idx", baton->affineIdx)
->set("idy", baton->affineIdy)
->set("odx", baton->affineOdx)
->set("ody", baton->affineOdy)
->set("interpolate", baton->affineInterpolator));
}
// Extend edges
if (baton->extendTop > 0 || baton->extendBottom > 0 || baton->extendLeft > 0 || baton->extendRight > 0) {
std::vector<double> background;
@@ -558,7 +570,7 @@ class PipelineWorker : public Napi::AsyncWorker {
int left;
int top;
compositeImage = compositeImage.replicate(across, down);
if (composite->left >= 0 && composite->top >= 0) {
if (composite->hasOffset) {
std::tie(left, top) = sharp::CalculateCrop(
compositeImage.width(), compositeImage.height(), image.width(), image.height(),
composite->left, composite->top);
@@ -580,7 +592,7 @@ class PipelineWorker : public Napi::AsyncWorker {
// Calculate position
int left;
int top;
if (composite->left >= 0 && composite->top >= 0) {
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);
@@ -684,6 +696,15 @@ class PipelineWorker : public Napi::AsyncWorker {
}
}
// Apply output ICC profile
if (!baton->withMetadataIcc.empty()) {
image = image.icc_transform(
const_cast<char*>(baton->withMetadataIcc.data()),
VImage::option()
->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);
@@ -773,6 +794,8 @@ class PipelineWorker : public Napi::AsyncWorker {
sharp::AssertImageTypeDimensions(image, sharp::ImageType::GIF);
VipsArea *area = VIPS_AREA(image.magicksave_buffer(VImage::option()
->set("strip", !baton->withMetadata)
->set("optimize_gif_frames", TRUE)
->set("optimize_gif_transparency", TRUE)
->set("format", "gif")));
baton->bufferOut = static_cast<char*>(area->data);
baton->bufferOutLength = area->length;
@@ -814,6 +837,7 @@ class PipelineWorker : public Napi::AsyncWorker {
->set("strip", !baton->withMetadata)
->set("compression", baton->heifCompression)
->set("Q", baton->heifQuality)
->set("speed", baton->heifSpeed)
->set("lossless", baton->heifLossless)));
baton->bufferOut = static_cast<char*>(area->data);
baton->bufferOutLength = area->length;
@@ -862,7 +886,7 @@ class PipelineWorker : public Napi::AsyncWorker {
bool const isV = sharp::IsV(baton->fileOut);
bool const mightMatchInput = baton->formatOut == "input";
bool const willMatchInput = mightMatchInput &&
!(isJpeg || isPng || isWebp || isGif || isTiff || isDz || isDzZip || isV);
!(isJpeg || isPng || isWebp || isGif || isTiff || isHeif || isDz || isDzZip || isV);
if (baton->formatOut == "jpeg" || (mightMatchInput && isJpeg) ||
(willMatchInput && inputImageType == sharp::ImageType::JPEG)) {
@@ -916,6 +940,8 @@ class PipelineWorker : public Napi::AsyncWorker {
sharp::AssertImageTypeDimensions(image, sharp::ImageType::GIF);
image.magicksave(const_cast<char*>(baton->fileOut.data()), VImage::option()
->set("strip", !baton->withMetadata)
->set("optimize_gif_frames", TRUE)
->set("optimize_gif_transparency", TRUE)
->set("format", "gif"));
baton->formatOut = "gif";
} else if (baton->formatOut == "tiff" || (mightMatchInput && isTiff) ||
@@ -941,13 +967,11 @@ class PipelineWorker : public Napi::AsyncWorker {
} else if (baton->formatOut == "heif" || (mightMatchInput && isHeif) ||
(willMatchInput && inputImageType == sharp::ImageType::HEIF)) {
// Write HEIF to file
if (sharp::IsAvif(baton->fileOut)) {
baton->heifCompression = VIPS_FOREIGN_HEIF_COMPRESSION_AV1;
}
image.heifsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
->set("strip", !baton->withMetadata)
->set("Q", baton->heifQuality)
->set("compression", baton->heifCompression)
->set("speed", baton->heifSpeed)
->set("lossless", baton->heifLossless));
baton->formatOut = "heif";
} else if (baton->formatOut == "dz" || isDz || isDzZip) {
@@ -1001,6 +1025,7 @@ class PipelineWorker : public Napi::AsyncWorker {
->set("suffix", const_cast<char*>(suffix.data()))
->set("angle", CalculateAngleRotation(baton->tileAngle))
->set("background", baton->tileBackground)
->set("centre", baton->tileCentre)
->set("skip_blanks", baton->tileSkipBlanks);
// libvips chooses a default depth based on layout. Instead of replicating that logic here by
// not passing anything - libvips will handle choice
@@ -1228,6 +1253,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
composite->gravity = sharp::AttrAsUint32(compositeObject, "gravity");
composite->left = sharp::AttrAsInt32(compositeObject, "left");
composite->top = sharp::AttrAsInt32(compositeObject, "top");
composite->hasOffset = sharp::AttrAsBool(compositeObject, "hasOffset");
composite->tile = sharp::AttrAsBool(compositeObject, "tile");
composite->premultiplied = sharp::AttrAsBool(compositeObject, "premultiplied");
baton->composite.push_back(composite);
@@ -1235,7 +1261,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
// Resize options
baton->withoutEnlargement = sharp::AttrAsBool(options, "withoutEnlargement");
baton->position = sharp::AttrAsInt32(options, "position");
baton->resizeBackground = sharp::AttrAsRgba(options, "resizeBackground");
baton->resizeBackground = sharp::AttrAsVectorOfDouble(options, "resizeBackground");
baton->kernel = sharp::AttrAsStr(options, "kernel");
baton->fastShrinkOnLoad = sharp::AttrAsBool(options, "fastShrinkOnLoad");
// Join Channel Options
@@ -1248,7 +1274,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
}
// Operators
baton->flatten = sharp::AttrAsBool(options, "flatten");
baton->flattenBackground = sharp::AttrAsRgba(options, "flattenBackground");
baton->flattenBackground = sharp::AttrAsVectorOfDouble(options, "flattenBackground");
baton->negate = sharp::AttrAsBool(options, "negate");
baton->blurSigma = sharp::AttrAsDouble(options, "blurSigma");
baton->brightness = sharp::AttrAsDouble(options, "brightness");
@@ -1270,7 +1296,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
baton->useExifOrientation = sharp::AttrAsBool(options, "useExifOrientation");
baton->angle = sharp::AttrAsInt32(options, "angle");
baton->rotationAngle = sharp::AttrAsDouble(options, "rotationAngle");
baton->rotationBackground = sharp::AttrAsRgba(options, "rotationBackground");
baton->rotationBackground = sharp::AttrAsVectorOfDouble(options, "rotationBackground");
baton->rotateBeforePreExtract = sharp::AttrAsBool(options, "rotateBeforePreExtract");
baton->flip = sharp::AttrAsBool(options, "flip");
baton->flop = sharp::AttrAsBool(options, "flop");
@@ -1278,8 +1304,15 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
baton->extendBottom = sharp::AttrAsInt32(options, "extendBottom");
baton->extendLeft = sharp::AttrAsInt32(options, "extendLeft");
baton->extendRight = sharp::AttrAsInt32(options, "extendRight");
baton->extendBackground = sharp::AttrAsRgba(options, "extendBackground");
baton->extendBackground = sharp::AttrAsVectorOfDouble(options, "extendBackground");
baton->extractChannel = sharp::AttrAsInt32(options, "extractChannel");
baton->affineMatrix = sharp::AttrAsVectorOfDouble(options, "affineMatrix");
baton->affineBackground = sharp::AttrAsVectorOfDouble(options, "affineBackground");
baton->affineIdx = sharp::AttrAsDouble(options, "affineIdx");
baton->affineIdy = sharp::AttrAsDouble(options, "affineIdy");
baton->affineOdx = sharp::AttrAsDouble(options, "affineOdx");
baton->affineOdy = sharp::AttrAsDouble(options, "affineOdy");
baton->affineInterpolator = vips::VInterpolate::new_from_name(sharp::AttrAsStr(options, "affineInterpolator").data());
baton->removeAlpha = sharp::AttrAsBool(options, "removeAlpha");
baton->ensureAlpha = sharp::AttrAsBool(options, "ensureAlpha");
@@ -1319,6 +1352,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
baton->fileOut = sharp::AttrAsStr(options, "fileOut");
baton->withMetadata = sharp::AttrAsBool(options, "withMetadata");
baton->withMetadataOrientation = sharp::AttrAsUint32(options, "withMetadataOrientation");
baton->withMetadataIcc = sharp::AttrAsStr(options, "withMetadataIcc");
// Format-specific
baton->jpegQuality = sharp::AttrAsUint32(options, "jpegQuality");
baton->jpegProgressive = sharp::AttrAsBool(options, "jpegProgressive");
@@ -1361,6 +1395,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
baton->heifCompression = static_cast<VipsForeignHeifCompression>(
vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_HEIF_COMPRESSION,
sharp::AttrAsStr(options, "heifCompression").data()));
baton->heifSpeed = sharp::AttrAsUint32(options, "heifSpeed");
// Animated output
if (sharp::HasAttr(options, "pageHeight")) {
@@ -1377,7 +1412,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
baton->tileSize = sharp::AttrAsUint32(options, "tileSize");
baton->tileOverlap = sharp::AttrAsUint32(options, "tileOverlap");
baton->tileAngle = sharp::AttrAsInt32(options, "tileAngle");
baton->tileBackground = sharp::AttrAsRgba(options, "tileBackground");
baton->tileBackground = sharp::AttrAsVectorOfDouble(options, "tileBackground");
baton->tileSkipBlanks = sharp::AttrAsInt32(options, "tileSkipBlanks");
baton->tileContainer = static_cast<VipsForeignDzContainer>(
vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_DZ_CONTAINER,
@@ -1389,6 +1424,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
baton->tileDepth = static_cast<VipsForeignDzDepth>(
vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_DZ_DEPTH,
sharp::AttrAsStr(options, "tileDepth").data()));
baton->tileCentre = sharp::AttrAsBool(options, "tileCentre");
// Force random access for certain operations
if (baton->input->access == VIPS_ACCESS_SEQUENTIAL) {

View File

@@ -40,6 +40,7 @@ struct Composite {
int gravity;
int left;
int top;
bool hasOffset;
bool tile;
bool premultiplied;
@@ -47,8 +48,9 @@ struct Composite {
input(nullptr),
mode(VIPS_BLEND_MODE_OVER),
gravity(0),
left(-1),
top(-1),
left(0),
top(0),
hasOffset(false),
tile(false),
premultiplied(false) {}
};
@@ -79,6 +81,7 @@ struct PipelineBaton {
int cropOffsetLeft;
int cropOffsetTop;
bool premultiplied;
bool tileCentre;
std::string kernel;
bool fastShrinkOnLoad;
double tintA;
@@ -118,6 +121,13 @@ struct PipelineBaton {
int extendRight;
std::vector<double> extendBackground;
bool withoutEnlargement;
std::vector<double> affineMatrix;
std::vector<double> affineBackground;
double affineIdx;
double affineIdy;
double affineOdx;
double affineOdy;
vips::VInterpolate affineInterpolator;
int jpegQuality;
bool jpegProgressive;
std::string jpegChromaSubsampling;
@@ -151,10 +161,12 @@ struct PipelineBaton {
double tiffYres;
int heifQuality;
VipsForeignHeifCompression heifCompression;
int heifSpeed;
bool heifLossless;
std::string err;
bool withMetadata;
int withMetadataOrientation;
std::string withMetadataIcc;
std::unique_ptr<double[]> convKernel;
int convKernelWidth;
int convKernelHeight;
@@ -229,6 +241,13 @@ struct PipelineBaton {
extendRight(0),
extendBackground{ 0.0, 0.0, 0.0, 255.0 },
withoutEnlargement(false),
affineMatrix{ 1.0, 0.0, 0.0, 1.0 },
affineBackground{ 0.0, 0.0, 0.0, 255.0 },
affineIdx(0),
affineIdy(0),
affineOdx(0),
affineOdy(0),
affineInterpolator(vips::VInterpolate::new_from_name("bicubic")),
jpegQuality(80),
jpegProgressive(false),
jpegChromaSubsampling("4:2:0"),
@@ -260,8 +279,9 @@ struct PipelineBaton {
tiffTileWidth(256),
tiffXres(1.0),
tiffYres(1.0),
heifQuality(80),
heifCompression(VIPS_FOREIGN_HEIF_COMPRESSION_HEVC),
heifQuality(50),
heifCompression(VIPS_FOREIGN_HEIF_COMPRESSION_AV1),
heifSpeed(5),
heifLossless(false),
withMetadata(false),
withMetadataOrientation(-1),

View File

@@ -22,6 +22,8 @@
#include "stats.h"
static void* sharp_vips_init(void*) {
g_setenv("VIPS_MIN_STACK_SIZE", "2m", FALSE);
g_setenv("PANGOCAIRO_BACKEND", "fontconfig", FALSE);
vips_init("sharp");
return nullptr;
}

View File

@@ -80,12 +80,14 @@ class StatsWorker : public Napi::AsyncWorker {
// Estimate entropy via histogram of greyscale value frequency
baton->entropy = std::abs(greyscale.hist_find().hist_entropy());
// Estimate sharpness via standard deviation of greyscale laplacian
VImage laplacian = VImage::new_matrixv(3, 3,
0.0, 1.0, 0.0,
1.0, -4.0, 1.0,
0.0, 1.0, 0.0);
laplacian.set("scale", 9.0);
baton->sharpness = greyscale.conv(laplacian).deviate();
if (image.width() > 1 || image.height() > 1) {
VImage laplacian = VImage::new_matrixv(3, 3,
0.0, 1.0, 0.0,
1.0, -4.0, 1.0,
0.0, 1.0, 0.0);
laplacian.set("scale", 9.0);
baton->sharpness = greyscale.conv(laplacian).deviate();
}
// Most dominant sRGB colour via 4096-bin 3D histogram
vips::VImage hist = sharp::RemoveAlpha(image)
.colourspace(VIPS_INTERPRETATION_sRGB)

View File

@@ -8,16 +8,16 @@
"test": "node perf && node random && node parallel"
},
"devDependencies": {
"async": "^3.1.0",
"benchmark": "^2.1.4",
"gm": "^1.23.1",
"imagemagick": "^0.1.3",
"jimp": "^0.9.3",
"mapnik": "^4.3.1",
"semver": "^7.1.1"
"async": "3.2.0",
"benchmark": "2.1.4",
"gm": "1.23.1",
"imagemagick": "0.1.3",
"jimp": "0.16.1",
"mapnik": "4.5.5",
"semver": "7.3.4"
},
"license": "Apache-2.0",
"engines": {
"node": ">=8.5.0"
"node": "14"
}
}

View File

@@ -564,8 +564,9 @@ async.series({
},
// PNG
png: function (callback) {
const inputPngBuffer = fs.readFileSync(fixtures.inputPng);
const inputPngBuffer = fs.readFileSync(fixtures.inputPngAlphaPremultiplicationLarge);
const pngSuite = new Benchmark.Suite('png');
const minSamples = 64;
// jimp
pngSuite.add('jimp-buffer-buffer', {
defer: true,
@@ -576,6 +577,8 @@ async.series({
} else {
image
.resize(width, height)
.deflateLevel(6)
.filterType(0)
.getBuffer(jimp.MIME_PNG, function (err) {
if (err) {
throw err;
@@ -589,12 +592,14 @@ async.series({
}).add('jimp-file-file', {
defer: true,
fn: function (deferred) {
jimp.read(fixtures.inputPng, function (err, image) {
jimp.read(fixtures.inputPngAlphaPremultiplicationLarge, function (err, image) {
if (err) {
throw err;
} else {
image
.resize(width, height)
.deflateLevel(6)
.filterType(0)
.write(fixtures.outputPng, function (err) {
if (err) {
throw err;
@@ -610,7 +615,7 @@ async.series({
pngSuite.add('mapnik-file-file', {
defer: true,
fn: function (deferred) {
mapnik.Image.open(fixtures.inputPng, function (err, img) {
mapnik.Image.open(fixtures.inputPngAlphaPremultiplicationLarge, function (err, img) {
if (err) throw err;
img.premultiply(function (err, img) {
if (err) throw err;
@@ -657,11 +662,15 @@ async.series({
defer: true,
fn: function (deferred) {
imagemagick.resize({
srcPath: fixtures.inputPng,
srcPath: fixtures.inputPngAlphaPremultiplicationLarge,
dstPath: fixtures.outputPng,
width: width,
height: height,
filter: 'Lanczos'
filter: 'Lanczos',
customArgs: [
'-define', 'PNG:compression-level=6',
'-define', 'PNG:compression-filter=0'
]
}, function (err) {
if (err) {
throw err;
@@ -675,9 +684,11 @@ async.series({
pngSuite.add('gm-file-file', {
defer: true,
fn: function (deferred) {
gm(fixtures.inputPng)
gm(fixtures.inputPngAlphaPremultiplicationLarge)
.filter('Lanczos')
.resize(width, height)
.define('PNG:compression-level=6')
.define('PNG:compression-filter=0')
.write(fixtures.outputPng, function (err) {
if (err) {
throw err;
@@ -689,9 +700,11 @@ async.series({
}).add('gm-file-buffer', {
defer: true,
fn: function (deferred) {
gm(fixtures.inputPng)
gm(fixtures.inputPngAlphaPremultiplicationLarge)
.filter('Lanczos')
.resize(width, height)
.define('PNG:compression-level=6')
.define('PNG:compression-filter=0')
.toBuffer(function (err, buffer) {
if (err) {
throw err;
@@ -705,9 +718,11 @@ async.series({
// sharp
pngSuite.add('sharp-buffer-file', {
defer: true,
minSamples,
fn: function (deferred) {
sharp(inputPngBuffer)
.resize(width, height)
.png({ compressionLevel: 6 })
.toFile(fixtures.outputPng, function (err) {
if (err) {
throw err;
@@ -718,9 +733,11 @@ async.series({
}
}).add('sharp-buffer-buffer', {
defer: true,
minSamples,
fn: function (deferred) {
sharp(inputPngBuffer)
.resize(width, height)
.png({ compressionLevel: 6 })
.toBuffer(function (err, buffer) {
if (err) {
throw err;
@@ -732,9 +749,11 @@ async.series({
}
}).add('sharp-file-file', {
defer: true,
minSamples,
fn: function (deferred) {
sharp(fixtures.inputPng)
sharp(fixtures.inputPngAlphaPremultiplicationLarge)
.resize(width, height)
.png({ compressionLevel: 6 })
.toFile(fixtures.outputPng, function (err) {
if (err) {
throw err;
@@ -745,9 +764,11 @@ async.series({
}
}).add('sharp-file-buffer', {
defer: true,
minSamples,
fn: function (deferred) {
sharp(fixtures.inputPng)
sharp(fixtures.inputPngAlphaPremultiplicationLarge)
.resize(width, height)
.png({ compressionLevel: 6 })
.toBuffer(function (err, buffer) {
if (err) {
throw err;
@@ -759,10 +780,11 @@ async.series({
}
}).add('sharp-progressive', {
defer: true,
minSamples,
fn: function (deferred) {
sharp(inputPngBuffer)
.resize(width, height)
.png({ progressive: true })
.png({ compressionLevel: 6, progressive: true })
.toBuffer(function (err, buffer) {
if (err) {
throw err;
@@ -774,10 +796,27 @@ async.series({
}
}).add('sharp-adaptiveFiltering', {
defer: true,
minSamples,
fn: function (deferred) {
sharp(inputPngBuffer)
.resize(width, height)
.png({ adaptiveFiltering: true })
.png({ adaptiveFiltering: true, compressionLevel: 6 })
.toBuffer(function (err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add('sharp-compressionLevel=9', {
defer: true,
minSamples,
fn: function (deferred) {
sharp(inputPngBuffer)
.resize(width, height)
.png({ compressionLevel: 9 })
.toBuffer(function (err, buffer) {
if (err) {
throw err;

BIN
test/fixtures/big-height.webp vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

3
test/fixtures/circle.svg vendored Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8">
<circle r="3.75" cx="4" cy="4" fill="deeppink" />
</svg>

After

Width:  |  Height:  |  Size: 122 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
test/fixtures/expected/circle.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
test/fixtures/expected/hilutite.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
test/fixtures/expected/icc-cmyk.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
test/fixtures/expected/tile_centered.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

BIN
test/fixtures/hilutite.icm vendored Normal file

Binary file not shown.

View File

@@ -95,6 +95,7 @@ module.exports = {
inputWebPWithTransparency: getPath('5_webp_a.webp'), // http://www.gstatic.com/webp/gallery3/5_webp_a.webp
inputWebPAnimated: getPath('rotating-squares.webp'), // http://www.gstatic.com/webp/gallery3/5_webp_a.webp
inputWebPAnimatedLoop3: getPath('animated-loop-3.webp'), // http://www.gstatic.com/webp/gallery3/5_webp_a.webp
inputWebPAnimatedBigHeight: getPath('big-height.webp'),
inputTiff: getPath('G31D.TIF'), // http://www.fileformat.info/format/tiff/sample/e6c9a6e5253348f4aef6d17b534360ab/index.htm
inputTiffMultipage: getPath('G31D_MULTI.TIF'), // gm convert G31D.TIF -resize 50% G31D_2.TIF ; tiffcp G31D.TIF G31D_2.TIF G31D_MULTI.TIF
inputTiffCielab: getPath('cielab-dagams.tiff'), // https://github.com/lovell/sharp/issues/646
@@ -106,7 +107,9 @@ module.exports = {
inputGifAnimated: getPath('rotating-squares.gif'), // CC0 https://loading.io/spinner/blocks/-rotating-squares-preloader-gif
inputGifAnimatedLoop3: getPath('animated-loop-3.gif'), // CC-BY-SA-4.0 Petrus3743 https://commons.wikimedia.org/wiki/File:01-Goldener_Schnitt_Formel-Animation.gif
inputSvg: getPath('check.svg'), // http://dev.w3.org/SVG/tools/svgweb/samples/svg-files/check.svg
inputSvgSmallViewBox: getPath('circle.svg'),
inputSvgWithEmbeddedImages: getPath('struct-image-04-t.svg'), // https://dev.w3.org/SVG/profiles/1.2T/test/svg/struct-image-04-t.svg
inputAvif: getPath('cosmos_frame12924_yuv420_10bpc_bt2020_pq_q50.avif'), // CC by-nc-nd https://github.com/AOMediaCodec/av1-avif/tree/master/testFiles/Netflix
inputJPGBig: getPath('flowers.jpeg'),

View File

@@ -39,6 +39,16 @@
Memcheck:Cond
obj:*/libjpeg.so*
}
{
value_jpeg_obj_static
Memcheck:Value8
obj:*/libvips.so*
}
{
cond_jpeg_obj_static
Memcheck:Cond
obj:*/libvips.so*
}
{
param_jpeg_jpeg_finish_compress
Memcheck:Param
@@ -179,6 +189,24 @@
...
fun:gsf_output_write
}
{
param_gsf_new_do_write
Memcheck:Param
write(buf)
...
fun:new_do_write
...
fun:gsf_output_close
}
{
param_gsf_output_write
Memcheck:Param
write(buf)
...
fun:new_do_write
...
fun:gsf_output_write
}
# fontconfig
{
@@ -208,6 +236,84 @@
fun:XML_ParseBuffer
obj:*/libfontconfig.so.*
}
{
leak_fontconfig_FcInitLoadConfigAndFonts
Memcheck:Leak
match-leak-kinds: definite
fun:malloc
...
fun:XML_ParseBuffer
...
fun:FcInitLoadConfigAndFonts
}
# heif
{
cond_heif_encode_image
Memcheck:Cond
...
fun:heif_context_encode_image
}
{
value8_heif_encode_image
Memcheck:Value8
...
fun:heif_context_encode_image
}
{
cond_heif_aom_codec_encode
Memcheck:Cond
...
fun:aom_codec_encode
}
{
value8_heif_aom_codec_encode
Memcheck:Value8
...
fun:aom_codec_encode
}
{
value1_heif_aom_codec_encode
Memcheck:Value1
...
fun:aom_codec_encode
}
{
cond_heif_av1_encode_frame
Memcheck:Cond
...
fun:av1_encode_frame
}
{
value8_heif_av1_encode_frame
Memcheck:Value8
...
fun:av1_encode_frame
}
{
cond_heif_context_write
Memcheck:Cond
...
fun:heif_context_write
}
{
value8_heif_context_write
Memcheck:Value8
...
fun:heif_context_write
}
{
cond_heif_context_read
Memcheck:Cond
...
fun:heif_context_read_from_reader
}
{
value8_heif_context_read
Memcheck:Value8
...
fun:heif_context_read_from_reader
}
# libvips
{
@@ -296,6 +402,30 @@
...
fun:start_thread
}
{
cond_libvips_source_read
Memcheck:Cond
...
fun:vips_source_read
}
{
value8_libvips_source_read
Memcheck:Value8
...
fun:vips_source_read
}
{
cond_libvips_target_finish
Memcheck:Cond
...
fun:vips_target_finish
}
{
value8_libvips_target_finish
Memcheck:Value8
...
fun:vips_target_finish
}
{
leak_libvips_init
Memcheck:Leak
@@ -304,6 +434,42 @@
...
fun:vips__init
}
{
leak_rsvg_static_data
Memcheck:Leak
match-leak-kinds: definite
fun:malloc
...
fun:rsvg_rust_handle_new_from_stream_sync
}
{
leak_rsvg_rsvg_rust_handle_new_from_gfile_sync
Memcheck:Leak
match-leak-kinds: definite
fun:malloc
...
fun:rsvg_rust_handle_new_from_gfile_sync
}
{
leak_rsvg_rust_handle_new_from_stream_sync
Memcheck:Leak
match-leak-kinds: possible
fun:malloc
...
fun:xmlParseElement
...
fun:rsvg_rust_handle_new_from_stream_sync
}
{
leak_rsvg_rust_handle_new_from_gfile_sync
Memcheck:Leak
match-leak-kinds: possible
fun:malloc
...
fun:xmlParseElement
...
fun:rsvg_rust_handle_new_from_gfile_sync
}
# libuv warnings
{
@@ -418,6 +584,13 @@
...
fun:_ZN4node17CreateEnvironmentEPN2v87IsolateEP9uv_loop_sNS0_5LocalINS0_7ContextEEEiPKPKciSB_
}
{
leak_nodejs_CreateEnvironment_IsolateData
Memcheck:Leak
match-leak-kinds: possible
...
fun:_ZN4node17CreateEnvironmentEPNS_11IsolateDataEN2v85LocalINS2_7ContextEEERKSt6vectorISsSaISsEESA_NS_16EnvironmentFlags5FlagsENS_8ThreadIdESt10unique_ptrINS_21InspectorParentHandleESt14default_deleteISF_EE
}
{
leak_nodejs_Environment_Start
Memcheck:Leak
@@ -664,3 +837,27 @@
...
fun:_ZN12v8_inspector10toString16ERKNS_10StringViewE
}
{
cond_v8_Builtins_InterpreterEntryTrampoline
Memcheck:Cond
...
fun:Builtins_InterpreterEntryTrampoline
}
{
cond_v8_ZN2v88internal18ArrayBufferSweeper9SweepFullEv
Memcheck:Cond
...
fun:_ZN2v88internal18ArrayBufferSweeper9SweepFullEv
}
{
cond_v8_ZN4node11Environment27RunAndClearNativeImmediatesEb
Memcheck:Cond
...
fun:_ZN4node11Environment27RunAndClearNativeImmediatesEb
}
{
cond_v8_ZN2v88internal18ArrayBufferSweeper10ReleaseAllEv
Memcheck:Cond
...
fun:_ZN2v88internal18ArrayBufferSweeper10ReleaseAllEv
}

175
test/unit/affine.js Normal file
View File

@@ -0,0 +1,175 @@
'use strict';
const assert = require('assert');
const sharp = require('../../');
const fixtures = require('../fixtures');
describe('Affine transform', () => {
describe('Invalid input', () => {
it('Missing matrix', () => {
assert.throws(() => {
sharp(fixtures.inputJpg)
.affine();
});
});
it('Invalid 1d matrix', () => {
assert.throws(() => {
sharp(fixtures.inputJpg)
.affine(['123', 123, 123, 123]);
});
});
it('Invalid 2d matrix', () => {
assert.throws(() => {
sharp(fixtures.inputJpg)
.affine([[123, 123], [null, 123]]);
});
});
it('Invalid options parameter type', () => {
assert.throws(() => {
sharp(fixtures.inputJpg)
.affine([[1, 0], [0, 1]], 'invalid options type');
});
});
it('Invalid background color', () => {
assert.throws(() => {
sharp(fixtures.inputJpg)
.affine([4, 4, 4, 4], { background: 'not a color' });
});
});
it('Invalid idx offset type', () => {
assert.throws(() => {
sharp(fixtures.inputJpg)
.affine([[4, 4], [4, 4]], { idx: 'invalid idx type' });
});
});
it('Invalid idy offset type', () => {
assert.throws(() => {
sharp(fixtures.inputJpg)
.affine([4, 4, 4, 4], { idy: 'invalid idy type' });
});
});
it('Invalid odx offset type', () => {
assert.throws(() => {
sharp(fixtures.inputJpg)
.affine([[4, 4], [4, 4]], { odx: 'invalid odx type' });
});
});
it('Invalid ody offset type', () => {
assert.throws(() => {
sharp(fixtures.inputJpg)
.affine([[4, 4], [4, 4]], { ody: 'invalid ody type' });
});
});
it('Invalid interpolator', () => {
assert.throws(() => {
sharp(fixtures.inputJpg)
.affine([[4, 4], [4, 4]], { interpolator: 'cubic' });
});
});
});
it('Applies identity matrix', done => {
const input = fixtures.inputJpg;
sharp(input)
.affine([[1, 0], [0, 1]])
.toBuffer((err, data) => {
if (err) throw err;
fixtures.assertSimilar(input, data, done);
});
});
it('Applies resize affine matrix', done => {
const input = fixtures.inputJpg;
const inputWidth = 2725;
const inputHeight = 2225;
sharp(input)
.affine([[0.2, 0], [0, 1.5]])
.toBuffer((err, data, info) => {
if (err) throw err;
fixtures.assertSimilar(input, data, done);
assert.strictEqual(info.width, Math.ceil(inputWidth * 0.2));
assert.strictEqual(info.height, Math.ceil(inputHeight * 1.5));
});
});
it('Resizes and applies affine transform', done => {
const input = fixtures.inputJpg;
sharp(input)
.resize(500, 500)
.affine([[0.5, 1], [1, 0.5]])
.toBuffer((err, data) => {
if (err) throw err;
fixtures.assertSimilar(data, fixtures.expected('affine-resize-expected.jpg'), done);
});
});
it('Extracts and applies affine transform', done => {
sharp(fixtures.inputJpg)
.extract({ left: 300, top: 300, width: 600, height: 600 })
.affine([0.3, 0, -0.5, 0.3])
.toBuffer((err, data) => {
if (err) throw err;
fixtures.assertSimilar(data, fixtures.expected('affine-extract-expected.jpg'), done);
});
});
it('Rotates and applies affine transform', done => {
sharp(fixtures.inputJpg320x240)
.rotate(90)
.affine([[-1.2, 0], [0, -1.2]])
.toBuffer((err, data) => {
if (err) throw err;
fixtures.assertSimilar(data, fixtures.expected('affine-rotate-expected.jpg'), done);
});
});
it('Extracts, rotates and applies affine transform', done => {
sharp(fixtures.inputJpg)
.extract({ left: 1000, top: 1000, width: 200, height: 200 })
.rotate(45, { background: 'blue' })
.affine([[2, 1], [2, -0.5]], { background: 'red' })
.toBuffer((err, data) => {
if (err) throw err;
fixtures.assertSimilar(fixtures.expected('affine-extract-rotate-expected.jpg'), data, done);
});
});
it('Applies affine transform with background color', done => {
sharp(fixtures.inputJpg320x240)
.rotate(180)
.affine([[-1.5, 1.2], [-1, 1]], { background: 'red' })
.toBuffer((err, data) => {
if (err) throw err;
fixtures.assertSimilar(fixtures.expected('affine-background-expected.jpg'), data, done);
});
});
it('Applies affine transform with background color and output offsets', done => {
sharp(fixtures.inputJpg320x240)
.rotate(180)
.affine([[-2, 1.5], [-1, 2]], { background: 'blue', odx: 40, ody: -100 })
.toBuffer((err, data) => {
if (err) throw err;
fixtures.assertSimilar(fixtures.expected('affine-background-output-offsets-expected.jpg'), data, done);
});
});
it('Applies affine transform with background color and all offsets', done => {
sharp(fixtures.inputJpg320x240)
.rotate(180)
.affine([[-1.2, 1.8], [-1, 2]], { background: 'yellow', idx: 10, idy: -40, odx: 10, ody: -50 })
.toBuffer((err, data) => {
if (err) throw err;
fixtures.assertSimilar(fixtures.expected('affine-background-all-offsets-expected.jpg'), data, done);
});
});
describe('Interpolations', () => {
const input = fixtures.inputJpg320x240;
const inputWidth = 320;
const inputHeight = 240;
for (const interp in sharp.interpolators) {
it(`Performs 2x upscale with ${interp} interpolation`, done => {
sharp(input)
.affine([[2, 0], [0, 2]], { interpolator: sharp.interpolators[interp] })
.toBuffer((err, data, info) => {
if (err) throw err;
assert.strictEqual(info.width, Math.ceil(inputWidth * 2));
assert.strictEqual(info.height, Math.ceil(inputHeight * 2));
fixtures.assertSimilar(fixtures.expected(`affine-${sharp.interpolators[interp]}-2x-upscale-expected.jpg`), data, done);
});
});
}
});
});

84
test/unit/avif.js Normal file
View File

@@ -0,0 +1,84 @@
'use strict';
const assert = require('assert');
const sharp = require('../../');
const { inputAvif, inputJpg } = require('../fixtures');
describe('AVIF', () => {
it('called without options does not throw an error', () => {
assert.doesNotThrow(() => {
sharp().avif();
});
});
it('can passthrough AVIF', async () => {
const data = await sharp(inputAvif)
.resize(32)
.toBuffer();
const metadata = await sharp(data)
.metadata();
const { size, ...metadataWithoutSize } = metadata;
assert.deepStrictEqual(metadataWithoutSize, {
channels: 3,
depth: 'uchar',
format: 'heif',
hasAlpha: false,
hasProfile: false,
height: 12,
isProgressive: false,
pageHeight: 12,
pagePrimary: 0,
pages: 1,
space: 'srgb',
width: 32
});
});
it('can convert AVIF to JPEG', async () => {
const data = await sharp(inputAvif)
.resize(32)
.jpeg()
.toBuffer();
const metadata = await sharp(data)
.metadata();
const { size, ...metadataWithoutSize } = metadata;
assert.deepStrictEqual(metadataWithoutSize, {
channels: 3,
chromaSubsampling: '4:2:0',
density: 72,
depth: 'uchar',
format: 'jpeg',
hasAlpha: false,
hasProfile: false,
height: 13,
isProgressive: false,
space: 'srgb',
width: 32
});
});
it('can convert JPEG to AVIF', async () => {
const data = await sharp(inputJpg)
.resize(32)
.avif()
.toBuffer();
const metadata = await sharp(data)
.metadata();
const { size, ...metadataWithoutSize } = metadata;
assert.deepStrictEqual(metadataWithoutSize, {
channels: 3,
depth: 'uchar',
format: 'heif',
hasAlpha: false,
hasProfile: false,
height: 26,
isProgressive: false,
pageHeight: 26,
pagePrimary: 0,
pages: 1,
space: 'srgb',
width: 32
});
});
});

View File

@@ -172,6 +172,24 @@ describe('composite', () => {
});
});
it('negative offset and gravity', done => {
sharp(fixtures.inputJpg)
.resize(400)
.composite([{
input: fixtures.inputPngWithTransparency16bit,
left: -10,
top: -10,
gravity: 4
}])
.toBuffer((err, data, info) => {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3, info.channels);
fixtures.assertSimilar(
fixtures.expected('overlay-negative-offset-with-gravity.jpg'), data, done);
});
});
it('offset, gravity and tile', done => {
sharp(fixtures.inputJpg)
.resize(80)
@@ -333,13 +351,25 @@ describe('composite', () => {
it('invalid left', () => {
assert.throws(() => {
sharp().composite([{ input: 'test', left: 0.5 }]);
}, /Expected positive integer for left but received 0.5 of type number/);
}, /Expected integer for left but received 0.5 of type number/);
assert.throws(() => {
sharp().composite([{ input: 'test', left: 'invalid' }]);
}, /Expected integer for left but received invalid of type string/);
assert.throws(() => {
sharp().composite([{ input: 'test', left: 'invalid', top: 10 }]);
}, /Expected integer for left but received invalid of type string/);
});
it('invalid top', () => {
assert.throws(() => {
sharp().composite([{ input: 'test', top: -1 }]);
}, /Expected positive integer for top but received -1 of type number/);
sharp().composite([{ input: 'test', top: 0.5 }]);
}, /Expected integer for top but received 0.5 of type number/);
assert.throws(() => {
sharp().composite([{ input: 'test', top: 'invalid' }]);
}, /Expected integer for top but received invalid of type string/);
assert.throws(() => {
sharp().composite([{ input: 'test', top: 'invalid', left: 10 }]);
}, /Expected integer for top but received invalid of type string/);
});
it('left but no top', () => {

View File

@@ -98,4 +98,36 @@ describe('GIF input', () => {
sharp().gif({ delay: [65536] });
});
});
it('should work with streams when only animated is set', function (done) {
if (sharp.format.magick.output.buffer) {
fs.createReadStream(fixtures.inputGifAnimated)
.pipe(sharp({ animated: true }))
.gif()
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('gif', info.format);
fixtures.assertSimilar(fixtures.inputGifAnimated, data, done);
});
} else {
done();
}
});
it('should work with streams when only pages is set', function (done) {
if (sharp.format.magick.output.buffer) {
fs.createReadStream(fixtures.inputGifAnimated)
.pipe(sharp({ pages: -1 }))
.gif()
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('gif', info.format);
fixtures.assertSimilar(fixtures.inputGifAnimated, data, done);
});
} else {
done();
}
});
});

View File

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

View File

@@ -302,7 +302,6 @@ describe('Input/output', function () {
});
it('Fail when input is empty Buffer', function (done) {
if (sharp.format.magick.input.buffer) return this.skip(); // can be removed with libvips 8.10.0+
sharp(Buffer.alloc(0)).toBuffer().then(function () {
assert(false);
done();
@@ -647,7 +646,16 @@ describe('Input/output', function () {
it('Invalid density: string', function () {
assert.throws(function () {
sharp({ density: 'zoinks' });
}, /Expected number between 1 and 2400 for density but received zoinks of type string/);
}, /Expected number between 1 and 100000 for density but received zoinks of type string/);
});
it('Setting animated property updates pages property', function () {
assert.strictEqual(sharp({ animated: false }).options.input.pages, 1);
assert.strictEqual(sharp({ animated: true }).options.input.pages, -1);
});
it('Invalid animated property throws', function () {
assert.throws(function () {
sharp({ animated: -1 });
}, /Expected boolean for animated but received -1 of type number/);
});
it('Invalid page property throws', function () {
assert.throws(function () {

View File

@@ -501,6 +501,42 @@ describe('Image metadata', function () {
});
});
it('Apply CMYK output ICC profile', function (done) {
const output = fixtures.path('output.icc-cmyk.jpg');
sharp(fixtures.inputJpg)
.withMetadata({ icc: 'cmyk' })
.toFile(output, function (err, info) {
if (err) throw err;
sharp(output).metadata(function (err, metadata) {
if (err) throw err;
assert.strictEqual(true, metadata.hasProfile);
assert.strictEqual('cmyk', metadata.space);
assert.strictEqual(4, metadata.channels);
// ICC
assert.strictEqual('object', typeof metadata.icc);
assert.strictEqual(true, metadata.icc instanceof Buffer);
const profile = icc.parse(metadata.icc);
assert.strictEqual('object', typeof profile);
assert.strictEqual('CMYK', profile.colorSpace);
assert.strictEqual('Relative', profile.intent);
assert.strictEqual('Printer', profile.deviceClass);
});
fixtures.assertSimilar(output, fixtures.path('expected/icc-cmyk.jpg'), { threshold: 0 }, done);
});
});
it('Apply custom output ICC profile', function (done) {
const output = fixtures.path('output.hilutite.jpg');
sharp(fixtures.inputJpg)
.withMetadata({ icc: fixtures.path('hilutite.icm') })
.toFile(output, function (err, info) {
if (err) throw err;
fixtures.assertMaxColourDistance(output, fixtures.path('expected/hilutite.jpg'), 0);
fixtures.assertMaxColourDistance(output, fixtures.inputJpg, 16.5);
done();
});
});
it('Include metadata in output, enabled via empty object', () =>
sharp(fixtures.inputJpgWithExif)
.withMetadata({})
@@ -675,5 +711,10 @@ describe('Image metadata', function () {
sharp().withMetadata({ orientation: 9 });
});
});
it('Non string icc', function () {
assert.throws(function () {
sharp().withMetadata({ icc: true });
});
});
});
});

View File

@@ -7,7 +7,7 @@ const sharp = require('../../');
const fixtures = require('../fixtures');
// Test Helpers
var threshold = 0.001;
const threshold = 0.001;
function isInAcceptableRange (actual, expected) {
return actual >= ((1 - threshold) * expected) && actual <= ((1 + threshold) * expected);
}
@@ -279,10 +279,10 @@ describe('Image Stats', function () {
// red channel
assert.strictEqual(0, stats.channels[0].min);
assert.strictEqual(true, isInRange(stats.channels[0].max, 254, 255));
assert.strictEqual(true, isInAcceptableRange(stats.channels[0].sum, 82506996));
assert.strictEqual(true, isInAcceptableRange(stats.channels[0].squaresSum, 11213984832));
assert.strictEqual(true, isInAcceptableRange(stats.channels[0].mean, 104.36947963892487));
assert.strictEqual(true, isInAcceptableRange(stats.channels[0].stdev, 57.379896254993135));
assert.strictEqual(true, isInAcceptableRange(stats.channels[0].sum, 83291370));
assert.strictEqual(true, isInAcceptableRange(stats.channels[0].squaresSum, 11379783198));
assert.strictEqual(true, isInAcceptableRange(stats.channels[0].mean, 105.36169496842616));
assert.strictEqual(true, isInAcceptableRange(stats.channels[0].stdev, 57.39412151419967));
assert.strictEqual(true, isInteger(stats.channels[0].minX));
assert.strictEqual(true, isInRange(stats.channels[0].minX, 0, 1024));
assert.strictEqual(true, isInteger(stats.channels[0].minY));
@@ -295,10 +295,10 @@ describe('Image Stats', function () {
// green channel
assert.strictEqual(0, stats.channels[1].min);
assert.strictEqual(true, isInRange(stats.channels[1].max, 254, 255));
assert.strictEqual(true, isInAcceptableRange(stats.channels[1].sum, 120089056));
assert.strictEqual(true, isInAcceptableRange(stats.channels[1].squaresSum, 20533721114));
assert.strictEqual(true, isInAcceptableRange(stats.channels[1].mean, 151.90993361398964));
assert.strictEqual(true, isInAcceptableRange(stats.channels[1].stdev, 53.83370206587037));
assert.strictEqual(true, isInAcceptableRange(stats.channels[1].sum, 120877425));
assert.strictEqual(true, isInAcceptableRange(stats.channels[1].squaresSum, 20774687595));
assert.strictEqual(true, isInAcceptableRange(stats.channels[1].mean, 152.9072025279307));
assert.strictEqual(true, isInAcceptableRange(stats.channels[1].stdev, 53.84143349689916));
assert.strictEqual(true, isInteger(stats.channels[1].minX));
assert.strictEqual(true, isInRange(stats.channels[1].minX, 0, 1024));
assert.strictEqual(true, isInteger(stats.channels[1].minY));
@@ -311,10 +311,10 @@ describe('Image Stats', function () {
// blue channel
assert.strictEqual(0, stats.channels[2].min);
assert.strictEqual(true, isInRange(stats.channels[2].max, 254, 255));
assert.strictEqual(true, isInAcceptableRange(stats.channels[2].sum, 138153653));
assert.strictEqual(true, isInAcceptableRange(stats.channels[2].squaresSum, 28172033081));
assert.strictEqual(true, isInAcceptableRange(stats.channels[2].mean, 174.76123932359133));
assert.strictEqual(true, isInAcceptableRange(stats.channels[2].stdev, 71.38276338513747));
assert.strictEqual(true, isInAcceptableRange(stats.channels[2].sum, 138938859));
assert.strictEqual(true, isInAcceptableRange(stats.channels[2].squaresSum, 28449125593));
assert.strictEqual(true, isInAcceptableRange(stats.channels[2].mean, 175.75450711423252));
assert.strictEqual(true, isInAcceptableRange(stats.channels[2].stdev, 71.39929031070358));
assert.strictEqual(true, isInteger(stats.channels[2].minX));
assert.strictEqual(true, isInRange(stats.channels[2].minX, 0, 1024));
assert.strictEqual(true, isInteger(stats.channels[2].minY));
@@ -462,6 +462,19 @@ describe('Image Stats', function () {
})
);
it('Entropy and sharpness of 1x1 input are zero', async () => {
const { entropy, sharpness } = await sharp({
create: {
width: 1,
height: 1,
channels: 3,
background: 'red'
}
}).stats();
assert.strictEqual(entropy, 0);
assert.strictEqual(sharpness, 0);
});
it('Stream in, Callback out', function (done) {
const readable = fs.createReadStream(fixtures.inputJpg);
const pipeline = sharp().stats(function (err, stats) {

View File

@@ -48,6 +48,31 @@ describe('SVG input', function () {
});
});
it('Convert SVG to PNG at DPI larger than 2400', function (done) {
const size = 1024;
sharp(fixtures.inputSvgSmallViewBox).metadata(function (err, metadata) {
if (err) throw err;
const density = (size / Math.max(metadata.width, metadata.height)) * metadata.density;
sharp(fixtures.inputSvgSmallViewBox, { density })
.resize(size)
.toFormat('png')
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('png', info.format);
assert.strictEqual(size, info.width);
assert.strictEqual(size, info.height);
fixtures.assertSimilar(fixtures.expected('circle.png'), data, function (err) {
if (err) throw err;
sharp(data).metadata(function (err, info) {
if (err) throw err;
assert.strictEqual(9216, info.density);
done();
});
});
});
});
});
it('Convert SVG to PNG at 14.4DPI', function (done) {
sharp(fixtures.inputSvg, { density: 14.4 })
.toFormat('png')

View File

@@ -161,9 +161,7 @@ describe('TIFF', function () {
.then(() => sharp(fixtures.outputTiff)
.metadata()
.then(({ density }) => {
assert.strictEqual(true,
density === 2540 || // libvips <= 8.8.2
density === 25400); // libvips >= 8.8.3
assert.strictEqual(25400, density);
return promisify(rimraf)(fixtures.outputTiff);
})
)
@@ -179,9 +177,7 @@ describe('TIFF', function () {
.then(data => sharp(data)
.metadata()
.then(({ density }) => {
assert.strictEqual(true,
density === 2540 || // libvips <= 8.8.2
density === 25400); // libvips >= 8.8.3
assert.strictEqual(25400, density);
})
)
);

View File

@@ -289,6 +289,14 @@ describe('Tile', function () {
});
});
it('Invalid center parameter value fail', function () {
assert.throws(function () {
sharp().tile({
centre: 'true'
});
});
});
it('Deep Zoom layout', function (done) {
const directory = fixtures.path('output.dzi_files');
rimraf(directory, function () {
@@ -765,6 +773,46 @@ describe('Tile', function () {
});
});
it('Google layout with center image in tile', function (done) {
const directory = fixtures.path('output.google_center.dzi');
rimraf(directory, function () {
sharp(fixtures.inputJpg)
.tile({
center: true,
layout: 'google'
})
.toFile(directory, function (err, info) {
if (err) throw err;
assert.strictEqual('dz', info.format);
assert.strictEqual(2725, info.width);
assert.strictEqual(2225, info.height);
assert.strictEqual(3, info.channels);
assert.strictEqual('number', typeof info.size);
fixtures.assertSimilar(fixtures.expected('tile_centered.jpg'), fs.readFileSync(path.join(directory, '0', '0', '0.jpg')), done);
});
});
});
it('Google layout with center image in tile centre', function (done) {
const directory = fixtures.path('output.google_center.dzi');
rimraf(directory, function () {
sharp(fixtures.inputJpg)
.tile({
centre: true,
layout: 'google'
})
.toFile(directory, function (err, info) {
if (err) throw err;
assert.strictEqual('dz', info.format);
assert.strictEqual(2725, info.width);
assert.strictEqual(2225, info.height);
assert.strictEqual(3, info.channels);
assert.strictEqual('number', typeof info.size);
fixtures.assertSimilar(fixtures.expected('tile_centered.jpg'), fs.readFileSync(path.join(directory, '0', '0', '0.jpg')), done);
});
});
});
it('IIIF layout', function (done) {
const directory = fixtures.path('output.iiif.info');
rimraf(directory, function () {

View File

@@ -16,4 +16,12 @@ describe('toBuffer', () => {
});
});
});
it('correctly process animated webp with height > 16383', (done) => {
const image = sharp(fixtures.inputWebPAnimatedBigHeight, { animated: true });
image.toBuffer().then((buff) => {
assert.strictEqual(Buffer.isBuffer(buff), true);
done();
});
});
});

View File

@@ -1,5 +1,6 @@
'use strict';
const fs = require('fs');
const assert = require('assert');
const sharp = require('../../');
@@ -184,4 +185,28 @@ describe('WebP', function () {
assert.deepStrictEqual(updated.delay, expectedDelay);
});
it('should work with streams when only animated is set', function (done) {
fs.createReadStream(fixtures.inputWebPAnimated)
.pipe(sharp({ animated: true }))
.webp({ lossless: true })
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('webp', info.format);
fixtures.assertSimilar(fixtures.inputWebPAnimated, data, done);
});
});
it('should work with streams when only pages is set', function (done) {
fs.createReadStream(fixtures.inputWebPAnimated)
.pipe(sharp({ pages: -1 }))
.webp({ lossless: true })
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('webp', info.format);
fixtures.assertSimilar(fixtures.inputWebPAnimated, data, done);
});
});
});