Allow EXIF metadata to be set/update #650

This commit is contained in:
Lovell Fuller 2021-04-05 11:39:53 +01:00
parent 43a085d1ae
commit bc60daff9e
9 changed files with 108 additions and 1 deletions

View File

@ -119,6 +119,7 @@ sRGB colour space and strip all metadata, including the removal of any ICC profi
- `options` **[Object][6]?**
- `options.orientation` **[number][9]?** value between 1 and 8, used to update the EXIF `Orientation` tag.
- `options.icc` **[string][2]?** filesystem path to output ICC profile, defaults to sRGB.
- `options.exif` **[Object][6]<[Object][6]>** Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data. (optional, default `{}`)
### Examples
@ -129,6 +130,19 @@ sharp('input.jpg')
.then(info => { ... });
```
```javascript
// Set "IFD0-Copyright" in output EXIF metadata
await sharp(input)
.withMetadata({
exif: {
IFD0: {
Copyright: 'Wernham Hogg'
}
}
})
.toBuffer();
```
- Throws **[Error][4]** Invalid parameters
Returns **Sharp**

View File

@ -8,6 +8,9 @@ Requires libvips v8.10.6
* Ensure all installation errors are logged with a more obvious prefix.
* Allow `withMetadata` to set and update EXIF metadata.
[#650](https://github.com/lovell/sharp/issues/650)
* Add support for OME-TIFF Sub Image File Directories (subIFD).
[#2557](https://github.com/lovell/sharp/issues/2557)

View File

@ -232,6 +232,7 @@ const Sharp = function (input, options) {
withMetadata: false,
withMetadataOrientation: -1,
withMetadataIcc: '',
withMetadataStrs: {},
resolveWithObject: false,
// output format
jpegQuality: 80,

View File

@ -148,9 +148,22 @@ function toBuffer (options, callback) {
* .toFile('output-with-metadata.jpg')
* .then(info => { ... });
*
* @example
* // Set "IFD0-Copyright" in output EXIF metadata
* await sharp(input)
* .withMetadata({
* exif: {
* IFD0: {
* Copyright: 'Wernham Hogg'
* }
* }
* })
* .toBuffer();
*
* @param {Object} [options]
* @param {number} [options.orientation] value between 1 and 8, used to update the EXIF `Orientation` tag.
* @param {string} [options.icc] filesystem path to output ICC profile, defaults to sRGB.
* @param {Object<Object>} [options.exif={}] Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data.
* @returns {Sharp}
* @throws {Error} Invalid parameters
*/
@ -171,6 +184,25 @@ function withMetadata (options) {
throw is.invalidParameterError('icc', 'string filesystem path to ICC profile', 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);
}
}
}
return this;
}

View File

@ -36,6 +36,9 @@ namespace sharp {
std::string AttrAsStr(Napi::Object obj, std::string attr) {
return obj.Get(attr).As<Napi::String>();
}
std::string AttrAsStr(Napi::Object obj, unsigned int const attr) {
return obj.Get(attr).As<Napi::String>();
}
uint32_t AttrAsUint32(Napi::Object obj, std::string attr) {
return obj.Get(attr).As<Napi::Number>().Uint32Value();
}

View File

@ -95,6 +95,7 @@ namespace sharp {
// Convenience methods to access the attributes of a Napi::Object
bool HasAttr(Napi::Object obj, std::string attr);
std::string AttrAsStr(Napi::Object obj, std::string attr);
std::string AttrAsStr(Napi::Object obj, unsigned int const attr);
uint32_t AttrAsUint32(Napi::Object obj, std::string attr);
int32_t AttrAsInt32(Napi::Object obj, std::string attr);
int32_t AttrAsInt32(Napi::Object obj, unsigned int const attr);

View File

@ -717,11 +717,17 @@ class PipelineWorker : public Napi::AsyncWorker {
->set("input_profile", "srgb")
->set("intent", VIPS_INTENT_PERCEPTUAL));
}
// Override EXIF Orientation tag
if (baton->withMetadata && baton->withMetadataOrientation != -1) {
image = sharp::SetExifOrientation(image, baton->withMetadataOrientation);
}
// Metadata key/value pairs, e.g. EXIF
if (!baton->withMetadataStrs.empty()) {
image = image.copy();
for (const auto& s : baton->withMetadataStrs) {
image.set(s.first.data(), s.second.data());
}
}
// Number of channels used in output image
baton->channels = image.bands();
@ -1379,6 +1385,12 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
baton->withMetadata = sharp::AttrAsBool(options, "withMetadata");
baton->withMetadataOrientation = sharp::AttrAsUint32(options, "withMetadataOrientation");
baton->withMetadataIcc = sharp::AttrAsStr(options, "withMetadataIcc");
Napi::Object mdStrs = options.Get("withMetadataStrs").As<Napi::Object>();
Napi::Array mdStrKeys = mdStrs.GetPropertyNames();
for (unsigned int i = 0; i < mdStrKeys.Length(); i++) {
std::string k = sharp::AttrAsStr(mdStrKeys, i);
baton->withMetadataStrs.insert(std::make_pair(k, sharp::AttrAsStr(mdStrs, k)));
}
// Format-specific
baton->jpegQuality = sharp::AttrAsUint32(options, "jpegQuality");
baton->jpegProgressive = sharp::AttrAsBool(options, "jpegProgressive");

View File

@ -18,6 +18,7 @@
#include <memory>
#include <string>
#include <vector>
#include <unordered_map>
#include <napi.h>
#include <vips/vips8>
@ -168,6 +169,7 @@ struct PipelineBaton {
bool withMetadata;
int withMetadataOrientation;
std::string withMetadataIcc;
std::unordered_map<std::string, std::string> withMetadataStrs;
std::unique_ptr<double[]> convKernel;
int convKernelWidth;
int convKernelHeight;

View File

@ -599,6 +599,30 @@ describe('Image metadata', function () {
});
});
it('Add EXIF metadata to JPEG', async () => {
const data = await sharp({
create: {
width: 8,
height: 8,
channels: 3,
background: 'red'
}
})
.jpeg()
.withMetadata({
exif: {
IFD0: { Software: 'sharp' },
IFD2: { ExposureTime: '0.2' }
}
})
.toBuffer();
const { exif } = await sharp(data).metadata();
const parsedExif = exifReader(exif);
assert.strictEqual(parsedExif.image.Software, 'sharp');
assert.strictEqual(parsedExif.exif.ExposureTime, 0.2);
});
it('chromaSubsampling 4:4:4:4 CMYK JPEG', function () {
return sharp(fixtures.inputJpgWithCmykProfile)
.metadata()
@ -717,5 +741,20 @@ describe('Image metadata', function () {
sharp().withMetadata({ icc: true });
});
});
it('Non object exif', function () {
assert.throws(function () {
sharp().withMetadata({ exif: false });
});
});
it('Non string value in object exif', function () {
assert.throws(function () {
sharp().withMetadata({ exif: { ifd0: false } });
});
});
it('Non string value in nested object exif', function () {
assert.throws(function () {
sharp().withMetadata({ exif: { ifd0: { fail: false } } });
});
});
});
});