Increase control over output metadata (#3856)

Add withX and keepX functions to take advantage of
libvips 8.15.0 new 'keep' metadata feature.
This commit is contained in:
Lovell Fuller 2023-11-22 09:03:57 +00:00 committed by GitHub
parent 3f7313d031
commit e78200cc84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 694 additions and 179 deletions

View File

@ -12,7 +12,7 @@ An alpha channel may be present and will be unchanged by the operation.
| Param | Type | Description | | Param | Type | Description |
| --- | --- | --- | | --- | --- | --- |
| tint | <code>String</code> \| <code>Object</code> | Parsed by the [color](https://www.npmjs.org/package/color) module. | | tint | <code>string</code> \| <code>Object</code> | Parsed by the [color](https://www.npmjs.org/package/color) module. |
**Example** **Example**
```js ```js

View File

@ -111,17 +111,157 @@ await sharp(pixelArray, { raw: { width, height, channels } })
``` ```
## keepExif
> keepExif() ⇒ <code>Sharp</code>
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) ⇒ <code>Sharp</code>
Set EXIF metadata in the output image, ignoring any EXIF in the input image.
**Throws**:
- <code>Error</code> Invalid parameters
**Since**: 0.33.0
| Param | Type | Description |
| --- | --- | --- |
| exif | <code>Object.&lt;string, Object.&lt;string, string&gt;&gt;</code> | 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) ⇒ <code>Sharp</code>
Update EXIF metadata from the input image in the output image.
**Throws**:
- <code>Error</code> Invalid parameters
**Since**: 0.33.0
| Param | Type | Description |
| --- | --- | --- |
| exif | <code>Object.&lt;string, Object.&lt;string, string&gt;&gt;</code> | 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() ⇒ <code>Sharp</code>
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]) ⇒ <code>Sharp</code>
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**:
- <code>Error</code> Invalid parameters
**Since**: 0.33.0
| Param | Type | Default | Description |
| --- | --- | --- | --- |
| icc | <code>string</code> | | Absolute filesystem path to output ICC profile or built-in profile name (srgb, p3, cmyk). |
| [options] | <code>Object</code> | | |
| [options.attach] | <code>number</code> | <code>true</code> | Should the ICC profile be included in the output image metadata? |
**Example**
```js
const outputWithP3 = await sharp(input)
.withIccProfile('p3')
.toBuffer();
```
## keepMetadata
> keepMetadata() ⇒ <code>Sharp</code>
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
const outputWithMetadata = await sharp(inputWithMetadata)
.keepMetadata()
.toBuffer();
```
## withMetadata ## withMetadata
> withMetadata([options]) ⇒ <code>Sharp</code> > withMetadata([options]) ⇒ <code>Sharp</code>
Include all metadata (EXIF, XMP, IPTC) from the input image in the output image. 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,
unless a custom output profile is provided.
The default behaviour, when `withMetadata` is not used, is to convert to the device-independent This will also convert to and add a web-friendly sRGB ICC profile if appropriate.
sRGB colour space and strip all metadata, including the removal of any ICC profile.
EXIF metadata is unsupported for TIFF output. Allows orientation and density to be set or updated.
**Throws**: **Throws**:
@ -129,38 +269,16 @@ EXIF metadata is unsupported for TIFF output.
- <code>Error</code> Invalid parameters - <code>Error</code> Invalid parameters
| Param | Type | Default | Description | | Param | Type | Description |
| --- | --- | --- | --- | | --- | --- | --- |
| [options] | <code>Object</code> | | | | [options] | <code>Object</code> | |
| [options.orientation] | <code>number</code> | | value between 1 and 8, used to update the EXIF `Orientation` tag. | | [options.orientation] | <code>number</code> | Used to update the EXIF `Orientation` tag, integer between 1 and 8. |
| [options.icc] | <code>string</code> | <code>&quot;&#x27;srgb&#x27;&quot;</code> | Filesystem path to output ICC profile, relative to `process.cwd()`, defaults to built-in sRGB. | | [options.density] | <code>number</code> | Number of pixels per inch (DPI). |
| [options.exif] | <code>Object.&lt;Object&gt;</code> | <code>{}</code> | Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data. |
| [options.density] | <code>number</code> | | Number of pixels per inch (DPI). |
**Example** **Example**
```js ```js
sharp('input.jpg') const outputSrgbWithMetadata = await sharp(inputRgbWithMetadata)
.withMetadata() .withMetadata()
.toFile('output-with-metadata.jpg')
.then(info => { ... });
```
**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'
}
}
})
.toBuffer(); .toBuffer();
``` ```
**Example** **Example**

View File

