java-压缩图片-后记-webp的坑

在上次完成压缩图片的方法封装之后,

测试小伙伴发现一个问题,仍然有部分图片是压缩失败的??

在各方搜索之下发现:谷歌的webp格式的图片不会随着文件后缀而改变,也就是说,可能他下载的png/jpg 等等格式的图片内核仍然是webp(可以用记事本打开看看或者notepad++)

这就导致:java自带的ImageIO.read( InputStream ) 读取到的的是会返回一个null,无法对这部分图片处理;

webp格式图片资料:WebP是google开发的一种旨在加快图片加载速度的图片格式。图片压缩体积大约只有JPEG的2/3,并能节省大量的服务器带宽资源和数据空间。是现代图像格式,提供了优越的无损和有损压缩的图片在网络上。使用WebP,网站管理员和web开发人员可以创建更小、更丰富的图像,使网页更快。

WebP无损的png图像小26%。WebP有损图像是25 - 34%小于等效SSIM质量指数可比JPEG图像

无损WebP支持透明(也称为alpha通道)的成本只有22%额外的字节。对有损压缩RGB压缩情况下是可以接受的,有损WebP还支持透明度,通常提供3×PNG相比较小的文件大小。

但是!!!!

但是!!!由于Webp格式推出比较晚, Jdk 内置的图片编解码库对此并不支持(所以返回null)。因此,需要在处理图片之前,先检验图片的真实格式(不是通过后缀去解析)

代码如下,最后附上参考大佬的链接:


    public static String imgType(InputStream inputStream) throws IOException {
        // 读取文件前几位
        byte[] fileHeader = new byte[4];
        int read = inputStream.read(fileHeader, 0, fileHeader.length);
        inputStream.close();
        
        // 转为十六进制字符串
        String header = ByteUtil.bytes2Hex(fileHeader);
        
        if (header.contains("FFD8FF")) {
            return "jpg";
        } else if (header.contains("89504E47")) {
            return "png";
        } else if (header.contains("47494638")) {
            return "gif";
        } else if (header.contains("424D")) {
            return "bmp";
        } else if (header.contains("52494646")) {
            return "webp";
        } else if (header.contains("49492A00")) {
            return "tif";
        } else {
            return "unknown";
        }
 
    }
 
    public static String bytes2Hex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            String hex = Integer.toHexString(b & 0xff);
            sb.append(hex.length() == 2 ? hex : ("0"+hex));
        }
        return sb.toString();}                 
原文链接:https://blog.csdn.net/Apoca_lypse/article/details/106723876

所以,完整处理流程:

  1. 下载github中处理webp格式的jar包,四方打听0.1.0版本是最好的:

https://objects.githubusercontent.com/github-production-release-asset-2e65be/147946359/f846fb80-b3c9-11e8-8de9-81b6da286204?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAVCODYLSA53PQK4ZA%2F20240418%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20240418T120856Z&X-Amz-Expires=300&X-Amz-Signature=e60d0f938ea7b3f21b7898a5f6176c7cbc5eb458bb7b15bdf51f3418727d4592&X-Amz-SignedHeaders=host&actor_id=0&key_id=0&repo_id=147946359&response-content-disposition=attachment%3B%20filename%3Dwebp-imageio-core-0.1.0.jar&response-content-type=application%2Foctet-stream

📎webp-imageio-core-0.1.0.jar

  1. 使用maven命令引入自己的仓库

mvn install:install-file -Dfile=E:\Downloads\webp-imageio-core-0.1.0.jar -DgroupId=org.sejda.imageio -DartifactId=webp-imageio-core -Dversion=0.1.0 -Dpackaging=jar

解析(来源于通义千问):

这条命令是在命令行中使用Apache Maven工具来将一个本地的jar包安装到Maven本地仓库中。命令的具体解释如下:

  • mvn: 这是Maven命令行工具的执行命令。
  • install:install-file: 这是一个Maven插件的目标,用于安装指定的文件到Maven仓库。在这里主要用于将一个外部的jar包安装到本地仓库中,使得在项目的构建过程中可以通过Maven坐标来引用它。

