由于最近写的项目需求需要为导出的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文件错误
然后还是有点小创新,比如水印图片大小根据水印内容自定义 (∠?ω< )⌒☆