本篇博客主要解決java後臺動態生成word(docx格式),並將word轉換爲pdf並添加水印。html
項目需求是要導出帶水印的pdf,表格樣式仍是有點複雜的,以前考慮過用itextpdf根據html來生成pdf,但框架用的是先後臺分java
離的,前臺用的是react,而且是在沒有展現出表格的狀況下,因此無法經過前臺獲取html代碼塊生成,後來又本身手動拼接react
html,但代碼量太大,難維護,且樣式不怎麼好看。因此決定用freemarker模板生成word,再轉成pdf。翻閱網上不少資料給spring
出了不少方案將word轉pdf,有用poi的、有用第三方工具的等等。用poi的寫的都太複雜,jar引用不少,用第三方工具的有局app
限性,不適合誇平臺,須要安裝服務。因此決定用docx4j,但docx4j只支持docx格式的word轉pdf,因此須要freemarker框架
生成docx的word。ide
一、pom引入依賴工具
<dependency> <groupId>com.itextpdf</groupId> <artifactId>itextpdf</artifactId> <version>5.4.3</version> </dependency> <dependency> <groupId>freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.23</version> </dependency> <dependency> <groupId>org.docx4j</groupId> <artifactId>docx4j</artifactId> <version>6.1.2</version> </dependency> <dependency> <groupId>org.docx4j</groupId> <artifactId>docx4j-export-fo</artifactId> <version>6.0.0</version> </dependency>
二、freemarker生成word(docx)編碼
以前用freemarker生成word的時候都是生成doc格式的,將doc模板文件保存爲xml格式再生成word,生成出來的實際上是XML文件,idea
只不過文件後綴名是doc的,即便在生成的時候將文件後綴名改成docx,生成出來的文件是打不開的。其實docx本質上是個壓縮包,
裏面有包含word主要內容的document.xml文件、資源定義文件document.xml.rels、還有頁眉頁腳文件、圖片資源等等,解決思路是替
換內容區的document.xml文件。
1)、準備docx模板文件,編輯插入佔位符。
2)、將docx文件複製一份,將複製的文件後綴名改成zip
3)、拷貝出zip包裏word路徑下的document.xml文件,這個就是word的主內容文件。以前的那個tmp.docx和拷貝出的document.xml
這兩個文件是咱們所須要用到的兩個模板文件,拷貝到項目工程裏面去。
4)、工具類代碼
1 package com.eazytec.zqtong.common.utils; 2 3 import com.itextpdf.text.*; 4 import com.itextpdf.text.pdf.*; 5 import org.docx4j.Docx4J; 6 import org.docx4j.convert.out.FOSettings; 7 import org.docx4j.fonts.IdentityPlusMapper; 8 import org.docx4j.fonts.Mapper; 9 import org.docx4j.fonts.PhysicalFonts; 10 import org.docx4j.openpackaging.packages.WordprocessingMLPackage; 11 import org.springframework.core.io.ClassPathResource; 12 13 import java.io.*; 14 import java.util.*; 15 import java.util.zip.ZipEntry; 16 import java.util.zip.ZipFile; 17 import java.util.zip.ZipOutputStream; 18 19 public class PdfUtil { 20 private static String separator = File.separator;//文件夾路徑分格符 21 22 //=========================================生成申請表pdf=================================== 23 24 /** 25 * freemark生成word----docx格式 26 * @param dataMap 數據源 27 * @param documentXmlName document.xml模板的文件名 28 * @param docxTempName docx模板的文件名 29 * @return 生成的文件路徑 30 */ 31 public static String createApplyPdf(Map<String,Object> dataMap,String documentXmlName,String docxTempName) { 32 ZipOutputStream zipout = null;//word輸出流 33 File tempPath = null;//docx格式的word文件路徑 34 try { 35 //freemark根據模板生成內容xml 36 //================================獲取 document.xml 輸入流================================ 37 ByteArrayInputStream documentInput = FreeMarkUtils.getFreemarkerContentInputStream(dataMap, documentXmlName, separator + "template" + separator + "downLoad" + separator); 38 //================================獲取 document.xml 輸入流================================ 39 //獲取主模板docx 40 ClassPathResource resource = new ClassPathResource("template" + File.separator + "downLoad" + File.separator + docxTempName); 41 File docxFile = resource.getFile(); 42 43 ZipFile zipFile = new ZipFile(docxFile); 44 Enumeration<? extends ZipEntry> zipEntrys = zipFile.entries(); 45 46 //輸出word文件路徑和名稱 47 String fileName = "applyWord_" + System.currentTimeMillis() + ".docx"; 48 String outPutWordPath = System.getProperty("java.io.tmpdir").replaceAll(separator + "$", "") + separator + fileName; 49 50 tempPath = new File(outPutWordPath); 51 //若是輸出目標文件夾不存在,則建立 52 if (!tempPath.getParentFile().exists()) { 53 tempPath.mkdirs(); 54 } 55 //docx文件輸出流 56 zipout = new ZipOutputStream(new FileOutputStream(tempPath)); 57 58 //循環遍歷主模板docx文件,替換掉主內容區,也就是上面獲取的document.xml的內容 59 //------------------覆蓋文檔------------------ 60 int len = -1; 61 byte[] buffer = new byte[1024]; 62 while (zipEntrys.hasMoreElements()) { 63 ZipEntry next = zipEntrys.nextElement(); 64 InputStream is = zipFile.getInputStream(next); 65 if (next.toString().indexOf("media") < 0) { 66 zipout.putNextEntry(new ZipEntry(next.getName())); 67 if ("word/document.xml".equals(next.getName())) { 68 //寫入填充數據後的主數據信息 69 if (documentInput != null) { 70 while ((len = documentInput.read(buffer)) != -1) { 71 zipout.write(buffer, 0, len); 72 } 73 documentInput.close(); 74 } 75 }else {//不是主數據區的都用主模板的 76 while ((len = is.read(buffer)) != -1) { 77 zipout.write(buffer, 0, len); 78 } 79 is.close(); 80 } 81 } 82 } 83 //------------------覆蓋文檔------------------ 84 zipout.close();//關閉 85 86 //----------------word轉pdf-------------- 87 return convertDocx2Pdf(outPutWordPath); 88 89 } catch (Exception e) { 90 e.printStackTrace(); 91 try { 92 if(zipout!=null){ 93 zipout.close(); 94 } 95 }catch (Exception ex){ 96 ex.printStackTrace(); 97 } 98 } 99 return ""; 100 } 101 102 /** 103 * word(docx)轉pdf 104 * @param wordPath docx文件路徑 105 * @return 生成的帶水印的pdf路徑 106 */ 107 public static String convertDocx2Pdf(String wordPath) { 108 OutputStream os = null; 109 InputStream is = null; 110 try { 111 is = new FileInputStream(new File(wordPath)); 112 WordprocessingMLPackage mlPackage = WordprocessingMLPackage.load(is); 113 Mapper fontMapper = new IdentityPlusMapper(); 114 fontMapper.put("隸書", PhysicalFonts.get("LiSu")); 115 fontMapper.put("宋體", PhysicalFonts.get("SimSun")); 116 fontMapper.put("微軟雅黑", PhysicalFonts.get("Microsoft Yahei")); 117 fontMapper.put("黑體", PhysicalFonts.get("SimHei")); 118 fontMapper.put("楷體", PhysicalFonts.get("KaiTi")); 119 fontMapper.put("新宋體", PhysicalFonts.get("NSimSun")); 120 fontMapper.put("華文行楷", PhysicalFonts.get("STXingkai")); 121 fontMapper.put("華文仿宋", PhysicalFonts.get("STFangsong")); 122 fontMapper.put("宋體擴展", PhysicalFonts.get("simsun-extB")); 123 fontMapper.put("仿宋", PhysicalFonts.get("FangSong")); 124 fontMapper.put("仿宋_GB2312", PhysicalFonts.get("FangSong_GB2312")); 125 fontMapper.put("幼圓", PhysicalFonts.get("YouYuan")); 126 fontMapper.put("華文宋體", PhysicalFonts.get("STSong")); 127 fontMapper.put("華文中宋", PhysicalFonts.get("STZhongsong")); 128 129 mlPackage.setFontMapper(fontMapper); 130 131 //輸出pdf文件路徑和名稱 132 String fileName = "pdfNoMark_" + System.currentTimeMillis() + ".pdf"; 133 String pdfNoMarkPath = System.getProperty("java.io.tmpdir").replaceAll(separator + "$", "") + separator + fileName; 134 135 os = new java.io.FileOutputStream(pdfNoMarkPath); 136 137 //docx4j docx轉pdf 138 FOSettings foSettings = Docx4J.createFOSettings(); 139 foSettings.setWmlPackage(mlPackage); 140 Docx4J.toFO(foSettings, os, Docx4J.FLAG_EXPORT_PREFER_XSL); 141 142 is.close();//關閉輸入流 143 os.close();//關閉輸出流 144 145 //添加水印 146 return addTextMark(pdfNoMarkPath); 147 } catch (Exception e) { 148 e.printStackTrace(); 149 try { 150 if(is != null){ 151 is.close(); 152 } 153 if(os != null){ 154 os.close(); 155 } 156 }catch (Exception ex){ 157 ex.printStackTrace(); 158 } 159 }finally { 160 File file = new File(wordPath); 161 if(file!=null&&file.isFile()&&file.exists()){ 162 file.delete(); 163 } 164 } 165 return ""; 166 } 167 168 /** 169 * 添加水印圖片 170 * @param inPdfPath 無水印pdf路徑 171 * @return 生成的帶水印的pdf路徑 172 */ 173 private static String addTextMark(String inPdfPath) { 174 PdfStamper stamp = null; 175 PdfReader reader = null; 176 try { 177 //輸出pdf帶水印文件路徑和名稱 178 String fileName = "pdfMark_" + System.currentTimeMillis() + ".pdf"; 179 String outPdfMarkPath = System.getProperty("java.io.tmpdir").replaceAll(separator + "$", "") + separator + fileName; 180 181 //添加水印 182 reader = new PdfReader(inPdfPath, "PDF".getBytes()); 183 stamp = new PdfStamper(reader, new FileOutputStream(new File(outPdfMarkPath))); 184 PdfContentByte under; 185 int pageSize = reader.getNumberOfPages();// 原pdf文件的總頁數 186 //水印圖片 187 ClassPathResource resource = new ClassPathResource("template" + File.separator + "downLoad" + File.separator + "mark.png"); 188 File file = resource.getFile(); 189 Image image = Image.getInstance(file.getPath()); 190 for (int i = 1; i <= pageSize; i++) { 191 under = stamp.getUnderContent(i);// 水印在以前文本下 192 image.setAbsolutePosition(100, 210);//水印位置 193 under.addImage(image); 194 } 195 stamp.close();// 關閉 196 reader.close();//關閉 197 198 return outPdfMarkPath; 199 } catch (Exception e) { 200 e.printStackTrace(); 201 try { 202 if (stamp != null) { 203 stamp.close(); 204 } 205 if (reader != null) { 206 reader.close();//關閉 207 } 208 } catch (Exception ex) { 209 ex.printStackTrace(); 210 } 211 } finally { 212 //刪除生成的無水印pdf 213 File file = new File(inPdfPath); 214 if (file != null && file.exists() && file.isFile()) { 215 file.delete(); 216 } 217 } 218 return ""; 219 } 220 221 public static void main(String[] args) { 222 // createApplyPdf(); 223 // convert(); 224 // addTextMark("D:\\test\\test.pdf", "D:\\test\\test2.pdf"); 225 } 226 227 }
package com.eazytec.zqtong.common.utils; import freemarker.template.Configuration; import freemarker.template.Template; import java.io.*; import java.util.Map; /** * 獲取freemarker模板字符串 */ public class FreeMarkUtils { /** * 獲取模板字符串 * @param dataMap 參數 * @param templateName 模板名稱 * @param temp_path 模板路徑 classes下的路徑 若是 classes/templates 傳入 /templates便可 * @return */ public static String getFreemarkerContent(Map dataMap, String templateName, String temp_path) { String result = ""; try { //建立配置實例 Configuration configuration = new Configuration(); //設置編碼 configuration.setDefaultEncoding("UTF-8"); configuration.setClassForTemplateLoading(FreeMarkUtils.class, temp_path); //獲取模板 Template template = configuration.getTemplate(templateName); StringWriter swriter = new StringWriter(); //生成文件 template.process(dataMap, swriter); result = swriter.toString(); } catch (Exception e) { e.printStackTrace(); } return result; } /** * 生成主數據模板xml * * @param dataMap 數據參數 * @param templateName 模板名稱 eg: xxx.xml * @param pathPrefix 模板路徑 eg: /templates/ * @param filePath 生成路徑 eg: d:/ex/ee/xxx.xml */ public static void createTemplateXml(Map dataMap, String templateName, String pathPrefix, String filePath) { try { //建立配置實例 Configuration configuration = new Configuration(); //設置編碼 configuration.setDefaultEncoding("UTF-8"); //ftl模板文件統一放至 com.lun.template 包下面 // configuration.setDirectoryForTemplateLoading(new File("D:/idea_workspace/alarm/alarm/src/main/resources/template/")); // configuration.setClassForTemplateLoading(FreemarkerWordUtils.class,"/template/doc"); configuration.setClassForTemplateLoading(FreeMarkUtils.class, pathPrefix); //獲取模板 Template template = configuration.getTemplate(templateName); // System.out.println("filePath ==> " + filePath); //輸出文件 File outFile = new File(filePath); // System.out.println("outFile.getParentFile() ==> " + outFile.getParentFile()); //若是輸出目標文件夾不存在,則建立 if (!outFile.getParentFile().exists()) { outFile.getParentFile().mkdirs(); } //將模板和數據模型合併生成文件 Writer out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outFile), "UTF-8")); //生成文件 template.process(dataMap, out); //關閉流 out.flush(); out.close(); } catch (Exception e) { e.printStackTrace(); } } /** * 獲取模板字符串輸入流 * @param dataMap 參數 * @param templateName 模板名稱 * @param tempPath 模板路徑 classes下的路徑 若是 classes/templates 傳入 /templates便可 * @return */ public static ByteArrayInputStream getFreemarkerContentInputStream(Map dataMap, String templateName, String tempPath) { ByteArrayInputStream in = null; try { //建立配置實例 Configuration configuration = new Configuration(); //設置編碼 configuration.setDefaultEncoding("UTF-8"); //ftl模板文件統一放至 com.lun.template 包下面 // configuration.setDirectoryForTemplateLoading(new File("D:/idea_workspace/alarm/alarm/src/main/resources/template/")); configuration.setClassForTemplateLoading(FreeMarkUtils.class, tempPath); //獲取模板 Template template = configuration.getTemplate(templateName); StringWriter swriter = new StringWriter(); //生成文件 template.process(dataMap, swriter); String result = swriter.toString(); in = new ByteArrayInputStream(swriter.toString().getBytes()); } catch (Exception e) { e.printStackTrace(); } return in; } }
生成文件下載的時候感受稍微有點慢,其實文件不大,有個八、9秒的樣子纔會有下載框出現,不知道是否是先後臺分離的緣由。