命令后面的参数是对要安装的jar包的相关信息的定义:

  • -Dfile=E:\Downloads\webp-imageio-core-0.1.0.jar: 定义了要安装的jar包文件的路径和文件名。这意味着Maven会从E盘的Downloads目录下的webp-imageio-core-0.1.0.jar文件读取jar包内容。
  • -DgroupId=org.sejda.imageio: 指定了jar包在Maven坐标系中的Group ID。Group ID是用来组织相关的项目和模块的一个命名空间。
  • -DartifactId=webp-imageio-core: 指定了jar包在Maven坐标系中的Artifact ID。Artifact ID是项目构建产生的主要工件的唯一标识符,通常对应于一个 jar 包的名字。
  • -Dversion=0.1.0: 指定了jar包的版本号。
  • -Dpackaging=jar: 表明此工件打包的形式是jar。在Maven中,packaging可以是jar、war、ear等多种形式,这里指明它是jar格式的包。

执行这条命令后,Maven会将指定的jar包安装到本地仓库中,并且可以用 <groupId>:<artifactId>:<version> 这样的Maven坐标在其他项目中引用它。

  1. 例如,在其他项目的pom.xml文件中,可以这样添加依赖:


    <dependency>
       <groupId>org.sejda.imageio</groupId>
        <artifactId>webp-imageio-core</artifactId>
        <version>0.1.0</version>
    </dependency>
</dependencies>
  1. 示例代码如下:

fileService.createFile是我自己的上传方法

    /**
     * 压缩图片
     * @param uploadReqVO
     * @return FileDO
     * @throws Exception
     */
    @PostMapping("/compressImage")
    @PermitAll
    public CommonResult<FileDO> compressImage(FileUploadReqVO uploadReqVO) throws Exception {
         MultipartFile file = uploadReqVO.getFile();
        String fileName = file.getOriginalFilename();
        String fileSuffix = FileUtils.getFileNameSuffix(fileName);
        if (StrUtil.isNotEmpty(fileSuffix) && StrUtil.containsAny(fileSuffix, new CharSequence[]{"bmp", "gif", "jpg", "jpeg", "png"})) {
            // 验证文件是否实际上是WebP格式
            if (imgType(file.getInputStream()).equals("webp")) {
                // 处理WebP格式的逻辑,例如使用webp-imageio库
                return CommonResult.success(compressPictureByMultipartFile(file));
            } else {
                // 如果不是WebP,按原逻辑处理
                return CommonResult.success(compressImage(file)); // 假设压缩质量为80%
             // 假设压缩质量为80%
            }
            // 调用压缩方法

        } else {
            log.error("当前仅支持:bmp、gif、jpg、jpeg、png等格式图片压缩");
            return CommonResult.error(500, "当前支持压缩的图片格式:bmp、gif、jpg、jpeg、png");
        }
    }
    private static  final double targetSize = 2 * 1024 * 1024; // 目标大小(2MB)

    public FileDO compressImage(MultipartFile file) throws IOException {
        //如果小于500kb直接返回
        long originalSize = file.getSize();
        if (originalSize <= targetSize) {
            return this.fileService.createFile(0L,file.getOriginalFilename(), null, file.getBytes(), false, file.getInputStream());
        }
        // 初始质量估计,考虑一个基础比例
        double quality = Math.max(0.1, Math.min(1.0, targetSize / originalSize));

        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        // 第一次压缩
        compress(file, quality, outputStream);
        // 如果第一次压缩后的图片仍大于2MB,则继续压缩
        while (outputStream.size() > targetSize && quality > 0.05) { // 防止压缩质量过低
            quality -= 0.05; // 逐步降低质量
            outputStream.reset(); // 重置输出流以便重新压缩
            compress(file, quality, outputStream);
        }
        byte[] bytes = outputStream.toByteArray();
        return this.fileService.createFile(0L,file.getOriginalFilename(), null, bytes, false, file.getInputStream());
    }


    /**
     * 压缩图片
     *
     * @param inputFile
     * @param quality
     * @param outputStream
     * @throws IOException
     */
    private void compress(MultipartFile inputFile, double quality, ByteArrayOutputStream outputStream) throws IOException {
        Thumbnails.of(inputFile.getInputStream())
                .scale(1.0) // 保持原始尺寸
                .outputQuality(quality)
                .outputFormat("jpg")
                .toOutputStream(outputStream);
    }





/**
 * 将MultipartFile对象转换为File对象并保存在临时目录下。
 *
 * @param multipartFile 从HTTP请求中接收到的MultipartFile对象
 * @return 转换后的File对象,存储在临时目录下
 * @throws IOException 在转换过程中出现I/O错误时抛出异常
 */
