png图片压缩后黑底问题解决

问题背景

使用thumbnail对图片进行压缩,偶然会发现对png图片出现黑底的情况如下:

压缩前

压缩后

问题解决

对网上搜到的解决方法主要有两种:

1.指定png输出

JAVA - Get black background when uploading PNG image - Stack Overflow

一句话解决Thumbnails缩略图工具PNG透明背景缩放后变黑问题_thumbnails背景变黑_applebomb的博客-CSDN博客

但是这样有个问题就是要指定大小,可以参考

Java压缩图片util,可等比例宽高不失真压缩,也可直接指定压缩后的宽高_xiaoxiansweety的博客-CSDN博客 做等比例,不过代码稍微复杂点

2.重绘图,用白底重绘

用白的背景重画,就会有类似如下问题:左边为原图,右边为重绘后的图

解决方案

看thumbnail的tofile方法可以看到

看fileImageLink,可以看到getExtension()

那么是怎么确定输出文件的后缀呢?获取.后面的内容 

 问题到这基本就清晰了,黑底的原因是png压缩过程中alpha通道值没了,用黑色补充,为什么alpha通道会丢失呢?因为按jpg处理了,所以我们只需要处理png时,识别到真实png,保证其后缀名为.png即可。

步骤如下:

  1. 读取文件头

判断文件类型为jpg还是png,对后缀名不对的,强行补充.png等

  1. 一行压缩

 Thumbnails.of(oriFile).scale(compressFactor).toFile(finalFileName);

-----

2023.6.3补充

实际上thumbnailator在设计时就没考虑png位深的问题,见

How to convert a image to a 8-bit png ? · Issue #83 · coobird/thumbnailator · GitHub

所以最后方案改为jpg使用thumbnailator压缩,png使用比较冷门的压缩软件:OpenViewerFX,这个软件实际不是非本人写的,只是把java的图片功能给抠出来了
 

<dependency>
<groupId>org.jpedal</groupId>
<artifactId>OpenViewerFX</artifactId>
<version>6.6.14</version>
</dependency>

更神奇的是就这个版本行,其它版本都恰好把要用的几个类给踢掉了,2015年的包,从压缩效果看,几乎和在线的tinypng差不多。试了透明底/彩色/普通的png,都能完美过关。

检索到的入口Java压缩png图片文件大小,效果跟Tinypng压缩效果大致一样_MAYHENG的博客-CSDN博客

完整代码:

package com.htsc.project.common;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.text.DecimalFormat;
import java.util.Map;

import javax.annotation.PostConstruct;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import lombok.extern.slf4j.Slf4j;
import net.coobird.thumbnailator.Thumbnails;

/**
 * @author 020152
 * @date 2023/5/9
 */
@Service
@Slf4j
public class ImageCompress {
    private static final Integer COMPRESSED = 1;
    /**
     * 支持的文件的头,key:文件头,value:支持的文件后缀,以逗号分割
     * {"FFD8FF":"jpg,jpeg","89504E47":"png"}
     */
    @Value(value = "#{${image.compress.fileHeader:{\"FFD8FF\":\"jpg,jpeg\",\"89504E47\":\"png\"}}}")
    private Map<String, String> compressFileHeaders;

    /**
     * 是否压缩图片,0不压缩,1压缩
     */
    @Value(value = "${image.compressed:0}")
    private Integer compressed;

    /**
     * png图片开始压缩的大小,对较小的png不压缩,防止无用功
     */
    @Value(value = "${image.compress.minSize:100000}")
    private Long minCompressedSize;

    /**
     * 太大的png压缩必要不大,可能是无用功
     */
    @Value(value = "${image.compress.maxSize:10000000}")
    private Long maxCompressedSize;

    /**
     * 压缩率,默认0.9
     */
    @Value(value = "${image.compressFactor:0.9}")
    private Double compressFactor;

    @Autowired
    private Config config;

    @PostConstruct
    public void init() {
        log.info("compressHeaders:{} compressed{}compressFactor:{}", compressFileHeaders, compressed, compressFactor);
        log.info(Constant.LOG_DEVMODESTR + config.getDevMode());
    }

