mirror of
https://github.com/lovell/sharp.git
synced 2025-07-09 10:30:15 +02:00
Allow override of EXIF Orientation tag #189
Clear Orientation when rotate/flip/flop are used
This commit is contained in:
parent
642e5687b6
commit
d303703dc5
13
README.md
13
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.
|
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])
|
#### tile([size], [overlap])
|
||||||
|
|
||||||
|
18
index.js
18
index.js
@ -74,6 +74,7 @@ var Sharp = function(input) {
|
|||||||
optimiseScans: false,
|
optimiseScans: false,
|
||||||
streamOut: false,
|
streamOut: false,
|
||||||
withMetadata: false,
|
withMetadata: false,
|
||||||
|
withMetadataOrientation: -1,
|
||||||
tileSize: 256,
|
tileSize: 256,
|
||||||
tileOverlap: 0,
|
tileOverlap: 0,
|
||||||
// Function to notify of queue length changes
|
// 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
|
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) {
|
Sharp.prototype.withMetadata = function(withMetadata) {
|
||||||
this.options.withMetadata = (typeof withMetadata === 'boolean') ? withMetadata : true;
|
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;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -25,6 +25,8 @@
|
|||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#define EXIF_IFD0_ORIENTATION "exif-ifd0-Orientation"
|
||||||
|
|
||||||
namespace sharp {
|
namespace sharp {
|
||||||
|
|
||||||
// How many tasks are in the queue?
|
// How many tasks are in the queue?
|
||||||
@ -141,14 +143,30 @@ namespace sharp {
|
|||||||
int orientation = 0;
|
int orientation = 0;
|
||||||
const char *exif;
|
const char *exif;
|
||||||
if (
|
if (
|
||||||
vips_image_get_typeof(image, "exif-ifd0-Orientation") != 0 &&
|
vips_image_get_typeof(image, EXIF_IFD0_ORIENTATION) != 0 &&
|
||||||
!vips_image_get_string(image, "exif-ifd0-Orientation", &exif)
|
!vips_image_get_string(image, EXIF_IFD0_ORIENTATION, &exif)
|
||||||
) {
|
) {
|
||||||
orientation = atoi(&exif[0]);
|
orientation = atoi(&exif[0]);
|
||||||
}
|
}
|
||||||
return orientation;
|
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,
|
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.
|
a window size of 3 means a 3x3 pixel grid is used for the calculation.
|
||||||
|
10
src/common.h
10
src/common.h
@ -64,6 +64,16 @@ namespace sharp {
|
|||||||
*/
|
*/
|
||||||
int ExifOrientation(VipsImage const *image);
|
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,
|
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.
|
a window size of 3 means a 3x3 pixel grid is used for the calculation.
|
||||||
|
@ -36,6 +36,8 @@ using sharp::InterpolatorWindowSize;
|
|||||||
using sharp::HasProfile;
|
using sharp::HasProfile;
|
||||||
using sharp::HasAlpha;
|
using sharp::HasAlpha;
|
||||||
using sharp::ExifOrientation;
|
using sharp::ExifOrientation;
|
||||||
|
using sharp::SetExifOrientation;
|
||||||
|
using sharp::RemoveExifOrientation;
|
||||||
using sharp::IsJpeg;
|
using sharp::IsJpeg;
|
||||||
using sharp::IsPng;
|
using sharp::IsPng;
|
||||||
using sharp::IsWebp;
|
using sharp::IsWebp;
|
||||||
@ -109,6 +111,7 @@ struct PipelineBaton {
|
|||||||
bool optimiseScans;
|
bool optimiseScans;
|
||||||
std::string err;
|
std::string err;
|
||||||
bool withMetadata;
|
bool withMetadata;
|
||||||
|
int withMetadataOrientation;
|
||||||
int tileSize;
|
int tileSize;
|
||||||
int tileOverlap;
|
int tileOverlap;
|
||||||
|
|
||||||
@ -142,6 +145,7 @@ struct PipelineBaton {
|
|||||||
overshootDeringing(false),
|
overshootDeringing(false),
|
||||||
optimiseScans(false),
|
optimiseScans(false),
|
||||||
withMetadata(false),
|
withMetadata(false),
|
||||||
|
withMetadataOrientation(-1),
|
||||||
tileSize(256),
|
tileSize(256),
|
||||||
tileOverlap(0) {
|
tileOverlap(0) {
|
||||||
background[0] = 0.0;
|
background[0] = 0.0;
|
||||||
@ -246,6 +250,7 @@ class PipelineWorker : public NanAsyncWorker {
|
|||||||
}
|
}
|
||||||
vips_object_local(hook, rotated);
|
vips_object_local(hook, rotated);
|
||||||
image = rotated;
|
image = rotated;
|
||||||
|
RemoveExifOrientation(image);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre extraction
|
// Pre extraction
|
||||||
@ -563,6 +568,7 @@ class PipelineWorker : public NanAsyncWorker {
|
|||||||
}
|
}
|
||||||
vips_object_local(hook, rotated);
|
vips_object_local(hook, rotated);
|
||||||
image = rotated;
|
image = rotated;
|
||||||
|
RemoveExifOrientation(image);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flip (mirror about Y axis)
|
// Flip (mirror about Y axis)
|
||||||
@ -573,6 +579,7 @@ class PipelineWorker : public NanAsyncWorker {
|
|||||||
}
|
}
|
||||||
vips_object_local(hook, flipped);
|
vips_object_local(hook, flipped);
|
||||||
image = flipped;
|
image = flipped;
|
||||||
|
RemoveExifOrientation(image);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flop (mirror about X axis)
|
// Flop (mirror about X axis)
|
||||||
@ -583,6 +590,7 @@ class PipelineWorker : public NanAsyncWorker {
|
|||||||
}
|
}
|
||||||
vips_object_local(hook, flopped);
|
vips_object_local(hook, flopped);
|
||||||
image = flopped;
|
image = flopped;
|
||||||
|
RemoveExifOrientation(image);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Crop/embed
|
// 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))
|
#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+
|
// Generate image tile cache when interlace output is required - no longer required as of libvips 7.40.5+
|
||||||
if (baton->progressive) {
|
if (baton->progressive) {
|
||||||
@ -1194,6 +1207,7 @@ NAN_METHOD(pipeline) {
|
|||||||
baton->overshootDeringing = options->Get(NanNew<String>("overshootDeringing"))->BooleanValue();
|
baton->overshootDeringing = options->Get(NanNew<String>("overshootDeringing"))->BooleanValue();
|
||||||
baton->optimiseScans = options->Get(NanNew<String>("optimiseScans"))->BooleanValue();
|
baton->optimiseScans = options->Get(NanNew<String>("optimiseScans"))->BooleanValue();
|
||||||
baton->withMetadata = options->Get(NanNew<String>("withMetadata"))->BooleanValue();
|
baton->withMetadata = options->Get(NanNew<String>("withMetadata"))->BooleanValue();
|
||||||
|
baton->withMetadataOrientation = options->Get(NanNew<String>("withMetadataOrientation"))->Int32Value();
|
||||||
// Output filename or __format for Buffer
|
// Output filename or __format for Buffer
|
||||||
baton->output = *String::Utf8Value(options->Get(NanNew<String>("output"))->ToString());
|
baton->output = *String::Utf8Value(options->Get(NanNew<String>("output"))->ToString());
|
||||||
baton->tileSize = options->Get(NanNew<String>("tileSize"))->Int32Value();
|
baton->tileSize = options->Get(NanNew<String>("tileSize"))->Int32Value();
|
||||||
|
@ -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});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -23,15 +23,19 @@ describe('Rotation', function() {
|
|||||||
it('Input image has Orientation EXIF tag but do not rotate output', function(done) {
|
it('Input image has Orientation EXIF tag but do not rotate output', function(done) {
|
||||||
sharp(fixtures.inputJpgWithExif)
|
sharp(fixtures.inputJpgWithExif)
|
||||||
.resize(320)
|
.resize(320)
|
||||||
|
.withMetadata()
|
||||||
.toBuffer(function(err, data, info) {
|
.toBuffer(function(err, data, info) {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
assert.strictEqual(true, data.length > 0);
|
assert.strictEqual(true, data.length > 0);
|
||||||
assert.strictEqual('jpeg', info.format);
|
assert.strictEqual('jpeg', info.format);
|
||||||
assert.strictEqual(320, info.width);
|
assert.strictEqual(320, info.width);
|
||||||
assert.strictEqual(427, info.height);
|
assert.strictEqual(427, info.height);
|
||||||
|
sharp(data).metadata(function(err, metadata) {
|
||||||
|
assert.strictEqual(8, metadata.orientation);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('Input image has Orientation EXIF tag value of 8 (270 degrees), auto-rotate', function(done) {
|
it('Input image has Orientation EXIF tag value of 8 (270 degrees), auto-rotate', function(done) {
|
||||||
sharp(fixtures.inputJpgWithExif)
|
sharp(fixtures.inputJpgWithExif)
|
||||||
@ -46,18 +50,39 @@ describe('Rotation', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Input image has Orientation EXIF tag value of 5 (270 degrees + flip), auto-rotate', function(done) {
|
it('Override EXIF Orientation tag metadata after auto-rotate', function(done) {
|
||||||
sharp(fixtures.inputJpgWithExifMirroring)
|
sharp(fixtures.inputJpgWithExif)
|
||||||
.rotate()
|
.rotate()
|
||||||
.resize(320)
|
.resize(320)
|
||||||
|
.withMetadata({orientation: 3})
|
||||||
.toBuffer(function(err, data, info) {
|
.toBuffer(function(err, data, info) {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
assert.strictEqual('jpeg', info.format);
|
assert.strictEqual('jpeg', info.format);
|
||||||
assert.strictEqual(320, info.width);
|
assert.strictEqual(320, info.width);
|
||||||
assert.strictEqual(240, info.height);
|
assert.strictEqual(240, info.height);
|
||||||
|
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);
|
fixtures.assertSimilar(fixtures.expected('exif-5.jpg'), data, done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('Attempt to auto-rotate using image that has no EXIF', function(done) {
|
it('Attempt to auto-rotate using image that has no EXIF', function(done) {
|
||||||
sharp(fixtures.inputJpg).rotate().resize(320).toBuffer(function(err, data, info) {
|
sharp(fixtures.inputJpg).rotate().resize(320).toBuffer(function(err, data, info) {
|
||||||
@ -95,27 +120,37 @@ describe('Rotation', function() {
|
|||||||
sharp(fixtures.inputJpg)
|
sharp(fixtures.inputJpg)
|
||||||
.resize(320)
|
.resize(320)
|
||||||
.flip()
|
.flip()
|
||||||
|
.withMetadata()
|
||||||
.toBuffer(function(err, data, info) {
|
.toBuffer(function(err, data, info) {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
assert.strictEqual('jpeg', info.format);
|
assert.strictEqual('jpeg', info.format);
|
||||||
assert.strictEqual(320, info.width);
|
assert.strictEqual(320, info.width);
|
||||||
assert.strictEqual(261, info.height);
|
assert.strictEqual(261, info.height);
|
||||||
|
sharp(data).metadata(function(err, metadata) {
|
||||||
|
if (err) throw err;
|
||||||
|
assert.strictEqual(false, 'orientation' in metadata);
|
||||||
fixtures.assertSimilar(fixtures.expected('flip.jpg'), data, done);
|
fixtures.assertSimilar(fixtures.expected('flip.jpg'), data, done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('Flop - horizontal', function(done) {
|
it('Flop - horizontal', function(done) {
|
||||||
sharp(fixtures.inputJpg)
|
sharp(fixtures.inputJpg)
|
||||||
.resize(320)
|
.resize(320)
|
||||||
.flop()
|
.flop()
|
||||||
|
.withMetadata()
|
||||||
.toBuffer(function(err, data, info) {
|
.toBuffer(function(err, data, info) {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
assert.strictEqual('jpeg', info.format);
|
assert.strictEqual('jpeg', info.format);
|
||||||
assert.strictEqual(320, info.width);
|
assert.strictEqual(320, info.width);
|
||||||
assert.strictEqual(261, info.height);
|
assert.strictEqual(261, info.height);
|
||||||
|
sharp(data).metadata(function(err, metadata) {
|
||||||
|
if (err) throw err;
|
||||||
|
assert.strictEqual(false, 'orientation' in metadata);
|
||||||
fixtures.assertSimilar(fixtures.expected('flop.jpg'), data, done);
|
fixtures.assertSimilar(fixtures.expected('flop.jpg'), data, done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('Flip and flop', function(done) {
|
it('Flip and flop', function(done) {
|
||||||
sharp(fixtures.inputJpg)
|
sharp(fixtures.inputJpg)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user