Compare commits

...

15 Commits

Author SHA1 Message Date
Lovell Fuller
6d18c6fdc6 Add Media Type to metadata response #4492 2026-02-01 21:20:16 +00:00
Lovell Fuller
2291c0b864 Switch from custom VError to standard runtime_error 2026-01-23 22:42:23 +00:00
Lovell Fuller
ed6b7384d0 Ensure TIFF output bitdepth option is limited to 1, 2 or 4 2026-01-23 21:29:40 +00:00
Lovell Fuller
ef77388a73 Force MSVC to use exception handling
As of 8.18.0, libvips C++ wrapper retrieves error messages at
exception construction time rather than lazily when accessed.

On Windows this led to error messages being referenced rather
than copied, leading to access beyond their lifetime and possible
corruption.
2026-01-22 12:52:48 +00:00
Lovell Fuller
66764b359b Remove unused option parameter added in 8561f0d 2026-01-20 21:10:56 +00:00
Lovell Fuller
8561f0da1d Ensure HEIF primary item is used as default page #4487 2026-01-18 20:24:34 +00:00
Lovell Fuller
0468c1be9f Encoding lossless AVIF is mutually exclusive with iq tuning 2026-01-08 12:43:53 +00:00
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
54 changed files with 616 additions and 196 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

@@ -18,6 +18,7 @@ Dimensions in the response will respect the `page` and `pages` properties of the
A `Promise` is returned when `callback` is not provided.
- `format`: Name of decoder used to parse image e.g. `jpeg`, `png`, `webp`, `gif`, `svg`, `heif`, `tiff`
- `mediaType`: Media Type (MIME Type) e.g. `image/jpeg`, `image/png`, `image/svg+xml`, `image/avif`
- `size`: Total size of image in bytes, for Stream and Buffer input only
- `width`: Number of pixels wide (EXIF orientation is not taken into consideration, see example below)
- `height`: Number of pixels high (EXIF orientation is not taken into consideration, see example below)

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**
@@ -716,7 +717,7 @@ instead of providing `xres` and `yres` in pixels/mm.
| [options.xres] | <code>number</code> | <code>1.0</code> | horizontal resolution in pixels/mm |
| [options.yres] | <code>number</code> | <code>1.0</code> | vertical resolution in pixels/mm |
| [options.resolutionUnit] | <code>string</code> | <code>&quot;&#x27;inch&#x27;&quot;</code> | resolution unit options: inch, cm |
| [options.bitdepth] | <code>number</code> | <code>8</code> | reduce bitdepth to 1, 2 or 4 bit |
| [options.bitdepth] | <code>number</code> | <code>0</code> | reduce bitdepth to 1, 2 or 4 bit |
| [options.miniswhite] | <code>boolean</code> | <code>false</code> | write 1-bit images as miniswhite |
**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' (default when lossless) 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,42 @@ 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.
* Ensure TIFF output `bitdepth` option is limited to 1, 2 or 4.
* 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)
* Ensure HEIF primary item is used as default page/frame.
[#4487](https://github.com/lovell/sharp/issues/4487)
* Add image Media Type (MIME Type) to metadata response.
[#4492](https://github.com/lovell/sharp/issues/4492)
* 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,
@@ -364,7 +366,7 @@ const Sharp = function (input, options) {
tiffPredictor: 'horizontal',
tiffPyramid: false,
tiffMiniswhite: false,
tiffBitdepth: 8,
tiffBitdepth: 0,
tiffTile: false,
tiffTileHeight: 256,
tiffTileWidth: 256,
@@ -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,

26
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 {
@@ -1471,8 +1479,8 @@ declare namespace sharp {
xres?: number | undefined;
/** Vertical resolution in pixels/mm (optional, default 1.0) */
yres?: number | undefined;
/** Reduce bitdepth to 1, 2 or 4 bit (optional, default 8) */
bitdepth?: 1 | 2 | 4 | 8 | undefined;
/** Reduce bitdepth to 1, 2 or 4 bit (optional) */
bitdepth?: 1 | 2 | 4 | undefined;
/** Write 1-bit images as miniswhite (optional, default false) */
miniswhite?: boolean | undefined;
/** Resolution unit options: inch, cm (optional, default 'inch') */
@@ -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

@@ -567,6 +567,7 @@ function _isStreamInput () {
* A `Promise` is returned when `callback` is not provided.
*
* - `format`: Name of decoder used to parse image e.g. `jpeg`, `png`, `webp`, `gif`, `svg`, `heif`, `tiff`
* - `mediaType`: Media Type (MIME Type) e.g. `image/jpeg`, `image/png`, `image/svg+xml`, `image/avif`
* - `size`: Total size of image in bytes, for Stream and Buffer input only
* - `width`: Number of pixels wide (EXIF orientation is not taken into consideration, see example below)
* - `height`: Number of pixels high (EXIF orientation is not taken into consideration, see example below)

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)) {
@@ -1051,7 +1055,7 @@ function trySetAnimationOptions (source, target) {
* @param {number} [options.xres=1.0] - horizontal resolution in pixels/mm
* @param {number} [options.yres=1.0] - vertical resolution in pixels/mm
* @param {string} [options.resolutionUnit='inch'] - resolution unit options: inch, cm
* @param {number} [options.bitdepth=8] - reduce bitdepth to 1, 2 or 4 bit
* @param {number} [options.bitdepth=0] - reduce bitdepth to 1, 2 or 4 bit
* @param {boolean} [options.miniswhite=false] - write 1-bit images as miniswhite
* @returns {Sharp}
* @throws {Error} Invalid options
@@ -1066,10 +1070,10 @@ function tiff (options) {
}
}
if (is.defined(options.bitdepth)) {
if (is.integer(options.bitdepth) && is.inArray(options.bitdepth, [1, 2, 4, 8])) {
if (is.integer(options.bitdepth) && is.inArray(options.bitdepth, [1, 2, 4])) {
this.options.tiffBitdepth = options.bitdepth;
} else {
throw is.invalidParameterError('bitdepth', '1, 2, 4 or 8', options.bitdepth);
throw is.invalidParameterError('bitdepth', '1, 2 or 4', options.bitdepth);
}
}
// tiling
@@ -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' (default when lossless) 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,17 @@ 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'])) {
if (this.options.heifLossless && options.tune === 'iq') {
this.options.heifTune = 'ssim';
} else {
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

@@ -21,6 +21,7 @@
'defines': [
'_VIPS_PUBLIC=__declspec(dllexport)',
'_ALLOW_KEYWORD_MACROS',
'_HAS_EXCEPTIONS=1',
'G_DISABLE_ASSERT',
'G_DISABLE_CAST_CHECKS',
'G_DISABLE_CHECKS'
@@ -148,7 +149,8 @@
['OS == "win"', {
'defines': [
'_ALLOW_KEYWORD_MACROS',
'_FILE_OFFSET_BITS=64'
'_FILE_OFFSET_BITS=64',
'_HAS_EXCEPTIONS=1'
],
'link_settings': {
'libraries': [

View File

@@ -426,7 +426,7 @@ namespace sharp {
}
if (ImageTypeSupportsPage(imageType)) {
option->set("n", descriptor->pages);
option->set("page", descriptor->page);
option->set("page", std::max(0, descriptor->page));
}
switch (imageType) {
case ImageType::SVG:
@@ -456,6 +456,22 @@ namespace sharp {
return option;
}
/*
Should HEIF image be re-opened using the primary item?
*/
static bool HeifPrimaryPageReopen(VImage image, InputDescriptor *descriptor) {
if (image.get_typeof(VIPS_META_N_PAGES) == G_TYPE_INT && image.get_typeof("heif-primary") == G_TYPE_INT) {
if (image.get_int(VIPS_META_N_PAGES) > 1 && descriptor->pages == 1 && descriptor->page == -1) {
int const pagePrimary = image.get_int("heif-primary");
if (pagePrimary != 0) {
descriptor->page = pagePrimary;
return true;
}
}
}
return false;
}
/*
Open an image from the given InputDescriptor (filesystem, compressed buffer, raw pixel data)
*/
@@ -490,12 +506,15 @@ namespace sharp {
image = VImage::new_from_buffer(descriptor->buffer, descriptor->bufferLength, nullptr, option);
if (imageType == ImageType::SVG || imageType == ImageType::PDF || imageType == ImageType::MAGICK) {
image = SetDensity(image, descriptor->density);
} else if (imageType == ImageType::HEIF && HeifPrimaryPageReopen(image, descriptor)) {
option = GetOptionsForImageType(imageType, descriptor);
image = VImage::new_from_buffer(descriptor->buffer, descriptor->bufferLength, nullptr, option);
}
} catch (vips::VError const &err) {
throw vips::VError(std::string("Input buffer has corrupt header: ") + err.what());
} catch (std::runtime_error const &err) {
throw std::runtime_error(std::string("Input buffer has corrupt header: ") + err.what());
}
} else {
throw vips::VError("Input buffer contains unsupported image format");
throw std::runtime_error("Input buffer contains unsupported image format");
}
}
} else {
@@ -566,10 +585,10 @@ namespace sharp {
imageType = DetermineImageType(descriptor->file.data());
if (imageType == ImageType::MISSING) {
if (descriptor->file.find("<svg") != std::string::npos) {
throw vips::VError("Input file is missing, did you mean "
throw std::runtime_error("Input file is missing, did you mean "
"sharp(Buffer.from('" + descriptor->file.substr(0, 8) + "...')?");
}
throw vips::VError("Input file is missing: " + descriptor->file);
throw std::runtime_error("Input file is missing: " + descriptor->file);
}
if (imageType != ImageType::UNKNOWN) {
try {
@@ -577,12 +596,15 @@ namespace sharp {
image = VImage::new_from_file(descriptor->file.data(), option);
if (imageType == ImageType::SVG || imageType == ImageType::PDF || imageType == ImageType::MAGICK) {
image = SetDensity(image, descriptor->density);
} else if (imageType == ImageType::HEIF && HeifPrimaryPageReopen(image, descriptor)) {
option = GetOptionsForImageType(imageType, descriptor);
image = VImage::new_from_file(descriptor->file.data(), option);
}
} catch (vips::VError const &err) {
throw vips::VError(std::string("Input file has corrupt header: ") + err.what());
} catch (std::runtime_error const &err) {
throw std::runtime_error(std::string("Input file has corrupt header: ") + err.what());
}
} else {
throw vips::VError("Input file contains unsupported image format");
throw std::runtime_error("Input file contains unsupported image format");
}
}
}
@@ -590,7 +612,7 @@ namespace sharp {
// Limit input images to a given number of pixels, where pixels = width * height
if (descriptor->limitInputPixels > 0 &&
static_cast<uint64_t>(image.width()) * image.height() > descriptor->limitInputPixels) {
throw vips::VError("Input image exceeds pixel limit");
throw std::runtime_error("Input image exceeds pixel limit");
}
return std::make_tuple(image, imageType);
}
@@ -766,19 +788,19 @@ namespace sharp {
: image.height();
if (imageType == ImageType::JPEG) {
if (image.width() > 65535 || height > 65535) {
throw vips::VError("Processed image is too large for the JPEG format");
throw std::runtime_error("Processed image is too large for the JPEG format");
}
} else if (imageType == ImageType::WEBP) {
if (image.width() > 16383 || height > 16383) {
throw vips::VError("Processed image is too large for the WebP format");
throw std::runtime_error("Processed image is too large for the WebP format");
}
} else if (imageType == ImageType::GIF) {
if (image.width() > 65535 || height > 65535) {
throw vips::VError("Processed image is too large for the GIF format");
throw std::runtime_error("Processed image is too large for the GIF format");
}
} else if (imageType == ImageType::HEIF) {
if (image.width() > 16384 || height > 16384) {
throw vips::VError("Processed image is too large for the HEIF format");
throw std::runtime_error("Processed image is too large for the HEIF format");
}
}
}

View File

@@ -105,7 +105,7 @@ namespace sharp {
rawPremultiplied(false),
rawPageHeight(0),
pages(1),
page(0),
page(-1),
createChannels(0),
createWidth(0),
createHeight(0),

View File

@@ -31,7 +31,7 @@ class MetadataWorker : public Napi::AsyncWorker {
sharp::ImageType imageType = sharp::ImageType::UNKNOWN;
try {
std::tie(image, imageType) = OpenInput(baton->input);
} catch (vips::VError const &err) {
} catch (std::runtime_error const &err) {
(baton->err).append(err.what());
}
if (imageType != sharp::ImageType::UNKNOWN) {
@@ -151,6 +151,50 @@ class MetadataWorker : public Napi::AsyncWorker {
}
// PNG comments
vips_image_map(image.get_image(), readPNGComment, &baton->comments);
// Media type
std::string mediaType;
switch (imageType) {
case sharp::ImageType::JPEG:
case sharp::ImageType::PNG:
case sharp::ImageType::WEBP:
case sharp::ImageType::JP2:
case sharp::ImageType::TIFF:
case sharp::ImageType::GIF:
case sharp::ImageType::FITS:
case sharp::ImageType::JXL:
baton->mediaType = "image/" + baton->format;
break;
case sharp::ImageType::SVG:
baton->mediaType = "image/svg+xml";
break;
case sharp::ImageType::HEIF:
if (baton->compression == "av1") {
baton->mediaType = "image/avif";
} else if (baton->compression == "hevc") {
baton->mediaType = "image/heic";
}
break;
case sharp::ImageType::PDF:
baton->mediaType = "application/pdf";
break;
case sharp::ImageType::OPENSLIDE:
baton->mediaType = "image/tiff";
break;
case sharp::ImageType::PPM:
baton->mediaType = "image/x-portable-pixmap";
break;
case sharp::ImageType::EXR:
baton->mediaType = "image/x-exr";
break;
case sharp::ImageType::RAD:
baton->mediaType = "image/vnd.radiance";
break;
case sharp::ImageType::UHDR:
baton->mediaType = "image/jpeg";
break;
default:
break;
}
}
// Clean up
@@ -172,6 +216,9 @@ class MetadataWorker : public Napi::AsyncWorker {
if (baton->err.empty()) {
Napi::Object info = Napi::Object::New(env);
info.Set("format", baton->format);
if (!baton->mediaType.empty()) {
info.Set("mediaType", baton->mediaType);
}
if (baton->input->bufferLength > 0) {
info.Set("size", baton->input->bufferLength);
}

View File

@@ -19,6 +19,7 @@ struct MetadataBaton {
sharp::InputDescriptor *input;
// Output
std::string format;
std::string mediaType;
int width;
int height;
std::string space;

View File

@@ -14,7 +14,6 @@
#include "./operations.h"
using vips::VImage;
using vips::VError;
namespace sharp {
/*
@@ -285,9 +284,9 @@ 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");
throw std::runtime_error("Image to trim must be at least 3x3 pixels");
}
if (background.size() == 0) {
// Top-left pixel provides the default background colour if none is given
@@ -320,18 +319,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;
@@ -343,7 +360,7 @@ namespace sharp {
VImage Linear(VImage image, std::vector<double> const a, std::vector<double> const b) {
size_t const bands = static_cast<size_t>(image.bands());
if (a.size() > bands) {
throw VError("Band expansion using linear is unsupported");
throw std::runtime_error("Band expansion using linear is unsupported");
}
bool const uchar = !Is16Bit(image.interpretation());
if (image.has_alpha() && a.size() != bands && (a.size() == 1 || a.size() == bands - 1 || bands - 1 == 1)) {

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

@@ -84,7 +84,7 @@ class PipelineWorker : public Napi::AsyncWorker {
if (nPages == -1) {
// Resolve the number of pages if we need to render until the end of the document
nPages = image.get_typeof(VIPS_META_N_PAGES) != 0
? image.get_int(VIPS_META_N_PAGES) - baton->input->page
? image.get_int(VIPS_META_N_PAGES) - std::max(0, baton->input->page)
: 1;
}
@@ -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();
}
@@ -274,7 +274,7 @@ class PipelineWorker : public Napi::AsyncWorker {
}
sharp::SetDensity(image, baton->input->density);
if (image.width() > 32767 || image.height() > 32767) {
throw vips::VError("Input SVG image will exceed 32767x32767 pixel limit when scaled");
throw std::runtime_error("Input SVG image will exceed 32767x32767 pixel limit when scaled");
}
} else if (inputImageType == sharp::ImageType::PDF) {
if (baton->input->buffer != nullptr) {
@@ -290,7 +290,7 @@ class PipelineWorker : public Napi::AsyncWorker {
}
} else {
if (inputImageType == sharp::ImageType::SVG && (image.width() > 32767 || image.height() > 32767)) {
throw vips::VError("Input SVG image exceeds 32767x32767 pixel limit");
throw std::runtime_error("Input SVG image exceeds 32767x32767 pixel limit");
}
}
if (baton->input->autoOrient) {
@@ -675,7 +675,7 @@ class PipelineWorker : public Napi::AsyncWorker {
// Verify within current dimensions
if (compositeImage.width() > image.width() || compositeImage.height() > image.height()) {
throw vips::VError("Image to composite must have same dimensions or smaller");
throw std::runtime_error("Image to composite must have same dimensions or smaller");
}
// Check if overlay is tiled
if (composite->tile) {
@@ -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)));
@@ -1084,20 +1086,19 @@ class PipelineWorker : public Napi::AsyncWorker {
// Get raw image data
baton->bufferOut = static_cast<char*>(image.write_to_memory(&baton->bufferOutLength));
if (baton->bufferOut == nullptr) {
(baton->err).append("Could not allocate enough memory for raw output");
return Error();
throw std::runtime_error("Could not allocate enough memory for raw output");
}
baton->formatOut = "raw";
} else {
// Unsupported output format
(baton->err).append("Unsupported output format ");
auto unsupported = std::string("Unsupported output format ");
if (baton->formatOut == "input") {
(baton->err).append("when trying to match input format of ");
(baton->err).append(ImageTypeId(inputImageType));
unsupported.append("when trying to match input format of ");
unsupported.append(ImageTypeId(inputImageType));
} else {
(baton->err).append(baton->formatOut);
unsupported.append(baton->formatOut);
}
return Error();
throw std::runtime_error(unsupported);
}
} else {
// File output
@@ -1176,6 +1177,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 +1233,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));
@@ -1270,7 +1273,7 @@ class PipelineWorker : public Napi::AsyncWorker {
return Error();
}
}
} catch (vips::VError const &err) {
} catch (std::runtime_error const &err) {
char const *what = err.what();
if (what && what[0]) {
(baton->err).append(what);
@@ -1302,7 +1305,6 @@ class PipelineWorker : public Napi::AsyncWorker {
}
warning = sharp::VipsWarningPop();
}
if (baton->err.empty()) {
int width = baton->width;
int height = baton->height;
@@ -1403,7 +1405,7 @@ class PipelineWorker : public Napi::AsyncWorker {
void MultiPageUnsupported(int const pages, std::string op) {
if (pages > 1) {
throw vips::VError(op + " is not supported for multi-page images");
throw std::runtime_error(op + " is not supported for multi-page images");
}
}
@@ -1486,6 +1488,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 +1635,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 +1764,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 +1799,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),
@@ -360,7 +365,7 @@ struct PipelineBaton {
tiffBigtiff(false),
tiffPredictor(VIPS_FOREIGN_TIFF_PREDICTOR_HORIZONTAL),
tiffPyramid(false),
tiffBitdepth(8),
tiffBitdepth(0),
tiffMiniswhite(false),
tiffTile(false),
tiffTileHeight(256),
@@ -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

@@ -39,7 +39,7 @@ class StatsWorker : public Napi::AsyncWorker {
sharp::ImageType imageType = sharp::ImageType::UNKNOWN;
try {
std::tie(image, imageType) = OpenInput(baton->input);
} catch (vips::VError const &err) {
} catch (std::runtime_error const &err) {
(baton->err).append(err.what());
}
if (imageType != sharp::ImageType::UNKNOWN) {
@@ -92,7 +92,7 @@ class StatsWorker : public Napi::AsyncWorker {
baton->dominantRed = dx * 16 + 8;
baton->dominantGreen = dy * 16 + 8;
baton->dominantBlue = dz * 16 + 8;
} catch (vips::VError const &err) {
} catch (std::runtime_error const &err) {
(baton->err).append(err.what());
}
}
@@ -112,7 +112,6 @@ class StatsWorker : public Napi::AsyncWorker {
debuglog.Call(Receiver().Value(), { Napi::String::New(env, warning) });
warning = sharp::VipsWarningPop();
}
if (baton->err.empty()) {
// Stats Object
Napi::Object info = Napi::Object::New(env);

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
@@ -243,7 +244,7 @@ Napi::Value _maxColourDistance(const Napi::CallbackInfo& info) {
}
// Calculate colour distance
maxColourDistance = image1.dE00(image2).max();
} catch (vips::VError const &err) {
} catch (std::runtime_error const &err) {
throw Napi::Error::New(env, err.what());
}

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'),
@@ -126,7 +127,7 @@ module.exports = {
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('sdr_cosmos12920_cicp1-13-6_yuv444_full_qp10.avif'), // CC by-nc-nd https://github.com/AOMediaCodec/av1-avif/tree/master/testFiles/Netflix
inputAvifWithPitmBox: getPath('pitm.avif'), // https://github.com/lovell/sharp/issues/4487
inputJPGBig: getPath('flowers.jpeg'),
inputPngDotAndLines: getPath('dot-and-lines.png'),

BIN
test/fixtures/pitm.avif vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

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

@@ -7,7 +7,13 @@ const { describe, it } = require('node:test');
const assert = require('node:assert');
const sharp = require('../../');
const { inputAvif, inputJpg, inputGifAnimated } = require('../fixtures');
const {
inputAvif,
inputAvifWithPitmBox,
inputJpg,
inputGifAnimated,
inputPng,
} = require('../fixtures');
describe('AVIF', () => {
it('called without options does not throw an error', () => {
@@ -17,22 +23,20 @@ describe('AVIF', () => {
});
it('can convert AVIF to JPEG', async () => {
const data = await sharp(inputAvif)
.resize(32)
.jpeg()
.toBuffer();
const data = await sharp(inputAvif).resize(32).jpeg().toBuffer();
const { size, ...metadata } = await sharp(data).metadata();
void size;
assert.deepStrictEqual(metadata, {
autoOrient: {
height: 13,
width: 32
width: 32,
},
channels: 3,
chromaSubsampling: '4:2:0',
density: 72,
depth: 'uchar',
format: 'jpeg',
mediaType: 'image/jpeg',
hasAlpha: false,
hasProfile: false,
// 32 / (2048 / 858) = 13.40625
@@ -41,7 +45,7 @@ describe('AVIF', () => {
isProgressive: false,
isPalette: false,
space: 'srgb',
width: 32
width: 32,
});
});
@@ -55,12 +59,13 @@ describe('AVIF', () => {
assert.deepStrictEqual(metadata, {
autoOrient: {
height: 26,
width: 32
width: 32,
},
channels: 3,
compression: 'av1',
depth: 'uchar',
format: 'heif',
mediaType: 'image/avif',
hasAlpha: false,
hasProfile: false,
height: 26,
@@ -70,25 +75,54 @@ describe('AVIF', () => {
pagePrimary: 0,
pages: 1,
space: 'srgb',
width: 32
width: 32,
});
});
it('can passthrough AVIF', async () => {
const data = await sharp(inputAvif)
it('can convert PNG to lossless AVIF', async () => {
const data = await sharp(inputPng)
.resize(32)
.avif({ lossless: true, effort: 0 })
.toBuffer();
const { size, ...metadata } = await sharp(data).metadata();
void size;
assert.deepStrictEqual(metadata, {
autoOrient: {
height: 13,
width: 32
height: 24,
width: 32,
},
channels: 3,
compression: 'av1',
depth: 'uchar',
format: 'heif',
mediaType: 'image/avif',
hasAlpha: false,
hasProfile: false,
height: 24,
isProgressive: false,
isPalette: false,
bitsPerSample: 8,
pagePrimary: 0,
pages: 1,
space: 'srgb',
width: 32,
});
});
it('can passthrough AVIF', async () => {
const data = await sharp(inputAvif).resize(32).toBuffer();
const { size, ...metadata } = await sharp(data).metadata();
void size;
assert.deepStrictEqual(metadata, {
autoOrient: {
height: 13,
width: 32,
},
channels: 3,
compression: 'av1',
depth: 'uchar',
format: 'heif',
mediaType: 'image/avif',
hasAlpha: false,
hasProfile: false,
height: 13,
@@ -98,7 +132,7 @@ describe('AVIF', () => {
pagePrimary: 0,
pages: 1,
space: 'srgb',
width: 32
width: 32,
});
});
@@ -112,12 +146,13 @@ describe('AVIF', () => {
assert.deepStrictEqual(metadata, {
autoOrient: {
height: 300,
width: 10
width: 10,
},
channels: 4,
compression: 'av1',
depth: 'uchar',
format: 'heif',
mediaType: 'image/avif',
hasAlpha: true,
hasProfile: false,
height: 300,
@@ -127,7 +162,7 @@ describe('AVIF', () => {
pagePrimary: 0,
pages: 1,
space: 'srgb',
width: 10
width: 10,
});
});
@@ -142,12 +177,13 @@ describe('AVIF', () => {
assert.deepStrictEqual(metadata, {
autoOrient: {
height: 26,
width: 32
width: 32,
},
channels: 3,
compression: 'av1',
depth: 'uchar',
format: 'heif',
mediaType: 'image/avif',
hasAlpha: false,
hasProfile: false,
height: 26,
@@ -157,28 +193,91 @@ describe('AVIF', () => {
pagePrimary: 0,
pages: 1,
space: 'srgb',
width: 32
width: 32,
});
});
it('Invalid width - too large', async () =>
assert.rejects(
() => sharp({ create: { width: 16385, height: 16, channels: 3, background: 'red' } }).avif().toBuffer(),
/Processed image is too large for the HEIF format/
)
);
() =>
sharp({
create: { width: 16385, height: 16, channels: 3, background: 'red' },
})
.avif()
.toBuffer(),
/Processed image is too large for the HEIF format/,
));
it('Invalid height - too large', async () =>
assert.rejects(
() => sharp({ create: { width: 16, height: 16385, channels: 3, background: 'red' } }).avif().toBuffer(),
/Processed image is too large for the HEIF format/
)
);
() =>
sharp({
create: { width: 16, height: 16385, channels: 3, background: 'red' },
})
.avif()
.toBuffer(),
/Processed image is too large for the HEIF format/,
));
it('Invalid bitdepth value throws error', () =>
assert.throws(
() => sharp().avif({ bitdepth: 11 }),
/Expected 8, 10 or 12 for bitdepth but received 11 of type number/
)
);
/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);
});
it('AVIF with non-zero primary item uses it as default page', async () => {
const { exif, ...metadata } = await sharp(inputAvifWithPitmBox).metadata();
void exif;
assert.deepStrictEqual(metadata, {
format: 'heif',
mediaType: 'image/avif',
width: 4096,
height: 800,
space: 'srgb',
channels: 3,
depth: 'uchar',
isProgressive: false,
isPalette: false,
bitsPerSample: 8,
pages: 5,
pagePrimary: 4,
compression: 'av1',
resolutionUnit: 'cm',
hasProfile: false,
hasAlpha: false,
autoOrient: { width: 4096, height: 800 },
});
const data = await sharp(inputAvifWithPitmBox)
.png({ compressionLevel: 0 })
.toBuffer();
const { size, ...pngMetadata } = await sharp(data).metadata();
assert.deepStrictEqual(pngMetadata, {
format: 'png',
mediaType: 'image/png',
width: 4096,
height: 800,
space: 'srgb',
channels: 3,
depth: 'uchar',
isProgressive: false,
isPalette: false,
bitsPerSample: 8,
hasProfile: false,
hasAlpha: false,
autoOrient: { width: 4096, height: 800 },
});
});
});

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

@@ -19,6 +19,7 @@ describe('Image metadata', () => {
sharp(fixtures.inputJpg).metadata((err, metadata) => {
if (err) throw err;
assert.strictEqual('jpeg', metadata.format);
assert.strictEqual('image/jpeg', metadata.mediaType);
assert.strictEqual('undefined', typeof metadata.size);
assert.strictEqual(2725, metadata.width);
assert.strictEqual(2225, metadata.height);
@@ -41,6 +42,7 @@ describe('Image metadata', () => {
sharp(fixtures.inputJpgWithExif).metadata((err, metadata) => {
if (err) throw err;
assert.strictEqual('jpeg', metadata.format);
assert.strictEqual('image/jpeg', metadata.mediaType);
assert.strictEqual('undefined', typeof metadata.size);
assert.strictEqual(450, metadata.width);
assert.strictEqual(600, metadata.height);
@@ -66,6 +68,7 @@ describe('Image metadata', () => {
const profile = icc.parse(metadata.icc);
assert.strictEqual('object', typeof profile);
assert.strictEqual('Generic RGB Profile', profile.description);
assert.strictEqual('image/jpeg', metadata.mediaType);
done();
});
});
@@ -92,6 +95,7 @@ describe('Image metadata', () => {
sharp(fixtures.inputTiff).metadata((err, metadata) => {
if (err) throw err;
assert.strictEqual('tiff', metadata.format);
assert.strictEqual('image/tiff', metadata.mediaType);
assert.strictEqual('undefined', typeof metadata.size);
assert.strictEqual(2464, metadata.width);
assert.strictEqual(3248, metadata.height);
@@ -111,6 +115,7 @@ describe('Image metadata', () => {
assert.strictEqual('undefined', typeof metadata.xmp);
assert.strictEqual('undefined', typeof metadata.xmpAsString);
assert.strictEqual('inch', metadata.resolutionUnit);
assert.strictEqual('image/tiff', metadata.mediaType);
done();
});
});
@@ -119,6 +124,7 @@ describe('Image metadata', () => {
sharp(fixtures.inputTiffMultipage).metadata((err, metadata) => {
if (err) throw err;
assert.strictEqual('tiff', metadata.format);
assert.strictEqual('image/tiff', metadata.mediaType);
assert.strictEqual('undefined', typeof metadata.size);
assert.strictEqual(2464, metadata.width);
assert.strictEqual(3248, metadata.height);
@@ -142,6 +148,7 @@ describe('Image metadata', () => {
sharp(fixtures.inputPng).metadata((err, metadata) => {
if (err) throw err;
assert.strictEqual('png', metadata.format);
assert.strictEqual('image/png', metadata.mediaType);
assert.strictEqual('undefined', typeof metadata.size);
assert.strictEqual(2809, metadata.width);
assert.strictEqual(2074, metadata.height);
@@ -166,6 +173,7 @@ describe('Image metadata', () => {
sharp(fixtures.inputPngTestJoinChannel).metadata((err, metadata) => {
if (err) throw err;
assert.strictEqual('png', metadata.format);
assert.strictEqual('image/png', metadata.mediaType);
assert.strictEqual('undefined', typeof metadata.size);
assert.strictEqual(320, metadata.width);
assert.strictEqual(240, metadata.height);
@@ -191,6 +199,7 @@ describe('Image metadata', () => {
sharp(fixtures.inputPngWithTransparency).metadata((err, metadata) => {
if (err) throw err;
assert.strictEqual('png', metadata.format);
assert.strictEqual('image/png', metadata.mediaType);
assert.strictEqual('undefined', typeof metadata.size);
assert.strictEqual(2048, metadata.width);
assert.strictEqual(1536, metadata.height);
@@ -225,6 +234,7 @@ describe('Image metadata', () => {
height: 32,
isPalette: false,
isProgressive: false,
mediaType: 'image/png',
space: 'b-w',
width: 32,
autoOrient: {
@@ -250,6 +260,7 @@ describe('Image metadata', () => {
height: 32,
isPalette: false,
isProgressive: false,
mediaType: 'image/png',
space: 'grey16',
width: 32,
autoOrient: {
@@ -263,6 +274,7 @@ describe('Image metadata', () => {
sharp(fixtures.inputWebP).metadata((err, metadata) => {
if (err) throw err;
assert.strictEqual('webp', metadata.format);
assert.strictEqual('image/webp', metadata.mediaType);
assert.strictEqual('undefined', typeof metadata.size);
assert.strictEqual(1024, metadata.width);
assert.strictEqual(772, metadata.height);
@@ -285,11 +297,12 @@ describe('Image metadata', () => {
sharp(fixtures.inputWebPAnimated)
.metadata()
.then(({
format, width, height, space, channels, depth,
format, mediaType, width, height, space, channels, depth,
isProgressive, pages, loop, delay, hasProfile,
hasAlpha
}) => {
assert.strictEqual(format, 'webp');
assert.strictEqual(mediaType, 'image/webp');
assert.strictEqual(width, 80);
assert.strictEqual(height, 80);
assert.strictEqual(space, 'srgb');
@@ -308,11 +321,12 @@ describe('Image metadata', () => {
sharp(fixtures.inputWebPAnimated, { pages: -1 })
.metadata()
.then(({
format, width, height, space, channels, depth,
format, mediaType, width, height, space, channels, depth,
isProgressive, pages, pageHeight, loop, delay,
hasProfile, hasAlpha
}) => {
assert.strictEqual(format, 'webp');
assert.strictEqual(mediaType, 'image/webp');
assert.strictEqual(width, 80);
assert.strictEqual(height, 720);
assert.strictEqual(space, 'srgb');
@@ -332,11 +346,12 @@ describe('Image metadata', () => {
sharp(fixtures.inputWebPAnimatedLoop3)
.metadata()
.then(({
format, width, height, space, channels, depth,
format, mediaType, width, height, space, channels, depth,
isProgressive, pages, loop, delay, hasProfile,
hasAlpha
}) => {
assert.strictEqual(format, 'webp');
assert.strictEqual(mediaType, 'image/webp');
assert.strictEqual(width, 370);
assert.strictEqual(height, 285);
assert.strictEqual(space, 'srgb');
@@ -355,6 +370,7 @@ describe('Image metadata', () => {
sharp(fixtures.inputGif).metadata((err, metadata) => {
if (err) throw err;
assert.strictEqual('gif', metadata.format);
assert.strictEqual('image/gif', metadata.mediaType);
assert.strictEqual('undefined', typeof metadata.size);
assert.strictEqual(800, metadata.width);
assert.strictEqual(533, metadata.height);
@@ -375,6 +391,7 @@ describe('Image metadata', () => {
sharp(fixtures.inputGifGreyPlusAlpha).metadata((err, metadata) => {
if (err) throw err;
assert.strictEqual('gif', metadata.format);
assert.strictEqual('image/gif', metadata.mediaType);
assert.strictEqual('undefined', typeof metadata.size);
assert.strictEqual(2, metadata.width);
assert.strictEqual(1, metadata.height);
@@ -395,11 +412,12 @@ describe('Image metadata', () => {
sharp(fixtures.inputGifAnimated)
.metadata()
.then(({
format, width, height, space, channels, depth,
format, mediaType, width, height, space, channels, depth,
isProgressive, pages, loop, delay, background,
hasProfile, hasAlpha
}) => {
assert.strictEqual(format, 'gif');
assert.strictEqual(mediaType, 'image/gif');
assert.strictEqual(width, 80);
assert.strictEqual(height, 80);
assert.strictEqual(space, 'srgb');
@@ -419,11 +437,12 @@ describe('Image metadata', () => {
sharp(fixtures.inputGifAnimatedLoop3)
.metadata()
.then(({
format, width, height, space, channels, depth,
format, mediaType, width, height, space, channels, depth,
isProgressive, pages, loop, delay, hasProfile,
hasAlpha
}) => {
assert.strictEqual(format, 'gif');
assert.strictEqual(mediaType, 'image/gif');
assert.strictEqual(width, 370);
assert.strictEqual(height, 285);
assert.strictEqual(space, 'srgb');
@@ -462,6 +481,7 @@ describe('Image metadata', () => {
it('File in, Promise out', (_t, done) => {
sharp(fixtures.inputJpg).metadata().then((metadata) => {
assert.strictEqual('jpeg', metadata.format);
assert.strictEqual('image/jpeg', metadata.mediaType);
assert.strictEqual('undefined', typeof metadata.size);
assert.strictEqual(2725, metadata.width);
assert.strictEqual(2225, metadata.height);
@@ -508,6 +528,7 @@ describe('Image metadata', () => {
const pipeline = sharp();
pipeline.metadata().then((metadata) => {
assert.strictEqual('jpeg', metadata.format);
assert.strictEqual('image/jpeg', metadata.mediaType);
assert.strictEqual(829183, metadata.size);
assert.strictEqual(2725, metadata.width);
assert.strictEqual(2225, metadata.height);
@@ -559,6 +580,7 @@ describe('Image metadata', () => {
const pipeline = sharp().metadata((err, metadata) => {
if (err) throw err;
assert.strictEqual('jpeg', metadata.format);
assert.strictEqual('image/jpeg', metadata.mediaType);
assert.strictEqual(829183, metadata.size);
assert.strictEqual(2725, metadata.width);
assert.strictEqual(2225, metadata.height);
@@ -583,6 +605,7 @@ describe('Image metadata', () => {
image.metadata((err, metadata) => {
if (err) throw err;
assert.strictEqual('jpeg', metadata.format);
assert.strictEqual('image/jpeg', metadata.mediaType);
assert.strictEqual('undefined', typeof metadata.size);
assert.strictEqual(2725, metadata.width);
assert.strictEqual(2225, metadata.height);
@@ -917,6 +940,7 @@ describe('Image metadata', () => {
.metadata()
.then(metadata => {
assert.strictEqual(metadata.format, 'tiff');
assert.strictEqual(metadata.mediaType, 'image/tiff');
assert.strictEqual(metadata.width, 317);
assert.strictEqual(metadata.height, 211);
assert.strictEqual(metadata.space, 'rgb16');
@@ -931,6 +955,7 @@ describe('Image metadata', () => {
const metadata = await sharp(fixtures.inputAvif).metadata();
assert.deepStrictEqual(metadata, {
format: 'heif',
mediaType: 'image/avif',
width: 2048,
height: 858,
space: 'srgb',
@@ -1014,6 +1039,7 @@ describe('Image metadata', () => {
const metadata = await sharp(fixtures.inputJpgLossless).metadata();
assert.deepStrictEqual(metadata, {
format: 'jpeg',
mediaType: 'image/jpeg',
width: 227,
height: 149,
space: 'srgb',

View File

@@ -145,6 +145,7 @@ describe('PNG', () => {
width: 68
},
format: 'png',
mediaType: 'image/png',
width: 68,
height: 68,
space: 'srgb',

View File

@@ -122,7 +122,6 @@ describe('TIFF', () => {
sharp(fixtures.inputTiff8BitDepth)
.toColourspace('b-w') // can only squash 1 band uchar images
.tiff({
bitdepth: 8,
compression: 'none',
predictor: 'none'
})
@@ -154,7 +153,7 @@ describe('TIFF', () => {
it('Invalid TIFF bitdepth value throws error', () => {
assert.throws(() => {
sharp().tiff({ bitdepth: 3 });
}, /Error: Expected 1, 2, 4 or 8 for bitdepth but received 3 of type number/);
}, /Error: Expected 1, 2 or 4 for bitdepth but received 3 of type number/);
});
it('TIFF setting xres and yres on file', () =>

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 });