mirror of
https://github.com/lovell/sharp.git
synced 2025-07-09 10:30:15 +02:00
Add support for mirroring #62
This commit is contained in:
parent
f214673c3c
commit
db6dc6431b
10
README.md
10
README.md
@ -321,7 +321,15 @@ Rotate the output image by either an explicit angle or auto-orient based on the
|
|||||||
|
|
||||||
`angle`, if present, is a Number with a value of `0`, `90`, `180` or `270`.
|
`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. Mirroring is currently unsupported.
|
Use this method without `angle` to determine the angle from EXIF data. Mirroring is supported and may infer the use of a `flip` operation.
|
||||||
|
|
||||||
|
#### flip()
|
||||||
|
|
||||||
|
Flip the image about the vertical Y axis. This always occurs after rotation, if any.
|
||||||
|
|
||||||
|
#### flop()
|
||||||
|
|
||||||
|
Flop the image about the horizontal X axis. This always occurs after rotation, if any.
|
||||||
|
|
||||||
#### withoutEnlargement()
|
#### withoutEnlargement()
|
||||||
|
|
||||||
|
18
index.js
18
index.js
@ -31,6 +31,8 @@ var Sharp = function(input) {
|
|||||||
canvas: 'c',
|
canvas: 'c',
|
||||||
gravity: 0,
|
gravity: 0,
|
||||||
angle: 0,
|
angle: 0,
|
||||||
|
flip: false,
|
||||||
|
flop: false,
|
||||||
withoutEnlargement: false,
|
withoutEnlargement: false,
|
||||||
interpolator: 'bilinear',
|
interpolator: 'bilinear',
|
||||||
// operations
|
// operations
|
||||||
@ -168,6 +170,22 @@ Sharp.prototype.rotate = function(angle) {
|
|||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Flip the image vertically, about the Y axis
|
||||||
|
*/
|
||||||
|
Sharp.prototype.flip = function(flip) {
|
||||||
|
this.options.flip = (typeof flip === 'boolean') ? flip : true;
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Flop the image horizontally, about the X axis
|
||||||
|
*/
|
||||||
|
Sharp.prototype.flop = function(flop) {
|
||||||
|
this.options.flop = (typeof flop === 'boolean') ? flop : true;
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Do not enlarge the output if the input width *or* height are already less than the required dimensions
|
Do not enlarge the output if the input width *or* height are already less than the required dimensions
|
||||||
This is equivalent to GraphicsMagick's ">" geometry option:
|
This is equivalent to GraphicsMagick's ">" geometry option:
|
||||||
|
60
src/sharp.cc
60
src/sharp.cc
@ -43,12 +43,14 @@ struct resize_baton {
|
|||||||
bool sharpen;
|
bool sharpen;
|
||||||
double gamma;
|
double gamma;
|
||||||
bool greyscale;
|
bool greyscale;
|
||||||
|
int angle;
|
||||||
|
bool flip;
|
||||||
|
bool flop;
|
||||||
bool progressive;
|
bool progressive;
|
||||||
bool without_enlargement;
|
bool without_enlargement;
|
||||||
VipsAccess access_method;
|
VipsAccess access_method;
|
||||||
int quality;
|
int quality;
|
||||||
int compression_level;
|
int compression_level;
|
||||||
int angle;
|
|
||||||
std::string err;
|
std::string err;
|
||||||
bool with_metadata;
|
bool with_metadata;
|
||||||
|
|
||||||
@ -65,6 +67,8 @@ struct resize_baton {
|
|||||||
sharpen(false),
|
sharpen(false),
|
||||||
gamma(0.0),
|
gamma(0.0),
|
||||||
greyscale(false),
|
greyscale(false),
|
||||||
|
flip(false),
|
||||||
|
flop(false),
|
||||||
progressive(false),
|
progressive(false),
|
||||||
without_enlargement(false),
|
without_enlargement(false),
|
||||||
with_metadata(false) {}
|
with_metadata(false) {}
|
||||||
@ -125,15 +129,16 @@ typedef enum {
|
|||||||
} Angle;
|
} Angle;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Calculate the angle of rotation for the output image.
|
Calculate the angle of rotation and need-to-flip for the output image.
|
||||||
In order of priority:
|
In order of priority:
|
||||||
1. Use explicitly requested angle (supports 90, 180, 270)
|
1. Use explicitly requested angle (supports 90, 180, 270)
|
||||||
2. Use input image EXIF Orientation header (does not support mirroring)
|
2. Use input image EXIF Orientation header - supports mirroring
|
||||||
3. Otherwise default to zero, i.e. no rotation
|
3. Otherwise default to zero, i.e. no rotation
|
||||||
*/
|
*/
|
||||||
static Angle
|
static std::tuple<Angle, bool>
|
||||||
sharp_calc_rotation(int const angle, VipsImage const *input) {
|
sharp_calc_rotation_and_flip(int const angle, VipsImage const *input) {
|
||||||
Angle rotate = ANGLE_0;
|
Angle rotate = ANGLE_0;
|
||||||
|
bool flip = FALSE;
|
||||||
if (angle == -1) {
|
if (angle == -1) {
|
||||||
const char *exif;
|
const char *exif;
|
||||||
if (!vips_image_get_string(input, "exif-ifd0-Orientation", &exif)) {
|
if (!vips_image_get_string(input, "exif-ifd0-Orientation", &exif)) {
|
||||||
@ -143,6 +148,17 @@ sharp_calc_rotation(int const angle, VipsImage const *input) {
|
|||||||
rotate = ANGLE_180;
|
rotate = ANGLE_180;
|
||||||
} else if (exif[0] == 0x38) { // "8"
|
} else if (exif[0] == 0x38) { // "8"
|
||||||
rotate = ANGLE_270;
|
rotate = ANGLE_270;
|
||||||
|
} else if (exif[0] == 0x32) { // "2" (flip 1)
|
||||||
|
flip = TRUE;
|
||||||
|
} else if (exif[0] == 0x37) { // "7" (flip 6)
|
||||||
|
rotate = ANGLE_90;
|
||||||
|
flip = TRUE;
|
||||||
|
} else if (exif[0] == 0x34) { // "4" (flip 3)
|
||||||
|
rotate = ANGLE_180;
|
||||||
|
flip = TRUE;
|
||||||
|
} else if (exif[0] == 0x35) { // "5" (flip 8)
|
||||||
|
rotate = ANGLE_270;
|
||||||
|
flip = TRUE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -154,7 +170,7 @@ sharp_calc_rotation(int const angle, VipsImage const *input) {
|
|||||||
rotate = ANGLE_270;
|
rotate = ANGLE_270;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return rotate;
|
return std::make_tuple(rotate, flip);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -443,13 +459,19 @@ class ResizeWorker : public NanAsyncWorker {
|
|||||||
int inputHeight = image->Ysize;
|
int inputHeight = image->Ysize;
|
||||||
|
|
||||||
// Calculate angle of rotation, to be carried out later
|
// Calculate angle of rotation, to be carried out later
|
||||||
Angle rotation = sharp_calc_rotation(baton->angle, image);
|
Angle rotation;
|
||||||
|
bool flip;
|
||||||
|
std::tie(rotation, flip) = sharp_calc_rotation_and_flip(baton->angle, image);
|
||||||
if (rotation == ANGLE_90 || rotation == ANGLE_270) {
|
if (rotation == ANGLE_90 || rotation == ANGLE_270) {
|
||||||
// Swap input output width and height when rotating by 90 or 270 degrees
|
// Swap input output width and height when rotating by 90 or 270 degrees
|
||||||
int swap = inputWidth;
|
int swap = inputWidth;
|
||||||
inputWidth = inputHeight;
|
inputWidth = inputHeight;
|
||||||
inputHeight = swap;
|
inputHeight = swap;
|
||||||
}
|
}
|
||||||
|
if (flip && !baton->flip) {
|
||||||
|
// Add flip operation due to EXIF mirroring
|
||||||
|
baton->flip = TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
// Scaling calculations
|
// Scaling calculations
|
||||||
double factor;
|
double factor;
|
||||||
@ -645,6 +667,28 @@ class ResizeWorker : public NanAsyncWorker {
|
|||||||
image = rotated;
|
image = rotated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flip (mirror about Y axis)
|
||||||
|
if (baton->flip) {
|
||||||
|
VipsImage *flipped = vips_image_new();
|
||||||
|
vips_object_local(hook, flipped);
|
||||||
|
if (vips_flip(image, &flipped, VIPS_DIRECTION_VERTICAL, NULL)) {
|
||||||
|
return resize_error(baton, hook);
|
||||||
|
}
|
||||||
|
g_object_unref(image);
|
||||||
|
image = flipped;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flop (mirror about X axis)
|
||||||
|
if (baton->flop) {
|
||||||
|
VipsImage *flopped = vips_image_new();
|
||||||
|
vips_object_local(hook, flopped);
|
||||||
|
if (vips_flip(image, &flopped, VIPS_DIRECTION_HORIZONTAL, NULL)) {
|
||||||
|
return resize_error(baton, hook);
|
||||||
|
}
|
||||||
|
g_object_unref(image);
|
||||||
|
image = flopped;
|
||||||
|
}
|
||||||
|
|
||||||
// Crop/embed
|
// Crop/embed
|
||||||
if (image->Xsize != baton->width || image->Ysize != baton->height) {
|
if (image->Xsize != baton->width || image->Ysize != baton->height) {
|
||||||
if (baton->canvas == EMBED) {
|
if (baton->canvas == EMBED) {
|
||||||
@ -960,6 +1004,8 @@ NAN_METHOD(resize) {
|
|||||||
baton->gamma = options->Get(NanNew<String>("gamma"))->NumberValue();
|
baton->gamma = options->Get(NanNew<String>("gamma"))->NumberValue();
|
||||||
baton->greyscale = options->Get(NanNew<String>("greyscale"))->BooleanValue();
|
baton->greyscale = options->Get(NanNew<String>("greyscale"))->BooleanValue();
|
||||||
baton->angle = options->Get(NanNew<String>("angle"))->Int32Value();
|
baton->angle = options->Get(NanNew<String>("angle"))->Int32Value();
|
||||||
|
baton->flip = options->Get(NanNew<String>("flip"))->BooleanValue();
|
||||||
|
baton->flop = options->Get(NanNew<String>("flop"))->BooleanValue();
|
||||||
// Output options
|
// Output options
|
||||||
baton->progressive = options->Get(NanNew<String>("progressive"))->BooleanValue();
|
baton->progressive = options->Get(NanNew<String>("progressive"))->BooleanValue();
|
||||||
baton->quality = options->Get(NanNew<String>("quality"))->Int32Value();
|
baton->quality = options->Get(NanNew<String>("quality"))->Int32Value();
|
||||||
|
BIN
test/fixtures/Landscape_5.jpg
vendored
Normal file
BIN
test/fixtures/Landscape_5.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 134 KiB |
1
test/fixtures/index.js
vendored
1
test/fixtures/index.js
vendored
@ -10,6 +10,7 @@ module.exports = {
|
|||||||
|
|
||||||
inputJpg: getPath('2569067123_aca715a2ee_o.jpg'), // http://www.flickr.com/photos/grizdave/2569067123/
|
inputJpg: getPath('2569067123_aca715a2ee_o.jpg'), // http://www.flickr.com/photos/grizdave/2569067123/
|
||||||
inputJpgWithExif: getPath('Landscape_8.jpg'), // https://github.com/recurser/exif-orientation-examples/blob/master/Landscape_8.jpg
|
inputJpgWithExif: getPath('Landscape_8.jpg'), // https://github.com/recurser/exif-orientation-examples/blob/master/Landscape_8.jpg
|
||||||
|
inputJpgWithExifMirroring: getPath('Landscape_5.jpg'), // https://github.com/recurser/exif-orientation-examples/blob/master/Landscape_5.jpg
|
||||||
inputJpgWithGammaHoliness: getPath('gamma_dalai_lama_gray.jpg'), // http://www.4p8.com/eric.brasseur/gamma.html
|
inputJpgWithGammaHoliness: getPath('gamma_dalai_lama_gray.jpg'), // http://www.4p8.com/eric.brasseur/gamma.html
|
||||||
inputJpgWithCmykProfile: getPath('Channel_digital_image_CMYK_color.jpg'), // http://en.wikipedia.org/wiki/File:Channel_digital_image_CMYK_color.jpg
|
inputJpgWithCmykProfile: getPath('Channel_digital_image_CMYK_color.jpg'), // http://en.wikipedia.org/wiki/File:Channel_digital_image_CMYK_color.jpg
|
||||||
|
|
||||||
|
@ -19,25 +19,42 @@ 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).resize(320).toBuffer(function(err, data, info) {
|
sharp(fixtures.inputJpgWithExif)
|
||||||
if (err) throw err;
|
.resize(320)
|
||||||
assert.strictEqual(true, data.length > 0);
|
.toBuffer(function(err, data, info) {
|
||||||
assert.strictEqual('jpeg', info.format);
|
if (err) throw err;
|
||||||
assert.strictEqual(320, info.width);
|
assert.strictEqual(true, data.length > 0);
|
||||||
assert.strictEqual(426, info.height);
|
assert.strictEqual('jpeg', info.format);
|
||||||
done();
|
assert.strictEqual(320, info.width);
|
||||||
});
|
assert.strictEqual(426, info.height);
|
||||||
|
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).rotate().resize(320).toBuffer(function(err, data, info) {
|
sharp(fixtures.inputJpgWithExif)
|
||||||
if (err) throw err;
|
.rotate()
|
||||||
assert.strictEqual(true, data.length > 0);
|
.resize(320)
|
||||||
assert.strictEqual('jpeg', info.format);
|
.toFile(fixtures.path('output.exif.8.jpg'), function(err, info) {
|
||||||
assert.strictEqual(320, info.width);
|
if (err) throw err;
|
||||||
assert.strictEqual(240, info.height);
|
assert.strictEqual('jpeg', info.format);
|
||||||
done();
|
assert.strictEqual(320, info.width);
|
||||||
});
|
assert.strictEqual(240, info.height);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Input image has Orientation EXIF tag value of 5 (270 degrees + flip), auto-rotate', function(done) {
|
||||||
|
sharp(fixtures.inputJpgWithExifMirroring)
|
||||||
|
.rotate()
|
||||||
|
.resize(320)
|
||||||
|
.toFile(fixtures.path('output.exif.5.jpg'), function(err, info) {
|
||||||
|
if (err) throw err;
|
||||||
|
assert.strictEqual('jpeg', info.format);
|
||||||
|
assert.strictEqual(320, info.width);
|
||||||
|
assert.strictEqual(240, info.height);
|
||||||
|
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) {
|
||||||
@ -61,4 +78,57 @@ describe('Rotation', function() {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Flip - vertical', function(done) {
|
||||||
|
sharp(fixtures.inputJpg)
|
||||||
|
.resize(320)
|
||||||
|
.flip()
|
||||||
|
.toFile(fixtures.path('output.flip.jpg'), function(err, info) {
|
||||||
|
if (err) throw err;
|
||||||
|
assert.strictEqual('jpeg', info.format);
|
||||||
|
assert.strictEqual(320, info.width);
|
||||||
|
assert.strictEqual(261, info.height);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Flop - horizontal', function(done) {
|
||||||
|
sharp(fixtures.inputJpg)
|
||||||
|
.resize(320)
|
||||||
|
.flop()
|
||||||
|
.toFile(fixtures.path('output.flop.jpg'), function(err, info) {
|
||||||
|
if (err) throw err;
|
||||||
|
assert.strictEqual('jpeg', info.format);
|
||||||
|
assert.strictEqual(320, info.width);
|
||||||
|
assert.strictEqual(261, info.height);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Flip and flop', function(done) {
|
||||||
|
sharp(fixtures.inputJpg)
|
||||||
|
.resize(320)
|
||||||
|
.flop()
|
||||||
|
.toFile(fixtures.path('output.flip.flop.jpg'), function(err, info) {
|
||||||
|
if (err) throw err;
|
||||||
|
assert.strictEqual('jpeg', info.format);
|
||||||
|
assert.strictEqual(320, info.width);
|
||||||
|
assert.strictEqual(261, info.height);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Neither flip nor flop', function(done) {
|
||||||
|
sharp(fixtures.inputJpg)
|
||||||
|
.resize(320)
|
||||||
|
.flip(false)
|
||||||
|
.flop(false)
|
||||||
|
.toFile(fixtures.path('output.flip.flop.nope.jpg'), function(err, info) {
|
||||||
|
if (err) throw err;
|
||||||
|
assert.strictEqual('jpeg', info.format);
|
||||||
|
assert.strictEqual(320, info.width);
|
||||||
|
assert.strictEqual(261, info.height);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user