在使用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生成,也可以用于任何需要图片方向修正的场景。
文章评论