itext 实现动态绘制word表格
java对于word的操作目前开源的一个是poi,另一个是itext。先说一下这两者本人在这几天使用的体验,itext用起来方便快捷,操作比较简单,但是itext对于word的操作版本比较低一直停留二点几版本,高版本的都是对pdf的操作,感觉开发者似乎放弃了word的版本,以至于找遍全网也找不出itext对于word内的table中的单元格cell的高度调整,看源码也没有直接高度的调整,这个简直是个既简单又要命的问题,但是其他的操作还是的可以的。poi对word操作比较繁琐且麻烦,毕竟两者都在没有中文API文档参考下,使用itext无疑会快上许多。
pom
<dependency> <groupId>com.lowagie</groupId> <artifactId>itext</artifactId> <version>2.1.7</version> </dependency> <dependency> <groupId>com.lowagie</groupId> <artifactId>itext-rups</artifactId> <version>2.1.7</version> </dependency> <dependency> <groupId>com.lowagie</groupId> <artifactId>itext-rtf</artifactId> <version>2.1.7</version> </dependency>
itext动态绘制表格,比较麻烦的一个点就在于cell坐标的规划,一旦坐标错误,例如重复或者溢出的情况就会直接抛出异常。根据业务需求,定制table合适的列数就可以了。封装一下x,y坐标,每次加入一个cell,x,y随之移动一个坐标。
public Cell createCell(String text, Font font, int span) throws BadElementException { if (null == font) { font = this.font; } Cell cell = new Cell(new Paragraph(text, font)); cell.setColspan(span); cell.setHorizontalAlignment(Element.ALIGN_CENTER); return cell; }
createCell封装一下 单元格生成的,默认的字体样式,以及单元格样式,需要中多数字段需要合并单元格列,setColspan()也封装在一起。
public void addTableCell(Table table, Cell cell, int y, int x, int yOffset, int xOffset) throws BadElementException { table.addCell(cell, y, x); createXYCoordinate(yOffset, xOffset); } public void addTableCell(Table table, Cell cell, int y, int x, int yOffset) throws BadElementException { table.addCell(cell, y, x); createXYCoordinate(yOffset, 2); } public void createCoordinate() { x++; if (x >= 12) { x = 0; } } public void createYCoordinate(int yOffset) { y = y + yOffset; } public void createXCoordinate(int xOffset) { x = x + xOffset; if (x >= 12) { x = 0; } } public void createXYCoordinate(int yOffset, int xOffset) { x = x + xOffset; y = y + yOffset; if (x >= 12) { x = 0; } }
每次添加单元格,坐标也要移动到下一个单元格开始点。这里table表格设置了12列,所以当x坐标>= 12时,重置。
public void createHeaderFooter() { //页脚段落(这里为空,可以自己添加文字) Paragraph paraFooter = new Paragraph(""); Font font = new Font(); //页脚的字体大小 font.setSize(12f); font.setColor(new Color(0, 0, 0)); paraFooter.setFont(font); paraFooter.setAlignment("center"); //(参数一)页脚的段落 和 (参数二)是否有页码 HeaderFooter footer = new HeaderFooter(paraFooter, true); //页脚的对齐方式(应该在footer设置而不是段落中设置) footer.setAlignment(1); document.setFooter(footer); }
对于页脚的设置
完整的代码:
import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.etc.business.modules.entrust.entity.EntrustDict; import com.etc.business.modules.entrust.mapper.EntrustDictMapper; import com.etc.business.modules.entrust.service.IEntrustOrderService; import com.etc.business.modules.entrust.vo.EntrustOrderVO; import com.etc.business.modules.inspectconfig.entity.InspectionItem; import com.etc.business.modules.inspectconfig.entity.InspectionItemStandard; import com.etc.business.modules.inspectconfig.entity.Qualification; import com.etc.business.modules.inspectconfig.mapper.InspectionItemMapper; import com.etc.business.modules.inspectconfig.mapper.InspectionItemStandardMapper; import com.etc.business.modules.inspectconfig.mapper.QualificationMapper; import com.lowagie.text.*; import com.lowagie.text.Font; import com.lowagie.text.Image; import com.lowagie.text.rtf.RtfWriter2; import org.junit.Test; import org.junit.platform.commons.util.StringUtils; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.context.web.WebAppConfiguration; import java.awt.*; import java.io.*; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; /** * @Description: * @Author * @Date: * @Version: 1.0 */ @RunWith(SpringRunner.class) @SpringBootTest(classes = {com.etc.business.EtcBusinessApplication.class}, properties = {"application.properties"}) public class WordExportUtil { @Autowired private EntrustDictMapper dictMapper; @Autowired private InspectionItemMapper inspectionItemMapper; @Autowired private InspectionItemStandardMapper inspectionItemStandardMapper; @Autowired private QualificationMapper qualificationMapper; @Autowired private IEntrustOrderService entrustOrderService; private Document document; private Font font; //委托单的总行数 private int count = 0; private int y = 0; private int x = 0; private int span = 1; //设置word主体表格的总行数 private static final int totalCount = 20; //是指检测依据最大行数为20 private static final int testItemsMaxCount = 18; public WordExportUtil() { this.document = new Document(PageSize.A4, 0, 0, 5, 0); try { RtfWriter2.getInstance(document, new FileOutputStream("D:\\static\\sharedoc\\" + System.currentTimeMillis() + ".doc")); } catch (Exception e) { e.printStackTrace(); } document.open(); this.font = FontFactory.getFont("STSong-Light", "Cp1252", 8); } @Test public void test() { try { for (int i = 0; i < 3; i++) { EntrustDict entrustDict = dictMapper.selectById(10097); System.out.println(entrustDict.toString()); EntrustOrderVO entrustOrderVO = entrustOrderService.findEntrustOrderAllInfo(53L); JSONObject jsonObject = JSONObject.parseObject(entrustOrderVO.getExtraAttribute()); this.makeWord(entrustDict, jsonObject); if (!(i + 1 >= 3)) { document.newPage(); } } } catch (Exception e) { e.printStackTrace(); } finally { //关闭 closeDocument(); } } public void makeWord(EntrustDict entrustDict, JSONObject jsonValue) { //生产头部标题,二维码等 createTableHead(entrustDict); //生产主体信息 createMainTableInfo(entrustDict, jsonValue); //页脚设置 createHeaderFooter(); } public void closeDocument(){ if (document != null) { document.close(); } } public void createMainTableInfo(EntrustDict entrustDict, JSONObject jsonValue) { //计算委托单的总行数 getCountRows(entrustDict); JSONArray jsonArray = JSONArray.parseArray(entrustDict.getContent()); if (jsonArray.size() == 0) { return; } try { //创建表格,12列 Table table = new Table(12); table.setAlignment(0); table.setWidth(80); table.setPadding(12); table.setAlignment(Element.ALIGN_CENTER); for (int i = 0; i < jsonArray.size(); i++) { JSONObject jsonObject = jsonArray.getJSONObject(i); //加粗,四号 Font fontMo = FontFactory.getFont("STSong-Light", "Cp1252", 8); fontMo.setStyle(1); Cell cell = createCell(jsonObject.getString("moduleName"), fontMo, 12); //第一层模块信息 addTableCell(table, cell, y, x, 1, 12); JSONArray jsonArraySec = jsonObject.getJSONArray("options"); if (null == jsonArraySec || jsonArraySec.size() == 0) { //最后一个模块下处理说明信息value if (i == jsonArray.size() - 1) { addTableCell(table, createCell(entrustDict.getRemarks(), 12), y, x, 1, 12); } continue; } //循环模块下的每一行 for (int j = 0; j < jsonArraySec.size(); j++) { JSONArray jsonArrayThr = jsonArraySec.getJSONArray(j); //循环每一行的单元格 for (int k = 0; k < jsonArrayThr.size(); k++) { JSONObject object = jsonArrayThr.getJSONObject(k); Cell cellThr = createCell(object.getString("fieldName"),2); //获取value String value = jsonValue.getString(object.getString("fieldEn") == null ? "" : object.getString("fieldEn")); //检验项目,检验依据,格式特殊拿出来单处理 if ("testItems".equals(object.getString("fieldEn")) || "testBasis".equals(object.getString("fieldEn"))) { //执行一次,完成两个信息的录入 if ("testBasis".equals(object.getString("fieldEn"))) { continue; } //判断x坐标是否是新一行开始,否则重启一行 if (x != 0) { x = 0; y++; } //key addTableCell(table, createCell("检测依据", 6), y, x, 0, 6); addTableCell(table, createCell("检测项目", 6), y, x, 1, 6); //value-检测依据 value = getTestBasis(jsonValue); //通过value自动填充扩充单元格高度 value = fillingHeight(value); Cell cellBasis= createCell(value, 6); cellBasis.setHorizontalAlignment(Element.ALIGN_LEFT); addTableCell(table, cellBasis, y, x, 0, 6); //value-检测项目 List<String> valueList = getTestItems(jsonValue); Cell cellItem=createCell(valueList.get(0), 3); cellItem.setHorizontalAlignment(Element.ALIGN_LEFT); addTableCell(table, cellItem, y, x, 0, 3); String valueTest2=""; if (valueList.size() == 2) { valueTest2=valueList.get(1); } //设置检测项目第二个存储value单元格 Cell cellTest = createCell(valueTest2,3); cellTest.setHorizontalAlignment(Element.ALIGN_LEFT); addTableCell(table, cellTest, y, x, 0, 3); } else { addTableCell(table, cellThr, y, x, 0,2); //规格种类数据结构不同,单独处理 if ("sampleModel".equals(object.getString("fieldEn")) && StringUtils.isNotBlank(value)) { value = JSONObject.parseObject(value).getString("sampleModel"); } //解析获取资质证明名称 if ("qualification".equals(object.getString("fieldEn")) && StringUtils.isNotBlank(value)) { value = getQualification(jsonValue); } Cell cellThrValue = createCell((value), getSpan(object.getInteger("span"))); addTableCell(table, cellThrValue, y, x, 0, getSpan(object.getInteger("span"))); } } //最后一个单元格完毕,新增一行,重置x坐标 y++; x = 0; } } System.out.println("==============================================table:" + table.getHeight()); document.add(table); //完成一个委托单后重置坐标 x = 0; y = 0; } catch (Exception e) { e.printStackTrace(); } } public void createHeaderFooter() { //页脚段落(这里为空,可以自己添加文字) Paragraph paraFooter = new Paragraph(""); Font font = new Font(); //页脚的字体大小 font.setSize(12f); font.setColor(new Color(0, 0, 0)); paraFooter.setFont(font); paraFooter.setAlignment("center"); //(参数一)页脚的段落 和 (参数二)是否有页码 HeaderFooter footer = new HeaderFooter(paraFooter, true); //页脚的对齐方式(应该在footer设置而不是段落中设置) footer.setAlignment(1); document.setFooter(footer); } public void createTableHead(EntrustDict entrustDict) { try { Table tableHead = new Table(3); tableHead.setBorder(0); //二维码 String fileName = System.currentTimeMillis() + ".jpg"; File file = new File("D:\\static\\temp\\" + fileName); if (!file.exists()) { file.createNewFile(); } OutputStream os = new FileOutputStream(file); QrCodeUtils.encode("http://www.baidu.com", null, os, true); System.out.println("filePath:" + file.getAbsolutePath()); Image image = Image.getInstance(file.getAbsolutePath()); //设置图片大小 image.scalePercent(22.2f, 22.2f); image.setAlignment(Element.ALIGN_LEFT); Cell tCell = new Cell(image); tCell.setBorder(0); tCell.setRowspan(2); tableHead.addCell(tCell, 0, 0); //公司名称 Font fontCom = FontFactory.getFont("STSong-Light", "Cp1252", 12); Paragraph companyNamePar = new Paragraph(entrustDict.getDetectCompanyName(), fontCom); companyNamePar.setAlignment(Element.ALIGN_CENTER); Cell companyNameCell = new Cell(companyNamePar); companyNameCell.setBorder(0); tableHead.addCell(companyNameCell, 0, 1); //code Font fontCode = FontFactory.getFont("STSong-Light", "Cp1252", 6.5f); Paragraph codePar = new Paragraph(entrustDict.getTemplateCode(), fontCode); Cell codeCell = new Cell(codePar); codeCell.setHorizontalAlignment(Element.ALIGN_RIGHT); codeCell.setVerticalAlignment(Element.ALIGN_BOTTOM); codeCell.setBorder(0); codeCell.setRowspan(2); tableHead.addCell(codeCell, 0, 2); //委托单名称 Font fontCategoryName = FontFactory.getFont("STSong-Light", "Cp1252", 14); //加粗 fontCategoryName.setStyle(1); Paragraph categoryNamePar = new Paragraph(entrustDict.getEntrustOrderCategoryName(), fontCategoryName); Cell categoryNameParCell = new Cell(categoryNamePar); categoryNameParCell.setHorizontalAlignment(Element.ALIGN_CENTER); categoryNameParCell.setVerticalAlignment(Element.ALIGN_CENTER); categoryNameParCell.setBorder(0); tableHead.addCell(categoryNameParCell, 1, 1); document.add(tableHead); } catch (Exception e) { e.printStackTrace(); } } public String getQualification(JSONObject jsonValue) { String value = ""; Qualification qualification = qualificationMapper.selectById(jsonValue.getString("qualification")); if (null != qualification) { value = qualification.getQualificationName(); } return value; } /** * 检验项目value解析 * * @param jsonValue * @return */ public List<String> getTestItems(JSONObject jsonValue) { List<String> list = new ArrayList<>(); Object arrays[] = jsonValue.getJSONArray("testItems").toArray(); String value = ""; List<String> listItem = new ArrayList<>(); if (arrays.length > 0) { QueryWrapper<InspectionItem> queryWrapper = new QueryWrapper<>(); queryWrapper.lambda().in(InspectionItem::getInspectionItemId, arrays); listItem = inspectionItemMapper.selectList(queryWrapper).stream().map(InspectionItem::getInspectionItemName).collect(Collectors.toList()); } //一列最多存储14个 StringBuilder sb1 = new StringBuilder(); StringBuilder sb2 = new StringBuilder(); for (int i = 0; i < listItem.size(); i++) { if (i < 13&&i == listItem.size() - 1) { sb1.append(listItem.get(i)); list.add(sb1.toString()); continue; } if (i < 13) { sb1.append(listItem.get(i)).append("\n"); } if (i == 13) { sb1.append(listItem.get(i)); list.add(sb1.toString()); } if (i > 13&&i == listItem.size() - 1) { sb2.append(listItem.get(i)); list.add(sb2.toString()); continue; } if (i > 13) { sb2.append(listItem.get(i)).append("\n"); } } return list; } public String getTestBasis(JSONObject jsonValue) { Object[] arrays = jsonValue.getJSONArray("testBasis").toArray(); StringBuilder value = new StringBuilder(); if (arrays.length > 0) { QueryWrapper<InspectionItemStandard> queryWrapper = new QueryWrapper<>(); queryWrapper.lambda().in(InspectionItemStandard::getInspectionItemStandardId, arrays); List<InspectionItemStandard> list = inspectionItemStandardMapper.selectList(queryWrapper); for (int i = 0; i < list.size(); i++) { if (i == list.size() - 1) { value.append(list.get(i).getInspectionItemMethodName()).append(list.get(i).getInspectionItemMethodNumber()); } else { value.append(list.get(i).getInspectionItemMethodName()).append(list.get(i).getInspectionItemMethodNumber()).append("\n"); } } } return value.toString(); } /** * 计算委托单主体表的行数 * * @param entrustDict */ public void getCountRows(EntrustDict entrustDict) { JSONArray jsonArray = JSONArray.parseArray(entrustDict.getContent()); int count = jsonArray.size(); for (int i = 0; i < jsonArray.size(); i++) { JSONObject jsonObject = jsonArray.getJSONObject(i); JSONArray jsonArraySec = jsonObject.getJSONArray("options"); count = count + jsonArraySec.size(); } this.count = count; } public String fillingHeight(String value) { int valueLength = value.split(",").length; value = value.replace("[", "").replace("]", "").replace(",", "\n"); StringBuilder sb = new StringBuilder(value); //totalCount-count:如果不满足总行数则需要扩充行数;testItemsMaxCount-valueLength:检验项目最大行数减去已有内容占的行数 int total = totalCount - count + testItemsMaxCount - valueLength; if (total <= 0) { return value; } for (int i = 0; i < total; i++) { if (i == total - 1) { continue; } sb.append("\n"); } return sb.toString(); } public void addTableCell(Table table, Cell cell, int y, int x, int yOffset, int xOffset) throws BadElementException { table.addCell(cell, y, x); createXYCoordinate(yOffset, xOffset); } public void addTableCell(Table table, Cell cell, int y, int x, int yOffset) throws BadElementException { table.addCell(cell, y, x); createXYCoordinate(yOffset, 2); } public void createCoordinate() { x++; if (x >= 12) { x = 0; } } public void createYCoordinate(int yOffset) { y = y + yOffset; } public void createXCoordinate(int xOffset) { x = x + xOffset; if (x >= 12) { x = 0; } } public void createXYCoordinate(int yOffset, int xOffset) { x = x + xOffset; y = y + yOffset; if (x >= 12) { x = 0; } } public Cell createCell(String text) throws BadElementException { Cell cell = new Cell(new Paragraph(text, font)); cell.setHorizontalAlignment(Element.ALIGN_CENTER); return cell; } public Cell createCell(String text, Font font) throws BadElementException { if (null == font) { font = this.font; } Cell cell = new Cell(new Paragraph(text, font)); cell.setHorizontalAlignment(Element.ALIGN_CENTER); return cell; } public Cell createCell(String text, Font font, int span) throws BadElementException { if (null == font) { font = this.font; } Cell cell = new Cell(new Paragraph(text, font)); cell.setColspan(span); cell.setHorizontalAlignment(Element.ALIGN_CENTER); return cell; } public Cell createCell(String text, int span) throws BadElementException { Cell cell = new Cell(new Paragraph(text, font)); cell.setColspan(span); cell.setHorizontalAlignment(Element.ALIGN_CENTER); return cell; } public int getSpan(int span) { return 2*(span * 2 - 1); } }
涉及到二维码的生成工具类:
import com.google.zxing.BarcodeFormat; import com.google.zxing.EncodeHintType; import com.google.zxing.MultiFormatWriter; import com.google.zxing.common.BitMatrix; import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; import javax.imageio.ImageIO; import java.awt.*; import java.awt.geom.RoundRectangle2D; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Hashtable; /** * @Description:二维码生成工具类 * @Author * @Date: * @Version: 1.0 */ public class QrCodeUtils { private static final String CHARSET = "utf-8"; public static final String FORMAT = "JPG"; // 二维码尺寸 private static final int QRCODE_SIZE = 300; // LOGO宽度 private static final int LOGO_WIDTH = 60; // LOGO高度 private static final int LOGO_HEIGHT = 60; /** * 生成二维码 * * @param content 二维码内容 * @param logoPath logo地址 * @param needCompress 是否压缩logo * @return 图片 * @throws Exception */ public static BufferedImage createImage(String content, String logoPath, boolean needCompress) throws Exception { Hashtable<EncodeHintType, Object> hints = new Hashtable<EncodeHintType, Object>(); hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); hints.put(EncodeHintType.CHARACTER_SET, CHARSET); hints.put(EncodeHintType.MARGIN, 1); BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, QRCODE_SIZE, QRCODE_SIZE, hints); int width = bitMatrix.getWidth(); int height = bitMatrix.getHeight(); BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { image.setRGB(x, y, bitMatrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF); } } if (logoPath == null || "".equals(logoPath)) { return image; } // 插入图片 QrCodeUtils.insertImage(image, logoPath, needCompress); return image; } /** * 插入LOGO * * @param source 二维码图片 * @param logoPath LOGO图片地址 * @param needCompress 是否压缩 * @throws IOException */ private static void insertImage(BufferedImage source, String logoPath, boolean needCompress) throws IOException { InputStream inputStream = null; try { inputStream = QrCodeUtils.getResourceAsStream(logoPath); Image src = ImageIO.read(inputStream); int width = src.getWidth(null); int height = src.getHeight(null); if (needCompress) { // 压缩LOGO if (width > LOGO_WIDTH) { width = LOGO_WIDTH; } if (height > LOGO_HEIGHT) { height = LOGO_HEIGHT; } Image image = src.getScaledInstance(width, height, Image.SCALE_SMOOTH); BufferedImage tag = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics g = tag.getGraphics(); g.drawImage(image, 0, 0, null); // 绘制缩小后的图 g.dispose(); src = image; } // 插入LOGO Graphics2D graph = source.createGraphics(); int x = (QRCODE_SIZE - width) / 2; int y = (QRCODE_SIZE - height) / 2; graph.drawImage(src, x, y, width, height, null); Shape shape = new RoundRectangle2D.Float(x, y, width, width, 6, 6); graph.setStroke(new BasicStroke(3f)); graph.draw(shape); graph.dispose(); } catch (IOException e) { e.printStackTrace(); throw new RuntimeException(e); } finally { if (inputStream != null) { inputStream.close(); } } } /** * 生成二维码(内嵌LOGO) * * @param content 内容 * @param logoPath LOGO地址 * @param output 输出流 * @param needCompress 是否压缩LOGO * @throws Exception */ public static void encode(String content, String logoPath, OutputStream output, boolean needCompress) throws Exception { BufferedImage image = QrCodeUtils.createImage(content, logoPath, needCompress); ImageIO.write(image, FORMAT, output); } /** * 获取指定文件的输入流,获取logo * * @param logoPath 文件的路径 * @return */ public static InputStream getResourceAsStream(String logoPath) { return QrCodeUtils.class.getResourceAsStream(logoPath); } }
对于cell的高度调整,虽然cell本身没有,但是table.setPadding(12),可以统一设置整个表格的每行的高度。cell的高度是以文字内容的大小自动填充的,这里我使用在特殊处理的单元格上添加换行符,来扩充单元格内容达到调整cell高度的目的。
一些基本itext对于word的操作参考:https://www.jianshu.com/p/cb14bff0a6b4