@ -14,6 +14,8 @@ Requires libvips v8.15.0
* Remove `sharp.vendor`. * Remove `sharp.vendor`.
* Partially deprecate `withMetadata()`, use `withExif()` and `withIccProfile()`.
* Add experimental support for WebAssembly-based runtimes. * Add experimental support for WebAssembly-based runtimes.
[@RReverser](https://github.com/RReverser) [@RReverser](https://github.com/RReverser)
@ -41,6 +43,9 @@ Requires libvips v8.15.0
[#3823](https://github.com/lovell/sharp/pull/3823) [#3823](https://github.com/lovell/sharp/pull/3823)
[@uhthomas](https://github.com/uhthomas) [@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. * Ensure multi-page extract remains sequential.
[#3837](https://github.com/lovell/sharp/issues/3837) [#3837](https://github.com/lovell/sharp/issues/3837)

File diff suppressed because one or more lines are too long

View File

@ -257,11 +257,12 @@ const Sharp = function (input, options) {
fileOut: '', fileOut: '',
formatOut: 'input', formatOut: 'input',
streamOut: false, streamOut: false,
withMetadata: false, keepMetadata: 0,
withMetadataOrientation: -1, withMetadataOrientation: -1,
withMetadataDensity: 0, withMetadataDensity: 0,
withMetadataIcc: '', withIccProfile: '',
withMetadataStrs: {}, withExif: {},
withExifMerge: true,
resolveWithObject: false, resolveWithObject: false,
// output format // output format
jpegQuality: 80, jpegQuality: 80,

73
lib/index.d.ts vendored
View File

@ -633,6 +633,43 @@ declare namespace sharp {
*/ */
toBuffer(options: { resolveWithObject: true }): Promise<{ data: Buffer; info: OutputInfo }>; 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. * 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. * 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 * @param withMetadata
* @throws {Error} Invalid parameters. * @throws {Error} Invalid parameters.
*/ */
withMetadata(withMetadata?: boolean | WriteableMetadata): Sharp; withMetadata(withMetadata?: WriteableMetadata): Sharp;
/** /**
* Use these JPEG options for output image. * Use these JPEG options for output image.
@ -978,15 +1015,32 @@ declare namespace sharp {
wrap?: TextWrap; wrap?: TextWrap;
} }
interface ExifDir {
[k: string]: string;
}
interface Exif {
'IFD0'?: ExifDir;
'IFD1'?: ExifDir;
'IFD2'?: ExifDir;
'IFD3'?: ExifDir;
}
interface WriteableMetadata { interface WriteableMetadata {
/** Value between 1 and 8, used to update the EXIF Orientation tag. */
orientation?: number | undefined;
/** Filesystem path to output ICC profile, defaults to sRGB. */
icc?: string | undefined;
/** Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data. (optional, default {}) */
exif?: Record<string, any> | undefined;
/** Number of pixels per inch (DPI) */ /** Number of pixels per inch (DPI) */
density?: number | undefined; 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.
* @deprecated Use `withIccProfile()` instead.
*/
icc?: string | 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 { interface Metadata {
@ -1096,6 +1150,11 @@ declare namespace sharp {
force?: boolean | undefined; 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 { interface JpegOptions extends OutputOptions {
/** Quality, integer 1-100 (optional, default 80) */ /** Quality, integer 1-100 (optional, default 80) */
quality?: number | undefined; quality?: number | undefined;

View File

@ -163,39 +163,185 @@ function toBuffer (options, callback) {
} }
/** /**
* Include all metadata (EXIF, XMP, IPTC) from the input image in the output image. * Keep all EXIF metadata 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.
* *
* EXIF metadata is unsupported for TIFF output. * EXIF metadata is unsupported for TIFF output.
* *
* @example * @since 0.33.0
* sharp('input.jpg')
* .withMetadata()
* .toFile('output-with-metadata.jpg')
* .then(info => { ... });
* *
* @example * @example
* // Set output EXIF metadata * const outputWithExif = await sharp(inputWithExif)
* const data = await sharp(input) * .keepExif()
* .withMetadata({ * .toBuffer();
* exif: { *
* IFD0: { * @returns {Sharp}
* Copyright: 'The National Gallery' */
* }, function keepExif () {
* IFD3: { this.options.keepMetadata |= 0b00001;
* GPSLatitudeRef: 'N', return this;
* GPSLatitude: '51/1 30/1 3230/100', }
* GPSLongitudeRef: 'W',
* GPSLongitude: '0/1 7/1 4366/100' /**
* } * Set EXIF metadata in the output image, ignoring any EXIF in the input image.
*
* @since 0.33.0
*
* @example
* 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(); * .toBuffer();
* *
* @param {Object<string, Object<string, string>>} 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<string, Object<string, string>>} 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 * @example
* // Set output metadata to 96 DPI * // Set output metadata to 96 DPI
* const data = await sharp(input) * const data = await sharp(input)
@ -203,15 +349,14 @@ function toBuffer (options, callback) {
* .toBuffer(); * .toBuffer();
* *
* @param {Object} [options] * @param {Object} [options]
* @param {number} [options.orientation] value between 1 and 8, used to update the EXIF `Orientation` tag. * @param {number} [options.orientation] Used to update the EXIF `Orientation` tag, integer between 1 and 8.
* @param {string} [options.icc='srgb'] Filesystem path to output ICC profile, relative to `process.cwd()`, defaults to built-in sRGB.
* @param {Object<Object>} [options.exif={}] Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data.
* @param {number} [options.density] Number of pixels per inch (DPI). * @param {number} [options.density] Number of pixels per inch (DPI).
* @returns {Sharp} * @returns {Sharp}
* @throws {Error} Invalid parameters * @throws {Error} Invalid parameters
*/ */
function withMetadata (options) { function withMetadata (options) {
this.options.withMetadata = is.bool(options) ? options : true; this.keepMetadata();
this.withIccProfile('srgb');
if (is.object(options)) { if (is.object(options)) {
if (is.defined(options.orientation)) { if (is.defined(options.orientation)) {
if (is.integer(options.orientation) && is.inRange(options.orientation, 1, 8)) { 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.defined(options.icc)) {
if (is.string(options.icc)) { this.withIccProfile(options.icc);
this.options.withMetadataIcc = options.icc;
} else {
throw is.invalidParameterError('icc', 'string filesystem path to ICC profile', options.icc);
}
} }
if (is.defined(options.exif)) { if (is.defined(options.exif)) {
if (is.object(options.exif)) { this.withExifMerge(options.exif);
for (const [ifd, entries] of Object.entries(options.exif)) {
if (is.object(entries)) {
for (const [k, v] of Object.entries(entries)) {
if (is.string(v)) {
this.options.withMetadataStrs[`exif-${ifd.toLowerCase()}-${k}`] = v;
} else {
throw is.invalidParameterError(`exif.${ifd}.${k}`, 'string', v);
}
}
} else {
throw is.invalidParameterError(`exif.${ifd}`, 'object', entries);
}
}
} else {
throw is.invalidParameterError('exif', 'object', options.exif);
}
} }
} }
return this; return this;
@ -1407,6 +1532,12 @@ module.exports = function (Sharp) {
// Public // Public
toFile, toFile,
toBuffer, toBuffer,
keepExif,
withExif,
withExifMerge,
keepIccProfile,
withIccProfile,
keepMetadata,
withMetadata, withMetadata,
toFormat, toFormat,
jpeg, jpeg,

View File

@ -531,7 +531,33 @@ namespace sharp {
Does this image have an embedded profile? Does this image have an embedded profile?
*/ */
bool HasProfile(VImage image) { 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<char*, size_t> GetProfile(VImage image) {
std::pair<char*, size_t> icc(nullptr, 0);
if (HasProfile(image)) {
size_t length;
const void *data = image.get_blob(VIPS_META_ICC_NAME, &length);
icc.first = static_cast<char*>(g_malloc(length));
icc.second = length;
memcpy(icc.first, data, length);
}
return icc;
}
/*
Set embedded profile.
*/
VImage SetProfile(VImage image, std::pair<char*, size_t> icc) {
if (icc.first != nullptr) {
image = image.copy();
image.set(VIPS_META_ICC_NAME, reinterpret_cast<VipsCallbackFn>(vips_area_free_cb), icc.first, icc.second);
}
return image;
} }
/* /*
@ -542,6 +568,27 @@ namespace sharp {
return image.has_alpha(); return image.has_alpha();
} }
static void* RemoveExifCallback(VipsImage *image, char const *field, GValue *value, void *data) {
std::vector<std::string> *fieldNames = static_cast<std::vector<std::string> *>(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<std::string> fieldNames;
vips_image_map(image.get_image(), static_cast<VipsImageMapFn>(RemoveExifCallback), &fieldNames);
for (const auto& f : fieldNames) {
image.remove(f.data());
}
return image;
}
/* /*
Get EXIF Orientation of image, if any. Get EXIF Orientation of image, if any.
*/ */

View File

@ -222,12 +222,27 @@ namespace sharp {
*/ */
bool HasProfile(VImage image); bool HasProfile(VImage image);
/*
Get copy of embedded profile.
*/
std::pair<char*, size_t> GetProfile(VImage image);
/*
Set embedded profile.
*/
VImage SetProfile(VImage image, std::pair<char*, size_t> icc);
/* /*
Does this image have an alpha channel? Does this image have an alpha channel?
Uses colour space interpretation with number of channels to guess this. Uses colour space interpretation with number of channels to guess this.
*/ */
bool HasAlpha(VImage image); bool HasAlpha(VImage image);
/*
Remove all EXIF-related image fields.
*/
VImage RemoveExif(VImage image);
/* /*
Get EXIF Orientation of image, if any. Get EXIF Orientation of image, if any.
*/ */

View File

@ -315,6 +315,11 @@ class PipelineWorker : public Napi::AsyncWorker {
} }
// Ensure we're using a device-independent colour space // Ensure we're using a device-independent colour space
std::pair<char*, size_t> 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"; char const *processingProfile = image.interpretation() == VIPS_INTERPRETATION_RGB16 ? "p3" : "srgb";
if ( if (
sharp::HasProfile(image) && 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 // 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())); image = image.colourspace(baton->colourspace, VImage::option()->set("source_space", image.interpretation()));
// Transform colours from embedded profile to output profile // 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() image = image.icc_transform("srgb", VImage::option()
->set("embedded", TRUE) ->set("embedded", TRUE)
->set("depth", sharp::Is16Bit(image.interpretation()) ? 16 : 8) ->set("depth", sharp::Is16Bit(image.interpretation()) ? 16 : 8)
@ -787,27 +793,30 @@ class PipelineWorker : public Napi::AsyncWorker {
} }
// Apply output ICC profile // Apply output ICC profile
if (baton->withMetadata) { if (!baton->withIccProfile.empty()) {
image = image.icc_transform( image = image.icc_transform(const_cast<char*>(baton->withIccProfile.data()), VImage::option()
baton->withMetadataIcc.empty() ? "srgb" : const_cast<char*>(baton->withMetadataIcc.data()), ->set("input_profile", processingProfile)
VImage::option() ->set("embedded", TRUE)
->set("input_profile", processingProfile) ->set("depth", sharp::Is16Bit(image.interpretation()) ? 16 : 8)
->set("embedded", TRUE) ->set("intent", VIPS_INTENT_PERCEPTUAL));
->set("depth", sharp::Is16Bit(image.interpretation()) ? 16 : 8) } else if (baton->keepMetadata & VIPS_FOREIGN_KEEP_ICC) {
->set("intent", VIPS_INTENT_PERCEPTUAL)); image = sharp::SetProfile(image, inputProfile);
} }
// Override EXIF Orientation tag // Override EXIF Orientation tag
if (baton->withMetadata && baton->withMetadataOrientation != -1) { if (baton->withMetadataOrientation != -1) {
image = sharp::SetExifOrientation(image, baton->withMetadataOrientation); image = sharp::SetExifOrientation(image, baton->withMetadataOrientation);
} }
// Override pixel density // Override pixel density
if (baton->withMetadataDensity > 0) { if (baton->withMetadataDensity > 0) {
image = sharp::SetDensity(image, baton->withMetadataDensity); image = sharp::SetDensity(image, baton->withMetadataDensity);
} }
// Metadata key/value pairs, e.g. EXIF // EXIF key/value pairs
if (!baton->withMetadataStrs.empty()) { if (baton->keepMetadata & VIPS_FOREIGN_KEEP_EXIF) {
image = image.copy(); 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()); image.set(s.first.data(), s.second.data());
} }
} }
@ -828,7 +837,7 @@ class PipelineWorker : public Napi::AsyncWorker {
// Write JPEG to buffer // Write JPEG to buffer
sharp::AssertImageTypeDimensions(image, sharp::ImageType::JPEG); sharp::AssertImageTypeDimensions(image, sharp::ImageType::JPEG);
VipsArea *area = reinterpret_cast<VipsArea*>(image.jpegsave_buffer(VImage::option() VipsArea *area = reinterpret_cast<VipsArea*>(image.jpegsave_buffer(VImage::option()
->set("strip", !baton->withMetadata) ->set("keep", baton->keepMetadata)
->set("Q", baton->jpegQuality) ->set("Q", baton->jpegQuality)
->set("interlace", baton->jpegProgressive) ->set("interlace", baton->jpegProgressive)
->set("subsample_mode", baton->jpegChromaSubsampling == "4:4:4" ->set("subsample_mode", baton->jpegChromaSubsampling == "4:4:4"
@ -870,7 +879,7 @@ class PipelineWorker : public Napi::AsyncWorker {
// Write PNG to buffer // Write PNG to buffer
sharp::AssertImageTypeDimensions(image, sharp::ImageType::PNG); sharp::AssertImageTypeDimensions(image, sharp::ImageType::PNG);
VipsArea *area = reinterpret_cast<VipsArea*>(image.pngsave_buffer(VImage::option() VipsArea *area = reinterpret_cast<VipsArea*>(image.pngsave_buffer(VImage::option()
->set("strip", !baton->withMetadata) ->set("keep", baton->keepMetadata)
->set("interlace", baton->pngProgressive) ->set("interlace", baton->pngProgressive)
->set("compression", baton->pngCompressionLevel) ->set("compression", baton->pngCompressionLevel)
->set("filter", baton->pngAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_ALL : VIPS_FOREIGN_PNG_FILTER_NONE) ->set("filter", baton->pngAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_ALL : VIPS_FOREIGN_PNG_FILTER_NONE)
@ -889,7 +898,7 @@ class PipelineWorker : public Napi::AsyncWorker {
// Write WEBP to buffer // Write WEBP to buffer
sharp::AssertImageTypeDimensions(image, sharp::ImageType::WEBP); sharp::AssertImageTypeDimensions(image, sharp::ImageType::WEBP);
VipsArea *area = reinterpret_cast<VipsArea*>(image.webpsave_buffer(VImage::option() VipsArea *area = reinterpret_cast<VipsArea*>(image.webpsave_buffer(VImage::option()
->set("strip", !baton->withMetadata) ->set("keep", baton->keepMetadata)
->set("Q", baton->webpQuality) ->set("Q", baton->webpQuality)
->set("lossless", baton->webpLossless) ->set("lossless", baton->webpLossless)
->set("near_lossless", baton->webpNearLossless) ->set("near_lossless", baton->webpNearLossless)
@ -909,7 +918,7 @@ class PipelineWorker : public Napi::AsyncWorker {
// Write GIF to buffer // Write GIF to buffer
sharp::AssertImageTypeDimensions(image, sharp::ImageType::GIF); sharp::AssertImageTypeDimensions(image, sharp::ImageType::GIF);
VipsArea *area = reinterpret_cast<VipsArea*>(image.gifsave_buffer(VImage::option() VipsArea *area = reinterpret_cast<VipsArea*>(image.gifsave_buffer(VImage::option()
->set("strip", !baton->withMetadata) ->set("keep", baton->keepMetadata)
->set("bitdepth", baton->gifBitdepth) ->set("bitdepth", baton->gifBitdepth)
->set("effort", baton->gifEffort) ->set("effort", baton->gifEffort)
->set("reuse", baton->gifReuse) ->set("reuse", baton->gifReuse)
@ -934,7 +943,7 @@ class PipelineWorker : public Napi::AsyncWorker {
image = image.cast(VIPS_FORMAT_FLOAT); image = image.cast(VIPS_FORMAT_FLOAT);
} }
VipsArea *area = reinterpret_cast<VipsArea*>(image.tiffsave_buffer(VImage::option() VipsArea *area = reinterpret_cast<VipsArea*>(image.tiffsave_buffer(VImage::option()
->set("strip", !baton->withMetadata) ->set("keep", baton->keepMetadata)
->set("Q", baton->tiffQuality) ->set("Q", baton->tiffQuality)
->set("bitdepth", baton->tiffBitdepth) ->set("bitdepth", baton->tiffBitdepth)
->set("compression", baton->tiffCompression) ->set("compression", baton->tiffCompression)
@ -958,7 +967,7 @@ class PipelineWorker : public Napi::AsyncWorker {
sharp::AssertImageTypeDimensions(image, sharp::ImageType::HEIF); sharp::AssertImageTypeDimensions(image, sharp::ImageType::HEIF);
image = sharp::RemoveAnimationProperties(image).cast(VIPS_FORMAT_UCHAR); image = sharp::RemoveAnimationProperties(image).cast(VIPS_FORMAT_UCHAR);
VipsArea *area = reinterpret_cast<VipsArea*>(image.heifsave_buffer(VImage::option() VipsArea *area = reinterpret_cast<VipsArea*>(image.heifsave_buffer(VImage::option()
->set("strip", !baton->withMetadata) ->set("keep", baton->keepMetadata)
->set("Q", baton->heifQuality) ->set("Q", baton->heifQuality)
->set("compression", baton->heifCompression) ->set("compression", baton->heifCompression)
->set("effort", baton->heifEffort) ->set("effort", baton->heifEffort)
@ -990,7 +999,7 @@ class PipelineWorker : public Napi::AsyncWorker {
// Write JXL to buffer // Write JXL to buffer
image = sharp::RemoveAnimationProperties(image); image = sharp::RemoveAnimationProperties(image);
VipsArea *area = reinterpret_cast<VipsArea*>(image.jxlsave_buffer(VImage::option() VipsArea *area = reinterpret_cast<VipsArea*>(image.jxlsave_buffer(VImage::option()
->set("strip", !baton->withMetadata) ->set("keep", baton->keepMetadata)
->set("distance", baton->jxlDistance) ->set("distance", baton->jxlDistance)
->set("tier", baton->jxlDecodingTier) ->set("tier", baton->jxlDecodingTier)
->set("effort", baton->jxlEffort) ->set("effort", baton->jxlEffort)
@ -1051,7 +1060,7 @@ class PipelineWorker : public Napi::AsyncWorker {
// Write JPEG to file // Write JPEG to file
sharp::AssertImageTypeDimensions(image, sharp::ImageType::JPEG); sharp::AssertImageTypeDimensions(image, sharp::ImageType::JPEG);
image.jpegsave(const_cast<char*>(baton->fileOut.data()), VImage::option() image.jpegsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
->set("strip", !baton->withMetadata) ->set("keep", baton->keepMetadata)
->set("Q", baton->jpegQuality) ->set("Q", baton->jpegQuality)
->set("interlace", baton->jpegProgressive) ->set("interlace", baton->jpegProgressive)
->set("subsample_mode", baton->jpegChromaSubsampling == "4:4:4" ->set("subsample_mode", baton->jpegChromaSubsampling == "4:4:4"
@ -1081,7 +1090,7 @@ class PipelineWorker : public Napi::AsyncWorker {
// Write PNG to file // Write PNG to file
sharp::AssertImageTypeDimensions(image, sharp::ImageType::PNG); sharp::AssertImageTypeDimensions(image, sharp::ImageType::PNG);
image.pngsave(const_cast<char*>(baton->fileOut.data()), VImage::option() image.pngsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
->set("strip", !baton->withMetadata) ->set("keep", baton->keepMetadata)
->set("interlace", baton->pngProgressive) ->set("interlace", baton->pngProgressive)
->set("compression", baton->pngCompressionLevel) ->set("compression", baton->pngCompressionLevel)
->set("filter", baton->pngAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_ALL : VIPS_FOREIGN_PNG_FILTER_NONE) ->set("filter", baton->pngAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_ALL : VIPS_FOREIGN_PNG_FILTER_NONE)
@ -1096,7 +1105,7 @@ class PipelineWorker : public Napi::AsyncWorker {
// Write WEBP to file // Write WEBP to file
sharp::AssertImageTypeDimensions(image, sharp::ImageType::WEBP); sharp::AssertImageTypeDimensions(image, sharp::ImageType::WEBP);
image.webpsave(const_cast<char*>(baton->fileOut.data()), VImage::option() image.webpsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
->set("strip", !baton->withMetadata) ->set("keep", baton->keepMetadata)
->set("Q", baton->webpQuality) ->set("Q", baton->webpQuality)
->set("lossless", baton->webpLossless) ->set("lossless", baton->webpLossless)
->set("near_lossless", baton->webpNearLossless) ->set("near_lossless", baton->webpNearLossless)
@ -1112,7 +1121,7 @@ class PipelineWorker : public Napi::AsyncWorker {
// Write GIF to file // Write GIF to file
sharp::AssertImageTypeDimensions(image, sharp::ImageType::GIF); sharp::AssertImageTypeDimensions(image, sharp::ImageType::GIF);
image.gifsave(const_cast<char*>(baton->fileOut.data()), VImage::option() image.gifsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
->set("strip", !baton->withMetadata) ->set("keep", baton->keepMetadata)
->set("bitdepth", baton->gifBitdepth) ->set("bitdepth", baton->gifBitdepth)
->set("effort", baton->gifEffort) ->set("effort", baton->gifEffort)
->set("reuse", baton->gifReuse) ->set("reuse", baton->gifReuse)
@ -1131,7 +1140,7 @@ class PipelineWorker : public Napi::AsyncWorker {
image = image.cast(VIPS_FORMAT_FLOAT); image = image.cast(VIPS_FORMAT_FLOAT);
} }
image.tiffsave(const_cast<char*>(baton->fileOut.data()), VImage::option() image.tiffsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
->set("strip", !baton->withMetadata) ->set("keep", baton->keepMetadata)
->set("Q", baton->tiffQuality) ->set("Q", baton->tiffQuality)
->set("bitdepth", baton->tiffBitdepth) ->set("bitdepth", baton->tiffBitdepth)
->set("compression", baton->tiffCompression) ->set("compression", baton->tiffCompression)
@ -1151,7 +1160,7 @@ class PipelineWorker : public Napi::AsyncWorker {
sharp::AssertImageTypeDimensions(image, sharp::ImageType::HEIF); sharp::AssertImageTypeDimensions(image, sharp::ImageType::HEIF);
image = sharp::RemoveAnimationProperties(image).cast(VIPS_FORMAT_UCHAR); image = sharp::RemoveAnimationProperties(image).cast(VIPS_FORMAT_UCHAR);
image.heifsave(const_cast<char*>(baton->fileOut.data()), VImage::option() image.heifsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
->set("strip", !baton->withMetadata) ->set("keep", baton->keepMetadata)
->set("Q", baton->heifQuality) ->set("Q", baton->heifQuality)
->set("compression", baton->heifCompression) ->set("compression", baton->heifCompression)
->set("effort", baton->heifEffort) ->set("effort", baton->heifEffort)
@ -1165,7 +1174,7 @@ class PipelineWorker : public Napi::AsyncWorker {
// Write JXL to file // Write JXL to file
image = sharp::RemoveAnimationProperties(image); image = sharp::RemoveAnimationProperties(image);
image.jxlsave(const_cast<char*>(baton->fileOut.data()), VImage::option() image.jxlsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
->set("strip", !baton->withMetadata) ->set("keep", baton->keepMetadata)
->set("distance", baton->jxlDistance) ->set("distance", baton->jxlDistance)
->set("tier", baton->jxlDecodingTier) ->set("tier", baton->jxlDecodingTier)
->set("effort", baton->jxlEffort) ->set("effort", baton->jxlEffort)
@ -1187,7 +1196,7 @@ class PipelineWorker : public Napi::AsyncWorker {
(willMatchInput && inputImageType == sharp::ImageType::VIPS)) { (willMatchInput && inputImageType == sharp::ImageType::VIPS)) {
// Write V to file // Write V to file
image.vipssave(const_cast<char*>(baton->fileOut.data()), VImage::option() image.vipssave(const_cast<char*>(baton->fileOut.data()), VImage::option()
->set("strip", !baton->withMetadata)); ->set("keep", baton->keepMetadata));
baton->formatOut = "v"; baton->formatOut = "v";
} else { } else {
// Unsupported output format // Unsupported output format
@ -1401,7 +1410,7 @@ class PipelineWorker : public Napi::AsyncWorker {
suffix = AssembleSuffixString(extname, options); suffix = AssembleSuffixString(extname, options);
} }
vips::VOption *options = VImage::option() vips::VOption *options = VImage::option()
->set("strip", !baton->withMetadata) ->set("keep", baton->keepMetadata)
->set("tile_size", baton->tileSize) ->set("tile_size", baton->tileSize)
->set("overlap", baton->tileOverlap) ->set("overlap", baton->tileOverlap)
->set("container", baton->tileContainer) ->set("container", baton->tileContainer)
@ -1593,18 +1602,19 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
// Output // Output
baton->formatOut = sharp::AttrAsStr(options, "formatOut"); baton->formatOut = sharp::AttrAsStr(options, "formatOut");
baton->fileOut = sharp::AttrAsStr(options, "fileOut"); 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->withMetadataOrientation = sharp::AttrAsUint32(options, "withMetadataOrientation");
baton->withMetadataDensity = sharp::AttrAsDouble(options, "withMetadataDensity"); baton->withMetadataDensity = sharp::AttrAsDouble(options, "withMetadataDensity");
baton->withMetadataIcc = sharp::AttrAsStr(options, "withMetadataIcc"); baton->withIccProfile = sharp::AttrAsStr(options, "withIccProfile");
Napi::Object mdStrs = options.Get("withMetadataStrs").As<Napi::Object>(); Napi::Object withExif = options.Get("withExif").As<Napi::Object>();
Napi::Array mdStrKeys = mdStrs.GetPropertyNames(); Napi::Array withExifKeys = withExif.GetPropertyNames();
for (unsigned int i = 0; i < mdStrKeys.Length(); i++) { for (unsigned int i = 0; i < withExifKeys.Length(); i++) {
std::string k = sharp::AttrAsStr(mdStrKeys, i); std::string k = sharp::AttrAsStr(withExifKeys, i);
if (mdStrs.HasOwnProperty(k)) { if (withExif.HasOwnProperty(k)) {
baton->withMetadataStrs.insert(std::make_pair(k, sharp::AttrAsStr(mdStrs, k))); baton->withExif.insert(std::make_pair(k, sharp::AttrAsStr(withExif, k)));
} }
} }
baton->withExifMerge = sharp::AttrAsBool(options, "withExifMerge");
baton->timeoutSeconds = sharp::AttrAsUint32(options, "timeoutSeconds"); baton->timeoutSeconds = sharp::AttrAsUint32(options, "timeoutSeconds");
// Format-specific // Format-specific
baton->jpegQuality = sharp::AttrAsUint32(options, "jpegQuality"); baton->jpegQuality = sharp::AttrAsUint32(options, "jpegQuality");

View File

@ -187,11 +187,12 @@ struct PipelineBaton {
bool jxlLossless; bool jxlLossless;
VipsBandFormat rawDepth; VipsBandFormat rawDepth;
std::string err; std::string err;
bool withMetadata; int keepMetadata;
int withMetadataOrientation; int withMetadataOrientation;
double withMetadataDensity; double withMetadataDensity;
std::string withMetadataIcc; std::string withIccProfile;
std::unordered_map<std::string, std::string> withMetadataStrs; std::unordered_map<std::string, std::string> withExif;
bool withExifMerge;
int timeoutSeconds; int timeoutSeconds;
std::unique_ptr<double[]> convKernel; std::unique_ptr<double[]> convKernel;
int convKernelWidth; int convKernelWidth;
@ -353,9 +354,10 @@ struct PipelineBaton {
jxlEffort(7), jxlEffort(7),
jxlLossless(false), jxlLossless(false),
rawDepth(VIPS_FORMAT_UCHAR), rawDepth(VIPS_FORMAT_UCHAR),
withMetadata(false), keepMetadata(0),
withMetadataOrientation(-1), withMetadataOrientation(-1),
withMetadataDensity(0.0), withMetadataDensity(0.0),
withExifMerge(true),
timeoutSeconds(0), timeoutSeconds(0),
convKernelWidth(0), convKernelWidth(0),
convKernelHeight(0), convKernelHeight(0),

View File

@ -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: 'text' }).toFile('out.webp');
sharp('input.tiff').webp({ preset: 'default' }).toFile('out.webp'); sharp('input.tiff').webp({ preset: 'default' }).toFile('out.webp');
// Allow a boolean or an object for metadata options. sharp(input)
// https://github.com/lovell/sharp/issues/3822 .keepExif()
sharp(input).withMetadata().withMetadata({}).withMetadata(false); .withExif({
IFD0: {
k1: 'v1'
}
})
.withExifMerge({
IFD1: {
k2: 'v2'
}
})
.keepIccProfile()
.withIccProfile('filename')
.withIccProfile('filename', { attach: false });

View File

@ -11,6 +11,8 @@ const icc = require('icc');
const sharp = require('../../'); const sharp = require('../../');
const fixtures = require('../fixtures'); const fixtures = require('../fixtures');
const create = { width: 1, height: 1, channels: 3, background: 'red' };
describe('Image metadata', function () { describe('Image metadata', function () {
it('JPEG', function (done) { it('JPEG', function (done) {
sharp(fixtures.inputJpg).metadata(function (err, metadata) { 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) { it('Apply CMYK output ICC profile', function (done) {
const output = fixtures.path('output.icc-cmyk.jpg'); const output = fixtures.path('output.icc-cmyk.jpg');
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)
.resize(64) .resize(64)
.withMetadata({ icc: 'cmyk' }) .withIccProfile('cmyk')
.toFile(output, function (err) { .toFile(output, function (err) {
if (err) throw err; if (err) throw err;
sharp(output).metadata(function (err, metadata) { sharp(output).metadata(function (err, metadata) {
@ -581,7 +627,7 @@ describe('Image metadata', function () {
const output = fixtures.path('output.hilutite.jpg'); const output = fixtures.path('output.hilutite.jpg');
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)
.resize(64) .resize(64)
.withMetadata({ icc: fixtures.path('hilutite.icm') }) .withIccProfile(fixtures.path('hilutite.icm'))
.toFile(output, function (err, info) { .toFile(output, function (err, info) {
if (err) throw err; if (err) throw err;
fixtures.assertMaxColourDistance(output, fixtures.expected('hilutite.jpg'), 9); 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) { it('Remove EXIF metadata after a resize', function (done) {
sharp(fixtures.inputJpgWithExif) sharp(fixtures.inputJpgWithExif)
.resize(320, 240) .resize(320, 240)
.withMetadata(false)
.toBuffer(function (err, buffer) { .toBuffer(function (err, buffer) {
if (err) throw err; if (err) throw err;
sharp(buffer).metadata(function (err, metadata) { sharp(buffer).metadata(function (err, metadata) {
@ -651,14 +696,7 @@ describe('Image metadata', function () {
}); });
it('Add EXIF metadata to JPEG', async () => { it('Add EXIF metadata to JPEG', async () => {
const data = await sharp({ const data = await sharp({ create })
create: {
width: 8,
height: 8,
channels: 3,
background: 'red'
}
})
.jpeg() .jpeg()
.withMetadata({ .withMetadata({
exif: { exif: {
@ -675,14 +713,7 @@ describe('Image metadata', function () {
}); });
it('Set density of JPEG', async () => { it('Set density of JPEG', async () => {
const data = await sharp({ const data = await sharp({ create })
create: {
width: 8,
height: 8,
channels: 3,
background: 'red'
}
})
.withMetadata({ .withMetadata({
density: 300 density: 300
}) })
@ -694,14 +725,7 @@ describe('Image metadata', function () {
}); });
it('Set density of PNG', async () => { it('Set density of PNG', async () => {
const data = await sharp({ const data = await sharp({ create })
create: {
width: 8,
height: 8,
channels: 3,
background: 'red'
}
})
.withMetadata({ .withMetadata({
density: 96 density: 96
}) })
@ -809,11 +833,7 @@ describe('Image metadata', function () {
}); });
it('withMetadata adds default sRGB profile to RGB16', async () => { it('withMetadata adds default sRGB profile to RGB16', async () => {
const data = await sharp({ const data = await sharp({ create })
create: {
width: 8, height: 8, channels: 4, background: 'orange'
}
})
.toColorspace('rgb16') .toColorspace('rgb16')
.png() .png()
.withMetadata() .withMetadata()
@ -827,11 +847,7 @@ describe('Image metadata', function () {
}); });
it('withMetadata adds P3 profile to 16-bit PNG', async () => { it('withMetadata adds P3 profile to 16-bit PNG', async () => {
const data = await sharp({ const data = await sharp({ create })
create: {
width: 8, height: 8, channels: 4, background: 'orange'
}
})
.toColorspace('rgb16') .toColorspace('rgb16')
.png() .png()
.withMetadata({ icc: 'p3' }) .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 () { it('String orientation', function () {
assert.throws(function () { assert.throws(function () {
sharp().withMetadata({ orientation: 'zoinks' }); sharp().withMetadata({ orientation: 'zoinks' });
@ -922,5 +1020,22 @@ describe('Image metadata', function () {
sharp().withMetadata({ exif: { ifd0: { fail: false } } }); 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/
);
});
}); });
}); });