文档转图片实践总结

需求: ppt,pptx,word,pdf转化为图片,不限于png,jpg

1 初级方案

  方案:linux+poi+icepdf

  缺点:office文档转化图片不清晰,smart图形无法渲染,效率不高

  转化方案需要针对不同的文档类型分开处理,office文档早期采用poi的方案处理,优点是简单,但是带来的问题是转化的图片不清晰,文档中的图片严重失真,pdf最终采用的是icepdf进行转化,但是需要处理的中文乱码和水印的问题。期间调研了集中pdf转图片的方案,PDFRenderer,Pdfbox、Icepdf,JPedal,jacob调用adobe等,其中

PDFRenderer: 确实效率最高,但是缺少字体支持对大多数中文pdf处理不了,.

pdfbox:字体基本都可以转换,但容易内存溢出。

JPedal是一个商业的处理PDF软件,但是JPedal有一个裁切版,裁切版JPedal使用LGPL协议进行开源,可免费使用,但是不能满足需求,只能作罢,但是通过测试效果确实不错,由此可见花钱的就是好啊。

poi-ppt转图片代码

 @SuppressWarnings("resource")
    public static Map<String, Object> converPPTtoImage(String orignalPPTFileName, String targetImageFileDir,
                                                       String imageFormatNameString) {
        Map<String, Object> map = new HashMap<String, Object>();
        boolean converReturnResult = true;//是否全部转成功
        List<String> imgNamesList = new ArrayList<String>();//PPT转成图片后所有名称集合
        FileInputStream orignalPPTFileInputStream = null;
        FileOutputStream orignalPPTFileOutStream = null;
        HSLFSlideShow oneHSLFSlideShow = null;
        try {
            try {
                orignalPPTFileInputStream = new FileInputStream(orignalPPTFileName);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
                converReturnResult = false;
                map.put("converReturnResult", converReturnResult);
                return map;
            }

            try {
                oneHSLFSlideShow = new HSLFSlideShow(orignalPPTFileInputStream);
            } catch (IOException e) {
                e.printStackTrace();
                converReturnResult = false;
                map.put("converReturnResult", converReturnResult);
                return map;
            }
            //获取PPT每页的大小(宽和高度)
            Dimension onePPTPageSize = oneHSLFSlideShow.getPageSize();

            //获得PPT文件中的所有的PPT页面(获得每一张幻灯片),并转为一张张的播放片
            List<HSLFSlide> pptPageSlideList = oneHSLFSlideShow.getSlides();
            //下面循环的主要功能是实现对PPT文件中的每一张幻灯片进行转换和操作
            for (int i = 0; i < pptPageSlideList.size(); i++) {
                //这几个循环只要是设置字体为宋体,防止中文乱码,
                List<List<HSLFTextParagraph>> oneTextParagraphs = pptPageSlideList.get(i).getTextParagraphs();
                for (List<HSLFTextParagraph> list : oneTextParagraphs) {
                    for (HSLFTextParagraph hslfTextParagraph : list) {
                        List<HSLFTextRun> HSLFTextRunList = hslfTextParagraph.getTextRuns();
                        for (int j = 0; j < HSLFTextRunList.size(); j++) {

                            /*
                             * 如果PPT在WPS中保存过,则 HSLFTextRunList.get(j).getFontSize();的值为0或者26040,
                             * 因此首先识别当前文本框内的字体尺寸是否为0或者大于26040,则设置默认的字体尺寸。
                             *
                             */
                            //设置字体大小
                            Double size = HSLFTextRunList.get(j).getFontSize();
                            if ((size <= 0) || (size >= 26040)) {
                                HSLFTextRunList.get(j).setFontSize(20.0);
                            }
                            //设置字体样式为宋体
                            HSLFTextRunList.get(j).setFontFamily("宋体");
                        }
                    }
                }
                /**
                 * 创建BufferedImage对象,图像的尺寸为原来的每页的尺寸
                 */
                BufferedImage oneBufferedImage = new BufferedImage(onePPTPageSize.width, onePPTPageSize.height, BufferedImage.TYPE_INT_RGB);
                Graphics2D oneGraphics2D = oneBufferedImage.createGraphics();
                /**
                 * 设置转换后的图片背景色为白色
                 *
                 */
                oneGraphics2D.setPaint(Color.white);
                oneGraphics2D.fill(new Rectangle2D.Float(0, 0, onePPTPageSize.width, onePPTPageSize.height));
                pptPageSlideList.get(i).draw(oneGraphics2D);
                /**
                 * 设置图片的存放路径和图片格式,注意生成的图片路径为绝对路径,最终获得各个图像文件所对应的输出流对象
                 */
                try {
                    String imgName = (i + 1) + "." + imageFormatNameString;
                    imgNamesList.add(imgName);//将图片名称添加的集合中
                    orignalPPTFileOutStream = new FileOutputStream(targetImageFileDir + imgName);
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                    converReturnResult = false;
                    map.put("converReturnResult", converReturnResult);
                    return map;
                }

                /**
                 * 转换后的图片文件保存的指定的目录中
                 */
                try {
                    ImageIO.write(oneBufferedImage, imageFormatNameString, orignalPPTFileOutStream);
                } catch (IOException e) {
                    e.printStackTrace();
                    converReturnResult = false;
                    map.put("converReturnResult", converReturnResult);
                    return map;
                }
            }
        } finally {
            try {
                if (orignalPPTFileInputStream != null) {
                    orignalPPTFileInputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

            try {
                if (orignalPPTFileOutStream != null) {
                    orignalPPTFileOutStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            map.put("converReturnResult", converReturnResult);
            map.put("imgNames", imgNamesList);
        }
        return map;
    }

pptx转图片代码

 @SuppressWarnings("resource")
    public static Map<String, Object> converPPTXtoImage(String orignalPPTFileName, String targetImageFileDir,
                                                        String imageFormatNameString) {
        Map<String, Object> map = new HashMap<String, Object>();
        boolean converReturnResult = true;//是否全部转成功
        List<String> imgNamesList = new ArrayList<String>();//PPT转成图片后所有名称集合
        FileInputStream orignalPPTFileInputStream = null;
        FileOutputStream orignalPPTFileOutStream = null;
        XMLSlideShow oneSlideShow = null;
        try {
            try {
                orignalPPTFileInputStream = new FileInputStream(orignalPPTFileName);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
                converReturnResult = false;
                map.put("converReturnResult", converReturnResult);
                return map;
            }

            try {
                oneSlideShow = new XMLSlideShow(orignalPPTFileInputStream);
            } catch (IOException e) {
                e.printStackTrace();
                converReturnResult = false;
                map.put("converReturnResult", converReturnResult);
                return map;
            }
            //获取PPT每页的尺寸大小(宽和高度)
            Dimension onePPTPageSize = oneSlideShow.getPageSize();
            //获取PPT文件中的所有PPT页面,并转换为一张张播放片
            List<XSLFSlide> pptPageXSLFSLiseList = oneSlideShow.getSlides();

            String xmlFontFormat = "<xml-fragment xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\" xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\" xmlns:p=\"http://schemas.openxmlformats.org/presentationml/2006/main\">" +
                    "<a:rPr lang=\"zh-CN\" altLang=\"en-US\" dirty=\"0\" smtClean=\"0\"> " +
                    "<a:latin typeface=\"+mj-ea\"/> " +
                    "</a:rPr>" +
                    "</xml-fragment>";

            for (int i = 0; i < pptPageXSLFSLiseList.size(); i++) {
                /**
                 * 设置中文为宋体,解决中文乱码问题
                 */
                CTSlide oneCTSlide = pptPageXSLFSLiseList.get(i).getXmlObject();
                CTGroupShape oneCTGroupShape = oneCTSlide.getCSld().getSpTree();
                List<CTShape> oneCTShapeList = oneCTGroupShape.getSpList();
                for (CTShape ctShape : oneCTShapeList) {
                    CTTextBody oneCTTextBody = ctShape.getTxBody();

                    if (null == oneCTTextBody) {
                        continue;
                    }
                    CTTextParagraph[] oneCTTextParagraph = oneCTTextBody.getPArray();
                    CTTextFont oneCTTextFont = null;
                    try {
                        oneCTTextFont = CTTextFont.Factory.parse(xmlFontFormat);
                    } catch (XmlException e) {
                        e.printStackTrace();
                    }

                    for (CTTextParagraph ctTextParagraph : oneCTTextParagraph) {
                        CTRegularTextRun[] onrCTRegularTextRunArray = ctTextParagraph.getRArray();
                        for (CTRegularTextRun ctRegularTextRun : onrCTRegularTextRunArray) {
                            CTTextCharacterProperties oneCTTextCharacterProperties = ctRegularTextRun.getRPr();
                            oneCTTextCharacterProperties.setLatin(oneCTTextFont);
                        }
                    }
                }

                //创建BufferedImage 对象,图像尺寸为原来的PPT的每页尺寸

                BufferedImage oneBufferedImage = new BufferedImage(onePPTPageSize.width, onePPTPageSize.height, BufferedImage.TYPE_INT_RGB);
                Graphics2D oneGraphics2D = oneBufferedImage.createGraphics();
                //将PPT文件中的每个页面中的相关内容画到转换后的图片中
                pptPageXSLFSLiseList.get(i).draw(oneGraphics2D);

                /**
                 * 设置图片的存放路径和图片格式,注意生成的文件路径为绝对路径,最终获得各个图像文件所对应的输出流的对象
                 */
                try {
                    String imgName = (i + 1) + "." + imageFormatNameString;
                    imgNamesList.add(imgName);//将图片名称添加的集合中
                    orignalPPTFileOutStream = new FileOutputStream(targetImageFileDir + imgName);
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                    converReturnResult = false;
                    map.put("converReturnResult", converReturnResult);
                    return map;
                }

                //将转换后的各个图片文件保存带指定的目录中
                try {
                    ImageIO.write(oneBufferedImage, imageFormatNameString, orignalPPTFileOutStream);
                } catch (IOException e) {
                    e.printStackTrace();
                    converReturnResult = false;
                    map.put("converReturnResult", converReturnResult);
                    return map;
                }
            }
        } finally {
            try {
                if (orignalPPTFileInputStream != null) {
                    orignalPPTFileInputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

            try {
                if (orignalPPTFileOutStream != null) {
                    orignalPPTFileOutStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            map.put("converReturnResult", converReturnResult);
            map.put("imgNames", imgNamesList);
        }
        return map;
    }

pdf转图片

public static int convert(String filePath,String targetFilePath) {
        Document document = new Document();
        try {
            document.setFile(filePath);
        } catch (Exception e1) {
            e1.printStackTrace();
        }
        float scale = 2.5f;// 缩放比例
        float rotation = 0f;// 旋转角度
        int pageSize = document.getNumberOfPages();
        for (int i = 0; i < pageSize; i++) {
            BufferedImage image = null;
            try {
                image = (BufferedImage) document.getPageImage(i, GraphicsRenderingHints.SCREEN,
                        org.icepdf.core.pobjects.Page.BOUNDARY_CROPBOX, rotation, scale);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            RenderedImage rendImage = image;
            try {
                File file = new File(targetFilePath + i + ".jpg");
                ImageIO.write(rendImage, "jpg", file);
            } catch (IOException e) {
                e.printStackTrace();
            }
            image.flush();
        }
        document.dispose();
        return pageSize;
    }

文档格式判断代码

  /**
     * 判断是否是2003版本
     * @param filePath
     * @return
     */
    public static boolean isExcel2003(String filePath) {
        return filePath.matches("^.+\\.(?i)(ppt)$");
    }

    /**
     * 判断是否是2007版本
     * @param filePath
     * @return
     */
    public static boolean isExcel2007(String filePath) {
        return filePath.matches("^.+\\.(?i)(pptx)$");
    }

    /**
     * 判断是否是pdf
     * @param filePath
     * @return
     */
    public static boolean isPDF(String filePath) {
        return filePath.matches("^.+\\.(?i)(pdf)$");
    }

(备注:为了提高效率,采用的队列+多线程的方式,采用2层多线程的方式,外层负责处理文件级别的多线程,内层负责单个文件按照页数多线程处理,为了过度的占用资源,控制了并发的个数,内层服务虽然多线程,但是可能会出现图像渲染阻塞的问题,需要额外加锁处理) 

CountDownLatch countDownLatch = new CountDownLatch(pageSize);
            for (int i = 0; i < pageSize; i++) {
                String imgName = targetFilePath+(i+1) + "."+imageFormatNameString;
                ExecutorConvertPDFToImage executorConvertPDFToImage = new ExecutorConvertPDFToImage(
                        i,
                        imgName,
                        document,
                        countDownLatch);
                ThreadPool.submitCallBack(executorConvertPDFToImage);
                while(ThreadPool.getQueueSize()>=200) {
                    Thread.sleep(100);
                }
            }
      countDownLatch.await();

icepdf中文乱码处理

升级到最新的6.2.2版本,源码中已经带了字体引擎,但是为了保险起见,还是在linux服务器下安装了相应的中文字体,相关命令为mkdirscale  ,mkdirfont , fc-cache  fv  ,  fc-list :lang=zh,

icepdf水印处理

复写icepdf对应的源码,只需要在项目中建立相同的文件路径和类名,覆写相应的方法即可

productInfo.java中覆写getVersion() ,让它返回空字符串即可。

Padding.java中主要是绘制文字,让其绘制的内容为空即可。padding1={},padding3={}.

(不同版本可能有所不同)

图片本来按照原始大小,考虑到客户端的需求,做了图片等比缩放,但是有可能会不清晰(大分辨率转成小分辨率)

   

            double imgScale = sz.getHeight()/sz.getWidth();
            double newWith = width;     //2018 
            double newHeight = imgScale*newWith;   //等比缩放
            newImage = image.getScaledInstance((int)newWith, (int)newHeight, image.SCALE_SMOOTH);
            AffineTransformOp ato = new AffineTransformOp(AffineTransform.getScaleInstance(newWith/pageWidth, newHeight/pageHeight), null);
            newImage = ato.filter(image, null);
            ImageIO.write((BufferedImage)newImage, "jpg", file);

   

2 过渡方案

 方案:linux+libreoffice  +jodconverter+icepdf 

 缺点:office中数学公式错位,smart图形转化稍有改善

  在初级方案 中最不能容忍的是office转化为图片的清晰度的问题,于是采用过度方案,过度方案重新做了架构上的改变,从原来的同步方式,变成了异步的方式,当用户上传完文件后,把文件相关的信息插入到任务队列,消费线程监听任务队列,当发现有任务的时候,根据文件类型采用不同的转化策略,当为office文档类型时,先转化为pdf,然后统一采用icepdf进行转化,通过初级方案中的调试和升级icepdf版本,转化图片的质量还是让人非常满意的。

由于中间多了一步,先让office转化为pdf,通过调研有如下几种方案

poi +itext  样式设计比较繁琐,并且样式效果也不是太好,由于初级方案中图片问题,对poi没有好感pass.

openoffice/libreoffice  linux转化服务,libreoffice比openoffice有更能好的兼容性,最终选用了libreoffice ,但是对于数学公式转化为pdf有些错位,更不用说pdf转图片了。

jacob  终极方案,暂不叙述。

其中为了保证服务的稳定的性和提高效率,做了2个架构上的优化。

优化1 :libreoffice以服务的方式启动,nohup /usr/bin/libreoffice6.0 --headless --accept="socket,host=127.0.0.1,port=8100;urp;" --nofirststartwizard    &   ,并且通过crontab 定时启动脚本监测服务,* * * * * /bin/bash /etc/checking.sh 由于linux下crontabl ,定时力度知道分钟,所以为了保证服务异常后被速度拉起,故每2s中监测一次

checking.sh 如下:

#!/bin/bash

libpids=`ps -ef | grep /opt/libreoffice6.0/program/oosplash | grep port=8888 | wc -l`
echo "时间:`date +"%Y-%m-%d %H:%M:%S"`,信息:libreoffice进程数为###### $libpids" >> /home/liuzhaoming/info.log

for((i=1;i<30;i++));
do
libpid=`ps -ef | grep /opt/libreoffice6.0/program/oosplash | grep port=8888 | wc -l`
sleep 2
if [ "$libpid" = 0 ]
        then
        echo "时间:`date +"%Y-%m-%d %H:%M:%S"`,信息:没有libreoffice进程执行,执行启动命令,启动进程" >> /home/liuzhaoming/info.log
        nohup /usr/bin/libreoffice6.0 --headless --accept="socket,host=0.0.0.0,port=8888;urp;" --nofirststartwizard   >>/home/liuzhaoming/info.log  2>&1 &
        echo "时间:`date +"%Y-%m-%d %H:%M:%S"`,信息:启动完成" >> /home/liuzhaoming/info.log
fi
done

优化2:由于中间必须转化为pdf,所以把服务拆分成2级,第一层服务实现office转化为pdf,代码层采用了jodconverter来调用服务,当然也可以通过直接调用脚本的方式,第二层服务实现pdf转化为图片,并且两层服务支持横向扩展,分别部署了集群,并且实现了简答的负载均衡算法,消费队列由原来的1个拆分成了4个(2个节点为一个集群),分别对应两层服务,两层服务每秒进行上报,采用本机ip+时间戳作为队列标识,由于项目是基于docker运行,中间牵扯到如何容器如何访问本机服务的问题,最终结合docker的版本,采用了hosts的方式解决,也就是启动的时候把宿主机器的ip写入镜像hosts。

3  终极方案

 方案:windows+jacob+icepdf (可选)

 缺点:需要单独维护windows服务,转化速度慢(windows+office2010)

由于过渡方案中的出现的数学公式错位(样式问题),并且通过验证发现,利用wps也会发生错位,这可能就是office文件的兼容性问题,如果要想原真原味还原文档,只能用微软的自己的办公软件打开并且渲染,于是有了终极方案jacob,原生调用ms office.

原理:java通过jni调用jacob.dll文件,利用程序模拟人为操作,利用office办公软件打开文档,调用另存为pdf,或者转存为图片,实现转化。(虽然利用wps也可以,但是尽量用office,因为之前说过如果文档是office,用wps渲染会有公式错位的问题,但是用office打开的话,兼容性会好很多)

ppt转化为pdf(也可以直接转图片,只需要调用SavaAs传不同的数字即可,17转jpg,16bmp,gif等等经国测试位图相对清晰)

 public void ppt2pdf(String srcFilePath, String pdfFilePath) throws Exception {
        ActiveXComponent app = null;
        Dispatch ppt = null;
        try {
            ComThread.InitSTA();
            app = new ActiveXComponent("PowerPoint.Application");
            Dispatch ppts = app.getProperty("Presentations").toDispatch();

            /*
             * call 
             * param 4: ReadOnly
             * param 5: Untitled指定文件是否有标题
             * param 6: WithWindow指定文件是否可见
             * */
            ppt = Dispatch.call(ppts, "Open", srcFilePath, false,false, false).toDispatch();
            Dispatch.call(ppt, "SaveAs", pdfFilePath, PPT_TO_PDF_OPERAND); // ppSaveAsPDF为特定值32  

        } catch (Exception e) {
            e.printStackTrace();
            throw e;
        } finally {
            if (ppt != null) {
                Dispatch.call(ppt, "Close");
            }
            if (app != null) {
                app.invoke("Quit");
            }
            ComThread.Release();
        }
    }

doc转pdf

public void doc2pdf(String srcFilePath, String pdfFilePath) throws Exception {  
        ActiveXComponent app = null;  
        Dispatch doc = null;  
        try {  
            ComThread.InitSTA();  
            app = new ActiveXComponent("Word.Application");  
            app.setProperty("Visible", false);  
            Dispatch docs = app.getProperty("Documents").toDispatch(); 
            Object[] obj = new Object[]{
                    srcFilePath, 
                    new Variant(false),  
                    new Variant(false),//是否只读  
                    new Variant(false),   
                    new Variant("pwd")
            };
            doc = Dispatch.invoke(docs, "Open", Dispatch.Method, obj, new int[1]).toDispatch();  
//          Dispatch.put(doc, "Compatibility", false);  //兼容性检查,为特定值false不正确  
            Dispatch.put(doc, "RemovePersonalInformation", false);  
            Dispatch.call(doc, "ExportAsFixedFormat", pdfFilePath, WORD_TO_PDF_OPERAND); // word保存为pdf格式宏,值为17  
  
        }catch (Exception e) {  
            e.printStackTrace();
            throw e;
        } finally {  
            if (doc != null) {  
                Dispatch.call(doc, "Close", false);  
            }  
            if (app != null) {  
                app.invoke("Quit", 0);  
            }  
            ComThread.Release();  
        }  
    }  

备注:请确保dll和jar放到相应的位置,另外安装office的时候,尽量最大化安装,jni调用office需要.netFramework组件的支持,当使用完后一定要释放资源,另外在测试中发现如果直接转图片不太清晰,可以采用先转pdf,在调用其他服务转图片的策略。

4 花钱方案

花钱的方案就是好,省心省力效果好, 推荐aspose

猜你喜欢

转载自blog.csdn.net/sky1988818/article/details/85624371