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(); } } } /** * 旋转图片180度 * * @param sourceFile 源图片文件 * @param targetFile 目标图片文件 * @throws IOException 读取或写入文件失败 */ public static void rotateImage180(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(width, height, sourceImage.getType()); Graphics2D g2d = rotatedImage.createGraphics(); // 设置旋转变换 AffineTransform transform = new AffineTransform(); transform.translate(width / 2.0, height / 2.0); transform.rotate(Math.PI); 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("图片旋转180度成功,尺寸保持: {}x{}", width, height); } 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.replace("oss.zhentuai.com", "frametour-assets.oss-cn-shanghai-internal.aliyuncs.com")); 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; // 测试两种情况: 不旋转、旋转270度 int[][] scenarios = { {0, srcWidth, srcHeight}, // 不旋转 {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(); // 270度会交换宽高 BufferedImage rotated = new BufferedImage(height, width, source.getType()); Graphics2D g2d = null; try { g2d = rotated.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(source, 0, 0, null); 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; } /** * 为图片添加白边框并向上偏移内容 * 用于拼图打印场景,避免打印机偏移问题 * * @param sourceFile 源图片文件 * @param targetFile 目标图片文件 * @param horizontalBorder 左右白边框宽度(像素) * @param verticalBorder 上下白边框高度(像素) * @param upwardShift 内容向上偏移的像素数 * @throws IOException 读取或写入文件失败 */ public static void addBorderAndShiftUp(File sourceFile, File targetFile, int horizontalBorder, int verticalBorder, int upwardShift) throws IOException { BufferedImage sourceImage = null; BufferedImage resultImage = null; try { sourceImage = ImageIO.read(sourceFile); if (sourceImage == null) { throw new IOException("无法读取图片文件: " + sourceFile.getPath()); } int srcWidth = sourceImage.getWidth(); int srcHeight = sourceImage.getHeight(); // 计算新图片尺寸(原图 + 左右边框 + 上下边框) int newWidth = srcWidth + horizontalBorder * 2; int newHeight = srcHeight + verticalBorder * 2; // 创建新图片 resultImage = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB); Graphics2D g2d = resultImage.createGraphics(); try { // 填充白色背景 g2d.setColor(java.awt.Color.WHITE); g2d.fillRect(0, 0, newWidth, newHeight); // 绘制原图到新图中 // 原图应该绘制在: x=horizontalBorder, y=verticalBorder-upwardShift 的位置 // 这样图片内容会向上偏移upwardShift像素 int drawX = horizontalBorder; int drawY = verticalBorder - upwardShift; g2d.drawImage(sourceImage, drawX, drawY, null); log.info("图片添加白边框并向上偏移: 原始尺寸={}x{}, 边框=(左右{}px,上下{}px), 向上偏移={}px, 结果尺寸={}x{}", srcWidth, srcHeight, horizontalBorder, verticalBorder, upwardShift, newWidth, newHeight); } finally { g2d.dispose(); } // 保存处理后的图片 ImageIO.write(resultImage, "png", targetFile); } finally { if (sourceImage != null) { sourceImage.flush(); } if (resultImage != null) { resultImage.flush(); } } } /** * 向上偏移图片以避免打印机偏移问题 * 舍弃顶部指定像素,整体向上移动,并在底部补充白底 * * @param sourceFile 源图片文件 * @param targetFile 目标图片文件 * @param offsetPixels 向上偏移的像素数(舍弃顶部的像素数,底部补充相同像素的白底) * @throws IOException 读取或写入文件失败 * @deprecated 使用 addBorderAndShiftUp 代替 */ @Deprecated public static void shiftImageUp(File sourceFile, File targetFile, int offsetPixels) throws IOException { BufferedImage sourceImage = null; BufferedImage shiftedImage = null; try { sourceImage = ImageIO.read(sourceFile); if (sourceImage == null) { throw new IOException("无法读取图片文件: " + sourceFile.getPath()); } int width = sourceImage.getWidth(); int height = sourceImage.getHeight(); if (offsetPixels <= 0 || offsetPixels >= height) { throw new IllegalArgumentException("偏移像素必须大于0且小于图片高度,当前值: " + offsetPixels + ", 图片高度: " + height); } // 创建新图片,保持原始宽度和高度 shiftedImage = new BufferedImage(width, height, sourceImage.getType()); Graphics2D g2d = shiftedImage.createGraphics(); try { // 先填充白色背景 g2d.setColor(java.awt.Color.WHITE); g2d.fillRect(0, 0, width, height); // 从源图的offsetPixels位置开始截取到底部,绘制到目标图的顶部 // 源图: 从(0, offsetPixels)到(width, height)的区域 // 目标图: 绘制到(0, 0)到(width, height-offsetPixels)的区域 g2d.drawImage(sourceImage, 0, 0, width, height - offsetPixels, 0, offsetPixels, width, height, null); // 底部的offsetPixels像素保持白色(已通过fillRect填充) } finally { g2d.dispose(); } // 保存处理后的图片 ImageIO.write(shiftedImage, "png", targetFile); log.info("图片向上偏移成功,原始尺寸: {}x{}, 偏移: {}px, 结果尺寸: {}x{} (底部补充{}px白底)", width, height, offsetPixels, width, height, offsetPixels); } finally { if (sourceImage != null) { sourceImage.flush(); } if (shiftedImage != null) { shiftedImage.flush(); } } } /** * 裁切策略内部类 */ 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); } } }