问题背景
使用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即可。
步骤如下:
- 读取文件头
判断文件类型为jpg还是png,对后缀名不对的,强行补充.png等
- 一行压缩
Thumbnails.of(oriFile).scale(compressFactor).toFile(finalFileName);
完整代码:
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); // 校验文件头信息,防止txt、html等文件 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~9、A~F来表示1~16,所以如果Byte转换成Hex后如果是<16,就会是一个字符(比如A=10),通常是使用两个字符来表示16进制位的, //假如一个字符的话,遇到字符串11,这到底是1个字节,还是1和1两个字节,容易混淆,如果是补0,那么1和1补充后就是0101,11就表示纯粹的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); } } |