feat: bookmark replace

This commit is contained in:
zzs 2025-03-07 17:46:53 +08:00
parent dfee41ef5a
commit 5c8468260a
9 changed files with 254 additions and 18 deletions

View File

@ -3,8 +3,13 @@ package com.wmyun.farmwork.word.core;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.file.FileNameUtil;
import com.google.common.collect.Maps;
import com.wmyun.farmwork.word.core.model.BookmarkInfo;
import com.wmyun.farmwork.word.core.model.BookmarkReplaceDataModel;
import com.wmyun.farmwork.word.core.model.TableBookmarkInfo;
import com.wmyun.farmwork.word.core.model.ext.ListExData;
import com.wmyun.farmwork.word.core.model.ext.TableExData;
import com.wmyun.farmwork.word.core.model.ext.TextExData;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.xwpf.usermodel.*;
import org.apache.xmlbeans.XmlCursor;
@ -38,15 +43,31 @@ public class BookmarkExec {
private static final String ATTR_BOOKMARK_ID = "w:id";
private static final String ATTR_BOOKMARK_NAME = "w:name";
private static final String TABLE_ROW_BOOKMARK_START = "w:colFirst";
private static final String TABLE_ROW_BOOKMARK_END = "w:colLast";
public static File replace(List<BookmarkReplaceDataModel> data, byte[] word, File file) {
Map<String, BookmarkReplaceDataModel> dataMap = data.stream()
.collect(Collectors.toMap(BookmarkReplaceDataModel::getName, b -> b));
try (XWPFDocument doc = new XWPFDocument(IoUtil.toStream(word))) {
// 收集文档中所有段落
List<XWPFParagraph> allParagraphs = new ArrayList<>();
collectionAllParagraphs(doc, allParagraphs);
// 列表书签
List<String> listMark = new ArrayList<>();
// 表格内书签信息
List<TableBookmarkInfo> tableBookmarkInfos = queryBookmarkInTable(doc);
// 所有待替换数据
Map<String, BookmarkReplaceDataModel> allDataMap = data.stream()
.collect(Collectors.toMap(BookmarkReplaceDataModel::getName, b -> b));
// 常规替换数据
Map<String, BookmarkReplaceDataModel> dataMap = data.stream()
.filter(d -> {
List<String> bookmarks = tableBookmarkInfos.stream()
.flatMap(info -> info.getBookmark().stream())
.toList();
return bookmarks.contains(d.getName());
}) // 过滤表格中需要整行替换的书签
.collect(Collectors.toMap(BookmarkReplaceDataModel::getName, b -> b));
// 处理段落中书签
for (XWPFParagraph paragraph : allParagraphs) {
@ -76,7 +97,9 @@ public class BookmarkExec {
ctr.removeT(i);
}
CTText newTextNode = ctr.addNewT();
newTextNode.setStringValue(model.getExtData().getValue());
if (model.getExtData() instanceof TextExData exData) {
newTextNode.setStringValue(exData.getValue());
}
// 删除其余的run
for (int i = startIdx + 1; i < endIdx; i++) {
@ -92,10 +115,26 @@ public class BookmarkExec {
// 处理列表
for (String mk : listMark) {
handleListContent(doc, new String[]{"行1", "行2", "行3", "行4", "行5"}, mk);
if (allDataMap.containsKey(mk)) {
BookmarkReplaceDataModel model = allDataMap.get(mk);
if (model.getExtData() instanceof ListExData exData) {
handleListContent(doc, exData.getValue(), mk);
}
}
}
// 处理表格
for (TableBookmarkInfo tableBkInfo : tableBookmarkInfos) {
String masterBookmark = tableBkInfo.getMasterBookmark();
if (allDataMap.containsKey(masterBookmark)) {
BookmarkReplaceDataModel model = allDataMap.get(masterBookmark);
if (model.getExtData() instanceof TableExData exData) {
handleTableContent(doc, tableBkInfo, exData.getValue());
}
}
}
// 保存文件
File outFile = FileUtil.newFile(tmpDir + "gen/" + FileNameUtil.getName(file));
File parentDir = outFile.getParentFile();
if (!parentDir.exists()) {
@ -110,6 +149,126 @@ public class BookmarkExec {
}
}
/**
* 处理word中的表格删除模板行新增行
*/
private static void handleTableContent(XWPFDocument doc, TableBookmarkInfo tableBkInfo, List<Map<String, String>> values) {
List<XWPFTable> tables = doc.getTables();
int tableIdx = tableBkInfo.getTableIdx();
XWPFTable table = tables.get(tableIdx);
int templateRowIdx = tableBkInfo.getTemplateRowIdx();
// 获取模板行
XWPFTableRow templateRow = table.getRow(templateRowIdx);
// 提取各列的书签名称
List<String> columnKeys = new ArrayList<>();
for (XWPFTableCell cell : templateRow.getTableCells()) {
String bookmarkName = null;
// 遍历单元格的段落查找第一个书签
for (XWPFParagraph paragraph : cell.getParagraphs()) {
for (CTBookmark bookmark : paragraph.getCTP().getBookmarkStartList()) {
bookmarkName = bookmark.getName();
break; // 取第一个书签
}
if (bookmarkName != null) {
break;
}
}
if (bookmarkName == null) {
throw new IllegalArgumentException("模板行的单元格中未找到书签");
}
columnKeys.add(bookmarkName);
}
// 插入新行并填充数据
int currentInsertPos = templateRowIdx; // 初始插入位置为模板行的索引
for (Map<String, String> dataRow : values) {
// 在指定位置插入新行
XWPFTableRow newRow = table.insertNewTableRow(currentInsertPos);
// 复制模板行的每个单元格的格式并填充数据
for (int i = 0; i < templateRow.getTableCells().size(); i++) {
XWPFTableCell templateCell = templateRow.getCell(i);
XWPFTableCell newCell = newRow.addNewTableCell();
// 复制单元格样式
CTTcPr templateTcPr = templateCell.getCTTc().getTcPr();
if (templateTcPr != null) {
newCell.getCTTc().setTcPr((CTTcPr) templateTcPr.copy());
}
// 设置单元格内容
String key = columnKeys.get(i);
String value = dataRow.getOrDefault(key, "");
// 清除单元格原有内容
newCell.removeParagraph(0); // 移除默认创建的段落
XWPFParagraph paragraph = newCell.addParagraph();
XWPFRun run = paragraph.createRun();
run.setText(value);
}
currentInsertPos++; // 插入位置后移
}
// 删除模板行此时模板行的新索引为原索引加上插入的行数
int finalTemplateRowIndex = templateRowIdx + values.size();
table.removeRow(finalTemplateRowIndex);
}
/**
* 查询word中表格中需要整行替换的书签
*/
private static List<TableBookmarkInfo> queryBookmarkInTable(XWPFDocument doc) {
List<TableBookmarkInfo> list = new ArrayList<>();
List<XWPFTable> tables = doc.getTables();
// 表格
for (XWPFTable table : tables) {
int idx = tables.indexOf(table);
TableBookmarkInfo info = new TableBookmarkInfo();
List<String> filterMark = new ArrayList<>();
//
for (XWPFTableRow row : table.getRows()) {
int currentRowIdx = table.getRows().indexOf(row);
// 单元格
for (XWPFTableCell cell : row.getTableCells()) {
boolean isReplaceRow = false;
List<XWPFParagraph> paragraphs = cell.getParagraphs();
for (XWPFParagraph paragraph : paragraphs) {
CTP ctp = paragraph.getCTP();
List<CTBookmark> bookmark = ctp.getBookmarkStartList();
// 单元格内书签
for (CTBookmark bk : bookmark) {
NamedNodeMap attr = bk.getDomNode().getAttributes();
Optional<Node> startAttrOpt = Optional.ofNullable(attr.getNamedItem(TABLE_ROW_BOOKMARK_START));
Optional<Node> endAttrOpt = Optional.ofNullable(attr.getNamedItem(TABLE_ROW_BOOKMARK_END));
// 是否是整行是书签如果是标记这一行为模板
if (startAttrOpt.map(Node::getNodeValue).isPresent() && endAttrOpt.map(Node::getNodeValue).isPresent()) {
filterMark.add(bk.getName());
isReplaceRow = true;
info.setTableIdx(idx);
info.setTemplateRowIdx(currentRowIdx);
info.setMasterBookmark(bk.getName());
}
}
// 收集模板行的所有书签
if (isReplaceRow) {
filterMark.addAll(bookmark.stream().map(CTBookmark::getName).collect(Collectors.toSet()));
info.setBookmark(filterMark);
list.add(info);
}
}
}
}
}
return list;
}
/**
* 查询本段中的书签信息
*
@ -138,6 +297,9 @@ public class BookmarkExec {
return new ArrayList<>(set);
}
/**
* 查找书签在段落中包含的位置
*/
private static void queryBookmarkIdx(XWPFParagraph paragraph, BookmarkInfo info) {
List<XWPFRun> runs = paragraph.getRuns();
boolean foundStart = false;
@ -198,9 +360,6 @@ public class BookmarkExec {
/**
* 获取文档中所有段落
*
* @param doc 文档
* @param allParagraphs 段落
*/
private static void collectionAllParagraphs(XWPFDocument doc, List<XWPFParagraph> allParagraphs) {
@ -241,9 +400,9 @@ public class BookmarkExec {
}
/**
* 处理行新增
* 处理列表行新增
*/
public static void handleListContent(XWPFDocument doc, String[] newListItems, String bookmarkName) {
public static void handleListContent(XWPFDocument doc, List<String> newListItems, String bookmarkName) {
// 查找书签所在的段落
XWPFParagraph bookmarkParagraph = findBookmarkParagraph(doc, bookmarkName);
if (bookmarkParagraph == null) {
@ -316,7 +475,7 @@ public class BookmarkExec {
}
}
private static void insertNewListItems(XWPFDocument doc, String[] items, int insertPos,
private static void insertNewListItems(XWPFDocument doc, List<String> items, int insertPos,
BigInteger numId, BigInteger ilvl, XWPFParagraph originalPara, XWPFRun xwpfRun) {
// 获取插入位置的游标锚点
XmlCursor cursor = findInsertCursor(doc, insertPos);

View File

@ -18,7 +18,11 @@ public enum BookmarkType {
PICTURE("PICTURE"),
PICTURE_DESC("PICTURE_DESC");
PICTURE_DESC("PICTURE_DESC"),
TABLE("TABLE"),
LIST("LIST"),;
private final String type;

View File

@ -4,7 +4,9 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.wmyun.farmwork.word.core.enums.BookmarkType;
import com.wmyun.farmwork.word.core.model.ext.ListExData;
import com.wmyun.farmwork.word.core.model.ext.PictureExData;
import com.wmyun.farmwork.word.core.model.ext.TableExData;
import com.wmyun.farmwork.word.core.model.ext.TextExData;
import lombok.Data;
@ -23,12 +25,12 @@ import lombok.Data;
@JsonSubTypes({
@JsonSubTypes.Type(value = TextExData.class, name = "TEXT"),
@JsonSubTypes.Type(value = PictureExData.class, name = "PICTURE"),
@JsonSubTypes.Type(value = PictureExData.class, name = "PICTURE_DESC")
@JsonSubTypes.Type(value = PictureExData.class, name = "PICTURE_DESC"),
@JsonSubTypes.Type(value = ListExData.class, name = "LIST"),
@JsonSubTypes.Type(value = TableExData.class, name = "TABLE")
})
public class AbstractExData {
@JsonProperty("type")
private BookmarkType type;
private String value;
}

View File

@ -30,6 +30,9 @@ public class BookmarkInfo {
// 是否是列表
private boolean listMark;
// 是否在表格中
private boolean tableMark;
public BookmarkInfo(String bookmarkName, String bookmarkId) {
this.bookmarkName = bookmarkName;
this.bookmarkId = bookmarkId;

View File

@ -0,0 +1,27 @@
package com.wmyun.farmwork.word.core.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* @Description: TODO
* @Date: 2025/3/7 16:35
* @Created: by ZZSLL
*/
@Data
@NoArgsConstructor
public class TableBookmarkInfo {
// 表格在word中的index
private int tableIdx;
private String masterBookmark;
private List<String> bookmark;
private int templateRowIdx;
}

View File

@ -0,0 +1,19 @@
package com.wmyun.farmwork.word.core.model.ext;
import com.wmyun.farmwork.word.core.model.AbstractExData;
import lombok.Data;
import java.util.List;
/**
* @Description: TODO
* @Date: 2025/3/7 16:52
* @Created: by ZZSLL
*/
@Data
public class ListExData extends AbstractExData {
private List<String> value;
}

View File

@ -28,16 +28,18 @@ public class PictureExData extends AbstractExData {
private PictureProvideDataType dataType;
private String value;
@SneakyThrows
public byte[] readAsByteArray() {
if (PictureProvideDataType.BASE64.equals(dataType)) {
return Base64.getDecoder().decode(this.getValue());
return Base64.getDecoder().decode(value);
}
if (PictureProvideDataType.DOWNLOAD_URL.equals(dataType)) {
return HttpUtil.downloadBytes(this.getValue());
return HttpUtil.downloadBytes(value);
}
if (PictureProvideDataType.ABSOLUTE_PATH.equals(dataType)) {
return FileUtil.readBytes(this.getValue());
return FileUtil.readBytes(value);
}
return null;
}

View File

@ -0,0 +1,18 @@
package com.wmyun.farmwork.word.core.model.ext;
import com.wmyun.farmwork.word.core.model.AbstractExData;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
* @Description: TODO
* @Date: 2025/3/7 16:52
* @Created: by ZZSLL
*/
@Data
public class TableExData extends AbstractExData {
private List<Map<String, String>> value;
}

View File

@ -15,6 +15,8 @@ import lombok.Data;
@AllArgsConstructor
public class TextExData extends AbstractExData {
private String value;
private Boolean bold;
private Boolean italic;