You've already forked FrameTour-BE
- 人脸上传后自动将关联照片添加到优先打印列表 - 根据景区和设备配置自动处理type=2的照片 - 支持按设备分组处理并限制打印数量 - 实现智能图片裁剪功能,支持自动旋转以减少裁切损失 - 添加图片尺寸配置读取和默认值处理 - 完善异常处理确保不影响主流程执行 -优化打印服务中照片上传和裁剪逻辑 - 增加详细的日志记录便于问题追踪
478 lines
16 KiB
Java
478 lines
16 KiB
Java
package com.ycwl.basic.utils;
|
|
|
|
import cn.hutool.core.codec.Base64;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.springframework.web.multipart.MultipartFile;
|
|
|
|
import javax.imageio.ImageIO;
|
|
import java.awt.Graphics2D;
|
|
import java.awt.geom.AffineTransform;
|
|
import java.awt.image.BufferedImage;
|
|
import java.io.ByteArrayInputStream;
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.io.File;
|
|
import java.io.FileOutputStream;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
|
|
@Slf4j
|
|
public class ImageUtils {
|
|
public static MultipartFile base64ToMultipartFile(String base64) {
|
|
String[] baseStrs = base64.split(",");
|
|
byte[] b;
|
|
b = Base64.decode(baseStrs[0]);
|
|
for (int i = 0; i < b.length; ++i) {
|
|
if (b[i] < 0) {
|
|
b[i] += (byte) 256;
|
|
}
|
|
}
|
|
return new Base64DecodedMultipartFile(b, baseStrs[0]);
|
|
}
|
|
|
|
public static MultipartFile cropImage(MultipartFile file, int x, int y, int w, int h) throws IOException {
|
|
BufferedImage image = null;
|
|
BufferedImage targetImage = null;
|
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
try {
|
|
image = ImageIO.read(file.getInputStream());
|
|
log.info("图片宽高:{}", image.getWidth() + "x" + image.getHeight());
|
|
log.info("图片裁切:{}@{}", w + "x" + h, x + "," + y);
|
|
if (image.getWidth() < w) {
|
|
w = image.getWidth();
|
|
}
|
|
if (image.getHeight() < h) {
|
|
h = image.getHeight();
|
|
}
|
|
int targetX = x;
|
|
if (x < 0) {
|
|
targetX = 0;
|
|
} else if ((x + w) > image.getWidth()) {
|
|
targetX = image.getWidth() - w;
|
|
}
|
|
int targetY = y;
|
|
if (y < 0) {
|
|
targetY = 0;
|
|
} else if ((y + h) > image.getHeight()) {
|
|
targetY = image.getHeight() - h;
|
|
}
|
|
log.info("图片实际裁切:{}@{}", w + "x" + h, targetX + "," + targetY);
|
|
targetImage = image.getSubimage(targetX, targetY, w, h);
|
|
ImageIO.write(targetImage, "jpg", baos);
|
|
return new Base64DecodedMultipartFile(baos.toByteArray(), "image/jpeg");
|
|
} finally {
|
|
// 修复内存泄漏:显式释放图片资源
|
|
if (image != null) {
|
|
image.flush();
|
|
image = null;
|
|
}
|
|
if (targetImage != null) {
|
|
targetImage.flush();
|
|
targetImage = null;
|
|
}
|
|
try {
|
|
baos.close();
|
|
} catch (IOException e) {
|
|
log.warn("关闭ByteArrayOutputStream失败", e);
|
|
}
|
|
// 建议JVM进行垃圾回收
|
|
System.gc();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 判断图片是否为横图(宽度大于高度)
|
|
*
|
|
* @param file 图片文件
|
|
* @return true表示横图,false表示竖图
|
|
* @throws IOException 读取文件失败
|
|
*/
|
|
public static boolean isLandscape(File file) throws IOException {
|
|
BufferedImage image = null;
|
|
try {
|
|
image = ImageIO.read(file);
|
|
if (image == null) {
|
|
throw new IOException("无法读取图片文件: " + file.getPath());
|
|
}
|
|
return image.getWidth() > image.getHeight();
|
|
} finally {
|
|
if (image != null) {
|
|
image.flush();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 旋转图片90度(顺时针)
|
|
*
|
|
* @param sourceFile 源图片文件
|
|
* @param targetFile 目标图片文件
|
|
* @throws IOException 读取或写入文件失败
|
|
*/
|
|
public static void rotateImage90(File sourceFile, File targetFile) throws IOException {
|
|
BufferedImage sourceImage = null;
|
|
BufferedImage rotatedImage = null;
|
|
try {
|
|
sourceImage = ImageIO.read(sourceFile);
|
|
if (sourceImage == null) {
|
|
throw new IOException("无法读取图片文件: " + sourceFile.getPath());
|
|
}
|
|
|
|
int width = sourceImage.getWidth();
|
|
int height = sourceImage.getHeight();
|
|
|
|
// 创建旋转后的图片(宽高互换)
|
|
rotatedImage = new BufferedImage(height, width, sourceImage.getType());
|
|
Graphics2D g2d = rotatedImage.createGraphics();
|
|
|
|
// 设置旋转变换
|
|
AffineTransform transform = new AffineTransform();
|
|
transform.translate(height / 2.0, width / 2.0);
|
|
transform.rotate(Math.PI / 2);
|
|
transform.translate(-width / 2.0, -height / 2.0);
|
|
|
|
g2d.setTransform(transform);
|
|
g2d.drawImage(sourceImage, 0, 0, null);
|
|
g2d.dispose();
|
|
|
|
// 保存旋转后的图片
|
|
ImageIO.write(rotatedImage, "jpg", targetFile);
|
|
log.info("图片旋转成功,原始尺寸: {}x{}, 旋转后尺寸: {}x{}", width, height, height, width);
|
|
} finally {
|
|
if (sourceImage != null) {
|
|
sourceImage.flush();
|
|
}
|
|
if (rotatedImage != null) {
|
|
rotatedImage.flush();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 旋转图片270度(逆时针90度)
|
|
*
|
|
* @param sourceFile 源图片文件
|
|
* @param targetFile 目标图片文件
|
|
* @throws IOException 读取或写入文件失败
|
|
*/
|
|
public static void rotateImage270(File sourceFile, File targetFile) throws IOException {
|
|
BufferedImage sourceImage = null;
|
|
BufferedImage rotatedImage = null;
|
|
try {
|
|
sourceImage = ImageIO.read(sourceFile);
|
|
if (sourceImage == null) {
|
|
throw new IOException("无法读取图片文件: " + sourceFile.getPath());
|
|
}
|
|
|
|
int width = sourceImage.getWidth();
|
|
int height = sourceImage.getHeight();
|
|
|
|
// 创建旋转后的图片(宽高互换)
|
|
rotatedImage = new BufferedImage(height, width, sourceImage.getType());
|
|
Graphics2D g2d = rotatedImage.createGraphics();
|
|
|
|
// 设置旋转变换
|
|
AffineTransform transform = new AffineTransform();
|
|
transform.translate(height / 2.0, width / 2.0);
|
|
transform.rotate(-Math.PI / 2);
|
|
transform.translate(-width / 2.0, -height / 2.0);
|
|
|
|
g2d.setTransform(transform);
|
|
g2d.drawImage(sourceImage, 0, 0, null);
|
|
g2d.dispose();
|
|
|
|
// 保存旋转后的图片
|
|
ImageIO.write(rotatedImage, "jpg", targetFile);
|
|
log.info("图片旋转成功,原始尺寸: {}x{}, 旋转后尺寸: {}x{}", width, height, height, width);
|
|
} finally {
|
|
if (sourceImage != null) {
|
|
sourceImage.flush();
|
|
}
|
|
if (rotatedImage != null) {
|
|
rotatedImage.flush();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 智能裁切图片以填充目标尺寸,支持自动旋转以减少裁切损失
|
|
*
|
|
* @param imageSource 图片源,可以是URL字符串或File对象
|
|
* @param targetWidth 目标宽度
|
|
* @param targetHeight 目标高度
|
|
* @return 处理后的临时文件
|
|
* @throws IOException 读取或处理图片失败
|
|
* @throws IllegalArgumentException 参数无效
|
|
*/
|
|
public static File smartCropAndFill(Object imageSource, int targetWidth, int targetHeight) throws IOException {
|
|
if (targetWidth <= 0 || targetHeight <= 0) {
|
|
throw new IllegalArgumentException("目标宽高必须大于0");
|
|
}
|
|
|
|
BufferedImage originalImage = loadImage(imageSource);
|
|
if (originalImage == null) {
|
|
throw new IOException("无法加载图片: " + imageSource);
|
|
}
|
|
|
|
File resultFile = null;
|
|
BufferedImage processedImage = null;
|
|
try {
|
|
// 计算最优方案(是否需要旋转以及如何裁切)
|
|
CropStrategy strategy = calculateOptimalCropStrategy(
|
|
originalImage.getWidth(),
|
|
originalImage.getHeight(),
|
|
targetWidth,
|
|
targetHeight
|
|
);
|
|
|
|
log.info("图片处理策略: 原始尺寸={}x{}, 目标尺寸={}x{}, 旋转={}, 裁切损失={}像素",
|
|
originalImage.getWidth(), originalImage.getHeight(),
|
|
targetWidth, targetHeight,
|
|
strategy.rotationDegrees, strategy.pixelsLost);
|
|
|
|
// 如果需要旋转,先旋转图片
|
|
BufferedImage workingImage = originalImage;
|
|
if (strategy.rotationDegrees != 0) {
|
|
workingImage = rotateImage(originalImage, strategy.rotationDegrees);
|
|
}
|
|
|
|
// 执行居中裁切并缩放
|
|
processedImage = cropAndResize(workingImage, targetWidth, targetHeight);
|
|
|
|
// 保存到临时文件
|
|
resultFile = File.createTempFile("smartcrop_", ".jpg");
|
|
ImageIO.write(processedImage, "jpg", resultFile);
|
|
|
|
log.info("图片处理完成,输出文件: {}", resultFile.getAbsolutePath());
|
|
return resultFile;
|
|
|
|
} finally {
|
|
if (originalImage != null) {
|
|
originalImage.flush();
|
|
}
|
|
if (processedImage != null) {
|
|
processedImage.flush();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 从URL或File加载图片
|
|
*/
|
|
private static BufferedImage loadImage(Object imageSource) throws IOException {
|
|
if (imageSource instanceof String) {
|
|
String urlStr = (String) imageSource;
|
|
if (urlStr.startsWith("http://") || urlStr.startsWith("https://")) {
|
|
// 从URL加载
|
|
java.net.URL url = new java.net.URL(urlStr);
|
|
return ImageIO.read(url);
|
|
} else {
|
|
// 作为文件路径处理
|
|
return ImageIO.read(new File(urlStr));
|
|
}
|
|
} else if (imageSource instanceof File) {
|
|
return ImageIO.read((File) imageSource);
|
|
} else {
|
|
throw new IllegalArgumentException("图片源必须是String(URL或路径)或File对象,实际类型: " +
|
|
(imageSource != null ? imageSource.getClass().getName() : "null"));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 计算最优裁切策略
|
|
*/
|
|
private static CropStrategy calculateOptimalCropStrategy(
|
|
int srcWidth, int srcHeight, int targetWidth, int targetHeight) {
|
|
|
|
CropStrategy best = new CropStrategy();
|
|
best.rotationDegrees = 0;
|
|
best.pixelsLost = Integer.MAX_VALUE;
|
|
|
|
// 测试三种情况: 不旋转、旋转90度、旋转270度
|
|
int[][] scenarios = {
|
|
{0, srcWidth, srcHeight}, // 不旋转
|
|
{90, srcHeight, srcWidth}, // 旋转90度
|
|
{270, srcHeight, srcWidth} // 旋转270度
|
|
};
|
|
|
|
for (int[] scenario : scenarios) {
|
|
int rotation = scenario[0];
|
|
int currentWidth = scenario[1];
|
|
int currentHeight = scenario[2];
|
|
|
|
// 计算裁切损失
|
|
double srcRatio = (double) currentWidth / currentHeight;
|
|
double targetRatio = (double) targetWidth / targetHeight;
|
|
|
|
int pixelsLost;
|
|
if (srcRatio > targetRatio) {
|
|
// 源图更宽,需要裁掉左右两边
|
|
int neededWidth = (int) Math.ceil(currentHeight * targetRatio);
|
|
pixelsLost = (currentWidth - neededWidth) * currentHeight;
|
|
} else {
|
|
// 源图更高,需要裁掉上下两边
|
|
int neededHeight = (int) Math.ceil(currentWidth / targetRatio);
|
|
pixelsLost = (currentHeight - neededHeight) * currentWidth;
|
|
}
|
|
|
|
if (pixelsLost < best.pixelsLost) {
|
|
best.rotationDegrees = rotation;
|
|
best.pixelsLost = pixelsLost;
|
|
}
|
|
}
|
|
|
|
return best;
|
|
}
|
|
|
|
/**
|
|
* 旋转图片
|
|
*/
|
|
private static BufferedImage rotateImage(BufferedImage source, int degrees) {
|
|
if (degrees == 0) {
|
|
return source;
|
|
}
|
|
|
|
int width = source.getWidth();
|
|
int height = source.getHeight();
|
|
|
|
// 90度和270度会交换宽高
|
|
BufferedImage rotated;
|
|
Graphics2D g2d = null;
|
|
|
|
try {
|
|
if (degrees == 90 || degrees == 270) {
|
|
rotated = new BufferedImage(height, width, source.getType());
|
|
g2d = rotated.createGraphics();
|
|
|
|
AffineTransform transform = new AffineTransform();
|
|
if (degrees == 90) {
|
|
transform.translate(height / 2.0, width / 2.0);
|
|
transform.rotate(Math.PI / 2);
|
|
transform.translate(-width / 2.0, -height / 2.0);
|
|
} else { // 270度
|
|
transform.translate(height / 2.0, width / 2.0);
|
|
transform.rotate(-Math.PI / 2);
|
|
transform.translate(-width / 2.0, -height / 2.0);
|
|
}
|
|
|
|
g2d.setTransform(transform);
|
|
g2d.drawImage(source, 0, 0, null);
|
|
} else {
|
|
throw new IllegalArgumentException("仅支持90度和270度旋转");
|
|
}
|
|
|
|
return rotated;
|
|
} finally {
|
|
if (g2d != null) {
|
|
g2d.dispose();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 居中裁切并缩放到目标尺寸
|
|
*/
|
|
private static BufferedImage cropAndResize(BufferedImage source, int targetWidth, int targetHeight) {
|
|
int srcWidth = source.getWidth();
|
|
int srcHeight = source.getHeight();
|
|
|
|
// 计算裁切区域(居中)
|
|
double srcRatio = (double) srcWidth / srcHeight;
|
|
double targetRatio = (double) targetWidth / targetHeight;
|
|
|
|
int cropX, cropY, cropWidth, cropHeight;
|
|
|
|
if (srcRatio > targetRatio) {
|
|
// 源图更宽,裁掉左右
|
|
cropHeight = srcHeight;
|
|
cropWidth = (int) Math.round(srcHeight * targetRatio);
|
|
cropX = (srcWidth - cropWidth) / 2;
|
|
cropY = 0;
|
|
} else {
|
|
// 源图更高,裁掉上下
|
|
cropWidth = srcWidth;
|
|
cropHeight = (int) Math.round(srcWidth / targetRatio);
|
|
cropX = 0;
|
|
cropY = (srcHeight - cropHeight) / 2;
|
|
}
|
|
|
|
// 裁切
|
|
BufferedImage cropped = source.getSubimage(cropX, cropY, cropWidth, cropHeight);
|
|
|
|
// 如果裁切后的尺寸与目标尺寸不完全一致,进行缩放
|
|
if (cropWidth != targetWidth || cropHeight != targetHeight) {
|
|
BufferedImage resized = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB);
|
|
Graphics2D g2d = resized.createGraphics();
|
|
try {
|
|
g2d.drawImage(cropped, 0, 0, targetWidth, targetHeight, null);
|
|
return resized;
|
|
} finally {
|
|
g2d.dispose();
|
|
cropped.flush();
|
|
}
|
|
}
|
|
|
|
return cropped;
|
|
}
|
|
|
|
/**
|
|
* 裁切策略内部类
|
|
*/
|
|
private static class CropStrategy {
|
|
int rotationDegrees; // 0, 90, 或 270
|
|
int pixelsLost; // 损失的像素数
|
|
}
|
|
|
|
public static class Base64DecodedMultipartFile implements MultipartFile {
|
|
|
|
private final byte[] imgContent;
|
|
private final String header;
|
|
|
|
public Base64DecodedMultipartFile(byte[] imgContent, String header) {
|
|
this.imgContent = imgContent;
|
|
this.header = header.split(";")[0];
|
|
}
|
|
|
|
@Override
|
|
public String getName() {
|
|
return System.currentTimeMillis() + Math.random() + "." + header.split("/")[1];
|
|
}
|
|
|
|
@Override
|
|
public String getOriginalFilename() {
|
|
return System.currentTimeMillis() + "." + header.split("/")[1];
|
|
}
|
|
|
|
@Override
|
|
public String getContentType() {
|
|
return "";
|
|
}
|
|
|
|
@Override
|
|
public boolean isEmpty() {
|
|
return imgContent == null || imgContent.length == 0;
|
|
}
|
|
|
|
@Override
|
|
public long getSize() {
|
|
return imgContent.length;
|
|
}
|
|
|
|
@Override
|
|
public byte[] getBytes() {
|
|
return imgContent;
|
|
}
|
|
|
|
@Override
|
|
public InputStream getInputStream() {
|
|
return new ByteArrayInputStream(imgContent);
|
|
}
|
|
|
|
@Override
|
|
public void transferTo(File dest) throws IOException, IllegalStateException {
|
|
new FileOutputStream(dest).write(imgContent);
|
|
}
|
|
|
|
}
|
|
|
|
}
|