php预览word文档的实现 以及实现过程中遇到的各种坑
在做软件工程的课程设计的时候,我们小组选择做一个资料分享网站,网站最重要的功能当然就是上传文件和下载文件。但是这中间就需要一个比较重要的过程:预览。
预览最终结果是一张长图,很长很长的png图片。大致可以分为下面这几个步骤:
- 将WORD文档转为PDF
- 将PDF拆分,毕竟只是预览,而不是查看全部,这里我设置的是预览10页
- 将PDF按页转换为PNG图片
- 将所有的PNG图片合并成一张长图
四个步骤,分别需要用到不同的工具:
第一步:对于WORD转PDF,在度娘的帮助下,我们决定使用Java语言实现这一功能。使用开源的openoffice+jodconverter来对WORD进行转换。
首先我们要考虑php如何调用Java,很幸运,有一个叫做JavaBridge的东西为我们解决了这个问题,JavaBridge的使用在百度中有大量的博客教程(ps:虽然我对国内这些博客互相抄袭,还错误百出很看不习惯,但确实也有好的博客,并且数量很多);
然后就是要安装OpenOffice,这个很简单,无论是Windows还是Linux都不难(直接百度搜索openoffice安装即可);
最后下载jodconverter的jar包,编写程序将WORD转为PDF。
源码如下
import java.io.File;
import java.io.IOException;
import com.artofsolving.jodconverter.DocumentConverter;
import com.artofsolving.jodconverter.openoffice.connection.OpenOfficeConnection;
import com.artofsolving.jodconverter.openoffice.connection.SocketOpenOfficeConnection;
import com.artofsolving.jodconverter.openoffice.converter.OpenOfficeDocumentConverter;
public class PDFConverter {
/**
* 将WORD文档转换为PDF
* @param srcPath WORD文档路径
* @param desPath 目标PDF保存路径
* @param pages 转换页数
* @throws IOException
*/
public void Word2Pdf(String srcPath, String desPath) throws IOException {
// 源文件目录
File inputFile = new File(srcPath);
if (!inputFile.exists()) {
System.out.println("源文件不存在!");
System.out.println(srcPath + ", " + desPath);
return;
}
// 输出文件目录
File outputFile = new File(desPath);
if (!outputFile.getParentFile().exists()) {
outputFile.getParentFile().mkdirs();
}
// 调用openoffice服务线程
// String command = "C:\\Program Files (x86)\\OpenOffice 4\\program\\soffice.exe -headless -accept=\"socket,host=127.0.0.1,port=8100;urp;\"";
String command = "/opt/openoffice4/program/soffice -headless -accept=\"socket,host=127.0.0.1,port=8100;urp;\" -nofirststartwizard";
Process p = Runtime.getRuntime().exec(command);
// 连接openoffice服务
OpenOfficeConnection connection = new SocketOpenOfficeConnection(
"127.0.0.1", 8100);
connection.connect();
// 转换word到pdf
DocumentConverter converter = new OpenOfficeDocumentConverter(
connection);
converter.convert(inputFile, outputFile);
// 关闭连接
connection.disconnect();
// 关闭进程
p.destroy();
System.out.println("转换完成!");
}
}
在程序中我们创建一个线程来打开OpenOffice的服务
Process p = Runtime.getRuntime().exec(command);
这条代码的作用相当于在命令行(终端)输入command字符串,而command字符串就是我们启动openoffice服务的命令。后面连接openoffice然后利用它提供的接口将WORD转为PDF即可。(注意:
注释起来的command是Windows下的启动命令,因为Windows系统和linux系统中openoffice安装路径不同,所以需要使用不同的路径启动服务,所以在安装OpenOffice时一定要注意安装路径)
第二步:将一个大的PDF拆分为小的PDF,可能是PHP这方面的支持不够,也可能是我对Java很有好感,这一步我选择的是使用Apache的pdfbox结合Java实现的。
在apache官网中找到pdfbox,下载fontbox-2.0.15.jar、pdfbox-2.0.15.jar、commons-logging-1.2.jar这三个jar包,但是我在apahce官网上面并没有找到最后一个jar包,是在别人分享的百度云盘里面下载的,所以最后我会把我用到的所有jar包上传到百度云并提供永久下载链接;获得这三个jar包后编写程序;
源码如下
/**
* 将一个大pdf拆分为小pdf
* @param src 大PDF路径
* @param dest 小PDF保存路径
* @param pages 拆分页数
*/
public void split(String src, String dest, int pages) {
File srcFile = new File(src);
if(!srcFile.exists()) {
System.out.println("原文件不存在");
return;
}
// 如果目标路径的父目录不存在,则创建
File destFile = new File(dest);
if(!destFile.getParentFile().exists()) {
destFile.getParentFile().mkdirs();
}
try {
System.out.println("开始拆分");
// 加载原PDF文件
PDDocument pdf = PDDocument.load(srcFile, MemoryUsageSetting.setupTempFileOnly());
// 获取原PDF文件总页数
int pageCount = pdf.getPages().getCount();
// 当所需要的页数大于总页数时,按最大总页数进行拆分
if(pages > pageCount) {
pages = pageCount;
}
PDDocument newPdf = new PDDocument();
for(int i=0;i<pages;i++) {
newPdf.addPage(pdf.getPage(i));
}
newPdf.save(destFile);
pdf.close();
newPdf.close();
System.out.println("拆分结束");
} catch (IOException e) {
e.printStackTrace();
}
}
第三步:将PDF转为PNG图片,很幸运在pdfbox中提供了这样的功能,所以这一步不需要任何工具包
直接就可以编写代码
/**
* 将pdf转为png图片
* @param src 原pdf路径
* @param dest 图片的父目录
*/
public void Pdf2Png(String src, String dest) {
File srcFile = new File(src);
if(!srcFile.exists()) {
System.out.println("源文件不存在");
return;
}
File destFile = new File(dest);
if(!destFile.exists()) {
destFile.mkdirs();
}
try {
PDDocument doc = PDDocument.load(srcFile);
PDFRenderer renderer = new PDFRenderer(doc);
int pageCount = doc.getNumberOfPages();
for(int i=0;i<pageCount;i++){
BufferedImage image = renderer.renderImageWithDPI(i, 300);
File file = new File(dest + "\" + i + ".png");
ImageIO.write(image, "PNG", file);
}
doc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
由于第二步和第三步用到了同样的外部jar包,所以我把它们放在同一个项目,下面是完整的项目
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import org.apache.pdfbox.io.MemoryUsageSetting;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.PDFRenderer;
public class PDFProcess {
/**
* 将pdf转为png图片
* @param src 原pdf路径
* @param dest 图片的父目录
*/
public void Pdf2Png(String src, String dest) {
File srcFile = new File(src);
if(!srcFile.exists()) {
System.out.println("源文件不存在");
return;
}
File destFile = new File(dest);
if(!destFile.exists()) {
destFile.mkdirs();
}
try {
PDDocument doc = PDDocument.load(srcFile);
PDFRenderer renderer = new PDFRenderer(doc);
int pageCount = doc.getNumberOfPages();
for(int i=0;i<pageCount;i++){
BufferedImage image = renderer.renderImageWithDPI(i, 300);
File file = new File(dest + "\" + i + ".png");
ImageIO.write(image, "PNG", file);
}
doc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 将一个大pdf拆分为小pdf
* @param src 大PDF路径
* @param dest 小PDF保存路径
* @param pages 拆分页数
*/
public void split(String src, String dest, int pages) {
File srcFile = new File(src);
if(!srcFile.exists()) {
System.out.println("原文件不存在");
return;
}
// 如果目标路径的父目录不存在,则创建
File destFile = new File(dest);
if(!destFile.getParentFile().exists()) {
destFile.getParentFile().mkdirs();
}
try {
System.out.println("开始拆分");
// 加载原PDF文件
PDDocument pdf = PDDocument.load(srcFile, MemoryUsageSetting.setupTempFileOnly());
// 获取原PDF文件总页数
int pageCount = pdf.getPages().getCount();
// 当所需要的页数大于总页数时,按最大总页数进行拆分
if(pages > pageCount) {
pages = pageCount;
}
PDDocument newPdf = new PDDocument();
for(int i=0;i<pages;i++) {
newPdf.addPage(pdf.getPage(i));
}
newPdf.save(destFile);
pdf.close();
newPdf.close();
System.out.println("拆分结束");
} catch (IOException e) {
e.printStackTrace();
}
}
}
最后一个合并PNG,这个要求直接使用php即可实现,不过需要GD2扩展库,如果是Windows下这个扩展是自带的,Linux下需要自己安装扩展
<?php
/**
* 竖直方向上合并图片(合并为png格式),目标图片的宽度取所有图片中宽度最大的那张,高度取所有图片高度之和
* @param array 一组图片的路径
* @param String $dest 合并之后的图片地址
*/
public static function merge($source, String $dest) {
$width = 0; // 合并后图片的宽
$height = 0; // 合并后图片的高
$arr = array();
for($i=0;$i<count($source);$i++) {
$info = getimagesize($source[$i]); // 获取图片详细信息(宽、高、类型等)
// 获取图片类型
$type = image_type_to_extension($info[2], false);
$fun = "Imagecreatefrom{$type}"; // 根据图片类型构造一个符合该类型图片的读取函数
$arr[$i]['source'] = $fun($source[$i]);
$arr[$i]['size'] = $info;
if($arr[$i]['size'][0] > $width) {
$width = $arr[$i]['size'][0];
}
$height += $arr[$i]['size'][1];
}
$merge = imagecreate($width, $height+10*count($source));
$space = imagecreate($width, 10);
$dst_x = 0;
$dst_y = 0;
for($i=0;$i<count($source);$i++) {
imagecopy($merge, $arr[$i]['source'], $dst_x, $dst_y, 0, 0, $arr[$i]['size'][0], $arr[$i]['size'][0]);
imagecopy($merge, $space, $dst_x, $dst_y, 0, 0, $width, 10);
$dst_y += $arr[$i]['size'][1];
}
imagepng($merge, $dest);
imagedestroy($merge);
}
?>
具体的思想就是先创建一张空的长图,然后将要合并的图片一张一张放进去(用的是imagecopy()函数),这里我做了一个间隔处理,每两张图片之间间隔了10像素。
到目前为止貌似所有的问题都得到了解决,我们只需要将Java项目打包成jar包,放入JavaBridge所要求的jre环境的ext目录中,就能完成所有的功能了。
但是,当我将所有东西放入项目中运行时,会发生各种“意外”(特别是在Windows开发,然后源码移植到linux上时问题最为严重)。具体有哪些问题,下面我一一列举,并且将之与我的项目结合在一起说明
- 由于系统不同,所以文件路径的分隔符也不同,在windows下分隔符为””,而Linux下分隔符为”/”,于是在我将PDF转为PNG时,这个问题出现了,现在回去看PDF转PNG的源码,会有这样一条代码
File file = new File(dest + "\" + i + ".png");
如果你看懂了我的代码,就会知道问题所在。在Windows下一切运行正常,但是到Linux下PNG文件的文件名和生成路径就会发生变化,这里的””不会被当作路径分隔符了,而是当作文件名的一部分,其实修改起来也很简单:File file = new File(dest + File.separator + i + ".png");
- 创建另一个线程启动openoffice服务,老是会出现无法连接服务的异常。根据我的各种调试,发现有时候会有这样的情况发生:在程序运行到连接服务的时候,服务并没有开启。按理说开启服务的代码写在连接服务之前,不应该出现这样的问题。我猜测是由于创建了另一个线程来启动服务,而主线程并没有停止,而会继续执行,如果主线程先执行到连接服务的代码,那么就会出现这个错误。虽然是猜测,我觉得八九不离十了,有两种方式解决这个问题:
第一种就是线程等待,让主线程等待,直到另一个线程执行完毕后唤醒主线程;
第二种就是让openoffice服务长期开启,而不需要在程序中启动服务;
由于此时处于开发初期,小组选择先长期开启服务,等后期再改为线程等待策略。那么代码也得做相应的修改,将启动openoffice服务的代码删除即可,但是要记住需要手动启动服务。
- 如果web网站放在远程服务器上,而且只能用终端进行远程连接没有图形界面时,JavaBridge和openoffice服务就必须放在后台执行了,还好Linux有直接提供将进程转为后台执行的命令,这都不是大问题。
- 第一个问题和第二个问题需要不断的尝试,才能发现问题,浪费了大部分时间。最后一个问题和Java编程经验有很大关系了。前面提到我们需要将Java项目打包成jar包,而且需要将外部jar包也放入其中。而eclipse在打包的时候不能包含外部jar包(这里打包需要选择JAR File而不能选择Runnable Jar File),我们得想办法将外部jar包塞进去。这里我提供三种方法,但是我只测试过其中一种方法。
方法一:将打包好的jar包用rar打开,将外部jar包直接复制到里面,至于复制到哪里,根据程序中import外部jar包张的类所使用的路径来判断,实在判断不出来就一个目录一个目录尝试。
方法二:将外部jar包解压缩成许多class文件,将class文件复制到目标jar包中,这种方式我通过了测试。例如:将jodconverter的jar包解压缩后,有四个文件夹(com、drafts、org、META-INT),将这四个文件夹复制到目标jar包的顶层目录中。
方法三:如果能找到外部jar包的源代码,可以将源代码直接复制到项目中,跟项目一起打包成jar。这里的源代码指的是.java文件而不是.class文件,这种方式应该百分之百能成,但是一般想要找到源代码很难。
总结一下:
- jar文件相当于一个压缩包,可以使用winrar这样的压缩软件打开,并且往里面加入其他文件;
- 编写程序中如果使用到路径,不要直接用””或”/”,使用编程语言中提供的常量来表示分隔符,例如Java中的File.separator,php中的DIRECTORY_SEPARATOR常量;
- 排查错误的时候,要结合前端和数据库一起进行排查,同时可以通过输入日志判断错误位置,php中提供了error_log()函数输入日志。
jar包的永久下载链接:
jodconverter:https://pan.baidu.com/s/1XwhiVhmlXxVvkPiiIkYi_Q
提取码:1dgj
pdfbox:https://pan.baidu.com/s/19bPBsoJhEv-m0l5ZLlMZbg
提取码:ymnt
JavaBridge:https://pan.baidu.com/s/1XKdC8vSLlmOGIGYRAmSQTA
提取码:k3lb
联系方式(qq):1518542802
若是觉得这篇博客有地方不明白可以加我的qq问我,在下必然知无不言言无不尽。