Add support for image rotation including EXIF auto-orient

This commit is contained in:
Lovell Fuller 2014-05-26 14:59:44 +01:00
parent bc3311cbad
commit a94dd2b354
7 changed files with 206 additions and 43 deletions

View File

@ -5,7 +5,7 @@ node_js:
before_install: before_install:
- sudo add-apt-repository ppa:lyrasis/precise-backports -y - sudo add-apt-repository ppa:lyrasis/precise-backports -y
- sudo apt-get update -qq - sudo apt-get update -qq
- sudo apt-get install -qq automake gobject-introspection gtk-doc-tools libfftw3-dev libglib2.0-dev libjpeg-turbo8-dev libpng12-dev libwebp-dev libtiff4-dev libxml2-dev swig graphicsmagick libmagick++-dev - sudo apt-get install -qq automake gobject-introspection gtk-doc-tools libfftw3-dev libglib2.0-dev libjpeg-turbo8-dev libpng12-dev libwebp-dev libtiff4-dev libexif-dev libxml2-dev swig graphicsmagick libmagick++-dev
- git clone https://github.com/jcupitt/libvips.git - git clone https://github.com/jcupitt/libvips.git
- cd libvips - cd libvips
- git checkout 7.38 - git checkout 7.38

View File

@ -48,7 +48,7 @@ The _gettext_ dependency of _libvips_ [can lead](https://github.com/lovell/sharp
Compiling from source is recommended: Compiling from source is recommended:
sudo apt-get install automake build-essential git gobject-introspection gtk-doc-tools libfftw3-dev libglib2.0-dev libjpeg-turbo8-dev libpng12-dev libwebp-dev libtiff5-dev libxml2-dev swig sudo apt-get install automake build-essential git gobject-introspection gtk-doc-tools libfftw3-dev libglib2.0-dev libjpeg-turbo8-dev libpng12-dev libwebp-dev libtiff5-dev libexif-dev libxml2-dev swig
git clone https://github.com/jcupitt/libvips.git git clone https://github.com/jcupitt/libvips.git
cd libvips cd libvips
git checkout 7.38 git checkout 7.38
@ -85,20 +85,20 @@ sharp('input.jpg').resize(300, 200).write('output.jpg', function(err) {
``` ```
```javascript ```javascript
sharp('input.jpg').resize(null, 200).progressive().toBuffer(function(err, outputBuffer) { sharp('input.jpg').rotate().resize(null, 200).progressive().toBuffer(function(err, outputBuffer) {
if (err) { if (err) {
throw err; throw err;
} }
// outputBuffer contains progressive JPEG image data, 200 pixels high // outputBuffer contains 200px high progressive JPEG image data, auto-rotated using EXIF Orientation tag
}); });
``` ```
```javascript ```javascript
sharp('input.png').resize(300).sharpen().quality(90).webp(function(err, outputBuffer) { sharp('input.png').rotate(180).resize(300).sharpen().quality(90).webp(function(err, outputBuffer) {
if (err) { if (err) {
throw err; throw err;
} }
// outputBuffer contains 300 pixels wide, sharpened, 90% quality WebP image data // outputBuffer contains 300px wide, upside down, sharpened, 90% quality WebP image data
}); });
``` ```
@ -143,7 +143,7 @@ Constructor to which further methods are chained. `input` can be one of:
### resize(width, [height]) ### resize(width, [height])
Scale to `width` x `height`. By default, the resized image is cropped to the exact size specified. Scale output to `width` x `height`. By default, the resized image is cropped to the exact size specified.
`width` is the Number of pixels wide the resultant image should be. Use `null` or `undefined` to auto-scale the width to match the height. `width` is the Number of pixels wide the resultant image should be. Use `null` or `undefined` to auto-scale the width to match the height.
@ -167,6 +167,14 @@ Embed the resized image on a white background of the exact size specified.
Embed the resized image on a black background of the exact size specified. Embed the resized image on a black background of the exact size specified.
### rotate([angle])
Rotate the output image by either an explicit angle or auto-orient based on the EXIF `Orientation` tag. Mirroring is not supported.
`angle`, if present, is a Number with a value of `0`, `90`, `180` or `270`.
Use this method without `angle` to determine the angle from EXIF data.
### sharpen() ### sharpen()
Perform a mild sharpen of the resultant image. This typically reduces performance by 30%. Perform a mild sharpen of the resultant image. This typically reduces performance by 30%.

View File

@ -11,6 +11,7 @@ var Sharp = function(input) {
width: -1, width: -1,
height: -1, height: -1,
canvas: 'c', canvas: 'c',
angle: 0,
sharpen: false, sharpen: false,
progressive: false, progressive: false,
sequentialRead: false, sequentialRead: false,
@ -49,6 +50,20 @@ Sharp.prototype.max = function() {
return this; return this;
}; };
/*
Rotate output image by 0, 90, 180 or 270 degrees
Auto-rotation based on the EXIF Orientation tag is represented by an angle of -1
*/
Sharp.prototype.rotate = function(angle) {
if (typeof angle === 'undefined') {
this.options.angle = -1;
} else if (!Number.isNaN(angle) && [0, 90, 180, 270].indexOf(angle) !== -1) {
this.options.angle = angle;
} else {
throw 'Unsupport angle (0, 90, 180, 270) ' + angle;
}
return this;
};
Sharp.prototype.sharpen = function(sharpen) { Sharp.prototype.sharpen = function(sharpen) {
this.options.sharpen = (typeof sharpen === 'boolean') ? sharpen : true; this.options.sharpen = (typeof sharpen === 'boolean') ? sharpen : true;
@ -147,6 +162,7 @@ Sharp.prototype._sharp = function(output, callback) {
this.options.sequentialRead, this.options.sequentialRead,
this.options.quality, this.options.quality,
this.options.compressionLevel, this.options.compressionLevel,
this.options.angle,
callback callback
); );
return this; return this;

View File

@ -27,6 +27,7 @@ struct resize_baton {
VipsAccess access_method; VipsAccess access_method;
int quality; int quality;
int compressionLevel; int compressionLevel;
int angle;
std::string err; std::string err;
resize_baton(): buffer_in_len(0), buffer_out_len(0), crop(false), max(false), sharpen(false), progressive(false) {} resize_baton(): buffer_in_len(0), buffer_out_len(0), crop(false), max(false), sharpen(false), progressive(false) {}
@ -40,31 +41,31 @@ typedef enum {
MAGICK MAGICK
} ImageType; } ImageType;
unsigned char MARKER_JPEG[] = {0xff, 0xd8}; unsigned char const MARKER_JPEG[] = {0xff, 0xd8};
unsigned char MARKER_PNG[] = {0x89, 0x50}; unsigned char const MARKER_PNG[] = {0x89, 0x50};
unsigned char MARKER_WEBP[] = {0x52, 0x49}; unsigned char const MARKER_WEBP[] = {0x52, 0x49};
bool ends_with(std::string const &str, std::string const &end) { static bool ends_with(std::string const &str, std::string const &end) {
return str.length() >= end.length() && 0 == str.compare(str.length() - end.length(), end.length(), end); return str.length() >= end.length() && 0 == str.compare(str.length() - end.length(), end.length(), end);
} }
bool is_jpeg(std::string const &str) { static bool is_jpeg(std::string const &str) {
return ends_with(str, ".jpg") || ends_with(str, ".jpeg") || ends_with(str, ".JPG") || ends_with(str, ".JPEG"); return ends_with(str, ".jpg") || ends_with(str, ".jpeg") || ends_with(str, ".JPG") || ends_with(str, ".JPEG");
} }
bool is_png(std::string const &str) { static bool is_png(std::string const &str) {
return ends_with(str, ".png") || ends_with(str, ".PNG"); return ends_with(str, ".png") || ends_with(str, ".PNG");
} }
bool is_webp(std::string const &str) { static bool is_webp(std::string const &str) {
return ends_with(str, ".webp") || ends_with(str, ".WEBP"); return ends_with(str, ".webp") || ends_with(str, ".WEBP");
} }
bool is_tiff(std::string const &str) { static bool is_tiff(std::string const &str) {
return ends_with(str, ".tif") || ends_with(str, ".tiff") || ends_with(str, ".TIF") || ends_with(str, ".TIFF"); return ends_with(str, ".tif") || ends_with(str, ".tiff") || ends_with(str, ".TIF") || ends_with(str, ".TIFF");
} }
void resize_error(resize_baton *baton, VipsImage *unref) { static void resize_error(resize_baton *baton, VipsImage *unref) {
(baton->err).append(vips_error_buffer()); (baton->err).append(vips_error_buffer());
vips_error_clear(); vips_error_clear();
g_object_unref(unref); g_object_unref(unref);
@ -72,6 +73,38 @@ void resize_error(resize_baton *baton, VipsImage *unref) {
return; return;
} }
/*
Calculate the angle of rotation for the output image.
In order of priority:
1. Use explicitly requested angle (supports 90, 180, 270)
2. Use input image EXIF Orientation header (does not support mirroring)
3. Otherwise default to zero, i.e. no rotation
*/
static VipsAngle calc_rotation(int const angle, VipsImage const *input) {
VipsAngle rotate = VIPS_ANGLE_0;
if (angle == -1) {
const char *exif;
if (!vips_image_get_string(input, "exif-ifd0-Orientation", &exif)) {
if (exif[0] == 0x36) { // "6"
rotate = VIPS_ANGLE_90;
} else if (exif[0] == 0x33) { // "3"
rotate = VIPS_ANGLE_180;
} else if (exif[0] == 0x38) { // "8"
rotate = VIPS_ANGLE_270;
}
}
} else {
if (angle == 90) {
rotate = VIPS_ANGLE_90;
} else if (angle == 180) {
rotate = VIPS_ANGLE_180;
} else if (angle == 270) {
rotate = VIPS_ANGLE_270;
}
}
return rotate;
}
class ResizeWorker : public NanAsyncWorker { class ResizeWorker : public NanAsyncWorker {
public: public:
ResizeWorker(NanCallback *callback, resize_baton *baton) ResizeWorker(NanCallback *callback, resize_baton *baton)
@ -132,34 +165,47 @@ class ResizeWorker : public NanAsyncWorker {
return; return;
} }
// Get input image width and height
int inputWidth = in->Xsize;
int inputHeight = in->Ysize;
// Calculate angle of rotation, to be carried out later
VipsAngle rotation = calc_rotation(baton->angle, in);
if (rotation == VIPS_ANGLE_90 || rotation == VIPS_ANGLE_270) {
// Swap input output width and height when rotating by 90 or 270 degrees
int swap = inputWidth;
inputWidth = inputHeight;
inputHeight = swap;
}
// Scaling calculations // Scaling calculations
double factor; double factor;
if (baton->width > 0 && baton->height > 0) { if (baton->width > 0 && baton->height > 0) {
// Fixed width and height // Fixed width and height
double xfactor = static_cast<double>(in->Xsize) / static_cast<double>(baton->width); double xfactor = static_cast<double>(inputWidth) / static_cast<double>(baton->width);
double yfactor = static_cast<double>(in->Ysize) / static_cast<double>(baton->height); double yfactor = static_cast<double>(inputHeight) / static_cast<double>(baton->height);
factor = baton->crop ? std::min(xfactor, yfactor) : std::max(xfactor, yfactor); factor = baton->crop ? std::min(xfactor, yfactor) : std::max(xfactor, yfactor);
// if max is set, we need to compute the real size of the thumb image // if max is set, we need to compute the real size of the thumb image
if (baton->max) { if (baton->max) {
if (xfactor > yfactor) { if (xfactor > yfactor) {
baton->height = round(static_cast<double>(in->Ysize) / xfactor); baton->height = round(static_cast<double>(inputHeight) / xfactor);
} else { } else {
baton->width = round(static_cast<double>(in->Xsize) / yfactor); baton->width = round(static_cast<double>(inputWidth) / yfactor);
} }
} }
} else if (baton->width > 0) { } else if (baton->width > 0) {
// Fixed width, auto height // Fixed width, auto height
factor = static_cast<double>(in->Xsize) / static_cast<double>(baton->width); factor = static_cast<double>(inputWidth) / static_cast<double>(baton->width);
baton->height = floor(static_cast<double>(in->Ysize) / factor); baton->height = floor(static_cast<double>(inputHeight) / factor);
} else if (baton->height > 0) { } else if (baton->height > 0) {
// Fixed height, auto width // Fixed height, auto width
factor = static_cast<double>(in->Ysize) / static_cast<double>(baton->height); factor = static_cast<double>(inputHeight) / static_cast<double>(baton->height);
baton->width = floor(static_cast<double>(in->Xsize) / factor); baton->width = floor(static_cast<double>(inputWidth) / factor);
} else { } else {
// Identity transform // Identity transform
factor = 1; factor = 1;
baton->width = in->Xsize; baton->width = inputWidth;
baton->height = in->Ysize; baton->height = inputHeight;
} }
int shrink = floor(factor); int shrink = floor(factor);
if (shrink < 1) { if (shrink < 1) {
@ -186,7 +232,7 @@ class ResizeWorker : public NanAsyncWorker {
// Recalculate integral shrink and double residual // Recalculate integral shrink and double residual
factor = std::max(factor, 1.0); factor = std::max(factor, 1.0);
shrink = floor(factor); shrink = floor(factor);
residual = shrink / factor; residual = static_cast<double>(shrink) / factor;
// Reload input using shrink-on-load // Reload input using shrink-on-load
if (baton->buffer_in_len > 1) { if (baton->buffer_in_len > 1) {
if (vips_jpegload_buffer(baton->buffer_in, baton->buffer_in_len, &shrunk_on_load, "shrink", shrink_on_load, NULL)) { if (vips_jpegload_buffer(baton->buffer_in, baton->buffer_in_len, &shrunk_on_load, "shrink", shrink_on_load, NULL)) {
@ -209,8 +255,16 @@ class ResizeWorker : public NanAsyncWorker {
return resize_error(baton, shrunk_on_load); return resize_error(baton, shrunk_on_load);
} }
// Recalculate residual float based on dimensions of required vs shrunk images // Recalculate residual float based on dimensions of required vs shrunk images
double residualx = static_cast<double>(baton->width) / static_cast<double>(shrunk->Xsize); double shrunkWidth = shrunk->Xsize;
double residualy = static_cast<double>(baton->height) / static_cast<double>(shrunk->Ysize); double shrunkHeight = shrunk->Ysize;
if (rotation == VIPS_ANGLE_90 || rotation == VIPS_ANGLE_270) {
// Swap input output width and height when rotating by 90 or 270 degrees
int swap = shrunkWidth;
shrunkWidth = shrunkHeight;
shrunkHeight = swap;
}
double residualx = static_cast<double>(baton->width) / static_cast<double>(shrunkWidth);
double residualy = static_cast<double>(baton->height) / static_cast<double>(shrunkHeight);
if (baton->crop || baton->max) { if (baton->crop || baton->max) {
residual = std::max(residualx, residualy); residual = std::max(residualx, residualy);
} else { } else {
@ -232,30 +286,41 @@ class ResizeWorker : public NanAsyncWorker {
} }
g_object_unref(shrunk); g_object_unref(shrunk);
// Rotate
VipsImage *rotated = vips_image_new();
if (rotation != VIPS_ANGLE_0) {
if (vips_rot(affined, &rotated, rotation, NULL)) {
return resize_error(baton, affined);
}
} else {
vips_copy(affined, &rotated, NULL);
}
g_object_unref(affined);
// Crop/embed // Crop/embed
VipsImage *canvased = vips_image_new(); VipsImage *canvased = vips_image_new();
if (affined->Xsize != baton->width || affined->Ysize != baton->height) { if (rotated->Xsize != baton->width || rotated->Ysize != baton->height) {
if (baton->crop || baton->max) { if (baton->crop || baton->max) {
// Crop/max // Crop/max
int width = std::min(affined->Xsize, baton->width); int width = std::min(rotated->Xsize, baton->width);
int height = std::min(affined->Ysize, baton->height); int height = std::min(rotated->Ysize, baton->height);
int left = (affined->Xsize - width + 1) / 2; int left = (rotated->Xsize - width + 1) / 2;
int top = (affined->Ysize - height + 1) / 2; int top = (rotated->Ysize - height + 1) / 2;
if (vips_extract_area(affined, &canvased, left, top, width, height, NULL)) { if (vips_extract_area(rotated, &canvased, left, top, width, height, NULL)) {
return resize_error(baton, affined); return resize_error(baton, rotated);
} }
} else { } else {
// Embed // Embed
int left = (baton->width - affined->Xsize) / 2; int left = (baton->width - rotated->Xsize) / 2;
int top = (baton->height - affined->Ysize) / 2; int top = (baton->height - rotated->Ysize) / 2;
if (vips_embed(affined, &canvased, left, top, baton->width, baton->height, "extend", baton->extend, NULL)) { if (vips_embed(rotated, &canvased, left, top, baton->width, baton->height, "extend", baton->extend, NULL)) {
return resize_error(baton, affined); return resize_error(baton, rotated);
} }
} }
} else { } else {
vips_copy(affined, &canvased, NULL); vips_copy(rotated, &canvased, NULL);
} }
g_object_unref(affined); g_object_unref(rotated);
// Mild sharpen // Mild sharpen
VipsImage *sharpened = vips_image_new(); VipsImage *sharpened = vips_image_new();
@ -366,8 +431,9 @@ NAN_METHOD(resize) {
baton->access_method = args[8]->BooleanValue() ? VIPS_ACCESS_SEQUENTIAL : VIPS_ACCESS_RANDOM; baton->access_method = args[8]->BooleanValue() ? VIPS_ACCESS_SEQUENTIAL : VIPS_ACCESS_RANDOM;
baton->quality = args[9]->Int32Value(); baton->quality = args[9]->Int32Value();
baton->compressionLevel = args[10]->Int32Value(); baton->compressionLevel = args[10]->Int32Value();
baton->angle = args[11]->Int32Value();
NanCallback *callback = new NanCallback(args[11].As<v8::Function>()); NanCallback *callback = new NanCallback(args[12].As<v8::Function>());
NanAsyncQueueWorker(new ResizeWorker(callback, baton)); NanAsyncQueueWorker(new ResizeWorker(callback, baton));
NanReturnUndefined(); NanReturnUndefined();

BIN
tests/fixtures/Landscape_8.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

View File

@ -178,6 +178,18 @@ async.series({
} }
}); });
} }
}).add("sharp-file-buffer-rotate", {
defer: true,
fn: function(deferred) {
sharp(inputJpg).rotate(90).resize(width, height).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add("sharp-file-buffer-sequentialRead", { }).add("sharp-file-buffer-sequentialRead", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {

View File

@ -12,6 +12,8 @@ var outputJpg = path.join(fixturesPath, "output.jpg");
var inputTiff = path.join(fixturesPath, "G31D.TIF"); // http://www.fileformat.info/format/tiff/sample/e6c9a6e5253348f4aef6d17b534360ab/index.htm var inputTiff = path.join(fixturesPath, "G31D.TIF"); // http://www.fileformat.info/format/tiff/sample/e6c9a6e5253348f4aef6d17b534360ab/index.htm
var outputTiff = path.join(fixturesPath, "output.tiff"); var outputTiff = path.join(fixturesPath, "output.tiff");
var inputJpgWithExif = path.join(fixturesPath, "Landscape_8.jpg"); // https://github.com/recurser/exif-orientation-examples/blob/master/Landscape_8.jpg
async.series([ async.series([
// Resize with exact crop // Resize with exact crop
function(done) { function(done) {
@ -152,5 +154,64 @@ async.series([
assert(!!err); assert(!!err);
done(); done();
}); });
},
// Rotate by 90 degrees, respecting output input size
function(done) {
sharp(inputJpg).rotate(90).resize(320, 240).write(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(320, features.width);
assert.strictEqual(240, features.height);
done();
});
});
},
// Input image has Orientation EXIF tag but do not rotate output
function(done) {
sharp(inputJpgWithExif).resize(320).write(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(320, features.width);
assert.strictEqual(426, features.height);
done();
});
});
},
// Input image has Orientation EXIF tag value of 8 (270 degrees), auto-rotate
function(done) {
sharp(inputJpgWithExif).rotate().resize(320).write(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(320, features.width);
assert.strictEqual(240, features.height);
done();
});
});
},
// Attempt to auto-rotate using image that has no EXIF
function(done) {
sharp(inputJpg).rotate().resize(320).write(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(320, features.width);
assert.strictEqual(261, features.height);
done();
});
});
},
// Rotate to an invalid angle, should fail
function(done) {
var isValid = false;
try {
sharp(inputJpg).rotate(1);
isValid = true;
} catch (e) {}
assert(!isValid);
done();
} }
]); ]);