Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite handling EXIF orientation — add tests, make it plugin-independent #875

Merged
merged 3 commits into from
Apr 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 117 additions & 34 deletions packages/core/src/utils/image-bitmap.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,44 +25,127 @@ function getMIMEFromBuffer(buffer, path) {
}

/*
* Automagically rotates an image based on its EXIF data (if present)
* @param img a constants object
* Obtains image orientation from EXIF metadata.
*
* @param img {Jimp} a Jimp image object
* @returns {number} a number 1-8 representing EXIF orientation,
* in particular 1 if orientation tag is missing
*/
function exifRotate(img) {
const exif = img._exif;

if (exif && exif.tags && exif.tags.Orientation) {
switch (img._exif.tags.Orientation) {
case 1: // Horizontal (normal)
// do nothing
break;
case 2: // Mirror horizontal
img.mirror(true, false);
break;
case 3: // Rotate 180
img.rotate(180);
break;
case 4: // Mirror vertical
img.mirror(false, true);
break;
case 5: // Mirror horizontal and rotate 270 CW
img.rotate(-90).mirror(true, false);
break;
case 6: // Rotate 90 CW
img.rotate(-90);
break;
case 7: // Mirror horizontal and rotate 90 CW
img.rotate(90).mirror(true, false);
break;
case 8: // Rotate 270 CW
img.rotate(-270);
break;
default:
break;
function getExifOrientation(img) {
return (img._exif && img._exif.tags && img._exif.tags.Orientation) || 1;
}

/**
* Returns a function which translates EXIF-rotated coordinates into
* non-rotated ones.
*
* Transformation reference: http://sylvana.net/jpegcrop/exif_orientation.html.
*
* @param img {Jimp} a Jimp image object
* @returns {function} transformation function for transformBitmap().
*/
function getExifOrientationTransformation(img) {
const w = img.getWidth();
const h = img.getHeight();

switch (getExifOrientation(img)) {
case 1: // Horizontal (normal)
// does not need to be supported here
return null;

case 2: // Mirror horizontal
return function(x, y) {
return [w - x - 1, y];
};

case 3: // Rotate 180
return function(x, y) {
return [w - x - 1, h - y - 1];
};

case 4: // Mirror vertical
return function(x, y) {
return [x, h - y - 1];
};

case 5: // Mirror horizontal and rotate 270 CW
return function(x, y) {
return [y, x];
};

case 6: // Rotate 90 CW
return function(x, y) {
return [y, h - x - 1];
};

case 7: // Mirror horizontal and rotate 90 CW
return function(x, y) {
return [w - y - 1, h - x - 1];
};

case 8: // Rotate 270 CW
return function(x, y) {
return [w - y - 1, x];
};

default:
return null;
}
}

/*
* Transforms bitmap in place (moves pixels around) according to given
* transformation function.
*
* @param img {Jimp} a Jimp image object, which bitmap is supposed to
* be transformed
* @param width {number} bitmap width after the transformation
* @param height {number} bitmap height after the transformation
* @param transformation {function} transformation function which defines pixel
* mapping between new and source bitmap. It takes a pair of coordinates
* in the target, and returns a respective pair of coordinates in
* the source bitmap, i.e. has following form:
* `function(new_x, new_y) { return [src_x, src_y] }`.
*/
function transformBitmap(img, width, height, transformation) {
// Underscore-prefixed values are related to the source bitmap
// Their counterparts with no prefix are related to the target bitmap
const _data = img.bitmap.data;
const _width = img.bitmap.width;

const data = Buffer.alloc(_data.length);

for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
const [_x, _y] = transformation(x, y);

const idx = (width * y + x) << 2;
const _idx = (_width * _y + _x) << 2;

const pixel = _data.readUInt32BE(_idx);
data.writeUInt32BE(pixel, idx);
}
}

return img;
img.bitmap.data = data;
img.bitmap.width = width;
img.bitmap.height = height;
}

/*
* Automagically rotates an image based on its EXIF data (if present).
* @param img {Jimp} a Jimp image object
*/
function exifRotate(img) {
if (getExifOrientation(img) < 2) return;

const transformation = getExifOrientationTransformation(img);
const swapDimensions = getExifOrientation(img) > 4;

const newWidth = swapDimensions ? img.bitmap.height : img.bitmap.width;
const newHeight = swapDimensions ? img.bitmap.width : img.bitmap.height;

transformBitmap(img, newWidth, newHeight, transformation);
}

// parses a bitmap from the constructor to the JIMP bitmap property
Expand Down
24 changes: 24 additions & 0 deletions packages/jimp/test/exif-rotation.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Jimp, getTestDir } from '@jimp/test-utils';

import configure from '@jimp/custom';

const jimp = configure({ plugins: [] }, Jimp);

describe('EXIF orientation', () => {
for (let orientation = 1; orientation <= 8; orientation++) {
it(`is fixed when EXIF orientation is ${orientation}`, async () => {
const regularImg = await imageWithOrientation(1);
const orientedImg = await imageWithOrientation(orientation);

orientedImg.getWidth().should.be.equal(regularImg.getWidth());
orientedImg.getHeight().should.be.equal(regularImg.getHeight());
Jimp.distance(regularImg, orientedImg).should.lessThan(0.07);
});
}
});

function imageWithOrientation(orientation) {
const imageName = `Landscape_${orientation}.jpg`;
const path = getTestDir(__dirname) + '/images/exif-orientation/' + imageName;
return jimp.read(path);
}