easyExcel - 带图片导出

目录


前言

Java-easyExcel入门教程:https://blog.csdn.net/xhmico/article/details/134714025

之前有介绍过如何使用 easyExcel,以及写了两个入门的 demo ,这两个 demo 能应付在开发中大多数的导入和导出需求,不过有时候面对一些复杂的表格,就会有点不够用,该篇讲述的是如何实现带图片导出


一、情景介绍

在实际的开发过程中可能会遇到需要带图片导出的表格,比如以下案例:

如果有多张图片要放在一个单元格中,并且单元格随着图片数量自动扩宽


二、问题分析

关于如何实现带图片导出的功能,在官方文档中有一个简单的说明:

官方文档:图片导出

从官方文档中给的代码示例中可以看出,带图片导出有 6 种方式

java 复制代码
@Getter
@Setter
@EqualsAndHashCode
@ContentRowHeight(100)
@ColumnWidth(100 / 8)
public class ImageDemoData {
    private File file;
    private InputStream inputStream;
    /**
     * 如果string类型 必须指定转换器,string默认转换成string
     */
    @ExcelProperty(converter = StringImageConverter.class)
    private String string;
    private byte[] byteArray;
    /**
     * 根据url导出
     *
     * @since 2.1.1
     */
    private URL url;

    /**
     * 根据文件导出 并设置导出的位置。
     *
     * @since 3.0.0-beta1
     */
    private WriteCellData<Void> writeCellDataFile;
}

我在 D://picture 下存放了一张图片 1.png

D:\\excel-files 下创建了一个 excel 文件 demo01.xlsx

拷贝了下官方给的代码示例,改一改:

java 复制代码
    /**
     * 带图片导出:官方案例
     */
    @Test
    public void exportWithPicture01() throws Exception {
        // 输出文件路径
        String fileName = "D:\\excel-files\\demo01.xlsx";

        // 这里注意下 所有的图片都会放到内存 暂时没有很好的解法,大量图片的情况下建议 2选1:
        // 1. 将图片上传到oss 或者其他存储网站: https://www.aliyun.com/product/oss ,然后直接放链接
        // 2. 使用: https://github.com/coobird/thumbnailator 或者其他工具压缩图片

        String imagePath = "D:\\picture\\1.png";
        try (InputStream inputStream = FileUtils.openInputStream(new File(imagePath))) {
            List<ImageDemoData> list = ListUtils.newArrayList();
            ImageDemoData imageDemoData = new ImageDemoData();
            list.add(imageDemoData);
            // 放入五种类型的图片 实际使用只要选一种即可
            imageDemoData.setByteArray(FileUtils.readFileToByteArray(new File(imagePath)));
            imageDemoData.setFile(new File(imagePath));
            imageDemoData.setString(imagePath);
            imageDemoData.setInputStream(inputStream);
            imageDemoData.setUrl(new URL(
                    "https://img-blog.csdnimg.cn/direct/c11088e1790049a5b84a0fda21a271b1.png"));

            // 这里演示
            // 需要额外放入文字
            // 而且需要放入2个图片
            // 第一个图片靠左
            // 第二个靠右 而且要额外的占用他后面的单元格
            WriteCellData<Void> writeCellData = new WriteCellData<>();
            imageDemoData.setWriteCellDataFile(writeCellData);
            // 这里可以设置为 EMPTY 则代表不需要其他数据了
            writeCellData.setType(CellDataTypeEnum.EMPTY);
            writeCellData.setStringValue("额外的放一些文字");

            // 可以放入多个图片
            List<ImageData> imageDataList = new ArrayList<>();
            ImageData imageData = new ImageData();
            imageDataList.add(imageData);
            writeCellData.setImageDataList(imageDataList);
            // 放入2进制图片
            imageData.setImage(FileUtils.readFileToByteArray(new File(imagePath)));
            // 图片类型
            imageData.setImageType(ImageData.ImageType.PICTURE_TYPE_PNG);
            // 上 右 下 左 需要留空
            // 这个类似于 css 的 margin
            // 这里实测 不能设置太大 超过单元格原始大小后 打开会提示修复。暂时未找到很好的解法。
            imageData.setTop(5);
            imageData.setRight(40);
            imageData.setBottom(5);
            imageData.setLeft(5);

            // 放入第二个图片
            imageData = new ImageData();
            imageDataList.add(imageData);
            writeCellData.setImageDataList(imageDataList);
            imageData.setImage(FileUtils.readFileToByteArray(new File(imagePath)));
            imageData.setImageType(ImageData.ImageType.PICTURE_TYPE_PNG);
            imageData.setTop(5);
            imageData.setRight(5);
            imageData.setBottom(5);
            imageData.setLeft(50);
            // 设置图片的位置 假设 现在目标 是 覆盖 当前单元格 和当前单元格右边的单元格
            // 起点相对于当前单元格为0 当然可以不写
            imageData.setRelativeFirstRowIndex(0);
            imageData.setRelativeFirstColumnIndex(0);
            imageData.setRelativeLastRowIndex(0);
            // 前面3个可以不写  下面这个需要写 也就是 结尾 需要相对当前单元格 往右移动一格
            // 也就是说 这个图片会覆盖当前单元格和 后面的那一格
            imageData.setRelativeLastColumnIndex(1);

            // 写入数据
            EasyExcel.write(fileName, ImageDemoData.class).sheet().doWrite(list);
        }
    }

