Allow override of EXIF Orientation tag #189

Clear Orientation when rotate/flip/flop are used
This commit is contained in:
Lovell Fuller 2015-07-13 20:00:33 +01:00
parent 642e5687b6
commit d303703dc5
7 changed files with 130 additions and 11 deletions

View File

@ -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])

View File

@ -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;
};

View File

@ -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.

View File

@ -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.

View File

@ -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<String>("overshootDeringing"))->BooleanValue();
baton->optimiseScans = options->Get(NanNew<String>("optimiseScans"))->BooleanValue();
baton->withMetadata = options->Get(NanNew<String>("withMetadata"))->BooleanValue();
baton->withMetadataOrientation = options->Get(NanNew<String>("withMetadataOrientation"))->Int32Value();
// Output filename or __format for Buffer
baton->output = *String::Utf8Value(options->Get(NanNew<String>("output"))->ToString());
baton->tileSize = options->Get(NanNew<String>("tileSize"))->Int32Value();

View File

@ -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});
});
});
});
});

View File

@ -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);
});
});
});