目录
一、前言介绍:
最近的项目要求前端自定义制作模板及打印,因技术能力及时间成本有限,模板相关数据由后端写死,无法动态修改。前端效果是由后端传递数据及相关样式,动态渲染成一个类似word的效果展示(也可以前端写死),打印及下载功能则由FreeMarker 制作成模板后,通过aspose转换成pdf前端预览打印,虽难FreeMarker维护难度大,但是可实现效果更多,而转pdf试过几个组件,都有不同程度的样式失效,故使用了这一套方案
-- 相关jar
<!--导出word-->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.30</version>
</dependency>
<!--word转pdf -->
<dependency>
<groupId>com.aspose</groupId>
<artifactId>aspose-words</artifactId>
<version>19.5</version>
<scope>system</scope>
<systemPath>${project.basedir}/lib/aspose-words-19.5-jdk.jar</systemPath>
</dependency>
<!-- excel转pdf -->
<dependency>
<groupId>com.aspose</groupId>
<artifactId>aspose-cells</artifactId>
<version>18.8</version>
<scope>system</scope>
<systemPath>${project.basedir}/lib/aspose-cells-18.8.jar</systemPath>
</dependency>
-- aspose相关jar下载 百度网盘 ,提取码:s53u
二、word制作
2.1 基本格式
按照实际需求制作好word,然后填充好相关占位符,建议只写一个1占位,便于修改。这里列举了可能会用到的6种常用数据渲染,注:如果图表/图片在表格内,需要修改布局选项-改为文字环绕,否则pdf打印会丢失。
多选框及其他特殊字符可以通过 插入--符号 进行选择
2.2 图表的相关制作
通过插入-->图表-->选择差不多的图表示例,然后修改样式,标记及线条(标记可以插入自定义图片),修改数据,调整成相差不多的效果,点击图表线条出现右边图标可以修改样式及数据等。
注:1.选中纵坐标后右键可以固定坐标轴刻度。2.隐藏单元格处可以使图表非相邻的两个点连接起来。3.如果有数据,可以选择图标后通过右侧标记选项隐藏图标,模仿无数据,效果见下图的黑线。4.标记可以插入自定义图片,选择内置标记为正方形然后边框设置无,插入图片的大小跟随tu'biao
三、ftl编辑
编辑好的word然后再通过文件-另存为--选择Word XML(*.xml),然后修改后缀为.ftl,然后通过编辑器进行修改,变量格式为${name},因为转成ftl后会可能拆分成多个字符,需要手动修改,编辑好的xml直接修改后缀为ftl,然后通过编辑器格式化,找到 <w:body> 的地方往下这是word正式内容,然后找到 ${name}的地方把生成的多余的标签删除
注:1.修改前的ftl最好备份一个,方便后期修改/维护。2.选择的.xml选项不建议选择2003版的,虽难它更简洁。3.如果需要同时显示多个相同的word(批量打印等),可以把内容体用一个list包含起来
3.1 普通文本渲染
3.2 普通list数据渲染
3.3 横向list数据渲染
找到需要渲染的单元格,单独渲染单元格,方法和普通list基本一致
3.4 图片插入
图片则需要先在图片使用处找到对应的标识embed="rId*" 在 word/_rels/document.xml.rels 标签处找到对应的 图片具体的标签 /media/image**.png,替换内容体为对应的Base64字符串即可
图片引用处
图片存放地点
3.5图表ftl制作
单个图表会比较简单,只需要找到 word/charts/chart**.xml 标签里面的信息,找到相应的数据进行修改,对应标签分别为<c:cat> 和<c:val>。
如果图表隐藏了横坐标,制作的PDF内的图表会拉伸(变形),此时在加入一列隐藏数据即可,即统计数据的第一列数据默认全为0,并隐藏图标
然后替换内置的excel表格数据。注:1.要把图表内的数据对应格式生成一个后缀为.xlsx的表格,并转换成base64的字符串。2.表格内数据渲染需要类型为数字。3.sheet 页名 为 Sheet1。 4.如果出现问题,可以把word内的表格保存一份,然后用自己生成的表格进行对比
3.6 循环情况下图表处理
为保证生成的word可打开,需要把chart**.xml.rels 相关的除了静态资源(图片)外的其他数据全部遍历一遍。图表实际显示的地方,通过id进行关联
编辑图表对应的标签,在 /word/_rels/document.xml.rels 内,循环创建
图表相关标签的循环,搜索并修改 chart.xml 和 chart.xml.rels 名称 及chart.xml.rels内引用的名称
折线图有数据时不显示图标,用来保证分页的情况下上一页最后几项无数据,导致无法与下一页的图标连接起来
3.7 freemaker相关语法说明
循环 :<#list dataList as data></#list> 是否最后一条数据:data_has_next 当前list的下标:nibpd_index 数据不存在则取!后面的数据:${data.name!} if判断:<#if data.name?? && data.name=='1'><#else></#if> if判断 word分页符号: <w:br w:type="page"/> 三元表达式:${(data_index>9)?string(data_index,'0'+data_index)} 特殊字符处理 ${data?html} 替换 ${data?replace("\n","aa")} <>特殊字符处理 ${data?replace("<","<")?replace(">",">")}
四、相关工具类
4.1 WordToPdfUtils
请注意版本,文中使用不需校验,大多数情况下,需要校验license,这时候请把注释代码放开
import com.aspose.words.*;
import freemarker.template.*;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.xssf.streaming.SXSSFCell;
import org.apache.poi.xssf.streaming.SXSSFRow;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import sun.misc.BASE64Encoder;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
/**
* Word转PDF工具类
* @author pw
*
*/
@Component
public class WordToPdfUtils {
/**
* word模板存放地址
*/
private static String templatePath ="ftl";
@Value("${template.path:ftl}")
public void setTemplatePath(String templatePath) {
String os = System.getProperty("os.name");
//Linux操作系统
if (os != null && os.toLowerCase().startsWith("linux")) {
WordToPdfUtils.templatePath = templatePath;
}
}
//使用19.5版本不需要校验许可
private static final Configuration CONFIGURATION;
// private static boolean license;
static {
CONFIGURATION = new Configuration(Configuration.VERSION_2_3_30);
CONFIGURATION.setDefaultEncoding("utf-8");
CONFIGURATION.setObjectWrapper(new DefaultObjectWrapper(Configuration.VERSION_2_3_30));
// try(InputStream is = ParamUtil.class.getClassLoader().getResourceAsStream("license.xml")) {
// License aposeLic = new License();
// aposeLic.setLicense(is);
// license = true;
// } catch (Exception e) {
// e.printStackTrace();
// license = false;
// }
}
/**
* 用freemaker模板生成word文档
* @param dataMap 要填充的数据
* @param templateName 模板名称
* @param fileName 要输出的文件名称
*/
public static void createWord(Map<String, Object> dataMap,String templateName, String fileName,HttpServletResponse response) throws IOException {
setResponse(response,fileName+".doc");
try (Writer out = new BufferedWriter(new OutputStreamWriter(response.getOutputStream(), StandardCharsets.UTF_8))){
CONFIGURATION.setDirectoryForTemplateLoading(new File(templatePath));
Template template = CONFIGURATION.getTemplate(templateName,"UTF-8");
//创建一个Word文档输出流
template.process(dataMap, out);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 用freemaker模板生成word文档
* @param dataMap 要填充的数据
* @param templateName 模板名称
* @param fileName 要输出的文件名称
*/
public static void createPdf(Map<String, Object> dataMap,String templateName, String fileName,HttpServletResponse response) throws IOException {
///临时路径
String wordPath =templatePath+"/short/"+System.currentTimeMillis();
setResponse(response,fileName+".pdf");
File f = new File(wordPath+"/"+fileName+".doc");
//判断文件夹是否存在,不存在,则新建一个文件夹
if (!f.getParentFile().exists()) {
f.getParentFile().mkdirs();
}
try (Writer out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(f), StandardCharsets.UTF_8))){
CONFIGURATION.setDirectoryForTemplateLoading(new File(templatePath));
Template template = CONFIGURATION.getTemplate(templateName,"UTF-8");
//创建一个Word文档输出流
template.process(dataMap, out);
//word文档转PDF
ServletOutputStream outputStream = response.getOutputStream();
FileInputStream fio = new FileInputStream(f);
wordOrPdf(fio, outputStream);
} catch (Exception e) {
e.printStackTrace();
}finally {
deleteFolder(f.getParentFile());
}
}
private static void deleteFolder(File folder) {
if (!folder.exists()) {
return;
}
File[] files = folder.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
//递归直到目录下没有文件
deleteFolder(file);
} else {
//删除
file.delete();
}
}
}
//删除
folder.delete();
}
/**
* 合并多个word文件
*/
public void twoDocToOneDoc(List<String> files, String newFile) throws Exception {
Document doc3 = new Document();
for(String s:files){
doc3.appendDocument( new Document(s), ImportFormatMode.USE_DESTINATION_STYLES );
}
doc3.save(newFile);
}
/**
* 使用aspose.word把word文档转为pdf文档
*/
public static void wordOrPdf(InputStream in, OutputStream os) throws Exception {
try {
Document doc = new Document(in);
doc.save(os, SaveFormat.PDF);
} catch (Exception e) {
e.printStackTrace();
throw new Exception("生成PDF文档失败!");
}finally {
os.flush();
os.close();
in.close();
}
}
/**
* pdf预览、下载
*/
public static void setResponse(HttpServletResponse response,String fileName) throws UnsupportedEncodingException {
response.setContentType("application/octet-stream; charset=UTF-8");
response.setCharacterEncoding("utf-8");
//Content-Disposition属性名 (attachment表示以附件的方式下载;inline表示在页面内打开)
response.setHeader("content-disposition", "attachment; filename="+ URLEncoder.encode(fileName, "UTF-8"));
}
/**
* 获取网络图片
*/
public static String getImageFromNetByUrl(String strUrl) {
InputStream inStream =null;
try {
URL url = new URL(strUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5 * 1000);
inStream = conn.getInputStream();// 通过输入流获取图片数据
return readInputStream(inStream);
} catch (Exception e) {
e.printStackTrace();
}finally {
if(null!=inStream){
try {
inStream.close();
}catch (Exception e1){
e1.printStackTrace();
}
}
}
return null;
}
/**
* 从输入流中获取数据
* @param inputStream
* 输入流
* @return
*/
public static String readInputStream(InputStream inputStream){
try (ByteArrayOutputStream outStream = new ByteArrayOutputStream()){
byte[] buffer = new byte[10240];
int len;
while ((len = inputStream.read(buffer)) != -1) {
outStream.write(buffer, 0, len);
}
outStream.close();
return Base64.encodeBase64String(outStream.toByteArray());
}catch (Exception e){
e.printStackTrace();
}
return null;
}
/**
* 图片转码工具类
* @param imgFile 图片地址
*/
public static String getImgFileToBase64(String imgFile){
byte[] buffer = null;
try(InputStream inputStream = new FileInputStream(imgFile)) {
return readInputStream(inputStream);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 流转base64
*/
public static String getByteToBase64(byte[] buffer) {
return Base64.encodeBase64String(buffer);
}
}
4.2 ExcelPdfUtil
excel转pdf使用比较少,可供参考
import com.alibaba.nacos.client.utils.ParamUtil;
import com.aspose.cells.License;
import com.aspose.cells.PdfSaveOptions;
import com.aspose.cells.Workbook;
import com.aspose.cells.Worksheet;
import org.springframework.beans.factory.annotation.Value;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
/**
* @author Administrator
*/
public class ExcelPdfUtil {
/**
* word模板存放地址
*/
private static String templatePath ="ftl";
@Value("${template.path:ftl}")
public void setTemplatePath(String templatePath) {
//Linux操作系统
if (IS_LINUX) {
ExcelPdfUtil.templatePath = templatePath;
}
}
private static boolean license;
private static final Boolean IS_LINUX;
static {
String os = System.getProperty("os.name");
IS_LINUX = os!=null && os.toLowerCase().startsWith("linux");
try {
InputStream is = ParamUtil.class.getClassLoader().getResourceAsStream("license-18.8.xml");
License aposeLic = new License();
aposeLic.setLicense(is);
license = true;
} catch (Exception e) {
license = false;
e.printStackTrace();
}
}
/**
* pdf预览、下载
*/
public static void setResponse(HttpServletResponse response,String fileName) throws UnsupportedEncodingException {
response.setContentType("application/octet-stream; charset=UTF-8");
response.setCharacterEncoding("utf-8");
//content-disposition属性名 (attachment表示以附件的方式下载;inline表示在页面内打开)
response.setHeader("Content-Disposition", "attachment; filename*=utf-8'zh_cn'"+URLEncoder.encode(fileName+".pdf", "UTF-8"));
}
/**
* 将excel转为pdf
* @param in 需要转换的word
* @param os 保存pdf文件
*/
public static void excel2pdf(InputStream in, OutputStream os) {
if (!license) {
return;
}
try {
Workbook wb = new Workbook(in); // Address是将要被转化的excel文档
PdfSaveOptions pdfSaveOptions = new PdfSaveOptions();
pdfSaveOptions.setOnePagePerSheet(true);
int sheetCount = wb.getWorksheets().getCount();
for (int i = 0; i < sheetCount; i++) {
Worksheet worksheet = wb.getWorksheets().get(i);
//单元格自动高度
worksheet.autoFitRows();
//自动拉伸比例
worksheet.getHorizontalPageBreaks().clear();
worksheet.getVerticalPageBreaks().clear();
}
wb.save(os, pdfSaveOptions);
os.flush();
os.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
4.3 工具类简单调用
@Override
public void test(HttpServletResponse response) throws IOException {
Map<String, Object> dataMap = new HashMap<>(8);
dataMap.put("name","测试");
dataMap.put("isSelect","0");
dataMap.put("isSelect2","1");
//普通list
List<Map<String,String>> list1 = new ArrayList<>();
Map<String,String> map = new HashMap<>();
map.put("id","1");
map.put("name","张三");
map.put("sex","男");
map.put("post","测试");
list1.add(map);
Map<String,String> map2 = new HashMap<>();
map2.put("id","2");
map2.put("name","李四");
map2.put("sex","女");
map2.put("post","主管");
list1.add(map2);
dataMap.put("list1",list1);
//横向数据
Map<String,List<String>> list2 = new HashMap<>();
list2.put("name", Arrays.asList("张三","李四","王五"));
list2.put("sex", Arrays.asList("男","女","--"));
list2.put("post", Arrays.asList("测试","主管","运维"));
dataMap.put("list2",list2);
//图片
dataMap.put("img",WordToPdfUtils.getImgFileToBase64("F:\\1668418379310.jpg"));
//图表数据组装
String[] headMap = {"","血压1","血压2","体温"};
String[] dataStrMap = {"nibps","nibpd","animalHeat"};
List<String> time= Arrays.asList("05","06","07","08","09","10","11");
Map<String, List<String>> chartDate = new HashMap<>();
chartDate.put("nibps",Arrays.asList("83","68","72","80","72","88","95"));
chartDate.put("nibpd",Arrays.asList("54","35","43","56","60","81","61"));
chartDate.put("animalHeat",Arrays.asList("38","34","36","40","39","35","36"));
//生成excel
dataMap.put("charExcel", WordToPdfUtils.getWordExcel(headMap, dataStrMap,time, chartDate));
//图表数据
dataMap.put("chart", chartDate);
dataMap.put("timeList", time);
WordToPdfUtils.createWord(dataMap,"测试.ftl",String.valueOf(System.currentTimeMillis()),response);
}
五、实际显示效果
5.1.图例渲染效果
5.2 其它渲染效果显现
注:展示的HTML为前端动态渲染(因能力和时间有限数据,样式需要手动在数据库录入/修改,维护难度较大),pdf为FreeMarker模板渲染,如需与打印效果一致/或需前端直接打印网页,需要比较详细的配置每个单元格的样式及打印样式,使用的单位最好使用cm
左边为html渲染,右边为生成的pdf
普通图表word 效果:
层叠图表word 效果: