Bài viết này trình bày cách sử dụng thư viện Apache POI trong Java để thực hiện các thao tác chỉnh sửa nội dung trong tệp Word (.docx), bao gồm thay thế, xóa và chèn văn bản, bảng biểu, hình ảnh. Các thao tác này được thực hiện dựa trên việc xác định vị trí của các "dấu trang" (bookmarks) đã được đánh dấu trong tài liệu Word.
Các Khái niệm Cơ bản
Apache POI
Apache POI là một bộ API Java mã nguồn mở, được phát triển bởi Apache Software Foundation, cho phép làm việc với các định dạng tệp Microsoft Office. Nó hỗ trợ đọc, ghi và sửa đổi nội dung của các tài liệu Word (.doc, .docx), Excel (.xls, .xlsx) và PowerPoint (.ppt, .pptx).
Dấu trang (Bookmark)
Trong Word, dấu trang hoạt động như một điểm đánh dấu có tên, cho phép bạn xác định vị trí cụ thể trong tài liệu để thực hiện các thao tác. Khi làm việc tự động với các tệp Word bằng lập trình, dấu trang cung cấp một mục tiêu rõ ràng và ổn định cho các chỉnh sửa, thay vì phải duyệt toàn bộ cấu trúc tài liệu.
Docx4j
Docx4j là một thư viện Java khác tập trung vào việc xử lý các tệp định dạng DOCX (Office Open XML). Nó ánh xạ cấu trúc XML của tài liệu .docx thành các đối tượng Java, giúp việc thao tác trở nên trực quan hơn.
Aspose.Words
Aspose.Words là một giải pháp thương mại mạnh mẽ để làm việc với tài liệu Word. Nó cung cấp bộ tính năng toàn diện cho các yêu cầu phức tạp ở cấp độ doanh nghiệp, nhưng yêu cầu giấy phép thương mại.
HWPF và XWPF
Trong Apache POI, HWPF (Horrible Word Processor Format) là thành phần xử lý các định dạng Word cũ (.doc), trong khi XWPF (XML Word Processor Format) là thành phần xử lý các định dạng Word mới hơn (.docx) dựa trên chuẩn Office Open XML (OOXML).
Office Open XML (OOXML)
OOXML là một chuẩn định dạng tài liệu dựa trên XML được Microsoft giới thiệu từ Office 2007. Các tệp .docx thực chất là một bộ sưu tập các tệp XML được nén lại dưới dạng tệp ZIP. Cấu trúc OOXML bao gồm các phần như document.xml (nội dung chính), styles.xml (định dạng), header.xml, footer.xml, v.v.
API Cấp cao và Cấp thấp của POI
POI cung cấp hai cấp độ API:
- API Cấp cao: Sử dụng các lớp như
XWPFDocument,XWPFParagraph,XWPFRun,XWPFTableđể thao tác với tài liệu theo hướng đối tượng, trừu tượng hóa sự phức tạp của XML. - API Cấp thấp: Tương tác trực tiếp với các đối tượng XMLBeans được sinh ra từ sơ đồ OOXML (ví dụ:
CTP,CTR), cho phép kiểm soát chi tiết hơn nhưng phức tạp hơn.
XMLBeans và DOM API
XMLBeans là một công nghệ liên kết XML và Java, giúp POI chuyển đổi cấu trúc XML của tài liệu OOXML thành các đối tượng Java. DOM API (Document Object Model) là một chuẩn giao diện để truy cập và thao tác tài liệu XML dưới dạng cây đối tượng.
Các Bước Thực hiện
1. Thêm Thư viện
Cần thêm các dependency sau vào tệp pom.xml của dự án Maven:
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
<version>5.2.3</version>
</dependency>
poi-ooxml là module cốt lõi để xử lý các tệp .docx và các dấu trang.
2. Đánh dấu Dấu trang trong Word
Để sử dụng chức năng này, bạn cần đánh dấu các dấu trang trong tài liệu Word theo các quy ước đặt tên sau:
- Thay thế (Replace): Bắt đầu bằng
replace_text_,replace_table_, hoặcreplace_image_. Các dấu trang này nên bao quanh chính xác nội dung cần thay thế (văn bản, bảng, hoặc hình ảnh). - Chèn (Insert): Bắt đầu bằng
insert_text_,insert_image_, hoặcinsert_table_. Đặt dấu trang tại vị trí mong muốn chèn nội dung mới. - Xóa (Delete): Bắt đầu bằng
delete_theo sau là một số. Đánh dấu các nội dung cần xóa.
Lưu ý quan trọng khi đánh dấu:
- Đối với văn bản: Chọn đúng đoạn văn bản, tránh bao gồm khoảng trắng hoặc ký tự xuống dòng không cần thiết.
- Đối với bảng: Chọn toàn bộ bảng (thường bằng cách nhấp vào biểu tượng dấu cộng ở góc trên bên trái).
- Đối với hình ảnh: Chọn chính hình ảnh đó.
- Đối với đầu trang/chân trang: Mở đầu trang/chân trang, chọn nội dung văn bản và đánh dấu.
- Khi thay thế đầu trang/chân trang, cấu trúc "Liên kết đến Mục trước" có thể ảnh hưởng. Đảm bảo các phần (sections) được phân chia đúng cách nếu cần.
3. Xử lý Dấu trang bằng Java và POI
Mã Java sẽ duyệt qua các đoạn văn (paragraphs), bảng (tables) và các phần tử khác của tài liệu để tìm kiếm các dấu trang. Dựa trên tiền tố của tên dấu trang, các hành động sau sẽ được thực hiện:
- Xử lý
replace_: Tìm kiếm dấu trang, thay thế nội dung bên trong bằng dữ liệu cung cấp. Đối với hình ảnh, nó sẽ thay thế hình ảnh gốc bằng hình ảnh mới, giữ lại định dạng gốc nếu có thể. Đối với bảng, nó sẽ phân tích cú pháp HTML của bảng mới và chèn nó vào vị trí của bảng cũ, cố gắng sao chép định dạng bảng gốc. - Xử lý
insert_: Chèn văn bản, hình ảnh hoặc bảng mới tại vị trí của dấu trang. - Xử lý
delete_: Đánh dấu các đoạn văn hoặc phần tử liên quan đến dấu trang để xóa sau cùng.
Mã nguồn cung cấp các phương thức chi tiết để xử lý từng loại dấu trang, bao gồm cả các trường hợp phức tạp như dấu trang vượt qua nhiều đoạn văn hoặc xử lý các định dạng đặc biệt (như xuống dòng, định dạng HTML, đánh dấu đầu/cuối).
Cuối cùng, tài liệu đã chỉnh sửa sẽ được lưu lại.
Mã nguồn Java minh họa (trích đoạn)
Dưới đây là một phần mã Java thể hiện cách xử lý các loại dấu trang khác nhau:
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.util.Units;
import org.apache.poi.xwpf.usermodel.*;
import org.apache.xmlbeans.XmlCursor;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
public class WordBookmarkProcessor {
// Định nghĩa các tiền tố cho các loại dấu trang
private static final String REPLACE_IMAGE_PREFIX = "replace_image_";
private static final String REPLACE_TABLE_PREFIX = "replace_table_";
private static final String REPLACE_TEXT_PREFIX = "replace_text_";
private static final String INSERT_TEXT_SUFFIX = "insert_text_";
private static final String INSERT_TABLE_SUFFIX = "insert_table_";
private static final String INSERT_IMAGE_SUFFIX = "insert_image_";
private static final String DELETE_PREFIX = "delete_";
/**
* Xử lý tất cả các loại dấu trang (thay thế, chèn, xóa).
* @param replaceBookmarkAndValue Dữ liệu thay thế cho nội dung chính.
* @param replaceHeaderAndFooterBookmarkAndValue Dữ liệu thay thế cho đầu trang/chân trang.
* @param insertBookmarkAndValue Dữ liệu chèn mới.
* @param deleteBookmarks Danh sách các dấu trang cần xóa.
* @param templateFilePath Đường dẫn đến tệp mẫu Word.
* @param outputFilePath Đường dẫn lưu tệp Word sau khi xử lý.
*/
public void processBookmarks(Map<String, String> replaceBookmarkAndValue,
Map<String, String> replaceHeaderAndFooterBookmarkAndValue,
Map<String, String> insertBookmarkAndValue, List<String> deleteBookmarks, String templateFilePath, String outputFilePath) {
File templateFile = new File(templateFilePath);
try (InputStream inputStream = Files.newInputStream(templateFile.toPath());
XWPFDocument document = new XWPFDocument(inputStream)) {
// Lấy các phần tử XML của tài liệu
Node bodyNode = document.getDocument().getBody().getDomNode();
List<XWPFParagraph> paragraphs = document.getParagraphs();
List<XWPFTable> tables = document.getTables();
NodeList bodyChildNodes = bodyNode.getChildNodes();
long maxWidthInEMU = getWordMaxContentWidthInEMU(document);
// Danh sách các nút cần xóa để tránh lỗi ConcurrentModificationException
List<Node> nodesToRemove = new ArrayList<>();
List<Node> pChildNodesToRemove = new ArrayList<>();
List<Node> imageNodesToRemove = new ArrayList<>();
// 1. Xử lý dấu trang thay thế (văn bản, bảng, hình ảnh)
handleReplaceBookmarks(replaceBookmarkAndValue, replaceHeaderAndFooterBookmarkAndValue, paragraphs, bodyChildNodes,
pChildNodesToRemove, nodesToRemove, imageNodesToRemove, document, maxWidthInEMU);
// 2. Xử lý dấu trang chèn (văn bản, hình ảnh, bảng)
handleInsertBookmarks(insertBookmarkAndValue, paragraphs, document, maxWidthInEMU);
// 3. Xử lý dấu trang xóa
handleDeleteBookmarks(deleteBookmarks, bodyChildNodes, pChildNodesToRemove, nodesToRemove);
// 4. Xử lý các trường hợp đặc biệt và định dạng (ví dụ: xuống dòng, bảng HTML, hình ảnh markdown)
handleSpecialCases(paragraphs, document, nodesToRemove, tables, maxWidthInEMU);
// 5. Xóa các nút đã đánh dấu và các dấu trang còn sót lại
cleanupNodes(paragraphs, pChildNodesToRemove, tables, nodesToRemove, imageNodesToRemove, document);
// 6. Lưu tài liệu đã chỉnh sửa
Path outputPath = Paths.get(outputFilePath);
Files.createDirectories(outputPath.getParent());
try (OutputStream outputStream = Files.newOutputStream(outputPath)) {
document.write(outputStream);
}
} catch (Exception e) {
// Log lỗi xử lý
System.err.println("Lỗi khi xử lý tài liệu: " + e.getMessage());
e.printStackTrace();
}
}
/**
* Xóa các nút đã đánh dấu, các dấu trang còn sót lại và các đối tượng tham chiếu không cần thiết.
*/
private void cleanupNodes(List<XWPFParagraph> paragraphs, List<Node> pChildNodesToRemove, List<XWPFTable> tables,
List<Node> nodesToRemove, List<Node> imageNodesToRemove, XWPFDocument document) {
// Xóa các đoạn văn bản (paragraphs)
List<Integer> paragraphIndicesToRemove = new ArrayList<>();
for (XWPFParagraph paragraph : paragraphs) {
Node paragraphDomNode = paragraph.getCTP().getDomNode();
if (nodesToRemove.contains(paragraphDomNode)) {
paragraphIndicesToRemove.add(document.getPosOfParagraph(paragraph));
continue;
}
// Xóa các phần tử con của đoạn văn
List<XWPFRun> runs = paragraph.getRuns();
for (int i = runs.size() - 1; i >= 0; i--) {
XWPFRun run = runs.get(i);
Node runDomNode = run.getCTR().getDomNode();
if (pChildNodesToRemove.contains(runDomNode)) {
paragraph.removeRun(i);
}
// Xóa hình ảnh gốc đã được thay thế
List<CTDrawing> drawingList = run.getCTR().getDrawingList();
if (!drawingList.isEmpty()) {
for (int j = 0; j < drawingList.size(); j++) {
if (imageNodesToRemove.contains(drawingList.get(j).getDomNode())) {
run.getCTR().removeDrawing(j);
break;
}
}
}
}
// Xóa các đoạn văn chỉ chứa dấu trang xóa
CTP ctp = paragraph.getCTP();
if (ctp.getBookmarkStartList().stream().anyMatch(bm -> bm.getName().startsWith(DELETE_PREFIX)) && paragraph.getRuns().isEmpty()) {
paragraphIndicesToRemove.add(document.getPosOfParagraph(paragraph));
}
// Xóa tất cả các dấu trang đã xử lý khỏi cấu trúc
removeBookmarksFromParagraph(ctp, document);
}
// Xóa các bảng
for (XWPFTable table : tables) {
CTRow firstRow = table.getRow(0).getCTRow();
if (!firstRow.getTcArray(0).getPArray(0).getBookmarkStartList().isEmpty() &&
firstRow.getTcArray(0).getPArray(0).getBookmarkStartList().get(0).getName().startsWith(REPLACE_TABLE_PREFIX)) {
paragraphIndicesToRemove.add(document.getPosOfTable(table));
continue;
}
// Xóa các phần tử con của bảng
cleanupTableCells(table, nodesToRemove, pChildNodesToRemove);
}
// Sắp xếp và xóa các đoạn văn bản theo thứ tự
Collections.sort(paragraphIndicesToRemove);
Collections.reverse(paragraphIndicesToRemove);
for (Integer index : paragraphIndicesToRemove) {
document.removeBodyElement(index);
}
}
/**
* Xóa các dấu trang khỏi một đoạn văn.
*/
private void removeBookmarksFromParagraph(CTP ctp, XWPFDocument docx) {
List<String> standardBookmarkPrefix = Arrays.asList(REPLACE_TEXT_PREFIX, REPLACE_TABLE_PREFIX, REPLACE_IMAGE_PREFIX, INSERT_TEXT_SUFFIX, INSERT_TABLE_SUFFIX, INSERT_IMAGE_SUFFIX, DELETE_PREFIX);
List<BigInteger> bookmarkIdsToRemove = new ArrayList<>();
List<CTBookmark> bookmarkStartList = ctp.getBookmarkStartList();
for (int i = bookmarkStartList.size() - 1; i >= 0; i--) {
CTBookmark ctBookmark = bookmarkStartList.get(i);
if (standardBookmarkPrefix.stream().anyMatch(prefix -> ctBookmark.getName().startsWith(prefix))) {
bookmarkIdsToRemove.add(ctBookmark.getId());
ctp.removeBookmarkStart(i);
}
}
List<CTMarkupRange> bookmarkEndList = ctp.getBookmarkEndList();
for (int i = bookmarkEndList.size() - 1; i >= 0; i--) {
if (bookmarkIdsToRemove.contains(bookmarkEndList.get(i).getId())) {
ctp.removeBookmarkEnd(i);
}
}
}
/**
* Xử lý các dấu trang bắt đầu bằng "replace_".
*/
private void handleReplaceBookmarks(Map<String, String> replaceBookmarkMap,
Map<String, String> headerFooterBookmarkMap, List<XWPFParagraph> paragraphs, NodeList bodyChildNodes,
List<Node> pChildNodesToRemove, List<Node> nodesToRemove,
List<Node> imageNodesToRemove, XWPFDocument document, Long maxWidthInEMU) {
List<String> replaceBookmarks = new ArrayList<>(replaceBookmarkMap.keySet());
// Xử lý thay thế hình ảnh và văn bản
for (String bookmark : replaceBookmarks) {
try {
String value = replaceBookmarkMap.get(bookmark);
if (bookmark.startsWith(REPLACE_TEXT_PREFIX) || bookmark.startsWith(REPLACE_IMAGE_PREFIX)) {
List<Node> pNodesInRange = new ArrayList<>();
List<Node> extraPChildNodes = new ArrayList<>();
AtomicInteger continueIndex = new AtomicInteger(0);
boolean bookmarkEndFound = findBookmarkRange(bodyChildNodes, bookmark, extraPChildNodes, pNodesInRange, continueIndex);
if (bookmarkEndFound) {
if (bookmark.startsWith(REPLACE_TEXT_PREFIX)) {
replaceTextInRange(document, bodyChildNodes, pNodesInRange, extraPChildNodes, value, pChildNodesToRemove, continueIndex.get());
} else if (bookmark.startsWith(REPLACE_IMAGE_PREFIX)) {
replaceImageInRange(document, paragraphs, extraPChildNodes, value, imageNodesToRemove, maxWidthInEMU);
}
}
} else if (bookmark.startsWith(REPLACE_TABLE_PREFIX)) {
replaceTable(document, bodyChildNodes, bookmark, value, maxWidthInEMU);
}
} catch (Exception e) {
System.err.println("Lỗi khi xử lý dấu trang thay thế: " + bookmark + " - " + e.getMessage());
e.printStackTrace();
}
}
// Xử lý thay thế đầu trang và chân trang
handleHeaderFooterReplacement(document, headerFooterBookmarkMap);
}
/**
* Tìm phạm vi của dấu trang và các nút liên quan.
*/
private boolean findBookmarkRange(NodeList bodyNodes, String bookmarkName, List<Node> extraPChildNodes, List<Node> pNodes, AtomicInteger continueIndex) {
boolean foundStart = false;
boolean foundEnd = false;
String startBookmarkId = "";
Node startNode = null;
for (int i = 0; i < bodyNodes.getLength(); i++) {
Node node = bodyNodes.item(i);
if (!foundStart) {
if (node.getNodeName().equals("w:p")) {
NodeList pChildren = node.getChildNodes();
for (int j = 0; j < pChildren.getLength(); j++) {
Node pChild = pChildren.item(j);
if (pChild.getNodeName().equals("w:bookmarkStart")) {
if (bookmarkName.equals(pChild.getAttributes().getNamedItem("w:name").getNodeValue())) {
startBookmarkId = pChild.getAttributes().getNamedItem("w:id").getNodeValue();
foundStart = true;
startNode = node;
continueIndex.set(j);
break;
}
}
}
} else if (node.getNodeName().equals("w:bookmarkStart")) { // Dấu trang nằm ngoài đoạn văn bản
if (bookmarkName.equals(node.getAttributes().getNamedItem("w:name").getNodeValue())) {
startBookmarkId = node.getAttributes().getNamedItem("w:id").getNodeValue();
foundStart = true;
startNode = node;
continueIndex.set(0); // Bắt đầu từ đầu
}
} else if (node.getNodeName().equals("w:tbl")) { // Xử lý trường hợp dấu trang trong bảng
if (findBookmarkInRangeInTable(node, bookmarkName, extraPChildNodes, pNodes, continueIndex)) {
foundStart = true;
startNode = node; // Đánh dấu là đã bắt đầu xử lý bảng
}
}
}
if (foundStart) {
if (node.getNodeName().equals("w:bookmarkEnd")) {
if (startBookmarkId.equals(node.getAttributes().getNamedItem("w:id").getNodeValue())) {
foundEnd = true;
break; // Đã tìm thấy điểm kết thúc
}
} else if (node.getNodeName().equals("w:p")) {
if (startNode == node) { // Nếu dấu trang bắt đầu và kết thúc trong cùng một đoạn văn
NodeList pChildren = node.getChildNodes();
for (int j = continueIndex.get(); j < pChildren.getLength(); j++) {
Node pChild = pChildren.item(j);
if (pChild.getNodeName().equals("w:bookmarkEnd")) {
if (startBookmarkId.equals(pChild.getAttributes().getNamedItem("w:id").getNodeValue())) {
foundEnd = true;
break;
}
}
extraPChildNodes.add(pChild);
}
if (!foundEnd) { // Nếu không có dấu trang kết thúc trong đoạn này
pNodes.add(node); // Thêm đoạn văn này vào danh sách cần xử lý
extraPChildNodes.clear(); // Xóa các nút tạm thời vì nó không nằm trong phạm vi kết thúc
}
} else if (startNode != null && !node.equals(startNode)) { // Dấu trang vượt qua nhiều đoạn văn
NodeList pChildren = node.getChildNodes();
for (int j = 0; j < pChildren.getLength(); j++) {
Node pChild = pChildren.item(j);
if (pChild.getNodeName().equals("w:bookmarkEnd")) {
if (startBookmarkId.equals(pChild.getAttributes().getNamedItem("w:id").getNodeValue())) {
foundEnd = true;
break;
}
}
extraPChildNodes.add(pChild);
}
if (!foundEnd) {
pNodes.add(node);
extraPChildNodes.clear();
}
}
} else if (node.getNodeName().equals("w:tbl")) { // Nếu bắt đầu từ bảng và tìm thấy dấu trang kết thúc trong bảng
if (startNode != null && node.equals(startNode)) { // Đang xử lý bảng đã bắt đầu
if(foundEnd) break; // Đã tìm thấy kết thúc trong bảng
else continue; // Chưa tìm thấy kết thúc, tiếp tục xử lý bảng
} else if (startNode != null && !node.equals(startNode)){ // Bảng mới, không liên quan
foundEnd = true; // Coi như kết thúc phạm vi hiện tại
break;
}
} else if (node.getNodeName().equals("w:bookmarkEnd") && startNode == node) { // Dấu trang bắt đầu và kết thúc là cùng một phần tử (hiếm gặp)
foundEnd = true;
break;
} else if (node.getNodeName().equals("w:bookmarkEnd") && startNode != null && !node.equals(startNode)) {
// Nếu gặp bookmarkEnd và startNode không phải là chính nó, và phạm vi chưa kết thúc
if (!foundEnd) {
extraPChildNodes.add(node); // Thêm bookmarkEnd vào danh sách các nút con cần xử lý
}
} else if (startNode != null && !foundEnd && !node.getNodeName().equals("w:bookmarkStart")) {
// Nếu đang trong phạm vi và chưa tìm thấy kết thúc
if (!node.getNodeName().equals("w:bookmarkStart")) {
extraPChildNodes.add(node);
}
}
}
}
return foundEnd;
}
/**
* Tìm kiếm dấu trang bên trong một bảng.
*/
private boolean findBookmarkInRangeInTable(Node tableNode, String bookmarkName, List<Node> extraPChildNodes, List<Node> pNodes, AtomicInteger continueIndex) {
NodeList tableChildren = tableNode.getChildNodes();
for (int i = 0; i < tableChildren.getLength(); i++) {
Node rowNode = tableChildren.item(i);
if (rowNode.getNodeName().equals("w:tr")) {
NodeList cellNodes = rowNode.getChildNodes();
for (int j = 0; j < cellNodes.getLength(); j++) {
Node cellNode = cellNodes.item(j);
if (cellNode.getNodeName().equals("w:tc")) {
NodeList pNodesInCell = cellNode.getChildNodes();
for (int k = 0; k < pNodesInCell.getLength(); k++) {
Node pNode = pNodesInCell.item(k);
if (pNode.getNodeName().equals("w:p")) {
NodeList paragraphChildren = pNode.getChildNodes();
for (int l = 0; l < paragraphChildren.getLength(); l++) {
Node pChild = paragraphChildren.item(l);
if (pChild.getNodeName().equals("w:bookmarkStart")) {
if (bookmarkName.equals(pChild.getAttributes().getNamedItem("w:name").getNodeValue())) {
continueIndex.set(l); // Lưu chỉ số bắt đầu trong đoạn văn
pNodes.add(pNode); // Thêm đoạn văn bản vào danh sách
return true; // Đã tìm thấy dấu trang bắt đầu trong bảng
}
}
}
}
}
}
}
}
}
return false;
}
/**
* Thay thế văn bản trong phạm vi dấu trang.
*/
private void replaceTextInRange(XWPFDocument document, NodeList bodyNodes, List<Node> pNodesInRange, List<Node> extraPChildNodes, String newValue, List<Node> pChildNodesToRemove, int startIndex) {
if (!pNodesInRange.isEmpty()) {
Node firstPNode = pNodesInRange.get(0);
NodeList firstPChildren = firstPNode.getChildNodes();
// Xử lý các nút con của đoạn văn đầu tiên
for (int i = startIndex; i < firstPChildren.getLength(); i++) {
Node childNode = firstPChildren.item(i);
if (childNode.getNodeName().equals("w:r")) { // Tìm thấy một run
NodeList runChildren = childNode.getChildNodes();
for (int j = 0; j < runChildren.getLength(); j++) {
Node runChild = runChildren.item(j);
if (runChild.getNodeName().equals("w:t")) { // Tìm thấy thẻ văn bản
// Cập nhật nội dung văn bản
if (runChild.getFirstChild() != null) {
runChild.getFirstChild().setNodeValue(newValue);
} else { // Nếu thẻ văn bản rỗng, tạo nút văn bản mới
runChild.appendChild(runChild.getOwnerDocument().createTextNode(newValue));
}
// Xóa các nút con thừa còn lại trong phạm vi này
for (int k = i + 1; k < firstPChildren.getLength(); k++) {
pChildNodesToRemove.add(firstPChildren.item(k));
}
return; // Hoàn thành thay thế
}
}
} else {
pChildNodesToRemove.add(childNode); // Thêm các nút khác (không phải run) vào danh sách xóa
}
}
}
// Nếu không có đoạn văn nào trong phạm vi, xử lý các nút con bổ sung
for (Node node : extraPChildNodes) {
if (node.getNodeName().equals("w:r")) {
NodeList runChildren = node.getChildNodes();
for (int j = 0; j < runChildren.getLength(); j++) {
Node runChild = runChildren.item(j);
if (runChild.getNodeName().equals("w:t")) {
if (runChild.getFirstChild() != null) {
runChild.getFirstChild().setNodeValue(newValue);
} else {
runChild.appendChild(runChild.getOwnerDocument().createTextNode(newValue));
}
return;
}
}
} else {
pChildNodesToRemove.add(node);
}
}
}
/**
* Thay thế hình ảnh trong phạm vi dấu trang.
*/
private void replaceImageInRange(XWPFDocument document, List<XWPFParagraph> paragraphs, List<Node> extraPChildNodes, String imagePath, List<Node> imageNodesToRemove, Long maxWidthInEMU) {
Node targetNode = null;
// Tìm nút hình ảnh cần thay thế
for (Node node : extraPChildNodes) {
if (node.getNodeName().equals("w:drawing")) {
targetNode = node;
imageNodesToRemove.add(targetNode);
break;
}
}
if (targetNode == null) return;
// Tìm đoạn văn chứa hình ảnh và thực hiện thêm ảnh mới
for (XWPFParagraph paragraph : paragraphs) {
List<XWPFRun> runs = paragraph.getRuns();
for (XWPFRun run : runs) {
List<CTDrawing> drawings = run.getCTR().getDrawingList();
for (CTDrawing drawing : drawings) {
if (drawing.getDomNode() == targetNode) {
try {
// Đọc ảnh và lấy kích thước
File imageFile = new File(imagePath);
BufferedImage bufferedImage = ImageIO.read(imageFile);
int width = Units.pixelToEMU(bufferedImage.getWidth());
int height = Units.pixelToEMU(bufferedImage.getHeight());
// Điều chỉnh kích thước nếu vượt quá chiều rộng tối đa
if (width > maxWidthInEMU) {
double scale = (double) maxWidthInEMU / width;
width = maxWidthInEMU.intValue();
height = (int) (height * scale);
}
// Thêm ảnh mới
try (InputStream inputStream = Files.newInputStream(imageFile.toPath())) {
run.addPicture(inputStream, XWPFDocument.PICTURE_TYPE_PNG, "image", Units.toEMU(width), Units.toEMU(height));
}
return; // Đã thay thế xong
} catch (IOException | InvalidFormatException e) {
System.err.println("Lỗi khi thêm ảnh: " + imagePath + " - " + e.getMessage());
e.printStackTrace();
}
}
}
}
}
}
/**
* Thay thế bảng trong phạm vi dấu trang.
*/
private void replaceTable(XWPFDocument document, NodeList bodyNodes, String bookmarkName, String htmlTableContent, Long maxWidthInEMU) {
List<Node> pNodes = new ArrayList<>();
List<Node> extraPChildNodes = new ArrayList<>();
AtomicInteger continueIndex = new AtomicInteger(0);
boolean bookmarkEndFound = findBookmarkRange(bodyNodes, bookmarkName, extraPChildNodes, pNodes, continueIndex);
if (bookmarkEndFound && !pNodes.isEmpty()) {
Node tableNode = null;
// Tìm nút bảng gốc
for (Node pNode : pNodes) {
NodeList pChildren = pNode.getChildNodes();
for (int i = 0; i < pChildren.getLength(); i++) {
if (pChild.getNodeName().equals("w:tbl")) {
tableNode = pChild;
break;
}
}
if (tableNode != null) break;
}
if (tableNode != null) {
try {
Document htmlDoc = Jsoup.parse(htmlTableContent);
Element tableElement = htmlDoc.selectFirst("table");
if (tableElement != null) {
XmlCursor cursor = tableNode.getOwnerDocument().createDocument().newCursor(); // Cursor giả để lấy đối tượng CTTblPr
cursor.toNextToken(); // Di chuyển đến vị trí hợp lệ
// Lấy thuộc tính của bảng gốc để áp dụng cho bảng mới
CTTblPr originalTableProps = null;
if (tableNode instanceof org.apache.xmlbeans.impl.dom.ElementImpl) {
originalTableProps = ((org.apache.xmlbeans.impl.dom.ElementImpl) tableNode).get_element();
}
// Chèn bảng mới vào vị trí của bảng cũ
insertHtmlTable(tableElement, document, cursor, originalTableProps, null, null, null, null, null, maxWidthInEMU);
}
} catch (Exception e) {
System.err.println("Lỗi khi thay thế bảng: " + bookmarkName + " - " + e.getMessage());
e.printStackTrace();
}
}
}
}
/**
* Xử lý các dấu trang bắt đầu bằng "insert_".
*/
private void handleInsertBookmarks(Map<String, String> insertBookmarkMap, List<XWPFParagraph> paragraphs, XWPFDocument document, Long maxWidthInEMU) {
for (Map.Entry<String, String> entry : insertBookmarkMap.entrySet()) {
String bookmarkName = entry.getKey();
String value = entry.getValue();
for (XWPFParagraph paragraph : paragraphs) {
CTP ctp = paragraph.getCTP();
List<CTBookmark> bookmarkList = ctp.getBookmarkStartList();
for (CTBookmark bookmark : bookmarkList) {
if (bookmarkName.equals(bookmark.getName())) {
try {
int runIndex = 0;
Node prevNode = bookmark.getDomNode().getPreviousSibling();
if (prevNode != null && prevNode.getNodeName().equals("w:r")) {
// Tìm chỉ số của run tương ứng
List<XWPFRun> runs = paragraph.getRuns();
for(int i = 0; i < runs.size(); i++) {
if (runs.get(i).getCTR().getDomNode() == prevNode) {
runIndex = i + 1;
break;
}
}
}
if (bookmarkName.startsWith(INSERT_TEXT_SUFFIX)) {
XWPFRun run = paragraph.insertNewRun(runIndex);
run.setText(value);
} else if (bookmarkName.startsWith(INSERT_IMAGE_SUFFIX)) {
XWPFRun run = paragraph.insertNewRun(runIndex);
try (InputStream imageStream = Files.newInputStream(Paths.get(value))) {
run.addPicture(imageStream, XWPFDocument.PICTURE_TYPE_PNG, "inserted_image", Units.toEMU(300), Units.toEMU(150));
}
} else if (bookmarkName.startsWith(INSERT_TABLE_SUFFIX)) {
XmlCursor cursor = ctp.newCursor();
cursor.toNextSibling(); // Chèn sau đoạn văn chứa dấu trang
Document htmlDoc = Jsoup.parse(value);
Element tableElement = htmlDoc.selectFirst("table");
if (tableElement != null) {
insertHtmlTable(tableElement, document, cursor, null, null, null, null, null, null, maxWidthInEMU);
}
}
break; // Đã xử lý dấu trang này, chuyển sang dấu trang tiếp theo
} catch (Exception e) {
System.err.println("Lỗi khi chèn nội dung cho dấu trang: " + bookmarkName + " - " + e.getMessage());
e.printStackTrace();
}
}
}
}
}
}
/**
* Xử lý các dấu trang bắt đầu bằng "delete_".
*/
private void handleDeleteBookmarks(List<String> deleteBookmarks, NodeList bodyChildNodes, List<Node> pChildNodesToRemove, List<Node> nodesToRemove) {
if (deleteBookmarks == null || deleteBookmarks.isEmpty()) {
// Nếu không có dấu trang xóa được chỉ định, tìm tất cả dấu trang bắt đầu bằng DELETE_PREFIX
deleteBookmarks = new ArrayList<>();
for (int i = 0; i < bodyChildNodes.getLength(); i++) {
Node node = bodyChildNodes.item(i);
if (node.getNodeName().equals("w:p")) {
NodeList pChildren = node.getChildNodes();
for (int j = 0; j < pChildren.getLength(); j++) {
Node pChild = pChildren.item(j);
if (pChild.getNodeName().equals("w:bookmarkStart")) {
String name = pChild.getAttributes().getNamedItem("w:name").getNodeValue();
if (name.startsWith(DELETE_PREFIX)) {
deleteBookmarks.add(name);
}
}
}
}
}
}
for (String bookmarkName : deleteBookmarks) {
List<Node> pNodesInRange = new ArrayList<>();
List<Node> extraPChildNodes = new ArrayList<>();
AtomicInteger continueIndex = new AtomicInteger(0);
boolean bookmarkEndFound = findBookmarkRange(bodyChildNodes, bookmarkName, extraPChildNodes, pNodesInRange, continueIndex);
if (bookmarkEndFound) {
// Đánh dấu các đoạn văn bản cần xóa
for (Node pNode : pNodesInRange) {
nodesToRemove.add(pNode);
}
// Đánh dấu các phần tử con của đoạn văn cần xóa
for(Node extraNode : extraPChildNodes) {
if (extraNode.getNodeName().equals("w:p") || extraNode.getNodeName().equals("w:r") || extraNode.getNodeName().equals("w:tbl")) { // Chỉ xóa các phần tử có thể xóa trực tiếp
pChildNodesToRemove.add(extraNode);
}
}
}
}
}
/**
* Xử lý các trường hợp đặc biệt và định dạng như xuống dòng, bảng HTML, hình ảnh markdown.
*/
private void handleSpecialCases(List<XWPFParagraph> paragraphs, XWPFDocument document, List<Node> nodesToRemove, List<XWPFTable> tables, Long maxWidthInEMU) {
// 1. Xử lý ký tự xuống dòng (\n)
processNewlines(paragraphs, document);
// 2. Chuyển đổi bảng HTML trong văn bản thành bảng Word
convertHtmlTablesToWordTables(paragraphs, document, nodesToRemove);
// 3. Chuyển đổi thẻ ảnh HTML và Markdown thành ảnh Word
convertImageTagsToWordImages(paragraphs, document, maxWidthInEMU);
// 4. Xử lý định dạng in đậm Markdown (**)
processMarkdownBold(paragraphs);
// 5. Xử lý thẻ subscript (<sub>) và superscript (<sup>) trong văn bản
processSubSuperScript(paragraphs);
// 6. Xử lý thẻ subscript và superscript trong ô bảng
processTableCellSubSuperScript(tables);
// 7. Xử lý chú thích (comments)
processComments(paragraphs, document);
}
/**
* Xử lý ký tự xuống dòng bằng cách tách đoạn văn và thêm thẻ xuống dòng.
*/
private void processNewlines(List<XWPFParagraph> paragraphs, XWPFDocument document) {
for (int i = 0; i < paragraphs.size(); i++) {
XWPFParagraph paragraph = paragraphs.get(i);
List<XWPFRun> runs = paragraph.getRuns();
for (XWPFRun run : runs) {
String text = run.getText(0);
if (text != null && text.contains("\n")) {
CTR ctr = run.getCTR();
CTRPr originalRPr = ctr.getRPr(); // Sao chép thuộc tính của run gốc
ctr.removeT(0); // Xóa văn bản gốc
String[] lines = text.split("\n");
List<String> validLines = Arrays.stream(lines)
.filter(line -> !line.trim().isEmpty())
.collect(Collectors.toList());
if (!validLines.isEmpty()) {
// Xử lý dòng đầu tiên
String firstLine = validLines.get(0);
run.setText(firstLine);
if (originalRPr != null) {
run.getCTR().setRPr((CTRPr) originalRPr.copy());
}
// Xử lý các dòng còn lại bằng cách chèn đoạn văn mới
XmlCursor cursor = ctp.newCursor();
cursor.toNextSibling(); // Đặt con trỏ sau đoạn văn hiện tại
for (int k = 1; k < validLines.size(); k++) {
XWPFParagraph newParagraph = document.insertNewParagraph(cursor);
XWPFRun newRun = newParagraph.createRun();
if (originalRPr != null) {
newRun.getCTR().setRPr((CTRPr) originalRPr.copy());
}
newRun.setText(validLines.get(k));
cursor = newParagraph.getCTP().newCursor();
cursor.toNextSibling();
}
}
}
}
}
}
/**
* Chuyển đổi các bảng HTML được nhúng trong văn bản thành bảng Word thực sự.
*/
private void convertHtmlTablesToWordTables(List<XWPFParagraph> paragraphs, XWPFDocument document, List<Node> nodesToRemove) {
for (XWPFParagraph paragraph : paragraphs) {
String text = paragraph.getText();
if (text.contains(" maxWidthInEMU) {
double scale = (double) maxWidthInEMU / width;
width = maxWidthInEMU.intValue();
height = (int) (height * scale);
}
try (InputStream is = Files.newInputStream(imageFile.toPath())) {
currentRun.addPicture(is, getPictureType(imageFile), "image", width, height);
}
}
} else {
currentRun.setText(part); // Thêm văn bản thường
}
} catch (Exception e) {
System.err.println("Lỗi khi xử lý thẻ ảnh: " + part + " - " + e.getMessage());
e.printStackTrace();
}
}
}
}
}
/**
* Phân tích cú pháp kích thước chiều dài (ví dụ: "100px", "2in").
*/
private int parseDimension(String dimension) {
dimension = dimension.trim().toLowerCase();
if (dimension.endsWith("px")) {
return Units.pixelToEMU(Integer.parseInt(dimension.replace("px", "")));
} else if (dimension.endsWith("in")) {
return (int) (Units.EMU_PER_INCH * Double.parseDouble(dimension.replace("in", "")));
} else {
return Units.toEMU(Integer.parseInt(dimension)); // Mặc định là inch hoặc đơn vị phù hợp
}
}
/**
* Xác định loại hình ảnh dựa trên đuôi tệp.
*/
private int getPictureType(File file) {
String name = file.getName().toLowerCase();
if (name.endsWith(".png")) return XWPFDocument.PICTURE_TYPE_PNG;
if (name.endsWith(".jpg") || name.endsWith(".jpeg")) return XWPFDocument.PICTURE_TYPE_JPEG;
if (name.endsWith(".gif")) return XWPFDocument.PICTURE_TYPE_GIF;
if (name.endsWith(".emf")) return XWPFDocument.PICTURE_TYPE_EMF;
if (name.endsWith(".wmf")) return XWPFDocument.PICTURE_TYPE_WMF;
return XWPFDocument.PICTURE_TYPE_PNG; // Mặc định
}
/**
* Xử lý định dạng in đậm Markdown.
*/
private void processMarkdownBold(List<XWPFParagraph> paragraphs) {
for (XWPFParagraph paragraph : paragraphs) {
List<XWPFRun> runs = paragraph.getRuns();
for (int i = 0; i < runs.size(); i++) {
XWPFRun run = runs.get(i);
String text = run.getText(0);
if (text != null && text.contains("**")) {
String[] parts = text.split("\\*\\*");
run.setText(parts[0], 0); // Đặt phần đầu tiên vào run hiện tại
for (int j = 1; j < parts.length; j++) {
XWPFRun newRun = paragraph.insertNewRun(i + j);
newRun.setText(parts[j]);
if (j % 2 == 1) { // Nếu là phần nằm giữa cặp **
newRun.setBold(true);
}
}
}
}
}
}
/**
* Xử lý thẻ subscript (<sub>) và superscript (<sup>).
*/
private void processSubSuperScript(List<XWPFParagraph> paragraphs) {
for (XWPFParagraph paragraph : paragraphs) {
List<XWPFRun> runs = paragraph.getRuns();
for (int i = 0; i < runs.size(); i++) {
XWPFRun run = runs.get(i);
String text = run.getText(0);
if (text == null) continue;
// Xử lý subscript
if (text.contains("<sub>")) {
String[] parts = text.split("<sub>");
run.setText(parts[0], 0);
for (int j = 1; j < parts.length; j++) {
if (parts[j].contains("</sub>")) {
String[] subParts = parts[j].split("</sub>");
XWPFRun newRun = paragraph.insertNewRun(i + j);
newRun.setText(subParts[0]);
newRun.getCTR().getRPr().addNewVertAlign().setVal(STVerticalAlignRun.SUBSCRIPT);
if (subParts.length > 1) { // Nếu có phần văn bản sau </sub>
XWPFRun nextRun = paragraph.insertNewRun(i + j + 1);
nextRun.setText(subParts[1]);
}
} else { // Nếu thẻ </sub> bị thiếu
XWPFRun newRun = paragraph.insertNewRun(i + j);
newRun.setText(parts[j]);
newRun.getCTR().getRPr().addNewVertAlign().setVal(STVerticalAlignRun.SUBSCRIPT);
}
}
// Cập nhật lại danh sách runs sau khi chèn
runs = paragraph.getRuns();
i = runs.size() - 1; // Đặt lại chỉ số để xử lý các run mới tạo
}
// Xử lý superscript (tương tự subscript)
text = run.getText(0); // Lấy lại văn bản sau khi xử lý subscript
if (text != null && text.contains("<sup>")) {
String[] parts = text.split("<sup>");
run.setText(parts[0], 0);
for (int j = 1; j < parts.length; j++) {
if (parts[j].contains("</sup>")) {
String[] supParts = parts[j].split("</sup>");
XWPFRun newRun = paragraph.insertNewRun(i + j);
newRun.setText(supParts[0]);
newRun.getCTR().getRPr().addNewVertAlign().setVal(STVerticalAlignRun.SUPERSCRIPT);
if (supParts.length > 1) {
XWPFRun nextRun = paragraph.insertNewRun(i + j + 1);
nextRun.setText(supParts[1]);
}
} else {
XWPFRun newRun = paragraph.insertNewRun(i + j);
newRun.setText(parts[j]);
newRun.getCTR().getRPr().addNewVertAlign().setVal(STVerticalAlignRun.SUPERSCRIPT);
}
}
// Cập nhật lại danh sách runs sau khi chèn
runs = paragraph.getRuns();
i = runs.size() - 1;
}
}
}
}
/**
* Xử lý thẻ subscript và superscript trong các ô bảng.
*/
private void processTableCellSubSuperScript(List<XWPFTable> tables) {
for (XWPFTable table : tables) {
for (XWPFTableRow row : table.getRows()) {
for (XWPFTableCell cell : row.getTableCells()) {
List<XWPFParagraph> cellParagraphs = cell.getParagraphs();
for (XWPFParagraph paragraph : cellParagraphs) {
processSubSuperScript(Collections.singletonList(paragraph)); // Gọi lại hàm xử lý cho từng đoạn văn trong ô
}
}
}
}
}
/**
* Xử lý chú thích trong tài liệu.
*/
private void processComments(List<XWPFParagraph> paragraphs, XWPFDocument document) {
XWPFComments comments = document.getDocComments();
if (comments == null) {
comments = document.createComments();
}
CTComments ctComments = comments.getCtComments();
for (int i = 0; i < paragraphs.size(); i++) {
XWPFParagraph paragraph = paragraphs.get(i);
String text = paragraph.getText();
if (text.contains("")) {
String[] parts = text.split("", 2); // Tách thành 2 phần
if (parts.length == 2) {
String commentText = parts[1];
// Tạo chú thích mới
CTComment ctComment = ctComments.addNewComment();
ctComment.setId(BigInteger.valueOf(i)); // Sử dụng chỉ số đoạn văn làm ID
ctComment.setAuthor("AI"); // Đặt tên tác giả
ctComment.addNewP().addNewR().addNewT().setStringValue(commentText);
// Gắn chú thích vào đoạn văn bản
CTP ctp = paragraph.getCTP();
CTMarkupRange startRange = ctp.addNewCommentRangeStart();
startRange.setId(BigInteger.valueOf(i));
startRange.setB(BigInteger.ZERO); // Chỉ số bắt đầu trong đoạn văn
startRange.setE(BigInteger.valueOf(1)); // Chỉ số kết thúc trong đoạn văn (ví dụ)
// Xóa phần tử đánh dấu chú thích khỏi văn bản gốc
String originalText = parts[0];
paragraph.getRuns().forEach(run -> {
String runText = run.getText(0);
if (runText != null && runText.contains("")) {
run.setText(runText.replace("", ""), 0);
}
});
// Thêm tham chiếu đến chú thích vào cuối đoạn văn
XWPFRun commentRefRun = paragraph.createRun();
CTR ctr = commentRefRun.getCTR();
CTMarkup commentMarkup = ctr.addNewCommentReference();
commentMarkup.setId(BigInteger.valueOf(i));
}
}
}
}
/**
* Xử lý thay thế đầu trang và chân trang.
*/
private void handleHeaderFooterReplacement(XWPFDocument document, Map<String, String> headerFooterBookmarkMap) {
// Xử lý đầu trang
List<String> headerBookmarks = headerFooterBookmarkMap.keySet().stream()
.filter(key -> key.startsWith("replace_header_")).collect(Collectors.toList());
for (String bookmark : headerBookmarks) {
String value = headerFooterBookmarkMap.get(bookmark);
for (XWPFHeader header : document.getHeaderList()) {
processBookmarkInPart(header.getParagraphs(), bookmark, value);
processBookmarkInTables(header.getTables(), bookmark, value);
}
}
// Xử lý chân trang
List<String> footerBookmarks = headerFooterBookmarkMap.keySet().stream()
.filter(key -> key.startsWith("replace_footer_")).collect(Collectors.toList());
for (String bookmark : footerBookmarks) {
String value = headerFooterBookmarkMap.get(bookmark);
for (XWPFFooter footer : document.getFooterList()) {
processBookmarkInPart(footer.getParagraphs(), bookmark, value);
processBookmarkInTables(footer.getTables(), bookmark, value);
}
}
}
/**
* Tìm và thay thế dấu trang trong các đoạn văn của đầu trang/chân trang.
*/
private void processBookmarkInPart(List<XWPFParagraph> paragraphs, String bookmarkName, String value) {
for (XWPFParagraph paragraph : paragraphs) {
CTP ctp = paragraph.getCTP();
List<CTBookmark> bookmarkList = ctp.getBookmarkStartList();
for (CTBookmark bookmark : bookmarkList) {
if (bookmarkName.equals(bookmark.getName())) {
Node bookmarkNode = bookmark.getDomNode();
NodeList children = ctp.getDomNode().getChildNodes();
boolean inBookmark = false;
for (int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
if (child == bookmarkNode) {
inBookmark = true;
}
if (inBookmark && child.getNodeName().equals("w:r")) {
NodeList runChildren = child.getChildNodes();
for (int j = 0; j < runChildren.getLength(); j++) {
Node runChild = runChildren.item(j);
if (runChild.getNodeName().equals("w:t")) {
runChild.getFirstChild().setNodeValue(value);
return; // Đã thay thế xong
}
}
}
}
}
}
}
}
/**
* Tìm và thay thế dấu trang trong các bảng của đầu trang/chân trang.
*/
private void processBookmarkInTables(List<XWPFTable> tables, String bookmarkName, String value) {
for (XWPFTable table : tables) {
for (XWPFTableRow row : table.getRows()) {
for (XWPFTableCell cell : row.getTableCells()) {
for (XWPFParagraph paragraph : cell.getParagraphs()) {
processBookmarkInPart(Collections.singletonList(paragraph), bookmarkName, value);
}
}
}
}
}
/**
* Tính toán chiều rộng nội dung tối đa của tài liệu Word (tính bằng EMU).
* @param document Tài liệu XWPF.
* @return Chiều rộng nội dung tối đa (EMU).
*/
private long getWordMaxContentWidthInEMU(XWPFDocument document) {
CTSectPr sectPr = document.getDocument().getBody().getSectPr();
if (sectPr == null) sectPr = document.getDocument().getBody().addNewSectPr();
long pageWidth = 11906; // Mặc định là 11906 DXA (khoảng 6.5 inch)
if (sectPr.getPgSz() != null && sectPr.getPgSz().getW() != null) {
pageWidth = ((BigInteger) sectPr.getPgSz().getW()).longValue();
}
long leftMargin = 1800; // Mặc định là 1800 DXA (khoảng 1 inch)
long rightMargin = 1800;
if (sectPr.getPgMar() != null) {
if (sectPr.getPgMar().getLeft() != null) leftMargin = ((BigInteger) sectPr.getPgMar().getLeft()).longValue();
if (sectPr.getPgMar().getRight() != null) rightMargin = ((BigInteger) sectPr.getPgMar().getRight()).longValue();
}
return (pageWidth - leftMargin - rightMargin) * Units.EMU_PER_DXA;
}
/**
* Tách một chuỗi thành các phần văn bản và thẻ ảnh.
* @param text Chuỗi đầu vào.
* @return Danh sách các phần văn bản và thẻ ảnh.
*/
private List<String> splitTextAndImageTags(String text) {
List<String> result = new ArrayList<>();
// Regex để tìm kiếm thẻ img, thẻ markdown  và văn bản thông thường
String regex = "(
]*?>)|(!\\[]\\([^)]*?\\))|(.*?)(?=(
]*?>|\\[]\\([^)]*?\\)|$))";
Pattern pattern = Pattern.compile(regex, Pattern.DOTALL);
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
String imgTag = matcher.group(1);
String mdTag = matcher.group(2);
String plainText = matcher.group(3);
if (imgTag != null) {
result.add(imgTag);
} else if (mdTag != null) {
result.add(mdTag);
} else if (plainText != null && !plainText.trim().isEmpty()) {
result.add(plainText);
}
}
return result;
}
/**
* Chèn một bảng HTML vào tài liệu Word.
* @param htmlTable Phần tử bảng HTML.
* @param docx Tài liệu Word hiện tại.
* @param cursor Vị trí chèn.
* @param tblPr Thuộc tính bảng gốc (nếu có).
* @param firstLinePPr Thuộc tính đoạn văn dòng đầu tiên.
* @param firstLineRPr Thuộc tính run dòng đầu tiên.
* @param firstLineTcPr Thuộc tính ô đầu tiên.
* @param secondLinePPr Thuộc tính đoạn văn dòng thứ hai.
* @param secondLineRPr Thuộc tính run dòng thứ hai.
* @param secondLineTcPr Thuộc tính ô thứ hai.
* @param maxWidthInEMU Chiều rộng tối đa của trang.
*/
private void insertHtmlTable(Element htmlTable, XWPFDocument docx, XmlCursor cursor, CTTblPr tblPr,
CTPPr firstLinePPr, CTRPr firstLineRPr, CTTcPr firstLineTcPr, CTPPr secondLinePPr,
CTRPr secondLineRPr, CTTcPr secondLineTcPr, Long maxWidthInEMU) {
Elements rows = htmlTable.select("tr");
int numRows = rows.size();
int maxCols = 0;
// Tính toán số cột tối đa, có tính đến colspan
for (Element rowElem : rows) {
int currentCol = 0;
for (Element cellElem : rowElem.select("th,td")) {
int colspan = cellElem.hasAttr("colspan") ? Integer.parseInt(cellElem.attr("colspan")) : 1;
currentCol += colspan;
}
maxCols = Math.max(maxCols, currentCol);
}
XWPFTable table = docx.insertNewTbl(cursor);
if (tblPr != null) {
table.getCTTbl().setTblPr(tblPr);
} else {
table.setWidth("100%"); // Đặt chiều rộng bảng là 100% mặc định
}
// Khởi tạo số hàng và cột cần thiết
for (int r = 0; r < numRows; r++) {
if (r >= table.getRows().size()) {
table.createRow();
}
XWPFTableRow row = table.getRow(r);
for (int c = 0; c < maxCols; c++) {
if (c >= row.getCells().size()) {
row.createCell();
}
}
}
// Điền dữ liệu vào bảng và xử lý colspan, rowspan
for (int r = 0; r < numRows; r++) {
XWPFTableRow tableRow = table.getRow(r);
int currentCol = 0;
Elements cells = rows.get(r).select("th,td");
for (int cellIndex = 0; cellIndex < cells.size(); ) {
Element cellElem = cells.get(cellIndex++);
int colspan = cellElem.hasAttr("colspan") ? Integer.parseInt(cellElem.attr("colspan")) : 1;
int rowspan = cellElem.hasAttr("rowspan") ? Integer.parseInt(cellElem.attr("rowspan")) : 1;
// Tìm ô đích dựa trên currentCol và rowspan
XWPFTableCell cell = tableRow.getCell(currentCol);
if (cell == null) { // Nếu ô chưa tồn tại do rowspan từ hàng trước
cell = tableRow.getCells().get(currentCol);
}
// Đặt thuộc tính rowspan và colspan
if (rowspan > 1) {
cell.getCTTc().getTcPr().addNewVMerge().setVal(STMerge.RESTART);
}
if (colspan > 1) {
cell.getCTTc().getTcPr().addNewGridSpan().setVal(BigInteger.valueOf(colspan));
}
// Xử lý nội dung ô
processCellContent(cellElem, cell, document, firstLinePPr, firstLineRPr, firstLineTcPr, secondLinePPr, secondLineRPr, secondLineTcPr, maxWidthInEMU);
currentCol += colspan; // Tăng chỉ số cột hiện tại lên theo colspan
}
}
}
/**
* Xử lý nội dung của một ô bảng HTML.
*/
private void processCellContent(Element cellElem, XWPFTableCell cell, XWPFDocument docx,
CTPPr firstLinePPr, CTRPr firstLineRPr, CTTcPr firstLineTcPr,
CTPPr secondLinePPr, CTRPr secondLineRPr, CTTcPr secondLineTcPr, Long maxWidthInEMU) {
XWPFParagraph para = cell.getParagraphs().get(0);
CTP ctp = para.getCTP();
CTPPr pPr = null;
// Xác định thuộc tính P dựa trên vị trí hàng
if (cell.getCTTc().getTcPr().getVMerge() != null && cell.getCTTc().getTcPr().getVMerge().getVal() == STMerge.CONTINUE) {
// Nếu là ô tiếp tục của rowspan, sử dụng thuộc tính của hàng trước
// (Cần logic phức tạp hơn để xác định chính xác hàng, tạm thời bỏ qua hoặc sử dụng thuộc tính mặc định)
} else {
if (cell.getVerticalAlignment() == XWPFTableCell.XWPFVertAlign.TOP) { // Hàng đầu tiên
pPr = firstLinePPr;
} else { // Các hàng sau
pPr = secondLinePPr;
}
}
if (pPr != null) {
ctp.setPPr((CTPPr) pPr.copy()); // Sao chép thuộc tính
}
// Xử lý nội dung văn bản và hình ảnh
String html = cellElem.html();
// Loại bỏ các thẻ HTML không mong muốn, chỉ giữ lại thẻ ảnh, sup, sub
Safelist safelist = new Safelist();
safelist.addTags("img", "sup", "sub");
String cleanedHtml = Jsoup.clean(html, safelist);
List<String> parts = splitTextAndImageTags(cleanedHtml);
for (int i = 0; i < parts.size(); i++) {
XWPFRun run = (i == 0) ? para.getRuns().get(0) : para.createRun(); // Sử dụng run đầu tiên hoặc tạo mới
String part = parts.get(i);
try {
if (part.contains("
Tổng kết
Việc sử dụng Apache POI để thao tác với dấu trang trong Word là một phương pháp mạnh mẽ để tự động hóa việc tạo và chỉnh sửa tài liệu. Bằng cách hiểu rõ cấu trúc OOXML và các API của POI, chúng ta có thể thực hiện các thao tác phức tạp như thay thế, chèn và xóa nội dung một cách hiệu quả.
Các Vấn đề Thường gặp và Lưu ý
XmlValueDisconnectedException: Lỗi này thường xảy ra khi thao tác trực tiếp trên các nút DOM trong khi POI vẫn đang quản lý các đối tượng cấp cao hơn. Nên ưu tiên sử dụng API cấp cao của POI hoặc đảm bảo các thao tác DOM được thực hiện một cách cẩn thận và đồng bộ.
- Khuyến nghị sử dụng API Cấp cao: Đối với các tác vụ như chèn hoặc xóa cấu trúc phức tạp (bảng, hình ảnh), nên sử dụng API cấp cao của POI để đảm bảo tính nhất quán và tránh các lỗi liên quan đến DOM.
- Xử lý Đa luồng: Nếu cần xử lý đồng thời nhiều tài liệu, hãy đảm bảo mỗi luồng có một thể hiện
XWPFDocument riêng biệt để tránh xung đột.
Đăng vào ngày 11 tháng 6 lúc 18:38