当前位置: 首页 > news >正文

java导出word含表格并且带图片

背景

我们需要通过 Java 动态导出 Word 文档,基于预定义的 模板文件(如 .docx 格式)。模板中包含 表格,程序需要完成以下操作:

  1. 替换模板中的文本(如占位符 ${设备类型}  等)。

  2. 替换模板中的图片(如占位符 {{图片_作业现场}} )。

模板示例

模板文件(如 template.docx)结构大致如下:

maven依赖

<dependency><groupId>org.apache.poi</groupId><artifactId>poi</artifactId><version>3.17</version></dependency>
<dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml</artifactId><version>3.17</version></dependency><dependency><groupId>com.deepoove</groupId><artifactId>poi-tl</artifactId><version>1.12.1</version></dependency>

Controller

@ApiOperation(notes = "模板导出", value = "使用模板导出文档")
@RequestMapping(value = "/exportByTemplate", method = RequestMethod.GET)
public void exportByTemplate(HttpServletResponse response) {try {// 1. 设置响应头response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");response.setHeader("Content-Disposition", "attachment;filename=report.docx");// 2. 准备数据Map<String, Object> data = new HashMap<>();data.put("设备类型", "开关");data.put("属地运维单位", "湘江公司");data.put("作业现场", new String[]{"D:\\upload\\upload\\2025\\04\\14\\20250414070702.jpg","D:\\upload\\upload\\2025\\04\\14\\20250414070720.jpg"});// 3. 调用生成方法pdPointProblemService.generateFromTemplate(response,"D:\\1.docx", // 模板路径data);} catch (Exception e) {e.printStackTrace();// 异常处理(略)}
}

ServiceImpl

@Override
public void generateFromTemplate(HttpServletResponse response,String templatePath,Map<String, Object> data) throws Exception {// 1. 初始化文档(不使用try-with-resources)FileInputStream fis = new FileInputStream(templatePath);XWPFDocument doc = new XWPFDocument(fis);try {// 2. 执行替换replaceText(doc, data);replaceImages(doc, data);OutputStream out = response.getOutputStream();doc.write(out);out.flush();} finally {if (fis != null) {fis.close();}}
}private void replaceText(XWPFDocument doc, Map<String, Object> data) {// 替换段落中的文本for (XWPFParagraph p : doc.getParagraphs()) {replaceTextInParagraph(p, data);}// 替换表格中的文本for (XWPFTable table : doc.getTables()) {for (XWPFTableRow row : table.getRows()) {for (XWPFTableCell cell : row.getTableCells()) {for (XWPFParagraph p : cell.getParagraphs()) {replaceTextInParagraph(p, data);}}}}
}private void replaceTextInParagraph(XWPFParagraph paragraph, Map<String, Object> data) {// 1. 合并段落内所有Run的文本String fullText = mergeAllRuns(paragraph);if (!fullText.contains("${")) return;// 2. 执行全局替换String newText = replacePlaceholders(fullText, data);// 3. 清空原有Run的文本(保留样式)clearRunTexts(paragraph);// 4. 将新文本写入第一个Run(保留原始格式)if (!paragraph.getRuns().isEmpty()) {XWPFRun firstRun = paragraph.getRuns().get(0);firstRun.setText(newText, 0);} else {paragraph.createRun().setText(newText);}
}/*** 正则替换完整文本*/
private String replacePlaceholders(String text, Map<String, Object> data) {Pattern pattern = Pattern.compile("\\$\\{(.+?)}");Matcher matcher = pattern.matcher(text);StringBuffer sb = new StringBuffer();while (matcher.find()) {String key = matcher.group(1);Object value = data.getOrDefault(key, "");matcher.appendReplacement(sb, Matcher.quoteReplacement(value.toString()));}matcher.appendTail(sb);return sb.toString();
}/*** 清空所有Run的文本(保留样式)*/
private void clearRunTexts(XWPFParagraph paragraph) {for (XWPFRun run : paragraph.getRuns()) {run.setText("", 0); // 清空文本但保留Run对象}
}private void replaceImages(XWPFDocument doc, Map<String, Object> data) throws Exception {// 1. 处理普通段落for (XWPFParagraph p : doc.getParagraphs()) {processParagraphForImages(p, data);}// 2. 处理表格内的段落for (XWPFTable table : doc.getTables()) {for (XWPFTableRow row : table.getRows()) {for (XWPFTableCell cell : row.getTableCells()) {for (XWPFParagraph p : cell.getParagraphs()) {processParagraphForImages(p, data);}}}}
}/*** 统一处理段落中的图片占位符*/
private void processParagraphForImages(XWPFParagraph p, Map<String, Object> data) throws Exception {// 合并段落内所有Run的文本String mergedText = mergeAllRuns(p);if (mergedText.isEmpty()) return;// 正则匹配图片占位符Matcher matcher = Pattern.compile("\\{\\{图片_(.+?)}}").matcher(mergedText);if (!matcher.find()) return;String placeholder = matcher.group(0);String fieldName = matcher.group(1);// 清理占位符clearPlaceholderRuns(p, placeholder);// 插入图片if (data.containsKey(fieldName)) {
//            String imagePath = (String) data.get(fieldName);
//            insertImage(p, imagePath);String[] imageList  = (String[]) data.get(fieldName);insertImageList(p,imageList);}
}private void insertImageList(XWPFParagraph paragraph, String[] imagePaths) throws Exception {for (String imagePath : imagePaths) {File imageFile = new File(imagePath);if (!imageFile.exists()) {System.out.println("图片文件不存在: " + imagePath);}FileInputStream fis = new FileInputStream(imageFile);byte[] bytes = IOUtils.toByteArray(fis);fis.close();int format = getImageFormat(imagePath);// 添加图片到文档中,返回的是图片IDString blipId = paragraph.getDocument().addPictureData(bytes, format);// 创建图片关联的 CTDrawingint id = paragraph.getDocument().getNextPicNameNumber(format);XWPFRun run = paragraph.createRun();int width = 300; // pxint height = 200; // pxint widthEmu = Units.toEMU(width);int heightEmu = Units.toEMU(height);String picXml = getPicXml(blipId, widthEmu, heightEmu, id);// 读取为 CTInlineCTInline inline = run.getCTR().addNewDrawing().addNewInline();XmlToken xmlToken = XmlToken.Factory.parse(picXml);inline.set(xmlToken);// 设置图片的大小和描述inline.setDistT(0);inline.setDistB(0);inline.setDistL(0);inline.setDistR(0);CTPositiveSize2D extent = inline.addNewExtent();extent.setCx(widthEmu);extent.setCy(heightEmu);CTNonVisualDrawingProps docPr = inline.addNewDocPr();docPr.setId(id);docPr.setName("图片_" + id);docPr.setDescr("描述_" + id);// 可选:图片之间加个换行run.addBreak();}
}/*** 合并段落内所有Run的文本*/
private String mergeAllRuns(XWPFParagraph paragraph) {StringBuilder sb = new StringBuilder();for (XWPFRun run : paragraph.getRuns()) {String text = run.getText(0);if (text != null) {sb.append(text);}}return sb.toString();
}/*** 处理占位符跨多个Run的情况,并删除相关Run*/
private void clearPlaceholderRuns(XWPFParagraph paragraph, String placeholder) {List<XWPFRun> runs = paragraph.getRuns();if (runs == null || runs.isEmpty()) {return;}StringBuilder allText = new StringBuilder();List<Integer> runPositions = new ArrayList<>();// 收集每个run的起始位置for (XWPFRun run : runs) {runPositions.add(allText.length());String text = run.getText(0);if (text != null) {allText.append(text);}}String fullText = allText.toString();int startIndex = fullText.indexOf(placeholder);if (startIndex == -1) {return; // 找不到占位符,不处理}int endIndex = startIndex + placeholder.length();// 找到涉及到的 run 范围int runStart = -1;int runEnd = -1;for (int i = 0; i < runPositions.size(); i++) {int runPos = runPositions.get(i);if (runStart == -1 && runPos <= startIndex && (i == runPositions.size() - 1 || runPositions.get(i + 1) > startIndex)) {runStart = i;}if (runPos <= endIndex && (i == runPositions.size() - 1 || runPositions.get(i + 1) >= endIndex)) {runEnd = i;break;}}// 删除 run,注意:从后往前删,避免下标错乱for (int i = runEnd; i >= runStart; i--) {paragraph.removeRun(i);}
}/*** 获取图片格式类型*/
private int getImageFormat(String fileName) {String extension = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();switch (extension) {case "jpg":case "jpeg": return XWPFDocument.PICTURE_TYPE_JPEG;case "png":  return XWPFDocument.PICTURE_TYPE_PNG;default:     return XWPFDocument.PICTURE_TYPE_JPEG;}
}private static String getPicXml(String blipId, int widthEmu, int heightEmu, int id) {return"<a:graphic xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\">" +"   <a:graphicData uri=\"http://schemas.openxmlformats.org/drawingml/2006/picture\">" +"      <pic:pic xmlns:pic=\"http://schemas.openxmlformats.org/drawingml/2006/picture\">" +"         <pic:nvPicPr>" +"            <pic:cNvPr id=\"" + id + "\" name=\"Generated\"/>" +"            <pic:cNvPicPr/>" +"         </pic:nvPicPr>" +"         <pic:blipFill>" +"            <a:blip r:embed=\"" + blipId + "\" xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\"/>" +"            <a:stretch><a:fillRect/></a:stretch>" +"         </pic:blipFill>" +"         <pic:spPr>" +"            <a:xfrm>" +"               <a:off x=\"0\" y=\"0\"/>" +"               <a:ext cx=\"" + widthEmu + "\" cy=\"" + heightEmu + "\"/>" +"            </a:xfrm>" +"            <a:prstGeom prst=\"rect\">" +"               <a:avLst/>" +"            </a:prstGeom>" +"         </pic:spPr>" +"      </pic:pic>" +"   </a:graphicData>" +"</a:graphic>";
}

相关文章:

  • 一种改进的CFAR算法用于目标检测(解决多目标掩蔽)
  • 996引擎-实战笔记:Lua 的 NPC 面板获取 Input 内容
  • 从基础概念到前沿应用了解机器学习
  • 23种设计模式-创建型模式之单例模式(Java版本)
  • 用 Deepseek 写的html油耗计算器
  • AI 模型高效化:推理加速与训练优化的技术原理与理论解析
  • 基于Python的医疗质量管理指标智能提取系统【2025代码版】
  • 从入门到精通【MySQL】 JDBC
  • 05-DevOps-Jenkins自动拉取构建代码2
  • 「数据可视化 D3系列」入门第七章:坐标轴的使用
  • 数据结构——八大排序算法
  • 第十节:性能优化-如何排查组件不必要的重复渲染?
  • PH热榜 | 2025-04-17
  • requestAnimationFrame 深度理解
  • 第二十三天 - 性能优化技巧 - 内存分析与调优 - 练习:资源泄漏检测工具
  • GPT对话UI--通义千问API
  • 【LangChain4j快速入门】5分钟用Java玩转GPT-4o-mini,Spring Boot整合实战!| 附源码
  • 基于labview模拟出租车计价器的设计
  • 解锁动态规划的奥秘:从零到精通的创新思维解析(9)
  • React 设计艺术:如何精确拆分组件接口,实现接口隔离原则
  • 谷歌推出AI全家桶订阅计划:每月付费250美元,搜索引擎加入AI模式
  • 商务部:“一国一策”落实对非合作“十大伙伴行动”
  • 演员辛柏青发讣告,妻子朱媛媛去世
  • 凤阳文旅局回应鼓楼瓦片脱落:鼓楼楼宇系仿古建筑,动工时已履行报批手续
  • 上海乐高乐园客流预测来了:工作日0.8万人次/日,周末节假日2万人次/日
  • 上海国际电影节将于6月3日公布排片表,6月5日中午开票