public File convertMultipartFileToFile(MultipartFile multipartFile) throws IOException {
    // 获取文件原始名称
    String fileName = multipartFile.getOriginalFilename();
    // 创建一个临时目录(默认为系统的临时目录)
    File tempDir = new File(System.getProperty("java.io.tmpdir"));
    // 创建一个临时文件,文件名与原始名称相同,扩展名保留
    File tempFile = null;
    if (fileName != null) {
        tempFile = File.createTempFile(fileName, "", tempDir);
        // 将MultipartFile的内容写入临时文件
        multipartFile.transferTo(tempFile);
    }
    return tempFile;
}

/**
 * 压缩图片方法,接受一个MultipartFile对象作为输入,将其转换为压缩后的图片,并以FileDO对象返回。
 *
 * @param file 用户上传的MultipartFile对象
 * @return 压缩后的图片信息封装在FileDO对象中
 * @throws IOException 图片读取、压缩或写入过程中发生错误时抛出异常
 */
public FileDO compressPictureByMultipartFile(MultipartFile file) throws IOException {
    // 记录日志,显示MultipartFile对象的输入流信息
    log.info("file.getInputStream(){}", file.getInputStream());

    // 获取WebP图像读取器
    ImageReader reader = ImageIO.getImageReadersByMIMEType("image/webp").next();

    // 配置WebP解码参数
    WebPReadParam readParam = new WebPReadParam();
    readParam.setBypassFiltering(true);

    // 设置读取器的输入源为从MultipartFile转换而来的File对象
    reader.setInput(new FileImageInputStream(convertMultipartFileToFile(file)));

    // 解码WebP图片
    BufferedImage image = reader.read(0, readParam);

    // 初始化压缩质量及输出流
    float quality = 0.8f;
    ByteArrayOutputStream baos = new ByteArrayOutputStream();

    // 循环调整压缩质量直至文件大小小于2MB
    do {
        baos.reset(); // 清空输出流以便重写压缩数据
        ImageWriter imgWrier = ImageIO.getImageWritersByFormatName("jpg").next();
        ImageWriteParam imgWriteParams = new JPEGImageWriteParam(null);
        imgWriteParams.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
        imgWriteParams.setCompressionQuality(quality);
        imgWriteParams.setProgressiveMode(ImageWriteParam.MODE_DISABLED);

        imgWrier.setOutput(ImageIO.createImageOutputStream(baos));
        imgWrier.write(null, new IIOImage(image, null, null), imgWriteParams);
        imgWrier.dispose(); // 关闭ImageWriter以释放资源

        byte[] compressedBytes = baos.toByteArray();
        if (compressedBytes.length < 2 * 1024 * 1024) { // 判断压缩后大小是否小于2MB
            break;
        } else {
            quality -= 0.1f; // 若大于2MB则减小压缩质量
        }
    } while (quality >= 0.1f); // 保证最低质量不低于0.1

    // 假设fileService.createFile方法用于处理压缩后的字节数组并返回FileDO对象
    return fileService.createFile(0L, file.getOriginalFilename(), null, baos.toByteArray(), false, null);
}

/**
 * 根据文件输入流的前几个字节识别图片文件类型。
 *
 * @param inputStream 文件输入流
 * @return 图片文件类型(如"jpg"、"png"、"gif"等)
 * @throws IOException 在读取文件头时发生错误时抛出异常
 */
public String imgType(InputStream inputStream) throws IOException {
    // 读取文件头的前4个字节
    byte[] fileHeader = new byte[4];
    int read = inputStream.read(fileHeader, 0, fileHeader.length);
    inputStream.close();

    // 将字节转换为十六进制字符串
    String header = bytes2Hex(fileHeader);

    // 根据文件头判断图片类型
    if (header.contains("FFD8FF")) {
        return "jpg";
    } else if (header.contains("89504E47")) {
        return "png";
    } else if (header.contains("47494638")) {
        return "gif";
    } else if (header.contains("424D")) {
        return "bmp";
    } else if (header.contains("52494646")) {
        return "webp";
    } else if (header.contains("49492A00")) {
        return "tif";
    } else {
        return "unknown";
    }
}

/**
 * 将字节数组转换为十六进制字符串。
 *
 * @param bytes 字节数组
 * @return 十六进制表示的字符串
 */
public String bytes2Hex(byte[] bytes) {
    StringBuilder sb = new StringBuilder();
    for (byte b : bytes) {
        String hex = Integer.toHexString(b & 0xff);
        sb.append(hex.length() == 2 ? hex : ("0" + hex));
    }
    return sb.toString();
}

方法来源各方大佬的链接和资料,如果有没有指出的地方还望私聊我,侵删

也欢迎大佬们给出指正意见,谢谢各位!

  • 16
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值