feat: bookmark replace

This commit is contained in:
zzs 2025-03-07 12:00:35 +08:00
parent bfab299485
commit 6e32047bcd
2 changed files with 175 additions and 121 deletions

View File

@ -3,22 +3,21 @@ 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.wmyun.farmwork.word.core.enums.BookmarkType;
import com.wmyun.farmwork.word.core.model.BookmarkInfo;
import com.wmyun.farmwork.word.core.model.BookmarkReplaceDataModel;
import com.wmyun.farmwork.word.core.model.ext.PictureExData;
import com.wmyun.farmwork.word.core.model.ext.TextExData;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.util.Units;
import org.apache.poi.xwpf.usermodel.*;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBookmark;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTText;
import org.springframework.stereotype.Component;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import java.awt.print.Book;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.stream.Collectors;
/**
@ -33,131 +32,45 @@ public class BookmarkExec {
public final static String tmpDir = System.getProperty("java.io.tmpdir") + File.separator;
private static final String BOOKMARK_START = "w:bookmarkStart";
private static final String BOOKMARK_END = "w:bookmarkEnd";
private static final String MARK_PARAGRAPH = "w:pPr";
private static final String MARK_RUN = "w:r";
private static final String ATTR_BOOKMARK_ID = "w:id";
private static final String ATTR_BOOKMARK_NAME = "w:name";
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))) {
// 收集所有段落
// 1. 正文中的段落
List<XWPFParagraph> allParagraphs = new ArrayList<>(doc.getParagraphs());
// 2. 正文中表格内的段落
for (XWPFTable table : doc.getTables()) {
for (XWPFTableRow row : table.getRows()) {
for (XWPFTableCell cell : row.getTableCells()) {
allParagraphs.addAll(cell.getParagraphs());
}
}
}
// 3. 页眉中的段落及页眉内表格中的段落
for (XWPFHeader header : doc.getHeaderList()) {
allParagraphs.addAll(header.getParagraphs());
for (XWPFTable table : header.getTables()) {
for (XWPFTableRow row : table.getRows()) {
for (XWPFTableCell cell : row.getTableCells()) {
allParagraphs.addAll(cell.getParagraphs());
}
}
}
}
// 4. 页脚中的段落及页脚内表格中的段落
for (XWPFFooter footer : doc.getFooterList()) {
allParagraphs.addAll(footer.getParagraphs());
for (XWPFTable table : footer.getTables()) {
for (XWPFTableRow row : table.getRows()) {
for (XWPFTableCell cell : row.getTableCells()) {
allParagraphs.addAll(cell.getParagraphs());
}
}
}
}
List<XWPFParagraph> allParagraphs = new ArrayList<>();
collectionAllParagraphs(doc, allParagraphs);
for (XWPFParagraph paragraph : allParagraphs) {
List<XWPFRun> runs = paragraph.getRuns();
List<CTBookmark> bookmarkStartList = paragraph.getCTP().getBookmarkStartList();
for (CTBookmark bookmark : bookmarkStartList) {
String name = bookmark.getName();
List<BookmarkInfo> info = queryBookmarkInfo(paragraph);
Map<String, BookmarkInfo> bkMap = info.stream().collect(Collectors.toMap(BookmarkInfo::getBookmarkName, bk -> bk));
for (Map.Entry<String, BookmarkInfo> bk : bkMap.entrySet()) {
String name = bk.getKey();
BookmarkInfo bkInfo = bk.getValue();
if (!dataMap.containsKey(name)) {
continue;
}
XWPFRun run = null;
int idx = 0;
boolean doR = false;
for (int i = 0; i < runs.size(); i++) {
Node previousSibling = runs.get(i).getCTR().getDomNode().getPreviousSibling();
if (previousSibling != null) {
String nodeName = previousSibling.getNodeName();
if ("w:bookmarkEnd".equals(nodeName)) {
// end前一个是start
Node bookmarkStartNode = previousSibling.getPreviousSibling();
if (bookmarkStartNode != null) {
Node nameAttribute = bookmarkStartNode.getAttributes().getNamedItem("w:name");
// 获取书签名称确保同一行内多个书签情况能正确处理
if (nameAttribute != null && name.equals(nameAttribute.getNodeValue())) {
idx = i - 1;
doR = true;
break;
}
}
}
}
}
if (idx < runs.size() && doR) {
if (idx < 0) {// 如果是-1说明这一段的开头有书签
// 在本段开头位置插入新的run
run = paragraph.insertNewRun(0);
// 并复制原有开头的run的所有内容后续只修改文本内容不改变样式
run.getCTR().set(runs.get(0).getCTR().copy());
} else {
run = runs.get(idx);
}
} else if (!runs.isEmpty()){
// 处理一段末尾的书签一般不存在这种情况
run = runs.get(runs.size() - 1);
} else {
run = paragraph.createRun();
}
if (run == null) {
continue;
}
BookmarkReplaceDataModel model = dataMap.get(name);
if (model.getExtData() instanceof TextExData textExData) {
BookmarkType type = textExData.getType();
// 处理文本
if (BookmarkType.TEXT.equals(type)) {
run.setText(textExData.getValue());
}
int startIdx = bkInfo.getStartIdx();
int endIdx = bkInfo.getEndIdx();
XWPFRun run = runs.get(startIdx);
CTR ctr = run.getCTR();
List<CTText> tList = ctr.getTList();
for (int i = 0; i < ctr.getTList().size(); i++) {
ctr.removeT(i);
}
if (model.getExtData() instanceof PictureExData pictureData) {
BookmarkType type = pictureData.getType();
// 处理图片以嵌入式插入图片
if (BookmarkType.PICTURE.equals(type)) {
try {
run.addPicture(new ByteArrayInputStream(pictureData.readAsByteArray()), pictureData.readPictureType(), pictureData.getPictureName(), Units.toEMU(pictureData.getWidth()), Units.toEMU(pictureData.getHeight()));
} catch (InvalidFormatException e) {
log.error(e.getMessage(), e);
throw new RuntimeException(e);
}
}
}
}
for (XWPFRun run : runs) {
List<XWPFPicture> pictures = run.getEmbeddedPictures();
for (XWPFPicture picture : pictures) {
String description = picture.getDescription();
if (dataMap.containsKey(description)) {
BookmarkReplaceDataModel model = dataMap.get(description);
if (model.getExtData() instanceof PictureExData pic) {
String picId = doc.addPictureData(pic.readAsByteArray(), pic.readPictureType());
picture.getCTPicture().getBlipFill().getBlip().setEmbed(picId);
}
}
CTText newTextNode = ctr.addNewT();
newTextNode.setStringValue(model.getExtData().getValue());
for (int i = 0; i < endIdx - startIdx; i++) {
paragraph.removeRun(i);
}
}
}
@ -171,8 +84,119 @@ public class BookmarkExec {
doc.write(out);
}
return outFile;
} catch (IOException | InvalidFormatException e) {
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 查询本段中的书签信息
* @param paragraph
* @return List<BookmarkInfo>
*/
private static List<BookmarkInfo> queryBookmarkInfo(XWPFParagraph paragraph) {
Set<BookmarkInfo> set = new LinkedHashSet<>();
CTP ctp = paragraph.getCTP();
List<CTBookmark> startList = ctp.getBookmarkStartList();
for (CTBookmark ctBookmark : startList) {
set.add(new BookmarkInfo(ctBookmark.getName(), ctBookmark.getId().toString()));
}
for (BookmarkInfo info : set) {
queryBookmarkIdx(paragraph, info);
}
return new ArrayList<>(set);
}
private static void queryBookmarkIdx(XWPFParagraph paragraph, BookmarkInfo info) {
List<XWPFRun> runs = paragraph.getRuns();
boolean foundStart = false;
boolean foundEnd = false;
// 倒序遍历每个Runi为原列表的索引
for (int i = runs.size() - 1; i >= 0; i--) {
XWPFRun run = runs.get(i);
CTR ctr = run.getCTR();
Node node = ctr.getDomNode();
Node currentNode = node.getPreviousSibling();
// 递归查找前一个兄弟节点
while (currentNode != null) {
String nodeName = currentNode.getNodeName();
if (BOOKMARK_START.equals(nodeName)) {
NamedNodeMap attrs = currentNode.getAttributes();
Node idNode = attrs.getNamedItem(ATTR_BOOKMARK_ID);
String id = idNode != null ? idNode.getNodeValue() : "";
// 匹配书签ID或名称
if (id.equals(info.getBookmarkId())) {
info.setStartIdx(i); // 记录当前Run的索引为起始位置
foundStart = true;
break;
}
} else if (BOOKMARK_END.equals(nodeName)) {
NamedNodeMap attrs = currentNode.getAttributes();
Node idNode = attrs.getNamedItem(ATTR_BOOKMARK_ID);
String id = idNode != null ? idNode.getNodeValue() : "";
// 匹配书签ID
if (id.equals(info.getBookmarkId())) {
info.setEndIdx(i); // 记录当前Run的索引为结束位置
foundEnd = true;
break;
}
}
currentNode = currentNode.getPreviousSibling(); // 继续查找前一个节点
}
// 如果已找到起始和结束提前退出循环
if (foundStart && foundEnd) {
break;
}
}
}
/**
* 获取文档中所有段落
* @param doc 文档
* @param allParagraphs 段落
*/
private static void collectionAllParagraphs(XWPFDocument doc, List<XWPFParagraph> allParagraphs) {
// 1. 正文中段落
allParagraphs.addAll(doc.getParagraphs());
// 2. 正文中表格内的段落
for (XWPFTable table : doc.getTables()) {
for (XWPFTableRow row : table.getRows()) {
for (XWPFTableCell cell : row.getTableCells()) {
allParagraphs.addAll(cell.getParagraphs());
}
}
}
// 3. 页眉中的段落及页眉内表格中的段落
for (XWPFHeader header : doc.getHeaderList()) {
allParagraphs.addAll(header.getParagraphs());
for (XWPFTable table : header.getTables()) {
for (XWPFTableRow row : table.getRows()) {
for (XWPFTableCell cell : row.getTableCells()) {
allParagraphs.addAll(cell.getParagraphs());
}
}
}
}
// 4. 页脚中的段落及页脚内表格中的段落
for (XWPFFooter footer : doc.getFooterList()) {
allParagraphs.addAll(footer.getParagraphs());
for (XWPFTable table : footer.getTables()) {
for (XWPFTableRow row : table.getRows()) {
for (XWPFTableCell cell : row.getTableCells()) {
allParagraphs.addAll(cell.getParagraphs());
}
}
}
}
}
}

View File

@ -0,0 +1,30 @@
package com.wmyun.farmwork.word.core.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @Description: TODO
* @Date: 2025/3/7 11:10
* @Created: by ZZSLL
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class BookmarkInfo {
private String bookmarkName;
private String bookmarkId;
private int startIdx;
private int endIdx;
public BookmarkInfo(String bookmarkName, String bookmarkId) {
this.bookmarkName = bookmarkName;
this.bookmarkId = bookmarkId;
}
}