如何根据方向元数据旋转JPEG图像?

74

我有一些服务器代码,当上传图片时会生成缩略图。问题在于,当照片被拍摄并且相机/设备旋转时,即使完整尺寸的图片在任何图像查看软件中显示正确方向,缩略图也会被旋转。这只发生在JPG格式的图片上。

在OSX的预览中,我可以看到JPG格式的图片嵌入了方向元数据。当我使用ImageTools(Grails插件)生成缩略图时,缩略图中没有EXIF元数据,这就是为什么缩略图会出现旋转的原因。

通过线下对话,我了解到虽然读取EXIF元数据相对容易,但写入它却没有简单的方法,这就是为什么在生成JPG缩略图时会丢失数据。

所以似乎我有两个选择:

  1. 使用ImageMagick生成缩略图。缺点是需要在我们的服务器上安装更多软件。
  2. 在代码中读取EXIF方向数据,并适当旋转缩略图。

有人知道其他选项吗?


2
如果您只需要批处理命令行选项,imagickmagick可以做到这一点。请查看-auto-orient命令行标志。如果您正在转换jpeg并希望避免重新压缩的问题,也可以使用jhead来完成。jhead -autorot *.jpg应该可以满足您的需求。不过,很抱歉我没有Java解决方案... - Joe Kington
@joe,最终我只想让缩略图“看起来正确”。如果可能的话,我希望通过某种方式使浏览器意识到该物体的方向以解决此问题。 - hvgotcodes
9个回答

77

如果你想要旋转你的图像,我建议使用元数据提取库http://code.google.com/p/metadata-extractor/。你可以使用以下代码获取图像信息:

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

根据您检索到的方向,您可以将图像旋转和/或翻转到正确的方向。 EXIF方向的仿射变换通过以下方法给出:

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

图像的旋转可以通过以下方法实现:
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;
}

在服务器环境中,不要忘记使用 -Djava.awt.headless=true 运行。

1
除了我使用了具有旋转方法的Thumbnailator库之外,我做的就是与你一样。既然您花时间展示赏金的代码,您就得到它了。 - hvgotcodes
嗯,对我来说颜色模型偏差很大。最终得到的是一个CMYK的JPG格式,渲染效果非常糟糕,甚至根本无法渲染。 - Sam Barnum
3
执行变换方法后,图像的颜色发生了改变,为什么? - Liam Mazy
请注意:在元数据提取器中,getFirstDirectoryOfType()已经替换了getDirectory() - Decoded
我有点假设你不能进行这种EXIF转换并以无损方式保存相同的JPG文件,对吗? - Halvor Holsten Strand
显示剩余7条评论

26

Thumbnailator库支持EXIF方向标志。要以正确的方向读取全尺寸图像:

BufferedImage image = Thumbnails.of(inputStream).scale(1).asBufferedImage();

太棒了!我之前不知道这个隐藏的功能。它非常适合在读取图像时自动旋转,而且比使用元数据提取器要容易得多。 - Jeff
2
很遗憾,Thumbnailator旋转后的图像质量较差。https://github.com/coobird/thumbnailator/issues/101 - DylanYi

9
这可以通过使用JavaXT核心库的图像部分来轻松完成: (点击此处可跳转至JavaXT核心库的图像部分)
// 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);

就是这样了!

我曾试过使用Apache Commons Imaging,但那个实在是一团糟。JavaXT更加优雅。


