How to rotate JPEG images based on the orientation metadata?
I have some server code that is generati开发者_开发技巧ng thumbnails when an image is uploaded. The issue is that when the image was taken and the camera/device was rotated, the thumbnails are rotated, even though the full size images themselves are displayed in the correct orientation in any image viewing software. This is only happening with jpgs.
Using Preview on OSX, I can see that jpgs have orientation metadata embedded within. When I use ImageTools (Grails Plugin) to generate a thumbnail, the EXIF metadata is not in the thumbnail, which is why the thumbnails appear rotated.
Via offline conversations, I have learned that while it is relatively easy to read EXIF metadata, there is no easy way to write it, which is why the data is lost when generating a jpg thumbnail.
So it seems I have two options:
- Use ImageMagick to generate the thumbnails. The downside is it requires installed more software on our servers.
- Read the EXIF Orientation data is code and rotate the thumbnail appropriately.
Does anyone know of any other options?
If you want to rotate your images, I would suggest to use the metadata extractor library http://code.google.com/p/metadata-extractor/. You can get the image information with the following code:
// Inner class containing image information
public static class ImageInformation {
public final int orientation;
public final int width;
public final int height;
public ImageInformation(int orientation, int width, int height) {
this.orientation = orientation;
this.width = width;
this.height = height;
}
public String toString() {
return String.format("%dx%d,%d", this.width, this.height, this.orientation);
}
}
public static ImageInformation readImageInformation(File imageFile) throws IOException, MetadataException, ImageProcessingException {
Metadata metadata = ImageMetadataReader.readMetadata(imageFile);
Directory directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
JpegDirectory jpegDirectory = metadata.getFirstDirectoryOfType(JpegDirectory.class);
int orientation = 1;
try {
orientation = directory.getInt(ExifIFD0Directory.TAG_ORIENTATION);
} catch (MetadataException me) {
logger.warn("Could not get orientation");
}
int width = jpegDirectory.getImageWidth();
int height = jpegDirectory.getImageHeight();
return new ImageInformation(orientation, width, height);
}
Then given the orientation you retrieve, you can rotate and/or flip the image to the right orientation. The Affine transform for the EXIF orientation is given by the following method:
// Look at http://chunter.tistory.com/143 for information
public static AffineTransform getExifTransformation(ImageInformation info) {
AffineTransform t = new AffineTransform();
switch (info.orientation) {
case 1:
break;
case 2: // Flip X
t.scale(-1.0, 1.0);
t.translate(-info.width, 0);
break;
case 3: // PI rotation
t.translate(info.width, info.height);
t.rotate(Math.PI);
break;
case 4: // Flip Y
t.scale(1.0, -1.0);
t.translate(0, -info.height);
break;
case 5: // - PI/2 and Flip X
t.rotate(-Math.PI / 2);
t.scale(-1.0, 1.0);
break;
case 6: // -PI/2 and -width
t.translate(info.height, 0);
t.rotate(Math.PI / 2);
break;
case 7: // PI/2 and Flip
t.scale(-1.0, 1.0);
t.translate(-info.height, 0);
t.translate(0, info.width);
t.rotate( 3 * Math.PI / 2);
break;
case 8: // PI / 2
t.translate(0, info.width);
t.rotate( 3 * Math.PI / 2);
break;
}
return t;
}
The rotation of the image would be done by the following method:
public static BufferedImage transformImage(BufferedImage image, AffineTransform transform) throws Exception {
AffineTransformOp op = new AffineTransformOp(transform, AffineTransformOp.TYPE_BICUBIC);
BufferedImage destinationImage = op.createCompatibleDestImage(image, (image.getType() == BufferedImage.TYPE_BYTE_GRAY) ? image.getColorModel() : null );
Graphics2D g = destinationImage.createGraphics();
g.setBackground(Color.WHITE);
g.clearRect(0, 0, destinationImage.getWidth(), destinationImage.getHeight());
destinationImage = op.filter(image, destinationImage);
return destinationImage;
}
In a server environment, don't forget to run with -Djava.awt.headless=true
The Thumbnailator library honors EXIF orientation flags. To read an image at full size with correct orientation:
BufferedImage image = Thumbnails.of(inputStream).scale(1).asBufferedImage();
This can be done surprisingly easily by using the image part of JavaXT core library :
// Browsers today can't handle images with Exif Orientation tag
Image image = new Image(uploadedFilename);
// Auto-rotate based on Exif Orientation tag, and remove all Exif tags
image.rotate();
image.saveAs(permanentFilename);
That's it!
I have tried Apache Commons Imaging, but that was a mess. JavaXT is way more elegant.
Exif seems to be hard to write because of proprietary stuff in it. However, you can consider another option
Read original but only write orientation tag to thumbnails.
Apache Sanselan seems to have nice collection of tools to do it.
http://commons.apache.org/proper/commons-imaging/
Look at ExifRewriter class, for example.
As dnault mentioned in previous comment, Thumbnaliator library resolves the issue. But you should use correct input/output formats to avoid color change on this automatic rotation.
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(file.getContents());
Thumbnails.of(in)
.scale(1)
.toOutputStream(baos);
byte[] bytes = baos.toByteArray();
My solution is a combination of @PerLindberg's answer as well as @AntoineMartin's. I tried the other answers with Java 8 on Windows 10 and none seemed to do the trick. @AntoinMartin's com.drew.imaging solution was slow and image turned out black and white and full of artifacts. @PerLindberg's JavaXT solution did not read Exif 2.2 data.
1) Use com.drew.imaging to read exif information:
// Inner class containing image information
public static class ImageInformation {
public final int orientation;
public final int width;
public final int height;
public ImageInformation(int orientation, int width, int height) {
this.orientation = orientation;
this.width = width;
this.height = height;
}
public String toString() {
return String.format("%dx%d,%d", this.width, this.height, this.orientation);
}
}
public ImageInformation readImageInformation(File imageFile) throws IOException, MetadataException, ImageProcessingException {
Metadata metadata = ImageMetadataReader.readMetadata(imageFile);
Directory directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
JpegDirectory jpegDirectory = metadata.getFirstDirectoryOfType(JpegDirectory.class);
int orientation = 1;
if (directory != null) {
try {
orientation = directory.getInt(ExifIFD0Directory.TAG_ORIENTATION);
} catch (MetadataException me) {
logger.warn("Could not get orientation");
}
int width = jpegDirectory.getImageWidth();
int height = jpegDirectory.getImageHeight();
return new ImageInformation(orientation, width, height);
} else {
return null;
}
}
2) Use JavaXT to perform the rotation based on Exif data.
public void rotateMyImage(String imageDownloadFilenme);
File imageDownloadFile = new File(imgageDownloadFilenme);
Image image = new Image(imgageDownloadFilenme);
ImageInformation imageInformation = readImageInformation(imageDownloadFile);
if (imageInformation != null) {
rotate(imageInformation, image);
}
image.saveAs(imgageDownloadFilenme);
}
public void rotate(ImageInformation info, Image image) {
switch(info.orientation) {
case 1:
return;
case 2:
image.flip();
break;
case 3:
image.rotate(180.0D);
break;
case 4:
image.flip();
image.rotate(180.0D);
break;
case 5:
image.flip();
image.rotate(270.0D);
break;
case 6:
image.rotate(90.0D);
break;
case 7:
image.flip();
image.rotate(90.0D);
break;
case 8:
image.rotate(270.0D);
}
}
If you just want it to look right. You can just add a "rotate" -PI/2 (-90 degrees), PI/2 (90 degrees), or PI (+180 degrees) as necessary depending on the 'orientation' you've already extracted. The browser or any other program will correctly display the image as the orientation will have been applied and the metadata stripped from the thumbnail output.
The rotation functions above rotate the image data. And there is one more thing to do. You must set the width and height of BufferedImage. When rotating 90 degrees and 270 degrees, the width and height must be interchanged.
BufferedImage transformed;
switch (orientation) {
case 5:
case 6:
case 7:
case 8:
transformed = new BufferedImage(image.getHeight(), image.getWidth(), image.getType());
break;
default:
transformed = new BufferedImage(image.getWidth(), image.getHeight(), image.getType());
}
Based on the answers of Antoine Martin I created an own class for correcting the orientation of a given jpeg image (in my case as an input stream) based on the exif information of the image. With his solution I had the problem, that the colors of the resulting image were wrong, therefore I created this one. For retrieving the metadata of the image I used the metadata-extractor library.
I hope it will help some people.
public class ImageOrientationUtil {
/**
* Checks the orientation of the image and corrects it if necessary.
* <p>If the orientation of the image does not need to be corrected, no operation will be performed.</p>
* @param inputStream
* @return
* @throws ImageProcessingException
* @throws IOException
* @throws MetadataException
*/
public static BufferedImage correctOrientation(InputStream inputStream) throws ImageProcessingException, IOException, MetadataException {
Metadata metadata = ImageMetadataReader.readMetadata(inputStream);
if(metadata != null) {
if(metadata.containsDirectoryOfType(ExifIFD0Directory.class)) {
// Get the current orientation of the image
Directory directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
int orientation = directory.getInt(ExifIFD0Directory.TAG_ORIENTATION);
// Create a buffered image from the input stream
BufferedImage bimg = ImageIO.read(inputStream);
// Get the current width and height of the image
int[] imageSize = {bimg.getWidth(), bimg.getHeight()};
int width = imageSize[0];
int height = imageSize[1];
// Determine which correction is needed
AffineTransform t = new AffineTransform();
switch(orientation) {
case 1:
// no correction necessary skip and return the image
return bimg;
case 2: // Flip X
t.scale(-1.0, 1.0);
t.translate(-width, 0);
return transform(bimg, t);
case 3: // PI rotation
t.translate(width, height);
t.rotate(Math.PI);
return transform(bimg, t);
case 4: // Flip Y
t.scale(1.0, -1.0);
t.translate(0, -height);
return transform(bimg, t);
case 5: // - PI/2 and Flip X
t.rotate(-Math.PI / 2);
t.scale(-1.0, 1.0);
return transform(bimg, t);
case 6: // -PI/2 and -width
t.translate(height, 0);
t.rotate(Math.PI / 2);
return transform(bimg, t);
case 7: // PI/2 and Flip
t.scale(-1.0, 1.0);
t.translate(height, 0);
t.translate(0, width);
t.rotate( 3 * Math.PI / 2);
return transform(bimg, t);
case 8: // PI / 2
t.translate(0, width);
t.rotate( 3 * Math.PI / 2);
return transform(bimg, t);
}
}
}
return null;
}
/**
* Performs the tranformation
* @param bimage
* @param transform
* @return
* @throws IOException
*/
private static BufferedImage transform(BufferedImage bimage, AffineTransform transform) throws IOException {
// Create an transformation operation
AffineTransformOp op = new AffineTransformOp(transform, AffineTransformOp.TYPE_BICUBIC);
// Create an instance of the resulting image, with the same width, height and image type than the referenced one
BufferedImage destinationImage = new BufferedImage( bimage.getWidth(), bimage.getHeight(), bimage.getType() );
op.filter(bimage, destinationImage);
return destinationImage;
}
}
精彩评论