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);

		// 添加图片到文档中,返回的是图片ID
		String blipId = paragraph.getDocument().addPictureData(bytes, format);

		// 创建图片关联的 CTDrawing
		int id = paragraph.getDocument().getNextPicNameNumber(format);
		XWPFRun run = paragraph.createRun();

		int width = 300; // px
		int height = 200; // px

		int widthEmu = Units.toEMU(width);
		int heightEmu = Units.toEMU(height);

		String picXml = getPicXml(blipId, widthEmu, heightEmu, id);

		// 读取为 CTInline
		CTInline 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>";
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

凌晨两点钟同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值