74

I have some server code that is generating 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:

  1. Use ImageMagick to generate the thumbnails. The downside is it requires installed more software on our servers.
  2. Read the EXIF Orientation data is code and rotate the thumbnail appropriately.

Does anyone know of any other options?

RealSkeptic
  • 33,993
  • 7
  • 53
  • 79
hvgotcodes
  • 118,147
  • 33
  • 203
  • 236
  • 2
    If you just want a batch command-line option, imagickmagick can do this. Look into the `-auto-orient` command line flag. If you're transforming jpegs and want to avoid problems with re-compression, you can use `jhead` to do this, as well. `jhead -autorot *.jpg` should do what you need. I'm afraid I don't have a java solution, though... – Joe Kington May 06 '11 at 17:17
  • @joe, in the end all i want is for the thumbnails to 'look right'. if possible, id like to solve this issue by somehow making the browser realize that the thing is oriented. – hvgotcodes May 07 '11 at 18:55

9 Answers9

77

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

Antoine Martin
  • 1,137
  • 10
  • 6
  • 1
    exactly what i did, except i used the thumbnailarator library, which has a rotate method in it. Since you took the time to show code for a bounty, you get it. – hvgotcodes May 15 '11 at 18:56
  • Note that in `readImageInformation`, `directory` (and possibly also `jpegDirectory`) can be `null`. – Samuel Aug 10 '12 at 11:39
  • Thanks for the answer, it's almost working for me. Correct me if I'm mistaken though, but should the colormodel line in transformInage be: BufferedImage destinationImage = op.createCompatibleDestImage(image, (image.getType() == BufferedImage.TYPE_BYTE_GRAY)? null : image.getColorModel()); – jsaven Feb 02 '13 at 05:12
  • Hmm, the color model is way off for me. Ending up with a CMYK JPG which renders very badly or not at all. – Sam Barnum Sep 16 '13 at 19:23
  • 3
    Image's color have been changed after executing the transform method, why? – Liam Mazy Jun 16 '15 at 09:45
  • Just a note: `getFirstDirectoryOfType()` has replaced `getDirectory()` in the metadata extractor. – Decoded Apr 20 '16 at 15:03
  • @hvgotcodes Can you provide me with the code that you did using Thumbnailator – The Cloud Guy Dec 21 '16 at 07:23
  • To transform image i use: private static BufferedImage transformImage(BufferedImage bsrc, AffineTransform at) throws Exception { BufferedImage bdest = new BufferedImage(bsrc.getWidth(), bsrc.getHeight(), bsrc.getType()); Graphics2D g = bdest.createGraphics(); g.drawRenderedImage(bsrc, at); return bdest.getSubimage(0, 0, bsrc.getWidth(), bsrc.getHeight()); } – jlbofh Jan 26 '18 at 20:03
  • I'm half assuming you can't do this kind of EXIF-transform and save the same JPG losslessly, or...? – Halvor Holsten Strand Feb 22 '19 at 13:55
  • I had a problem at createCompatibleDestImage(), ti was trowing RasterFormatException. So I replaced it by: BufferedImage destinationImage = new BufferedImage(image.getWidth(), image.getHeight(), image.getType()); – Derzu Sep 21 '19 at 22:46
  • The transformImage part does not work for me. It produces an BufferedImage without any data (bytes). – cnmuc Jun 29 '20 at 06:20
  • I am getting the exception "javax.imageio.IIOException: Bogus input colorspace" when saving the transformed image. – Chilly Code Jul 30 '20 at 12:50
26

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();
dnault
  • 8,340
  • 1
  • 34
  • 53
  • Fantastic! I was not aware of this hidden capability. It works perfectly for auto-rotating images as they're read in. And it was way easier than working through metadata-extractor. – Jeff Oct 21 '15 at 02:31
  • 2
    Unfortunately, Thumbnailator's image quality after rotation is poor. https://github.com/coobird/thumbnailator/issues/101 – DylanYi Feb 11 '20 at 02:26
9

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.

Per Lindberg
  • 737
  • 8
  • 8
  • 1
    Sadly javaxt doesnt have a maven repo as far as I can tell (maybe I missed it?), which means I would need to do a bunch of custom build stuff just to use em :( – Gus May 10 '14 at 16:52
  • 1
    Unfortunately, the JavaXT core library does not rotate the image correctly in some cases. It works on some images, but not on others. One image that works has ExifVersion=Exif Version 2.1, one image that does not work has ExifVersion=Exif Version 2.2. Perhaps that's the problem, JavaXT core does not handle version 2.2. I don't know. – Per Lindberg Jun 10 '14 at 12:38
  • 2
    Also, image.saveAs() uses memory-mapped file, so the result file can be empty or locked in Windows. Saving via a byte array seems to work better. But I'm throwing out JavaXT anyway. – Per Lindberg Jun 10 '14 at 12:57
  • 1
    javaxt is available for maven: https://mvnrepository.com/artifact/javaxt/javaxt-core – yglodt Dec 17 '18 at 13:46
  • @PerLindberg, see my answer below. The hybrid solution works for both Exif 2.1 and Exid 2.2 and leverages your recommendation. – Ted Gulesserian May 06 '19 at 15:39
  • The javaxt.io.Image class should work for all Exif metadata. The problem is that it relies on standard ImageIO plugin from Java. A simple workaround is to use the JPEG ImageIO plugin from [TwelveMonkeys](https://github.com/haraldk/TwelveMonkeys). More info and workaround described [here](http://javaxt.com/javaxt-core/io/Image#MetaData_Limitations) – Peter Nov 03 '19 at 15:49
4

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);
    }

}
Ted Gulesserian
  • 277
  • 2
  • 8
  • Alternatively, you could use JavaXT with [TwelveMonkeys](https://github.com/haraldk/TwelveMonkeys). TwelveMonkeys provides a JPEG ImageIO plugin that allows javaxt.io.Image to handle most, if not all Exif metadata. More info and workaround described [here](http://javaxt.com/javaxt-core/io/Image#MetaData_Limitations). – Peter Nov 03 '19 at 15:56
3

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();
Anton Petrovskyi
  • 442
  • 5
  • 18
3

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.

quietmint
  • 13,885
  • 6
  • 48
  • 73
Alex Gitelman
  • 24,429
  • 7
  • 52
  • 49
1

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.

karmakaze
  • 34,689
  • 1
  • 30
  • 32
  • karmakaze -- yes, i think I am going to have to do that -- you are talking about on the server? I am worried that different cameras might have different metadata -- is that valid? Also, are there any image formats other than jpg that will require this? – hvgotcodes May 12 '11 at 12:29
  • According to Wikipedia, Exif is for JPEG and TIFF image files, as well as some audio file formats. It is not supported in JPEG 2000, PNG, or GIF. Many native formats used by digital cameras will have Exif tags. – karmakaze May 13 '11 at 00:25
0

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());
    }
HeeGu
  • 1
  • 1
-1

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;
}
}
Florian
  • 110
  • 7
  • This will not work as you are reading the input stream twice, once to obtain the `Metadata` and then to create the `BufferedImage`. You would need to either copy the stream or use a variant that can be reset and read again. The destination image may also have the incorrect bounds for something like a 90 degree CW or CCW rotation unless it is square, you need to obtain the new bounds from the transform operation when creating the destination image. – Robert Hunt Jan 04 '18 at 10:58
  • Will work only for square images since the dimensions of rotated-image are kept same as original-image. – nishantbhardwaj2002 Nov 16 '18 at 04:26