导出结果:


三、代码实现


1. 单图片导出

如果每个单元格只需要存放一张图片,使用官方给的方案就绰绰有余了,通常情况下使用 URL 的方式会比较多,例如:

输出对象类:UserInfoEntity.java

java 复制代码
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.*;
import com.alibaba.excel.enums.poi.BorderStyleEnum;
import com.alibaba.excel.enums.poi.FillPatternTypeEnum;
import com.alibaba.excel.enums.poi.HorizontalAlignmentEnum;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.net.URL;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
// 头背景设置
@HeadStyle(fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND, horizontalAlignment = HorizontalAlignmentEnum.CENTER, borderLeft = BorderStyleEnum.THIN, borderTop = BorderStyleEnum.THIN, borderRight = BorderStyleEnum.THIN, borderBottom = BorderStyleEnum.THIN)
//标题高度
@HeadRowHeight(20)
//内容高度
@ContentRowHeight(40)
//内容居中,左、上、右、下的边框显示
@ContentStyle(horizontalAlignment = HorizontalAlignmentEnum.CENTER, borderLeft = BorderStyleEnum.THIN, borderTop = BorderStyleEnum.THIN, borderRight = BorderStyleEnum.THIN, borderBottom = BorderStyleEnum.THIN)
public class UserInfoEntity {

    @ExcelProperty(value = "名称")
    @ColumnWidth(10)
    private String name;

    @ExcelProperty(value = "照片")
    @ColumnWidth(10)
    private URL image;

    @ExcelProperty(value = "年龄")
    @ColumnWidth(10)
    private Integer age;

}

代码示例:

java 复制代码
    /**
     * 带图片导出:单元格只需要存放单张图片
     */
    @Test
    public void exportWithPicture02() {

        // 输出文件路径
        String fileName = "D:\\excel-files\\demo01.xlsx";

        try {

            // 需要导出的数据
            List<UserInfoEntity> data = new ArrayList<>();
            data.add(UserInfoEntity.builder()
                    .name("米大傻")
                    .image(new URL("https://img-blog.csdnimg.cn/direct/c11088e1790049a5b84a0fda21a271b1.png"))
                    .age(18)
                    .build()
            );
            data.add(UserInfoEntity.builder()
                    .name("曹大力")
                    .image(new URL("https://img-blog.csdnimg.cn/direct/bef2fdeffa644fb4aa6231d485ddaaac.png"))
                    .age(17)
                    .build()
            );
            data.add(UserInfoEntity.builder()
                    .name("张大仙")
                    .image(new URL("https://img-blog.csdnimg.cn/direct/e264c110314d4ec49a7c79c51732f5f7.png"))
                    .age(18)
                    .build()
            );

            // 写入数据
            EasyExcel.write(fileName, UserInfoEntity.class).sheet().doWrite(data);

        } catch (Exception e) {
            System.out.println("导出异常");
        }
    }

