easyExcel - 带图片导出

news2024/11/19 15:21:34

目录

  • 前言
  • 一、情景介绍
  • 二、问题分析
  • 三、代码实现
    • 1. 单图片导出
    • 2. 多图片导出
    • 3. 多图片导出(优化)


前言

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

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


一、情景介绍

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

在这里插入图片描述

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


二、问题分析

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

官方文档:图片导出

在这里插入图片描述

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

@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

在这里插入图片描述

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


    /**
     * 带图片导出:官方案例
     */
    @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

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;

}

代码示例:

    /**
     * 带图片导出:单元格只需要存放单张图片
     */
    @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

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

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

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;

}

代码示例:

    /**
     * 带图片导出:多图片导出
     */
    @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 的单位并不是像素

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

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

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 文件会更小

在这里插入图片描述

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

		// 下载文件存放地址
        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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1624567.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

分布式文件系统--MinIO

1 MinIO安装(Docker) ●在root目录下新建docker_minio文件夹 ●在docker_minio文件夹下新建config文件夹,data文件夹 ●在root目录下新建docker_compose文件夹,在docker_compose文件夹中添加docker-compose.yaml services:minio:image: quay.io/minio/miniocontainer_name: mi…

Maya vs Blender:制作3D动画首选哪一个?

就 3D 动画而言&#xff0c;有两款3D软件引发了最多的争论&#xff1a;Blender 与 Maya。这两个强大的平台都提供强大的工具集&#xff0c;使动画故事和角色栩栩如生。但作为一名3D动画师&#xff0c;您应该投入时间学习和创作哪一个呢&#xff1f;下面我将从以下六点给您一个清…

从0开始用C写贪吃蛇(基于链表)

目录 1. 游戏背景 2. 游戏效果演示​编辑​编辑​编辑 3. 实现目标 4. 技术要点 5. 控制台程序 5.1 设置控制台窗口的长宽和名字 5.2 控制台屏幕上的坐标COORD 6.Win32 API 6.1 GetStdHandle 6.2 GetConsoleCursorInfo 6.3 CONSOLE_CURSOR_INFO 6.4 SetConsole…

python监听html click教程

&#x1f47d;发现宝藏 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。【点击进入巨牛的人工智能学习网站】。 Python实现监听HTML点击事件 在Web开发中&#xff0c;经常需要在用户与页面交互时执行一些…

电磁兼容(EMC):静电放电(ESD)抗扰度试验深度解读(七)

目录 1. 第一步 确定电磁环境 2. 第二步 确认设备工作状态 3. 第三步 制定试验计划 4. 间接施加的放电 4.1 水平耦合板 4.2 垂直耦合板 静电抗扰度的试验测试细节对测试结果影响比较大&#xff0c;本文详细介绍静电抗扰度试验的测试程序和注意事项。 1. 第一步 确定电磁…

工作任务管理平台作品集源文件 figma格式UX/UI设计师作品集

交付文件&#xff1a;作品集源文件项目源文件包装样机源文件字体文件 交付格式&#xff1a;figma、psd 作品集文件页数&#xff1a;28页 项目文件页数&#xff1a;12页&#xff08;Web&#xff09;12页&#xff08;App&#xff09; 以下重点哦&#xff0c;请认真阅读&#…

怡宝母公司冲刺上市:产能未满仍要募资扩产,突击分红25亿元

又一家瓶装水企业冲刺上市。 近日&#xff0c;怡宝母公司华润饮料&#xff08;控股&#xff09;有限公司&#xff08;下称“华润饮料”&#xff09;递交招股书&#xff0c;准备在港交所主板上市&#xff0c;BofA securities&#xff08;美银证券&#xff09;、中银国际、中信证…

nosql数据库 redis

一、介绍 1、redis与mysql的区别&#xff1a; Redis是一种基于键值对的内存数据库&#xff0c;数据存储在内存中&#xff0c;因此读写速度非常快。它支持多种数据结构&#xff0c;如字符串、哈希、列表等。 MySQL是一种关系型数据库&#xff0c;数据以表格的形式组织存储在磁…

linux-进程(2)

1.通过系统调用获取进程标示符 进程id&#xff08;PID&#xff09; 父进程id&#xff08;PPID&#xff09; 每一个可执行程序运行起来之后都会成为一个进程&#xff0c;每个进程都有一个自己的id&#xff0c;以及一个父进程id&#xff0c;父进程就是创建自己进程的进程&#xf…

