Thymeleaf生成PDF时图片旋转方向错误的解决方案

2025-10-10 22点热度 0人点赞 0条评论

问题背景

在使用Thymeleaf生成PDF文档并嵌入图片时,我遇到了一个常见但棘手的问题:某些图片在PDF中显示时会出现方向错误,最典型的是竖拍的照片会横着显示。

问题根源

这个问题的根本原因在于EXIF(Exchangeable Image File Format)方向信息的处理。现代智能手机和数码相机在拍摄照片时,会在图片文件中记录拍摄时的设备方向信息(EXIF Orientation标签)。然而,图片的实际像素数据可能并没有真正旋转,而是依靠EXIF信息告诉显示软件应该如何旋转显示。

大多数图片查看器和浏览器能够正确识别并应用EXIF方向信息,但在PDF生成过程中,这些EXIF信息往往会被忽略,导致图片以原始像素方向显示,从而出现旋转错误的情况。

解决方案

为了彻底解决这个问题,我开发了一个ImageProcessingUtil工具类,核心思路是:在将图片转换为Base64编码之前,先读取EXIF方向信息,然后对图片像素数据进行实际的旋转处理

实现思路

整个解决方案分为三个关键步骤:

  1. 读取图片的EXIF方向信息 - 使用metadata-extractor库解析图片的EXIF数据
  2. 根据方向值对图片进行旋转处理 - 使用Java的AffineTransform进行几何变换
  3. 转换为Base64编码 - 将处理后的图片编码供Thymeleaf模板使用

完整代码实现

ImageProcessingUtil.java

/**
 * 图片处理工具类,处理图片方向等问题
 *
 * @author Terry
 */
public class ImageProcessingUtil {
    private static final Logger log = LoggerFactory.getLogger(ImageProcessingUtil.class);

    /**
     * 读取图片并处理方向问题,转换为Base64编码
     *
     * @param imageFile 图片文件
     * @return Base64编码的图片数据
     */
    public static String readImageAsBase64(File imageFile) {
        try {
            // 获取文件扩展名
            String fileExtension = getImageExtension(imageFile);

            // 使用ImageIO读取图片
            BufferedImage originalImage = ImageIO.read(imageFile);
            if (originalImage != null) {
                // 处理EXIF方向信息
                BufferedImage correctedImage = correctOrientation(imageFile, originalImage);

                // 将图片转换为Base64
                ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                ImageIO.write(correctedImage, fileExtension, outputStream);
                byte[] imageBytes = outputStream.toByteArray();
                return Base64.getEncoder().encodeToString(imageBytes);
            } else {
                // 如果无法读取为BufferedImage,则使用原始方法
                byte[] fileContent = Files.readAllBytes(imageFile.toPath());
                return Base64.getEncoder().encodeToString(fileContent);
            }
        } catch (IOException e) {
            log.error("处理图片失败", e);
            // 如果图片处理失败,使用原始方法
            try {
                byte[] fileContent = Files.readAllBytes(imageFile.toPath());
                return Base64.getEncoder().encodeToString(fileContent);
            } catch (IOException ex) {
                log.error("读取图片文件失败", ex);
                return null;
            }
        }
    }

    /**
     * 获取图片文件的扩展名
     *
     * @param imageFile 图片文件
     * @return 扩展名(不含点)
     */
    public static String getImageExtension(File imageFile) {
        String fileName = imageFile.getName();
        String fileExtension = "";
        int i = fileName.lastIndexOf('.');
        if (i > 0) {
            fileExtension = fileName.substring(i + 1).toLowerCase();
        }
        return fileExtension;
    }

