php中调用Java实现word文档的预览

php预览word文档的实现 以及实现过程中遇到的各种坑

在做软件工程的课程设计的时候,我们小组选择做一个资料分享网站,网站最重要的功能当然就是上传文件和下载文件。但是这中间就需要一个比较重要的过程:预览。

预览最终结果是一张长图,很长很长的png图片。大致可以分为下面这几个步骤:

  1. 将WORD文档转为PDF
  2. 将PDF拆分,毕竟只是预览,而不是查看全部,这里我设置的是预览10页
  3. 将PDF按页转换为PNG图片
  4. 将所有的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问我,在下必然知无不言言无不尽。

    原文作者:傅豪
    原文地址: https://segmentfault.com/a/1190000018914192
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