Compare commits

..

8 Commits

Author SHA1 Message Date
Lovell Fuller
4b1680c312 Prerelease v0.35.0-rc.0 2026-01-02 11:12:47 +00:00
Lovell Fuller
2346722c0d Upgrade sharp-libvips to v1.3.0-rc.2 2026-01-02 10:58:18 +00:00
Dmytro Tiapukhin
a5e726002c Add margin option to trim operation #4480 2026-01-02 09:33:40 +00:00
Lovell Fuller
d161e45e06 TypeScript: Ensure 'FormatEnum' keys match reality #4475
Renames format.jp2k as format.jp2 for consistency
2026-01-02 08:04:46 +00:00
Lovell Fuller
006d37b2d0 Add AVIF/HEIF 'tune' option to control quality metrics #4227 2026-01-01 22:41:42 +00:00
Lovell Fuller
0d872bd13a Add WebP 'exact' option for control over transparent pixels 2026-01-01 19:19:20 +00:00
Lovell Fuller
1cf4b7f04d Deprecate win32-ia32 prebuilt binaries 2025-12-31 10:16:09 +00:00
Lovell Fuller
e50c0c2e04 CI: Migrate FreeBSD from Cirrus to GitHub Actions 2025-12-31 09:48:59 +00:00
42 changed files with 338 additions and 113 deletions

View File

@@ -1,18 +0,0 @@
freebsd_instance:
image_family: freebsd-16-0-snap
task:
name: FreeBSD
env:
IGNORE_OSVERSION: yes
skip_notifications: true
prerequisites_script:
- pkg update -f
- pkg upgrade -y
- pkg install -y devel/git devel/pkgconf graphics/vips www/node22 www/npm
- pkg-config --modversion vips-cpp
install_script:
- npm install
- npm run build
test_script:
- node --test test/unit/io.js

View File

@@ -117,11 +117,6 @@ jobs:
nodejs_version_major: 20
platform: win32-ia32
package: true
- os: windows-2022
nodejs_arch: x86
nodejs_version: "22.21.0"
nodejs_version_major: 22
platform: win32-ia32
- os: windows-2022
nodejs_arch: x64
nodejs_version: "^20.9.0"
@@ -293,6 +288,26 @@ jobs:
path: npm/${{ matrix.platform }}
retention-days: 1
if-no-files-found: error
build-freebsd:
permissions:
contents: read
needs: lint
name: "build-freebsd"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: vmactions/freebsd-vm@v1
continue-on-error: true
with:
prepare: |
pkg update -f
pkg upgrade -y
pkg install -y devel/git devel/pkgconf graphics/vips www/node22 www/npm
pkg-config --modversion vips-cpp
run: |
npm install
npm run build
node --test test/unit/io.js
build-emscripten:
permissions:
contents: read

View File

@@ -326,3 +326,6 @@ GitHub: https://github.com/tpatel
Name: Maël Nison
GitHub: https://github.com/arcanis
Name: Dmytro Tiapukhin
GitHub: https://github.com/eddienubes

View File

@@ -563,6 +563,7 @@ Use these WebP options for output image.
| [options.delay] | <code>number</code> \| <code>Array.&lt;number&gt;</code> | | delay(s) between animation frames (in milliseconds) |
| [options.minSize] | <code>boolean</code> | <code>false</code> | prevent use of animation key frames to minimise file size (slow) |
| [options.mixed] | <code>boolean</code> | <code>false</code> | allow mixture of lossy and lossless animation frames (slow) |
| [options.exact] | <code>boolean</code> | <code>false</code> | preserve the colour data in transparent pixels |
| [options.force] | <code>boolean</code> | <code>true</code> | force WebP output, otherwise attempt to use input format |
**Example**
@@ -757,6 +758,7 @@ When using Windows ARM64, this feature requires a CPU with ARM64v8.4 or later.
| [options.effort] | <code>number</code> | <code>4</code> | CPU effort, between 0 (fastest) and 9 (slowest) |
| [options.chromaSubsampling] | <code>string</code> | <code>&quot;&#x27;4:4:4&#x27;&quot;</code> | set to '4:2:0' to use chroma subsampling |
| [options.bitdepth] | <code>number</code> | <code>8</code> | set bitdepth to 8, 10 or 12 bit |
| [options.tune] | <code>string</code> | <code>&quot;&#x27;iq&#x27;&quot;</code> | tune output for a quality metric, one of 'iq' (default), 'ssim' or 'psnr' |
**Example**
```js
@@ -796,6 +798,7 @@ globally-installed libvips compiled with support for libheif, libde265 and x265.
| [options.effort] | <code>number</code> | <code>4</code> | CPU effort, between 0 (fastest) and 9 (slowest) |
| [options.chromaSubsampling] | <code>string</code> | <code>&quot;&#x27;4:4:4&#x27;&quot;</code> | set to '4:2:0' to use chroma subsampling |
| [options.bitdepth] | <code>number</code> | <code>8</code> | set bitdepth to 8, 10 or 12 bit |
| [options.tune] | <code>string</code> | <code>&quot;&#x27;ssim&#x27;&quot;</code> | tune output for a quality metric, one of 'ssim' (default), 'psnr' or 'iq' |
**Example**
```js

View File

@@ -284,6 +284,7 @@ The `info` response Object will contain `trimOffsetLeft` and `trimOffsetTop` pro
| [options.background] | <code>string</code> \| <code>Object</code> | <code>&quot;&#x27;top-left pixel&#x27;&quot;</code> | Background colour, parsed by the [color](https://www.npmjs.org/package/color) module, defaults to that of the top-left pixel. |
| [options.threshold] | <code>number</code> | <code>10</code> | Allowed difference from the above colour, a positive number. |
| [options.lineArt] | <code>boolean</code> | <code>false</code> | Does the input more closely resemble line art (e.g. vector) rather than being photographic? |
| [options.margin] | <code>number</code> | <code>0</code> | Leave a margin around trimmed content, value is in pixels. |
**Example**
```js
@@ -320,4 +321,13 @@ const output = await sharp(input)
threshold: 42,
})
.toBuffer();
```
**Example**
```js
// Trim image leaving (up to) a 10 pixel margin around the trimmed content.
const output = await sharp(input)
.trim({
margin: 10
})
.toBuffer();
```

