package com.ruoyi.app.utils; import com.ruoyi.common.config.RuoYiConfig; import com.ruoyi.common.constant.Constants; import com.ruoyi.common.utils.StringUtils; import net.coobird.thumbnailator.Thumbnails; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.nio.file.Paths; /** * 图片压缩工具类 - 使用Thumbnailator实现 * * @author ruoyi */ public class ImageCompressUtils { private static final Logger log = LoggerFactory.getLogger(ImageCompressUtils.class); /** * 目标文件大小(200KB) */ private static final long TARGET_SIZE = 100 * 1024; // 200KB = 200 * 1024 字节 /** * 压缩图片 - 根据Web访问路径 * * @param webPath Web访问路径 (如: /profile/upload/2025/08/05/image.jpg) * @return 压缩后的Web访问路径,如果不需要压缩则返回原路径 */ public static String compressImageByWebPath(String webPath) { log.info("开始处理Web路径图片压缩: {}", webPath); System.out.println("开始处理Web路径图片压缩: " + webPath); if (StringUtils.isEmpty(webPath)) { log.warn("Web路径为空,直接返回"); System.out.println("Web路径为空,直接返回"); return webPath; } try { // 将Web路径转换为本地文件路径 String localPath = convertWebPathToLocalPath(webPath); log.debug("Web路径转换为本地路径: {} -> {}", webPath, localPath); System.out.println("Web路径转换为本地路径: " + webPath + " -> " + localPath); if (StringUtils.isEmpty(localPath)) { log.warn("无法转换Web路径到本地路径"); System.out.println("无法转换Web路径到本地路径"); return webPath; } File originalFile = new File(localPath); if (!originalFile.exists() || !originalFile.isFile()) { log.warn("文件不存在或不是文件: {}", localPath); System.out.println("文件不存在或不是文件: " + localPath); return webPath; } // 如果文件小于等于200KB,直接返回原路径 if (originalFile.length() <= TARGET_SIZE) { log.info("文件大小 {} 字节,小于等于目标大小,无需压缩", originalFile.length()); System.out.println("文件大小 " + originalFile.length() + " 字节,小于等于目标大小,无需压缩"); return webPath; } // 生成压缩后的本地文件路径 String compressedLocalPath = generateCompressedLocalPath(localPath); log.debug("生成压缩后本地路径: {}", compressedLocalPath); System.out.println("生成压缩后本地路径: " + compressedLocalPath); // 执行压缩 log.info("开始压缩图片: {} -> {}", localPath, compressedLocalPath); System.out.println("开始压缩图片: " + localPath + " -> " + compressedLocalPath); if (compressImageWithThumbnailator(localPath, compressedLocalPath)) { // 将压缩后的本地路径转换为Web访问路径 String compressedWebPath = convertLocalPathToWebPath(compressedLocalPath); // String compressedWebPath = compressedLocalPath; log.info("图片压缩成功: {} -> {}", webPath, compressedWebPath); System.out.println("图片压缩成功: " + webPath + " -> " + compressedWebPath); return compressedWebPath; } else { log.warn("图片压缩失败,返回原始路径: {}", webPath); System.out.println("图片压缩失败,返回原始路径: " + webPath); return webPath; } } catch (Exception e) { log.error("处理Web路径图片压缩时发生异常: " + webPath, e); System.err.println("处理Web路径图片压缩时发生异常: " + webPath + ", 错误: " + e.getMessage()); e.printStackTrace(); return webPath; } finally { // 建议垃圾回收 System.gc(); } } /** * 压缩图片 - 根据本地文件路径 * * @param localPath 本地文件路径 * @return 压缩后的本地文件路径,如果不需要压缩则返回原路径 */ public static String compressImageByLocalPath(String localPath) { log.info("开始处理本地路径图片压缩: {}", localPath); System.out.println("开始处理本地路径图片压缩: " + localPath); if (StringUtils.isEmpty(localPath)) { log.warn("本地路径为空,直接返回"); System.out.println("本地路径为空,直接返回"); return localPath; } File originalFile = new File(localPath); if (!originalFile.exists() || !originalFile.isFile()) { log.warn("文件不存在或不是文件: {}", localPath); System.out.println("文件不存在或不是文件: " + localPath); return localPath; } // 如果文件小于等于200KB,直接返回原路径 if (originalFile.length() <= TARGET_SIZE) { log.info("文件大小 {} 字节,小于等于目标大小 {} 字节,无需压缩", originalFile.length(), TARGET_SIZE); System.out.println("文件大小 " + originalFile.length() + " 字节,小于等于目标大小 " + TARGET_SIZE + " 字节,无需压缩"); return localPath; } try { // 生成压缩后的文件路径 String compressedPath = generateCompressedLocalPath(localPath); log.debug("生成压缩后路径: {}", compressedPath); System.out.println("生成压缩后路径: " + compressedPath); // 执行压缩 log.info("开始压缩图片: {} -> {}", localPath, compressedPath); System.out.println("开始压缩图片: " + localPath + " -> " + compressedPath); if (compressImageWithThumbnailator(localPath, compressedPath)) { log.info("本地图片压缩成功: {} -> {}", localPath, compressedPath); System.out.println("本地图片压缩成功: " + localPath + " -> " + compressedPath); return compressedPath; } else { log.warn("本地图片压缩失败,返回原始路径: {}", localPath); System.out.println("本地图片压缩失败,返回原始路径: " + localPath); return localPath; } } catch (Exception e) { log.error("处理本地路径图片压缩时发生异常: " + localPath, e); System.err.println("处理本地路径图片压缩时发生异常: " + localPath + ", 错误: " + e.getMessage()); e.printStackTrace(); return localPath; } finally { System.gc(); } } /** * 压缩图片 - 简单方法,传入图片路径,返回压缩后的路径 * * @param imagePath 图片路径 * @return 压缩后的图片路径,如果不需要压缩则返回原路径 */ public static String compressImage(String imagePath) { log.info("开始压缩图片: {}", imagePath); System.out.println("开始压缩图片: " + imagePath); if (StringUtils.isEmpty(imagePath)) { log.warn("图片路径为空"); System.out.println("图片路径为空"); return imagePath; } // 检查文件是否存在 File imageFile = new File(imagePath); if (!imageFile.exists() || !imageFile.isFile()) { log.warn("图片文件不存在: {}", imagePath); System.out.println("图片文件不存在: " + imagePath); return imagePath; } // 获取文件大小 long fileSize = imageFile.length(); long fileSizeKB = fileSize / 1024; log.info("图片文件大小: {} 字节 ({} KB)", fileSize, fileSizeKB); System.out.println("图片文件大小: " + fileSize + " 字节 (" + fileSizeKB + " KB)"); // 如果文件小于等于200KB,直接返回原路径 if (fileSize <= TARGET_SIZE) { log.info("文件大小 {} KB,小于等于200KB,无需压缩", fileSizeKB); System.out.println("文件大小 " + fileSizeKB + " KB,小于等于200KB,无需压缩"); return imagePath; } log.info("文件大小 {} KB,大于200KB,开始压缩", fileSizeKB); System.out.println("文件大小 " + fileSizeKB + " KB,大于200KB,开始压缩"); // 调用本地路径压缩方法 return compressImageByLocalPath(imagePath); } /** * 将Web访问路径转换为本地文件路径 * * @param webPath Web路径 (如: /profile/upload/2025/08/05/image.jpg) * @return 本地文件路径 (如: /www/wwwroot/server/uploadPath/upload/2025/08/05/image.jpg) */ private static String convertWebPathToLocalPath(String webPath) { log.debug("转换Web路径到本地路径: {}", webPath); System.out.println("转换Web路径到本地路径: " + webPath); if (StringUtils.isEmpty(webPath)) { return null; } // 检查是否是有效的资源路径 if (!webPath.startsWith(Constants.RESOURCE_PREFIX + "/")) { log.warn("不是有效的资源路径: {}", webPath); System.out.println("不是有效的资源路径: " + webPath); return null; } // 移除资源前缀,获取相对路径 String relativePath = webPath.substring(Constants.RESOURCE_PREFIX.length() + 1); log.debug("相对路径: {}", relativePath); System.out.println("相对路径: " + relativePath); // 策略1: 使用配置的profile路径 String profilePath = RuoYiConfig.getProfile(); String localPath = Paths.get(profilePath, relativePath).toString(); log.debug("策略1 - 使用profile路径: {} -> {}", profilePath, localPath); System.out.println("策略1 - 使用profile路径: " + profilePath + " -> " + localPath); // 检查文件是否存在 File file = new File(localPath); if (file.exists()) { log.debug("策略1成功 - 文件存在: {}", localPath); System.out.println("策略1成功 - 文件存在: " + localPath); return localPath; } else { log.debug("策略1失败 - 文件不存在: {}", localPath); System.out.println("策略1失败 - 文件不存在: " + localPath); } return null; } /** * 将本地文件路径转换为Web访问路径 * * @param localPath 本地文件路径 * @return Web访问路径 */ private static String convertLocalPathToWebPath(String localPath) { // 步骤1:统一替换路径分隔符为正斜杠(兼容所有系统) if (StringUtils.isEmpty(localPath)) return null; String normalizedPath = localPath.replace("\\", "/"); // 关键点1:统一使用/ log.debug("转换本地路径到Web路径: {}", normalizedPath); String profilePath = RuoYiConfig.getProfile(); if (profilePath != null) { profilePath = profilePath.replace("\\", "/"); // 关键点2:配置路径也统一 } // 步骤2:优化路径匹配逻辑(使用统一的分隔符) String webPath = null; // 策略1: 检查配置目录 if (profilePath != null && normalizedPath.startsWith(profilePath)) { int startIndex = profilePath.endsWith("/") ? profilePath.length() : profilePath.length() + 1; String relativePath = normalizedPath.substring(startIndex); webPath = Constants.RESOURCE_PREFIX + "/" + relativePath; log.debug("策略1成功 - Web路径: {}", webPath); } // 策略2: 处理含upload的路径 else if (normalizedPath.contains("/upload/")) { int uploadIndex = normalizedPath.indexOf("/upload/") + 1; webPath = Constants.RESOURCE_PREFIX + "/" + normalizedPath.substring(uploadIndex); log.debug("策略2成功 - Web路径: {}", webPath); } // 策略3: 兜底方案(文件名直用) else { int lastSlash = normalizedPath.lastIndexOf("/"); String fileName = (lastSlash != -1) ? normalizedPath.substring(lastSlash + 1) : normalizedPath; webPath = Constants.RESOURCE_PREFIX + "/upload/" + fileName; log.debug("策略3成功 - Web路径: {}", webPath); } // 步骤3:确保结果有效性 if (webPath != null) { log.info("转换成功: {} → {}", normalizedPath, webPath); return webPath; } else { log.error("转换失败: {}", normalizedPath); return null; } } /** * 使用Thumbnailator压缩图片 * * @param inputPath 输入路径 * @param outputPath 输出路径 * @return 是否压缩成功 */ private static boolean compressImageWithThumbnailator(String inputPath, String outputPath) { log.info("使用Thumbnailator压缩图片: {} -> {}", inputPath, outputPath); System.out.println("使用Thumbnailator压缩图片: " + inputPath + " -> " + outputPath); try { // 确保输出目录存在 File outputFile = new File(outputPath); File parentDir = outputFile.getParentFile(); if (!parentDir.exists()) { log.debug("创建输出目录: {}", parentDir.getAbsolutePath()); System.out.println("创建输出目录: " + parentDir.getAbsolutePath()); parentDir.mkdirs(); } // 使用二分法查找合适的压缩质量 log.debug("查找最优压缩质量"); System.out.println("查找最优压缩质量"); float quality = findOptimalQuality(inputPath, outputPath); // 执行最终压缩 log.debug("执行最终压缩,质量: {}", quality); System.out.println("执行最终压缩,质量: " + quality); Thumbnails.of(inputPath) .scale(1.0f) // 保持原尺寸 .outputQuality(quality) // 设置压缩质量 .toFile(outputPath); // 检查压缩后的文件大小 File compressedFile = new File(outputPath); boolean success = compressedFile.exists() && compressedFile.length() <= TARGET_SIZE; log.info("压缩完成,文件存在: {}, 文件大小: {} 字节, 压缩成功: {}", compressedFile.exists(), compressedFile.length(), success); System.out.println("压缩完成,文件存在: " + compressedFile.exists() + ", 文件大小: " + compressedFile.length() + " 字节, 压缩成功: " + success); return success; } catch (IOException e) { log.error("压缩图片时发生IO异常: " + inputPath, e); System.err.println("压缩图片时发生IO异常: " + inputPath + ", 错误: " + e.getMessage()); e.printStackTrace(); return false; } } /** * 使用二分法查找最优压缩质量 */ private static float findOptimalQuality(String inputPath, String outputPath) { log.debug("使用二分法查找最优压缩质量: {}", inputPath); System.out.println("使用二分法查找最优压缩质量: " + inputPath); float minQuality = 0.1f; float maxQuality = 1.0f; float bestQuality = 0.8f; // 创建临时文件用于测试 String tempPath = generateTempPath(outputPath); log.debug("创建临时文件用于测试: {}", tempPath); System.out.println("创建临时文件用于测试: " + tempPath); try { // 确保临时文件目录存在 File tempFile = new File(tempPath); File parentDir = tempFile.getParentFile(); if (!parentDir.exists()) { log.debug("创建临时文件目录: {}", parentDir.getAbsolutePath()); System.out.println("创建临时文件目录: " + parentDir.getAbsolutePath()); parentDir.mkdirs(); } // 二分查找最优质量值 for (int i = 0; i < 10; i++) { // 减少迭代次数,提高效率 float testQuality = (minQuality + maxQuality) / 2; log.debug("第{}次测试,压缩质量: {}", i + 1, testQuality); System.out.println("第" + (i + 1) + "次测试,压缩质量: " + testQuality); try { // 确保临时文件不存在 if (tempFile.exists()) { tempFile.delete(); } // 测试当前质量的文件大小 Thumbnails.of(inputPath) .scale(1.0f) .outputQuality(testQuality) .toFile(tempPath); // 等待文件写入完成 Thread.sleep(100); long fileSize = tempFile.length(); log.debug("测试结果 - 文件大小: {} 字节, 目标大小: {} 字节", fileSize, TARGET_SIZE); System.out.println("测试结果 - 文件大小: " + fileSize + " 字节, 目标大小: " + TARGET_SIZE + " 字节"); if (fileSize <= TARGET_SIZE) { bestQuality = testQuality; minQuality = testQuality; log.debug("文件大小符合要求,调整最小质量为: {}", minQuality); System.out.println("文件大小符合要求,调整最小质量为: " + minQuality); } else { maxQuality = testQuality; log.debug("文件大小超过目标,调整最大质量为: {}", maxQuality); System.out.println("文件大小超过目标,调整最大质量为: " + maxQuality); } // 如果已经很接近目标大小,提前退出 if (Math.abs(fileSize - TARGET_SIZE) < 10240) { // 10KB的容差 bestQuality = testQuality; log.info("已接近目标大小,提前退出,最优质量: {}", bestQuality); System.out.println("已接近目标大小,提前退出,最优质量: " + bestQuality); break; } // 如果质量范围已经很小,提前退出 if (Math.abs(maxQuality - minQuality) < 0.05f) { log.info("质量范围已经很小,提前退出,最优质量: {}", bestQuality); System.out.println("质量范围已经很小,提前退出,最优质量: " + bestQuality); break; } } catch (IOException e) { log.warn("测试压缩质量 {} 时出现异常: {}", testQuality, e.getMessage()); System.out.println("测试压缩质量 " + testQuality + " 时出现异常: " + e.getMessage()); // 如果当前质量测试失败,调整范围继续测试 maxQuality = testQuality; continue; } catch (InterruptedException e) { log.warn("等待文件写入时被中断"); System.out.println("等待文件写入时被中断"); Thread.currentThread().interrupt(); break; } // 删除临时文件 if (tempFile.exists()) { tempFile.delete(); } } } catch (Exception e) { log.error("查找最优压缩质量时发生异常", e); System.err.println("查找最优压缩质量时发生异常: " + e.getMessage()); e.printStackTrace(); // 如果出错,使用默认质量 bestQuality = 0.6f; } finally { // 清理可能残留的临时文件 File tempFile = new File(tempPath); if (tempFile.exists()) { log.debug("清理残留临时文件: {}", tempPath); System.out.println("清理残留临时文件: " + tempPath); tempFile.delete(); } } log.info("最优压缩质量查找完成: {}", bestQuality); System.out.println("最优压缩质量查找完成: " + bestQuality); return bestQuality; } /** * 生成临时文件路径,保持正确的图片扩展名 * * @param outputPath 输出文件路径 * @return 临时文件路径 */ private static String generateTempPath(String outputPath) { File outputFile = new File(outputPath); String fileName = outputFile.getName(); String parentDir = outputFile.getParent(); int dotIndex = fileName.lastIndexOf('.'); if (dotIndex == -1) { // 没有扩展名,默认添加.jpg return Paths.get(parentDir, fileName + "_comp.jpg").toString(); } else { String nameWithoutExt = fileName.substring(0, dotIndex); String extension = fileName.substring(dotIndex); return Paths.get(parentDir, nameWithoutExt + "_comp" + extension).toString(); } } /** * 生成压缩后的本地文件路径(在同一目录下) * * @param originalPath 原文件路径 * @return 压缩后的文件路径 */ private static String generateCompressedLocalPath(String originalPath) { log.debug("生成压缩后本地文件路径: {}", originalPath); System.out.println("生成压缩后本地文件路径: " + originalPath); File originalFile = new File(originalPath); String parentDir = originalFile.getParent(); String fileName = originalFile.getName(); int dotIndex = fileName.lastIndexOf('.'); String compressedPath; if (dotIndex == -1) { compressedPath = Paths.get(parentDir, fileName + "_comp.jpg").toString(); } else { String nameWithoutExt = fileName.substring(0, dotIndex); String extension = fileName.substring(dotIndex); // 对于PNG文件,转换为JPG以获得更好的压缩效果 if (".png".equalsIgnoreCase(extension)) { extension = ".jpg"; } String compressedFileName = nameWithoutExt + "_comp" + extension; compressedPath = Paths.get(parentDir, compressedFileName).toString(); } log.debug("生成的压缩文件路径: {}", compressedPath); System.out.println("生成的压缩文件路径: " + compressedPath); return compressedPath; } /** * 批量压缩图片(Web路径) * * @param webPaths Web访问路径数组 * @return 压缩后的Web访问路径数组 */ public static String[] compressImagesByWebPath(String[] webPaths) { log.info("批量压缩图片,数量: {}", webPaths != null ? webPaths.length : 0); System.out.println("批量压缩图片,数量: " + (webPaths != null ? webPaths.length : 0)); if (webPaths == null) { log.warn("Web路径数组为空"); System.out.println("Web路径数组为空"); return null; } String[] compressedPaths = new String[webPaths.length]; for (int i = 0; i < webPaths.length; i++) { log.debug("批量压缩第{}张图片: {}", i + 1, webPaths[i]); System.out.println("批量压缩第" + (i + 1) + "张图片: " + webPaths[i]); compressedPaths[i] = compressImageByWebPath(webPaths[i]); } log.info("批量压缩完成"); System.out.println("批量压缩完成"); return compressedPaths; } /** * 检查路径是否为若依资源路径 * * @param path 路径 * @return 是否为资源路径 */ public static boolean isResourcePath(String path) { boolean result = StringUtils.isNotEmpty(path) && path.startsWith(Constants.RESOURCE_PREFIX + "/"); log.debug("检查是否为资源路径: {} -> {}", path, result); System.out.println("检查是否为资源路径: " + path + " -> " + result); return result; } /** * 获取文件大小(字节) * * @param webPath Web访问路径 * @return 文件大小,如果文件不存在返回-1 */ public static long getFileSize(String webPath) { log.debug("获取文件大小: {}", webPath); System.out.println("获取文件大小: " + webPath); String localPath = convertWebPathToLocalPath(webPath); if (StringUtils.isEmpty(localPath)) { log.warn("无法转换Web路径到本地路径"); System.out.println("无法转换Web路径到本地路径"); return -1; } File file = new File(localPath); long fileSize = file.exists() ? file.length() : -1; log.debug("文件大小: {} 字节", fileSize); System.out.println("文件大小: " + fileSize + " 字节"); return fileSize; } }