功能描述
1.用户通过在线/离线操作的方式对模板word进行插入书签
2.获取书签对应的内容(表格、图片、段落、普通文本)等内容插入word文档中
3.后续相关对应的内容发生变化时(例如下面的html假设为动态生成的),需要同步更新到word中。
最大的难点在于,当上面的填充的内容,如果发生变化时,需要更新word。此时就需要定位到书签的位置,然后先删除之前填充的,在获取最新的html资源,然后再次填充(这里我折腾了接近2天。)
下面是我的伪代码示例
public void generateReportWithBankmark(String documentId, List<ResourceInfo> listRes) throws Exception {
//从minio获取文档
InputStream fileStream = minioService.getFileStream(documentId);
Document docx = new Document(fileStream);
DocumentBuilder builder = new DocumentBuilder(docx);
//将和文档关联的资源填充到word
for (ResourceInfo temp : listRes) {
//这里的资源resId和书签名是同一个
ResourceInfo res = resourceInfoService.getResourceById(temp.getResId());
//resText是富文本内容
OfficeUtils.updateBookmark(builder, res.getResId(), res.getResText());
}
//文档回传到minio
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
docx.save(outputStream, SaveFormat.DOCX);
ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
long fileSize = outputStream.size();
minioService.deleteFile(documentId);
minioService.uploadFile(inputStream, documentId, Constant.WORD, fileSize);
}
准备工作
准备带标签的文档
准备3个富文本
分别为表格、段落、图片
String html1 = "<table border='1' cellpadding='4'>" +
" <tr><th>姓名</th><th>年龄</th><th>职位</th></tr>" +
" <tr><td>张三</td><td>30</td><td>工程师</td></tr>" +
" <tr><td>李四</td><td>28</td><td>设计师</td></tr>" +
"</table>";
String html2 = "<div style=\"font-family: Arial, sans-serif; font-size: 11pt; line-height: 1.5;\">"
+ " <p>1. 用户通过在线/离线操作的方式对模板Word进行插入书签</p>"
+ " <p>2. 获取书签对应的内容(表格、图片、段落、普通文本)等内容插入Word文档中</p>"
+ " <p>3. 后续相关对应的内容发生变化时,需要同步更新到Word中。</p>"
+ "</div>";
String html3 = "<img src=\"https://ww2.sinaimg.cn/mw690/007ut4Uhly1hx4v37mpxcj30u017cgrv.jpg\" alt=\"图片内容\" style=\"max-width: 100%; height: auto;\"/>";
设计思路(不可行)
一开始我的思路是下面这样的,结果导致无法成功,如果你也是下面的这种思路,你可以借鉴一下。看是不是这样的思路,提前换思路
书签始终不删除,只是清空书签内容,然后从新插入内容(无法满足复杂的富文本,例如表格)
- 获取文档
- 光标移动到指定书签位置处
- 清空标签内容
- 插入富文本
- 保存文档
理论上:开始书签->富文本表格->结束书签,但是实际上得到的是:开始书签->结束书签->富文本表格。最后会导致,书签没有包裹住富文本,最后当多次更新以后,无法删除以前的富文本表格,导致插入很多表格。
例如:下面的a1标签,点击定位时,发现不是定位表格,而是表格后面的光标,因为对于二次编辑就没办法进行定位了
这种设计方式,只能用于文档的第一次创建,没办法实现多次更新。
而对于普通的文本,则是正常的。点击定位能够找到文本(因此可以正常执行后续的更新操作)
对于下面这种,手动创建:开始书签->富文本表格->结束书签的方式,最后打开word文档,会发现,这个书签没有加进去,也就是没有办法执行后面的二次更新。
builder.startBookmark(tag);
builder.insertHtml(tableHtml);
builder.endBookmark(tag);
设计思路(可行)
下面的这个思路可行。但是留有一些小细节,但是不影响大体功能
- 获取文档
- 光标移动到指定书签位置处
- 删除书签的内容
- 删除书签
- 创建开始书签(和删除的书签保持同一个名)
- 创建临时文档
- 将富文本写入临时文档
- 将临时文档追加到我们目标文档
- 创建结束标签
- 清空多余的空行
- 保存文档
核心逻辑
下面的代码中,在插入文档之前,执行了一个builder.insertHtml(MARK);然后再文档书签更新完毕以后,又把MARK替换为空的原因是:
在word编辑中,当我们对某段文本设置了某个样式(例如黑体),然后接着把这段文本挨个删除以后,但是没有删除整段文本,接着在继续输入文本,会发现,输入的文本是黑体。也就是word中,文本是清除了,但是文本的样式却保留了。但是这里我们是不可以删除整段文本的,因为我们已经把书签删了,此时光标的位置替代了书签的位置,把整段文本删除,就会导致光标失去定位了,所以可以在这段文本的前面,从新插入一个新的文本标识,在新的文本后面插入新的富文本,这样新的富文本,就不会复用以前的富文本的样式了。最后我们把新插入的文本标识清空即可。
private static final String MARK = "3b069b88-8c7a-43af-a75c-45885a78f69e";
public static void updateBookmark(DocumentBuilder builder, String tag, String html) {
Bookmark bookmark = builder.getDocument().getRange().getBookmarks().get(tag);
if(bookmark != null) {
try {
//1清空书签内容并删除书签、2创建临时文档、3创建同名书签、4插入临时文档
builder.moveToBookmark(tag);
bookmark.setText("");
bookmark.remove();
builder.startBookmark(tag);
Document tmp = new Document();
DocumentBuilder tb = new DocumentBuilder(tmp);
tb.insertHtml(html);
//注销下面这行代码,会导致builder.insertDocument插入和原来的样式产生混乱(尤其表格),因此从新插入一个富文本,就不会混乱,最后将生成的MARK替换为空
builder.insertHtml(MARK);
//builder.insertHtml(html); 改代码插入复杂富文本会导致标签失效,例如表格富文本,因此采用插入临时文档
builder.insertDocument(tmp, ImportFormatMode.USE_DESTINATION_STYLES);
builder.endBookmark(tag);
builder.getDocument().getRange().replace(MARK, "");
} catch (Exception e) {
log.info("更新书签内容失败,失败原因:",e);
}
}
}
完整代码
import com.aspose.words.Bookmark;
import com.aspose.words.Document;
import com.aspose.words.DocumentBuilder;
import com.aspose.words.ImportFormatMode;
import com.aspose.words.Node;
import com.aspose.words.NodeCollection;
import com.aspose.words.NodeType;
import com.aspose.words.Paragraph;
import lombok.extern.log4j.Log4j2;
@Log4j2
public class OfficeUtils {
//一个随机的UUID,用于特殊用途,处理插入富文本样式混轮的BUG
private static final String MARK = "3b069b88-8c7a-43af-a75c-45885a78f69e";
/*
* @Description: 更新书签中的内容
* @author: 胡涛
* @mail: hutao_2017@aliyun.com
* @date: 2025年8月12日 下午1:50:47
*/
public static void updateBookmark(DocumentBuilder builder, String tag, String html) {
Bookmark bookmark = builder.getDocument().getRange().getBookmarks().get(tag);
if(bookmark != null) {
try {
//1清空书签内容并删除书签、2创建临时文档、3创建同名书签、4插入临时文档
builder.moveToBookmark(tag);
bookmark.setText("");
bookmark.remove();
builder.startBookmark(tag);
Document tmp = new Document();
DocumentBuilder tb = new DocumentBuilder(tmp);
tb.insertHtml(html);
//注销下面这行代码,会导致builder.insertDocument插入和原来的样式产生混乱(尤其表格),因此从新插入一个富文本,就不会混乱,最后将生成的MARK替换为空
builder.insertHtml(MARK);
//builder.insertHtml(html); 改代码插入复杂富文本会导致标签失效,例如表格富文本,因此采用插入临时文档
builder.insertDocument(tmp, ImportFormatMode.USE_DESTINATION_STYLES);
builder.endBookmark(tag);
builder.getDocument().getRange().replace(MARK, "");
} catch (Exception e) {
log.info("更新书签内容失败,失败原因:",e);
}
}
}
/*
* @Description: 移除空白行
* @author: 胡涛
* @mail: hutao_2017@aliyun.com
* @date: 2025年8月12日 下午2:15:49
*/
public static void removeBlank(Document docx) {
NodeCollection<?> paragraphs = docx.getChildNodes(NodeType.PARAGRAPH, true);
for (int i = paragraphs.getCount() - 1; i >= 0; i--) {
Node node = paragraphs.get(i);
if(node instanceof Paragraph) {
Paragraph para = (Paragraph)node;
if (para.getText().trim().isEmpty() && !isSpecialContent(para)) {
para.remove();
}
}
}
}
/*
* @Description: 是否包含特殊内容(书签、占位符、域)
* @author: 胡涛
* @mail: hutao_2017@aliyun.com
* @date: 2025年8月12日 下午2:10:11
*/
private static boolean isSpecialContent(Paragraph para) {
//书签
@SuppressWarnings("unchecked")
NodeCollection<Node> bookmarkStarts = para.getChildNodes(NodeType.BOOKMARK_START, true);
@SuppressWarnings("unchecked")
NodeCollection<Node> bookmarkEnds = para.getChildNodes(NodeType.BOOKMARK_END, true);
if (bookmarkStarts.getCount() > 0 || bookmarkEnds.getCount() > 0) {
return true;
}
// 占位符
String text = para.getText();
if (text.contains("${") && text.contains("}") || text.contains("{{") && text.contains("}}")) {
return true;
}
// 域
if (para.getRange().getFields().getCount() > 0) {
return true;
}
return false;
}
public static void main(String[] args) throws Exception {
// 示例用法
String path1 = "C:\\Users\\胡涛\\Desktop\\test.docx";
String path2 = "C:\\Users\\胡涛\\Desktop\\test2.docx";
String tag1 = "a1";
String tag2 = "a2";
String tag3 = "a3";
for (int i = 0; i < 1; i++) {
System.out.println("使用最初的模板test文档创建test2文档");
String html1 = "<table border='1' cellpadding='4'>" +
" <tr><th>姓名</th><th>年龄</th><th>职位</th></tr>" +
" <tr><td>张三</td><td>30</td><td>工程师</td></tr>" +
" <tr><td>李四</td><td>28</td><td>设计师</td></tr>" +
"</table>";
String html2 = "<div style=\"font-family: Arial, sans-serif; font-size: 11pt; line-height: 1.5;\">"
+ " <p>1. 用户通过在线/离线操作的方式对模板Word进行插入书签</p>"
+ " <p>2. 获取书签对应的内容(表格、图片、段落、普通文本)等内容插入Word文档中</p>"
+ " <p>3. 后续相关对应的内容发生变化时,需要同步更新到Word中。</p>"
+ "</div>";
String html3 = "<img src=\"https://ww2.sinaimg.cn/mw690/007ut4Uhly1hx4v37mpxcj30u017cgrv.jpg\" alt=\"图片内容\" style=\"max-width: 100%; height: auto;\"/>";
Document docx = new Document(path1);
DocumentBuilder builder = new DocumentBuilder(docx);
updateBookmark(builder,tag1, html1);
updateBookmark(builder,tag2, html2);
updateBookmark(builder,tag3, html3);
docx.save(path2);
}
for (int i = 1; i < 5; i++) {
System.out.println("模拟第"+ i+"次更新文档中的标签");
String html1 = "<table border='1' cellpadding='4'>" +
" <tr><th>姓名</th><th>年龄</th><th>职位</th></tr>" +
" <tr><td>张三</td><td>30</td><td>工程师</td></tr>" +
" <tr><td>李四</td><td>28</td><td>设计师</td></tr>" +
"</table>";
String html2 = "<div style=\"font-family: Arial, sans-serif; font-size: 11pt; line-height: 1.5;\">"
+ " <p>1. 用户通过在线/离线操作的方式对模板Word进行插入书签</p>"
+ " <p>2. 获取书签对应的内容(表格、图片、段落、普通文本)等内容插入Word文档中</p>"
+ " <p>3. 后续相关对应的内容发生变化时,需要同步更新到Word中。</p>"
+ "</div>";
String html3 = "<img src=\"https://bpic.wotucdn.com/original/33/51/83/33518300-1d89270902df00d01dedec878ee357ab.jpeg!/quality/91/unsharp/true/compress/true/watermark/url/bG9nby53YXRlci52MTAucG5n/repeat/true/rotate/auto/fw/320/clip/320x556a0a0\" alt=\"图片内容\" style=\"max-width: 100%; height: auto;\"/>";
Document docx = new Document(path2);
DocumentBuilder builder = new DocumentBuilder(docx);
updateBookmark(builder,tag1, html1);
updateBookmark(builder,tag2, html2);
updateBookmark(builder,tag3, html3);
removeBlank(docx);
docx.save(path2);
}
}
}