EasyExcel导出多个sheet带水印

由于最近写的项目需求需要为导出的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文件错误

然后还是有点小创新,比如水印图片大小根据水印内容自定义 (∠?ω< )⌒☆