Skip to content

Commit e78200c

Browse files
authoredNov 22, 2023
Increase control over output metadata (#3856)
Add withX and keepX functions to take advantage of libvips 8.15.0 new 'keep' metadata feature.
1 parent 3f7313d commit e78200c

13 files changed

+689
-174
lines changed
 

‎docs/api-colour.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ An alpha channel may be present and will be unchanged by the operation.
1212

1313
| Param | Type | Description |
1414
| --- | --- | --- |
15-
| tint | <code>String</code> \| <code>Object</code> | Parsed by the [color](https://www.npmjs.org/package/color) module. |
15+
| tint | <code>string</code> \| <code>Object</code> | Parsed by the [color](https://www.npmjs.org/package/color) module. |
1616

1717
**Example**
1818
```js

‎docs/api-output.md

+149-31
Original file line numberDiff line numberDiff line change
@@ -111,56 +111,174 @@ await sharp(pixelArray, { raw: { width, height, channels } })
111111
```
112112

113113

114-
## withMetadata
115-
> withMetadata([options]) ⇒ <code>Sharp</code>
114+
## keepExif
115+
> keepExif() ⇒ <code>Sharp</code>
116116
117-
Include all metadata (EXIF, XMP, IPTC) from the input image in the output image.
118-
This will also convert to and add a web-friendly sRGB ICC profile if appropriate,
119-
unless a custom output profile is provided.
120-
121-
The default behaviour, when `withMetadata` is not used, is to convert to the device-independent
122-
sRGB colour space and strip all metadata, including the removal of any ICC profile.
117+
Keep all EXIF metadata from the input image in the output image.
123118

124119
EXIF metadata is unsupported for TIFF output.
125120

126121

122+
**Since**: 0.33.0
123+
**Example**
124+
```js
125+
const outputWithExif = await sharp(inputWithExif)
126+
.keepExif()
127+
.toBuffer();
128+
```
129+
130+
131+
## withExif
132+
> withExif(exif) ⇒ <code>Sharp</code>
133+
134+
Set EXIF metadata in the output image, ignoring any EXIF in the input image.
135+
136+
137+
**Throws**:
138+
139+
- <code>Error</code> Invalid parameters
140+
141+
**Since**: 0.33.0
142+
143+
| Param | Type | Description |
144+
| --- | --- | --- |
145+
| 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. |
146+
147+
**Example**
148+
```js
149+
const dataWithExif = await sharp(input)
150+
.withExif({
151+
IFD0: {
152+
Copyright: 'The National Gallery'
153+
},
154+
IFD3: {
155+
GPSLatitudeRef: 'N',
156+
GPSLatitude: '51/1 30/1 3230/100',
157+
GPSLongitudeRef: 'W',
158+
GPSLongitude: '0/1 7/1 4366/100'
159+
}
160+
})
161+
.toBuffer();
162+
```
163+
164+
165+
## withExifMerge
166+
> withExifMerge(exif) ⇒ <code>Sharp</code>
167+
168+
Update EXIF metadata from the input image in the output image.
169+
170+
171+
**Throws**:
172+
173+
- <code>Error</code> Invalid parameters
174+
175+
**Since**: 0.33.0
176+
177+
| Param | Type | Description |
178+
| --- | --- | --- |
179+
| 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. |
180+
181+
**Example**
182+
```js
183+
const dataWithMergedExif = await sharp(inputWithExif)
184+
.withExifMerge({
185+
IFD0: {
186+
Copyright: 'The National Gallery'
187+
}
188+
})
189+
.toBuffer();
190+
```
191+
192+
193+
## keepIccProfile
194+
> keepIccProfile() ⇒ <code>Sharp</code>
195+
196+
Keep ICC profile from the input image in the output image.
197+
198+
Where necessary, will attempt to convert the output colour space to match the profile.
199+
200+
201+
**Since**: 0.33.0
202+
**Example**
203+
```js
204+
const outputWithIccProfile = await sharp(inputWithIccProfile)
205+
.keepIccProfile()
206+
.toBuffer();
207+
```
208+
209+
210+
## withIccProfile
211+
> withIccProfile(icc, [options]) ⇒ <code>Sharp</code>
212+
213+
Transform using an ICC profile and attach to the output image.
214+
215+
This can either be an absolute filesystem path or
216+
built-in profile name (`srgb`, `p3`, `cmyk`).
217+
218+
127219
**Throws**:
128220

129221
- <code>Error</code> Invalid parameters
130222

223+
**Since**: 0.33.0
131224

132225
| Param | Type | Default | Description |
133226
| --- | --- | --- | --- |
227+
| icc | <code>string</code> | | Absolute filesystem path to output ICC profile or built-in profile name (srgb, p3, cmyk). |
134228
| [options] | <code>Object</code> | | |
135-
| [options.orientation] | <code>number</code> | | value between 1 and 8, used to update the EXIF `Orientation` tag. |
136-
| [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. |
137-
| [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. |
138-
| [options.density] | <code>number</code> | | Number of pixels per inch (DPI). |
229+
| [options.attach] | <code>number</code> | <code>true</code> | Should the ICC profile be included in the output image metadata? |
139230

140231
**Example**
141232
```js
142-
sharp('input.jpg')
143-
.withMetadata()
144-
.toFile('output-with-metadata.jpg')
145-
.then(info => { ... });
233+
const outputWithP3 = await sharp(input)
234+
.withIccProfile('p3')
235+
.toBuffer();
146236
```
237+
238+
239+
## keepMetadata
240+
> keepMetadata() ⇒ <code>Sharp</code>
241+
242+
Keep all metadata (EXIF, ICC, XMP, IPTC) from the input image in the output image.
243+
244+
The default behaviour, when `keepMetadata` is not used, is to convert to the device-independent
245+
sRGB colour space and strip all metadata, including the removal of any ICC profile.
246+
247+
248+
**Since**: 0.33.0
147249
**Example**
148250
```js
149-
// Set output EXIF metadata
150-
const data = await sharp(input)
151-
.withMetadata({
152-
exif: {
153-
IFD0: {
154-
Copyright: 'The National Gallery'
155-
},
156-
IFD3: {
157-
GPSLatitudeRef: 'N',
158-
GPSLatitude: '51/1 30/1 3230/100',
159-
GPSLongitudeRef: 'W',
160-
GPSLongitude: '0/1 7/1 4366/100'
161-
}
162-
}
163-
})
251+
const outputWithMetadata = await sharp(inputWithMetadata)
252+
.keepMetadata()
253+
.toBuffer();
254+
```
255+
256+
257+
## withMetadata
258+
> withMetadata([options]) ⇒ <code>Sharp</code>
259+
260+
Keep most metadata (EXIF, XMP, IPTC) from the input image in the output image.
261+
262+
This will also convert to and add a web-friendly sRGB ICC profile if appropriate.
263+
264+
Allows orientation and density to be set or updated.
265+
266+
267+
**Throws**:
268+
269+
- <code>Error</code> Invalid parameters
270+
271+
272+
| Param | Type | Description |
273+
| --- | --- | --- |
274+
| [options] | <code>Object</code> | |
275+
| [options.orientation] | <code>number</code> | Used to update the EXIF `Orientation` tag, integer between 1 and 8. |
276+
| [options.density] | <code>number</code> | Number of pixels per inch (DPI). |
277+
278+
**Example**
279+
```js
280+
const outputSrgbWithMetadata = await sharp(inputRgbWithMetadata)
281+
.withMetadata()
164282
.toBuffer();
165283
```
166284
**Example**

‎docs/changelog.md

+5
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ Requires libvips v8.15.0
1414

1515
* Remove `sharp.vendor`.
1616

17+
* Partially deprecate `withMetadata()`, use `withExif()` and `withIccProfile()`.
18+
1719
* Add experimental support for WebAssembly-based runtimes.
1820
[@RReverser](https://github.com/RReverser)
1921

@@ -41,6 +43,9 @@ Requires libvips v8.15.0
4143
[#3823](https://github.com/lovell/sharp/pull/3823)
4244
[@uhthomas](https://github.com/uhthomas)
4345

46+
* Add more fine-grained control over output metadata.
47+
[#3824](https://github.com/lovell/sharp/issues/3824)
48+
4449
* Ensure multi-page extract remains sequential.
4550
[#3837](https://github.com/lovell/sharp/issues/3837)
4651

‎docs/search-index.json

+1-1
Large diffs are not rendered by default.

‎lib/constructor.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -257,11 +257,12 @@ const Sharp = function (input, options) {
257257
fileOut: '',
258258
formatOut: 'input',
259259
streamOut: false,
260-
withMetadata: false,
260+
keepMetadata: 0,
261261
withMetadataOrientation: -1,
262262
withMetadataDensity: 0,
263-
withMetadataIcc: '',
264-
withMetadataStrs: {},
263+
withIccProfile: '',
264+
withExif: {},
265+
withExifMerge: true,
265266
resolveWithObject: false,
266267
// output format
267268
jpegQuality: 80,

‎lib/index.d.ts

+65-6
Original file line numberDiff line numberDiff line change
@@ -633,14 +633,51 @@ declare namespace sharp {
633633
*/
634634
toBuffer(options: { resolveWithObject: true }): Promise<{ data: Buffer; info: OutputInfo }>;
635635

636+
/**
637+
* Keep all EXIF metadata from the input image in the output image.
638+
* EXIF metadata is unsupported for TIFF output.
639+
* @returns A sharp instance that can be used to chain operations
640+
*/
641+
keepExif(): Sharp;
642+
643+
/**
644+
* Set EXIF metadata in the output image, ignoring any EXIF in the input image.
645+
* @param {Exif} exif Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data.
646+
* @returns A sharp instance that can be used to chain operations
647+
* @throws {Error} Invalid parameters
648+
*/
649+
withExif(exif: Exif): Sharp;
650+
651+
/**
652+
* Update EXIF metadata from the input image in the output image.
653+
* @param {Exif} exif Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data.
654+
* @returns A sharp instance that can be used to chain operations
655+
* @throws {Error} Invalid parameters
656+
*/
657+
withExifMerge(exif: Exif): Sharp;
658+
659+
/**
660+
* Keep ICC profile from the input image in the output image where possible.
661+
* @returns A sharp instance that can be used to chain operations
662+
*/
663+
keepIccProfile(): Sharp;
664+
665+
/**
666+
* Transform using an ICC profile and attach to the output image.
667+
* @param {string} icc - Absolute filesystem path to output ICC profile or built-in profile name (srgb, p3, cmyk).
668+
* @returns A sharp instance that can be used to chain operations
669+
* @throws {Error} Invalid parameters
670+
*/
671+
withIccProfile(icc: string, options?: WithIccProfileOptions): Sharp;
672+
636673
/**
637674
* Include all metadata (EXIF, XMP, IPTC) from the input image in the output image.
638675
* The default behaviour, when withMetadata is not used, is to strip all metadata and convert to the device-independent sRGB colour space.
639676
* This will also convert to and add a web-friendly sRGB ICC profile.
640677
* @param withMetadata
641678
* @throws {Error} Invalid parameters.
642679
*/
643-
withMetadata(withMetadata?: boolean | WriteableMetadata): Sharp;
680+
withMetadata(withMetadata?: WriteableMetadata): Sharp;
644681

645682
/**
646683
* Use these JPEG options for output image.
@@ -978,15 +1015,32 @@ declare namespace sharp {
9781015
wrap?: TextWrap;
9791016
}
9801017

1018+
interface ExifDir {
1019+
[k: string]: string;
1020+
}
1021+
1022+
interface Exif {
1023+
'IFD0'?: ExifDir;
1024+
'IFD1'?: ExifDir;
1025+
'IFD2'?: ExifDir;
1026+
'IFD3'?: ExifDir;
1027+
}
1028+
9811029
interface WriteableMetadata {
1030+
/** Number of pixels per inch (DPI) */
1031+
density?: number | undefined;
9821032
/** Value between 1 and 8, used to update the EXIF Orientation tag. */
9831033
orientation?: number | undefined;
984-
/** Filesystem path to output ICC profile, defaults to sRGB. */
1034+
/**
1035+
* Filesystem path to output ICC profile, defaults to sRGB.
1036+
* @deprecated Use `withIccProfile()` instead.
1037+
*/
9851038
icc?: string | undefined;
986-
/** Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data. (optional, default {}) */
987-
exif?: Record<string, any> | undefined;
988-
/** Number of pixels per inch (DPI) */
989-
density?: number | undefined;
1039+
/**
1040+
* Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data.
1041+
* @deprecated Use `withExif()` or `withExifMerge()` instead.
1042+
*/
1043+
exif?: Exif | undefined;
9901044
}
9911045

9921046
interface Metadata {
@@ -1096,6 +1150,11 @@ declare namespace sharp {
10961150
force?: boolean | undefined;
10971151
}
10981152

1153+
interface WithIccProfileOptions {
1154+
/** Should the ICC profile be included in the output image metadata? (optional, default true) */
1155+
attach?: boolean | undefined;
1156+
}
1157+
10991158
interface JpegOptions extends OutputOptions {
11001159
/** Quality, integer 1-100 (optional, default 80) */
11011160
quality?: number | undefined;

‎lib/output.js

+180-49
Original file line numberDiff line numberDiff line change
@@ -163,55 +163,200 @@ function toBuffer (options, callback) {
163163
}
164164

165165
/**
166-
* Include all metadata (EXIF, XMP, IPTC) from the input image in the output image.
167-
* This will also convert to and add a web-friendly sRGB ICC profile if appropriate,
168-
* unless a custom output profile is provided.
169-
*
170-
* The default behaviour, when `withMetadata` is not used, is to convert to the device-independent
171-
* sRGB colour space and strip all metadata, including the removal of any ICC profile.
166+
* Keep all EXIF metadata from the input image in the output image.
172167
*
173168
* EXIF metadata is unsupported for TIFF output.
174169
*
170+
* @since 0.33.0
171+
*
175172
* @example
176-
* sharp('input.jpg')
177-
* .withMetadata()
178-
* .toFile('output-with-metadata.jpg')
179-
* .then(info => { ... });
173+
* const outputWithExif = await sharp(inputWithExif)
174+
* .keepExif()
175+
* .toBuffer();
176+
*
177+
* @returns {Sharp}
178+
*/
179+
function keepExif () {
180+
this.options.keepMetadata |= 0b00001;
181+
return this;
182+
}
183+
184+
/**
185+
* Set EXIF metadata in the output image, ignoring any EXIF in the input image.
186+
*
187+
* @since 0.33.0
180188
*
181189
* @example
182-
* // Set output EXIF metadata
183-
* const data = await sharp(input)
184-
* .withMetadata({
185-
* exif: {
186-
* IFD0: {
187-
* Copyright: 'The National Gallery'
188-
* },
189-
* IFD3: {
190-
* GPSLatitudeRef: 'N',
191-
* GPSLatitude: '51/1 30/1 3230/100',
192-
* GPSLongitudeRef: 'W',
193-
* GPSLongitude: '0/1 7/1 4366/100'
194-
* }
190+
* const dataWithExif = await sharp(input)
191+
* .withExif({
192+
* IFD0: {
193+
* Copyright: 'The National Gallery'
194+
* },
195+
* IFD3: {
196+
* GPSLatitudeRef: 'N',
197+
* GPSLatitude: '51/1 30/1 3230/100',
198+
* GPSLongitudeRef: 'W',
199+
* GPSLongitude: '0/1 7/1 4366/100'
200+
* }
201+
* })
202+
* .toBuffer();
203+
*
204+
* @param {Object<string, Object<string, string>>} exif Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data.
205+
* @returns {Sharp}
206+
* @throws {Error} Invalid parameters
207+
*/
208+
function withExif (exif) {
209+
if (is.object(exif)) {
210+
for (const [ifd, entries] of Object.entries(exif)) {
211+
if (is.object(entries)) {
212+
for (const [k, v] of Object.entries(entries)) {
213+
if (is.string(v)) {
214+
this.options.withExif[`exif-${ifd.toLowerCase()}-${k}`] = v;
215+
} else {
216+
throw is.invalidParameterError(`${ifd}.${k}`, 'string', v);
217+
}
218+
}
219+
} else {
220+
throw is.invalidParameterError(ifd, 'object', entries);
221+
}
222+
}
223+
} else {
224+
throw is.invalidParameterError('exif', 'object', exif);
225+
}
226+
this.options.withExifMerge = false;
227+
return this.keepExif();
228+
}
229+
230+
/**
231+
* Update EXIF metadata from the input image in the output image.
232+
*
233+
* @since 0.33.0
234+
*
235+
* @example
236+
* const dataWithMergedExif = await sharp(inputWithExif)
237+
* .withExifMerge({
238+
* IFD0: {
239+
* Copyright: 'The National Gallery'
195240
* }
196241
* })
197242
* .toBuffer();
198243
*
244+
* @param {Object<string, Object<string, string>>} exif Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data.
245+
* @returns {Sharp}
246+
* @throws {Error} Invalid parameters
247+
*/
248+
function withExifMerge (exif) {
249+
this.withExif(exif);
250+
this.options.withExifMerge = true;
251+
return this;
252+
}
253+
254+
/**
255+
* Keep ICC profile from the input image in the output image.
256+
*
257+
* Where necessary, will attempt to convert the output colour space to match the profile.
258+
*
259+
* @since 0.33.0
260+
*
261+
* @example
262+
* const outputWithIccProfile = await sharp(inputWithIccProfile)
263+
* .keepIccProfile()
264+
* .toBuffer();
265+
*
266+
* @returns {Sharp}
267+
*/
268+
function keepIccProfile () {
269+
this.options.keepMetadata |= 0b01000;
270+
return this;
271+
}
272+
273+
/**
274+
* Transform using an ICC profile and attach to the output image.
275+
*
276+
* This can either be an absolute filesystem path or
277+
* built-in profile name (`srgb`, `p3`, `cmyk`).
278+
*
279+
* @since 0.33.0
280+
*
281+
* @example
282+
* const outputWithP3 = await sharp(input)
283+
* .withIccProfile('p3')
284+
* .toBuffer();
285+
*
286+
* @param {string} icc - Absolute filesystem path to output ICC profile or built-in profile name (srgb, p3, cmyk).
287+
* @param {Object} [options]
288+
* @param {number} [options.attach=true] Should the ICC profile be included in the output image metadata?
289+
* @returns {Sharp}
290+
* @throws {Error} Invalid parameters
291+
*/
292+
function withIccProfile (icc, options) {
293+
if (is.string(icc)) {
294+
this.options.withIccProfile = icc;
295+
} else {
296+
throw is.invalidParameterError('icc', 'string', icc);
297+
}
298+
this.keepIccProfile();
299+
if (is.object(options)) {
300+
if (is.defined(options.attach)) {
301+
if (is.bool(options.attach)) {
302+
if (!options.attach) {
303+
this.options.keepMetadata &= ~0b01000;
304+
}
305+
} else {
306+
throw is.invalidParameterError('attach', 'boolean', options.attach);
307+
}
308+
}
309+
}
310+
return this;
311+
}
312+
313+
/**
314+
* Keep all metadata (EXIF, ICC, XMP, IPTC) from the input image in the output image.
315+
*
316+
* The default behaviour, when `keepMetadata` is not used, is to convert to the device-independent
317+
* sRGB colour space and strip all metadata, including the removal of any ICC profile.
318+
*
319+
* @since 0.33.0
320+
*
321+
* @example
322+
* const outputWithMetadata = await sharp(inputWithMetadata)
323+
* .keepMetadata()
324+
* .toBuffer();
325+
*
326+
* @returns {Sharp}
327+
*/
328+
function keepMetadata () {
329+
this.options.keepMetadata = 0b11111;
330+
return this;
331+
}
332+
333+
/**
334+
* Keep most metadata (EXIF, XMP, IPTC) from the input image in the output image.
335+
*
336+
* This will also convert to and add a web-friendly sRGB ICC profile if appropriate.
337+
*
338+
* Allows orientation and density to be set or updated.
339+
*
340+
* @example
341+
* const outputSrgbWithMetadata = await sharp(inputRgbWithMetadata)
342+
* .withMetadata()
343+
* .toBuffer();
344+
*
199345
* @example
200346
* // Set output metadata to 96 DPI
201347
* const data = await sharp(input)
202348
* .withMetadata({ density: 96 })
203349
* .toBuffer();
204350
*
205351
* @param {Object} [options]
206-
* @param {number} [options.orientation] value between 1 and 8, used to update the EXIF `Orientation` tag.
207-
* @param {string} [options.icc='srgb'] Filesystem path to output ICC profile, relative to `process.cwd()`, defaults to built-in sRGB.
208-
* @param {Object<Object>} [options.exif={}] Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data.
352+
* @param {number} [options.orientation] Used to update the EXIF `Orientation` tag, integer between 1 and 8.
209353
* @param {number} [options.density] Number of pixels per inch (DPI).
210354
* @returns {Sharp}
211355
* @throws {Error} Invalid parameters
212356
*/
213357
function withMetadata (options) {
214-
this.options.withMetadata = is.bool(options) ? options : true;
358+
this.keepMetadata();
359+
this.withIccProfile('srgb');
215360
if (is.object(options)) {
216361
if (is.defined(options.orientation)) {
217362
if (is.integer(options.orientation) && is.inRange(options.orientation, 1, 8)) {
@@ -228,30 +373,10 @@ function withMetadata (options) {
228373
}
229374
}
230375
if (is.defined(options.icc)) {
231-
if (is.string(options.icc)) {
232-
this.options.withMetadataIcc = options.icc;
233-
} else {
234-
throw is.invalidParameterError('icc', 'string filesystem path to ICC profile', options.icc);
235-
}
376+
this.withIccProfile(options.icc);
236377
}
237378
if (is.defined(options.exif)) {
238-
if (is.object(options.exif)) {
239-
for (const [ifd, entries] of Object.entries(options.exif)) {
240-
if (is.object(entries)) {
241-
for (const [k, v] of Object.entries(entries)) {
242-
if (is.string(v)) {
243-
this.options.withMetadataStrs[`exif-${ifd.toLowerCase()}-${k}`] = v;
244-
} else {
245-
throw is.invalidParameterError(`exif.${ifd}.${k}`, 'string', v);
246-
}
247-
}
248-
} else {
249-
throw is.invalidParameterError(`exif.${ifd}`, 'object', entries);
250-
}
251-
}
252-
} else {
253-
throw is.invalidParameterError('exif', 'object', options.exif);
254-
}
379+
this.withExifMerge(options.exif);
255380
}
256381
}
257382
return this;
@@ -1407,6 +1532,12 @@ module.exports = function (Sharp) {
14071532
// Public
14081533
toFile,
14091534
toBuffer,
1535+
keepExif,
1536+
withExif,
1537+
withExifMerge,
1538+
keepIccProfile,
1539+
withIccProfile,
1540+
keepMetadata,
14101541
withMetadata,
14111542
toFormat,
14121543
jpeg,

‎src/common.cc

+48-1
Original file line numberDiff line numberDiff line change
@@ -531,7 +531,33 @@ namespace sharp {
531531
Does this image have an embedded profile?
532532
*/
533533
bool HasProfile(VImage image) {
534-
return (image.get_typeof(VIPS_META_ICC_NAME) != 0) ? TRUE : FALSE;
534+
return image.get_typeof(VIPS_META_ICC_NAME) == VIPS_TYPE_BLOB;
535+
}
536+
537+
/*
538+
Get copy of embedded profile.
539+
*/
540+
std::pair<char*, size_t> GetProfile(VImage image) {
541+
std::pair<char*, size_t> icc(nullptr, 0);
542+
if (HasProfile(image)) {
543+
size_t length;
544+
const void *data = image.get_blob(VIPS_META_ICC_NAME, &length);
545+
icc.first = static_cast<char*>(g_malloc(length));
546+
icc.second = length;
547+
memcpy(icc.first, data, length);
548+
}
549+
return icc;
550+
}
551+
552+
/*
553+
Set embedded profile.
554+
*/
555+
VImage SetProfile(VImage image, std::pair<char*, size_t> icc) {
556+
if (icc.first != nullptr) {
557+
image = image.copy();
558+
image.set(VIPS_META_ICC_NAME, reinterpret_cast<VipsCallbackFn>(vips_area_free_cb), icc.first, icc.second);
559+
}
560+
return image;
535561
}
536562

537563
/*
@@ -542,6 +568,27 @@ namespace sharp {
542568
return image.has_alpha();
543569
}
544570

571+
static void* RemoveExifCallback(VipsImage *image, char const *field, GValue *value, void *data) {
572+
std::vector<std::string> *fieldNames = static_cast<std::vector<std::string> *>(data);
573+
std::string fieldName(field);
574+
if (fieldName.substr(0, 8) == ("exif-ifd")) {
575+
fieldNames->push_back(fieldName);
576+
}
577+
return nullptr;
578+
}
579+
580+
/*
581+
Remove all EXIF-related image fields.
582+
*/
583+
VImage RemoveExif(VImage image) {
584+
std::vector<std::string> fieldNames;
585+
vips_image_map(image.get_image(), static_cast<VipsImageMapFn>(RemoveExifCallback), &fieldNames);
586+
for (const auto& f : fieldNames) {
587+
image.remove(f.data());
588+
}
589+
return image;
590+
}
591+
545592
/*
546593
Get EXIF Orientation of image, if any.
547594
*/

‎src/common.h

+15
Original file line numberDiff line numberDiff line change
@@ -222,12 +222,27 @@ namespace sharp {
222222
*/
223223
bool HasProfile(VImage image);
224224

225+
/*
226+
Get copy of embedded profile.
227+
*/
228+
std::pair<char*, size_t> GetProfile(VImage image);
229+
230+
/*
231+
Set embedded profile.
232+
*/
233+
VImage SetProfile(VImage image, std::pair<char*, size_t> icc);
234+
225235
/*
226236
Does this image have an alpha channel?
227237
Uses colour space interpretation with number of channels to guess this.
228238
*/
229239
bool HasAlpha(VImage image);
230240

241+
/*
242+
Remove all EXIF-related image fields.
243+
*/
244+
VImage RemoveExif(VImage image);
245+
231246
/*
232247
Get EXIF Orientation of image, if any.
233248
*/

‎src/pipeline.cc

+47-37
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,11 @@ class PipelineWorker : public Napi::AsyncWorker {
315315
}
316316

317317
// Ensure we're using a device-independent colour space
318+
std::pair<char*, size_t> inputProfile(nullptr, 0);
319+
if ((baton->keepMetadata & VIPS_FOREIGN_KEEP_ICC) && baton->withIccProfile.empty()) {
320+
// Cache input profile for use with output
321+
inputProfile = sharp::GetProfile(image);
322+
}
318323
char const *processingProfile = image.interpretation() == VIPS_INTERPRETATION_RGB16 ? "p3" : "srgb";
319324
if (
320325
sharp::HasProfile(image) &&
@@ -758,7 +763,8 @@ class PipelineWorker : public Napi::AsyncWorker {
758763
// Convert colourspace, pass the current known interpretation so libvips doesn't have to guess
759764
image = image.colourspace(baton->colourspace, VImage::option()->set("source_space", image.interpretation()));
760765
// Transform colours from embedded profile to output profile
761-
if (baton->withMetadata && sharp::HasProfile(image) && baton->withMetadataIcc.empty()) {
766+
if ((baton->keepMetadata & VIPS_FOREIGN_KEEP_ICC) &&
767+
baton->withIccProfile.empty() && sharp::HasProfile(image)) {
762768
image = image.icc_transform("srgb", VImage::option()
763769
->set("embedded", TRUE)
764770
->set("depth", sharp::Is16Bit(image.interpretation()) ? 16 : 8)
@@ -787,27 +793,30 @@ class PipelineWorker : public Napi::AsyncWorker {
787793
}
788794

789795
// Apply output ICC profile
790-
if (baton->withMetadata) {
791-
image = image.icc_transform(
792-
baton->withMetadataIcc.empty() ? "srgb" : const_cast<char*>(baton->withMetadataIcc.data()),
793-
VImage::option()
794-
->set("input_profile", processingProfile)
795-
->set("embedded", TRUE)
796-
->set("depth", sharp::Is16Bit(image.interpretation()) ? 16 : 8)
797-
->set("intent", VIPS_INTENT_PERCEPTUAL));
796+
if (!baton->withIccProfile.empty()) {
797+
image = image.icc_transform(const_cast<char*>(baton->withIccProfile.data()), VImage::option()
798+
->set("input_profile", processingProfile)
799+
->set("embedded", TRUE)
800+
->set("depth", sharp::Is16Bit(image.interpretation()) ? 16 : 8)
801+
->set("intent", VIPS_INTENT_PERCEPTUAL));
802+
} else if (baton->keepMetadata & VIPS_FOREIGN_KEEP_ICC) {
803+
image = sharp::SetProfile(image, inputProfile);
798804
}
799805
// Override EXIF Orientation tag
800-
if (baton->withMetadata && baton->withMetadataOrientation != -1) {
806+
if (baton->withMetadataOrientation != -1) {
801807
image = sharp::SetExifOrientation(image, baton->withMetadataOrientation);
802808
}
803809
// Override pixel density
804810
if (baton->withMetadataDensity > 0) {
805811
image = sharp::SetDensity(image, baton->withMetadataDensity);
806812
}
807-
// Metadata key/value pairs, e.g. EXIF
808-
if (!baton->withMetadataStrs.empty()) {
813+
// EXIF key/value pairs
814+
if (baton->keepMetadata & VIPS_FOREIGN_KEEP_EXIF) {
809815
image = image.copy();
810-
for (const auto& s : baton->withMetadataStrs) {
816+
if (!baton->withExifMerge) {
817+
image = sharp::RemoveExif(image);
818+
}
819+
for (const auto& s : baton->withExif) {
811820
image.set(s.first.data(), s.second.data());
812821
}
813822
}
@@ -828,7 +837,7 @@ class PipelineWorker : public Napi::AsyncWorker {
828837
// Write JPEG to buffer
829838
sharp::AssertImageTypeDimensions(image, sharp::ImageType::JPEG);
830839
VipsArea *area = reinterpret_cast<VipsArea*>(image.jpegsave_buffer(VImage::option()
831-
->set("strip", !baton->withMetadata)
840+
->set("keep", baton->keepMetadata)
832841
->set("Q", baton->jpegQuality)
833842
->set("interlace", baton->jpegProgressive)
834843
->set("subsample_mode", baton->jpegChromaSubsampling == "4:4:4"
@@ -870,7 +879,7 @@ class PipelineWorker : public Napi::AsyncWorker {
870879
// Write PNG to buffer
871880
sharp::AssertImageTypeDimensions(image, sharp::ImageType::PNG);
872881
VipsArea *area = reinterpret_cast<VipsArea*>(image.pngsave_buffer(VImage::option()
873-
->set("strip", !baton->withMetadata)
882+
->set("keep", baton->keepMetadata)
874883
->set("interlace", baton->pngProgressive)
875884
->set("compression", baton->pngCompressionLevel)
876885
->set("filter", baton->pngAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_ALL : VIPS_FOREIGN_PNG_FILTER_NONE)
@@ -889,7 +898,7 @@ class PipelineWorker : public Napi::AsyncWorker {
889898
// Write WEBP to buffer
890899
sharp::AssertImageTypeDimensions(image, sharp::ImageType::WEBP);
891900
VipsArea *area = reinterpret_cast<VipsArea*>(image.webpsave_buffer(VImage::option()
892-
->set("strip", !baton->withMetadata)
901+
->set("keep", baton->keepMetadata)
893902
->set("Q", baton->webpQuality)
894903
->set("lossless", baton->webpLossless)
895904
->set("near_lossless", baton->webpNearLossless)
@@ -909,7 +918,7 @@ class PipelineWorker : public Napi::AsyncWorker {
909918
// Write GIF to buffer
910919
sharp::AssertImageTypeDimensions(image, sharp::ImageType::GIF);
911920
VipsArea *area = reinterpret_cast<VipsArea*>(image.gifsave_buffer(VImage::option()
912-
->set("strip", !baton->withMetadata)
921+
->set("keep", baton->keepMetadata)
913922
->set("bitdepth", baton->gifBitdepth)
914923
->set("effort", baton->gifEffort)
915924
->set("reuse", baton->gifReuse)
@@ -934,7 +943,7 @@ class PipelineWorker : public Napi::AsyncWorker {
934943
image = image.cast(VIPS_FORMAT_FLOAT);
935944
}
936945
VipsArea *area = reinterpret_cast<VipsArea*>(image.tiffsave_buffer(VImage::option()
937-
->set("strip", !baton->withMetadata)
946+
->set("keep", baton->keepMetadata)
938947
->set("Q", baton->tiffQuality)
939948
->set("bitdepth", baton->tiffBitdepth)
940949
->set("compression", baton->tiffCompression)
@@ -958,7 +967,7 @@ class PipelineWorker : public Napi::AsyncWorker {
958967
sharp::AssertImageTypeDimensions(image, sharp::ImageType::HEIF);
959968
image = sharp::RemoveAnimationProperties(image).cast(VIPS_FORMAT_UCHAR);
960969
VipsArea *area = reinterpret_cast<VipsArea*>(image.heifsave_buffer(VImage::option()
961-
->set("strip", !baton->withMetadata)
970+
->set("keep", baton->keepMetadata)
962971
->set("Q", baton->heifQuality)
963972
->set("compression", baton->heifCompression)
964973
->set("effort", baton->heifEffort)
@@ -990,7 +999,7 @@ class PipelineWorker : public Napi::AsyncWorker {
990999
// Write JXL to buffer
9911000
image = sharp::RemoveAnimationProperties(image);
9921001
VipsArea *area = reinterpret_cast<VipsArea*>(image.jxlsave_buffer(VImage::option()
993-
->set("strip", !baton->withMetadata)
1002+
->set("keep", baton->keepMetadata)
9941003
->set("distance", baton->jxlDistance)
9951004
->set("tier", baton->jxlDecodingTier)
9961005
->set("effort", baton->jxlEffort)
@@ -1051,7 +1060,7 @@ class PipelineWorker : public Napi::AsyncWorker {
10511060
// Write JPEG to file
10521061
sharp::AssertImageTypeDimensions(image, sharp::ImageType::JPEG);
10531062
image.jpegsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1054-
->set("strip", !baton->withMetadata)
1063+
->set("keep", baton->keepMetadata)
10551064
->set("Q", baton->jpegQuality)
10561065
->set("interlace", baton->jpegProgressive)
10571066
->set("subsample_mode", baton->jpegChromaSubsampling == "4:4:4"
@@ -1081,7 +1090,7 @@ class PipelineWorker : public Napi::AsyncWorker {
10811090
// Write PNG to file
10821091
sharp::AssertImageTypeDimensions(image, sharp::ImageType::PNG);
10831092
image.pngsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1084-
->set("strip", !baton->withMetadata)
1093+
->set("keep", baton->keepMetadata)
10851094
->set("interlace", baton->pngProgressive)
10861095
->set("compression", baton->pngCompressionLevel)
10871096
->set("filter", baton->pngAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_ALL : VIPS_FOREIGN_PNG_FILTER_NONE)
@@ -1096,7 +1105,7 @@ class PipelineWorker : public Napi::AsyncWorker {
10961105
// Write WEBP to file
10971106
sharp::AssertImageTypeDimensions(image, sharp::ImageType::WEBP);
10981107
image.webpsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1099-
->set("strip", !baton->withMetadata)
1108+
->set("keep", baton->keepMetadata)
11001109
->set("Q", baton->webpQuality)
11011110
->set("lossless", baton->webpLossless)
11021111
->set("near_lossless", baton->webpNearLossless)
@@ -1112,7 +1121,7 @@ class PipelineWorker : public Napi::AsyncWorker {
11121121
// Write GIF to file
11131122
sharp::AssertImageTypeDimensions(image, sharp::ImageType::GIF);
11141123
image.gifsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1115-
->set("strip", !baton->withMetadata)
1124+
->set("keep", baton->keepMetadata)
11161125
->set("bitdepth", baton->gifBitdepth)
11171126
->set("effort", baton->gifEffort)
11181127
->set("reuse", baton->gifReuse)
@@ -1131,7 +1140,7 @@ class PipelineWorker : public Napi::AsyncWorker {
11311140
image = image.cast(VIPS_FORMAT_FLOAT);
11321141
}
11331142
image.tiffsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1134-
->set("strip", !baton->withMetadata)
1143+
->set("keep", baton->keepMetadata)
11351144
->set("Q", baton->tiffQuality)
11361145
->set("bitdepth", baton->tiffBitdepth)
11371146
->set("compression", baton->tiffCompression)
@@ -1151,7 +1160,7 @@ class PipelineWorker : public Napi::AsyncWorker {
11511160
sharp::AssertImageTypeDimensions(image, sharp::ImageType::HEIF);
11521161
image = sharp::RemoveAnimationProperties(image).cast(VIPS_FORMAT_UCHAR);
11531162
image.heifsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1154-
->set("strip", !baton->withMetadata)
1163+
->set("keep", baton->keepMetadata)
11551164
->set("Q", baton->heifQuality)
11561165
->set("compression", baton->heifCompression)
11571166
->set("effort", baton->heifEffort)
@@ -1165,7 +1174,7 @@ class PipelineWorker : public Napi::AsyncWorker {
11651174
// Write JXL to file
11661175
image = sharp::RemoveAnimationProperties(image);
11671176
image.jxlsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1168-
->set("strip", !baton->withMetadata)
1177+
->set("keep", baton->keepMetadata)
11691178
->set("distance", baton->jxlDistance)
11701179
->set("tier", baton->jxlDecodingTier)
11711180
->set("effort", baton->jxlEffort)
@@ -1187,7 +1196,7 @@ class PipelineWorker : public Napi::AsyncWorker {
11871196
(willMatchInput && inputImageType == sharp::ImageType::VIPS)) {
11881197
// Write V to file
11891198
image.vipssave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1190-
->set("strip", !baton->withMetadata));
1199+
->set("keep", baton->keepMetadata));
11911200
baton->formatOut = "v";
11921201
} else {
11931202
// Unsupported output format
@@ -1401,7 +1410,7 @@ class PipelineWorker : public Napi::AsyncWorker {
14011410
suffix = AssembleSuffixString(extname, options);
14021411
}
14031412
vips::VOption *options = VImage::option()
1404-
->set("strip", !baton->withMetadata)
1413+
->set("keep", baton->keepMetadata)
14051414
->set("tile_size", baton->tileSize)
14061415
->set("overlap", baton->tileOverlap)
14071416
->set("container", baton->tileContainer)
@@ -1593,18 +1602,19 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
15931602
// Output
15941603
baton->formatOut = sharp::AttrAsStr(options, "formatOut");
15951604
baton->fileOut = sharp::AttrAsStr(options, "fileOut");
1596-
baton->withMetadata = sharp::AttrAsBool(options, "withMetadata");
1605+
baton->keepMetadata = sharp::AttrAsUint32(options, "keepMetadata");
15971606
baton->withMetadataOrientation = sharp::AttrAsUint32(options, "withMetadataOrientation");
15981607
baton->withMetadataDensity = sharp::AttrAsDouble(options, "withMetadataDensity");
1599-
baton->withMetadataIcc = sharp::AttrAsStr(options, "withMetadataIcc");
1600-
Napi::Object mdStrs = options.Get("withMetadataStrs").As<Napi::Object>();
1601-
Napi::Array mdStrKeys = mdStrs.GetPropertyNames();
1602-
for (unsigned int i = 0; i < mdStrKeys.Length(); i++) {
1603-
std::string k = sharp::AttrAsStr(mdStrKeys, i);
1604-
if (mdStrs.HasOwnProperty(k)) {
1605-
baton->withMetadataStrs.insert(std::make_pair(k, sharp::AttrAsStr(mdStrs, k)));
1608+
baton->withIccProfile = sharp::AttrAsStr(options, "withIccProfile");
1609+
Napi::Object withExif = options.Get("withExif").As<Napi::Object>();
1610+
Napi::Array withExifKeys = withExif.GetPropertyNames();
1611+
for (unsigned int i = 0; i < withExifKeys.Length(); i++) {
1612+
std::string k = sharp::AttrAsStr(withExifKeys, i);
1613+
if (withExif.HasOwnProperty(k)) {
1614+
baton->withExif.insert(std::make_pair(k, sharp::AttrAsStr(withExif, k)));
16061615
}
16071616
}
1617+
baton->withExifMerge = sharp::AttrAsBool(options, "withExifMerge");
16081618
baton->timeoutSeconds = sharp::AttrAsUint32(options, "timeoutSeconds");
16091619
// Format-specific
16101620
baton->jpegQuality = sharp::AttrAsUint32(options, "jpegQuality");

‎src/pipeline.h

+6-4
Original file line numberDiff line numberDiff line change
@@ -187,11 +187,12 @@ struct PipelineBaton {
187187
bool jxlLossless;
188188
VipsBandFormat rawDepth;
189189
std::string err;
190-
bool withMetadata;
190+
int keepMetadata;
191191
int withMetadataOrientation;
192192
double withMetadataDensity;
193-
std::string withMetadataIcc;
194-
std::unordered_map<std::string, std::string> withMetadataStrs;
193+
std::string withIccProfile;
194+
std::unordered_map<std::string, std::string> withExif;
195+
bool withExifMerge;
195196
int timeoutSeconds;
196197
std::unique_ptr<double[]> convKernel;
197198
int convKernelWidth;
@@ -353,9 +354,10 @@ struct PipelineBaton {
353354
jxlEffort(7),
354355
jxlLossless(false),
355356
rawDepth(VIPS_FORMAT_UCHAR),
356-
withMetadata(false),
357+
keepMetadata(0),
357358
withMetadataOrientation(-1),
358359
withMetadataDensity(0.0),
360+
withExifMerge(true),
359361
timeoutSeconds(0),
360362
convKernelWidth(0),
361363
convKernelHeight(0),

‎test/types/sharp.test-d.ts

+15-3
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,18 @@ sharp('input.tiff').webp({ preset: 'drawing' }).toFile('out.webp');
659659
sharp('input.tiff').webp({ preset: 'text' }).toFile('out.webp');
660660
sharp('input.tiff').webp({ preset: 'default' }).toFile('out.webp');
661661

662-
// Allow a boolean or an object for metadata options.
663-
// https://github.com/lovell/sharp/issues/3822
664-
sharp(input).withMetadata().withMetadata({}).withMetadata(false);
662+
sharp(input)
663+
.keepExif()
664+
.withExif({
665+
IFD0: {
666+
k1: 'v1'
667+
}
668+
})
669+
.withExifMerge({
670+
IFD1: {
671+
k2: 'v2'
672+
}
673+
})
674+
.keepIccProfile()
675+
.withIccProfile('filename')
676+
.withIccProfile('filename', { attach: false });

‎test/unit/metadata.js

+153-38
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ const icc = require('icc');
1111
const sharp = require('../../');
1212
const fixtures = require('../fixtures');
1313

14+
const create = { width: 1, height: 1, channels: 3, background: 'red' };
15+
1416
describe('Image metadata', function () {
1517
it('JPEG', function (done) {
1618
sharp(fixtures.inputJpg).metadata(function (err, metadata) {
@@ -552,11 +554,55 @@ describe('Image metadata', function () {
552554
});
553555
});
554556

557+
it('keep existing ICC profile', async () => {
558+
const data = await sharp(fixtures.inputJpgWithExif)
559+
.keepIccProfile()
560+
.toBuffer();
561+
562+
const metadata = await sharp(data).metadata();
563+
const { description } = icc.parse(metadata.icc);
564+
assert.strictEqual(description, 'Generic RGB Profile');
565+
});
566+
567+
it('keep existing ICC profile, ignore colourspace conversion', async () => {
568+
const data = await sharp(fixtures.inputJpgWithExif)
569+
.keepIccProfile()
570+
.toColourspace('cmyk')
571+
.toBuffer();
572+
573+
const metadata = await sharp(data).metadata();
574+
assert.strictEqual(metadata.channels, 3);
575+
const { description } = icc.parse(metadata.icc);
576+
assert.strictEqual(description, 'Generic RGB Profile');
577+
});
578+
579+
it('transform to ICC profile and attach', async () => {
580+
const data = await sharp({ create })
581+
.png()
582+
.withIccProfile('p3', { attach: true })
583+
.toBuffer();
584+
585+
const metadata = await sharp(data).metadata();
586+
const { description } = icc.parse(metadata.icc);
587+
assert.strictEqual(description, 'sP3C');
588+
});
589+
590+
it('transform to ICC profile but do not attach', async () => {
591+
const data = await sharp({ create })
592+
.png()
593+
.withIccProfile('p3', { attach: false })
594+
.toBuffer();
595+
596+
const metadata = await sharp(data).metadata();
597+
assert.strictEqual(3, metadata.channels);
598+
assert.strictEqual(undefined, metadata.icc);
599+
});
600+
555601
it('Apply CMYK output ICC profile', function (done) {
556602
const output = fixtures.path('output.icc-cmyk.jpg');
557603
sharp(fixtures.inputJpg)
558604
.resize(64)
559-
.withMetadata({ icc: 'cmyk' })
605+
.withIccProfile('cmyk')
560606
.toFile(output, function (err) {
561607
if (err) throw err;
562608
sharp(output).metadata(function (err, metadata) {
@@ -581,7 +627,7 @@ describe('Image metadata', function () {
581627
const output = fixtures.path('output.hilutite.jpg');
582628
sharp(fixtures.inputJpg)
583629
.resize(64)
584-
.withMetadata({ icc: fixtures.path('hilutite.icm') })
630+
.withIccProfile(fixtures.path('hilutite.icm'))
585631
.toFile(output, function (err, info) {
586632
if (err) throw err;
587633
fixtures.assertMaxColourDistance(output, fixtures.expected('hilutite.jpg'), 9);
@@ -620,7 +666,6 @@ describe('Image metadata', function () {
620666
it('Remove EXIF metadata after a resize', function (done) {
621667
sharp(fixtures.inputJpgWithExif)
622668
.resize(320, 240)
623-
.withMetadata(false)
624669
.toBuffer(function (err, buffer) {
625670
if (err) throw err;
626671
sharp(buffer).metadata(function (err, metadata) {
@@ -651,14 +696,7 @@ describe('Image metadata', function () {
651696
});
652697

653698
it('Add EXIF metadata to JPEG', async () => {
654-
const data = await sharp({
655-
create: {
656-
width: 8,
657-
height: 8,
658-
channels: 3,
659-
background: 'red'
660-
}
661-
})
699+
const data = await sharp({ create })
662700
.jpeg()
663701
.withMetadata({
664702
exif: {
@@ -675,14 +713,7 @@ describe('Image metadata', function () {
675713
});
676714

677715
it('Set density of JPEG', async () => {
678-
const data = await sharp({
679-
create: {
680-
width: 8,
681-
height: 8,
682-
channels: 3,
683-
background: 'red'
684-
}
685-
})
716+
const data = await sharp({ create })
686717
.withMetadata({
687718
density: 300
688719
})
@@ -694,14 +725,7 @@ describe('Image metadata', function () {
694725
});
695726

696727
it('Set density of PNG', async () => {
697-
const data = await sharp({
698-
create: {
699-
width: 8,
700-
height: 8,
701-
channels: 3,
702-
background: 'red'
703-
}
704-
})
728+
const data = await sharp({ create })
705729
.withMetadata({
706730
density: 96
707731
})
@@ -809,11 +833,7 @@ describe('Image metadata', function () {
809833
});
810834

811835
it('withMetadata adds default sRGB profile to RGB16', async () => {
812-
const data = await sharp({
813-
create: {
814-
width: 8, height: 8, channels: 4, background: 'orange'
815-
}
816-
})
836+
const data = await sharp({ create })
817837
.toColorspace('rgb16')
818838
.png()
819839
.withMetadata()
@@ -827,11 +847,7 @@ describe('Image metadata', function () {
827847
});
828848

829849
it('withMetadata adds P3 profile to 16-bit PNG', async () => {
830-
const data = await sharp({
831-
create: {
832-
width: 8, height: 8, channels: 4, background: 'orange'
833-
}
834-
})
850+
const data = await sharp({ create })
835851
.toColorspace('rgb16')
836852
.png()
837853
.withMetadata({ icc: 'p3' })
@@ -871,7 +887,89 @@ describe('Image metadata', function () {
871887
});
872888
});
873889

874-
describe('Invalid withMetadata parameters', function () {
890+
it('keepExif maintains all EXIF metadata', async () => {
891+
const data1 = await sharp({ create })
892+
.withExif({
893+
IFD0: {
894+
Copyright: 'Test 1',
895+
Software: 'sharp'
896+
}
897+
})
898+
.jpeg()
899+
.toBuffer();
900+
901+
const data2 = await sharp(data1)
902+
.keepExif()
903+
.toBuffer();
904+
905+
const md2 = await sharp(data2).metadata();
906+
const exif2 = exifReader(md2.exif);
907+
assert.strictEqual(exif2.Image.Copyright, 'Test 1');
908+
assert.strictEqual(exif2.Image.Software, 'sharp');
909+
});
910+
911+
it('withExif replaces all EXIF metadata', async () => {
912+
const data1 = await sharp({ create })
913+
.withExif({
914+
IFD0: {
915+
Copyright: 'Test 1',
916+
Software: 'sharp'
917+
}
918+
})
919+
.jpeg()
920+
.toBuffer();
921+
922+
const md1 = await sharp(data1).metadata();
923+
const exif1 = exifReader(md1.exif);
924+
assert.strictEqual(exif1.Image.Copyright, 'Test 1');
925+
assert.strictEqual(exif1.Image.Software, 'sharp');
926+
927+
const data2 = await sharp(data1)
928+
.withExif({
929+
IFD0: {
930+
Copyright: 'Test 2'
931+
}
932+
})
933+
.toBuffer();
934+
935+
const md2 = await sharp(data2).metadata();
936+
const exif2 = exifReader(md2.exif);
937+
assert.strictEqual(exif2.Image.Copyright, 'Test 2');
938+
assert.strictEqual(exif2.Image.Software, undefined);
939+
});
940+
941+
it('withExifMerge merges all EXIF metadata', async () => {
942+
const data1 = await sharp({ create })
943+
.withExif({
944+
IFD0: {
945+
Copyright: 'Test 1'
946+
}
947+
})
948+
.jpeg()
949+
.toBuffer();
950+
951+
const md1 = await sharp(data1).metadata();
952+
const exif1 = exifReader(md1.exif);
953+
assert.strictEqual(exif1.Image.Copyright, 'Test 1');
954+
assert.strictEqual(exif1.Image.Software, undefined);
955+
956+
const data2 = await sharp(data1)
957+
.withExifMerge({
958+
IFD0: {
959+
Copyright: 'Test 2',
960+
Software: 'sharp'
961+
962+
}
963+
})
964+
.toBuffer();
965+
966+
const md2 = await sharp(data2).metadata();
967+
const exif2 = exifReader(md2.exif);
968+
assert.strictEqual(exif2.Image.Copyright, 'Test 2');
969+
assert.strictEqual(exif2.Image.Software, 'sharp');
970+
});
971+
972+
describe('Invalid parameters', function () {
875973
it('String orientation', function () {
876974
assert.throws(function () {
877975
sharp().withMetadata({ orientation: 'zoinks' });
@@ -922,5 +1020,22 @@ describe('Image metadata', function () {
9221020
sharp().withMetadata({ exif: { ifd0: { fail: false } } });
9231021
});
9241022
});
1023+
it('withIccProfile invalid profile', () => {
1024+
assert.throws(
1025+
() => sharp().withIccProfile(false),
1026+
/Expected string for icc but received false of type boolean/
1027+
);
1028+
});
1029+
it('withIccProfile missing attach', () => {
1030+
assert.doesNotThrow(
1031+
() => sharp().withIccProfile('test', {})
1032+
);
1033+
});
1034+
it('withIccProfile invalid attach', () => {
1035+
assert.throws(
1036+
() => sharp().withIccProfile('test', { attach: 1 }),
1037+
/Expected boolean for attach but received 1 of type number/
1038+
);
1039+
});
9251040
});
9261041
});

0 commit comments

Comments
 (0)
Please sign in to comment.