在使用Thymeleaf生成PDF文档并嵌入图片时,我遇到了一个常见但棘手的问题:某些图片在PDF中显示时会出现方向错误,最典型的是竖拍的照片会横着显示。
这个问题的根本原因在于EXIF(Exchangeable Image File Format)方向信息的处理。现代智能手机和数码相机在拍摄照片时,会在图片文件中记录拍摄时的设备方向信息(EXIF Orientation标签)。然而,图片的实际像素数据可能并没有真正旋转,而是依靠EXIF信息告诉显示软件应该如何旋转显示。
大多数图片查看器和浏览器能够正确识别并应用EXIF方向信息,但在PDF生成过程中,这些EXIF信息往往会被忽略,导致图片以原始像素方向显示,从而出现旋转错误的情况。
为了彻底解决这个问题,我开发了一个ImageProcessingUtil
工具类,核心思路是:在将图片转换为Base64编码之前,先读取EXIF方向信息,然后对图片像素数据进行实际的旋转处理。
整个解决方案分为三个关键步骤:
- 读取图片的EXIF方向信息 - 使用metadata-extractor库解析图片的EXIF数据
- 根据方向值对图片进行旋转处理 - 使用Java的AffineTransform进行几何变换
- 转换为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>
技术亮点
- 完整的方向支持: 处理了全部8种EXIF方向值,覆盖所有可能的旋转和翻转情况
- 降级机制: 如果EXIF读取失败,会自动降级到直接读取原始图片,保证基本功能可用
- 宽高自适应: 对于90度和270度旋转,自动交换图片的宽高,确保显示正确
- 性能优化: 使用BufferedImage在内存中处理,避免多次磁盘IO
- 详细日志: 记录EXIF方向值和异常信息,便于问题排查
总结
通过这个工具类,我们成功解决了Thymeleaf生成PDF时图片方向错误的问题。核心思路是在图片编码前就完成方向修正,将EXIF信息"固化"到图片像素中,从而避免依赖PDF渲染器对EXIF的支持。这个方案不仅适用于PDF生成,也可以用于任何需要图片方向修正的场景。
文章评论