From 1ec4ad69c47b32f6ac6c99ae46a1ea71ca44c656 Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Tue, 21 Nov 2023 16:11:27 +0000 Subject: [PATCH] Increase control over output metadata #3824 Add new withX and keepX functions that take advantage of libvips 8.15.0 new 'keep' metadata feature. --- docs/api-colour.md | 2 +- docs/api-output.md | 180 ++++++++++++++++++++++++----- docs/changelog.md | 5 + docs/search-index.json | 2 +- lib/constructor.js | 7 +- lib/index.d.ts | 71 +++++++++++- lib/output.js | 229 +++++++++++++++++++++++++++++-------- src/common.cc | 49 +++++++- src/common.h | 15 +++ src/pipeline.cc | 78 +++++++------ src/pipeline.h | 10 +- test/types/sharp.test-d.ts | 18 ++- test/unit/metadata.js | 191 +++++++++++++++++++++++++------ 13 files changed, 687 insertions(+), 170 deletions(-) diff --git a/docs/api-colour.md b/docs/api-colour.md index a6f7841a4..cf0e693bb 100644 --- a/docs/api-colour.md +++ b/docs/api-colour.md @@ -12,7 +12,7 @@ An alpha channel may be present and will be unchanged by the operation. | Param | Type | Description | | --- | --- | --- | -| tint | String \| Object | Parsed by the [color](https://www.npmjs.org/package/color) module. | +| tint | string \| Object | Parsed by the [color](https://www.npmjs.org/package/color) module. | **Example** ```js diff --git a/docs/api-output.md b/docs/api-output.md index 03e20599f..98182318b 100644 --- a/docs/api-output.md +++ b/docs/api-output.md @@ -111,56 +111,174 @@ await sharp(pixelArray, { raw: { width, height, channels } }) ``` -## withMetadata -> withMetadata([options]) ⇒ Sharp +## keepExif +> keepExif() ⇒ Sharp -Include all metadata (EXIF, XMP, IPTC) from the input image in the output image. -This will also convert to and add a web-friendly sRGB ICC profile if appropriate, -unless a custom output profile is provided. - -The default behaviour, when `withMetadata` is not used, is to convert to the device-independent -sRGB colour space and strip all metadata, including the removal of any ICC profile. +Keep all EXIF metadata from the input image in the output image. EXIF metadata is unsupported for TIFF output. +**Since**: 0.33.0 +**Example** +```js +const outputWithExif = await sharp(inputWithExif) + .keepExif() + .toBuffer(); +``` + + +## withExif +> withExif(exif) ⇒ Sharp + +Set EXIF metadata in the output image, ignoring any EXIF in the input image. + + +**Throws**: + +- Error Invalid parameters + +**Since**: 0.33.0 + +| Param | Type | Description | +| --- | --- | --- | +| exif | Object.<string, Object.<string, string>> | Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data. | + +**Example** +```js +const dataWithExif = await sharp(input) + .withExif({ + IFD0: { + Copyright: 'The National Gallery' + }, + IFD3: { + GPSLatitudeRef: 'N', + GPSLatitude: '51/1 30/1 3230/100', + GPSLongitudeRef: 'W', + GPSLongitude: '0/1 7/1 4366/100' + } + }) + .toBuffer(); +``` + + +## withExifMerge +> withExifMerge(exif) ⇒ Sharp + +Update EXIF metadata from the input image in the output image. + + +**Throws**: + +- Error Invalid parameters + +**Since**: 0.33.0 + +| Param | Type | Description | +| --- | --- | --- | +| exif | Object.<string, Object.<string, string>> | Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data. | + +**Example** +```js +const dataWithMergedExif = await sharp(inputWithExif) + .withExifMerge({ + IFD0: { + Copyright: 'The National Gallery' + } + }) + .toBuffer(); +``` + + +## keepIccProfile +> keepIccProfile() ⇒ Sharp + +Keep ICC profile from the input image in the output image. + +Where necessary, will attempt to convert the output colour space to match the profile. + + +**Since**: 0.33.0 +**Example** +```js +const outputWithIccProfile = await sharp(inputWithIccProfile) + .keepIccProfile() + .toBuffer(); +``` + + +## withIccProfile +> withIccProfile(icc, [options]) ⇒ Sharp + +Transform using an ICC profile and attach to the output image. + +This can either be an absolute filesystem path or +built-in profile name (`srgb`, `p3`, `cmyk`). + + **Throws**: - Error Invalid parameters +**Since**: 0.33.0 | Param | Type | Default | Description | | --- | --- | --- | --- | +| icc | string | | Absolute filesystem path to output ICC profile or built-in profile name (srgb, p3, cmyk). | | [options] | Object | | | -| [options.orientation] | number | | value between 1 and 8, used to update the EXIF `Orientation` tag. | -| [options.icc] | string | "'srgb'" | Filesystem path to output ICC profile, relative to `process.cwd()`, defaults to built-in sRGB. | -| [options.exif] | Object.<Object> | {} | Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data. | -| [options.density] | number | | Number of pixels per inch (DPI). | +| [options.attach] | number | true | Should the ICC profile be included in the output image metadata? | **Example** ```js -sharp('input.jpg') - .withMetadata() - .toFile('output-with-metadata.jpg') - .then(info => { ... }); +const outputWithP3 = await sharp(input) + .withIccProfile('p3') + .toBuffer(); ``` + + +## keepMetadata +> keepMetadata() ⇒ Sharp + +Keep all metadata (EXIF, ICC, XMP, IPTC) from the input image in the output image. + +The default behaviour, when `keepMetadata` is not used, is to convert to the device-independent +sRGB colour space and strip all metadata, including the removal of any ICC profile. + + +**Since**: 0.33.0 **Example** ```js -// Set output EXIF metadata -const data = await sharp(input) - .withMetadata({ - exif: { - IFD0: { - Copyright: 'The National Gallery' - }, - IFD3: { - GPSLatitudeRef: 'N', - GPSLatitude: '51/1 30/1 3230/100', - GPSLongitudeRef: 'W', - GPSLongitude: '0/1 7/1 4366/100' - } - } - }) +const outputWithMetadata = await sharp(inputWithMetadata) + .keepMetadata() + .toBuffer(); +``` + + +## withMetadata +> withMetadata([options]) ⇒ Sharp + +Keep most metadata (EXIF, XMP, IPTC) from the input image in the output image. + +This will also convert to and add a web-friendly sRGB ICC profile if appropriate. + +Allows orientation and density to be set or updated. + + +**Throws**: + +- Error Invalid parameters + + +| Param | Type | Description | +| --- | --- | --- | +| [options] | Object | | +| [options.orientation] | number | Used to update the EXIF `Orientation` tag, integer between 1 and 8. | +| [options.density] | number | Number of pixels per inch (DPI). | + +**Example** +```js +const outputSrgbWithMetadata = await sharp(inputRgbWithMetadata) + .withMetadata() .toBuffer(); ``` **Example** diff --git a/docs/changelog.md b/docs/changelog.md index a92d38926..f3e792b8e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -14,6 +14,8 @@ Requires libvips v8.15.0 * Remove `sharp.vendor`. +* Partially deprecate `withMetadata()`, use `withExif()` and `withIccProfile()`. + * Add experimental support for WebAssembly-based runtimes. [@RReverser](https://github.com/RReverser) @@ -41,6 +43,9 @@ Requires libvips v8.15.0 [#3823](https://github.com/lovell/sharp/pull/3823) [@uhthomas](https://github.com/uhthomas) +* Add more fine-grained control over output metadata. + [#3824](https://github.com/lovell/sharp/issues/3824) + * Ensure multi-page extract remains sequential. [#3837](https://github.com/lovell/sharp/issues/3837) diff --git a/docs/search-index.json b/docs/search-index.json index 67a517394..91bbffb21 100644 --- a/docs/search-index.json +++ b/docs/search-index.json @@ -1 +1 @@ -[{"t":"Prerequisites","d":"Node-API v9 compatible runtime e.g. Node.js 18.17.0","k":"prerequisites node api compatible runtime","l":"/install#prerequisites"},{"t":"Prebuilt binaries","d":"Ready-compiled sharp and libvips binaries are provided for use on the most common platforms macOS x64 10.13 macOS ARM64 Linux ARM glibc 2.28 Linux ARM64 glibc 2.26, musl 1.2.2 Linux s390x glibc 2.28 L","k":"prebuilt binaries compiled libvips common platforms macos arm linux glibc musl","l":"/install#prebuilt-binaries"},{"t":"Custom libvips","d":"To use a custom, globally-installed version of libvips instead of the provided binaries, make sure it is at least the version listed under engines.libvips in the package.json file and that it can be l","k":"custom libvips globally installed version instead binaries listed engines package json file","l":"/install#custom-libvips"},{"t":"Building from source","d":"This module will be compiled from source at npm install time when a globally-installed libvips is detected set the SHARP_IGNORE_GLOBAL_LIBVIPS environment variable to skip this, or when the npm instal","k":"building source module compiled npm install time globally installed libvips detected environment variable skip instal","l":"/install#building-from-source"},{"t":"WebAssembly","d":"Experimental support is provided for runtime environments that provide multi-threaded Wasm via Workers. sh npm install --cpuwasm32 sharp","k":"webassembly experimental runtime environments multi threaded wasm workers npm install cpuwasm","l":"/install#webassembly"},{"t":"FreeBSD","d":"The vips package must be installed before npm install is run. sh pkg install -y pkgconf vips sh cd /usr/ports/graphics/vips/ make install clean","k":"freebsd vips package installed npm install pkg pkgconf usr ports graphics clean","l":"/install#freebsd"},{"t":"Linux memory allocator","d":"The default memory allocator on most glibc-based Linux systems e.g. Debian, Red Hat is unsuitable for long-running, multi-threaded processes that involve lots of small memory allocations. For this rea","k":"linux memory allocator glibc systems debian red hat long running multi threaded processes small allocations rea","l":"/install#linux-memory-allocator"},{"t":"AWS Lambda","d":"The node_modules directory of the deployment package must include binaries for either the linux-x64 or linux-arm64 platforms depending on the chosen architecture. When building your deployment package","k":"aws lambda nodemodules directory deployment package binaries linux arm platforms depending chosen architecture building","l":"/install#aws-lambda"},{"t":"webpack","d":"Ensure sharp is excluded from bundling via the externals configuration. js externals sharp commonjs sharp","k":"webpack excluded bundling externals configuration commonjs","l":"/install#webpack"},{"t":"esbuild","d":"Ensure sharp is excluded from bundling via the external","k":"esbuild excluded bundling external","l":"/install#esbuild"},{"t":"TypeScript","d":"TypeScript definitions are published as part of the sharp package from v0.32.0. Previously these were available via the types/sharp package, which is now deprecated. When using Typescript, please ensu","k":"typescript definitions published package types deprecated ensu","l":"/install#typescript"},{"t":"Fonts","d":"When creating text images or rendering SVG images that contain text elements, fontconfig is used to find the relevant fonts. On Windows and macOS systems, all system fonts are available for use. On ma","k":"fonts creating text images rendering svg contain elements fontconfig find relevant windows macos systems system","l":"/install#fonts"},{"t":"Canvas and Windows","d":"If both canvas and sharp modules are used in the same Windows process, the following error may occur The specified procedure could not be found.","k":"canvas windows modules process error procedure could found","l":"/install#canvas-and-windows"},{"t":"Sharp","d":"Emits codeSharpeventinfo/code, codeSharpeventwarning/code a namenew_Sharp_new/a","k":"emits code fail limit input pixels unlimited sequential read density ignore icc pages page subifd level animated raw create text","l":"/api-constructor#sharp"},{"t":"clone","d":"Take a snapshot of the Sharp instance, returning a new instance. Cloned instances inherit the input of their parent instance. This allows multiple output Streams and therefore multiple processing pip","k":"clone snapshot instance returning new cloned instances inherit input parent multiple output streams processing pip","l":"/api-constructor#clone"},{"t":"metadata","d":"Fast access to uncached image metadata without decoding any compressed pixel data.","k":"metadata fast access uncached decoding compressed pixel data","l":"/api-input#metadata"},{"t":"stats","d":"Access to pixel-derived image statistics for every channel in the image. A Promise is returned when callback is not provided.","k":"stats access pixel derived statistics channel promise","l":"/api-input#stats"},{"t":"toFile","d":"Write output image data to a file.","k":"tofile write output data file","l":"/api-output#tofile"},{"t":"toBuffer","d":"Write output to a Buffer. JPEG, PNG, WebP, AVIF, TIFF, GIF and raw pixel data output are supported.","k":"tobuffer write output buffer jpeg png webp avif tiff gif raw pixel data resolve object","l":"/api-output#tobuffer"},{"t":"withMetadata","d":"Include all metadata EXIF, XMP, IPTC from the input image in the output image. This will also convert to and add a web-friendly sRGB ICC profile if appropriate, unless a custom output profile is prov","k":"withmetadata metadata exif xmp iptc input output convert add web friendly srgb icc profile appropriate custom prov orientation density","l":"/api-output#withmetadata"},{"t":"toFormat","d":"Force output to a given format.","k":"toformat force output format","l":"/api-output#toformat"},{"t":"jpeg","d":"Use these JPEG options for output image.","k":"jpeg output quality progressive chroma subsampling optimise coding optimize mozjpeg trellis quantisation overshoot deringing scans table quantization force","l":"/api-output#jpeg"},{"t":"png","d":"Use these PNG options for output image.","k":"png output progressive compression level adaptive filtering palette quality effort colours colors dither force","l":"/api-output#png"},{"t":"webp","d":"Use these WebP options for output image.","k":"webp output quality alpha lossless near smart subsample preset effort loop delay min size mixed force","l":"/api-output#webp"},{"t":"gif","d":"Use these GIF options for the output image.","k":"gif output reuse progressive colours colors effort dither inter frame max error palette loop delay force","l":"/api-output#gif"},{"t":"tiff","d":"Use these TIFF options for output image.","k":"tiff output quality force compression predictor pyramid tile width height xres yres resolution unit bitdepth miniswhite","l":"/api-output#tiff"},{"t":"avif","d":"Use these AVIF options for output image.","k":"avif output quality lossless effort chroma subsampling","l":"/api-output#avif"},{"t":"heif","d":"Use these HEIF options for output image.","k":"heif output compression quality lossless effort chroma subsampling","l":"/api-output#heif"},{"t":"jxl","d":"Use these JPEG-XL JXL options for output image.","k":"jxl jpeg output distance quality decoding tier lossless effort depth","l":"/api-output#jxl"},{"t":"tile","d":"Use tile-based deep zoom image pyramid output.","k":"tile deep zoom pyramid output size overlap angle background depth skip blanks container layout centre center basename","l":"/api-output#tile"},{"t":"timeout","d":"Set a timeout for processing, in seconds. Use a value of zero to continue processing indefinitely, the default behaviour.","k":"timeout processing seconds zero continue indefinitely behaviour","l":"/api-output#timeout"},{"t":"resize","d":"Resize image to width, height or width x height.","k":"resize width height fit position background kernel enlargement reduction fast shrink load","l":"/api-resize#resize"},{"t":"extend","d":"Extend / pad / extrude one or more edges of the image with either the provided background colour or pixels derived from the image. This operation will always occur after resizing and extraction, if a","k":"extend pad extrude edges background colour pixels derived operation resizing extraction","l":"/api-resize#extend"},{"t":"extract","d":"Extract/crop a region of the image.","k":"extract crop region left top width height","l":"/api-resize#extract"},{"t":"trim","d":"Trim pixels from all edges that contain values similar to the given background colour, which defaults to that of the top-left pixel.","k":"trim pixels edges contain similar background colour defaults top left pixel threshold line art","l":"/api-resize#trim"},{"t":"composite","d":"Composite images over the processed resized, extracted etc. image.","k":"composite images processed resized extracted","l":"/api-composite#composite"},{"t":"rotate","d":"Rotate the output image by either an explicit angle or auto-orient based on the EXIF Orientation tag.","k":"rotate output explicit angle auto orient exif orientation tag background","l":"/api-operation#rotate"},{"t":"flip","d":"Mirror the image vertically up-down about the x-axis. This always occurs before rotation, if any.","k":"flip mirror vertically down axis rotation","l":"/api-operation#flip"},{"t":"flop","d":"Mirror the image horizontally left-right about the y-axis. This always occurs before rotation, if any.","k":"flop mirror horizontally left right axis rotation","l":"/api-operation#flop"},{"t":"affine","d":"Perform an affine transform on an image. This operation will always occur after resizing, extraction and rotation, if any.","k":"affine transform operation resizing extraction rotation background idx idy odx ody interpolator","l":"/api-operation#affine"},{"t":"sharpen","d":"Sharpen the image.","k":"sharpen sigma","l":"/api-operation#sharpen"},{"t":"median","d":"Apply median filter. When used without parameters the default window is 3x3.","k":"median apply filter parameters window","l":"/api-operation#median"},{"t":"blur","d":"Blur the image.","k":"blur","l":"/api-operation#blur"},{"t":"flatten","d":"Merge alpha transparency channel, if any, with a background, then remove the alpha channel.","k":"flatten merge alpha transparency channel background remove","l":"/api-operation#flatten"},{"t":"unflatten","d":"Ensure the image has an alpha channel with all white pixel values made fully transparent.","k":"unflatten alpha channel white pixel made fully transparent","l":"/api-operation#unflatten"},{"t":"gamma","d":"Apply a gamma correction by reducing the encoding darken pre-resize at a factor of 1/gamma then increasing the encoding brighten post-resize at a factor of gamma. This can improve the perceived brigh","k":"gamma apply correction reducing encoding darken resize factor increasing brighten post improve perceived brigh alpha","l":"/api-operation#gamma"},{"t":"normalise","d":"Enhance output image contrast by stretching its luminance to cover a full dynamic range.","k":"normalise enhance output contrast stretching luminance cover full dynamic range lower upper","l":"/api-operation#normalise"},{"t":"normalize","d":"Alternative spelling of normalise.","k":"normalize normalise lower upper","l":"/api-operation#normalize"},{"t":"clahe","d":"Perform contrast limiting adaptive histogram equalization CLAHE.","k":"clahe contrast limiting adaptive histogram equalization width height max slope","l":"/api-operation#clahe"},{"t":"convolve","d":"Convolve the image with the specified kernel.","k":"convolve kernel","l":"/api-operation#convolve"},{"t":"threshold","d":"Any pixel value greater than or equal to the threshold value will be set to 255, otherwise it will be set to 0.","k":"threshold pixel greater equal otherwise greyscale grayscale raw","l":"/api-operation#threshold"},{"t":"recomb","d":"Recombine the image with the specified matrix.","k":"recomb recombine matrix","l":"/api-operation#recomb"},{"t":"modulate","d":"Transforms the image using brightness, saturation, hue rotation, and lightness. Brightness and lightness both operate on luminance, with the difference being that brightness is multiplicative whereas","k":"modulate transforms brightness saturation hue rotation lightness operate luminance difference being multiplicative whereas","l":"/api-operation#modulate"},{"t":"removeAlpha","d":"Remove alpha channel, if any. This is a no-op if the image does not have an alpha channel.","k":"removealpha remove alpha channel","l":"/api-channel#removealpha"},{"t":"ensureAlpha","d":"Ensure the output image has an alpha transparency channel. If missing, the added alpha channel will have the specified transparency level, defaulting to fully-opaque 1. This is a no-op if the image a","k":"ensurealpha output alpha transparency channel missing added level defaulting fully opaque","l":"/api-channel#ensurealpha"},{"t":"extractChannel","d":"Extract a single channel from a multi-channel image.","k":"extractchannel extract single channel multi","l":"/api-channel#extractchannel"},{"t":"joinChannel","d":"Join one or more channels to the image. The meaning of the added channels depends on the output colourspace, set with toColourspace. By default the output image will be web-friendly sRGB, with additi","k":"joinchannel join channels meaning added depends output colourspace tocolourspace web friendly srgb additi","l":"/api-channel#joinchannel"},{"t":"tint","d":"Tint the image using the provided colour. An alpha channel may be present and will be unchanged by the operation.","k":"tint colour alpha channel present unchanged operation","l":"/api-colour#tint"},{"t":"greyscale","d":"Convert to 8-bit greyscale 256 shades of grey. This is a linear operation. If the input image is in a non-linear colour space such as sRGB, use gamma with greyscale for the best results. By default t","k":"greyscale convert bit shades grey linear operation input colour space srgb gamma results","l":"/api-colour#greyscale"},{"t":"grayscale","d":"Alternative spelling of greyscale.","k":"grayscale greyscale","l":"/api-colour#grayscale"},{"t":"pipelineColorspace","d":"Alternative spelling of pipelineColourspace.","k":"","l":"/api-colour#pipelinecolorspace"},{"t":"versions","d":"An Object containing the version numbers of sharp, libvips and when using prebuilt binaries its dependencies.","k":"versions object version numbers libvips prebuilt binaries dependencies","l":"/api-utility#versions"},{"t":"interpolators","d":"An Object containing the available interpolators and their proper values","k":"interpolators object proper","l":"/api-utility#interpolators"},{"t":"queue","d":"An EventEmitter that emits a change event when a task is either - queued, waiting for _libuv_ to provide a worker thread - complete","k":"queue eventemitter emits change event queued waiting libuv worker thread complete","l":"/api-utility#queue"},{"t":"cache","d":"Gets or, when options are provided, sets the limits of _libvips_ operation cache. Existing entries in the cache will be trimmed after any change in limits. This method always returns cache statistics","k":"cache limits libvips operation existing entries trimmed change method returns statistics memory files items","l":"/api-utility#cache"},{"t":"concurrency","d":"Gets or, when a concurrency is provided, sets the maximum number of threads _libvips_ should use to process _each image_. These are from a thread pool managed by glib, which helps avoid the overhead","k":"concurrency maximum number threads libvips process thread pool managed glib helps avoid overhead","l":"/api-utility#concurrency"},{"t":"counters","d":"Provides access to internal task counters. - queue is the number of tasks this module has queued waiting for _libuv_ to provide a worker thread from its pool. - process is the number of resize tasks","k":"counters provides access internal queue number tasks module queued waiting libuv worker thread pool process resize","l":"/api-utility#counters"},{"t":"simd","d":"Get and set use of SIMD vector unit instructions. Requires libvips to have been compiled with liborc support.","k":"simd vector unit instructions libvips compiled liborc","l":"/api-utility#simd"},{"t":"block","d":"Block libvips operations at runtime.","k":"block libvips operations runtime operation","l":"/api-utility#block"},{"t":"unblock","d":"Unblock libvips operations at runtime.","k":"unblock libvips operations runtime operation","l":"/api-utility#unblock"}] \ No newline at end of file +[{"t":"Prerequisites","d":"Node-API v9 compatible runtime e.g. Node.js 18.17.0","k":"prerequisites node api compatible runtime","l":"/install#prerequisites"},{"t":"Prebuilt binaries","d":"Ready-compiled sharp and libvips binaries are provided for use on the most common platforms macOS x64 10.13 macOS ARM64 Linux ARM glibc 2.28 Linux ARM64 glibc 2.26, musl 1.2.2 Linux s390x glibc 2.28 L","k":"prebuilt binaries compiled libvips common platforms macos arm linux glibc musl","l":"/install#prebuilt-binaries"},{"t":"Custom libvips","d":"To use a custom, globally-installed version of libvips instead of the provided binaries, make sure it is at least the version listed under engines.libvips in the package.json file and that it can be l","k":"custom libvips globally installed version instead binaries listed engines package json file","l":"/install#custom-libvips"},{"t":"Building from source","d":"This module will be compiled from source at npm install time when a globally-installed libvips is detected set the SHARP_IGNORE_GLOBAL_LIBVIPS environment variable to skip this, or when the npm instal","k":"building source module compiled npm install time globally installed libvips detected environment variable skip instal","l":"/install#building-from-source"},{"t":"WebAssembly","d":"Experimental support is provided for runtime environments that provide multi-threaded Wasm via Workers. sh npm install --cpuwasm32 sharp","k":"webassembly experimental runtime environments multi threaded wasm workers npm install cpuwasm","l":"/install#webassembly"},{"t":"FreeBSD","d":"The vips package must be installed before npm install is run. sh pkg install -y pkgconf vips sh cd /usr/ports/graphics/vips/ make install clean","k":"freebsd vips package installed npm install pkg pkgconf usr ports graphics clean","l":"/install#freebsd"},{"t":"Linux memory allocator","d":"The default memory allocator on most glibc-based Linux systems e.g. Debian, Red Hat is unsuitable for long-running, multi-threaded processes that involve lots of small memory allocations. For this rea","k":"linux memory allocator glibc systems debian red hat long running multi threaded processes small allocations rea","l":"/install#linux-memory-allocator"},{"t":"AWS Lambda","d":"The node_modules directory of the deployment package must include binaries for either the linux-x64 or linux-arm64 platforms depending on the chosen architecture. When building your deployment package","k":"aws lambda nodemodules directory deployment package binaries linux arm platforms depending chosen architecture building","l":"/install#aws-lambda"},{"t":"webpack","d":"Ensure sharp is excluded from bundling via the externals configuration. js externals sharp commonjs sharp","k":"webpack excluded bundling externals configuration commonjs","l":"/install#webpack"},{"t":"esbuild","d":"Ensure sharp is excluded from bundling via the external","k":"esbuild excluded bundling external","l":"/install#esbuild"},{"t":"TypeScript","d":"TypeScript definitions are published as part of the sharp package from v0.32.0. Previously these were available via the types/sharp package, which is now deprecated. When using Typescript, please ensu","k":"typescript definitions published package types deprecated ensu","l":"/install#typescript"},{"t":"Fonts","d":"When creating text images or rendering SVG images that contain text elements, fontconfig is used to find the relevant fonts. On Windows and macOS systems, all system fonts are available for use. On ma","k":"fonts creating text images rendering svg contain elements fontconfig find relevant windows macos systems system","l":"/install#fonts"},{"t":"Canvas and Windows","d":"If both canvas and sharp modules are used in the same Windows process, the following error may occur The specified procedure could not be found.","k":"canvas windows modules process error procedure could found","l":"/install#canvas-and-windows"},{"t":"Sharp","d":"Emits codeSharpeventinfo/code, codeSharpeventwarning/code a namenew_Sharp_new/a","k":"emits code fail limit input pixels unlimited sequential read density ignore icc pages page subifd level animated raw create text","l":"/api-constructor#sharp"},{"t":"clone","d":"Take a snapshot of the Sharp instance, returning a new instance. Cloned instances inherit the input of their parent instance. This allows multiple output Streams and therefore multiple processing pip","k":"clone snapshot instance returning new cloned instances inherit input parent multiple output streams processing pip","l":"/api-constructor#clone"},{"t":"metadata","d":"Fast access to uncached image metadata without decoding any compressed pixel data.","k":"metadata fast access uncached decoding compressed pixel data","l":"/api-input#metadata"},{"t":"stats","d":"Access to pixel-derived image statistics for every channel in the image. A Promise is returned when callback is not provided.","k":"stats access pixel derived statistics channel promise","l":"/api-input#stats"},{"t":"toFile","d":"Write output image data to a file.","k":"tofile write output data file","l":"/api-output#tofile"},{"t":"toBuffer","d":"Write output to a Buffer. JPEG, PNG, WebP, AVIF, TIFF, GIF and raw pixel data output are supported.","k":"tobuffer write output buffer jpeg png webp avif tiff gif raw pixel data resolve object","l":"/api-output#tobuffer"},{"t":"keepExif","d":"Keep all EXIF metadata from the input image in the output image.","k":"keepexif keep exif metadata input output","l":"/api-output#keepexif"},{"t":"withExif","d":"Set EXIF metadata in the output image, ignoring any EXIF in the input image.","k":"withexif exif metadata output ignoring input","l":"/api-output#withexif"},{"t":"withExifMerge","d":"Update EXIF metadata from the input image in the output image.","k":"withexifmerge update exif metadata input output","l":"/api-output#withexifmerge"},{"t":"keepIccProfile","d":"Keep ICC profile from the input image in the output image.","k":"keepiccprofile keep icc profile input output","l":"/api-output#keepiccprofile"},{"t":"withIccProfile","d":"Transform using an ICC profile and attach to the output image.","k":"withiccprofile transform icc profile attach output","l":"/api-output#withiccprofile"},{"t":"keepMetadata","d":"Keep all metadata EXIF, ICC, XMP, IPTC from the input image in the output image.","k":"keepmetadata keep metadata exif icc xmp iptc input output","l":"/api-output#keepmetadata"},{"t":"withMetadata","d":"Keep most metadata EXIF, XMP, IPTC from the input image in the output image.","k":"withmetadata keep metadata exif xmp iptc input output orientation density","l":"/api-output#withmetadata"},{"t":"toFormat","d":"Force output to a given format.","k":"toformat force output format","l":"/api-output#toformat"},{"t":"jpeg","d":"Use these JPEG options for output image.","k":"jpeg output quality progressive chroma subsampling optimise coding optimize mozjpeg trellis quantisation overshoot deringing scans table quantization force","l":"/api-output#jpeg"},{"t":"png","d":"Use these PNG options for output image.","k":"png output progressive compression level adaptive filtering palette quality effort colours colors dither force","l":"/api-output#png"},{"t":"webp","d":"Use these WebP options for output image.","k":"webp output quality alpha lossless near smart subsample preset effort loop delay min size mixed force","l":"/api-output#webp"},{"t":"gif","d":"Use these GIF options for the output image.","k":"gif output reuse progressive colours colors effort dither inter frame max error palette loop delay force","l":"/api-output#gif"},{"t":"tiff","d":"Use these TIFF options for output image.","k":"tiff output quality force compression predictor pyramid tile width height xres yres resolution unit bitdepth miniswhite","l":"/api-output#tiff"},{"t":"avif","d":"Use these AVIF options for output image.","k":"avif output quality lossless effort chroma subsampling","l":"/api-output#avif"},{"t":"heif","d":"Use these HEIF options for output image.","k":"heif output compression quality lossless effort chroma subsampling","l":"/api-output#heif"},{"t":"jxl","d":"Use these JPEG-XL JXL options for output image.","k":"jxl jpeg output distance quality decoding tier lossless effort depth","l":"/api-output#jxl"},{"t":"tile","d":"Use tile-based deep zoom image pyramid output.","k":"tile deep zoom pyramid output size overlap angle background depth skip blanks container layout centre center basename","l":"/api-output#tile"},{"t":"timeout","d":"Set a timeout for processing, in seconds. Use a value of zero to continue processing indefinitely, the default behaviour.","k":"timeout processing seconds zero continue indefinitely behaviour","l":"/api-output#timeout"},{"t":"resize","d":"Resize image to width, height or width x height.","k":"resize width height fit position background kernel enlargement reduction fast shrink load","l":"/api-resize#resize"},{"t":"extend","d":"Extend / pad / extrude one or more edges of the image with either the provided background colour or pixels derived from the image. This operation will always occur after resizing and extraction, if a","k":"extend pad extrude edges background colour pixels derived operation resizing extraction","l":"/api-resize#extend"},{"t":"extract","d":"Extract/crop a region of the image.","k":"extract crop region left top width height","l":"/api-resize#extract"},{"t":"trim","d":"Trim pixels from all edges that contain values similar to the given background colour, which defaults to that of the top-left pixel.","k":"trim pixels edges contain similar background colour defaults top left pixel threshold line art","l":"/api-resize#trim"},{"t":"composite","d":"Composite images over the processed resized, extracted etc. image.","k":"composite images processed resized extracted","l":"/api-composite#composite"},{"t":"rotate","d":"Rotate the output image by either an explicit angle or auto-orient based on the EXIF Orientation tag.","k":"rotate output explicit angle auto orient exif orientation tag background","l":"/api-operation#rotate"},{"t":"flip","d":"Mirror the image vertically up-down about the x-axis. This always occurs before rotation, if any.","k":"flip mirror vertically down axis rotation","l":"/api-operation#flip"},{"t":"flop","d":"Mirror the image horizontally left-right about the y-axis. This always occurs before rotation, if any.","k":"flop mirror horizontally left right axis rotation","l":"/api-operation#flop"},{"t":"affine","d":"Perform an affine transform on an image. This operation will always occur after resizing, extraction and rotation, if any.","k":"affine transform operation resizing extraction rotation background idx idy odx ody interpolator","l":"/api-operation#affine"},{"t":"sharpen","d":"Sharpen the image.","k":"sharpen sigma","l":"/api-operation#sharpen"},{"t":"median","d":"Apply median filter. When used without parameters the default window is 3x3.","k":"median apply filter parameters window","l":"/api-operation#median"},{"t":"blur","d":"Blur the image.","k":"blur","l":"/api-operation#blur"},{"t":"flatten","d":"Merge alpha transparency channel, if any, with a background, then remove the alpha channel.","k":"flatten merge alpha transparency channel background remove","l":"/api-operation#flatten"},{"t":"unflatten","d":"Ensure the image has an alpha channel with all white pixel values made fully transparent.","k":"unflatten alpha channel white pixel made fully transparent","l":"/api-operation#unflatten"},{"t":"gamma","d":"Apply a gamma correction by reducing the encoding darken pre-resize at a factor of 1/gamma then increasing the encoding brighten post-resize at a factor of gamma. This can improve the perceived brigh","k":"gamma apply correction reducing encoding darken resize factor increasing brighten post improve perceived brigh alpha","l":"/api-operation#gamma"},{"t":"normalise","d":"Enhance output image contrast by stretching its luminance to cover a full dynamic range.","k":"normalise enhance output contrast stretching luminance cover full dynamic range lower upper","l":"/api-operation#normalise"},{"t":"normalize","d":"Alternative spelling of normalise.","k":"normalize normalise lower upper","l":"/api-operation#normalize"},{"t":"clahe","d":"Perform contrast limiting adaptive histogram equalization CLAHE.","k":"clahe contrast limiting adaptive histogram equalization width height max slope","l":"/api-operation#clahe"},{"t":"convolve","d":"Convolve the image with the specified kernel.","k":"convolve kernel","l":"/api-operation#convolve"},{"t":"threshold","d":"Any pixel value greater than or equal to the threshold value will be set to 255, otherwise it will be set to 0.","k":"threshold pixel greater equal otherwise greyscale grayscale raw","l":"/api-operation#threshold"},{"t":"recomb","d":"Recombine the image with the specified matrix.","k":"recomb recombine matrix","l":"/api-operation#recomb"},{"t":"modulate","d":"Transforms the image using brightness, saturation, hue rotation, and lightness. Brightness and lightness both operate on luminance, with the difference being that brightness is multiplicative whereas","k":"modulate transforms brightness saturation hue rotation lightness operate luminance difference being multiplicative whereas","l":"/api-operation#modulate"},{"t":"removeAlpha","d":"Remove alpha channel, if any. This is a no-op if the image does not have an alpha channel.","k":"removealpha remove alpha channel","l":"/api-channel#removealpha"},{"t":"ensureAlpha","d":"Ensure the output image has an alpha transparency channel. If missing, the added alpha channel will have the specified transparency level, defaulting to fully-opaque 1. This is a no-op if the image a","k":"ensurealpha output alpha transparency channel missing added level defaulting fully opaque","l":"/api-channel#ensurealpha"},{"t":"extractChannel","d":"Extract a single channel from a multi-channel image.","k":"extractchannel extract single channel multi","l":"/api-channel#extractchannel"},{"t":"joinChannel","d":"Join one or more channels to the image. The meaning of the added channels depends on the output colourspace, set with toColourspace. By default the output image will be web-friendly sRGB, with additi","k":"joinchannel join channels meaning added depends output colourspace tocolourspace web friendly srgb additi","l":"/api-channel#joinchannel"},{"t":"tint","d":"Tint the image using the provided colour. An alpha channel may be present and will be unchanged by the operation.","k":"tint colour alpha channel present unchanged operation","l":"/api-colour#tint"},{"t":"greyscale","d":"Convert to 8-bit greyscale 256 shades of grey. This is a linear operation. If the input image is in a non-linear colour space such as sRGB, use gamma with greyscale for the best results. By default t","k":"greyscale convert bit shades grey linear operation input colour space srgb gamma results","l":"/api-colour#greyscale"},{"t":"grayscale","d":"Alternative spelling of greyscale.","k":"grayscale greyscale","l":"/api-colour#grayscale"},{"t":"pipelineColorspace","d":"Alternative spelling of pipelineColourspace.","k":"","l":"/api-colour#pipelinecolorspace"},{"t":"versions","d":"An Object containing the version numbers of sharp, libvips and when using prebuilt binaries its dependencies.","k":"versions object version numbers libvips prebuilt binaries dependencies","l":"/api-utility#versions"},{"t":"interpolators","d":"An Object containing the available interpolators and their proper values","k":"interpolators object proper","l":"/api-utility#interpolators"},{"t":"queue","d":"An EventEmitter that emits a change event when a task is either - queued, waiting for _libuv_ to provide a worker thread - complete","k":"queue eventemitter emits change event queued waiting libuv worker thread complete","l":"/api-utility#queue"},{"t":"cache","d":"Gets or, when options are provided, sets the limits of _libvips_ operation cache. Existing entries in the cache will be trimmed after any change in limits. This method always returns cache statistics","k":"cache limits libvips operation existing entries trimmed change method returns statistics memory files items","l":"/api-utility#cache"},{"t":"concurrency","d":"Gets or, when a concurrency is provided, sets the maximum number of threads _libvips_ should use to process _each image_. These are from a thread pool managed by glib, which helps avoid the overhead","k":"concurrency maximum number threads libvips process thread pool managed glib helps avoid overhead","l":"/api-utility#concurrency"},{"t":"counters","d":"Provides access to internal task counters. - queue is the number of tasks this module has queued waiting for _libuv_ to provide a worker thread from its pool. - process is the number of resize tasks","k":"counters provides access internal queue number tasks module queued waiting libuv worker thread pool process resize","l":"/api-utility#counters"},{"t":"simd","d":"Get and set use of SIMD vector unit instructions. Requires libvips to have been compiled with liborc support.","k":"simd vector unit instructions libvips compiled liborc","l":"/api-utility#simd"},{"t":"block","d":"Block libvips operations at runtime.","k":"block libvips operations runtime operation","l":"/api-utility#block"},{"t":"unblock","d":"Unblock libvips operations at runtime.","k":"unblock libvips operations runtime operation","l":"/api-utility#unblock"}] \ No newline at end of file diff --git a/lib/constructor.js b/lib/constructor.js index 4e624e950..38151d8fd 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -257,11 +257,12 @@ const Sharp = function (input, options) { fileOut: '', formatOut: 'input', streamOut: false, - withMetadata: false, + keepMetadata: 0, withMetadataOrientation: -1, withMetadataDensity: 0, - withMetadataIcc: '', - withMetadataStrs: {}, + withIccProfile: '', + withExif: {}, + withExifMerge: true, resolveWithObject: false, // output format jpegQuality: 80, diff --git a/lib/index.d.ts b/lib/index.d.ts index 41b13ac68..cc86bfa40 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -633,6 +633,43 @@ declare namespace sharp { */ toBuffer(options: { resolveWithObject: true }): Promise<{ data: Buffer; info: OutputInfo }>; + /** + * Keep all EXIF metadata from the input image in the output image. + * EXIF metadata is unsupported for TIFF output. + * @returns A sharp instance that can be used to chain operations + */ + keepExif(): Sharp; + + /** + * Set EXIF metadata in the output image, ignoring any EXIF in the input image. + * @param {Exif} exif Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data. + * @returns A sharp instance that can be used to chain operations + * @throws {Error} Invalid parameters + */ + withExif(exif: Exif): Sharp; + + /** + * Update EXIF metadata from the input image in the output image. + * @param {Exif} exif Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data. + * @returns A sharp instance that can be used to chain operations + * @throws {Error} Invalid parameters + */ + withExifMerge(exif: Exif): Sharp; + + /** + * Keep ICC profile from the input image in the output image where possible. + * @returns A sharp instance that can be used to chain operations + */ + keepIccProfile(): Sharp; + + /** + * Transform using an ICC profile and attach to the output image. + * @param {string} icc - Absolute filesystem path to output ICC profile or built-in profile name (srgb, p3, cmyk). + * @returns A sharp instance that can be used to chain operations + * @throws {Error} Invalid parameters + */ + withIccProfile(icc: string, options?: WithIccProfileOptions): Sharp; + /** * Include all metadata (EXIF, XMP, IPTC) from the input image in the output image. * The default behaviour, when withMetadata is not used, is to strip all metadata and convert to the device-independent sRGB colour space. @@ -640,7 +677,7 @@ declare namespace sharp { * @param withMetadata * @throws {Error} Invalid parameters. */ - withMetadata(withMetadata?: boolean | WriteableMetadata): Sharp; + withMetadata(withMetadata?: WriteableMetadata): Sharp; /** * Use these JPEG options for output image. @@ -978,15 +1015,32 @@ declare namespace sharp { wrap?: TextWrap; } + interface ExifDir { + [k: string]: string; + } + + interface Exif { + 'IFD0'?: ExifDir; + 'IFD1'?: ExifDir; + 'IFD2'?: ExifDir; + 'IFD3'?: ExifDir; + } + interface WriteableMetadata { + /** Number of pixels per inch (DPI) */ + density?: number | undefined; /** Value between 1 and 8, used to update the EXIF Orientation tag. */ orientation?: number | undefined; - /** Filesystem path to output ICC profile, defaults to sRGB. */ + /** + * Filesystem path to output ICC profile, defaults to sRGB. + * @deprecated Use `withIccProfile()` instead. + */ icc?: string | undefined; - /** Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data. (optional, default {}) */ - exif?: Record | undefined; - /** Number of pixels per inch (DPI) */ - density?: number | undefined; + /** + * Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data. + * @deprecated Use `withExif()` or `withExifMerge()` instead. + */ + exif?: Exif | undefined; } interface Metadata { @@ -1096,6 +1150,11 @@ declare namespace sharp { force?: boolean | undefined; } + interface WithIccProfileOptions { + /** Should the ICC profile be included in the output image metadata? (optional, default true) */ + attach?: boolean | undefined; + } + interface JpegOptions extends OutputOptions { /** Quality, integer 1-100 (optional, default 80) */ quality?: number | undefined; diff --git a/lib/output.js b/lib/output.js index c2e21ee19..ede2f14a5 100644 --- a/lib/output.js +++ b/lib/output.js @@ -163,39 +163,185 @@ function toBuffer (options, callback) { } /** - * Include all metadata (EXIF, XMP, IPTC) from the input image in the output image. - * This will also convert to and add a web-friendly sRGB ICC profile if appropriate, - * unless a custom output profile is provided. - * - * The default behaviour, when `withMetadata` is not used, is to convert to the device-independent - * sRGB colour space and strip all metadata, including the removal of any ICC profile. + * Keep all EXIF metadata from the input image in the output image. * * EXIF metadata is unsupported for TIFF output. * + * @since 0.33.0 + * * @example - * sharp('input.jpg') - * .withMetadata() - * .toFile('output-with-metadata.jpg') - * .then(info => { ... }); + * const outputWithExif = await sharp(inputWithExif) + * .keepExif() + * .toBuffer(); + * + * @returns {Sharp} + */ +function keepExif () { + this.options.keepMetadata |= 0b00001; + return this; +} + +/** + * Set EXIF metadata in the output image, ignoring any EXIF in the input image. + * + * @since 0.33.0 * * @example - * // Set output EXIF metadata - * const data = await sharp(input) - * .withMetadata({ - * exif: { - * IFD0: { - * Copyright: 'The National Gallery' - * }, - * IFD3: { - * GPSLatitudeRef: 'N', - * GPSLatitude: '51/1 30/1 3230/100', - * GPSLongitudeRef: 'W', - * GPSLongitude: '0/1 7/1 4366/100' - * } + * const dataWithExif = await sharp(input) + * .withExif({ + * IFD0: { + * Copyright: 'The National Gallery' + * }, + * IFD3: { + * GPSLatitudeRef: 'N', + * GPSLatitude: '51/1 30/1 3230/100', + * GPSLongitudeRef: 'W', + * GPSLongitude: '0/1 7/1 4366/100' + * } + * }) + * .toBuffer(); + * + * @param {Object>} exif Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data. + * @returns {Sharp} + * @throws {Error} Invalid parameters + */ +function withExif (exif) { + if (is.object(exif)) { + for (const [ifd, entries] of Object.entries(exif)) { + if (is.object(entries)) { + for (const [k, v] of Object.entries(entries)) { + if (is.string(v)) { + this.options.withExif[`exif-${ifd.toLowerCase()}-${k}`] = v; + } else { + throw is.invalidParameterError(`${ifd}.${k}`, 'string', v); + } + } + } else { + throw is.invalidParameterError(ifd, 'object', entries); + } + } + } else { + throw is.invalidParameterError('exif', 'object', exif); + } + this.options.withExifMerge = false; + return this.keepExif(); +} + +/** + * Update EXIF metadata from the input image in the output image. + * + * @since 0.33.0 + * + * @example + * const dataWithMergedExif = await sharp(inputWithExif) + * .withExifMerge({ + * IFD0: { + * Copyright: 'The National Gallery' * } * }) * .toBuffer(); * + * @param {Object>} exif Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data. + * @returns {Sharp} + * @throws {Error} Invalid parameters + */ +function withExifMerge (exif) { + this.withExif(exif); + this.options.withExifMerge = true; + return this; +} + +/** + * Keep ICC profile from the input image in the output image. + * + * Where necessary, will attempt to convert the output colour space to match the profile. + * + * @since 0.33.0 + * + * @example + * const outputWithIccProfile = await sharp(inputWithIccProfile) + * .keepIccProfile() + * .toBuffer(); + * + * @returns {Sharp} + */ +function keepIccProfile () { + this.options.keepMetadata |= 0b01000; + return this; +} + +/** + * Transform using an ICC profile and attach to the output image. + * + * This can either be an absolute filesystem path or + * built-in profile name (`srgb`, `p3`, `cmyk`). + * + * @since 0.33.0 + * + * @example + * const outputWithP3 = await sharp(input) + * .withIccProfile('p3') + * .toBuffer(); + * + * @param {string} icc - Absolute filesystem path to output ICC profile or built-in profile name (srgb, p3, cmyk). + * @param {Object} [options] + * @param {number} [options.attach=true] Should the ICC profile be included in the output image metadata? + * @returns {Sharp} + * @throws {Error} Invalid parameters + */ +function withIccProfile (icc, options) { + if (is.string(icc)) { + this.options.withIccProfile = icc; + } else { + throw is.invalidParameterError('icc', 'string', icc); + } + this.keepIccProfile(); + if (is.object(options)) { + if (is.defined(options.attach)) { + if (is.bool(options.attach)) { + if (!options.attach) { + this.options.keepMetadata &= ~0b01000; + } + } else { + throw is.invalidParameterError('attach', 'boolean', options.attach); + } + } + } + return this; +} + +/** + * Keep all metadata (EXIF, ICC, XMP, IPTC) from the input image in the output image. + * + * The default behaviour, when `keepMetadata` is not used, is to convert to the device-independent + * sRGB colour space and strip all metadata, including the removal of any ICC profile. + * + * @since 0.33.0 + * + * @example + * const outputWithMetadata = await sharp(inputWithMetadata) + * .keepMetadata() + * .toBuffer(); + * + * @returns {Sharp} + */ +function keepMetadata () { + this.options.keepMetadata = 0b11111; + return this; +} + +/** + * Keep most metadata (EXIF, XMP, IPTC) from the input image in the output image. + * + * This will also convert to and add a web-friendly sRGB ICC profile if appropriate. + * + * Allows orientation and density to be set or updated. + * + * @example + * const outputSrgbWithMetadata = await sharp(inputRgbWithMetadata) + * .withMetadata() + * .toBuffer(); + * * @example * // Set output metadata to 96 DPI * const data = await sharp(input) @@ -203,15 +349,14 @@ function toBuffer (options, callback) { * .toBuffer(); * * @param {Object} [options] - * @param {number} [options.orientation] value between 1 and 8, used to update the EXIF `Orientation` tag. - * @param {string} [options.icc='srgb'] Filesystem path to output ICC profile, relative to `process.cwd()`, defaults to built-in sRGB. - * @param {Object} [options.exif={}] Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data. + * @param {number} [options.orientation] Used to update the EXIF `Orientation` tag, integer between 1 and 8. * @param {number} [options.density] Number of pixels per inch (DPI). * @returns {Sharp} * @throws {Error} Invalid parameters */ function withMetadata (options) { - this.options.withMetadata = is.bool(options) ? options : true; + this.keepMetadata(); + this.withIccProfile('srgb'); if (is.object(options)) { if (is.defined(options.orientation)) { if (is.integer(options.orientation) && is.inRange(options.orientation, 1, 8)) { @@ -228,30 +373,10 @@ function withMetadata (options) { } } if (is.defined(options.icc)) { - if (is.string(options.icc)) { - this.options.withMetadataIcc = options.icc; - } else { - throw is.invalidParameterError('icc', 'string filesystem path to ICC profile', options.icc); - } + this.withIccProfile(options.icc); } if (is.defined(options.exif)) { - if (is.object(options.exif)) { - for (const [ifd, entries] of Object.entries(options.exif)) { - if (is.object(entries)) { - for (const [k, v] of Object.entries(entries)) { - if (is.string(v)) { - this.options.withMetadataStrs[`exif-${ifd.toLowerCase()}-${k}`] = v; - } else { - throw is.invalidParameterError(`exif.${ifd}.${k}`, 'string', v); - } - } - } else { - throw is.invalidParameterError(`exif.${ifd}`, 'object', entries); - } - } - } else { - throw is.invalidParameterError('exif', 'object', options.exif); - } + this.withExifMerge(options.exif); } } return this; @@ -1407,6 +1532,12 @@ module.exports = function (Sharp) { // Public toFile, toBuffer, + keepExif, + withExif, + withExifMerge, + keepIccProfile, + withIccProfile, + keepMetadata, withMetadata, toFormat, jpeg, diff --git a/src/common.cc b/src/common.cc index 3b6b28272..588b2a07e 100644 --- a/src/common.cc +++ b/src/common.cc @@ -531,7 +531,33 @@ namespace sharp { Does this image have an embedded profile? */ bool HasProfile(VImage image) { - return (image.get_typeof(VIPS_META_ICC_NAME) != 0) ? TRUE : FALSE; + return image.get_typeof(VIPS_META_ICC_NAME) == VIPS_TYPE_BLOB; + } + + /* + Get copy of embedded profile. + */ + std::pair GetProfile(VImage image) { + std::pair icc(nullptr, 0); + if (HasProfile(image)) { + size_t length; + const void *data = image.get_blob(VIPS_META_ICC_NAME, &length); + icc.first = static_cast(g_malloc(length)); + icc.second = length; + memcpy(icc.first, data, length); + } + return icc; + } + + /* + Set embedded profile. + */ + VImage SetProfile(VImage image, std::pair icc) { + if (icc.first != nullptr) { + image = image.copy(); + image.set(VIPS_META_ICC_NAME, reinterpret_cast(vips_area_free_cb), icc.first, icc.second); + } + return image; } /* @@ -542,6 +568,27 @@ namespace sharp { return image.has_alpha(); } + static void* RemoveExifCallback(VipsImage *image, char const *field, GValue *value, void *data) { + std::vector *fieldNames = static_cast *>(data); + std::string fieldName(field); + if (fieldName.substr(0, 8) == ("exif-ifd")) { + fieldNames->push_back(fieldName); + } + return nullptr; + } + + /* + Remove all EXIF-related image fields. + */ + VImage RemoveExif(VImage image) { + std::vector fieldNames; + vips_image_map(image.get_image(), static_cast(RemoveExifCallback), &fieldNames); + for (const auto& f : fieldNames) { + image.remove(f.data()); + } + return image; + } + /* Get EXIF Orientation of image, if any. */ diff --git a/src/common.h b/src/common.h index e7d9e0d52..13e7a17b6 100644 --- a/src/common.h +++ b/src/common.h @@ -222,12 +222,27 @@ namespace sharp { */ bool HasProfile(VImage image); + /* + Get copy of embedded profile. + */ + std::pair GetProfile(VImage image); + + /* + Set embedded profile. + */ + VImage SetProfile(VImage image, std::pair icc); + /* Does this image have an alpha channel? Uses colour space interpretation with number of channels to guess this. */ bool HasAlpha(VImage image); + /* + Remove all EXIF-related image fields. + */ + VImage RemoveExif(VImage image); + /* Get EXIF Orientation of image, if any. */ diff --git a/src/pipeline.cc b/src/pipeline.cc index 9e31adeec..9bc4fe3e6 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -315,6 +315,11 @@ class PipelineWorker : public Napi::AsyncWorker { } // Ensure we're using a device-independent colour space + std::pair inputProfile(nullptr, 0); + if ((baton->keepMetadata & VIPS_FOREIGN_KEEP_ICC) && baton->withIccProfile.empty()) { + // Cache input profile for use with output + inputProfile = sharp::GetProfile(image); + } char const *processingProfile = image.interpretation() == VIPS_INTERPRETATION_RGB16 ? "p3" : "srgb"; if ( sharp::HasProfile(image) && @@ -758,7 +763,8 @@ class PipelineWorker : public Napi::AsyncWorker { // Convert colourspace, pass the current known interpretation so libvips doesn't have to guess image = image.colourspace(baton->colourspace, VImage::option()->set("source_space", image.interpretation())); // Transform colours from embedded profile to output profile - if (baton->withMetadata && sharp::HasProfile(image) && baton->withMetadataIcc.empty()) { + if ((baton->keepMetadata & VIPS_FOREIGN_KEEP_ICC) && + baton->withIccProfile.empty() && sharp::HasProfile(image)) { image = image.icc_transform("srgb", VImage::option() ->set("embedded", TRUE) ->set("depth", sharp::Is16Bit(image.interpretation()) ? 16 : 8) @@ -787,27 +793,32 @@ class PipelineWorker : public Napi::AsyncWorker { } // Apply output ICC profile - if (baton->withMetadata) { - image = image.icc_transform( - baton->withMetadataIcc.empty() ? "srgb" : const_cast(baton->withMetadataIcc.data()), - VImage::option() + if (baton->keepMetadata & VIPS_FOREIGN_KEEP_ICC) { + if (!baton->withIccProfile.empty()) { + image = image.icc_transform(const_cast(baton->withIccProfile.data()), VImage::option() ->set("input_profile", processingProfile) ->set("embedded", TRUE) ->set("depth", sharp::Is16Bit(image.interpretation()) ? 16 : 8) ->set("intent", VIPS_INTENT_PERCEPTUAL)); + } else { + image = sharp::SetProfile(image, inputProfile); + } } // Override EXIF Orientation tag - if (baton->withMetadata && baton->withMetadataOrientation != -1) { + if (baton->withMetadataOrientation != -1) { image = sharp::SetExifOrientation(image, baton->withMetadataOrientation); } // Override pixel density if (baton->withMetadataDensity > 0) { image = sharp::SetDensity(image, baton->withMetadataDensity); } - // Metadata key/value pairs, e.g. EXIF - if (!baton->withMetadataStrs.empty()) { + // EXIF key/value pairs + if (baton->keepMetadata & VIPS_FOREIGN_KEEP_EXIF) { image = image.copy(); - for (const auto& s : baton->withMetadataStrs) { + if (!baton->withExifMerge) { + image = sharp::RemoveExif(image); + } + for (const auto& s : baton->withExif) { image.set(s.first.data(), s.second.data()); } } @@ -828,7 +839,7 @@ class PipelineWorker : public Napi::AsyncWorker { // Write JPEG to buffer sharp::AssertImageTypeDimensions(image, sharp::ImageType::JPEG); VipsArea *area = reinterpret_cast(image.jpegsave_buffer(VImage::option() - ->set("strip", !baton->withMetadata) + ->set("keep", baton->keepMetadata) ->set("Q", baton->jpegQuality) ->set("interlace", baton->jpegProgressive) ->set("subsample_mode", baton->jpegChromaSubsampling == "4:4:4" @@ -870,7 +881,7 @@ class PipelineWorker : public Napi::AsyncWorker { // Write PNG to buffer sharp::AssertImageTypeDimensions(image, sharp::ImageType::PNG); VipsArea *area = reinterpret_cast(image.pngsave_buffer(VImage::option() - ->set("strip", !baton->withMetadata) + ->set("keep", baton->keepMetadata) ->set("interlace", baton->pngProgressive) ->set("compression", baton->pngCompressionLevel) ->set("filter", baton->pngAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_ALL : VIPS_FOREIGN_PNG_FILTER_NONE) @@ -889,7 +900,7 @@ class PipelineWorker : public Napi::AsyncWorker { // Write WEBP to buffer sharp::AssertImageTypeDimensions(image, sharp::ImageType::WEBP); VipsArea *area = reinterpret_cast(image.webpsave_buffer(VImage::option() - ->set("strip", !baton->withMetadata) + ->set("keep", baton->keepMetadata) ->set("Q", baton->webpQuality) ->set("lossless", baton->webpLossless) ->set("near_lossless", baton->webpNearLossless) @@ -909,7 +920,7 @@ class PipelineWorker : public Napi::AsyncWorker { // Write GIF to buffer sharp::AssertImageTypeDimensions(image, sharp::ImageType::GIF); VipsArea *area = reinterpret_cast(image.gifsave_buffer(VImage::option() - ->set("strip", !baton->withMetadata) + ->set("keep", baton->keepMetadata) ->set("bitdepth", baton->gifBitdepth) ->set("effort", baton->gifEffort) ->set("reuse", baton->gifReuse) @@ -934,7 +945,7 @@ class PipelineWorker : public Napi::AsyncWorker { image = image.cast(VIPS_FORMAT_FLOAT); } VipsArea *area = reinterpret_cast(image.tiffsave_buffer(VImage::option() - ->set("strip", !baton->withMetadata) + ->set("keep", baton->keepMetadata) ->set("Q", baton->tiffQuality) ->set("bitdepth", baton->tiffBitdepth) ->set("compression", baton->tiffCompression) @@ -958,7 +969,7 @@ class PipelineWorker : public Napi::AsyncWorker { sharp::AssertImageTypeDimensions(image, sharp::ImageType::HEIF); image = sharp::RemoveAnimationProperties(image).cast(VIPS_FORMAT_UCHAR); VipsArea *area = reinterpret_cast(image.heifsave_buffer(VImage::option() - ->set("strip", !baton->withMetadata) + ->set("keep", baton->keepMetadata) ->set("Q", baton->heifQuality) ->set("compression", baton->heifCompression) ->set("effort", baton->heifEffort) @@ -990,7 +1001,7 @@ class PipelineWorker : public Napi::AsyncWorker { // Write JXL to buffer image = sharp::RemoveAnimationProperties(image); VipsArea *area = reinterpret_cast(image.jxlsave_buffer(VImage::option() - ->set("strip", !baton->withMetadata) + ->set("keep", baton->keepMetadata) ->set("distance", baton->jxlDistance) ->set("tier", baton->jxlDecodingTier) ->set("effort", baton->jxlEffort) @@ -1051,7 +1062,7 @@ class PipelineWorker : public Napi::AsyncWorker { // Write JPEG to file sharp::AssertImageTypeDimensions(image, sharp::ImageType::JPEG); image.jpegsave(const_cast(baton->fileOut.data()), VImage::option() - ->set("strip", !baton->withMetadata) + ->set("keep", baton->keepMetadata) ->set("Q", baton->jpegQuality) ->set("interlace", baton->jpegProgressive) ->set("subsample_mode", baton->jpegChromaSubsampling == "4:4:4" @@ -1081,7 +1092,7 @@ class PipelineWorker : public Napi::AsyncWorker { // Write PNG to file sharp::AssertImageTypeDimensions(image, sharp::ImageType::PNG); image.pngsave(const_cast(baton->fileOut.data()), VImage::option() - ->set("strip", !baton->withMetadata) + ->set("keep", baton->keepMetadata) ->set("interlace", baton->pngProgressive) ->set("compression", baton->pngCompressionLevel) ->set("filter", baton->pngAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_ALL : VIPS_FOREIGN_PNG_FILTER_NONE) @@ -1096,7 +1107,7 @@ class PipelineWorker : public Napi::AsyncWorker { // Write WEBP to file sharp::AssertImageTypeDimensions(image, sharp::ImageType::WEBP); image.webpsave(const_cast(baton->fileOut.data()), VImage::option() - ->set("strip", !baton->withMetadata) + ->set("keep", baton->keepMetadata) ->set("Q", baton->webpQuality) ->set("lossless", baton->webpLossless) ->set("near_lossless", baton->webpNearLossless) @@ -1112,7 +1123,7 @@ class PipelineWorker : public Napi::AsyncWorker { // Write GIF to file sharp::AssertImageTypeDimensions(image, sharp::ImageType::GIF); image.gifsave(const_cast(baton->fileOut.data()), VImage::option() - ->set("strip", !baton->withMetadata) + ->set("keep", baton->keepMetadata) ->set("bitdepth", baton->gifBitdepth) ->set("effort", baton->gifEffort) ->set("reuse", baton->gifReuse) @@ -1131,7 +1142,7 @@ class PipelineWorker : public Napi::AsyncWorker { image = image.cast(VIPS_FORMAT_FLOAT); } image.tiffsave(const_cast(baton->fileOut.data()), VImage::option() - ->set("strip", !baton->withMetadata) + ->set("keep", baton->keepMetadata) ->set("Q", baton->tiffQuality) ->set("bitdepth", baton->tiffBitdepth) ->set("compression", baton->tiffCompression) @@ -1151,7 +1162,7 @@ class PipelineWorker : public Napi::AsyncWorker { sharp::AssertImageTypeDimensions(image, sharp::ImageType::HEIF); image = sharp::RemoveAnimationProperties(image).cast(VIPS_FORMAT_UCHAR); image.heifsave(const_cast(baton->fileOut.data()), VImage::option() - ->set("strip", !baton->withMetadata) + ->set("keep", baton->keepMetadata) ->set("Q", baton->heifQuality) ->set("compression", baton->heifCompression) ->set("effort", baton->heifEffort) @@ -1165,7 +1176,7 @@ class PipelineWorker : public Napi::AsyncWorker { // Write JXL to file image = sharp::RemoveAnimationProperties(image); image.jxlsave(const_cast(baton->fileOut.data()), VImage::option() - ->set("strip", !baton->withMetadata) + ->set("keep", baton->keepMetadata) ->set("distance", baton->jxlDistance) ->set("tier", baton->jxlDecodingTier) ->set("effort", baton->jxlEffort) @@ -1187,7 +1198,7 @@ class PipelineWorker : public Napi::AsyncWorker { (willMatchInput && inputImageType == sharp::ImageType::VIPS)) { // Write V to file image.vipssave(const_cast(baton->fileOut.data()), VImage::option() - ->set("strip", !baton->withMetadata)); + ->set("keep", baton->keepMetadata)); baton->formatOut = "v"; } else { // Unsupported output format @@ -1401,7 +1412,7 @@ class PipelineWorker : public Napi::AsyncWorker { suffix = AssembleSuffixString(extname, options); } vips::VOption *options = VImage::option() - ->set("strip", !baton->withMetadata) + ->set("keep", baton->keepMetadata) ->set("tile_size", baton->tileSize) ->set("overlap", baton->tileOverlap) ->set("container", baton->tileContainer) @@ -1593,18 +1604,19 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) { // Output baton->formatOut = sharp::AttrAsStr(options, "formatOut"); baton->fileOut = sharp::AttrAsStr(options, "fileOut"); - baton->withMetadata = sharp::AttrAsBool(options, "withMetadata"); + baton->keepMetadata = sharp::AttrAsUint32(options, "keepMetadata"); baton->withMetadataOrientation = sharp::AttrAsUint32(options, "withMetadataOrientation"); baton->withMetadataDensity = sharp::AttrAsDouble(options, "withMetadataDensity"); - baton->withMetadataIcc = sharp::AttrAsStr(options, "withMetadataIcc"); - Napi::Object mdStrs = options.Get("withMetadataStrs").As(); - Napi::Array mdStrKeys = mdStrs.GetPropertyNames(); - for (unsigned int i = 0; i < mdStrKeys.Length(); i++) { - std::string k = sharp::AttrAsStr(mdStrKeys, i); - if (mdStrs.HasOwnProperty(k)) { - baton->withMetadataStrs.insert(std::make_pair(k, sharp::AttrAsStr(mdStrs, k))); + baton->withIccProfile = sharp::AttrAsStr(options, "withIccProfile"); + Napi::Object withExif = options.Get("withExif").As(); + Napi::Array withExifKeys = withExif.GetPropertyNames(); + for (unsigned int i = 0; i < withExifKeys.Length(); i++) { + std::string k = sharp::AttrAsStr(withExifKeys, i); + if (withExif.HasOwnProperty(k)) { + baton->withExif.insert(std::make_pair(k, sharp::AttrAsStr(withExif, k))); } } + baton->withExifMerge = sharp::AttrAsBool(options, "withExifMerge"); baton->timeoutSeconds = sharp::AttrAsUint32(options, "timeoutSeconds"); // Format-specific baton->jpegQuality = sharp::AttrAsUint32(options, "jpegQuality"); diff --git a/src/pipeline.h b/src/pipeline.h index 92ff34d41..09f826c06 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -187,11 +187,12 @@ struct PipelineBaton { bool jxlLossless; VipsBandFormat rawDepth; std::string err; - bool withMetadata; + int keepMetadata; int withMetadataOrientation; double withMetadataDensity; - std::string withMetadataIcc; - std::unordered_map withMetadataStrs; + std::string withIccProfile; + std::unordered_map withExif; + bool withExifMerge; int timeoutSeconds; std::unique_ptr convKernel; int convKernelWidth; @@ -353,9 +354,10 @@ struct PipelineBaton { jxlEffort(7), jxlLossless(false), rawDepth(VIPS_FORMAT_UCHAR), - withMetadata(false), + keepMetadata(0), withMetadataOrientation(-1), withMetadataDensity(0.0), + withExifMerge(true), timeoutSeconds(0), convKernelWidth(0), convKernelHeight(0), diff --git a/test/types/sharp.test-d.ts b/test/types/sharp.test-d.ts index 2d6804c25..4be5eeb60 100644 --- a/test/types/sharp.test-d.ts +++ b/test/types/sharp.test-d.ts @@ -659,6 +659,18 @@ sharp('input.tiff').webp({ preset: 'drawing' }).toFile('out.webp'); sharp('input.tiff').webp({ preset: 'text' }).toFile('out.webp'); sharp('input.tiff').webp({ preset: 'default' }).toFile('out.webp'); -// Allow a boolean or an object for metadata options. -// https://github.com/lovell/sharp/issues/3822 -sharp(input).withMetadata().withMetadata({}).withMetadata(false); +sharp(input) + .keepExif() + .withExif({ + IFD0: { + k1: 'v1' + } + }) + .withExifMerge({ + IFD1: { + k2: 'v2' + } + }) + .keepIccProfile() + .withIccProfile('filename') + .withIccProfile('filename', { attach: false }); diff --git a/test/unit/metadata.js b/test/unit/metadata.js index 39f37ae5e..84fe50e5a 100644 --- a/test/unit/metadata.js +++ b/test/unit/metadata.js @@ -11,6 +11,8 @@ const icc = require('icc'); const sharp = require('../../'); const fixtures = require('../fixtures'); +const create = { width: 1, height: 1, channels: 3, background: 'red' }; + describe('Image metadata', function () { it('JPEG', function (done) { sharp(fixtures.inputJpg).metadata(function (err, metadata) { @@ -552,11 +554,55 @@ describe('Image metadata', function () { }); }); + it('keep existing ICC profile', async () => { + const data = await sharp(fixtures.inputJpgWithExif) + .keepIccProfile() + .toBuffer(); + + const metadata = await sharp(data).metadata(); + const { description } = icc.parse(metadata.icc); + assert.strictEqual(description, 'Generic RGB Profile'); + }); + + it('keep existing ICC profile, ignore colourspace conversion', async () => { + const data = await sharp(fixtures.inputJpgWithExif) + .keepIccProfile() + .toColourspace('cmyk') + .toBuffer(); + + const metadata = await sharp(data).metadata(); + assert.strictEqual(metadata.channels, 3); + const { description } = icc.parse(metadata.icc); + assert.strictEqual(description, 'Generic RGB Profile'); + }); + + it('transform to ICC profile and attach', async () => { + const data = await sharp({ create }) + .png() + .withIccProfile('p3', { attach: true }) + .toBuffer(); + + const metadata = await sharp(data).metadata(); + const { description } = icc.parse(metadata.icc); + assert.strictEqual(description, 'sP3C'); + }); + + it('transform to ICC profile but do not attach', async () => { + const data = await sharp({ create }) + .png() + .withIccProfile('p3', { attach: false }) + .toBuffer(); + + const metadata = await sharp(data).metadata(); + assert.strictEqual(3, metadata.channels); + assert.strictEqual(undefined, metadata.icc); + }); + it('Apply CMYK output ICC profile', function (done) { const output = fixtures.path('output.icc-cmyk.jpg'); sharp(fixtures.inputJpg) .resize(64) - .withMetadata({ icc: 'cmyk' }) + .withIccProfile('cmyk') .toFile(output, function (err) { if (err) throw err; sharp(output).metadata(function (err, metadata) { @@ -581,7 +627,7 @@ describe('Image metadata', function () { const output = fixtures.path('output.hilutite.jpg'); sharp(fixtures.inputJpg) .resize(64) - .withMetadata({ icc: fixtures.path('hilutite.icm') }) + .withIccProfile(fixtures.path('hilutite.icm')) .toFile(output, function (err, info) { if (err) throw err; fixtures.assertMaxColourDistance(output, fixtures.expected('hilutite.jpg'), 9); @@ -620,7 +666,6 @@ describe('Image metadata', function () { it('Remove EXIF metadata after a resize', function (done) { sharp(fixtures.inputJpgWithExif) .resize(320, 240) - .withMetadata(false) .toBuffer(function (err, buffer) { if (err) throw err; sharp(buffer).metadata(function (err, metadata) { @@ -651,14 +696,7 @@ describe('Image metadata', function () { }); it('Add EXIF metadata to JPEG', async () => { - const data = await sharp({ - create: { - width: 8, - height: 8, - channels: 3, - background: 'red' - } - }) + const data = await sharp({ create }) .jpeg() .withMetadata({ exif: { @@ -675,14 +713,7 @@ describe('Image metadata', function () { }); it('Set density of JPEG', async () => { - const data = await sharp({ - create: { - width: 8, - height: 8, - channels: 3, - background: 'red' - } - }) + const data = await sharp({ create }) .withMetadata({ density: 300 }) @@ -694,14 +725,7 @@ describe('Image metadata', function () { }); it('Set density of PNG', async () => { - const data = await sharp({ - create: { - width: 8, - height: 8, - channels: 3, - background: 'red' - } - }) + const data = await sharp({ create }) .withMetadata({ density: 96 }) @@ -809,11 +833,7 @@ describe('Image metadata', function () { }); it('withMetadata adds default sRGB profile to RGB16', async () => { - const data = await sharp({ - create: { - width: 8, height: 8, channels: 4, background: 'orange' - } - }) + const data = await sharp({ create }) .toColorspace('rgb16') .png() .withMetadata() @@ -827,11 +847,7 @@ describe('Image metadata', function () { }); it('withMetadata adds P3 profile to 16-bit PNG', async () => { - const data = await sharp({ - create: { - width: 8, height: 8, channels: 4, background: 'orange' - } - }) + const data = await sharp({ create }) .toColorspace('rgb16') .png() .withMetadata({ icc: 'p3' }) @@ -871,7 +887,89 @@ describe('Image metadata', function () { }); }); - describe('Invalid withMetadata parameters', function () { + it('keepExif maintains all EXIF metadata', async () => { + const data1 = await sharp({ create }) + .withExif({ + IFD0: { + Copyright: 'Test 1', + Software: 'sharp' + } + }) + .jpeg() + .toBuffer(); + + const data2 = await sharp(data1) + .keepExif() + .toBuffer(); + + const md2 = await sharp(data2).metadata(); + const exif2 = exifReader(md2.exif); + assert.strictEqual(exif2.Image.Copyright, 'Test 1'); + assert.strictEqual(exif2.Image.Software, 'sharp'); + }); + + it('withExif replaces all EXIF metadata', async () => { + const data1 = await sharp({ create }) + .withExif({ + IFD0: { + Copyright: 'Test 1', + Software: 'sharp' + } + }) + .jpeg() + .toBuffer(); + + const md1 = await sharp(data1).metadata(); + const exif1 = exifReader(md1.exif); + assert.strictEqual(exif1.Image.Copyright, 'Test 1'); + assert.strictEqual(exif1.Image.Software, 'sharp'); + + const data2 = await sharp(data1) + .withExif({ + IFD0: { + Copyright: 'Test 2' + } + }) + .toBuffer(); + + const md2 = await sharp(data2).metadata(); + const exif2 = exifReader(md2.exif); + assert.strictEqual(exif2.Image.Copyright, 'Test 2'); + assert.strictEqual(exif2.Image.Software, undefined); + }); + + it('withExifMerge merges all EXIF metadata', async () => { + const data1 = await sharp({ create }) + .withExif({ + IFD0: { + Copyright: 'Test 1' + } + }) + .jpeg() + .toBuffer(); + + const md1 = await sharp(data1).metadata(); + const exif1 = exifReader(md1.exif); + assert.strictEqual(exif1.Image.Copyright, 'Test 1'); + assert.strictEqual(exif1.Image.Software, undefined); + + const data2 = await sharp(data1) + .withExifMerge({ + IFD0: { + Copyright: 'Test 2', + Software: 'sharp' + + } + }) + .toBuffer(); + + const md2 = await sharp(data2).metadata(); + const exif2 = exifReader(md2.exif); + assert.strictEqual(exif2.Image.Copyright, 'Test 2'); + assert.strictEqual(exif2.Image.Software, 'sharp'); + }); + + describe('Invalid parameters', function () { it('String orientation', function () { assert.throws(function () { sharp().withMetadata({ orientation: 'zoinks' }); @@ -922,5 +1020,22 @@ describe('Image metadata', function () { sharp().withMetadata({ exif: { ifd0: { fail: false } } }); }); }); + it('withIccProfile invalid profile', () => { + assert.throws( + () => sharp().withIccProfile(false), + /Expected string for icc but received false of type boolean/ + ); + }); + it('withIccProfile missing attach', () => { + assert.doesNotThrow( + () => sharp().withIccProfile('test', {}) + ); + }); + it('withIccProfile invalid attach', () => { + assert.throws( + () => sharp().withIccProfile('test', { attach: 1 }), + /Expected boolean for attach but received 1 of type number/ + ); + }); }); });