前言
前段时间接到了处理word文档的需求,因为没接触过,想找找相应的工具类,了解下文档处理的结构,第一时间查了hutool发现里面没有适用的工具方法,于是乎在网上扒了扒各大博客网友的轮子,加上自己的理解,写了自己适用的工具类,方便后续翻阅。
代码
RunStyle
import lombok.Builder;
import lombok.Data;
import org.apache.poi.xwpf.usermodel.XWPFRun;
/**
* Word文档对象Run,属性对象
*
* @author laozhou
* @date 2021/9/21
*/
@Builder
@Data
public class RunStyle {
/**
* 是否加粗:true=加粗、false=不加
*/
private Boolean bold;
/**
* 字体颜色
* 示例:#000000
*
* @return
*/
private String color;
/**
* 字体大小
*/
private Integer fontSize;
/**
* 字体主题
*/
private String fontFamily;
/**
* 是否在尾部携带换行
*/
private Boolean withNewline;
/**
* 设置XWPFRun的style
*
* @param run
*/
public void setStyle(XWPFRun run) {
if (null != run) {
run.setBold(null == this.bold ? run.isBold() : this.bold);
run.setColor(null == this.color ? run.getColor() : this.color);
run.setFontSize(null == this.fontSize ? run.getFontSize() : this.fontSize);
run.setFontFamily(null == this.fontFamily ? run.getFontFamily() : this.fontFamily);
// 是否换行
if (null != this.withNewline && this.withNewline) {
run.addBreak();
//run.addBreak(BreakType.TEXT_WRAPPING);
}
}
}
}
WordUtil
import org.apache.poi.util.Units;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.poi.xwpf.usermodel.XWPFTableCell;
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* <h3>poi原生-docx文档工具类</h3>
* <h3>基本概念说明:</h3>
* <ul>
* <li>XWPFDocument 代表一个docx文档,其可以用来读docx文档,也可以用来写docx文档</li>
* <li>XWPFParagraph 代表文档、表格、标题等种的段落,由多个XWPFRun组成</li>
* <li>XWPFRun 代表具有同样style的一段文本/区域</li>
* <li>XWPFTable 代表一个表格</li>
* <li>XWPFTableRow 代表表格的一行</li>
* <li>XWPFTableCell 代表表格的一个单元格</li>
* <li>XWPFChar 表示.docx文件中的图表</li>
* <li>XWPFHyperlink 表示超链接</li>
* <li>XWPFPicture 代表图片</li>
* <li>……</li>
* <li>注意:直接调用XWPFRun的setText()方法设置文本时,在底层会重新创建一个XWPFRun</li>
* </ul>
* <br/>
* <h3>docx相较于doc的区别:</h3>
* <ul>
* <li>在doc文件中中,微软还是用二进制存储方式;在docx中微软开始用xml方式,docx实际上成了一个打包的压缩文件;</li>
* <li>同样文件体积小</li>
* <li>对复杂对象处理更好,如公式编辑器,甚至可以在文件中播放视频和音频</li>
* <li>优化了加密</li>
* <li>……</li>
* </ul>
*
* @author laozhou
*/
public class WordUtil {
private static Logger log = LoggerFactory.getLogger(WordUtil.class);
private WordUtil() {
}
/**
* 中文Unicode编码值范围正则
* 注:在使用正则表达式时,利用好其预编译功能,可以有效加快正则匹配速度
*/
private static Pattern pattern = Pattern.compile("[\u4E00-\u9FA5]");
/**
* 创建一个word对象
*
* @return
*/
public static XWPFDocument createDoc() {
XWPFDocument document = new XWPFDocument();
return document;
}
/**
* 获取word文档对象
*
* @param filePath 文档路径
* @return
*/
public static XWPFDocument readDoc(String filePath) {
InputStream in = null;
try {
//XWPFDocument doc = new XWPFDocument(POIXMLDocument.openPackage(filePath));
in = new FileInputStream(filePath);
return new XWPFDocument(in);
} catch (Exception e) {
throw new RuntimeException("获取word文档对象失败,可能原因:" + e.getMessage());
} finally {
close(in);
}
}
/**
* 获取word文档对象
*
* @param in 数据输入流
* @return
*/
public static XWPFDocument readDoc(InputStream in) {
try {
return new XWPFDocument(in);
} catch (Exception e) {
throw new RuntimeException("获取word文档对象失败,可能原因:" + e.getMessage());
} finally {
close(in);
}
}
/**
* 写出word文档至指定路径
*
* @param doc 文档对象
* @param outPath 写出路径,包含文件名称及其后缀
*/
public static void writeDoc(XWPFDocument doc, String outPath) {
OutputStream out = null;
try {
out = new FileOutputStream(outPath);
doc.write(out);
} catch (Exception e) {
throw new RuntimeException("写出word文档对象失败,可能原因:" + e.getMessage());
} finally {
close(out);
}
}
/**
* 写出word文档至字节数组
*
* @param doc 文档对象
*/
public static byte[] writeDoc(XWPFDocument doc) {
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
doc.write(out);
return out.toByteArray();
} catch (Exception e) {
throw new RuntimeException("写出word文档对象失败,可能原因:" + e.getMessage());
} finally {
}
}
/**
* 根据段落,创建一段区域
* 注:setText方法可以设置文本,但新的文本不会替换旧的文本,而是会追加到原来的文本之后
*
* @param paragraph 段落对象
* @param index 插入的run的位置,从0开始;为空代表则直接创建,即插入尾部;
* @param text 待写入的文本
* @return
*/
public static XWPFRun createRun(XWPFParagraph paragraph, Integer index, String text, RunStyle style) {
try {
XWPFRun run;
if (null == index) {
run = paragraph.createRun();
} else {
for (int i = index; i < text.length() + index; i++) {
paragraph.removeRun(i);
}
run = paragraph.insertNewRun(index);
}
run.setText(text);
if (null != style) {
style.setStyle(run);
}
return run;
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
}
/**
* 创建段落/文本/区域
*
* @param cell
* @param text
*/
public static XWPFParagraph createParagraph(XWPFTableCell cell, String text, RunStyle style) {
// 新建一个标题段落对象(就是一段文字)
XWPFParagraph paragraph = cell.addParagraph();
// 样式居中
//paragraph.setAlignment(ParagraphAlignment.CENTER);
// 创建文本对象
XWPFRun run = paragraph.createRun();
// 设置文本名称
run.setText(text);
if (null != style) {
style.setStyle(run);
}
return paragraph;
}
/**
* 创建段落/文本/区域
*
* @param doc
* @param text
*/
public static XWPFParagraph createParagraph(XWPFDocument doc, String text, RunStyle style) {
// 新建一个标题段落对象(就是一段文字)
XWPFParagraph paragraph = doc.createParagraph();
// 样式居中
//paragraph.setAlignment(ParagraphAlignment.CENTER);
// 创建文本对象
XWPFRun run = paragraph.createRun();
// 设置文本名称
run.setText(text);
if (null != style) {
style.setStyle(run);
}
return paragraph;
}
/**
* 覆盖段落/文本/区域
*
* @param doc
* @param index 段落索引号,从0开始
* @param text
* @param style 段落文本样式
*/
public static XWPFParagraph converParagraph(XWPFDocument doc, int index, String text, RunStyle style) {
// 新建一个标题段落对象(就是一段文字)
XWPFParagraph paragraph = doc.createParagraph();
List<XWPFParagraph> paragraphs = doc.getParagraphs();
// 创建文本对象
XWPFRun run = paragraph.createRun();
// 设置文本名称
run.setText(text);
// 样式设置,必须放在段落移除之前,否则报错
if (null != style) {
style.setStyle(run);
}
int indexLen = paragraphs.size() - 1;
if (index != indexLen) {
// 覆盖段落
doc.setParagraph(paragraph, index);
// 去除尾部的段落
doc.removeBodyElement(doc.getPosOfParagraph(paragraphs.get(indexLen)));
}
return paragraph;
}
/**
* 根据模板生成新word文档
* 判断替换/插入逻辑:有$为替换,无$为插入
* 注:如果遇到poi读取例如{name}不能识别为一个整体,可以使用word的域操作;
* 如果不太清楚域的使用,可以这么操作,先在text文档中写好,例如{name},然后再整个复制到word中,不要一个一个在word中敲,不然有可能不会被poi识别为一个整体
*
* @param doc word文档对象
* @param textMap 需要替换的信息集合
* @param tableList 需要插入的表格信息集合
* @return 成功返回true, 失败返回false
*/
public static boolean changWord(XWPFDocument doc,
Map<String, String> textMap, List<String[]> tableList) {
boolean flag = true;
try {
// 解析替换文本段落对象
changeText(doc, textMap);
// 解析替换表格对象
changeTable(doc, textMap, tableList);
} catch (Exception e) {
flag = false;
}
return flag;
}
/**
* 替换段落文本
*
* @param document docx解析对象
* @param textMap 需要替换的信息集合
*/
public static void changeText(XWPFDocument document, Map<String, String> textMap) {
// 获取段落集合
List<XWPFParagraph> paragraphs = document.getParagraphs();
for (XWPFParagraph paragraph : paragraphs) {
// 判断此段落时候需要进行替换
String text = paragraph.getText();
if (checkText(text)) {
List<XWPFRun> runs = paragraph.getRuns();
for (XWPFRun run : runs) {
// 替换模板原来位置
run.setText(changeValue(run.toString(), textMap), 0);
}
}
}
}
/**
* 替换表格对象方法
*
* @param document docx解析对象
* @param textMap 需要替换的信息集合
* @param tableList 需要插入的表格信息集合
*/
public static void changeTable(XWPFDocument document, Map<String, String> textMap,
List<String[]> tableList) {
// 获取表格对象集合
List<XWPFTable> tables = document.getTables();
for (int i = 0; i < tables.size(); i++) {
// 只处理行数大于等于2的表格,且不循环表头
XWPFTable table = tables.get(i);
if (table.getRows().size() > 1) {
// 判断表格是需要替换还是需要插入,判断逻辑有$为替换,表格无$为插入
if (checkText(table.getText())) {
List<XWPFTableRow> rows = table.getRows();
// 遍历表格,并替换模板
eachTable(rows, textMap);
} else {
insertTable(table, tableList);
}
}
}
}
/**
* 遍历表格
*
* @param rows 表格行对象
* @param textMap 需要替换的信息集合
*/
public static void eachTable(List<XWPFTableRow> rows, Map<String, String> textMap) {
for (XWPFTableRow row : rows) {
List<XWPFTableCell> cells = row.getTableCells();
for (XWPFTableCell cell : cells) {
// 判断单元格是否需要替换
if (!checkText(cell.getText())) {
continue;
}
List<XWPFParagraph> paragraphs = cell.getParagraphs();
for (XWPFParagraph paragraph : paragraphs) {
List<XWPFRun> runs = paragraph.getRuns();
for (XWPFRun run : runs) {
run.setText(changeValue(run.toString(), textMap), 0);
}
}
}
}
}
/**
* 为制式表格插入数据,行数不够添加新行
*
* @param table 需要插入数据的表格
* @param tableList 插入数据集合
*/
public static void insertTable(XWPFTable table, List<String[]> tableList) {
if (null == tableList || 0 == tableList.size()) {
return;
}
// 创建行,根据需要插入的数据添加新行,不处理表头
for (int i = 1; i < tableList.size(); i++) {
table.createRow();
}
// 遍历表格插入数据
List<XWPFTableRow> rows = table.getRows();
for (int i = 1; i < rows.size(); i++) {
XWPFTableRow newRow = table.getRow(i);
List<XWPFTableCell> cells = newRow.getTableCells();
for (int j = 0; j < cells.size(); j++) {
XWPFTableCell cell = cells.get(j);
cell.setText(tableList.get(i - 1)[j]);
}
}
}
/**
* 插入word表格的指定位置
*
* @param table word表格对象
* @param rowNum 行号索引,从0开始
* @param colNum 列号索引,从0开始
* @param text 待插入的文本
* @description
*/
public static void insertText(XWPFTable table, int rowNum, int colNum, String text) {
try {
List<XWPFTableRow> rows = table.getRows();
XWPFTableRow row = rows.get(rowNum);
List<XWPFTableCell> cells = row.getTableCells();
XWPFTableCell cell = cells.get(colNum);
cell.setText(text);
//cell.setVerticalAlignment(XWPFTableCell.XWPFVertAlign.CENTER);
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
}
/**
* 重构段落不合格的占位符
* 比如:${abcd}分开为${a,b..,cd}
*
* @param paragraph
*/
public static void refactorParagraph(XWPFParagraph paragraph) {
int start = -1;
int end = -1;
List<XWPFRun> runs = paragraph.getRuns();
for (int i = 0; i < runs.size(); i++) {
String runText = runs.get(i).toString();
if ('$' == runText.charAt(0) && '}' == runText.charAt(runText.length() - 1)) {
continue;
}
if ('$' == runText.charAt(0)) {
start = i;
}
if ('}' == runText.charAt(runText.length() - 1)) {
end = i;
break;
}
}
if (start != -1) {
mergeRun(paragraph, start, end);
// 递归
refactorParagraph(paragraph);
}
}
/**
* 合并
*
* @param paragraph
* @param start
* @param end
*/
public static void mergeRun(XWPFParagraph paragraph, int start, int end) {
// 删除次数
int removeCount = end - start;
// 删除开始位置
int removeIndex = start + 1;
List<XWPFRun> runs = paragraph.getRuns();
StringBuilder sb = new StringBuilder();
sb.append(runs.get(start).toString());
for (int i = 0; i < removeCount; i++) {
sb.append(runs.get(removeIndex).toString());
paragraph.removeRun(removeIndex);
}
runs.get(start).setText(sb.toString(), 0);
}
/**
* 匹配传入信息集合与模板
*
* @param value 模板需要替换的区域
* @param textMap 传入信息集合
* @return 模板需要替换区域信息集合对应值
*/
public static String changeValue(String value, Map<String, String> textMap) {
Set<Map.Entry<String, String>> textSets = textMap.entrySet();
for (Map.Entry<String, String> textSet : textSets) {
// 匹配模板与替换值 格式${key}
String key = "${" + textSet.getKey() + "}";
if (value.indexOf(key) != -1) {
value = textSet.getValue();
}
}
// 模板未匹配到区域替换为空
if (checkText(value)) {
value = "";
}
return value;
}
/**
* 判断文本中时候包含$
*
* @param text 文本
* @return 包含返回true, 不包含返回false
*/
public static boolean checkText(String text) {
boolean check = false;
if (text.indexOf("$") != -1) {
check = true;
}
return check;
}
/**
* 根据空格数量创建空行
*
* @param count 空格数量
* @return
*/
public static String createSpace(int count) {
StringBuffer sb = new StringBuffer();
String space = " ";
for (int i = 0; i < count; i++) {
sb.append(space);
}
return sb.toString();
}
/**
* 根据空格数量创建空行
*
* @param totalCount 空格数量
* @param text 占用字符串
* @return
*/
public static String createSpaceByUse(int totalCount, String text) {
int useSpaceCount = getUseSpace(text);
return createSpace(totalCount - useSpaceCount);
}
/**
* 获取字符串所占用的空格数
*
* @param str 目标字符串
* @return
* @describe 中文字符默认占2个空格,其他为1
*/
public static int getUseSpace(String str) {
if (null == str || 0 == str.length()) {
return 0;
}
int totalLen = 0;
for (int i = 0; i < str.length(); i++) {
char ch = str.charAt(i);
boolean b = isChinese(ch);
totalLen += b ? 2 : 1;
}
return totalLen;
}
/**
* 根据TAB数量创建TAB
*
* @param count TAB数量
* @return
*/
public static String createTab(int count) {
StringBuffer sb = new StringBuffer();
String space = "\t";
for (int i = 0; i < count; i++) {
sb.append(space);
}
return sb.toString();
}
/**
* 关闭流
*
* @param closeable
*/
public static void close(AutoCloseable closeable) {
if (null != closeable) {
try {
closeable.close();
} catch (Exception e) {
}
}
}
/**
* 校验一个字符是否是中文(包括汉字、标点符号)
* 方法:根据byte的长度判断,英文的字母数字好标点符号都是一个byte,且值在0-255之间
*
* @param ch 待校验字符
* @return true代表是中文汉字
*/
public static boolean isChinese(char ch) {
try {
return String.valueOf(ch).getBytes("UTF-8").length > 1;
} catch (Exception e) {
return false;
}
}
/**
* 定制化构建尾部段落
*
* @param doc doc文档对象
* @param index 索引
* @param text1 文本一
* @param in 图片流一
* @param text2 文本二
* @param in2 图片流二
* @param style 段落样式对象
* @description 图片占位说明:图片宽度设为60,大概占12位空格,比例5:1;图片会根据所设宽度自适应比例
*/
public static XWPFParagraph converTailParagraph4Export(XWPFDocument doc, int index, String text1, InputStream in, String text2, InputStream in2, RunStyle style) {
try {
int totalSpace = 32;
int width = 60;
int heigth = 14;
int picSpace = width / 5;
XWPFParagraph paragraph = doc.createParagraph();
// 填充文本一
XWPFRun run1 = paragraph.createRun();
run1.setText(WordUtil.createSpace(4) + text1);
// 填充图片一
XWPFRun run2 = paragraph.createRun();
if (null != in) {
// 在run后面追加图片,不会覆盖该run本身
run2.addPicture(in, XWPFDocument.PICTURE_TYPE_JPEG, "fileName", Units.toEMU(width), Units.toEMU(heigth));
} else {
run2.setText(WordUtil.createSpace(12));
}
// 填充文本二
XWPFRun run3 = paragraph.createRun();
run3.setText(WordUtil.createSpace(totalSpace - picSpace) + text2);
// 填充图片二
XWPFRun run4 = paragraph.createRun();
if (null != in2) {
run4.addPicture(in2, XWPFDocument.PICTURE_TYPE_JPEG, "fileName", Units.toEMU(width), Units.toEMU(heigth));
} else {
}
// 统一设置样式,样式设置必须放在元素移除之前,否则报错:XmlValueDisconnectedException
if (null != style) {
style.setStyle(run1);
style.setStyle(run3);
}
// 段落覆盖处理
List<XWPFParagraph> paragraphs = doc.getParagraphs();
int indexLen = paragraphs.size() - 1;
if (index != indexLen) {
doc.setParagraph(paragraph, index);
doc.removeBodyElement(doc.getPosOfParagraph(paragraphs.get(indexLen)));
}
return paragraph;
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage());
} finally {
//close(in);
//close(in2);
}
}
/**
* 定制化构建头部段落
*
* @param doc doc文档对象
* @param index 索引
* @param text1 文本一
* @param dateStr 日期字符串
* @param text2 文本二
* @param numStr 索引号
* @param style 样式对象
* @description 注意事项:
* 1、调用示例:WordUtil.converHeadParagraph4Export(doc, 1, "日期:", dateStr, "索引号:", resp.getAuditRecordNo(), RunStyle.builder().bold(true).build());
* 2、调用时若不设置字体样式,则会沿用上个段落文本的样式
*/
public static XWPFParagraph converHeadParagraph4Export(XWPFDocument doc, int index, String text1, String dateStr, String text2, String numStr, RunStyle style) {
try {
int totalSpace = 40;
XWPFParagraph paragraph = doc.createParagraph();
// 填充文本一
XWPFRun run1 = paragraph.createRun();
run1.setText(text1);
run1.setBold(true);
// 填充日期字符串
XWPFRun run2 = paragraph.createRun();
run2.setText(dateStr);
XWPFRun run2HalfRun = paragraph.createRun();
run2HalfRun.setText(WordUtil.createSpaceByUse(totalSpace, dateStr));
// 填充文本二
XWPFRun run3 = paragraph.createRun();
run3.setText(text2);
// 填充索引号
XWPFRun run4 = paragraph.createRun();
run4.setText(numStr);
// 统一设置样式,样式设置必须放在元素移除之前,否则报错:XmlValueDisconnectedException
if (null != style) {
style.setStyle(run1);
style.setStyle(run3);
}
// 段落覆盖处理,先进行覆盖处理
List<XWPFParagraph> paragraphs = doc.getParagraphs();
int indexLen = paragraphs.size() - 1;
if (index != indexLen) {
doc.setParagraph(paragraph, index);
doc.removeBodyElement(doc.getPosOfParagraph(paragraphs.get(indexLen)));
}
return paragraph;
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage());
}
}
/**
* 将Run插入Paragraph中
*
* @param doc docx文档对象
* @param paraIndex 段落索引号,从0开始
* @param runIndex 待插入Run的目标索引号,从0开始,要根据已存在的run的index来插入,否则会报错
* @param text 待插入的文本
* @return
*/
public static XWPFParagraph breakInParagraphByRun(XWPFDocument doc, int paraIndex, int runIndex, String text, RunStyle style) {
try {
List<XWPFParagraph> paragraphs = doc.getParagraphs();
XWPFParagraph para = paragraphs.get(paraIndex);
XWPFRun run = para.insertNewRun(runIndex);
run.setText(text);
if (null != style) {
style.setStyle(run);
}
return para;
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
}
}
poi操作docx文档心得
-
上述工具代码涵盖功能:读/写docx文档
XWPFDocument
、创建/覆盖段落XWPFParagraph
、创建/合并/插入文本区域XWPFRun
、填充/插入表格XWPFTable
、填充图片、使用${自定义名称}
作模板关键字的替换等,适合用于文档定制化开发。 -
开发中应对问题的一些处理方法和注意事项:
- 使用空格计算并填充文本区域,把样式撑起来,如下图;
- 使用模板文件开发时,模板文件的一些空白的文本区域尽量使用空格号来填充;
- 填充图片时,注意图片宽度与空格的比例,避免在复杂需求中文档样式会错乱;
- 填充n张图片时,就需要n个不同的流对象(对于重复的图片流,poi只读取一次,生成唯一的标识,后续的填充数据则会被处理为null)。