Skip to content

Commit

Permalink
Allow EXIF metadata to be set/update #650
Browse files Browse the repository at this point in the history
  • Loading branch information
lovell committed Apr 5, 2021
1 parent 43a085d commit bc60daf
Show file tree
Hide file tree
Showing 9 changed files with 108 additions and 1 deletion.
14 changes: 14 additions & 0 deletions docs/api-output.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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**
Expand Down
3 changes: 3 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions lib/constructor.js
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ const Sharp = function (input, options) {
withMetadata: false,
withMetadataOrientation: -1,
withMetadataIcc: '',
withMetadataStrs: {},
resolveWithObject: false,
// output format
jpegQuality: 80,
Expand Down
32 changes: 32 additions & 0 deletions lib/output.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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;
}
Expand Down
3 changes: 3 additions & 0 deletions src/common.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
1 change: 1 addition & 0 deletions src/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
14 changes: 13 additions & 1 deletion src/pipeline.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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");
Expand Down
2 changes: 2 additions & 0 deletions src/pipeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include <memory>
#include <string>
#include <vector>
#include <unordered_map>

#include <napi.h>
#include <vips/vips8>
Expand Down Expand Up @@ -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;
Expand Down
39 changes: 39 additions & 0 deletions test/unit/metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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 } } });
});
});
});
});

0 comments on commit bc60daf

Please sign in to comment.