Spring Boot Apache POI 中,用于调整图片尺寸 XSSFPicture.resize()的用法讲解

示例代码:

            // 插入签名图片,固定在签名图片行的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 内部可能:

  1. 试图基于当前锚点的 dx2/dy2(均为 0)进行缩放,结果仍是 0,因此图片尺寸为 0,不可见。

  2. 或者,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) 等工具方法进行转换。

参数类型含义
dx1int左上角相对于起始单元格左边框的 X 偏移量(EMU)。
dy1int左上角相对于起始单元格上边框的 Y 偏移量(EMU)。
dx2int右下角相对于结束单元格左边框的 X 偏移量(EMU)。
dy2int右下角相对于结束单元格上边框的 Y 偏移量(EMU)。
col1int起始单元格的列索引(从 0 开始)。
row1int起始单元格的行索引(从 0 开始)。
col2int结束单元格的列索引(从 0 开始)。
row2int结束单元格的行索引(从 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的应用效果:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值