diff --git a/README.md b/README.md index 0bb050e2..bc730f68 100755 --- a/README.md +++ b/README.md @@ -564,11 +564,18 @@ The output quality to use for lossy JPEG, WebP and TIFF output formats. The defa Use progressive (interlace) scan for JPEG and PNG output. This typically reduces compression performance by 30% but results in an image that can be rendered sooner when decompressed. -#### withMetadata() +#### withMetadata([metadata]) -Include all metadata (EXIF, XMP, IPTC) from the input image in the output image. This will also convert to and add the latest web-friendly v2 sRGB ICC profile. +Include all metadata (EXIF, XMP, IPTC) from the input image in the output image. +This will also convert to and add the latest web-friendly v2 sRGB ICC profile. -The default behaviour is to strip all metadata and convert to the device-independent sRGB colour space. +The optional `metadata` parameter, if present, is an Object with the attributes to update. +New attributes cannot be inserted, only existing attributes updated. + +* `orientation` is an integral Number between 0 and 7, used to update the value of the EXIF `Orientation` tag. +This has no effect if the input image does not have an EXIF `Orientation` tag. + +The default behaviour, when `withMetadata` is not used, is to strip all metadata and convert to the device-independent sRGB colour space. #### tile([size], [overlap]) diff --git a/index.js b/index.js index 8b1be523..1259dd38 100755 --- a/index.js +++ b/index.js @@ -74,6 +74,7 @@ var Sharp = function(input) { optimiseScans: false, streamOut: false, withMetadata: false, + withMetadataOrientation: -1, tileSize: 256, tileOverlap: 0, // Function to notify of queue length changes @@ -479,9 +480,26 @@ Sharp.prototype.optimizeScans = Sharp.prototype.optimiseScans; /* Include all metadata (EXIF, XMP, IPTC) from the input image in the output image + Optionally provide an Object with attributes to update: + orientation: numeric value for EXIF Orientation tag */ Sharp.prototype.withMetadata = function(withMetadata) { this.options.withMetadata = (typeof withMetadata === 'boolean') ? withMetadata : true; + if (typeof withMetadata === 'object') { + if ('orientation' in withMetadata) { + if ( + typeof withMetadata.orientation === 'number' && + !Number.isNaN(withMetadata.orientation) && + withMetadata.orientation % 1 === 0 && + withMetadata.orientation >=0 && + withMetadata.orientation <= 7 + ) { + this.options.withMetadataOrientation = withMetadata.orientation; + } else { + throw new Error('Invalid orientation (0 to 7) ' + withMetadata.orientation); + } + } + } return this; }; diff --git a/src/common.cc b/src/common.cc index 8d5565e3..c5b2d86a 100755 --- a/src/common.cc +++ b/src/common.cc @@ -25,6 +25,8 @@ #endif #endif +#define EXIF_IFD0_ORIENTATION "exif-ifd0-Orientation" + namespace sharp { // How many tasks are in the queue? @@ -141,14 +143,30 @@ namespace sharp { int orientation = 0; const char *exif; if ( - vips_image_get_typeof(image, "exif-ifd0-Orientation") != 0 && - !vips_image_get_string(image, "exif-ifd0-Orientation", &exif) + vips_image_get_typeof(image, EXIF_IFD0_ORIENTATION) != 0 && + !vips_image_get_string(image, EXIF_IFD0_ORIENTATION, &exif) ) { orientation = atoi(&exif[0]); } return orientation; } + /* + Set EXIF Orientation of image. + */ + void SetExifOrientation(VipsImage *image, int const orientation) { + char exif[3]; + g_snprintf(exif, sizeof(exif), "%d", orientation); + vips_image_set_string(image, EXIF_IFD0_ORIENTATION, exif); + } + + /* + Remove EXIF Orientation from image. + */ + void RemoveExifOrientation(VipsImage *image) { + vips_image_remove(image, EXIF_IFD0_ORIENTATION); + } + /* Returns the window size for the named interpolator. For example, a window size of 3 means a 3x3 pixel grid is used for the calculation. diff --git a/src/common.h b/src/common.h index 33f63a32..09296a54 100755 --- a/src/common.h +++ b/src/common.h @@ -64,6 +64,16 @@ namespace sharp { */ int ExifOrientation(VipsImage const *image); + /* + Set EXIF Orientation of image. + */ + void SetExifOrientation(VipsImage *image, int const orientation); + + /* + Remove EXIF Orientation from image. + */ + void RemoveExifOrientation(VipsImage *image); + /* Returns the window size for the named interpolator. For example, a window size of 3 means a 3x3 pixel grid is used for the calculation. diff --git a/src/pipeline.cc b/src/pipeline.cc index 21b7334d..e0e2ce08 100755 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -36,6 +36,8 @@ using sharp::InterpolatorWindowSize; using sharp::HasProfile; using sharp::HasAlpha; using sharp::ExifOrientation; +using sharp::SetExifOrientation; +using sharp::RemoveExifOrientation; using sharp::IsJpeg; using sharp::IsPng; using sharp::IsWebp; @@ -109,6 +111,7 @@ struct PipelineBaton { bool optimiseScans; std::string err; bool withMetadata; + int withMetadataOrientation; int tileSize; int tileOverlap; @@ -142,6 +145,7 @@ struct PipelineBaton { overshootDeringing(false), optimiseScans(false), withMetadata(false), + withMetadataOrientation(-1), tileSize(256), tileOverlap(0) { background[0] = 0.0; @@ -246,6 +250,7 @@ class PipelineWorker : public NanAsyncWorker { } vips_object_local(hook, rotated); image = rotated; + RemoveExifOrientation(image); } // Pre extraction @@ -563,6 +568,7 @@ class PipelineWorker : public NanAsyncWorker { } vips_object_local(hook, rotated); image = rotated; + RemoveExifOrientation(image); } // Flip (mirror about Y axis) @@ -573,6 +579,7 @@ class PipelineWorker : public NanAsyncWorker { } vips_object_local(hook, flipped); image = flipped; + RemoveExifOrientation(image); } // Flop (mirror about X axis) @@ -583,6 +590,7 @@ class PipelineWorker : public NanAsyncWorker { } vips_object_local(hook, flopped); image = flopped; + RemoveExifOrientation(image); } // Crop/embed @@ -801,6 +809,11 @@ class PipelineWorker : public NanAsyncWorker { } } + // Override EXIF Orientation tag + if (baton->withMetadata && baton->withMetadataOrientation != -1) { + SetExifOrientation(image, baton->withMetadataOrientation); + } + #if !(VIPS_MAJOR_VERSION >= 8 || (VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 40 && VIPS_MINOR_VERSION >= 5)) // Generate image tile cache when interlace output is required - no longer required as of libvips 7.40.5+ if (baton->progressive) { @@ -1194,6 +1207,7 @@ NAN_METHOD(pipeline) { baton->overshootDeringing = options->Get(NanNew("overshootDeringing"))->BooleanValue(); baton->optimiseScans = options->Get(NanNew("optimiseScans"))->BooleanValue(); baton->withMetadata = options->Get(NanNew("withMetadata"))->BooleanValue(); + baton->withMetadataOrientation = options->Get(NanNew("withMetadataOrientation"))->Int32Value(); // Output filename or __format for Buffer baton->output = *String::Utf8Value(options->Get(NanNew("output"))->ToString()); baton->tileSize = options->Get(NanNew("tileSize"))->Int32Value(); diff --git a/test/unit/metadata.js b/test/unit/metadata.js index 2ccfa196..595502aa 100755 --- a/test/unit/metadata.js +++ b/test/unit/metadata.js @@ -312,4 +312,21 @@ describe('Image metadata', function() { }); }); + describe('Invalid withMetadata parameters', function() { + it('String orientation', function() { + assert.throws(function() { + sharp().withMetadata({orientation: 'zoinks'}); + }); + }); + it('Negative orientation', function() { + assert.throws(function() { + sharp().withMetadata({orientation: -1}); + }); + }); + it('Too large orientation', function() { + assert.throws(function() { + sharp().withMetadata({orientation: 8}); + }); + }); + }); }); diff --git a/test/unit/rotate.js b/test/unit/rotate.js index 98c888fa..17fcdf96 100755 --- a/test/unit/rotate.js +++ b/test/unit/rotate.js @@ -23,13 +23,17 @@ describe('Rotation', function() { it('Input image has Orientation EXIF tag but do not rotate output', function(done) { sharp(fixtures.inputJpgWithExif) .resize(320) + .withMetadata() .toBuffer(function(err, data, info) { if (err) throw err; assert.strictEqual(true, data.length > 0); assert.strictEqual('jpeg', info.format); assert.strictEqual(320, info.width); assert.strictEqual(427, info.height); - done(); + sharp(data).metadata(function(err, metadata) { + assert.strictEqual(8, metadata.orientation); + done(); + }); }); }); @@ -46,16 +50,37 @@ describe('Rotation', function() { }); }); - it('Input image has Orientation EXIF tag value of 5 (270 degrees + flip), auto-rotate', function(done) { - sharp(fixtures.inputJpgWithExifMirroring) + it('Override EXIF Orientation tag metadata after auto-rotate', function(done) { + sharp(fixtures.inputJpgWithExif) .rotate() .resize(320) + .withMetadata({orientation: 3}) .toBuffer(function(err, data, info) { if (err) throw err; assert.strictEqual('jpeg', info.format); assert.strictEqual(320, info.width); assert.strictEqual(240, info.height); - fixtures.assertSimilar(fixtures.expected('exif-5.jpg'), data, done); + sharp(data).metadata(function(err, metadata) { + assert.strictEqual(3, metadata.orientation); + fixtures.assertSimilar(fixtures.expected('exif-8.jpg'), data, done); + }); + }); + }); + + it('Input image has Orientation EXIF tag value of 5 (270 degrees + flip), auto-rotate', function(done) { + sharp(fixtures.inputJpgWithExifMirroring) + .rotate() + .resize(320) + .withMetadata() + .toBuffer(function(err, data, info) { + if (err) throw err; + assert.strictEqual('jpeg', info.format); + assert.strictEqual(320, info.width); + assert.strictEqual(240, info.height); + sharp(data).metadata(function(err, metadata) { + assert.strictEqual(false, 'orientation' in metadata); + fixtures.assertSimilar(fixtures.expected('exif-5.jpg'), data, done); + }); }); }); @@ -95,12 +120,17 @@ describe('Rotation', function() { sharp(fixtures.inputJpg) .resize(320) .flip() + .withMetadata() .toBuffer(function(err, data, info) { if (err) throw err; assert.strictEqual('jpeg', info.format); assert.strictEqual(320, info.width); assert.strictEqual(261, info.height); - fixtures.assertSimilar(fixtures.expected('flip.jpg'), data, done); + sharp(data).metadata(function(err, metadata) { + if (err) throw err; + assert.strictEqual(false, 'orientation' in metadata); + fixtures.assertSimilar(fixtures.expected('flip.jpg'), data, done); + }); }); }); @@ -108,12 +138,17 @@ describe('Rotation', function() { sharp(fixtures.inputJpg) .resize(320) .flop() + .withMetadata() .toBuffer(function(err, data, info) { if (err) throw err; assert.strictEqual('jpeg', info.format); assert.strictEqual(320, info.width); assert.strictEqual(261, info.height); - fixtures.assertSimilar(fixtures.expected('flop.jpg'), data, done); + sharp(data).metadata(function(err, metadata) { + if (err) throw err; + assert.strictEqual(false, 'orientation' in metadata); + fixtures.assertSimilar(fixtures.expected('flop.jpg'), data, done); + }); }); });