示例代码:
// 插入签名图片,固定在签名图片行的C列插入签名图片
if (signatureImageRow != null) {
// 获取制表人签名图片(bmp格式)
byte[] signatureImage = userService.getSignatureImage(PublicUtils.getUserName());
if (signatureImage != null && signatureImage.length > 0) {
try {
int pictureType = XSSFWorkbook.PICTURE_TYPE_DIB; // BMP 对应 PICTURE_TYPE_DIB
int pictureIdx = workbook.addPicture(signatureImage, pictureType);
XSSFDrawing drawing = (XSSFDrawing) sheet.createDrawingPatriarch();
// 锚定到C列(索引2),跨越一个单元格(C列到D列,因为C列和D列已经合并单元格了)
XSSFClientAnchor anchor = new XSSFClientAnchor(
0, 0, 0, 0,
2, signatureImageRow.getRowNum(), // 起始列C,起始行
3, signatureImageRow.getRowNum() // 结束列D,结束行
);
anchor.setAnchorType(ClientAnchor.AnchorType.MOVE_AND_RESIZE);
XSSFPicture picture = drawing.createPicture(anchor, pictureIdx);
picture.resize(); // 图片原始尺寸,可以再追加 picture.resize(scale); 缩小或扩大图片,但必须保留 picture.resize() 这句代码,实际测试可以显示图片原始尺寸
picture.resize(0.4); // 图片原始尺寸比例的 40%,并且必须保留 picture.resize(),不然无法显示图片,实际测试可以显示图片调整尺寸
} catch (Exception e) {
log.error("插入签名图片失败", e);
}
}
}
picture.resize(); // 图片原始尺寸,可以再追加 picture.resize(scale); 缩小或扩大图片,但必须保留 picture.resize() 这句代码,实际测试可以显示图片原始尺寸
picture.resize(0.4); // 图片原始尺寸比例的 40%,并且必须保留 picture.resize(),不然无法显示图片,实际测试可以显示图片调整尺寸
经过测试确认了。但是为什么呢?
在 Apache POI 中,XSSFPicture.resize() 方法的行为设计如下:
-
无参
resize():根据图片的原始尺寸重新计算并设置锚点的dx2和dy2(即图片显示区域的宽度和高度),使图片能够以原始比例完整显示。它会忽略当前锚点的已有值,直接覆盖。 -
带参
resize(double scale):此方法通常会在当前锚点已有尺寸的基础上应用缩放因子,或者先调用无参resize()获取原始尺寸,再乘以缩放因子调整。但具体实现可能依赖于锚点的初始状态。
为什么单独调用 resize(0.4) 不显示图片?
在你创建锚点时,dx2 和 dy2 均设为 0(因为锚点构造函数中的参数 0,0,0,0 表示左上角偏移为0,右下角偏移也为0,即图片显示区域大小为 0)。当直接调用 resize(0.4) 时,POI 内部可能:
-
试图基于当前锚点的
dx2/dy2(均为 0)进行缩放,结果仍是 0,因此图片尺寸为 0,不可见。 -
或者,
resize(scale)需要锚点已有一个有效的尺寸作为基础,但初始值为 0,导致计算异常。
而先调用无参 resize() 会强制将锚点尺寸设置为图片原始大小(覆盖 dx2/dy2 为对应 EMU 值),此时锚点有了正确的尺寸。接着调用 resize(0.4) 时,POI 会基于这个有效尺寸进行缩放,最终设置新的尺寸为原始大小的 40%,图片得以正确显示。
为什么这种设计?
这种设计可能是为了保持方法调用的灵活性:无参 resize() 用于将图片还原为原始大小,而带参 resize(scale) 用于在已有尺寸基础上调整比例。如果锚点未初始化,则带参方法无法正确工作。
可靠的替代方案
既然你已验证先调用无参 resize() 再调用带参 resize(scale) 有效,可以继续使用这种方式。不过,为了更精确控制图片尺寸,推荐直接通过锚点设置固定 EMU 值(如前文所述),这样可以完全避免依赖 resize 的内部行为,代码也更清晰可控。例如:
java
// 创建锚点并直接指定目标尺寸(像素转 EMU) int targetWidthEmu = 80 * 9525; int targetHeightEmu = 60 * 9525; XSSFClientAnchor anchor = new XSSFClientAnchor(0, 0, targetWidthEmu, targetHeightEmu, 2, rowNum, 2, rowNum); anchor.setAnchorType(ClientAnchor.AnchorType.MOVE_AND_RESIZE); XSSFPicture picture = drawing.createPicture(anchor, pictureIdx); // 不调用任何 resize()
这种方法不受 resize 内部逻辑干扰,图片会严格按照指定尺寸显示。
XSSFClientAnchor 是 Apache POI 中用于在 Excel 工作表(XSSF 格式)中定位图片、图表等绘图对象的锚点类。它定义了对象的起始和结束单元格,以及相对于这些单元格的偏移量,从而精确控制对象的位置和大小。
构造函数签名
java
XSSFClientAnchor(int dx1, int dy1, int dx2, int dy2, int col1, int row1, int col2, int row2)
参数详解
所有坐标和偏移量的单位均为 EMU(English Metric Unit,英制公制单位),1 EMU = 1/9525 英寸,或约为 1/360000 厘米。在 96 DPI 的屏幕上,1 像素 ≈ 9525 EMU。POI 提供了 Units.pixelToEMU(int pixels) 和 Units.toEMU(double points) 等工具方法进行转换。
| 参数 | 类型 | 含义 |
|---|---|---|
dx1 | int | 左上角相对于起始单元格左边框的 X 偏移量(EMU)。 |
dy1 | int | 左上角相对于起始单元格上边框的 Y 偏移量(EMU)。 |
dx2 | int | 右下角相对于结束单元格左边框的 X 偏移量(EMU)。 |
dy2 | int | 右下角相对于结束单元格上边框的 Y 偏移量(EMU)。 |
col1 | int | 起始单元格的列索引(从 0 开始)。 |
row1 | int | 起始单元格的行索引(从 0 开始)。 |
col2 | int | 结束单元格的列索引(从 0 开始)。 |
row2 | int | 结束单元格的行索引(从 0 开始)。 |
工作原理
图片的左上角位于单元格 (col1, row1) 的左上角,再向右偏移 dx1、向下偏移 dy1 的位置。
图片的右下角位于单元格 (col2, row2) 的左上角,再向右偏移 dx2、向下偏移 dy2 的位置。
因此,图片的实际宽度 = (col2 - col1) 列的宽度之和 + (dx2 - dx1),实际高度 = (row2 - row1) 行的高度之和 + (dy2 - dy1)。
常见用法示例
1. 将图片放入单个单元格(不跨列行)
希望图片完全位于 B2 单元格(列索引 1,行索引 1)内,并设置图片宽 100 像素、高 80 像素:
java
int col = 1, row = 1;
int widthPx = 100, heightPx = 80;
int widthEmu = Units.pixelToEMU(widthPx);
int heightEmu = Units.pixelToEMU(heightPx);
XSSFClientAnchor anchor = new XSSFClientAnchor(
0, 0, widthEmu, heightEmu, // 左上角无偏移,右下角偏移等于图片尺寸
col, row, col, row // 起始和结束是同一个单元格
);
2. 将图片放入合并的单元格(如 C 和 D 列合并)
假设 C 列索引 2,D 列索引 3,合并后单元格占据两列。如果希望图片居中显示在合并区域内,可以设置起始列和结束列覆盖合并范围,并适当调整偏移:
java
int startCol = 2, endCol = 3; // 跨越两列
int startRow = 2, endRow = 2; // 单行
int widthEmu = Units.pixelToEMU(150);
int heightEmu = Units.pixelToEMU(100);
// 将图片放在合并单元格的左上角,尺寸固定
XSSFClientAnchor anchor = new XSSFClientAnchor(
0, 0, widthEmu, heightEmu,
startCol, startRow, endCol, endRow
);
3. 设置图片随单元格移动和缩放
通过 anchor.setAnchorType(ClientAnchor.AnchorType.MOVE_AND_RESIZE) 可以使图片在单元格移动或调整大小时自动适应。其他可选类型:
-
MOVE_ONLY:只随单元格移动,不缩放。 -
DONT_MOVE_AND_RESIZE:既不移动也不缩放。
注意事项
-
偏移量
dx1, dy1, dx2, dy2必须是相对单元格的左上角计算,而不是单元格内部的其他位置。 -
如果希望图片完全填充单元格(不保持比例),可以将
dx2和dy2设为对应单元格的宽高(可通过sheet.getColumnWidthInPixels(col)和row.getHeightInPoints()计算),但这样可能导致图片变形。 -
使用
picture.resize()会自动调整锚点尺寸以匹配图片原始比例,但会覆盖你手动设置的dx2, dy2。如果希望精确控制尺寸,建议不要调用resize(),而是直接通过锚点指定尺寸。 -
EMU 转换:
Units.pixelToEMU(pixels)将像素转换为 EMU,Units.EMUToPixel(emu)反之。POI 4.0+ 提供这些方法。
结合 picture.resize() 的注意事项
如果你先创建了锚点(dx2,dy2 可能为 0),然后调用 picture.resize(),它会将锚点尺寸设置为图片原始尺寸(覆盖 dx2,dy2)。如果之后再调用 picture.resize(scale),会在当前锚点尺寸基础上缩放。这就是为什么你的代码中需要先 resize() 再 resize(0.4) 才能生效:第一次设置原始尺寸,第二次缩放。
理解这些参数后,你可以灵活控制图片在工作表中的位置和大小。
要实现图片与单元格边框保持一定间隔,可以通过设置锚点的 dx1 和 dy1 参数(左上角偏移量)来实现。这两个参数的单位是 EMU(1 像素 ≈ 9525 EMU),因此需要将希望的间隔像素值转换为 EMU。
修改后的代码
在创建 XSSFClientAnchor 时,为左上角指定偏移量,并相应调整右下角偏移量,使图片尺寸不变但整体平移。
java
int startCol = 2; // C列
int startRow = signatureImageRow.getRowNum();
int endCol = 2; // 仍为C列
int endRow = signatureImageRow.getRowNum();
int pictureWidthPx = 80;
int pictureHeightPx = 60;
// 设置图片距离单元格左边框和上边框的间隔(单位:像素)
int offsetXPx = 10; // 水平偏移10像素
int offsetYPx = 5; // 垂直偏移5像素
// 转换为EMU
int offsetXEmu = Units.pixelToEMU(offsetXPx);
int offsetYEmu = Units.pixelToEMU(offsetYPx);
int pictureWidthEmu = Units.pixelToEMU(pictureWidthPx);
int pictureHeightEmu = Units.pixelToEMU(pictureHeightPx);
// 创建锚点
XSSFClientAnchor anchor = new XSSFClientAnchor(
offsetXEmu, offsetYEmu, // 左上角偏移
offsetXEmu + pictureWidthEmu, offsetYEmu + pictureHeightEmu, // 右下角偏移 = 左上角偏移 + 图片尺寸
startCol, startRow, endCol, endRow
);
anchor.setAnchorType(ClientAnchor.AnchorType.MOVE_AND_RESIZE);
drawing.createPicture(anchor, pictureIdx);
原理说明
-
左上角坐标:
(dx1, dy1)指定图片左上角相对于起始单元格左上角的偏移量。 -
右下角坐标:
(dx2, dy2)指定图片右下角相对于结束单元格左上角的偏移量。 -
由于起始和结束是同一个单元格,图片的宽度 =
dx2 - dx1,高度 =dy2 - dy1。 -
因此,设置
dx1和dy1为正数即可产生间隔,同时保持图片宽高不变,需将dx2和dy2分别设置为dx1 + widthEmu和dy1 + heightEmu。
注意事项
-
Units.pixelToEMU()是 Apache POI 提供的工具方法,需导入org.apache.poi.util.Units。 -
如果希望图片居中显示在单元格内,可以结合单元格宽度计算偏移量,但简单场景下直接指定固定偏移即可。
-
偏移量不宜过大,以免图片超出单元格边界。若单元格较小,可适当减小图片尺寸或偏移量。
按照以上修改,图片就会与单元格边框保持设定的间隔,不会紧贴边框。
最终优化的代码:
// 插入签名图片,固定在签名图片行的C列插入签名图片
if (signatureImageRow != null) {
// 获取制表人签名图片(bmp格式)
byte[] signatureImage = userService.getSignatureImage(PublicUtils.getUserName());
if (signatureImage != null && signatureImage.length > 0) {
try {
int pictureType = XSSFWorkbook.PICTURE_TYPE_DIB; // BMP 对应 PICTURE_TYPE_DIB
int pictureIdx = workbook.addPicture(signatureImage, pictureType);
XSSFDrawing drawing = (XSSFDrawing) sheet.createDrawingPatriarch();
// 方法1:不指定图片尺寸,后面通过 resize() + resize(scale) 按照比例调整图片尺寸
/*
int startCol = 2; // 起始列:C列(索引2)
int startRow = signatureImageRow.getRowNum(); // 起始行
int endCol = 2; // 结束列:C列(索引2)
int endRow = signatureImageRow.getRowNum(); // 结束行
// 创建锚点
XSSFClientAnchor anchor = new XSSFClientAnchor(
0, 0, 0, 0, // 左上角无偏移(dx1和dy1为0),右下角无偏移(dx2和dy2为0,即不指定图片尺寸)
startCol, startRow, endCol, endRow // 起始和结束是同一个单元格
);
anchor.setAnchorType(ClientAnchor.AnchorType.MOVE_AND_RESIZE);
XSSFPicture picture = drawing.createPicture(anchor, pictureIdx);
picture.resize(); // 图片原始尺寸,可以再追加 picture.resize(scale); 缩小或扩大图片,但必须保留 picture.resize() 这句代码,实际测试可以显示图片原始尺寸
picture.resize(0.4); // 图片原始尺寸比例的 40%,并且必须保留 picture.resize(),不然无法显示图片,实际测试可以显示图片调整尺寸
*/
// 方法2:指定图片尺寸
// EMU(English Metric Unit,英制公制单位),1 EMU = 1/9525 英寸,或约为 1/360000 厘米。在 96 DPI 的屏幕上,1 像素 ≈ 9525 EMU。
// POI 提供了 Units.pixelToEMU(int pixels) 和 Units.toEMU(double points) 等工具方法进行转换。
int startCol = 2; // 起始列:C列(索引2)
int startRow = signatureImageRow.getRowNum(); // 起始行
int endCol = 2; // 结束列:C列(索引2)
int endRow = signatureImageRow.getRowNum(); // 结束行
// 设置图片宽度和高度(单位:像素)
int pictureWidthPx = 80; // 图片宽度
int pictureHeightPx = 60; // 图片高度
// 像素转EMU,1 像素 ≈ 9525 EMU
int pictureWidthEmu = Units.pixelToEMU(pictureWidthPx);
int pictureHeightEmu = Units.pixelToEMU(pictureHeightPx);
// 设置图片距离单元格左边框和上边框的间隔(单位:像素)
int offsetXPx = 2; // 水平偏移
int offsetYPx = 2; // 垂直偏移
// 像素转EMU,1 像素 ≈ 9525 EMU
int offsetXEmu = Units.pixelToEMU(offsetXPx);
int offsetYEmu = Units.pixelToEMU(offsetYPx);
// 创建锚点
XSSFClientAnchor anchor = new XSSFClientAnchor(
offsetXEmu, offsetYEmu, // 左上角偏移
offsetXEmu + pictureWidthEmu, offsetYEmu + pictureHeightEmu, // 右下角偏移 = 左上角偏移 + 图片尺寸
startCol, startRow, endCol, endRow // 起始和结束是同一个单元格
);
anchor.setAnchorType(ClientAnchor.AnchorType.MOVE_AND_RESIZE);
drawing.createPicture(anchor, pictureIdx);
} catch (Exception e) {
log.error("插入签名图片失败", e);
}
}
}
导出excel的应用效果:


2083

被折叠的 条评论
为什么被折叠?