导出结果


2. 多图片导出

但是如果要实现情景介绍案例中每个单元格需要存放多张图片就不能仅使用官方提供的方案去解决了,通常情况下需要自己写一个拦截器,对单元格中的图片进行处理

这里我借鉴了 木木子薇夏:EasyExcel导出多张图片(URL图片)的数据(图片放到一个单元格) 的实现方式,但做了以下几个优化:

  • ① 图片宽度可自设置,单位为 px
  • ② 添加像素转换因子,默认为 32 ,如果导入的图片超出或未占满表格,可调整该参数
  • ③ 解决图片遮挡单元格的上边框和右边框的问题

转换器:ImageUrlConverter.java

java 复制代码
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ImageData;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import com.alibaba.excel.util.IoUtils;
import com.alibaba.excel.util.ListUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;
import java.io.InputStream;
import java.net.URL;
import java.util.List;

@Slf4j
public class ImageUrlConverter implements Converter<List<URL>> {

    @Override
    public Class<?> supportJavaTypeKey() {
        return List.class;
    }

    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        return CellDataTypeEnum.EMPTY;
    }

    @Override
    public List<URL> convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
        return null;
    }

    @Override
    public WriteCellData<?> convertToExcelData(List<URL> value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
        // 这里进行对数据实体类URL集合处理
        List<ImageData> data = ListUtils.newArrayList();
        ImageData imageData;
        // for 循环一次读取
        for (URL url : value) {
            try (InputStream inputStream = url.openStream();) {
                byte[] bytes = IoUtils.toByteArray(inputStream);
                imageData = new ImageData();
                imageData.setImage(bytes);
                data.add(imageData);
            } catch (Exception e) {
                log.error("导出临时记录图片异常:", e);
            }
        }
        WriteCellData<?> cellData = new WriteCellData<>();
        if (!CollectionUtils.isEmpty(data)) {
            // 图片返回图片列表
            cellData.setImageDataList(data);
            cellData.setType(CellDataTypeEnum.EMPTY);
        } else {
            // 没有图片使用汉字表示
            cellData.setStringValue("无图");
            cellData.setType(CellDataTypeEnum.STRING);
        }
        return cellData;
    }
}

单元格拦截器:CustomImageModifyStrategy.java

java 复制代码
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.metadata.data.ImageData;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.write.handler.CellWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteTableHolder;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.util.Units;
import org.apache.poi.xssf.usermodel.XSSFDrawing;
import org.apache.poi.xssf.usermodel.XSSFPicture;
import org.apache.poi.xssf.usermodel.XSSFShape;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Excel导出单元格中有图片,图片会进行压缩,缩略,插入单元格
 * 注意:
 *      - 该策略是复制原表格中的图片进行缩放,原图片并没有删除掉,而是将尺寸设置为 0 看不到而已,但是依旧占用空间
 *      - 且在多 sheet 的环境下,除第一个 sheet,其余的 sheet 图片会被置 0
 * 目前上述问题并没有得到解决,如果在导出数据较多或者存在多个 sheet 的情况下不建议使用
 */
public class CustomImageModifyStrategy implements CellWriteHandler {

    /**
     * 已经处理的Cell
     */
    private final CopyOnWriteArrayList<String> REPEATS = new CopyOnWriteArrayList<>();

    /**
     * 单元格的图片最大张数(每列的单元格图片张数不确定,单元格宽度需按照张数最多的长度来设置)
     */
    private final AtomicReference<Integer> MAX_IMAGE_SIZE = new AtomicReference<>(0);

    /**
     * 标记手动添加的图片,用于排除EasyExcel自动添加的图片
     */
    private final CopyOnWriteArrayList<Integer> CREATE_PIC_INDEX = new CopyOnWriteArrayList<>();

    /**
     * 默认图片宽度(单位像素):60
     */
    private final static int DEFAULT_IMAGE_WIDTH = 60;

