mirror of
https://github.com/lovell/sharp.git
synced 2025-07-09 18:40:16 +02:00
Allow EXIF metadata to be set/update #650
This commit is contained in:
parent
43a085d1ae
commit
bc60daff9e
@ -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**
|
||||
|
@ -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)
|
||||
|
||||
|
@ -232,6 +232,7 @@ const Sharp = function (input, options) {
|
||||
withMetadata: false,
|
||||
withMetadataOrientation: -1,
|
||||
withMetadataIcc: '',
|
||||
withMetadataStrs: {},
|
||||
resolveWithObject: false,
|
||||
// output format
|
||||
jpegQuality: 80,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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");
|
||||
|
@ -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;
|
||||
|
@ -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 } } });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user