由于最近写的项目需求需要为导出的excel文件添加水印,于是便上网搜,在途中也遇到了问题,本文用于记录整个过程
可以先看一下Java使用EasyExcel导出添加水印,我基本上是参考这篇文章来进行开发的
一、引入jar包
<!-- poi 添加水印 --> <dependency> <groupId>org.apache.poi</groupId> <artifactId>ooxml-schemas</artifactId> <version>1.4</version> </dependency> <!-- easy excel --> <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>3.0.5</version> </dependency> <!-- hutool工具类 --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.20</version> </dependency>
正在开发的项目中依赖版本是这样,可以根据自己需求更换版本
二、编码
详细过程描述可以查看参考文章中的代码
水印配置类
@Data public class Watermark { /** * 获取默认水印 - "xxx 时间 xxx" * * @return 默认水印 */ public static String getDefaultWatermark() { String str1 = ""; String date = DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss"); return String.format("%s %s %s", str1, date, ""); } public Watermark(String content) { this.content = content; init(); } public Watermark(String content, String color, Font font, double angle) { this.content = content; this.color = color; this.font = font; this.angle = angle; init(); } /** * 根据水印内容长度自适应水印图片大小,简单的三角函数 */ private void init() { FontMetrics fontMetrics = new JLabel().getFontMetrics(this.font); int stringWidth = fontMetrics.stringWidth(this.content); int charWidth = fontMetrics.charWidth('A'); this.width = (int)Math.abs(stringWidth * Math.cos(Math.toRadians(this.angle))) + 2 * charWidth; this.height = (int)Math.abs(stringWidth * Math.sin(Math.toRadians(this.angle))) + 2 * charWidth; this.yAxis = this.height; this.xAxis = charWidth; } /** * 水印内容 */ private String content; /** * 画笔颜色 */ private String color = "#CCCCCC"; /** * 字体样式 */ private Font font = new Font("Microsoft YaHei", Font.BOLD, 25); /** * 水印宽度 */ private int width; /** * 水印高度 */ private int height; /** * 倾斜角度,非弧度制 */ private double angle = 25; /** * 字体的y轴位置 */ private int yAxis; /** * 字体的X轴位置 */ private int xAxis; }
EasyExcel SheetWrite拦截器,该拦截器用于sheet写完后拦截为其添加水印,主要原理是每次写完sheet后都会调用SheetWriteHandler接口中的方法afterSheetCreate,此时就可以对sheet进行处理,这里添加水印的原理就是为excel添加背景
@Slf4j public class CustomWaterMarkHandler implements SheetWriteHandler { private final Watermark watermark; public CustomWaterMarkHandler(Watermark watermark) { this.watermark = watermark; } @Override public void beforeSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) { } @Override public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) { try { BufferedImage bufferedImage = createWatermarkImage(); // 注意使用EasyExcel时一定要开启inMemory,否则这里会报错,类型转换失败 // 不过网上还有一种方法是转换为SXSSFSheet,然后通过反射获取"_sh"属性拿到XSSFSheet,因为SXSSFSheet本身也是对XSSFSheet的封装, // 但是因为追求性能,有一些方法无法使用,比如更换背景,以及获取"_sh"的get方法 XSSFSheet sheet = (XSSFSheet)writeSheetHolder.getSheet(); setWaterMarkToExcel(sheet, bufferedImage); } catch (Exception e) { log.error("添加水印出错"); throw new CustomException(ResultEnum.EXCEL_EXPORT_LIST_FAIL); } } private BufferedImage createWatermarkImage() { final Font font = watermark.getFont(); final int width = watermark.getWidth(); final int height = watermark.getHeight(); String text = watermark.getContent(); BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); // 背景透明 开始 Graphics2D g = image.createGraphics(); image = g.getDeviceConfiguration().createCompatibleImage(width, height, Transparency.TRANSLUCENT); g.dispose(); // 背景透明 结束 g = image.createGraphics(); // 设定画笔颜色 g.setColor(new Color(Integer.parseInt(watermark.getColor().substring(1), 16))); // 设置画笔字体 g.setFont(font); // 设置字体平滑 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); int y = watermark.getYAxis(); int x = watermark.getXAxis(); // 设定倾斜角度,逆时针 AffineTransform transform = AffineTransform.getRotateInstance(Math.toRadians(-watermark.getAngle()), 0, y); g.setTransform(transform); g.drawString(text, x, y); g.setTransform(new AffineTransform()); // 释放画笔 g.dispose(); return image; } // 这里参考文章中是XSSFWorkbook workbook,然后遍历该workbook,可能导出excel文件错误 private void setWaterMarkToExcel(XSSFSheet sheet, BufferedImage bfi) { // 将图片添加到工作簿 XSSFWorkbook workbook = sheet.getWorkbook(); int pictureIdx = workbook.addPicture(ImgUtil.toBytes(bfi, ImgUtil.IMAGE_TYPE_PNG), Workbook.PICTURE_TYPE_PNG); // 建立 sheet 和 图片 的关联关系 XSSFPictureData xssfPictureData = workbook.getAllPictures().get(pictureIdx); // todo 设置excel不被修改,密码需要设置 sheet.protectSheet("password"); PackagePartName packagePartName = xssfPictureData.getPackagePart().getPartName(); PackageRelationship packageRelationship = sheet.getPackagePart() .addRelationship(packagePartName, TargetMode.INTERNAL, XSSFRelation.IMAGES.getRelation(), null); // 添加水印到工作表 sheet.getCTWorksheet().addNewPicture().setId(packageRelationship.getId()); } }
具体使用
public static <T> void exportSheetWithWatermark(HttpServletResponse response, String tableName, String sheetName, String watermarkContent, Class<T> clazz, List<T> exportList) throws IOException { // 这个 MIME 类型用于指示返回的文件扩展名通常是.xlsx response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8"); //建议加上该段,否则可能会出现前端无法获取Content-disposition response.setHeader("Access-Control-Expose-Headers", "Content-Disposition"); // 这里URLEncoder.encode可以防止中文乱码 String fileName = URLEncoder.encode(tableName, "UTF-8").replace("+", "%20");; response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx"); // 一定要inMemory ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()) .registerWriteHandler(new CustomWaterMarkHandler(new Watermark(watermarkContent))) .inMemory(true).build(); // 这里只是简单的封装,主要就是ExcelWriter.write调用和WriteSheet的构建,可以查看参考文章中代码,由于种种原因不便展示 EasyExcelUtil.writeSheetUtil(0, sheetName, clazz, excelWriter, exportList); excelWriter.finish(); }
三、结尾
再次感谢这篇文章Java使用EasyExcel导出添加水印,本文主要参考这篇文章
但是我之所以要还要写这篇文章是因为自己想记录一下,并且参考文章里存在一点问题,就是在导出单个sheet时不会有问题,但是导出多个sheet时可能会存在某个sheet文件内容错误的情况,至少在我的电脑上是,根据我的分析时因为在水印拦截器的setWaterMarkToExcel方法中接受参数是XSSFWorkbook,然后遍历该workbook为每个sheet设置背景,但是这个函数会在每一次创建sheet时都调用,导出多个sheet时,以2个为例,第一次会遍历sheet1,第二次遍历sheet1,sheet2,这里可以看到重复遍历了一次sheet1,然后设置背景的代码具体原理不是很清楚,但是会引起excel文件错误
然后还是有点小创新,比如水印图片大小根据水印内容自定义 (∠?ω< )⌒☆