package com.ruoyi.common.utils.image;
|
|
import org.slf4j.Logger;
|
import org.slf4j.LoggerFactory;
|
import org.springframework.web.multipart.MultipartFile;
|
|
import javax.imageio.IIOImage;
|
import javax.imageio.ImageIO;
|
import javax.imageio.ImageWriteParam;
|
import javax.imageio.ImageWriter;
|
import javax.imageio.stream.ImageOutputStream;
|
import java.awt.Graphics2D;
|
import java.awt.RenderingHints;
|
import java.awt.image.BufferedImage;
|
import java.io.File;
|
import java.io.IOException;
|
import java.io.InputStream;
|
import java.util.Iterator;
|
|
/**
|
* 图片压缩工具类
|
* 专为OCR识别优化,在保证文字清晰度的前提下压缩图片大小
|
*
|
* @author ruoyi
|
* @date 2025-01-20
|
*/
|
public class ImageCompressUtil {
|
|
private static final Logger log = LoggerFactory.getLogger(ImageCompressUtil.class);
|
|
/**
|
* 默认最大文件大小(10MB)
|
*/
|
public static final long DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
/**
|
* OCR最佳识别分辨率(最小边像素)
|
*/
|
public static final int OCR_OPTIMAL_MIN_SIZE = 1500;
|
|
/**
|
* 高质量压缩质量参数(0.90 = 90%质量)
|
*/
|
public static final float HIGH_QUALITY = 0.90f;
|
|
/**
|
* 中等质量压缩质量参数(0.80 = 80%质量)
|
*/
|
public static final float MEDIUM_QUALITY = 0.80f;
|
|
/**
|
* 低质量压缩质量参数(0.75 = 75%质量)
|
*/
|
public static final float LOW_QUALITY = 0.75f;
|
|
/**
|
* 智能压缩图片(针对OCR优化)
|
* 使用默认3MB限制
|
*
|
* @param file 上传的图片文件
|
* @return 压缩后的临时文件
|
* @throws IOException IO异常
|
*/
|
public static File compressForOCR(MultipartFile file) throws IOException {
|
return compressForOCR(file, DEFAULT_MAX_FILE_SIZE);
|
}
|
|
/**
|
* 智能压缩图片(针对OCR优化)
|
* 保证识别准确率的同时压缩到指定大小以下
|
*
|
* @param file 上传的图片文件
|
* @param maxSize 最大文件大小(字节)
|
* @return 压缩后的临时文件
|
* @throws IOException IO异常
|
*/
|
public static File compressForOCR(MultipartFile file, long maxSize) throws IOException {
|
long fileSize = file.getSize();
|
|
// 如果文件小于限制,直接保存
|
if (fileSize <= maxSize) {
|
log.debug("图片大小 {} KB 未超过限制 {} KB,无需压缩",
|
fileSize / 1024.0, maxSize / 1024.0);
|
return saveToTempFile(file);
|
}
|
|
log.info("图片大小 {} MB 超过限制 {} MB,开始智能压缩...",
|
fileSize / 1024.0 / 1024.0, maxSize / 1024.0 / 1024.0);
|
|
// 读取原始图片
|
BufferedImage originalImage = ImageIO.read(file.getInputStream());
|
if (originalImage == null) {
|
throw new IOException("无法读取图片文件,可能格式不支持");
|
}
|
|
int originalWidth = originalImage.getWidth();
|
int originalHeight = originalImage.getHeight();
|
log.info("原始图片尺寸: {}x{}", originalWidth, originalHeight);
|
|
// 策略1:先尝试高质量JPEG压缩(不改变尺寸)
|
File compressedFile = compressWithQuality(originalImage, file.getOriginalFilename(), HIGH_QUALITY);
|
|
if (compressedFile.length() <= maxSize) {
|
log.info("压缩成功(高质量压缩),文件大小: {} MB",
|
compressedFile.length() / 1024.0 / 1024.0);
|
return compressedFile;
|
}
|
|
// 策略2:如果还是太大,适度缩小尺寸(保证OCR识别)
|
double scaleFactor = calculateScaleFactor(originalWidth, originalHeight, maxSize, compressedFile.length());
|
|
if (scaleFactor < 1.0) {
|
int newWidth = (int) (originalWidth * scaleFactor);
|
int newHeight = (int) (originalHeight * scaleFactor);
|
log.info("调整图片尺寸至: {}x{} (缩放比例: {}%)",
|
newWidth, newHeight, (int)(scaleFactor * 100));
|
|
BufferedImage resizedImage = resizeImageHighQuality(originalImage, newWidth, newHeight);
|
compressedFile.delete(); // 删除之前的临时文件
|
compressedFile = compressWithQuality(resizedImage, file.getOriginalFilename(), HIGH_QUALITY);
|
}
|
|
// 策略3:如果还是太大,降低压缩质量(最后手段)
|
if (compressedFile.length() > maxSize) {
|
log.warn("尺寸调整后仍超限,降低压缩质量");
|
BufferedImage finalImage;
|
if (scaleFactor < 1.0) {
|
int newWidth = (int) (originalWidth * scaleFactor);
|
int newHeight = (int) (originalHeight * scaleFactor);
|
finalImage = resizeImageHighQuality(originalImage, newWidth, newHeight);
|
} else {
|
finalImage = originalImage;
|
}
|
|
compressedFile.delete(); // 删除之前的临时文件
|
|
// 尝试中等质量
|
compressedFile = compressWithQuality(finalImage, file.getOriginalFilename(), MEDIUM_QUALITY);
|
|
// 如果还不行,使用低质量
|
if (compressedFile.length() > maxSize) {
|
log.warn("使用低质量压缩(75%)");
|
compressedFile.delete();
|
compressedFile = compressWithQuality(finalImage, file.getOriginalFilename(), LOW_QUALITY);
|
}
|
}
|
|
long finalSize = compressedFile.length();
|
double compressionRatio = (1 - (double)finalSize / fileSize) * 100;
|
log.info("最终压缩完成,文件大小: {} MB (压缩率: {}%)",
|
finalSize / 1024.0 / 1024.0, (int)compressionRatio);
|
|
if (finalSize > maxSize) {
|
log.warn("警告:压缩后文件大小 {} MB 仍超过限制 {} MB",
|
finalSize / 1024.0 / 1024.0, maxSize / 1024.0 / 1024.0);
|
}
|
|
return compressedFile;
|
}
|
|
/**
|
* 使用指定质量压缩图片为JPEG格式
|
*
|
* @param image 图片对象
|
* @param originalFilename 原始文件名
|
* @param quality 压缩质量(0.0-1.0)
|
* @return 压缩后的文件
|
* @throws IOException IO异常
|
*/
|
public static File compressWithQuality(BufferedImage image, String originalFilename, float quality) throws IOException {
|
if (quality < 0.0f || quality > 1.0f) {
|
throw new IllegalArgumentException("压缩质量必须在0.0到1.0之间");
|
}
|
|
// 转换为RGB格式(JPEG不支持透明度)
|
BufferedImage rgbImage = convertToRGB(image);
|
|
// 创建临时文件
|
String tempDir = System.getProperty("java.io.tmpdir");
|
String filename = System.currentTimeMillis() + "_compressed_" +
|
getJpegFilename(originalFilename);
|
File outputFile = new File(tempDir, filename);
|
|
// 使用ImageWriter进行高质量压缩
|
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg");
|
if (!writers.hasNext()) {
|
throw new IOException("系统不支持JPEG编码");
|
}
|
|
ImageWriter writer = writers.next();
|
ImageWriteParam param = writer.getDefaultWriteParam();
|
|
// 设置压缩模式和质量
|
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
|
param.setCompressionQuality(quality);
|
|
try (ImageOutputStream ios = ImageIO.createImageOutputStream(outputFile)) {
|
writer.setOutput(ios);
|
writer.write(null, new IIOImage(rgbImage, null, null), param);
|
} finally {
|
writer.dispose();
|
}
|
|
log.debug("压缩完成,质量: {}%, 文件大小: {} KB",
|
(int)(quality * 100), outputFile.length() / 1024.0);
|
|
return outputFile;
|
}
|
|
/**
|
* 高质量图片缩放(专为OCR优化)
|
* 使用双三次插值算法保证文字清晰度
|
*
|
* @param originalImage 原始图片
|
* @param targetWidth 目标宽度
|
* @param targetHeight 目标高度
|
* @return 缩放后的图片
|
*/
|
public static BufferedImage resizeImageHighQuality(BufferedImage originalImage, int targetWidth, int targetHeight) {
|
if (targetWidth <= 0 || targetHeight <= 0) {
|
throw new IllegalArgumentException("目标宽度和高度必须大于0");
|
}
|
|
BufferedImage resizedImage = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB);
|
Graphics2D g2d = resizedImage.createGraphics();
|
|
try {
|
// 设置高质量渲染参数(关键!)
|
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
|
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
|
|
g2d.drawImage(originalImage, 0, 0, targetWidth, targetHeight, null);
|
} finally {
|
g2d.dispose();
|
}
|
|
return resizedImage;
|
}
|
|
/**
|
* 通用图片压缩(按目标宽高)
|
*
|
* @param file 上传的图片文件
|
* @param targetWidth 目标宽度
|
* @param targetHeight 目标高度
|
* @param quality 压缩质量(0.0-1.0)
|
* @return 压缩后的文件
|
* @throws IOException IO异常
|
*/
|
public static File compress(MultipartFile file, int targetWidth, int targetHeight, float quality) throws IOException {
|
BufferedImage originalImage = ImageIO.read(file.getInputStream());
|
if (originalImage == null) {
|
throw new IOException("无法读取图片文件");
|
}
|
|
BufferedImage resizedImage = resizeImageHighQuality(originalImage, targetWidth, targetHeight);
|
return compressWithQuality(resizedImage, file.getOriginalFilename(), quality);
|
}
|
|
/**
|
* 按比例压缩图片
|
*
|
* @param file 上传的图片文件
|
* @param scale 缩放比例(0.0-1.0)
|
* @param quality 压缩质量(0.0-1.0)
|
* @return 压缩后的文件
|
* @throws IOException IO异常
|
*/
|
public static File compressByScale(MultipartFile file, double scale, float quality) throws IOException {
|
if (scale <= 0.0 || scale > 1.0) {
|
throw new IllegalArgumentException("缩放比例必须在0.0到1.0之间");
|
}
|
|
BufferedImage originalImage = ImageIO.read(file.getInputStream());
|
if (originalImage == null) {
|
throw new IOException("无法读取图片文件");
|
}
|
|
int newWidth = (int) (originalImage.getWidth() * scale);
|
int newHeight = (int) (originalImage.getHeight() * scale);
|
|
BufferedImage resizedImage = resizeImageHighQuality(originalImage, newWidth, newHeight);
|
return compressWithQuality(resizedImage, file.getOriginalFilename(), quality);
|
}
|
|
/**
|
* 计算最佳缩放比例
|
*
|
* @param width 原始宽度
|
* @param height 原始高度
|
* @param targetSize 目标文件大小
|
* @param currentSize 当前文件大小
|
* @return 缩放比例
|
*/
|
private static double calculateScaleFactor(int width, int height, long targetSize, long currentSize) {
|
int minDimension = Math.min(width, height);
|
double scaleFactor = 1.0;
|
|
// 基于文件大小估算缩放比例
|
double sizeRatio = Math.sqrt((double) targetSize / currentSize);
|
|
// 保护最小尺寸(保证OCR识别)
|
if (minDimension > 2000) {
|
// 大图片,可以适度缩小
|
scaleFactor = Math.min(sizeRatio, 2000.0 / minDimension);
|
} else if (minDimension > OCR_OPTIMAL_MIN_SIZE) {
|
// 中等图片,轻微缩小
|
scaleFactor = Math.min(sizeRatio, (double) OCR_OPTIMAL_MIN_SIZE / minDimension);
|
} else {
|
// 小图片,尽量不缩小
|
scaleFactor = Math.min(sizeRatio, 0.95);
|
}
|
|
return scaleFactor;
|
}
|
|
/**
|
* 将图片转换为RGB格式
|
*
|
* @param image 原始图片
|
* @return RGB格式图片
|
*/
|
private static BufferedImage convertToRGB(BufferedImage image) {
|
if (image.getType() == BufferedImage.TYPE_INT_RGB) {
|
return image;
|
}
|
|
BufferedImage rgbImage = new BufferedImage(
|
image.getWidth(),
|
image.getHeight(),
|
BufferedImage.TYPE_INT_RGB
|
);
|
Graphics2D g = rgbImage.createGraphics();
|
try {
|
g.drawImage(image, 0, 0, null);
|
} finally {
|
g.dispose();
|
}
|
|
return rgbImage;
|
}
|
|
/**
|
* 将文件名转换为JPEG格式
|
*
|
* @param originalFilename 原始文件名
|
* @return JPEG文件名
|
*/
|
private static String getJpegFilename(String originalFilename) {
|
if (originalFilename == null || originalFilename.isEmpty()) {
|
return "image.jpg";
|
}
|
|
return originalFilename.replaceAll("\\.(png|PNG|gif|GIF|bmp|BMP|webp|WEBP)$", ".jpg");
|
}
|
|
/**
|
* 保存MultipartFile到临时文件
|
*
|
* @param file 上传的文件
|
* @return 临时文件
|
* @throws IOException IO异常
|
*/
|
private static File saveToTempFile(MultipartFile file) throws IOException {
|
String tempDir = System.getProperty("java.io.tmpdir");
|
String originalFilename = file.getOriginalFilename();
|
File tempFile = new File(tempDir, System.currentTimeMillis() + "_" + originalFilename);
|
file.transferTo(tempFile);
|
return tempFile;
|
}
|
}
|