    /**
     * 默认像素转换因子:32
     */
    private final static int DEFAULT_PIXEL_CONVERSION_FACTOR = 32;

    /**
     * 图片宽度,单位像素
     */
    private final int imageWidth;

    /**
     * 像素转换因子
     */
    private final int pixelConversionFactor;

    public CustomImageModifyStrategy() {
        this.imageWidth = DEFAULT_IMAGE_WIDTH;
        this.pixelConversionFactor = DEFAULT_PIXEL_CONVERSION_FACTOR;
    }

    public CustomImageModifyStrategy(int imageWidth, int pixelConversionFactor) {
        this.imageWidth = imageWidth;
        this.pixelConversionFactor = pixelConversionFactor;
    }

    @Override
    public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead) {

    }

    @Override
    public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {

    }

    @Override
    public void afterCellDataConverted(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, WriteCellData<?> cellData, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
        //  在数据转换成功后 不是头就把类型设置成空
        if (isHead) {
            return;
        }
        //将要插入图片的单元格的type设置为空,下面再填充图片
        if (!CollectionUtils.isEmpty(cellData.getImageDataList())) {
            cellData.setType(CellDataTypeEnum.EMPTY);
        }
    }

    @Override
    public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
        //  在 单元格写入完毕后 ,自己填充图片
        if (isHead || CollectionUtils.isEmpty(cellDataList)) {
            return;
        }
        boolean listFlag = false;
        Sheet sheet = cell.getSheet();
        List<ImageData> imageDataList = cellDataList.get(0).getImageDataList();
        if (!CollectionUtils.isEmpty(imageDataList)) {
            listFlag = true;
        }
        if (!listFlag && imageDataList == null) {
            return;
        }
        String key = cell.getRowIndex() + "_" + cell.getColumnIndex();
        if (REPEATS.contains(key)) {
            return;
        }
        REPEATS.add(key);
        if (imageDataList.size() > MAX_IMAGE_SIZE.get()) {
            MAX_IMAGE_SIZE.set(imageDataList.size());
        }

        int widthValue =  imageWidth * pixelConversionFactor;
        sheet.setColumnWidth(cell.getColumnIndex(), listFlag ? widthValue * MAX_IMAGE_SIZE.get() + pixelConversionFactor : widthValue);

        if (listFlag) {
            for (int i = 0; i < imageDataList.size(); i++) {
                ImageData imageData = imageDataList.get(i);
                if (imageData == null) {
                    continue;
                }
                byte[] image = imageData.getImage();
                int index = this.insertImage(sheet, cell, image, i);
                CREATE_PIC_INDEX.add(index);
            }
        } else {
            this.insertImage(sheet, cell, imageDataList.get(0).getImage(), 0);
        }

        // 清除EasyExcel自动添加的没有格式的图片
        XSSFDrawing drawingPatriarch = (XSSFDrawing) sheet.getDrawingPatriarch();
        List<XSSFShape> shapes = drawingPatriarch.getShapes();
        for (int i = 0; i < shapes.size(); i++) {
            XSSFShape shape = shapes.get(i);
            if (shape instanceof XSSFPicture && !CREATE_PIC_INDEX.contains(i)) {
                CREATE_PIC_INDEX.add(i);
                XSSFPicture picture = (XSSFPicture) shape;
                // 这里只是将图片的大小设置为 0,所以表格依旧会存放该图片
                picture.resize(0);
            }
        }
    }

    /**
     * 重新插入一个图片
     *
     * @param sheet       Excel页面
     * @param cell        表格元素
     * @param pictureData 图片数据
     * @param i           图片顺序
     */
    public int insertImage(Sheet sheet, Cell cell, byte[] pictureData, int i) {
        int picWidth = Units.pixelToEMU(imageWidth);
        int index = sheet.getWorkbook().addPicture(pictureData, HSSFWorkbook.PICTURE_TYPE_PNG);
        Drawing<?> drawing = sheet.getDrawingPatriarch();
        if (drawing == null) {
            drawing = sheet.createDrawingPatriarch();
        }
        CreationHelper helper = sheet.getWorkbook().getCreationHelper();
        ClientAnchor anchor = helper.createClientAnchor();
        /*
         * 设置图片坐标
         * 为了不让图片遮挡单元格的上边框和右边框,故 x1、x2、y1 这几个坐标点均向后移动了一个像素点
         */
        anchor.setDx1(Units.pixelToEMU(1) + picWidth * i);
        anchor.setDx2(Units.pixelToEMU(1) + picWidth + picWidth * i);
        anchor.setDy1(Units.pixelToEMU(1));
        anchor.setDy2(0);
        //设置图片位置
        int columnIndex = cell.getColumnIndex();
        anchor.setCol1(columnIndex);
        anchor.setCol2(columnIndex);
        int rowIndex = cell.getRowIndex();
        anchor.setRow1(rowIndex);
        anchor.setRow2(rowIndex + 1);
        anchor.setAnchorType(ClientAnchor.AnchorType.DONT_MOVE_AND_RESIZE);
        drawing.createPicture(anchor, index);
        return index;
    }

}