    /**
     * 压缩图片
     *
     * @param oriFile  原始文件
     * @param fileName 文件名
     * @param random   uuid随机
     * @return 压缩后的文件
     */
    public File compress(File oriFile, String fileName, String random) {
        if (!COMPRESSED.equals(compressed)) {
            return oriFile;
        }
        try (InputStream inputStream = Files.newInputStream(oriFile.toPath())) {
            byte[] bytes = new byte[10];
            inputStream.read(bytes, 0, bytes.length);
            // 校验文件头信息,防止txthtml等文件
            String fileHeader = bytesToHex(bytes);
            String matchExt = compressFileHeaders.keySet().stream().filter(fileHeader::startsWith).findAny().orElse(null);
            if (matchExt == null) {
                // 非图片文件,直接返回
                return oriFile;
            }
            String suffix, prefix;
            if (fileName.lastIndexOf(".") == -1) {
                suffix = "." + compressFileHeaders.get(matchExt);
                if (suffix.contains(",")) {
                    suffix = ".jpg";
                }
                prefix = fileName;
            } else {
                suffix = fileName.substring(fileName.lastIndexOf("."));
                prefix = fileName.substring(0, fileName.lastIndexOf("."));
            }
            String finalFileName = prefix + suffix;
            // 后缀名和真实文件类型不匹配,如本身是png文件但是后缀为jpg,或反之
            if (!compressFileHeaders.get(matchExt).contains(suffix.substring(1))) {
                // 实际是jpg文件,但是用了png后缀,在toFile使用jpg压缩,保证压缩率,但是不修改目标文件后缀名
                if (!suffix.contains("jpg")) {
                    finalFileName = prefix + ".jpg";
                }
                // 实际是png文件,但是用jpg后缀,在toFile使用jpg压缩可能出现黑底
                if ("png".equals(compressFileHeaders.get(matchExt))) {
                    finalFileName = prefix + ".png";
                }
            }
            finalFileName = random + "_" + finalFileName;
            File result = compressFile(matchExt, oriFile, finalFileName);
            long oriLength = oriFile.length();
            long length = result.length();
            Double cut = (oriLength - length) / (double) oriLength;
            DecimalFormat df = new DecimalFormat("#.##%");
            String cutStr = df.format(cut);
            log.info("fileName:{}, finalFileName:{} , 压缩前:{}, 压缩后:{}, 节省:{}", fileName, finalFileName, oriLength, length, cutStr);
            // 压缩失败=0;压缩负优化;png原图大小不在压缩范围
            if (length == 0 || length > oriLength || result.equals(oriFile)) {
                return oriFile;
            }
            oriFile.delete();
            return result;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 字节数组转Hex
     *
     * @param bytes 字节数组
     * @return Hex
     */
    public String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        if (bytes != null) {
            for (byte aByte : bytes) {
                String hex = byteToHex(aByte);
                sb.append(hex);
            }
        }
        return sb.toString();
    }

    /**
     * Byte字节转Hex
     *
     * @param b 字节
     * @return Hex
     */
    public String byteToHex(byte b) {
        String hexString = Integer.toHexString(b & 0xFF);
        //由于十六进制是由0~9A~F来表示1~16,所以如果Byte转换成Hex后如果是<16,就会是一个字符(比如A=10),通常是使用两个字符来表示16进制位的,
        //假如一个字符的话,遇到字符串11,这到底是1个字节,还是11两个字节,容易混淆,如果是补0,那么11补充后就是010111就表示纯粹的11
        if (hexString.length() < 2) {
            hexString = 0 + hexString;
        }
        return hexString.toUpperCase();
    }

    private File compressFile(String matchExt, File oriFile, String finalFileName) throws IOException {
        if (compressFileHeaders.get(matchExt).contains("jpg")) {
            return compressJpg(oriFile, finalFileName);
        } else {
            return compressPng(oriFile, finalFileName);
        }
    }

    /**
     * 实际可以和compressJpg合并为一个方法,考虑后期png可能单独处理,暂时保留
     */
    private File compressPng(File oriFile, String finalFileName) throws IOException {
        if (oriFile.length() < minCompressedSize || oriFile.length() > maxCompressedSize) {
            return oriFile;
        }
        Thumbnails.of(oriFile).scale(compressFactor).toFile(finalFileName);
        return new File(finalFileName);
    }

    private File compressJpg(File oriFile, String finalFileName) throws IOException {
        Thumbnails.of(oriFile).scale(compressFactor).toFile(finalFileName);
        return new File(finalFileName);
    }
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值