玩转nginx的配置文件3

1. limit_req_zone配置限流 limit_req_zone $binary_remote_addr zonemylimit:10m rate10r/s;upstream myweb {server 10.0.105.196:80 weight1 max_fails1 fail_timeout1;}server {listen 80;server_name localhost;location /login {limit_req zonemylimit;proxy_pass http:…

C++/QT + Mysql + Tcp 企业协作管理系统

目录 一、项目介绍 二、项目展示 三、源码获取 一、项目介绍 1、项目概要&#xff1a;C/S架构、数据库Mysql、C、QT&#xff1b;支持实时通信、局域网内通信&#xff0c;可多个客户端同时登录&#xff1b; 2、&#xff08;Server&#xff09;管理端&#xff1a;用户管理、…

详解js中的console对象

对于前端开发而言&#xff0c;console对象大家肯定都很熟悉&#xff0c;最常用的 console.log() 是开发调试必用的 但是对于console对象的其他方法&#xff0c;相对而言使用的就比较少了。下面详细介绍一下&#xff1a; 谷歌浏览器输出console对象&#xff1a; 值得一提的是不…

2018-2023年上市公司富时罗素ESG评分数据

2018-2023年上市公司富时罗素ESG评分数据 1、时间&#xff1a;2018-2023年 2、来源&#xff1a;整理自WIND 3、指标&#xff1a;证券代码、简称、ESG评分 4、范围&#xff1a;上市公司 5、指标解释&#xff1a; 富时罗素将公司绿色收入的界定和计算作为公司ESG 评级打分结…

Windows批处理脚本,用于管理Nginx服务器

先看截图&#xff1a; Windows批处理脚本&#xff0c;用于管理Nginx服务器。它提供了启动、重启、关闭Nginx以及刷新控制台等功能。 设置环境变量&#xff1a; set NGINX_PATHD:&#xff1a;设置Nginx所在的盘符为D盘。set NGINX_DIRD:\nginx1912\&#xff1a;设置Nginx所在…

以太网ARP协议解析

一、什么是ARP协议 ARP协议&#xff0c;全称是Address Resolution Protocol&#xff0c;即地址解析协议。 ARP协议的作用&#xff0c;就是在已知目标设备的IP地址但是不知道其MAC地址的时候&#xff0c;根据IP地址&#xff0c;获取到其MAC地址&#xff0c;以便组成完整的IP包进…

profinet协议基础

文章目录 工业以太网自动化通讯金字塔工业以太网技术比较 profinet概述profinet特性 EtherNet通信EtherCAT通信EtherCat特性EtherCat过程同步 工业以太网 工业以太网是基于IEEE 802.3 (Ethernet)的强大的区域和单元网络。 自动化通讯金字塔 各个组织与工业以太网 工业以太网…

2024.4.25

#include <iostream> #include <iomanip> using namespace std; class Person{const string name;int age;char sex; public:Person(const string name):name(name){cout << "第一个Person构造函数" << endl;}Person():name("zhangsan&…

面试经典150题——路径总和

​ 1. 题目描述 2. 题目分析与解析 2.1 思路一 注意题目的关键点&#xff1a;判断该树中是否存在 根节点到叶子节点 的路径&#xff0c;起点是root&#xff0c;终点是叶子节点。 那么我们就可以从根节点按照层序遍历的方式&#xff0c;从根节点从根到 叶子不断对路径进行加…

javaSE(九):线程

目录 一、程序,进程,线程 1.概念 2.三者之间的关系&#xff1a; 二、创建线程 ①继承Thread类的方式 ②实现Runnable接口的方式 三、Thread类中方法 1.常用方法 2.线程优先级 四、线程状态 ①线程在它的生命周期中会处于不同的状态 ②线程的状态 五、多线程 1.概念…

君正X2100 RTOS 固件升级

使用cloner工具烧写固件需要在上电之前让boot_sel[2:0]处于boot from USB模式&#xff0c;但是电路板装在机壳内部后不方便改变boot_sel[2:0]的状态&#xff0c;如果要升级固件&#xff0c;需要通过机壳留出的USB口、网口、或者无线网络进行固件更新。 一、升级方案 1、固件分…