输出对象类:StaffInfoEntity.java

java 复制代码
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.*;
import com.alibaba.excel.enums.poi.BorderStyleEnum;
import com.alibaba.excel.enums.poi.FillPatternTypeEnum;
import com.alibaba.excel.enums.poi.HorizontalAlignmentEnum;
import com.mike.common.core.reactor.excel.converter.DownloadUrlConverter;
import com.mike.common.core.reactor.excel.converter.ImageUrlConverter;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.net.URL;
import java.util.List;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
// 头背景设置
@HeadStyle(fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND, horizontalAlignment = HorizontalAlignmentEnum.CENTER, borderLeft = BorderStyleEnum.THIN, borderTop = BorderStyleEnum.THIN, borderRight = BorderStyleEnum.THIN, borderBottom = BorderStyleEnum.THIN)
//标题高度
@HeadRowHeight(20)
//内容高度
@ContentRowHeight(40)
//内容居中,左、上、右、下的边框显示
@ContentStyle(horizontalAlignment = HorizontalAlignmentEnum.CENTER, borderLeft = BorderStyleEnum.THIN, borderTop = BorderStyleEnum.THIN, borderRight = BorderStyleEnum.THIN, borderBottom = BorderStyleEnum.THIN)
public class StaffInfoEntity {

    @ApiModelProperty(value = "名称")
    @ExcelProperty(value = "名称")
    @ColumnWidth(10)
    private String name;

    @ApiModelProperty(value = "照片")
    @ExcelProperty(value = "照片", converter = ImageUrlConverter.class)
    @ColumnWidth(15)
    private List<URL> imgList;

    @ApiModelProperty(value = "年龄")
    @ExcelProperty(value = "年龄")
    @ColumnWidth(10)
    private Integer age;

}

代码示例:

java 复制代码
    /**
     * 带图片导出:多图片导出
     */
    @Test
    public void exportWithPicture03() {

        // 输出文件路径
        String fileName = "D:\\excel-files\\demo02.xlsx";

        try {
            List<URL> imgList1 = new ArrayList<>();
            imgList1.add(new URL(
                    "https://img-blog.csdnimg.cn/direct/c11088e1790049a5b84a0fda21a271b1.png"));
            imgList1.add(new URL(
                    "https://img-blog.csdnimg.cn/direct/bef2fdeffa644fb4aa6231d485ddaaac.png"));

            List<URL> imgList2 = new ArrayList<>();
            imgList2.add(new URL(
                    "https://img-blog.csdnimg.cn/direct/e264c110314d4ec49a7c79c51732f5f7.png"));

            List<StaffInfoEntity> entityList = new ArrayList<>();
            entityList.add(StaffInfoEntity.builder()
                    .name("米大傻")
                    .imgList(imgList1)
                    .age(18)
                    .build());
            entityList.add(StaffInfoEntity.builder()
                    .name("曹大力")
                    .imgList(imgList2)
                    .age(17)
                    .build());
            entityList.add(StaffInfoEntity.builder()
                    .name("张大大")
                    .age(18)
                    .build());

            // 图片列最大图片数
            AtomicReference<Integer> maxImageSize = new AtomicReference<>(0);
            entityList.forEach(item -> {
                // 最大图片数大小
                if (!CollectionUtils.isEmpty(item.getImgList()) && item.getImgList().size() > maxImageSize.get()) {
                    maxImageSize.set(item.getImgList().size());
                }
            });
            // 导出数据
            EasyExcel.write(fileName, StaffInfoEntity.class)
                    .autoCloseStream(true)
                    // 使用图片处理策略
                    .registerWriteHandler(
                            // 设置每张图片的宽度为 60px,转换因子为 32
                            new CustomImageModifyStrategy(60, 32))
                    .sheet("sheet")
                    .doWrite(entityList);
        } catch (Exception e) {
            System.out.println("导出异常");
        }
    }

