给导出的excel添加水印
覆盖图片式添加水印
此方式是根据覆盖图片形式进行的水印的添加,缺点是在可以修改情况下,覆盖的图片会影响修改操作。
1.创建水印图片
/**
* 创建水印图片
* 注意:生成的图片会默认保存到classes目录下,可以根据自己的业务进行更改
* @param content
* @param fileName
* @return
* @throws IOException
*/
private static String createWaterMark(String content, String fileName) {
//生成水印图片的宽度
Integer width = 700;
//水印图片的高度
Integer height = 300;
// 获取bufferedImage对象
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
String fontType = "宋体";
Integer fontStyle = java.awt.Font.PLAIN;
Integer fontSize = 50;
java.awt.Font font = new java.awt.Font(fontType, fontStyle, fontSize);
// 获取Graphics2d对象
Graphics2D g2d = image.createGraphics();
image = g2d.getDeviceConfiguration().createCompatibleImage(width, height, Transparency.TRANSLUCENT);
g2d.dispose();
g2d = image.createGraphics();
g2d.setColor(new java.awt.Color(0, 0, 0, 80)); //设置字体颜色和透明度
// 设置字体
g2d.setStroke(new BasicStroke(1));
// 设置字体类型 加粗 大小
g2d.setFont(font);
//设置倾斜度
g2d.rotate(Math.toRadians(-10), (double) image.getWidth() / 2, (double) image.getHeight() / 2);
FontRenderContext context = g2d.getFontRenderContext();
Rectangle2D bounds = font.getStringBounds(content, context);
double x = (width - bounds.getWidth()) / 2;
double y = (height - bounds.getHeight()) / 2;
double ascent = -bounds.getY();
double baseY = y + ascent;
// 写入水印文字原定高度过小,所以累计写水印,增加高度
g2d.drawString(content, (int) x, (int) baseY);
// 设置透明度
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
// 释放对象
g2d.dispose();
//自定义存放位置
String targetImagePath ="F:\\data\\erp\\"+fileName;
//默认存放位置
// String s = Thread.currentThread().getContextClassLoader().getResource("").getPath() + fileName;
try {
ImageIO.write(image, "png", new File(targetImagePath));
} catch (IOException e) {
e.printStackTrace();
}
return targetImagePath;
}
2. 给excel加水印
/**
* 为Excel打上水印工具函数 请自行确保参数值,以保证水印图片之间不会覆盖。在计算水印的位置的时候,并没有考虑到单元格合并的情况,请注意
* @param wb Excel Workbook
* @param sheet 需要打水印的Excel
* @param waterRemarkPath 水印地址,classPath,目前只支持png格式的图片,
* 因为非png格式的图片打到Excel上后可能会有图片变红的问题,且不容易做出透明效果。
* 同时请注意传入的地址格式,应该为类似:"\\excelTemplate\\test.png"
* @param startXCol 水印起始列
* @param startYRow 水印起始行
* @param betweenXCol 水印横向之间间隔多少列
* @param betweenYRow 水印纵向之间间隔多少行
* @param XCount 横向共有水印多少个
* @param YCount 纵向共有水印多少个
* @param waterRemarkWidth 水印图片宽度为多少列
* @param waterRemarkHeight 水印图片高度为多少行
* @throws IOException
*/
private static void putWaterRemarkToExcel(Workbook wb, Sheet sheet, String waterRemarkPath, int startXCol,
int startYRow, int betweenXCol, int betweenYRow, int XCount, int YCount, int waterRemarkWidth,
int waterRemarkHeight) throws IOException {
// 校验传入的水印图片格式
if (!waterRemarkPath.endsWith("png") && !waterRemarkPath.endsWith("PNG")) {
throw new RuntimeException("向Excel上面打印水印,目前支持png格式的图片。");
}
// 加载图片
ByteArrayOutputStream byteArrayOut = new ByteArrayOutputStream();
//图片的存放位置
String path = "F:\\data\\erp\\" + waterRemarkPath;
InputStream imageIn = new FileInputStream(path);
//默认位置的图片
// InputStream imageIn = Thread.currentThread().getContextClassLoader().getResourceAsStream(waterRemarkPath);
if (null == imageIn || imageIn.available() < 1) {
throw new RuntimeException("向Excel上面打印水印,读取水印图片失败(1)。");
}
BufferedImage bufferImg = ImageIO.read(imageIn);
if (null == bufferImg) {
throw new RuntimeException("向Excel上面打印水印,读取水印图片失败(2)。");
}
ImageIO.write(bufferImg, "png", byteArrayOut);
// 开始打水印
Drawing drawing = sheet.createDrawingPatriarch();
// 按照共需打印多少行水印进行循环
for (int yCount = 0; yCount < YCount; yCount++) {
// 按照每行需要打印多少个水印进行循环
for (int xCount = 0; xCount < XCount; xCount++) {
// 创建水印图片位置
int xIndexInteger = startXCol + (xCount * waterRemarkWidth) + (xCount * betweenXCol);
int yIndexInteger = startYRow + (yCount * waterRemarkHeight) + (yCount * betweenYRow);
/** 参数定义:第一个参数是(x轴的开始节点);第二个参数是(是y轴的开始节点);第三个参数是(是x轴的结束节点);
* 第四个参数是(是y轴的结束节点);第五个参数是(是从Excel的第几列开始插入图片,从0开始计数);
* 第六个参数是(是从excel的第几行开始插入图片,从0开始计数);第七个参数是(图片宽度,共多少列);
* 第8个参数是(图片高度,共多少行);*/
ClientAnchor anchor = drawing.createAnchor(0, 0, 0, 0, xIndexInteger,
yIndexInteger, xIndexInteger + waterRemarkWidth, yIndexInteger + waterRemarkHeight);
Picture pic = drawing.createPicture(anchor, wb.addPicture(byteArrayOut.toByteArray(), Workbook.PICTURE_TYPE_PNG));
pic.resize();
}
}
}
3.使用
/**
* 给Excel添加水印
* @param wb 工作簿
* @param sheet sheet页
* @param content 水印内容
*/
private static void painWaterMark(Workbook wb,Sheet sheet,String content) {
String imgFileName = "waterMark_photo_"+content+".png";
//创建水印图片 (默认保存到classes目录下)
createWaterMark(content,imgFileName);
//将图片写入到excel中
try {
//也可以动态获取sheet中的行和列,根据行和列适当的放置水印图片
//获取excel实际所占行
//int row = sheet.getFirstRowNum() + sheet.getLastRowNum();
//获取excel实际所占列
//int cell = sheet.getRow(sheet.getFirstRowNum()).getLastCellNum() + 1;
putWaterRemarkToExcel(wb, sheet, imgFileName, 0, 0, 5, 5,2, 10, 2, 2);
} catch (IOException e) {
e.printStackTrace();
}
}
此时的导出的excel可能会支持修改,会造成水印图片支持拖动,所以可以对文件加密只读
//excel加密只读
sheet.protectSheet(UUID.randomUUID().toString());
加密后的文件输入密码后支持修改
实现样式:
此部分代码只需要添加到excel写出之前(下图)就可以实现excel的添加水印
注意:1.此种方法添加的水印,如果在可修改状态会造成图片可拖动问题
2.图片大小是写死的没有根据水印文字进行自动变化大小
3.列是写死的,如果不知修改,可能会造成部分数据不能被查看
优化:
动态展示水印(水印看起来规整):
/**
* @param startXCol 水印起始列
* @param startYRow 水印起始行
* @param betweenXCol 水印横向之间间隔多少列
* @param betweenYRow 水印纵向之间间隔多少行
* @param XCount 横向共有水印多少个
* @param YCount 纵向共有水印多少个
* @param waterRemarkWidth 水印图片宽度为多少列
* @param waterRemarkHeight 水印图片高度为多少行
* @throws IOException
*/
// 也可以动态获取sheet中的行和列,根据行和列适当的放置水印图片
// 获取excel实际所占行
int row = sheet.getFirstRowNum() + sheet.getLastRowNum();
// 获取excel实际所占列
int cell = sheet.getRow(sheet.getFirstRowNum()).getLastCellNum() + 1;
// 纵向个数,向上取整
int YCount = row / (waterRemarkHeight + betweenYRow) + ((row % (waterRemarkHeight + betweenYRow) != 0) ? 1 : 0);
// 横向个数,向上取整
int XCount = cell / (waterRemarkWidth + betweenXCol) + ((cell % (waterRemarkWidth + betweenXCol) != 0) ? 1 : 0);
让列宽随着导出的列长自动适应:
autoSizeColumn(colNum)方法可能会报错,就需要跟踪所有的列,但是跟踪所有的列会使性能变差。
trackAllColumnsForAutoSizing()方法是SXSSFSheet的方法,如果使用的是Sheet类,就需要强转一下
private void autoColumn() {
//让列宽随着导出的列长自动适应
int maxColumnWidth = 30 * 256;
int columnNum = sheet.getRow(0).getPhysicalNumberOfCells();
SXSSFSheet sheetAt;
for (int colNum = 0; colNum < columnNum; colNum++) {
//自动列宽
sheetAt = (SXSSFSheet) sheet;
// 跟踪所有用于自动调整大小的列,性能比较差
sheetAt.trackAllColumnsForAutoSizing();
sheet.autoSizeColumn(colNum);
//like12 add,20220122,设置最大宽度限制
int columnWidth = sheet.getColumnWidth(colNum);
if (columnWidth > maxColumnWidth) {
columnWidth = maxColumnWidth;
}
//手动调整列宽,解决中文不能自适应问题
sheet.setColumnWidth(colNum, columnWidth * 12 / 10);
}
}
2.添加背景图片形式的水印
此方法是在excel中添加背景图片,所以不会影响操作修改
结合本地的项目,先进行导包
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>ooxml-schemas</artifactId>
<version>1.4</version>
</dependency>
已经存在的包
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
1.还是先创建背景图片,使用的还是上面的方法
/**
* 创建水印图片
* 注意:生成的图片会默认保存到classes目录下,可以根据自己的业务进行更改
*
* @param content
* @param fileName
* @return
* @throws IOException
*/
private static String createWaterMark(String content, String fileName) {
//生成水印图片的宽度
Integer width = 700;
//水印图片的高度
Integer height = 300;
// 获取bufferedImage对象
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
String fontType = "宋体";
Integer fontStyle = java.awt.Font.PLAIN;
Integer fontSize = 30;
java.awt.Font font = new java.awt.Font(fontType, fontStyle, fontSize);
// 获取Graphics2d对象
Graphics2D g2d = image.createGraphics();
image = g2d.getDeviceConfiguration().createCompatibleImage(width, height, Transparency.TRANSLUCENT);
g2d.dispose();
g2d = image.createGraphics();
g2d.setColor(new java.awt.Color(0, 0, 0, 95)); //设置字体颜色和透明度
// 设置字体
g2d.setStroke(new BasicStroke(1));
// 设置字体类型 加粗 大小
g2d.setFont(font);
//设置倾斜度
g2d.rotate(Math.toRadians(-10), (double) image.getWidth() / 2, (double) image.getHeight() / 2);
FontRenderContext context = g2d.getFontRenderContext();
Rectangle2D bounds = font.getStringBounds(content, context);
double x = (width - bounds.getWidth()) / 2;
double y = (height - bounds.getHeight()) / 2;
double ascent = -bounds.getY();
double baseY = y + ascent;
// 写入水印文字原定高度过小,所以累计写水印,增加高度
g2d.drawString(content, (int) x, (int) baseY);
// 设置透明度
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
// 释放对象
g2d.dispose();
//自定义存放位置
String targetImagePath = "F:\\data\\erp\\" + fileName;
//默认存放位置
// String s = Thread.currentThread().getContextClassLoader().getResource("").getPath() + fileName;
try {
ImageIO.write(image, "png", new File(targetImagePath));
} catch (IOException e) {
e.printStackTrace();
}
return targetImagePath;
}
2.添加背景水印
/**
* 添加水印背景图片
*
* @param workbook 文件
* @param content 水印
*/
public static void setWaterMarkToExcel2(XSSFWorkbook workbook, String content) throws Exception {
// 获取背景图片
String imgFileName = "waterMark_photo" + StringUtils.trim(content) + ".png";
String path = "F:\\data\\erp\\" + imgFileName;
InputStream imageIn = new FileInputStream(path);
//默认位置的图片
// InputStream imageIn = Thread.currentThread().getContextClassLoader().getResourceAsStream(waterRemarkPath);
if (imageIn.available() < 1) {
throw new RuntimeException("向Excel上面打印水印,读取水印图片失败(1)。");
}
BufferedImage bfi = ImageIO.read(imageIn);
// 添加背景水印
ByteArrayOutputStream byteArrayOut = new ByteArrayOutputStream();
ImageIO.write(bfi, "png", byteArrayOut);
int pictureIdx = workbook.addPicture(byteArrayOut.toByteArray(), Workbook.PICTURE_TYPE_PNG);
//add relation from sheet to the picture data
POIXMLDocumentPart poixmlDocumentPart = workbook.getAllPictures().get(pictureIdx);
for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
XSSFSheet xssfSheet = workbook.getSheetAt(i);
PackagePartName ppn = poixmlDocumentPart.getPackagePart().getPartName();
String relType = XSSFRelation.IMAGES.getRelation();
PackageRelationship pr = xssfSheet.getPackagePart().addRelationship(ppn, TargetMode.INTERNAL, relType, null);
xssfSheet.getCTWorksheet().addNewPicture().setId(pr.getId());
}
}
3.整合的方法
public void waterRemarkToExcel(XSSFWorkbook wb, String content) {
String imgFileName = "waterMark_photo" + StringUtils.trim(content) + ".png";
//创建水印图片
createWaterMark(content, imgFileName);
//添加背景水印
try {
setWaterMarkToExcel2(wb, content);
} catch (Exception e) {
e.printStackTrace();
}
}
4.写在工具类中 原来的工具类是使用的 Workbook类,所以先使用流转成XSSFWorkbook,然后写出。
工具类中本次使用的代码
if (ArrayUtils.isNotEmpty(content) && content[0] != null) {
//让列宽随着导出的列长自动适应
// this.autoColumn();
// 添加背景图片水印
wb.write(bos);
byte[] barray = bos.toByteArray();
is = new ByteArrayInputStream(barray);
XSSFWorkbook ww = new XSSFWorkbook(is);
this.waterRemarkToExcel(ww, sheet, content[0]);
out = new FileOutputStream(getAbsoluteFile(fullfolderName));
ww.write(out);
return Result.succeed();
}
方法代码
/**
* 对list数据源将其里面的数据导入到excel表单
*
* @param content 添加水印和加密不支持修改 (有值 是,并且是水印内容 没有值不添加,不加密)
* @return 结果
*/
public Result exportExcel(String... content) {
OutputStream out = null;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
InputStream is = null;
try {
// 取出一共有多少个sheet.
double sheetNo = Math.ceil(list.size() / sheetSize);
for (int index = 0; index <= sheetNo; index++) {
createSheet(sheetNo, index);
// 产生一行
Row row = sheet.createRow(0);
int column = 0;
// 写入各个字段的列头名称
for (Object[] os : fields) {
Excel excel = (Excel) os[1];
this.createCell(excel, row, column++);
}
if (Type.EXPORT.equals(type)) {
fillExcelData(index, row);
addStatisticsRow();
}
}
// 加水印的excel不支持修改,所以要保证列宽自动调整展示全部内容
if (ArrayUtils.isNotEmpty(content) && content[0] != null) {
//让列宽随着导出的列长自动适应
// this.autoColumn();
// 添加背景图片水印
wb.write(bos);
byte[] barray = bos.toByteArray();
is = new ByteArrayInputStream(barray);
XSSFWorkbook ww = new XSSFWorkbook(is);
this.waterRemarkToExcel(ww, sheet, content[0]);
out = new FileOutputStream(getAbsoluteFile(fullfolderName));
ww.write(out);
return Result.succeed();
}
out = new FileOutputStream(getAbsoluteFile(fullfolderName));
wb.write(out);
return Result.succeed();
} catch (Exception e) {
log.error("导出Excel异常{}", e.getMessage());
throw new CustomException("导出Excel失败,请联系网站管理员!");
} finally {
if (wb != null) {
try {
wb.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
if (out != null) {
try {
out.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
if (is != null) {
try {
is.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
try {
bos.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
展示
继续优化,图片不做单独处理,直接传输
涉及方法:
/**
* 创建水印图片
*
* @param content 水印
* @return
* @throws IOException
*/
private BufferedImage createWaterMark(String content) {
//生成水印图片的宽度
Integer width = 700;
//水印图片的高度
Integer height = 300;
// 获取bufferedImage对象
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
String fontType = "宋体";
Integer fontStyle = java.awt.Font.PLAIN;
Integer fontSize = 30;
java.awt.Font font = new java.awt.Font(fontType, fontStyle, fontSize);
// 获取Graphics2d对象
Graphics2D g2d = image.createGraphics();
image = g2d.getDeviceConfiguration().createCompatibleImage(width, height, Transparency.TRANSLUCENT);
g2d.dispose();
g2d = image.createGraphics();
g2d.setColor(new java.awt.Color(0, 0, 0, 95)); //设置字体颜色和透明度
// 设置字体
g2d.setStroke(new BasicStroke(1));
// 设置字体类型 加粗 大小
g2d.setFont(font);
//设置倾斜度
g2d.rotate(Math.toRadians(-10), (double) image.getWidth() / 2, (double) image.getHeight() / 2);
FontRenderContext context = g2d.getFontRenderContext();
Rectangle2D bounds = font.getStringBounds(content, context);
double x = (width - bounds.getWidth()) / 2;
double y = (height - bounds.getHeight()) / 2;
double ascent = -bounds.getY();
double baseY = y + ascent;
// 写入水印文字原定高度过小,所以累计写水印,增加高度
g2d.drawString(content, (int) x, (int) baseY);
// 设置透明度
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
// 释放对象
g2d.dispose();
return image;
}
相关方法:
/**
* 创建水印图片,并添加进excel
*
* @param content 水印
* @param wb 文件
*/
private void waterRemarkToExcel(XSSFWorkbook wb, String content) throws Exception {
//创建水印图片
BufferedImage waterMark = createWaterMark(content);
//添加背景水印
this.setWaterMarkToExcel(wb, waterMark);
}
生成的图片直接用于excel的背景图,不用在写入写出图片
遇到的问题:
问题描述:本地测试正常,部署到开发环境后,生成的背景图上的中文乱码
问题原因:经过查询,是因为linux上的中文是需要下载安装的,现在linux上并没有支持中文
指令查询:fc-list
支持的中文字体:fc-list :lang=zh
支持的英文字体:fc-list :lang=en
问题:excel有一键删除水印的功能
一键删除
所以想着锁定水印不支持修改。
经过资料查询没有找到先关的介绍,而且excel就不支持对水印的锁定。退而求其次,对部分不包含数据单元格加密(数据支持修改,支持复制)。在excel上进行操作,发现是可以达到效果的,现在使用java实现,代码如下:
private void localRow(int rowCount) {
CellStyle lockStyle = wb.createCellStyle();
lockStyle.setLocked(true);
CellStyle unlockStyle = wb.createCellStyle();
unlockStyle.setLocked(false);//设置未锁定
for (int i = 0; i < rowCount + 8; i++) {
Row row = sheet.createRow(i);
for (int j = 0; j < 4; j++) {
Cell cell = row.createCell(j);
cell.setCellStyle(unlockStyle);//默认是锁定状态;将所有单元格设置为:未锁定;然后再对需要上锁的单元格单独锁定
//这里可以根据需要进行判断;我这就将第4列上锁了
if (i > rowCount) {
cell.setCellStyle(lockStyle);//将需要上锁的单元格进行锁定
cell.setCellValue("上锁了");
}
}
}
//sheet添加保护,这个一定要否则光锁定还是可以编辑的
sheet.protectSheet("123456");
}
rowCount是填充数据行的最大值
int rowCount = sheet.getLastRowNum();
简单实现如图:
上锁的区域不支持修改
融合进项目中代码: 对部分cell进行设置不加锁
CellStyle unlockStyle = wb.createCellStyle();
unlockStyle.setLocked(false);
cell.setCellStyle(unlockStyle);
// 加密
sheet.protectSheet("123456");
注意:一个CellStyle可以进行多种set,一个cell只能进行一次setCellStyle。
效果:部分(没有数据的表格)加密
对背景的操作:
不可删除背景。