    /**
     * 根据EXIF方向信息修正图片方向
     *
     * @param imageFile     原始图片文件(用于读取EXIF信息)
     * @param originalImage 原始图片数据
     * @return 修正方向后的图片
     */
    private static BufferedImage correctOrientation(File imageFile, BufferedImage originalImage) {
        try {
            // 读取图片的EXIF信息
            Metadata metadata = ImageMetadataReader.readMetadata(imageFile);
            ExifIFD0Directory exifIFD0 = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);

            if (exifIFD0 != null && exifIFD0.containsTag(ExifIFD0Directory.TAG_ORIENTATION)) {
                int orientation = exifIFD0.getInt(ExifIFD0Directory.TAG_ORIENTATION);
                log.info("图片 {} 的EXIF方向值为: {}", imageFile.getName(), orientation);

                int width = originalImage.getWidth();
                int height = originalImage.getHeight();

                // 根据方向值进行旋转处理
                AffineTransform affineTransform = new AffineTransform();

                switch (orientation) {
                    case 1: // 正常方向,不需要处理
                        return originalImage;
                    case 2: // 水平翻转
                        affineTransform.scale(-1.0, 1.0);
                        affineTransform.translate(-width, 0);
                        break;
                    case 3: // 旋转180度
                        affineTransform.translate(width, height);
                        affineTransform.rotate(Math.PI);
                        break;
                    case 4: // 垂直翻转
                        affineTransform.scale(1.0, -1.0);
                        affineTransform.translate(0, -height);
                        break;
                    case 5: // 顺时针旋转90度后水平翻转
                        affineTransform.rotate(Math.PI / 2);
                        affineTransform.scale(-1.0, 1.0);
                        break;
                    case 6: // 顺时针旋转90度
                        affineTransform.translate(height, 0);
                        affineTransform.rotate(Math.PI / 2);
                        break;
                    case 7: // 顺时针旋转90度后垂直翻转
                        affineTransform.scale(-1.0, 1.0);
                        affineTransform.translate(-height, 0);
                        affineTransform.translate(0, width);
                        affineTransform.rotate(3 * Math.PI / 2);
                        break;
                    case 8: // 逆时针旋转90度
                        affineTransform.translate(0, width);
                        affineTransform.rotate(3 * Math.PI / 2);
                        break;
                    default:
                        return originalImage;
                }

                // 创建新的图片并应用变换
                BufferedImage correctedImage;
                if (orientation == 5 || orientation == 6 || orientation == 7 || orientation == 8) {
                    // 对于需要旋转90度或270度的情况,宽高需要互换
                    correctedImage = new BufferedImage(height, width, originalImage.getType());
                } else {
                    correctedImage = new BufferedImage(width, height, originalImage.getType());
                }

                Graphics2D g2d = correctedImage.createGraphics();
                g2d.transform(affineTransform);
                g2d.drawImage(originalImage, 0, 0, null);
                g2d.dispose();

                return correctedImage;
            }
        } catch (ImageProcessingException | IOException e) {
            log.error("读取EXIF信息失败", e);
        } catch (Exception e) {
            log.error("处理图片方向失败", e);
        }

        // 如果无法处理EXIF信息,则返回原始图片
        return originalImage;
    }
}

EXIF方向值详解

EXIF Orientation标签有8个可能的值,分别代表不同的旋转和翻转状态:

  • 值1: 正常方向,无需处理
  • 值2: 水平翻转
  • 值3: 旋转180度
  • 值4: 垂直翻转
  • 值5: 顺时针旋转90度后水平翻转
  • 值6: 顺时针旋转90度(最常见,竖拍照片)
  • 值7: 顺时针旋转90度后垂直翻转
  • 值8: 逆时针旋转90度

使用方法

在Thymeleaf模板中使用时非常简单:

// 在Controller或Service中
File imageFile = new File("/path/to/image.jpg");
String base64Image = ImageProcessingUtil.readImageAsBase64(imageFile);
model.addAttribute("imageData", base64Image);
<!-- 在Thymeleaf模板中 -->
<img th:src="'data:image/jpeg;base64,' + ${imageData}" />

依赖配置

需要在项目中添加metadata-extractor依赖:

<dependency>
    <groupId>com.drewnoakes</groupId>
    <artifactId>metadata-extractor</artifactId>
    <version>2.18.0</version>
</dependency>

技术亮点

  1. 完整的方向支持: 处理了全部8种EXIF方向值,覆盖所有可能的旋转和翻转情况
  2. 降级机制: 如果EXIF读取失败,会自动降级到直接读取原始图片,保证基本功能可用
  3. 宽高自适应: 对于90度和270度旋转,自动交换图片的宽高,确保显示正确
  4. 性能优化: 使用BufferedImage在内存中处理,避免多次磁盘IO
  5. 详细日志: 记录EXIF方向值和异常信息,便于问题排查

总结

通过这个工具类,我们成功解决了Thymeleaf生成PDF时图片方向错误的问题。核心思路是在图片编码前就完成方向修正,将EXIF信息"固化"到图片像素中,从而避免依赖PDF渲染器对EXIF的支持。这个方案不仅适用于PDF生成,也可以用于任何需要图片方向修正的场景。

 

admin

这个人很懒,什么都没留下

文章评论

您需要 登录 之后才可以评论