导出结果

就是情景介绍中案例的效果了

这里我简单解释以下这个像素转换因子是怎么来的,为什么是 32

Sheet 中设置单元格宽度的方法为 setColumnWidth(),而 var2 的单位并不是像素

java 复制代码
public interface Sheet extends Iterable<Row> {
	
	...
	
	/*
	 * 设置单元格宽度大小:
	 * 		var1 为单元格列的索引
	 * 		var2 为单元格的宽度
	 */
	void setColumnWidth(int var1, int var2);

	...
}

然后我就通过几组数据分析得出像素和 var2 之间有个转换关系,大概是 32 (2560/80=32)

那为什么不写死 32?因为发现在笔记本上导出的话,这个比例就不是 32 了,这个问题后续待解决,故先添加一个转换因子的参数

然后还有三个问题就是:

  • 每个单元格中实际存放的图片比所看到的图片多一倍,因为该拦截器并非是从原有的图片上进行缩放处理,而是从新复制了原有的图片进行缩放,再把原有的图片宽度设置为 0,就显得不存在了,弊端就是如果图片比较多的情况下,表格文件就会异常的大
  • 在多 sheet 下使用该策略会有问题,除第一个 sheet,其余的 sheet 图片宽度会被错误的置为 0,导致图片 消失
  • 所有的图片都是放在内存当中,图片比较大的时候容易出现内存溢出,并且导出时间会比较长

如果能把需要置 0 的图片删掉,那就挺完美的了,但是目前我没有很好的解决办法,今后如果有处理方案,我会第一时间进行改进

在数据量比较小,并且没有多个 sheet 的话,还是没啥问题的


3. 多图片导出(优化)

鉴于上述多图片导出案例所出现的三个问题,我目前能给的策略就是先让图片下载到本地,然后再写入表格,但是要及时清理磁盘中临时下载的文件

转换器:DownloadUrlConverter.java

java 复制代码
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import com.alibaba.excel.util.ListUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;
import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.util.List;

/**
 * 该类主要是将 URL 资源下载到本地磁盘,需要配合 LocalImageModifyStrategy 使用
 */
@Slf4j
public class DownloadUrlConverter implements Converter<List<URL>> {