1
可惜的是,据我所知,javaxt没有Maven存储库(也许我错过了?),这意味着我需要做大量自定义构建工作才能使用它们:( - Gus
1
JavaXT 核心库在某些情况下无法正确旋转图像。它可以处理一些图像,但是对于另一些图像则不行。其中一个可处理的图像具有 ExifVersion=Exif 版本 2.1,而一个不可处理的图像具有 ExifVersion=Exif 版本 2.2。也许这就是问题所在,JavaXT 核心库无法处理版本 2.2。我不确定。 - Per Lindberg
2
另外,image.saveAs() 使用内存映射文件,因此在 Windows 中结果文件可能为空或被锁定。通过字节数组保存似乎效果更好。但是我无论如何都要放弃 JavaXT。 - Per Lindberg
1
javaxt 可以在 Maven 上获取:https://mvnrepository.com/artifact/javaxt/javaxt-core - yglodt
@PerLindberg,请见下面的答案。混合解决方案适用于Exif 2.1和Exid 2.2,并利用了您的建议。 - Ted Gulesserian
javaxt.io.Image类应该适用于所有Exif元数据。问题在于它依赖于Java标准ImageIO插件。一个简单的解决方法是使用TwelveMonkeys的JPEG ImageIO插件。更多信息和解决方法请参见此处 - Peter

4
我的解决方案是结合了@PerLindberg和@AntoineMartin的答案。我在Windows 10上使用Java 8尝试了其他答案,但似乎都不能做到。@AntoinMartin的com.drew.imaging解决方案速度较慢,图像变成了黑白色并充满了伪影。@PerLindberg的JavaXT解决方案无法读取Exif 2.2数据。
1)使用com.drew.imaging读取exif信息:
// 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) 使用JavaXT根据Exif数据进行旋转。

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

}

或者,您可以使用JavaXT和TwelveMonkeys。 TwelveMonkeys提供了一个JPEG ImageIO插件,允许javaxt.io.Image处理大多数(如果不是全部)Exif元数据。更多信息和解决方法在此处描述。 - Peter

3

由于Exif中涉及专有内容,因此编写似乎很困难。

但是,您可以考虑另一种选项:

仅读取原始文件,但将方向标签写入缩略图。

Apache Sanselan似乎有一个不错的工具集来完成这个任务。

http://commons.apache.org/proper/commons-imaging/

例如,可以查看ExifRewriter类。


3
正如dnault在先前的评论中提到的,Thumbnaliator库可以解决这个问题。但是为了避免自动旋转导致颜色变化,您需要使用正确的输入/输出格式。
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(file.getContents());
Thumbnails.of(in)
    .scale(1)
    .toOutputStream(baos);
byte[] bytes = baos.toByteArray();

1

如果你只是想让它看起来正确,你可以根据已经提取的“方向”,添加一个“旋转”-PI/2(-90度),PI/2(90度)或PI(+180度)。浏览器或任何其他程序将正确显示图像,因为方向已经被应用并且元数据已从缩略图输出中剥离。


karmakaze -- 是的,我想我必须这样做 -- 你是在说服务器上吗?我担心不同的相机可能有不同的元数据 -- 这是有效的吗?此外,除了 jpg 之外,还有其他需要这样做的图像格式吗? - hvgotcodes
根据维基百科,Exif 适用于 JPEG 和 TIFF 图像文件,以及一些音频文件格式。它不支持 JPEG 2000、PNG 或 GIF。许多数码相机使用的本地格式将具有 Exif 标签。 - karmakaze

0
上述旋转函数会旋转图像数据。还有一件事情需要做,就是设置BufferedImage的宽度和高度。当旋转90度和270度时,宽度和高度必须互换。
    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());
    }

-1

基于Antoine Martin的答案,我创建了一个自己的类来根据图像的exif信息(在我的情况下作为输入流)纠正给定jpeg图像的方向。使用他的解决方案时,我遇到了问题,即结果图像的颜色不正确,因此我创建了这个。

为了检索图像的元数据,我使用了metadata-extractor库。

希望它能帮助一些人。

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

这样做是行不通的,因为你读取了两次输入流,一次是为了获取“元数据”,另一次是为了创建“BufferedImage”。你需要复制流或使用可以重置和重新读取的变体。目标图像可能也有错误的边界,例如90度顺时针或逆时针旋转,除非它是正方形的,否则在创建目标图像时需要从变换操作中获取新的边界。 - Robert Hunt
仅适用于正方形图像,因为旋转图像的尺寸与原始图像相同。 - nishantbhardwaj2002

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接