在How do I change color of a particular word document using apache poi? 中,出于格式化原因,我展示了一种拆分XWPFRuns 的算法。这仅用于格式化一个字符,它不会克隆运行属性。但是基本显示出来了。我们必须查看整个段落,因为只有插入运行的方法。而且我们需要循环遍历运行文本字符,因为所有拆分成单词的方法都会导致标点符号出现问题,同时将单词重新组合成一个段落。
缺少的是一种将运行属性从原始运行克隆到新添加的运行属性的方法。这可以通过克隆底层的w:rPr 元素来完成。
那么整个方法就是遍历段落中的所有运行。如果我们有一个带有关键字的运行,则将运行文本拆分为字符。然后遍历该运行中的所有字符并缓冲它们。如果缓冲的字符流以关键字结尾,则将当前缓冲的所有字符(关键字除外)设置为实际运行的文本。然后为格式化的关键字插入新的运行,并从原始运行克隆运行属性。将关键字设置为运行并进行其他格式化。然后为下一个字符插入一个新的运行,并从原始运行中克隆运行属性。对于段落中的每次运行,依此类推。
完整示例:
import java.io.*;
import org.apache.poi.xwpf.usermodel.*;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.*;
import org.apache.xmlbeans.XmlObject;
import org.apache.xmlbeans.XmlCursor;
import java.util.*;
import java.awt.Desktop;
public class WordFormatWords {
static void cloneRunProperties(XWPFRun source, XWPFRun dest) { // clones the underlying w:rPr element
CTR tRSource = source.getCTR();
CTRPr rPrSource = tRSource.getRPr();
if (rPrSource != null) {
CTRPr rPrDest = (CTRPr)rPrSource.copy();
CTR tRDest = dest.getCTR();
tRDest.setRPr(rPrDest);
}
}
static void formatWord(XWPFParagraph paragraph, String keyword, Map<String, String> formats) {
int runNumber = 0;
while (runNumber < paragraph.getRuns().size()) { //go through all runs, we cannot use for each since we will possibly insert new runs
XWPFRun run = paragraph.getRuns().get(runNumber);
XWPFRun run2 = run;
String runText = run.getText(0);
if (runText != null && runText.contains(keyword)) { //if we have a run with keyword in it, then
// This code part is to manage comment ranges.
// Do we have commentRangeEnd immediately after the run?
// If so then remember that in a cursor.
XmlCursor commentRangeEndCursor = null;
XmlCursor cursor = run.getCTR().newCursor();
cursor.toEndToken();
if (cursor.hasNextToken()) {
cursor.toNextToken();
XmlObject commentRangeEnd = cursor.getObject();
if (commentRangeEnd != null && commentRangeEnd instanceof CTMarkupRange) {
commentRangeEndCursor = cursor;
}
}
char[] runChars = runText.toCharArray(); //split run text into characters
StringBuffer sb = new StringBuffer();
for (int charNumber = 0; charNumber < runChars.length; charNumber++) { //go through all characters in that run
sb.append(runChars[charNumber]); //buffer all characters
runText = sb.toString();
if (runText.endsWith(keyword)) { //if the bufferend character stream ends with the keyword
//set all chars, which are current buffered, except the keyword, as the text of the actual run
run.setText(runText.substring(0, runText.length() - keyword.length()), 0);
run2 = paragraph.insertNewRun(++runNumber); //insert new run for the formatted keyword
cloneRunProperties(run, run2); // clone the run properties from original run
run2.setText(keyword, 0); // set the keyword in run
for (String toSet : formats.keySet()) { // do the additional formatting
if ("color".equals(toSet)) {
run2.setColor(formats.get(toSet));
} else if ("bold".equals(toSet)) {
run2.setBold(Boolean.valueOf(formats.get(toSet)));
}
}
run2 = paragraph.insertNewRun(++runNumber); //insert a new run for the next characters
cloneRunProperties(run, run2); // clone the run properties from original run
run = run2;
sb = new StringBuffer(); //empty the buffer
}
}
run.setText(sb.toString(), 0); //set all characters, which are currently buffered, as the text of the actual run
// This code part is to manage comment ranges.
// If we had remembered commentRangeEnd, then move this to here now.
if(commentRangeEndCursor != null) {
cursor = run.getCTR().newCursor();
cursor.toEndToken();
if (cursor.hasNextToken()) {
cursor.toNextToken();
commentRangeEndCursor.moveXml(cursor);
}
cursor.dispose();
commentRangeEndCursor.dispose();
}
}
runNumber++;
}
}
public static void main(String[] args) throws Exception {
XWPFDocument doc = new XWPFDocument(new FileInputStream("source.docx"));
String[] keywords = new String[]{"fox", "dog"};
Map<String, String> formats = new HashMap<String, String>();
formats.put("bold", "true");
formats.put("color", "DC143C");
for (XWPFParagraph paragraph : doc.getParagraphs()) { //go through all paragraphs
for (String keyword : keywords) {
formatWord(paragraph, keyword, formats);
}
}
FileOutputStream out = new FileOutputStream("result.docx");
doc.write(out);
out.close();
doc.close();
System.out.println("Done");
Desktop.getDesktop().open(new File("result.docx"));
}
}
此代码还关注XML 标记范围元素,例如紧跟在运行的r 元素之后的commentRangeEnd。这种标记范围元素用于标记其他元素组的开始和结束。例如,应用注释的一组文本运行元素位于commentRangeStart 和commentRangeEnd 之间,具有相同的id。
如果在需要拆分的运行之后紧跟commentRangeEnd,那么我们在游标中记住它。然后在拆分运行后,我们将这个commentRangeEnd 立即移动到最后一个新插入的运行后面。所以 cmets 应该保持正确。
当然,即使这样也会有一些缺点,因为 Microsoft Word 有时在文本运行中存储文本的方式很笨拙。当Microsoft Word 是源时,没有唯一的通用解决方案。