    @Override
    public Class<?> supportJavaTypeKey() {
        return List.class;
    }

    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        return CellDataTypeEnum.EMPTY;
    }

    @Override
    public List<URL> convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
        return null;
    }

    @Override
    public WriteCellData<?> convertToExcelData(List<URL> value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
        // 这里进行对数据实体类URL集合处理
        List<String> filePaths = ListUtils.newArrayList();
        // 下载文件存放地址
        String folder = System.getProperty("java.io.tmpdir") + File.separator + "excel-temp" + File.separator;
        // for 循环一次读取
        for (URL url : value) {
            String path = url.getPath();
            String suffix = path.substring(path.indexOf("."));
            long millis = System.currentTimeMillis();
            String fileName = millis + suffix;
            String filePath = folder + fileName;
            // 下载文件到本地
            try {
                this.downloadURL(url, filePath);
                filePaths.add(filePath);
                log.info("The temporary file storage path: " + filePath);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        WriteCellData<?> cellData = new WriteCellData<>();
        if (!CollectionUtils.isEmpty(filePaths)) {
            // 图片返回图片列表
            cellData.setStringValue("Files:" + String.join(",", filePaths));
            cellData.setType(CellDataTypeEnum.EMPTY);
        } else {
            cellData.setType(CellDataTypeEnum.STRING);
        }
        return cellData;
    }

    /**
     * 从 URL 中下载文件到指定路径
     * @param url 统一资源定位符
     * @param savePath 存放路径
     */
    private void downloadURL(URL url, String savePath) throws IOException {

        URLConnection connection = url.openConnection();
        connection.connect();
        InputStream inputStream = new BufferedInputStream(connection.getInputStream());

        File file = new File(savePath);
        if (!file.getParentFile().exists()) {
            if (file.getParentFile().mkdirs()) {
                log.info("parent file had created.");
            }
        }

        OutputStream outputStream = new FileOutputStream(savePath);

        byte[] buffer = new byte[4096];
        int bytesRead;
        while ((bytesRead = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, bytesRead);
        }

        inputStream.close();
        outputStream.close();

        log.info("Image downloaded successfully!");
    }

}

拦截器:LocalImageModifyStrategy.java

java 复制代码
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.write.handler.CellWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteTableHolder;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.util.Units;
import org.springframework.util.CollectionUtils;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Excel导出单元格中有图片,图片会进行压缩,缩略,插入单元格(通过读取本地文件的方式)
 * 需要搭配 DownloadUrlConverter 使用
 */
public class LocalImageModifyStrategy implements CellWriteHandler {

    /**
     * 单元格的图片最大张数(每列的单元格图片张数不确定,单元格宽度需按照张数最多的长度来设置)
     */
    private final AtomicReference<Integer> MAX_IMAGE_SIZE = new AtomicReference<>(0);

    /**
     * 默认图片宽度(单位像素):60
     */
    private final static int DEFAULT_IMAGE_WIDTH = 60;

    /**
     * 默认像素转换因子:32
     */
    private final static int DEFAULT_PIXEL_CONVERSION_FACTOR = 32;

    /**
     * 图片宽度,单位像素
     */
    private final int imageWidth;

    /**
     * 像素转换因子
     */
    private final int pixelConversionFactor;

    public LocalImageModifyStrategy() {
        this.imageWidth = DEFAULT_IMAGE_WIDTH;
        this.pixelConversionFactor = DEFAULT_PIXEL_CONVERSION_FACTOR;
    }

    public LocalImageModifyStrategy(int imageWidth, int pixelConversionFactor) {
        this.imageWidth = imageWidth;
        this.pixelConversionFactor = pixelConversionFactor;
    }

    @Override
    public void afterCellDataConverted(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, WriteCellData<?> cellData, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
        //  在数据转换成功后 不是头就把类型设置成空
        if (isHead) {
            return;
        }
        //将要插入图片的单元格的type设置为空,下面再填充图片
        if (!CollectionUtils.isEmpty(cellData.getImageDataList())) {
            cellData.setType(CellDataTypeEnum.EMPTY);
        }
    }

    @Override
    public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
        //  在单元格写入完毕后 ,自己填充图片
        if (isHead || CollectionUtils.isEmpty(cellDataList)) {
            return;
        }
        boolean imgFlag = false;
        Sheet sheet = cell.getSheet();
        WriteCellData<?> writeCellData = cellDataList.get(0);
        CellDataTypeEnum type = writeCellData.getType();
        if (type != CellDataTypeEnum.EMPTY) {
            return;
        }
        String stringValue = writeCellData.getStringValue();
        if (stringValue == null) {
            return;
        }
        // 判断是否属于文件
        if (stringValue.startsWith("Files:")) {
            imgFlag = true;
            stringValue = stringValue.replace("Files:", "");
        }
        if (!imgFlag) {
            return;
        }
        List<String> filePaths = Arrays.asList(stringValue.split(","));

        if (filePaths.size() > MAX_IMAGE_SIZE.get()) {
            MAX_IMAGE_SIZE.set(filePaths.size());
        }

        int widthValue =  imageWidth * pixelConversionFactor;
        sheet.setColumnWidth(cell.getColumnIndex(), widthValue * MAX_IMAGE_SIZE.get() + pixelConversionFactor);

        for (int i = 0; i < filePaths.size(); i++) {
            String filePath = filePaths.get(i);
            // todo 这里可以对图片作一些处理,比如说压缩
            // ...
            // 读取文件
            byte[] image =  FileUtil.readBytes(filePath);
            this.insertImage(sheet, cell, image, i);
        }
    }


    /**
     * 重新插入一个图片
     *
     * @param sheet       Excel页面
     * @param cell        表格元素
     * @param pictureData 图片数据
     * @param i           图片顺序
     */
    public int insertImage(Sheet sheet, Cell cell, byte[] pictureData, int i) {
        int picWidth = Units.pixelToEMU(imageWidth);
        int index = sheet.getWorkbook().addPicture(pictureData, HSSFWorkbook.PICTURE_TYPE_PNG);
        Drawing<?> drawing = sheet.getDrawingPatriarch();
        if (drawing == null) {
            drawing = sheet.createDrawingPatriarch();
        }
        CreationHelper helper = sheet.getWorkbook().getCreationHelper();
        ClientAnchor anchor = helper.createClientAnchor();
        /*
         * 设置图片坐标
         * 为了不让图片遮挡单元格的上边框和右边框,故 x1、x2、y1 这几个坐标点均向后移动了一个像素点
         */
        anchor.setDx1(Units.pixelToEMU(1) + picWidth * i);
        anchor.setDx2(Units.pixelToEMU(1) + picWidth + picWidth * i);
        anchor.setDy1(Units.pixelToEMU(1));
        anchor.setDy2(0);
        //设置图片位置
        int columnIndex = cell.getColumnIndex();
        anchor.setCol1(columnIndex);
        anchor.setCol2(columnIndex);
        int rowIndex = cell.getRowIndex();
        anchor.setRow1(rowIndex);
        anchor.setRow2(rowIndex + 1);
        anchor.setAnchorType(ClientAnchor.AnchorType.DONT_MOVE_AND_RESIZE);
        drawing.createPicture(anchor, index);
        return index;
    }
}

