关于富文本html通过java生成并导出word 项目背景:由于前端用其他平台的缘故不支持直接引用三方库本地开发,仅可利用平台在线的方式。现要求将合同生成word并在线下载。
项目场景:
项目背景:由于前端用其他平台的缘故不支持直接引用三方库本地开发,仅可利用平台在线的方式。
现要求将合同生成word并在线下载。
问题描述
思路有很多,后端赋值并通过poi生成一篇word文档,通过前端处理富文本模板并将数据代入传至后台生成word文档
第一种思路:
1.制作word合同模板。
2.将值传递替换进模板占位符。
3.将替换完成模板导出。
这里试过这一种思路:弊端是代码量很多,在替换时会遍历每一行并将占位符进行替换,在此过程中有可能会将格式搞错,例如把下划线去掉了,改了字体颜色,多加部分空格下划线。
提供工具类
package app.modules.common.util;
import cn.hutool.core.lang.Assert;
import lombok.Data;
import lombok.Setter;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.xwpf.usermodel.*;
import org.apache.xmlbeans.XmlCursor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.*;
public class DynWordUtils {
private final Logger logger = LoggerFactory.getLogger(DynWordUtils.class);
/**
* 被list替换的段落 被替换的都是oldParagraph
*/
private XWPFParagraph oldParagraph;
/**
* 参数
*/
private Map paramMap;
/**
* 当前元素的位置
*/
int n = 0;
/**
* 判断当前是否是遍历的表格
*/
boolean isTable = false;
/**
* 模板对象
*/
XWPFDocument templateDoc;
/**
* 默认字体的大小
*/
final int DEFAULT_FONT_SIZE = 10;
/**
* 重复模式的占位符所在的行索引
*/
private int currentRowIndex;
/**
* 入口
*
* @param paramMap 模板中使用的参数
* @param templatePaht 模板全路径
* @param outPath 生成的文件存放的本地全路径
*/
public static void process(Map paramMap, String templatePaht, String outPath, HttpServletRequest request, HttpServletResponse response) {
DynWordUtils dynWordUtils = new DynWordUtils();
dynWordUtils.setParamMap(paramMap);
dynWordUtils.createWord(templatePaht, outPath,request,response);
}
private void setParamMap(Map paramMap) {
this.paramMap = paramMap;
}
/**
* 生成动态的word
* @param templatePath
* @param outPath
*/
public void createWord(String templatePath, String outPath,HttpServletRequest request, HttpServletResponse response) {
File inFile = new File(templatePath);
try (OutputStream out = response.getOutputStream()) {
templateDoc = new XWPFDocument(OPCPackage.open(inFile));
parseTemplateWord();
//写入流中
// inputStream =new FileInputStream(inFile);
//获取响应流
// 写入响应流中
// byte[] buffer = new byte[inputStream.available()];
// // 流中写入的是字节
// int bytesRead;
// // 读入流写出流
// while ((bytesRead = inputStream.read(buffer)) != -1) {
// outputStream.write(buffer, 0, bytesRead);
// }
//设置响应头和contentType
response.setHeader("Content-Disposition", "attachment;filename=exported_document.docx");
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
templateDoc.write(out);
out.flush();
// 结束关流,不关流会导致文件损坏
out.close();
} catch (Exception e) {
StackTraceElement[] stackTrace = e.getStackTrace();
String className = stackTrace[0].getClassName();
String methodName = stackTrace[0].getMethodName();
int lineNumber = stackTrace[0].getLineNumber();
logger.error("错误:第:{}行, 类名:{}, 方法名:{}", lineNumber, className, methodName);
throw new RuntimeException(e.getCause().getMessage());
}
}
/**
* 解析word模板
*/
public void parseTemplateWord() throws Exception {
List elements = templateDoc.getBodyElements();
for (; n < elements.size(); n++) {
IBodyElement element = elements.get(n);
// 普通段落
if (element instanceof XWPFParagraph) {
XWPFParagraph paragraph = (XWPFParagraph) element;
oldParagraph = paragraph;
if (paragraph.getParagraphText().isEmpty()) {
continue;
}
delParagraph(paragraph);
}
else if (element instanceof XWPFTable) {
// 表格
isTable = true;
XWPFTable table = (XWPFTable) element;
delTable(table, paramMap);
isTable = false;
}
}
}
/**
* 处理段落
*/
private void delParagraph(XWPFParagraph paragraph) throws Exception {
List runs = oldParagraph.getRuns();
StringBuilder sb = new StringBuilder();
for (XWPFRun run : runs) {
String text = run.getText(0);
if (text == null) {
continue;
}
sb.append(text);
run.setText("", 0);
}
Placeholder(paragraph, runs, sb);
}
/**
* 匹配传入信息集合与模板
*
* @param placeholder 模板需要替换的区域()
* @param paramMap 传入信息集合
* @return 模板需要替换区域信息集合对应值
*/
public void changeValue(XWPFRun currRun, String placeholder, Map paramMap) throws Exception {
String placeholderValue = placeholder;
if (paramMap == null || paramMap.isEmpty()) {
return;
}
// if (str!=null&str.contains(PoiWordUtils.underlineRowText)) {
// currRun.setUnderline(UnderlinePatterns.SINGLE);
// placeholderValue = placeholder.replace(PoiWordUtils.underlineRowText,"");
// }
Map stringBooleanSet= new HashMap();
Set textSets = paramMap.entrySet();
for (Map.Entry textSet : textSets) {
//匹配模板与替换值 格式${key}
String mapKey = textSet.getKey();
String docKey = PoiWordUtils.getDocKey(mapKey);
if (placeholderValue.indexOf(docKey) != -1) {
Object obj = textSet.getValue();
// 需要添加一个list
if (obj instanceof List) {
placeholderValue = delDynList(placeholder, (List) obj);
} else {
placeholderValue = placeholderValue.replaceAll(PoiWordUtils.getPlaceholderReg(mapKey), String.valueOf(obj));
}
}
}
currRun.setText(placeholderValue, 0);
}
/**
* 处理的动态的段落(参数为list)
*
* @param placeholder 段落占位符
* @param obj
* @return
*/
private String delDynList(String placeholder, List obj) {
String placeholderValue = placeholder;
List dataList = obj;
Collections.reverse(dataList);
for (int i = 0, size = dataList.size(); i < size; i++) {
Object text = dataList.get(i);
// 占位符的那行, 不用重新创建新的行
if (i == 0) {
placeholderValue = String.valueOf(text);
} else {
XWPFParagraph paragraph = createParagraph(String.valueOf(text));
if (paragraph != null) {
oldParagraph = paragraph;
}
// 增加段落后doc文档会的element的size会随着增加(在当前行的上面添加),回退并解析新增的行(因为可能新增的带有占位符,这里为了支持图片和表格)
if (!isTable) {
n--;
}
}
}
return placeholderValue;
}
/**
* 创建段落
*
* @param texts
*/
public XWPFParagraph createParagraph(String... texts) {
// 使用游标创建一个新行
XmlCursor cursor = oldParagraph.getCTP().newCursor();
XWPFParagraph newPar = templateDoc.insertNewParagraph(cursor);
// 设置段落样式
newPar.getCTP().setPPr(oldParagraph.getCTP().getPPr());
copyParagraph(oldParagraph, newPar, texts);
return newPar;
}
/**
* 处理表格(遍历)
*
* @param table 表格
* @param paramMap 需要替换的信息集合
*/
public void delTable(XWPFTable table, Map paramMap) throws Exception {
List rows = table.getRows();
for (int i = 0, size = rows.size(); i < size; i++) {
XWPFTableRow row = rows.get(i);
currentRowIndex = i;
// 如果是动态添加行 直接处理后返回
if (delAndJudgeRow(table, paramMap, row)) {
return;
}
}
}
/**
* 判断并且是否是动态行,并且处理表格占位符
* @param table 表格对象
* @param paramMap 参数map
* @param row 当前行
* @return
* @throws Exception
*/
private boolean delAndJudgeRow(XWPFTable table, Map paramMap, XWPFTableRow row) throws Exception {
// 当前行是动态行标志
if (PoiWordUtils.isAddRow(row)) {
List xwpfTableRows = addAndGetRows(table, row, paramMap);
// 回溯添加的行,这里是试图处理动态添加的图片
for (XWPFTableRow tbRow : xwpfTableRows) {
delAndJudgeRow(table, paramMap, tbRow);
}
return true;
}
// 如果是重复添加的行
if (PoiWordUtils.isAddRowRepeat(row)) {
List xwpfTableRows = addAndGetRepeatRows(table, row, paramMap);
// 回溯添加的行,这里是试图处理动态添加的图片
for (XWPFTableRow tbRow : xwpfTableRows) {
delAndJudgeRow(table, paramMap, tbRow);
}
return true;
}
// 当前行非动态行标签
List cells = row.getTableCells();
for (XWPFTableCell cell : cells) {
//判断单元格是否需要替换
if (PoiWordUtils.checkText(cell.getText())) {
List paragraphs = cell.getParagraphs();
for (XWPFParagraph paragraph : paragraphs) {
List runs = paragraph.getRuns();
StringBuilder sb = new StringBuilder();
for (XWPFRun run : runs) {
sb.append(run.toString());
run.setText("", 0);
}
Placeholder(paragraph, runs, sb);
}
}
}
return false;
}
/**
* 处理占位符
* @param runs 当前段的runs
* @param sb 当前段的内容
* @throws Exception
*/
private void Placeholder(XWPFParagraph currentPar, List runs, StringBuilder sb) throws Exception {
if (runs.size() > 0) {
String text = sb.toString();
List newRuns = currentPar.getRuns();
XWPFRun currRun = runs.get(0);
if (PoiWordUtils.isPicture(text)) {
// 该段落是图片占位符
} else {
changeValue(currRun, text, paramMap);
}
}
}
/**
* 添加行 标签行不是新创建的
* flagRow flagRow 表有标签的行
*/
private List addAndGetRows(XWPFTable table, XWPFTableRow flagRow, Map paramMap) throws Exception {
List flagRowCells = flagRow.getTableCells();
XWPFTableCell flagCell = flagRowCells.get(0);
String text = flagCell.getText();
List dataList = (List) PoiWordUtils.getValueByPlaceholder(paramMap, text);
// 新添加的行
List newRows = new ArrayList(dataList.size());
if (dataList == null || dataList.size() 0 ? sourcePar.getRuns().get(0) : null;
for (int i = 0, len = texts.length; i < len; i++) {
String text = arr[i];
XWPFRun run = targetPar.createRun();
run.setText(text);
run.setFontFamily(xwpfRun.getFontFamily());
int fontSize = xwpfRun.getFontSize();
run.setFontSize((fontSize == -1) ? DEFAULT_FONT_SIZE : fontSize);
run.setBold(xwpfRun.isBold());
run.setItalic(xwpfRun.isItalic());
}
}
}
}
package app.modules.common.util;
import org.apache.poi.xwpf.usermodel.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class PoiWordUtils {
/**
* 占位符第一个字符
*/
public static final String PREFIX_FIRST = "$";
/**
* 占位符第二个字符
*/
public static final String PREFIX_SECOND = "{";
/**
* 占位符的前缀
*/
public static final String PLACEHOLDER_PREFIX = PREFIX_FIRST + PREFIX_SECOND;
/**
* 占位符后缀
*/
public static final String PLACEHOLDER_END = "}";
/**
* 下划线格式标记
* */
public static final String underlineRowText = "underline:";
/**
* 表格中需要动态添加行的独特标记
*/
public static final String addRowText = "tbAddRow:";
public static final String addRowRepeatText = "tbAddRowRepeat:";
/**
* 表格中占位符的开头 ${tbAddRow: 例如${tbAddRow:tb1}
*/
public static final String addRowFlag = PLACEHOLDER_PREFIX + addRowText;
/**
* 表格中占位符的开头 ${tbAddRowRepeat: 例如 ${tbAddRowRepeat:0,2,0,1} 第0行到第2行,第0列到第1列 为模板样式
*/
public static final String addRowRepeatFlag = PLACEHOLDER_PREFIX + addRowRepeatText;
/**
* 重复矩阵的分隔符 比如:${tbAddRowRepeat:0,2,0,1} 分隔符为 ,
*/
public static final String tbRepeatMatrixSeparator = ",";
/**
* 占位符的后缀
*/
public static final String PLACEHOLDER_SUFFIX = "}";
/**
* 图片占位符的前缀
*/
public static final String PICTURE_PREFIX = PLACEHOLDER_PREFIX + "image:";
private static final Logger log = LoggerFactory.getLogger(PoiWordUtils.class);
/**
* 判断当前行是不是标志表格中需要添加行
*
* @param row
* @return
*/
public static boolean isAddRow(XWPFTableRow row) {
return isDynRow(row, addRowFlag);
}
/**
* 添加重复模板动态行(以多行为模板)
* @param row
* @return
*/
public static boolean isAddRowRepeat(XWPFTableRow row) {
return isDynRow(row, addRowRepeatFlag);
}
private static boolean isDynRow(XWPFTableRow row, String dynFlag) {
if (row == null) {
return false;
}
List tableCells = row.getTableCells();
if (tableCells != null) {
XWPFTableCell cell = tableCells.get(0);
if (cell != null) {
String text = cell.getText();
if (text != null & text.startsWith(dynFlag)) {
return true;
}
}
}
return false;
}
/**
* 从参数map中获取占位符对应的值
*
* @param paramMap
* @param key
* @return
*/
public static Object getValueByPlaceholder(Map paramMap, String key) {
if (paramMap != null) {
if (key != null) {
return paramMap.get(getKeyFromPlaceholder(key));
}
}
return null;
}
/**
* 后去占位符的重复行列矩阵
* @param key 占位符
* @return {0,2,0,1}
*/
public static String getTbRepeatMatrix(String key) {
String $1 = key.replaceAll("\\" + PREFIX_FIRST + "\\" + PREFIX_SECOND + addRowRepeatText + "(.*)" + "\\" + PLACEHOLDER_SUFFIX, "$1");
return $1;
}
/**
* 从占位符中获取key
*
* @return
*/
public static String getKeyFromPlaceholder(String placeholder) {
return Optional.ofNullable(placeholder).map(p -> p.replaceAll("[\\$\\{\\}]", "")).get();
}
/**
* 复制列的样式,并且设置值
* @param sourceCell
* @param targetCell
* @param text
*/
public static void copyCellAndSetValue(XWPFTableCell sourceCell, XWPFTableCell targetCell, String text) {
//段落属性
List sourceCellParagraphs = sourceCell.getParagraphs();
if (sourceCellParagraphs == null || sourceCellParagraphs.size() 0) {
// 如果当前cell中有run
List runs = targetPar.getRuns();
Optional.ofNullable(runs).ifPresent(rs -> rs.stream().forEach(r -> r.setText("", 0)));
if (runs != null & runs.size() > 0) {
runs.get(0).setText(text, 0);
} else {
XWPFRun cellR = targetPar.createRun();
cellR.setText(text, 0);
// 设置列的样式位模板的样式
targetCell.getCTTc().setTcPr(sourceCell.getCTTc().getTcPr());
}
setTypeface(sourcePar, targetPar);
} else {
targetCell.setText(text);
}
}
/**
* 复制字体
*/
private static void setTypeface(XWPFParagraph sourcePar, XWPFParagraph targetPar) {
XWPFRun sourceRun = sourcePar.getRuns().get(0);
String fontFamily = sourceRun.getFontFamily();
//int fontSize = sourceRun.getFontSize();
String color = sourceRun.getColor();
// String fontName = sourceRun.getFontName();
boolean bold = sourceRun.isBold();
boolean italic = sourceRun.isItalic();
int kerning = sourceRun.getKerning();
// String style = sourcePar.getStyle();
UnderlinePatterns underline = sourceRun.getUnderline();
XWPFRun targetRun = targetPar.getRuns().get(0);
targetRun.setFontFamily(fontFamily);
// targetRun.setFontSize(fontSize == -1 ? 10 : fontSize);
targetRun.setBold(bold);
targetRun.setColor(color);
log.info("存在颜色变动=====》");
targetRun.setItalic(italic);
targetRun.setKerning(kerning);
targetRun.setUnderline(underline);
//targetRun.setFontSize(fontSize);
}
/**
* 判断文本中时候包含$
* @param text 文本
* @return 包含返回true,不包含返回false
*/
public static boolean checkText(String text){
boolean check = false;
if(text.indexOf(PLACEHOLDER_PREFIX)!= -1){
check = true;
}
return check;
}
/**
* 获得占位符替换的正则表达式
* @return
*/
public static String getPlaceholderReg(String text) {
return "\\" + PREFIX_FIRST + "\\" + PREFIX_SECOND + text + "\\" + PLACEHOLDER_SUFFIX;
}
public static String getDocKey(String mapKey) {
return PLACEHOLDER_PREFIX + mapKey + PLACEHOLDER_SUFFIX;
}
/**
* 判断当前占位符是不是一个图片占位符
* @param text
* @return
*/
public static boolean isPicture(String text) {
return text.startsWith(PICTURE_PREFIX);
}
/**
* 删除一行的列
* @param row
*/
public static void removeCells(XWPFTableRow row) {
int size = row.getTableCells().size();
try {
for (int i = 0; i < size; i++) {
row.removeCell(i);
}
} catch (Exception e) {
}
}
}
- 调用部分
// 处理导出合同
@PostMapping("/getContractWord")
public void getContractWord(@RequestBody Map jsonObject, HttpServletRequest request, HttpServletResponse response) {
Map map =new HashMap();
map.put("customerName","高密海绵厂");
map.put("salesName","烟台顺达聚氨酯有限责任公司");
map.put("contractNo","123456");
log.info("============>{}",map.toString());
DynWordUtils.process(map,"D:\\bbb.docx","D:\\aaa.doc",request,response);
}
第二种思路
1.前端准备富文本html利用原生css处理格式。
2.前端利用es6模板语法将html中的关键字段值替换掉
3.传递后端生成word 并写入流中
1.html很多 这里只取片段 包含普通格式以及表格处理
es6只需要代码块中存在声明占位符部分,模板会自动填充
let data = this.value
const date = new Date(data.createdTime); // 获取当前日期
const year = date.getFullYear(); // 获取年份
const month = date.getMonth() + 1; // 获取月份,需要加1,因为月份是从0开始计数的
const day = date.getDate(); // 获取日期
const formattedDate = `${year}年${month}月${day}日`;
let html1 =
`
工业品买卖合同
供方:${data.orgName}
合同编号:${data.formNo}
签订方式:传真、扫描件
需方:${data.CustomerName}
签订时间:${formattedDate}
第一条 产品规格、数量、价款
序号
产品名称
牌号
规格
数量(吨)
单价(元/吨)
总金额(元)
备注
`
let data1 = this.value.ws_contractentry
data1.forEach((item,index)=>{
html1+=`
${index+1}
${item.groupName}
${item.mNameIdText}
${item.model}
${item.num}
${item.rPrice}
${item.sum}
${item.remark}
`;
})
// 发送数据到服务器
fetch(`${config.apiHost}/common/createWord`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ html: html1+html2 })
})
.then(response => response.blob())
.then(blob => {
// 创建一个下载链接并触发下载
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'document.docx'; // 文件名
document.body.appendChild(a);
a.click();
a.remove();
})
.catch(error => console.error('Error:', error));
java部分 html转成word导出
@PostMapping("/createWord")
public void generateWord(@RequestBody Map html, HttpServletRequest request, HttpServletResponse response) throws IOException {
// 读取富文本数据
String htmlContent = html.get("html");
// 必须要html标识的标签 否则可能会解析不出来
htmlContent = ""+htmlContent+"";
ByteArrayInputStream byteArrayInputStream = null;
POIFSFileSystem poifsFileSystem = null;
ServletOutputStream servletOutputStream = null;
try {
byte b[] = htmlContent.getBytes("GBK"); //这里是必须要设置编码的,不然导出中文就会乱码。
byteArrayInputStream = new ByteArrayInputStream(b);//将字节数组包装到流中
poifsFileSystem = new POIFSFileSystem();
DirectoryEntry directory = poifsFileSystem.getRoot();
directory.createDocument("WordDocument", byteArrayInputStream);
//输出文件
request.setCharacterEncoding("utf-8");
response.setContentType("application/msword");//导出word格式
response.addHeader("Content-Disposition", "attachment;filename=aaaa.doc");
servletOutputStream = response.getOutputStream();
poifsFileSystem.writeFilesystem(servletOutputStream);
byteArrayInputStream.close();
servletOutputStream.close();
poifsFileSystem.close();
} catch (Exception e) {
throw e;
}
}
第二种思路的弊端是poifsFileSystem不支持二次修改,例如修改页边距 修改字体颜色 调整文件内容,我尝试了一下poifsFileSystem转HWPFDdocument对象没找到合适的解决方法 所以暂定不能二次修改。
最后效果
关于富文本html通过java生成并导出word 项目背景:由于前端用其他平台的缘故不支持直接引用三方库本地开发,仅可利用平台在线的方式。现要求将合同生成word并在线下载。关于富文本html通过java生成并导出word 项目背景:由于前端用其他平台的缘故不支持直接引用三方库本地开发,仅可利用平台在线的方式。现要求将合同生成word并在线下载。关于富文本html通过java生成并导出word 项目背景:由于前端用其他平台的缘故不支持直接引用三方库本地开发,仅可利用平台在线的方式。现要求将合同生成word并在线下载。关于富文本html通过java生成并导出word 项目背景:由于前端用其他平台的缘故不支持直接引用三方库本地开发,仅可利用平台在线的方式。现要求将合同生成word并在线下载。关于富文本html通过java生成并导出word 项目背景:由于前端用其他平台的缘故不支持直接引用三方库本地开发,仅可利用平台在线的方式。现要求将合同生成word并在线下载。关于富文本html通过java生成并导出word 项目背景:由于前端用其他平台的缘故不支持直接引用三方库本地开发,仅可利用平台在线的方式。现要求将合同生成word并在线下载。关于富文本html通过java生成并导出word 项目背景:由于前端用其他平台的缘故不支持直接引用三方库本地开发,仅可利用平台在线的方式。现要求将合同生成word并在线下载。关于富文本html通过java生成并导出word 项目背景:由于前端用其他平台的缘故不支持直接引用三方库本地开发,仅可利用平台在线的方式。现要求将合同生成word并在线下载。关于富文本html通过java生成并导出word 项目背景:由于前端用其他平台的缘故不支持直接引用三方库本地开发,仅可利用平台在线的方式。现要求将合同生成word并在线下载。