上传头像功能实现
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
package com.yunhai.userbase.utils;
|
||||
|
||||
import cn.hutool.core.img.Img;
|
||||
import cn.hutool.core.img.ImgUtil;
|
||||
import com.yunhai.platform.utils.exception.DException;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.Color;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
/**
|
||||
* 头像图片压缩工具(基于 hutool-core 的 Img,无额外依赖)。<br>
|
||||
* <br>
|
||||
* 压缩规则(按原图字节数分支):
|
||||
* <ul>
|
||||
* <li>gif:原样返回——ImageIO 重编码会丢失动画帧;</li>
|
||||
* <li>解码失败(webp/heic/CMYK jpg 等 JDK 无解码器的格式):原样返回兜底,上传主流程不中断;</li>
|
||||
* <li>≤5MB:quality 0.95 不缩放(近无损);</li>
|
||||
* <li>>5MB:quality 0.75,且长边 >2000px 时等比缩至 2000px(手机照片体积大主要是分辨率大);</li>
|
||||
* <li>jpg/jpeg 走 JPEG 有损质量压缩(透明区域铺白底);png/bmp 为无损格式无质量参数,
|
||||
* 仅在需要缩放时才重编码;</li>
|
||||
* <li>压缩结果不小于原大小时保留原字节(已优化过的小图等效无损直存)。</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class ImageCompressUtil {
|
||||
|
||||
/** 超过该字节数走高压缩分支(5MB) */
|
||||
private static final int BIG_THRESHOLD = 5 * 1024 * 1024;
|
||||
/** 高压缩分支的最大长边(px) */
|
||||
private static final int BIG_MAX_SIDE = 2000;
|
||||
/** 近无损质量 */
|
||||
private static final float QUALITY_NEAR_LOSSLESS = 0.95f;
|
||||
/** 高压缩质量 */
|
||||
private static final float QUALITY_HIGH = 0.75f;
|
||||
|
||||
private ImageCompressUtil() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩头像,失败/无收益时一律返回原字节,保证调用方总能拿到可用数据。
|
||||
*
|
||||
* @param srcBytes 原图字节(非空)
|
||||
* @param ext 小写扩展名(不含点),用于判定格式分支
|
||||
* @return 压缩后的字节;无需/无法压缩时为原字节
|
||||
*/
|
||||
public static byte[] compressAvatar(byte[] srcBytes, String ext) {
|
||||
if (srcBytes == null || srcBytes.length == 0) {
|
||||
throw new DException("头像内容为空");
|
||||
}
|
||||
String type = ext == null ? "" : ext.toLowerCase().replaceFirst("^\\.", "");
|
||||
// 动图重编码丢帧,原样返回
|
||||
if ("gif".equals(type)) {
|
||||
return srcBytes;
|
||||
}
|
||||
BufferedImage image;
|
||||
try {
|
||||
image = ImageIO.read(new ByteArrayInputStream(srcBytes));
|
||||
} catch (Exception e) {
|
||||
image = null;
|
||||
}
|
||||
// webp/heic/CMYK jpg 等解不动:原样返回(前置已校验大小上限)
|
||||
if (image == null) {
|
||||
return srcBytes;
|
||||
}
|
||||
|
||||
boolean big = srcBytes.length > BIG_THRESHOLD;
|
||||
float quality = big ? QUALITY_HIGH : QUALITY_NEAR_LOSSLESS;
|
||||
int maxSide = big ? BIG_MAX_SIDE : Integer.MAX_VALUE;
|
||||
boolean lossyFormat = "jpg".equals(type) || "jpeg".equals(type);
|
||||
int w = image.getWidth();
|
||||
int h = image.getHeight();
|
||||
|
||||
// png/bmp 无质量参数:不需要缩放就不重编码(重编码无收益甚至变大)
|
||||
if (!lossyFormat && Math.max(w, h) <= maxSide) {
|
||||
return srcBytes;
|
||||
}
|
||||
try {
|
||||
String targetType = lossyFormat ? ImgUtil.IMAGE_TYPE_JPEG
|
||||
: ("bmp".equals(type) ? ImgUtil.IMAGE_TYPE_BMP : ImgUtil.IMAGE_TYPE_PNG);
|
||||
Img img = Img.from(image)
|
||||
.setTargetImageType(targetType)
|
||||
.setQuality(quality);
|
||||
if (lossyFormat) {
|
||||
// JPEG 无透明通道,透明区域统一铺白底
|
||||
img.setBackgroundColor(Color.WHITE);
|
||||
}
|
||||
if (Math.max(w, h) > maxSide) {
|
||||
double ratio = maxSide / (double) Math.max(w, h);
|
||||
img.scale(Math.max(1, (int) Math.round(w * ratio)),
|
||||
Math.max(1, (int) Math.round(h * ratio)));
|
||||
}
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
img.write(out);
|
||||
byte[] compressed = out.toByteArray();
|
||||
// 无收益(压完更大)时保留原字节
|
||||
return (compressed.length == 0 || compressed.length >= srcBytes.length) ? srcBytes : compressed;
|
||||
} catch (Exception e) {
|
||||
return srcBytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user