JAVA PDF转图片,以及几种方案对比
背景
项目需要将PDF转为图片存储,在网上搜索,找到了三种方案(这里链接都是找的github上的地址):
- pdfbox,开源软件,apache社区在维护,还比较活跃
- icepdf, 商业软件,但是github上有开源版本,最近一年还有提交
- jpedal, 商业软件,但是有LGPL版本的,很不活跃,上一个版本在2020年,但是没有上传到中央maven仓。
最终选择的方案是pdfbox。具体每种方案的实现分别说明。
PDFBOX
pdfbox是一个开源的转换实现,截止我使用时,在mvn库上看到的最新版本是3.0.0-alpha3
。
需要注意网上的很多示例还是针对2.x
的版本,3.x
版本接口有所变化,我这里是3.x
版本。
pdfbox代码实现
pom引用
<!-- https://mvnrepository.com/artifact/org.apache.pdfbox/pdfbox -->
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>3.0.0-alpha3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.pdfbox/fontbox -->
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>fontbox</artifactId>
<version>3.0.0-alpha3</version>
</dependency>
转换代码
实际使用时,碰到两个问题
- 发现压缩普通pdf没有问题,但是压缩超大pdf(300MB)时,出现内存溢出问题。
- 中文字体无法转换
这里先贴出初始代码,后面再贴出修改方案
//bis为一个InputStream,实际来源可能为文件、下载等
InputStream bis = .......
//这里使用了lombok的@Cleanup标记
@Cleanup PDDocument pdf = Loader.loadPDF(bis, MemoryUsageSetting.setupTempFileOnly());
//https://issues.apache.org/jira/browse/PDFBOX-3700, 这里参考这个问题进行内存优化
pdf.setResourceCache(null);
PDFRenderer pdfRenderer = new PDFRenderer(pdf);
pdfRenderer.setSubsamplingAllowed(true);
for (int i = 0; i < pdf.getNumberOfPages(); i++) {
BufferedImage image = pdfRenderer.renderImageWithDPI(i, 120);
String fileName = String.format("%d-%s.jpg", i, pdfFileId);
//这里创建了一个fileItem用于上传,也可以用本地文件方式创建输出流
FileItem fileItem = factory.createItem("file",
MediaType.MULTIPART_FORM_DATA_VALUE,
true,
fileName);
try (OutputStream os = fileItem.getOutputStream()) {
ImageIO.write(image, "JPG", os);
}
MultipartFile multi = new CommonsMultipartFile(fileItem);
//上传保存该MultipartFile,不在文本范围内
}
内存不足问题修改
简单分析了一下,也找了一些资料,初步分析代码并没有内存未释放问题,可能是pdfbox实现本身比较消耗内存,在解析pdf过程中会缓存一些信息导致内存利用率高。
我部署使用的机器为一台16GB内存的主机,如果主机内存足够大(如32G),个人猜想应该就不会出现该问题。
但是受资源限制,只能做一些内存优化,这里我尝试过单纯System.gc()
,并不能完全释放内存,最终选择每处理10页左右,重新创建PDFDocument对象,问题没有再出现。
修改后代码如下:
//bis为一个InputStream,实际来源可能为文件、下载等
InputStream bis = .......
int i = 0;
PDDocument pdf = null;
PDFRenderer pdfRenderer = null;
try {
while (true) {
if (0 == (i % 8)) {
if (null != pdf) {
//回收内存,重新处理
pdf.close();
pdf = null;
pdfRenderer = null;
}
log.info("GC.......");
System.gc();
}
if (null == pdf) {
bis.reset();
pdf = Loader.loadPDF(bis, MemoryUsageSetting.setupTempFileOnly());
pdf.setResourceCache(new DefaultResourceCacheExt());
pdfRenderer = new PDFRenderer(pdf);
pdfRenderer.setSubsamplingAllowed(true);
}
if (i >= pdf.getNumberOfPages()) {
break;
}
BufferedImage image = pdfRenderer.renderImageWithDPI(i, 120);
String fileName = String.format("%d-%s.jpg", i, pdfFileId);
//图片转存
FileItem fileItem = factory.createItem("file",
MediaType.MULTIPART_FORM_DATA_VALUE,
true,
fileName);
try (OutputStream os = fileItem.getOutputStream()) {
ImageIO.write(image, "JPG", os);
}
MultipartFile multi = new CommonsMultipartFile(fileItem);
//对MutiparFile进行保存
i++;
}
} finally {
if (null != pdf) {
pdf.close();
}
}
字体问题修改
这个问题在网上就能找到不少方案,这里简单总结一下
- 安装字体库, 如果已安装,则跳过此步
yum install fontconfig
2.安装更新字体命令, 如果已安装,则跳过此步
yum install mkfontscale
- 新建目录、上传中文字体
mkdir /usr/share/fonts/Chinese
# 切换到中文目录下 将Windows中文字体上传, 可以把Windows下的C:\Windows\Fonts中的文件拷贝到这里
cd /usr/share/fonts/Chinese
# 该目录及其下所有文件需要有执行权限
chmod -R 755 /usr/share/fonts/Chinese
- 重新建立字体索引、更新缓存
mkfontscale
mkfontdir
fc-cache
5、查看字体是否安装成功
fc-list :lang=zh
ICEPDF
搜索java pdf转图片能找到很多icepdf相关内容,但是想找(白嫖)一个商业版本,只在csdn上有,但是要的积分太多了。。
惊喜的是在github上竟然有开源版,而且看起来还有人在维护,虽然缺少商业版的一些组件,但是对于转图片功能已经够了。
虽然最终没有选择方案,但是使用下来也是能用的程度。
优点是转换速度不错,内存占用也比较好。
缺点是图片文字等,转换出来的还原度比pdfbox稍差一点,且生成相同质量、格式的图片,icepdf转换出来的比pdfbox大一些。
ICEPDF代码
ICEPDF pom引用
真正使用时,可以去maven上找一找是否有更新的版本
<dependency>
<groupId>com.github.pcorless.icepdf</groupId>
<artifactId>icepdf-core</artifactId>
<version>7.0.0</version>
</dependency>
<dependency>
<groupId>com.github.pcorless.icepdf</groupId>
<artifactId>icepdf-viewer</artifactId>
<version>7.0.0</version>
</dependency>
ICEPDF 代码逻辑
//bis为一个InputStream,实际来源可能为文件、下载等
InputStream bis = .......
Document document = new Document();
try {
document.setInputStream(bis, null);
float scale = 2.0f;//缩放比例,影响输出图片大小
float rotation = 0f;//旋转角度
for (int i = 0; i < document.getNumberOfPages(); i++) {
BufferedImage image = (BufferedImage)
document.getPageImage(i, GraphicsRenderingHints.SCREEN, org.icepdf.core.pobjects.Page.BOUNDARY_CROPBOX, rotation, scale);
try {
String fileName = String.format("%d-%s.jpg", i, pdfFileId);
//图片转存,也可以直接创建本地文件File进行保存
FileItem fileItem = factory.createItem("file",
MediaType.MULTIPART_FORM_DATA_VALUE,
true,
fileName);
try (OutputStream os = fileItem.getOutputStream()) {
ImageIO.write(image, "JPG", os);
}
MultipartFile multi = new CommonsMultipartFile(fileItem);
//这里可以上传或保存该MultiparFile
} catch (IOException e) {
e.printStackTrace();
}
}
} finally {
if (null != document) {
document.dispose();
}
}
JPEDAL
看起来是比较古老的代码了,maven上最后一个LGPL版本是2012年的,如果要用最新的版本,需要到github上去下载自己打包(地址)
最终测试速度和内存占用还可以,但是中文转换始终有乱码,按网络教程尝试了安装字体,增加字体映射,都无效。。
所以最终没有采用该方案。
但是还是把代码在这里贴一下,如果有其他人有方案解决中文问题,也可采用看看。
JPEDAL代码
JPEDAL pom引用
这里我是下载下来自己编译的最新版,所以用的<scope>system</scope>
。
我也上传到了CSDN,链接地址。
<dependency>
<groupId>org.jpedal</groupId>
<artifactId>jpedal_lgpl</artifactId>
<version>4.92-p13</version>
<scope>system</scope>
<systemPath>${project.basedir}/../lib/jpedal_lgpl-4.92-p13.jar</systemPath>
</dependency>
JPEDAL 代码逻辑
//bis为一个InputStream,实际来源可能为文件、下载等
InputStream bis = .......
//true as we are rendering page
PdfDecoder decodePdf = new PdfDecoder(true);
// /**optional JAI code for faster rendering*/
// org.jpedal.external.ImageHandler myExampleImageHandler = new org.jpedal.examples.handlers.ExampleImageDrawOnScreenHandler();
// decodePdf.addExternalHandler(myExampleImageHandler, Options.ImageHandler);
//mappings for non-embedded fonts to use
//这里是网上找到的中文转换方法,但是没有用
FontMappings.setFontReplacements();
//don't bother to extract text and images
decodePdf.setExtractionMode(0, 1.5f);
try {
decodePdf.openPdfFileFromInputStream(bis, false);
int start = 1, end = decodePdf.getPageCount();
for (int i = start; i < end + 1; i++) {
BufferedImage image = decodePdf.getPageAsImage(i);
try {
String fileName = String.format("%d-%s.jpg", i, pdfFileId);
//图片转存,也可以创建本地文件File进行保存
FileItem fileItem = factory.createItem("file",
MediaType.MULTIPART_FORM_DATA_VALUE,
true,
fileName);
try (OutputStream os = fileItem.getOutputStream()) {
ImageIO.write(image, "JPG", os);
}
MultipartFile multi = new CommonsMultipartFile(fileItem);
//对生成的MultipartFile进行保存
......
} catch (IOException e) {
e.printStackTrace();
}
}
} catch (PdfException e) {
e.printStackTrace();
} finally {
if (null != decodePdf) {
decodePdf.closePdfFile();
}
}
总结
优缺点对比
- PDFBOX
- 优点: 支持中文,转换图片大小相对较小,转换效果好;开发者相对活跃
- 缺点: 转换速度慢,内存占用大
- ICEPDF
- 优点: 支持中文,转换速度较快,内存占用较小;代码仍有在维护
- 缺点: 转换相比PDFBOX略有失真,图片大小也稍大一些
- JPEDAL
- 优点: 转换速度较快,内存占用较小
- 缺点: 不支持中文,几乎没有在维护