对应的地方改一改:

导出结果:

最后得出的效果是一样的,但是导出文件的大小小了一倍,如果对图片的清晰度要求不高的话,可以在拦截器当中添加图片压缩的逻辑,得到的 excel 文件会更小

不过得定时去清除临时文件

java 复制代码
		// 下载文件存放地址
        String folder = System.getProperty("java.io.tmpdir") + File.separator + "excel-temp" + File.separator;

参考文章:

Easyexcel导出文件(多图片)(自用)https://blog.csdn.net/weixin_45564990/article/details/130636029

Easyexcel导出图片,固定单元格宽度自动高度保持图片比例:https://blog.csdn.net/AhogeK/article/details/133955861

EasyExcel导出多张图片(URL图片)的数据(图片放到一个单元格):https://blog.csdn.net/qq_36353248/article/details/135871478

相关推荐
Re.不晚8 分钟前
Java入门15——抽象类
java·开发语言·学习·算法·intellij-idea
雷神乐乐14 分钟前
Maven学习——创建Maven的Java和Web工程,并运行在Tomcat上
java·maven
码农派大星。17 分钟前
Spring Boot 配置文件
java·spring boot·后端
顾北川_野24 分钟前
Android 手机设备的OEM-unlock解锁 和 adb push文件
android·java
江深竹静,一苇以航26 分钟前
springboot3项目整合Mybatis-plus启动项目报错:Invalid bean definition with name ‘xxxMapper‘
java·spring boot
confiself42 分钟前
大模型系列——LLAMA-O1 复刻代码解读
java·开发语言
Wlq04151 小时前
J2EE平台
java·java-ee
XiaoLeisj1 小时前
【JavaEE初阶 — 多线程】Thread类的方法&线程生命周期
java·开发语言·java-ee
豪宇刘1 小时前
SpringBoot+Shiro权限管理
java·spring boot·spring
Elaine2023911 小时前
02多线程基础知识
java·多线程