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
13 changed files with 694 additions and 179 deletions

View File

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

73
lib/index.d.ts vendored
View File

@@ -633,6 +633,43 @@ declare namespace sharp {
*/
toBuffer(options: { resolveWithObject: true }): Promise<{ data: Buffer; info: OutputInfo }>;
/**
* Keep all EXIF metadata from the input image in the output image.
* EXIF metadata is unsupported for TIFF output.
* @returns A sharp instance that can be used to chain operations
*/
keepExif(): Sharp;
/**
* Set EXIF metadata in the output image, ignoring any EXIF in the input image.
* @param {Exif} exif Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data.
* @returns A sharp instance that can be used to chain operations
* @throws {Error} Invalid parameters
*/
withExif(exif: Exif): Sharp;
/**
* Update EXIF metadata from the input image in the output image.
* @param {Exif} exif Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data.
* @returns A sharp instance that can be used to chain operations
* @throws {Error} Invalid parameters
*/
withExifMerge(exif: Exif): Sharp;
/**
* Keep ICC profile from the input image in the output image where possible.
* @returns A sharp instance that can be used to chain operations
*/
keepIccProfile(): Sharp;
/**
* Transform using an ICC profile and attach to the output image.
* @param {string} icc - Absolute filesystem path to output ICC profile or built-in profile name (srgb, p3, cmyk).
* @returns A sharp instance that can be used to chain operations
* @throws {Error} Invalid parameters
*/
withIccProfile(icc: string, options?: WithIccProfileOptions): Sharp;
/**
* Include all metadata (EXIF, XMP, IPTC) from the input image in the output image.
* The default behaviour, when withMetadata is not used, is to strip all metadata and convert to the device-independent sRGB colour space.
@@ -640,7 +677,7 @@ declare namespace sharp {
* @param withMetadata
* @throws {Error} Invalid parameters.
*/
withMetadata(withMetadata?: boolean | WriteableMetadata): Sharp;
withMetadata(withMetadata?: WriteableMetadata): Sharp;
/**
* Use these JPEG options for output image.
@@ -978,15 +1015,32 @@ declare namespace sharp {
wrap?: TextWrap;
}
interface ExifDir {
[k: string]: string;
}
interface Exif {
'IFD0'?: ExifDir;
'IFD1'?: ExifDir;
'IFD2'?: ExifDir;
'IFD3'?: ExifDir;
}
interface WriteableMetadata {
/** 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) */
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 {
@@ -1096,6 +1150,11 @@ declare namespace sharp {
force?: boolean | undefined;
}
interface WithIccProfileOptions {
/** Should the ICC profile be included in the output image metadata? (optional, default true) */
attach?: boolean | undefined;
}
interface JpegOptions extends OutputOptions {
/** Quality, integer 1-100 (optional, default 80) */
quality?: number | undefined;

View File

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