View File

@@ -8,16 +8,34 @@ slug: changelog/v0.35.0
* Breaking: Remove `install` script from `package.json` file.
Compiling from source is now opt-in via the `build` script.
* Breaking: AVIF output is now tuned using SSIMULACRA2-based `iq` quality metrics rather than `ssim`.
* Breaking: Remove deprecated `failOnError` constructor property.
* Breaking: Remove deprecated `paletteBitDepth` from `metadata` response.
* Breaking: Remove deprecated properties from `sharpen` operation.
* Breaking: Rename `format.jp2k` as `format.jp2` for API consistency.
* Upgrade to libvips v8.18.0 for upstream bug fixes.
* Deprecate Windows 32-bit (win32-ia32) prebuilt binaries.
* Add AVIF/HEIF `tune` option for control over quality metrics.
[#4227](https://github.com/lovell/sharp/issues/4227)
* Add `withGainMap` to process HDR JPEG images with embedded gain maps.
[#4314](https://github.com/lovell/sharp/issues/4314)
* Add `toUint8Array` for output image as a `TypedArray` backed by a transferable `ArrayBuffer`.
[#4355](https://github.com/lovell/sharp/issues/4355)
* TypeScript: Ensure `FormatEnum` keys match reality.
[#4475](https://github.com/lovell/sharp/issues/4475)
* Add `margin` option to `trim` operation.
[#4480](https://github.com/lovell/sharp/issues/4480)
[@eddienubes](https://github.com/eddienubes)
* Add WebP `exact` option for control over transparent pixel colour values.

View File

@@ -50,8 +50,8 @@ Ready-compiled sharp and libvips binaries are provided for use on the most commo
* Linux s390x (glibc >= 2.36)
* Linux x64 (glibc >= 2.26, musl >= 1.2.2, CPU with SSE4.2)
* Windows x64
* Windows x86
* Windows ARM64 (experimental, CPU with ARMv8.4 required for all features)
* Windows x86 (deprecated, Node.js 20 only)
* Windows ARM64 (CPU with ARMv8.4 required for all features)
This provides support for the
JPEG, PNG, WebP, AVIF (limited to 8-bit depth), TIFF, GIF and SVG (input) image formats.

View File

@@ -278,6 +278,7 @@ const Sharp = function (input, options) {
trimBackground: [],
trimThreshold: -1,
trimLineArt: false,
trimMargin: 0,
dilateWidth: 0,
erodeWidth: 0,
gamma: 0,
@@ -350,6 +351,7 @@ const Sharp = function (input, options) {
webpEffort: 4,
webpMinSize: false,
webpMixed: false,
webpExact: false,
gifBitdepth: 8,
gifEffort: 7,
gifDither: 1,
@@ -377,6 +379,7 @@ const Sharp = function (input, options) {
heifEffort: 4,
heifChromaSubsampling: '4:4:4',
heifBitdepth: 8,
heifTune: 'ssim',
jxlDistance: 1,
jxlDecodingTier: 0,
jxlEffort: 7,

22
lib/index.d.ts vendored
View File

@@ -840,7 +840,7 @@ declare namespace sharp {
* @returns A sharp instance that can be used to chain operations
*/
toFormat(
format: keyof FormatEnum | AvailableFormatInfo,
format: keyof FormatEnum | AvailableFormatInfo | "avif",
options?:
| OutputOptions
| JpegOptions
@@ -1167,6 +1167,8 @@ declare namespace sharp {
type HeifCompression = 'av1' | 'hevc';
type HeifTune = 'iq' | 'ssim' | 'psnr';
type Unit = 'inch' | 'cm';
interface WriteableMetadata {
@@ -1394,11 +1396,13 @@ declare namespace sharp {
/** Level of CPU effort to reduce file size, integer 0-6 (optional, default 4) */
effort?: number | undefined;
/** Prevent use of animation key frames to minimise file size (slow) (optional, default false) */
minSize?: boolean;
minSize?: boolean | undefined;
/** Allow mixture of lossy and lossless animation frames (slow) (optional, default false) */
mixed?: boolean;
mixed?: boolean | undefined;
/** Preset options: one of default, photo, picture, drawing, icon, text (optional, default 'default') */
preset?: keyof PresetEnum | undefined;
/** Preserve the colour data in transparent pixels (optional, default false) */
exact?: boolean | undefined;
}
interface AvifOptions extends OutputOptions {
@@ -1412,6 +1416,8 @@ declare namespace sharp {
chromaSubsampling?: string | undefined;
/** Set bitdepth to 8, 10 or 12 bit (optional, default 8) */
bitdepth?: 8 | 10 | 12 | undefined;
/** Tune output for a quality metric, one of 'iq', 'ssim' or 'psnr' (optional, default 'iq') */
tune?: HeifTune | undefined;
}
interface HeifOptions extends OutputOptions {
@@ -1427,6 +1433,8 @@ declare namespace sharp {
chromaSubsampling?: string | undefined;
/** Set bitdepth to 8, 10 or 12 bit (optional, default 8) */
bitdepth?: 8 | 10 | 12 | undefined;
/** Tune output for a quality metric, one of 'ssim', 'psnr' or 'iq' (optional, default 'ssim') */
tune?: HeifTune | undefined;
}
interface GifOptions extends OutputOptions, AnimationOptions {
@@ -1598,6 +1606,8 @@ declare namespace sharp {
threshold?: number | undefined;
/** Does the input more closely resemble line art (e.g. vector) rather than being photographic? (optional, default false) */
lineArt?: boolean | undefined;
/** Leave a margin around trimmed content, value is in pixels. (optional, default 0) */
margin?: number | undefined;
}
interface RawOptions {
@@ -1903,16 +1913,13 @@ declare namespace sharp {
}
interface FormatEnum {
avif: AvailableFormatInfo;
dcraw: AvailableFormatInfo;
dz: AvailableFormatInfo;
exr: AvailableFormatInfo;
fits: AvailableFormatInfo;
gif: AvailableFormatInfo;
heif: AvailableFormatInfo;
input: AvailableFormatInfo;
jpeg: AvailableFormatInfo;
jpg: AvailableFormatInfo;
jp2: AvailableFormatInfo;
jxl: AvailableFormatInfo;
magick: AvailableFormatInfo;
@@ -1924,8 +1931,7 @@ declare namespace sharp {
raw: AvailableFormatInfo;
svg: AvailableFormatInfo;
tiff: AvailableFormatInfo;
tif: AvailableFormatInfo;
v: AvailableFormatInfo;
vips: AvailableFormatInfo;
webp: AvailableFormatInfo;
}

View File

@@ -76,7 +76,7 @@ function toFile (fileOut, callback) {
err = new Error('Missing output file path');
} else if (is.string(this.options.input.file) && path.resolve(this.options.input.file) === path.resolve(fileOut)) {
err = new Error('Cannot use same file for input and output');
} else if (jp2Regex.test(path.extname(fileOut)) && !this.constructor.format.jp2k.output.file) {
} else if (jp2Regex.test(path.extname(fileOut)) && !this.constructor.format.jp2.output.file) {
err = errJp2Save();
}
if (err) {
@@ -749,6 +749,7 @@ function png (options) {
* @param {number|number[]} [options.delay] - delay(s) between animation frames (in milliseconds)
* @param {boolean} [options.minSize=false] - prevent use of animation key frames to minimise file size (slow)
* @param {boolean} [options.mixed=false] - allow mixture of lossy and lossless animation frames (slow)
* @param {boolean} [options.exact=false] - preserve the colour data in transparent pixels
* @param {boolean} [options.force=true] - force WebP output, otherwise attempt to use input format
* @returns {Sharp}
* @throws {Error} Invalid options
@@ -801,6 +802,9 @@ function webp (options) {
if (is.defined(options.mixed)) {
this._setBooleanOption('webpMixed', options.mixed);
}
if (is.defined(options.exact)) {
this._setBooleanOption('webpExact', options.exact);
}
}
trySetAnimationOptions(options, this.options);
return this._updateFormatOut('webp', options);
@@ -946,7 +950,7 @@ function gif (options) {
*/
function jp2 (options) {
/* node:coverage ignore next 41 */
if (!this.constructor.format.jp2k.output.buffer) {
if (!this.constructor.format.jp2.output.buffer) {
throw errJp2Save();
}
if (is.object(options)) {
@@ -1171,11 +1175,13 @@ function tiff (options) {
* @param {number} [options.effort=4] - CPU effort, between 0 (fastest) and 9 (slowest)
* @param {string} [options.chromaSubsampling='4:4:4'] - set to '4:2:0' to use chroma subsampling
* @param {number} [options.bitdepth=8] - set bitdepth to 8, 10 or 12 bit
* @param {string} [options.tune='iq'] - tune output for a quality metric, one of 'iq' (default), 'ssim' or 'psnr'
* @returns {Sharp}
* @throws {Error} Invalid options
*/
function avif (options) {
return this.heif({ ...options, compression: 'av1' });
const tune = is.object(options) && is.defined(options.tune) ? options.tune : 'iq';
return this.heif({ ...options, compression: 'av1', tune });
}
/**
@@ -1198,6 +1204,7 @@ function avif (options) {
* @param {number} [options.effort=4] - CPU effort, between 0 (fastest) and 9 (slowest)
* @param {string} [options.chromaSubsampling='4:4:4'] - set to '4:2:0' to use chroma subsampling
* @param {number} [options.bitdepth=8] - set bitdepth to 8, 10 or 12 bit
* @param {string} [options.tune='ssim'] - tune output for a quality metric, one of 'ssim' (default), 'psnr' or 'iq'
* @returns {Sharp}
* @throws {Error} Invalid options
*/
@@ -1246,6 +1253,13 @@ function heif (options) {
throw is.invalidParameterError('bitdepth', '8, 10 or 12', options.bitdepth);
}
}
if (is.defined(options.tune)) {
if (is.string(options.tune) && is.inArray(options.tune, ['iq', 'ssim', 'psnr'])) {
this.options.heifTune = options.tune;
} else {
throw is.invalidParameterError('tune', 'one of: psnr, ssim, iq', options.tune);
}
}
} else {
throw is.invalidParameterError('options', 'Object', options);
}

View File

@@ -540,10 +540,19 @@ function extract (options) {
* })
* .toBuffer();
*
* @example
* // Trim image leaving (up to) a 10 pixel margin around the trimmed content.
* const output = await sharp(input)
* .trim({
* margin: 10
* })
* .toBuffer();
*
* @param {Object} [options]
* @param {string|Object} [options.background='top-left pixel'] - Background colour, parsed by the [color](https://www.npmjs.org/package/color) module, defaults to that of the top-left pixel.
* @param {number} [options.threshold=10] - Allowed difference from the above colour, a positive number.
* @param {boolean} [options.lineArt=false] - Does the input more closely resemble line art (e.g. vector) rather than being photographic?
* @param {number} [options.margin=0] - Leave a margin around trimmed content, value is in pixels.
* @returns {Sharp}
* @throws {Error} Invalid parameters
*/
@@ -564,6 +573,13 @@ function trim (options) {
if (is.defined(options.lineArt)) {
this._setBooleanOption('trimLineArt', options.lineArt);
}
if (is.defined(options.margin)) {
if (is.integer(options.margin) && options.margin >= 0) {
this.options.trimMargin = options.margin;
} else {
throw is.invalidParameterError('margin', 'positive integer', options.margin);
}
}
} else {
throw is.invalidParameterError('trim', 'object', options);
}

View File

@@ -24,7 +24,7 @@ const format = sharp.format();
format.heif.output.alias = ['avif', 'heic'];
format.jpeg.output.alias = ['jpe', 'jpg'];
format.tiff.output.alias = ['tif'];
format.jp2k.output.alias = ['j2c', 'j2k', 'jp2', 'jpx'];
format.jp2.output.alias = ['j2c', 'j2k', 'jp2', 'jpx'];
/**
* An Object containing the available interpolators and their proper values

View File

@@ -1,6 +1,6 @@
{
"name": "@img/sharp-darwin-arm64",
"version": "0.34.5",
"version": "0.35.0-rc.0",
"description": "Prebuilt sharp for use with macOS 64-bit ARM",
"author": "Lovell Fuller <npm@lovell.info>",
"homepage": "https://sharp.pixelplumbing.com",
@@ -15,7 +15,7 @@
},
"preferUnplugged": true,
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.3.0-rc.1"
"@img/sharp-libvips-darwin-arm64": "1.3.0-rc.2"
},
"files": [
"index.cjs",

View File

@@ -1,6 +1,6 @@
{
"name": "@img/sharp-darwin-x64",
"version": "0.34.5",
"version": "0.35.0-rc.0",
"description": "Prebuilt sharp for use with macOS x64",
"author": "Lovell Fuller <npm@lovell.info>",
"homepage": "https://sharp.pixelplumbing.com",
@@ -15,7 +15,7 @@
},
"preferUnplugged": true,
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.3.0-rc.1"
"@img/sharp-libvips-darwin-x64": "1.3.0-rc.2"
},
"files": [
"index.cjs",

View File

@@ -1,6 +1,6 @@
{
"name": "@img/sharp-linux-arm",
"version": "0.34.5",
"version": "0.35.0-rc.0",
"description": "Prebuilt sharp for use with Linux (glibc) ARM (32-bit)",
"author": "Lovell Fuller <npm@lovell.info>",
"homepage": "https://sharp.pixelplumbing.com",
@@ -15,7 +15,7 @@
},
"preferUnplugged": true,
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.3.0-rc.1"
"@img/sharp-libvips-linux-arm": "1.3.0-rc.2"
},
"files": [
"index.cjs",

View File

@@ -1,6 +1,6 @@
{
"name": "@img/sharp-linux-arm64",
"version": "0.34.5",
"version": "0.35.0-rc.0",
"description": "Prebuilt sharp for use with Linux (glibc) 64-bit ARM",
"author": "Lovell Fuller <npm@lovell.info>",
"homepage": "https://sharp.pixelplumbing.com",
@@ -15,7 +15,7 @@
},
"preferUnplugged": true,
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.3.0-rc.1"
"@img/sharp-libvips-linux-arm64": "1.3.0-rc.2"
},
"files": [
"index.cjs",

View File

@@ -1,6 +1,6 @@
{
"name": "@img/sharp-linux-ppc64",
"version": "0.34.5",
"version": "0.35.0-rc.0",
"description": "Prebuilt sharp for use with Linux (glibc) ppc64",
"author": "Lovell Fuller <npm@lovell.info>",
"homepage": "https://sharp.pixelplumbing.com",
@@ -15,7 +15,7 @@
},
"preferUnplugged": true,
"optionalDependencies": {
"@img/sharp-libvips-linux-ppc64": "1.3.0-rc.1"
"@img/sharp-libvips-linux-ppc64": "1.3.0-rc.2"
},
"files": [
"index.cjs",

View File

@@ -1,6 +1,6 @@
{
"name": "@img/sharp-linux-riscv64",
"version": "0.34.5",
"version": "0.35.0-rc.0",
"description": "Prebuilt sharp for use with Linux (glibc) RISC-V 64-bit",
"author": "Lovell Fuller <npm@lovell.info>",
"homepage": "https://sharp.pixelplumbing.com",
@@ -15,7 +15,7 @@
},
"preferUnplugged": true,
"optionalDependencies": {
"@img/sharp-libvips-linux-riscv64": "1.3.0-rc.1"
"@img/sharp-libvips-linux-riscv64": "1.3.0-rc.2"
},
"files": [
"index.cjs",

View File

@@ -1,6 +1,6 @@
{
"name": "@img/sharp-linux-s390x",
"version": "0.34.5",
"version": "0.35.0-rc.0",
"description": "Prebuilt sharp for use with Linux (glibc) s390x",
"author": "Lovell Fuller <npm@lovell.info>",
"homepage": "https://sharp.pixelplumbing.com",
@@ -15,7 +15,7 @@
},
"preferUnplugged": true,
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.3.0-rc.1"
"@img/sharp-libvips-linux-s390x": "1.3.0-rc.2"
},
"files": [
"index.cjs",

View File

@@ -1,6 +1,6 @@
{
"name": "@img/sharp-linux-x64",
"version": "0.34.5",
"version": "0.35.0-rc.0",
"description": "Prebuilt sharp for use with Linux (glibc) x64",
"author": "Lovell Fuller <npm@lovell.info>",
"homepage": "https://sharp.pixelplumbing.com",
@@ -15,7 +15,7 @@
},
"preferUnplugged": true,
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.3.0-rc.1"
"@img/sharp-libvips-linux-x64": "1.3.0-rc.2"
},
"files": [
"index.cjs",

View File

@@ -1,6 +1,6 @@
{
"name": "@img/sharp-linuxmusl-arm64",
"version": "0.34.5",
"version": "0.35.0-rc.0",
"description": "Prebuilt sharp for use with Linux (musl) 64-bit ARM",
"author": "Lovell Fuller <npm@lovell.info>",
"homepage": "https://sharp.pixelplumbing.com",
@@ -15,7 +15,7 @@
},
"preferUnplugged": true,
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.3.0-rc.1"
"@img/sharp-libvips-linuxmusl-arm64": "1.3.0-rc.2"
},
"files": [
"index.cjs",

View File

@@ -1,6 +1,6 @@
{
"name": "@img/sharp-linuxmusl-x64",
"version": "0.34.5",
"version": "0.35.0-rc.0",
"description": "Prebuilt sharp for use with Linux (musl) x64",
"author": "Lovell Fuller <npm@lovell.info>",
"homepage": "https://sharp.pixelplumbing.com",
@@ -15,7 +15,7 @@
},
"preferUnplugged": true,
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.3.0-rc.1"
"@img/sharp-libvips-linuxmusl-x64": "1.3.0-rc.2"
},
"files": [
"index.cjs",

View File

@@ -1,6 +1,6 @@
{
"name": "@img/sharp",
"version": "0.34.5",
"version": "0.35.0-rc.0",
"private": "true",
"workspaces": [
"darwin-arm64",

View File

@@ -1,6 +1,6 @@
{
"name": "@img/sharp-wasm32",
"version": "0.34.5",
"version": "0.35.0-rc.0",
"description": "Prebuilt sharp for use with wasm32",
"author": "Lovell Fuller <npm@lovell.info>",
"homepage": "https://sharp.pixelplumbing.com",

View File

@@ -1,6 +1,6 @@
{
"name": "@img/sharp-win32-arm64",
"version": "0.34.5",
"version": "0.35.0-rc.0",
"description": "Prebuilt sharp for use with Windows 64-bit ARM",
"author": "Lovell Fuller <npm@lovell.info>",
"homepage": "https://sharp.pixelplumbing.com",

View File

@@ -1,7 +1,7 @@
{
"name": "@img/sharp-win32-ia32",
"version": "0.34.5",
"description": "Prebuilt sharp for use with Windows x86 (32-bit)",
"version": "0.35.0-rc.0",
"description": "Prebuilt sharp for use with Windows x86 (deprecated)",
"author": "Lovell Fuller <npm@lovell.info>",
"homepage": "https://sharp.pixelplumbing.com",
"repository": {
@@ -29,7 +29,7 @@
"./versions": "./versions.json"
},
"engines": {
"node": ">=20.9.0"
"node": "^20.9.0"
},
"os": [
"win32"

View File

@@ -1,6 +1,6 @@
{
"name": "@img/sharp-win32-x64",
"version": "0.34.5",
"version": "0.35.0-rc.0",
"description": "Prebuilt sharp for use with Windows x64",
"author": "Lovell Fuller <npm@lovell.info>",
"homepage": "https://sharp.pixelplumbing.com",

View File

@@ -1,7 +1,7 @@
{
"name": "sharp",
"description": "High performance Node.js image processing, the fastest module to resize JPEG, PNG, WebP, GIF, AVIF and TIFF images",
"version": "0.34.5",
"version": "0.35.0-rc.0",
"author": "Lovell Fuller <npm@lovell.info>",
"homepage": "https://sharp.pixelplumbing.com",
"contributors": [
@@ -89,7 +89,8 @@
"Lachlan Newman <lachnewman007@gmail.com>",
"Dennis Beatty <dennis@dcbeatty.com>",
"Ingvar Stepanyan <me@rreverser.com>",
"Don Denton <don@happycollision.com>"
"Don Denton <don@happycollision.com>",
"Dmytro Tiapukhin <cool.gegeg@gmail.com>"
],
"scripts": {
"build": "node install/build.js",
@@ -143,40 +144,40 @@
"semver": "^7.7.3"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.34.5",
"@img/sharp-darwin-x64": "0.34.5",
"@img/sharp-libvips-darwin-arm64": "1.3.0-rc.1",
"@img/sharp-libvips-darwin-x64": "1.3.0-rc.1",
"@img/sharp-libvips-linux-arm": "1.3.0-rc.1",
"@img/sharp-libvips-linux-arm64": "1.3.0-rc.1",
"@img/sharp-libvips-linux-ppc64": "1.3.0-rc.1",
"@img/sharp-libvips-linux-riscv64": "1.3.0-rc.1",
"@img/sharp-libvips-linux-s390x": "1.3.0-rc.1",
"@img/sharp-libvips-linux-x64": "1.3.0-rc.1",
"@img/sharp-libvips-linuxmusl-arm64": "1.3.0-rc.1",
"@img/sharp-libvips-linuxmusl-x64": "1.3.0-rc.1",
"@img/sharp-linux-arm": "0.34.5",
"@img/sharp-linux-arm64": "0.34.5",
"@img/sharp-linux-ppc64": "0.34.5",
"@img/sharp-linux-riscv64": "0.34.5",
"@img/sharp-linux-s390x": "0.34.5",
"@img/sharp-linux-x64": "0.34.5",
"@img/sharp-linuxmusl-arm64": "0.34.5",
"@img/sharp-linuxmusl-x64": "0.34.5",
"@img/sharp-wasm32": "0.34.5",
"@img/sharp-win32-arm64": "0.34.5",
"@img/sharp-win32-ia32": "0.34.5",
"@img/sharp-win32-x64": "0.34.5"
"@img/sharp-darwin-arm64": "0.35.0-rc.0",
"@img/sharp-darwin-x64": "0.35.0-rc.0",
"@img/sharp-libvips-darwin-arm64": "1.3.0-rc.2",
"@img/sharp-libvips-darwin-x64": "1.3.0-rc.2",
"@img/sharp-libvips-linux-arm": "1.3.0-rc.2",
"@img/sharp-libvips-linux-arm64": "1.3.0-rc.2",
"@img/sharp-libvips-linux-ppc64": "1.3.0-rc.2",
"@img/sharp-libvips-linux-riscv64": "1.3.0-rc.2",
"@img/sharp-libvips-linux-s390x": "1.3.0-rc.2",
"@img/sharp-libvips-linux-x64": "1.3.0-rc.2",
"@img/sharp-libvips-linuxmusl-arm64": "1.3.0-rc.2",
"@img/sharp-libvips-linuxmusl-x64": "1.3.0-rc.2",
"@img/sharp-linux-arm": "0.35.0-rc.0",
"@img/sharp-linux-arm64": "0.35.0-rc.0",
"@img/sharp-linux-ppc64": "0.35.0-rc.0",
"@img/sharp-linux-riscv64": "0.35.0-rc.0",
"@img/sharp-linux-s390x": "0.35.0-rc.0",
"@img/sharp-linux-x64": "0.35.0-rc.0",
"@img/sharp-linuxmusl-arm64": "0.35.0-rc.0",
"@img/sharp-linuxmusl-x64": "0.35.0-rc.0",
"@img/sharp-wasm32": "0.35.0-rc.0",
"@img/sharp-win32-arm64": "0.35.0-rc.0",
"@img/sharp-win32-ia32": "0.35.0-rc.0",
"@img/sharp-win32-x64": "0.35.0-rc.0"
},
"devDependencies": {
"@biomejs/biome": "^2.3.10",
"@cpplint/cli": "^0.1.0",
"@emnapi/runtime": "^1.7.1",
"@img/sharp-libvips-dev": "1.3.0-rc.1",
"@img/sharp-libvips-dev-wasm32": "1.3.0-rc.1",
"@img/sharp-libvips-win32-arm64": "1.3.0-rc.1",
"@img/sharp-libvips-win32-ia32": "1.3.0-rc.1",
"@img/sharp-libvips-win32-x64": "1.3.0-rc.1",
"@img/sharp-libvips-dev": "1.3.0-rc.2",
"@img/sharp-libvips-dev-wasm32": "1.3.0-rc.2",
"@img/sharp-libvips-win32-arm64": "1.3.0-rc.2",
"@img/sharp-libvips-win32-ia32": "1.3.0-rc.2",
"@img/sharp-libvips-win32-x64": "1.3.0-rc.2",
"@types/node": "*",
"emnapi": "^1.7.1",
"exif-reader": "^2.0.3",

View File

@@ -285,7 +285,7 @@ namespace sharp {
/*
Trim an image
*/
VImage Trim(VImage image, std::vector<double> background, double threshold, bool const lineArt) {
VImage Trim(VImage image, std::vector<double> background, double threshold, bool const lineArt, int const margin) {
if (image.width() < 3 && image.height() < 3) {
throw VError("Image to trim must be at least 3x3 pixels");
}
@@ -320,18 +320,36 @@ namespace sharp {
if (widthA > 0 && heightA > 0) {
if (width > 0 && height > 0) {
// Combined bounding box (B)
int const leftB = std::min(left, leftA);
int const topB = std::min(top, topA);
int const widthB = std::max(left + width, leftA + widthA) - leftB;
int const heightB = std::max(top + height, topA + heightA) - topB;
int leftB = std::min(left, leftA);
int topB = std::min(top, topA);
int widthB = std::max(left + width, leftA + widthA) - leftB;
int heightB = std::max(top + height, topA + heightA) - topB;
if (margin > 0) {
leftB = std::max(0, leftB - margin);
topB = std::max(0, topB - margin);
widthB = std::min(image.width() - leftB, widthB + 2 * margin);
heightB = std::min(image.height() - topB, heightB + 2 * margin);
}
return image.extract_area(leftB, topB, widthB, heightB);
} else {
// Use alpha only
if (margin > 0) {
leftA = std::max(0, leftA - margin);
topA = std::max(0, topA - margin);
widthA = std::min(image.width() - leftA, widthA + 2 * margin);
heightA = std::min(image.height() - topA, heightA + 2 * margin);
}
return image.extract_area(leftA, topA, widthA, heightA);
}
}
}
if (width > 0 && height > 0) {
if (margin > 0) {
left = std::max(0, left - margin);
top = std::max(0, top - margin);
width = std::min(image.width() - left, width + 2 * margin);
height = std::min(image.height() - top, height + 2 * margin);
}
return image.extract_area(left, top, width, height);
}
return image;

View File

@@ -82,7 +82,7 @@ namespace sharp {
/*
Trim an image
*/
VImage Trim(VImage image, std::vector<double> background, double threshold, bool const lineArt);
VImage Trim(VImage image, std::vector<double> background, double threshold, bool const lineArt, int const margin);
/*
* Linear adjustment (a * in + b)

View File

@@ -153,7 +153,7 @@ class PipelineWorker : public Napi::AsyncWorker {
if (baton->trimThreshold >= 0.0) {
MultiPageUnsupported(nPages, "Trim");
image = sharp::StaySequential(image);
image = sharp::Trim(image, baton->trimBackground, baton->trimThreshold, baton->trimLineArt);
image = sharp::Trim(image, baton->trimBackground, baton->trimThreshold, baton->trimLineArt, baton->trimMargin);
baton->trimOffsetLeft = image.xoffset();
baton->trimOffsetTop = image.yoffset();
}
@@ -965,6 +965,7 @@ class PipelineWorker : public Napi::AsyncWorker {
->set("effort", baton->webpEffort)
->set("min_size", baton->webpMinSize)
->set("mixed", baton->webpMixed)
->set("exact", baton->webpExact)
->set("alpha_q", baton->webpAlphaQuality)));
baton->bufferOut = static_cast<char*>(area->data);
baton->bufferOutLength = area->length;
@@ -1032,6 +1033,7 @@ class PipelineWorker : public Napi::AsyncWorker {
->set("compression", baton->heifCompression)
->set("effort", baton->heifEffort)
->set("bitdepth", baton->heifBitdepth)
->set("tune", baton->heifTune.c_str())
->set("subsample_mode", baton->heifChromaSubsampling == "4:4:4"
? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
->set("lossless", baton->heifLossless)));
@@ -1176,6 +1178,7 @@ class PipelineWorker : public Napi::AsyncWorker {
->set("effort", baton->webpEffort)
->set("min_size", baton->webpMinSize)
->set("mixed", baton->webpMixed)
->set("exact", baton->webpExact)
->set("alpha_q", baton->webpAlphaQuality));
baton->formatOut = "webp";
} else if (baton->formatOut == "gif" || (mightMatchInput && isGif) ||
@@ -1231,6 +1234,7 @@ class PipelineWorker : public Napi::AsyncWorker {
->set("compression", baton->heifCompression)
->set("effort", baton->heifEffort)
->set("bitdepth", baton->heifBitdepth)
->set("tune", baton->heifTune.c_str())
->set("subsample_mode", baton->heifChromaSubsampling == "4:4:4"
? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
->set("lossless", baton->heifLossless));
@@ -1486,6 +1490,7 @@ class PipelineWorker : public Napi::AsyncWorker {
{"preset", vips_enum_nick(VIPS_TYPE_FOREIGN_WEBP_PRESET, baton->webpPreset)},
{"min_size", baton->webpMinSize ? "true" : "false"},
{"mixed", baton->webpMixed ? "true" : "false"},
{"exact", baton->webpExact ? "true" : "false"},
{"effort", std::to_string(baton->webpEffort)}
};
suffix = AssembleSuffixString(".webp", options);
@@ -1632,6 +1637,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
baton->trimBackground = sharp::AttrAsVectorOfDouble(options, "trimBackground");
baton->trimThreshold = sharp::AttrAsDouble(options, "trimThreshold");
baton->trimLineArt = sharp::AttrAsBool(options, "trimLineArt");
baton->trimMargin = sharp::AttrAsUint32(options, "trimMargin");
baton->gamma = sharp::AttrAsDouble(options, "gamma");
baton->gammaOut = sharp::AttrAsDouble(options, "gammaOut");
baton->linearA = sharp::AttrAsVectorOfDouble(options, "linearA");
@@ -1760,6 +1766,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
baton->webpEffort = sharp::AttrAsUint32(options, "webpEffort");
baton->webpMinSize = sharp::AttrAsBool(options, "webpMinSize");
baton->webpMixed = sharp::AttrAsBool(options, "webpMixed");
baton->webpExact = sharp::AttrAsBool(options, "webpExact");
baton->gifBitdepth = sharp::AttrAsUint32(options, "gifBitdepth");
baton->gifEffort = sharp::AttrAsUint32(options, "gifEffort");
baton->gifDither = sharp::AttrAsDouble(options, "gifDither");
@@ -1794,6 +1801,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
baton->heifEffort = sharp::AttrAsUint32(options, "heifEffort");
baton->heifChromaSubsampling = sharp::AttrAsStr(options, "heifChromaSubsampling");
baton->heifBitdepth = sharp::AttrAsUint32(options, "heifBitdepth");
baton->heifTune = sharp::AttrAsStr(options, "heifTune");
baton->jxlDistance = sharp::AttrAsDouble(options, "jxlDistance");
baton->jxlDecodingTier = sharp::AttrAsUint32(options, "jxlDecodingTier");
baton->jxlEffort = sharp::AttrAsUint32(options, "jxlEffort");

View File

@@ -102,6 +102,7 @@ struct PipelineBaton {
bool trimLineArt;
int trimOffsetLeft;
int trimOffsetTop;
int trimMargin;
std::vector<double> linearA;
std::vector<double> linearB;
int dilateWidth;
@@ -168,6 +169,7 @@ struct PipelineBaton {
int webpEffort;
bool webpMinSize;
bool webpMixed;
bool webpExact;
int gifBitdepth;
int gifEffort;
double gifDither;
@@ -195,6 +197,7 @@ struct PipelineBaton {
std::string heifChromaSubsampling;
bool heifLossless;
int heifBitdepth;
std::string heifTune;
double jxlDistance;
int jxlDecodingTier;
int jxlEffort;
@@ -284,6 +287,7 @@ struct PipelineBaton {
trimLineArt(false),
trimOffsetLeft(0),
trimOffsetTop(0),
trimMargin(0),
linearA{},
linearB{},
dilateWidth(0),
@@ -347,6 +351,7 @@ struct PipelineBaton {
webpEffort(4),
webpMinSize(false),
webpMixed(false),
webpExact(false),
gifBitdepth(8),
gifEffort(7),
gifDither(1.0),
@@ -374,6 +379,7 @@ struct PipelineBaton {
heifChromaSubsampling("4:4:4"),
heifLossless(false),
heifBitdepth(8),
heifTune("ssim"),
jxlDistance(1.0),
jxlDecodingTier(0),
jxlEffort(7),

View File

@@ -123,6 +123,7 @@ Napi::Value format(const Napi::CallbackInfo& info) {
"jpeg", "png", "webp", "tiff", "magick", "openslide", "dz",
"ppm", "fits", "gif", "svg", "heif", "pdf", "vips", "jp2k", "jxl", "rad", "dcraw"
}) {
std::string id = f == "jp2k" ? "jp2" : f;
// Input
const VipsObjectClass *oc = vips_class_find("VipsOperation", (f + "load").c_str());
Napi::Boolean hasInputFile = Napi::Boolean::New(env, oc);
@@ -154,11 +155,11 @@ Napi::Value format(const Napi::CallbackInfo& info) {
output.Set("stream", hasOutputBuffer);
// Other attributes
Napi::Object container = Napi::Object::New(env);
container.Set("id", f);
container.Set("id", id);
container.Set("input", input);
container.Set("output", output);
// Add to set of formats
format.Set(f, container);
format.Set(id, container);
}
// Raw, uncompressed data

View File

@@ -74,6 +74,7 @@ module.exports = {
inputPng: getPath('50020484-00001.png'), // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png
inputPngGradients: getPath('gradients-rgb8.png'),
inputPngWithSlightGradientBorder: getPath('slight-gradient-border.png'),
inputPngWithTransparency: getPath('blackbug.png'), // public domain
inputPngCompleteTransparency: getPath('full-transparent.png'),
inputPngWithGreyAlpha: getPath('grey-8bit-alpha.png'),

BIN
test/fixtures/slight-gradient-border.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -234,7 +234,7 @@ sharp(input)
sharp(input)
.resize(100, 100)
.toFormat('jpg')
.toFormat('avif')
.toBuffer({ resolveWithObject: false })
.then((outputBuffer: Buffer) => {
// Resolves with a Buffer object when resolveWithObject is false
@@ -267,9 +267,7 @@ sharp(input)
// Output to tif
sharp(input)
.resize(100, 100)
.toFormat('tif')
.toFormat('tiff')
.toFormat(sharp.format.tif)
.toFormat(sharp.format.tiff)
.toBuffer();
@@ -365,7 +363,7 @@ sharp(input)
.avif({ quality: 50, lossless: false, effort: 5, chromaSubsampling: '4:2:0' })
.heif()
.heif({})
.heif({ quality: 50, compression: 'hevc', lossless: false, effort: 5, chromaSubsampling: '4:2:0' })
.heif({ quality: 50, compression: 'hevc', lossless: false, effort: 5, chromaSubsampling: '4:2:0', tune: 'psnr' })
.toBuffer({ resolveWithObject: true })
.then(({ data, info }) => {
console.log(data);
@@ -548,8 +546,8 @@ sharp('input.tiff').jxl({ decodingTier: 4 }).toFile('out.jxl');
sharp('input.tiff').jxl({ lossless: true }).toFile('out.jxl');
sharp('input.tiff').jxl({ effort: 7 }).toFile('out.jxl');
// Support `minSize` and `mixed` webp options
sharp('input.tiff').webp({ minSize: true, mixed: true }).toFile('out.gif');
// Support webp options
sharp('input.tiff').webp({ minSize: true, mixed: true, exact: true }).toFile('out.webp');
// 'failOn' input param
sharp('input.tiff', { failOn: 'none' });
@@ -601,7 +599,7 @@ const vertexSplitQuadraticBasisSpline: string = sharp.interpolators.vertexSplitQ
// Triming
sharp(input).trim({ background: '#000' }).toBuffer();
sharp(input).trim({ threshold: 10, lineArt: true }).toBuffer();
sharp(input).trim({ background: '#bf1942', threshold: 30 }).toBuffer();
sharp(input).trim({ background: '#bf1942', threshold: 30, margin: 20 }).toBuffer();
// Text input
sharp({
@@ -774,3 +772,35 @@ sharp().erode();
sharp().erode(1);
sharp().dilate();
sharp().dilate(1);
sharp.format.dcraw;
sharp.format.dz;
sharp.format.fits;
sharp.format.gif;
sharp.format.heif;
sharp.format.jp2;
sharp.format.jpeg;
sharp.format.jxl;
sharp.format.magick;
sharp.format.openslide;
sharp.format.pdf;
sharp.format.png;
sharp.format.ppm;
sharp.format.rad;
sharp.format.raw;
sharp.format.svg;
sharp.format.tiff;
sharp.format.vips;
sharp.format.webp;
// @ts-expect-error
sharp.format.avif;
// @ts-expect-error
sharp.format.input;
// @ts-expect-error
sharp.format.jp2k;
// @ts-expect-error
sharp.format.jpg;
// @ts-expect-error
sharp.format.tif;
// @ts-expect-error
sharp.format.v;

View File

@@ -181,4 +181,16 @@ describe('AVIF', () => {
/Expected 8, 10 or 12 for bitdepth but received 11 of type number/
)
);
it('Different tune options result in different file sizes', async () => {
const ssim = await sharp(inputJpg)
.resize(32)
.avif({ tune: 'ssim', effort: 0 })
.toBuffer();
const iq = await sharp(inputJpg)
.resize(32)
.avif({ tune: 'iq', effort: 0 })
.toBuffer();
assert(ssim.length < iq.length);
})
});

View File

@@ -96,4 +96,14 @@ describe('HEIF', () => {
sharp().heif({ compression: 'av1', bitdepth: 11 });
}, /Error: Expected 8, 10 or 12 for bitdepth but received 11 of type number/);
});
it('valid tune does not throw an error', () => {
assert.doesNotThrow(() => {
sharp().heif({ compression: 'hevc', tune: 'psnr' });
});
});
it('invalid tune should throw an error', () => {
assert.throws(() => {
sharp().heif({ compression: 'hevc', tune: 'fail' });
}, /Error: Expected one of: psnr, ssim, iq for tune but received fail of type string/);
});
});

View File

@@ -11,7 +11,7 @@ const sharp = require('../../');
const fixtures = require('../fixtures');
describe('JP2 output', () => {
if (!sharp.format.jp2k.input.buffer) {
if (!sharp.format.jp2.input.buffer) {
it('JP2 output should fail due to missing OpenJPEG', () =>
assert.rejects(async () =>
sharp(fixtures.inputJpg)

View File

@@ -180,7 +180,7 @@ describe('libvips binaries', () => {
process.env.npm_config_arch = 's390x';
process.env.npm_config_libc = '';
const locatorHash = libvips.yarnLocator();
assert.strictEqual(locatorHash, '8cdba194cb');
assert.strictEqual(locatorHash, 'f7e557e9d6');
delete process.env.npm_config_platform;
delete process.env.npm_config_arch;
delete process.env.npm_config_libc;

View File

@@ -222,6 +222,9 @@ describe('Trim borders', () => {
},
'Invalid lineArt': {
lineArt: 'fail'
},
'Invalid margin': {
margin: -1
}
}).forEach(([description, parameter]) => {
it(description, () => {
@@ -289,4 +292,42 @@ describe('Trim borders', () => {
assert.strictEqual(trimOffsetLeft, 0);
});
});
describe('Adds margin around content', () => {
it('Should trim complex gradients', async () => {
const { info } = await sharp(fixtures.inputPngGradients)
.trim({ threshold: 50, margin: 100 })
.toBuffer({ resolveWithObject: true });
const { width, height, trimOffsetTop, trimOffsetLeft } = info;
assert.strictEqual(width, 1000);
assert.strictEqual(height, 443);
assert.strictEqual(trimOffsetTop, -557);
assert.strictEqual(trimOffsetLeft, 0);
});
it('Should trim simple gradients', async () => {
const { info } = await sharp(fixtures.inputPngWithSlightGradientBorder)
.trim({ threshold: 70, margin: 50 })
.toBuffer({ resolveWithObject: true });
const { width, height, trimOffsetTop, trimOffsetLeft } = info;
assert.strictEqual(width, 900);
assert.strictEqual(height, 900);
assert.strictEqual(trimOffsetTop, -50);
assert.strictEqual(trimOffsetLeft, -50);
});
it('Should not overflow image bounding box', async () => {
const { info } = await sharp(fixtures.inputPngWithSlightGradientBorder)
.trim({ threshold: 70, margin: 9999999 })
.toBuffer({ resolveWithObject: true });
const { width, height, trimOffsetTop, trimOffsetLeft } = info;
assert.strictEqual(width, 1000);
assert.strictEqual(height, 1000);
assert.strictEqual(trimOffsetTop, 0);
assert.strictEqual(trimOffsetLeft, 0);
});
});
});

View File

@@ -213,6 +213,33 @@ describe('WebP', () => {
);
});
it('valid exact', () => {
assert.doesNotThrow(() => sharp().webp({ exact: true }));
});
it('invalid exact throws', () => {
assert.throws(
() => sharp().webp({ exact: 'fail' }),
/Expected boolean for webpExact but received fail of type string/
);
});
it('saving exact pixel colour values produces larger file size', async () => {
const withExact = await
sharp(fixtures.inputPngAlphaPremultiplicationSmall)
.resize(8, 8)
.webp({ exact: true, effort: 0 })
.toBuffer();
const withoutExact = await
sharp(fixtures.inputPngAlphaPremultiplicationSmall)
.resize(8, 8)
.webp({ exact: false, effort: 0 })
.toBuffer()
assert.strictEqual(true, withExact.length > withoutExact.length);
});
it('invalid loop throws', () => {
assert.throws(() => {
sharp().webp({ loop: -1 });