本文作者:薛定喵君
前言
项目在使用 kkFileView 时接到反馈说部分 PDF 在预览时没有内容,显示空白图片。
查看官方issue
也发现很多类似问题,但是也没有详尽好用的解决办法。
仅仅修改依赖增加特殊标准图片的处理会降低页面打开速度,因为图片转换操作比较耗时,所以我们还需要把图片转换操作改成异步并使用多线程去增加转换效率。
下面介绍一下如何更好地处理这个 PDF 特殊图片解析问题。
项目修改
- 依赖添加。
PDF 以图片模式预览时无内容是因为包含 JPEG2000 标准的图片,而 kk 并没有添加此类图片的解析依赖,所以我们要在pom.xml
添加相关依赖,这个与网上搜到的方法大致相同。
<dependency>
<groupId>com.github.jai-imageio</groupId>
<artifactId>jai-imageio-core</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>jbig2-imageio</artifactId>
<version>3.0.2</version>
</dependency>
<dependency>
<groupId>com.github.jai-imageio</groupId>
<artifactId>jai-imageio-jpeg2000</artifactId>
<version>1.3.0</version>
</dependency>
- 图片转换代码优化。
接到转换请求之后根据线程数及 PDF 总页数分配图片转换任务给每个线程,已经转换完成的页码不会重复转换。
/**
* pdf文件转换成jpg图片集
* @param pdfFilePath pdf文件路径
* @param pdfName pdf文件名称
* @param baseUrl 基础访问地址
* @return 图片访问集合
*/
public List<String> pdf2jpg(String pdfFilePath, String pdfName, String baseUrl) {
List<String> imageUrls = new ArrayList<>();
Integer imageCount = this.getConvertedPdfImage(pdfFilePath);
String imageFileSuffix = ".jpg";
String pdfFolder = pdfName.substring(0, pdfName.length() - 4);
String urlPrefix;
try {
urlPrefix = baseUrl + URLEncoder.encode(pdfFolder, uriEncoding).replaceAll("\\+", "%20");
} catch (UnsupportedEncodingException e) {
logger.error("UnsupportedEncodingException", e);
urlPrefix = baseUrl + pdfFolder;
}
// 如果当前pdf已缓存,则直接返回
try {
PDDocument doc = PDDocument.load(new File(pdfFilePath));
PDFRenderer pdfRendererMulti = new PDFRenderer(doc);
int pageCount = doc.getNumberOfPages();
int index = pdfFilePath.lastIndexOf(".");
String folder = pdfFilePath.substring(0, index);
for (int i = 0; i < pageCount; i++) {
imageUrls.add(urlPrefix + "/" + i + imageFileSuffix);
}
Integer pdf2jpgLock = this.getConvertedPdfImage(pdfFilePath.concat("_LOCK"));
if (pdf2jpgLock != null && pdf2jpgLock > 0 || (imageCount != null && imageCount > 0)) {
return imageUrls;
}
File path = new File(folder);
if (!path.exists() && !path.mkdirs()) {
logger.error("创建转换文件【{}】目录失败,请检查目录权限!", folder);
}
CompletableFuture.runAsync(() -> {
List<CompletableFuture> futures = new ArrayList<>();
for (int i = 0; i < pageCount; i++) {
int finalI = i;
CompletableFuture<String> future = CompletableFuture.supplyAsync(() ->
this.pdf2jpg(pdfRendererMulti, pdfFilePath, pdfName, baseUrl, finalI), commonThreadPool);
futures.add(future);
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
this.addConvertedPdfImage(pdfFilePath, pageCount);
this.addConvertedPdfImage(pdfFilePath.concat("_LOCK"), 0);
try {
doc.close();
logger.info("doc close");
} catch (IOException e) {
logger.error("doc close error", e);
}
}, commonThreadPool);
} catch (Exception e) {
this.addConvertedPdfImage(pdfFilePath.concat("_LOCK"), 0);
logger.error("Convert pdf to jpg exception, pdfFilePath:{}", pdfFilePath, e);
}
return imageUrls;
}
/**
* pdf文件转换成jpg图片集
* @param pdfFilePath pdf文件路径
* @param pdfName pdf文件名称
* @param baseUrl 基础访问地址
* @param pageIndex 当前页
* @return 图片访问集合
*/
public String pdf2jpg(PDFRenderer pdfRendererMulti, String pdfFilePath, String pdfName, String baseUrl, int pageIndex) {
logger.info("current thread {}, currentIndex:{}", Thread.currentThread().getName(), pageIndex);
this.addConvertedPdfImage(pdfFilePath.concat("_LOCK"), pageIndex + 1);
String imageFileSuffixMulti = ".jpg";
String pdfFolder = pdfName.substring(0, pdfName.length() - 4);
String urlPrefix;
try {
urlPrefix = baseUrl + URLEncoder.encode(pdfFolder, uriEncoding).replaceAll("\\+", "%20");
} catch (UnsupportedEncodingException e) {
logger.error("UnsupportedEncodingException", e);
urlPrefix = baseUrl + pdfFolder;
}
// 判断文件是否已存在,已存在直接返回
String imageUrl = urlPrefix + "/" + pageIndex + imageFileSuffixMulti;
File path = new File(imageUrl);
if (path.exists()) {
logger.info("{} 文件已存在!", imageUrl);
return imageUrl;
}
// 图片不存在需要转换
try {
int index = pdfFilePath.lastIndexOf(".");
String folder = pdfFilePath.substring(0, index);
String imageFilePath = folder + File.separator + pageIndex + imageFileSuffixMulti;
BufferedImage imageResource = pdfRendererMulti.renderImageWithDPI(pageIndex, 105, ImageType.RGB);
ImageIOUtil.writeImage(imageResource, imageFilePath, 105);
// 释放对象
imageResource.getGraphics().dispose();
imageResource = null;
} catch (IOException e) {
this.addConvertedPdfImage(pdfFilePath.concat("_LOCK"), 0);
logger.error("Convert pdf to jpg exception, pdfFilePath:{}", pdfFilePath, e);
}
return imageUrl;
}
- 页面图片加载优化。
正在转换的图片是无法正常显示的,所以在加载出错时隔一段时间再去请求图片,直到图片转换完成可以成功显示为止。
<!-- 在图片加载标签内添加加载出错事件处理 -->
<div class="img-area">
<img
class="my-photo"
alt="loading"
onerror="imgError(this)"
data-src="${img}"
src="images/loading.gif"
/>
</div>
图片加载报错处理:
// 图片加载出错时默认显示加载动画,6秒后显示原图
function imgError(img) {
img.setAttribute("src", "images/loading.gif") let t = setTimeout(function () {
img.setAttribute("src", $(img).data('src')) clearTimeout(t) }, 6000)
}
建议
本文只是提供一个修改思路,在实际使用过程中会略微减慢 PDF 的预览速度(图片解析需要时间),原本正常的图片也会打开地慢一点,如果确实有相关特殊 PDF 的预览需求可以参考处理。