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問我,在下必然知無不言言無不盡。