Files
Jarvis_java/src/main/java/cn/van/business/service/ImageConvertService.java
2025-11-03 15:29:21 +08:00

353 lines
12 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package cn.van.business.service;
import cn.van.business.model.pl.ImageConversion;
import cn.van.business.repository.ImageConversionRepository;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import net.coobird.thumbnailator.Thumbnails;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.*;
/**
* WebP图片IO支持工具类
* 尝试使用可用的方式读取webp图片
*/
class WebPImageIO {
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(WebPImageIO.class);
private static volatile boolean webpSupported = false;
private static volatile boolean checked = false;
/**
* 检查是否支持webp格式
*/
static synchronized boolean isWebPSupported() {
if (checked) {
return webpSupported;
}
try {
Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName("webp");
webpSupported = readers.hasNext();
if (webpSupported) {
log.info("WebP图片格式支持已启用");
} else {
log.warn("未检测到WebP图片格式支持webp图片转换将被跳过");
}
} catch (Exception e) {
log.warn("检查WebP支持时出错: {}", e.getMessage());
webpSupported = false;
}
checked = true;
return webpSupported;
}
}
/**
* 图片转换服务类
* 负责将webp格式的图片转换为jpg格式并缓存转换结果
*
* @author System
*/
@Slf4j
@Service
public class ImageConvertService {
@Autowired
private ImageConversionRepository imageConversionRepository;
/**
* 图片存储根目录,默认使用系统临时目录
*/
@Value("${image.convert.storage-path:${java.io.tmpdir}/comment-images}")
private String storagePath;
/**
* 图片访问基础URL用于生成转换后图片的访问地址
* 如果为空,则返回本地文件路径
*/
@Value("${image.convert.base-url:}")
private String baseUrl;
/**
* 转换图片URL列表将webp格式转换为jpg
*
* @param imageUrls 原始图片URL列表
* @return 转换后的图片URL列表webp已转换为jpg其他格式保持不变
*/
public List<String> convertImageUrls(List<String> imageUrls) {
if (imageUrls == null || imageUrls.isEmpty()) {
return Collections.emptyList();
}
List<String> convertedUrls = new ArrayList<>();
for (String imageUrl : imageUrls) {
if (StrUtil.isBlank(imageUrl)) {
continue;
}
try {
String convertedUrl = convertImageUrl(imageUrl);
convertedUrls.add(convertedUrl);
} catch (Exception e) {
// 转换失败时使用原URL不中断流程
log.warn("图片转换失败使用原URL: {}. 错误: {}", imageUrl, e.getMessage());
convertedUrls.add(imageUrl);
}
}
return convertedUrls;
}
/**
* 转换单个图片URL
*
* @param originalUrl 原始图片URL
* @return 转换后的图片URL如果不需要转换则返回原URL
* @throws Exception 转换过程中的异常调用方会捕获并返回原URL
*/
private String convertImageUrl(String originalUrl) throws Exception {
if (StrUtil.isBlank(originalUrl)) {
return originalUrl;
}
// 检查是否为webp格式
if (!isWebpFormat(originalUrl)) {
return originalUrl;
}
// 检查系统是否支持webp转换
if (!WebPImageIO.isWebPSupported()) {
log.debug("系统不支持webp格式跳过转换: {}", originalUrl);
throw new IOException("系统不支持webp格式转换");
}
// 检查是否已转换
Optional<ImageConversion> existing = imageConversionRepository.findByOriginalUrl(originalUrl);
if (existing.isPresent()) {
ImageConversion conversion = existing.get();
log.debug("使用缓存的转换结果: {} -> {}", originalUrl, conversion.getConvertedUrl());
return conversion.getConvertedUrl();
}
// 执行转换
String convertedUrl = performConversion(originalUrl);
log.info("图片转换成功: {} -> {}", originalUrl, convertedUrl);
return convertedUrl;
}
/**
* 检查URL是否为webp格式
*
* @param url 图片URL
* @return 是否为webp格式
*/
private boolean isWebpFormat(String url) {
if (StrUtil.isBlank(url)) {
return false;
}
// 检查URL中是否包含.webp
String lowerUrl = url.toLowerCase();
// 检查URL参数或路径中是否包含webp
return lowerUrl.contains(".webp") ||
lowerUrl.contains("format=webp") ||
lowerUrl.contains("?webp") ||
lowerUrl.contains("&webp");
}
/**
* 执行图片转换
*
* @param originalUrl 原始webp图片URL
* @return 转换后的jpg图片URL或路径
*/
private String performConversion(String originalUrl) throws IOException {
// 确保存储目录存在
Path storageDir = Paths.get(storagePath);
if (!Files.exists(storageDir)) {
Files.createDirectories(storageDir);
}
// 生成转换后的文件名基于原URL的MD5
String fileName = generateFileName(originalUrl);
Path outputPath = storageDir.resolve(fileName);
// 如果转换后的文件已存在,直接返回
if (Files.exists(outputPath)) {
String convertedUrl = generateConvertedUrl(fileName);
// 如果数据库中没有记录,保存记录
if (!imageConversionRepository.existsByOriginalUrl(originalUrl)) {
saveConversionRecord(originalUrl, convertedUrl, Files.size(outputPath));
}
return convertedUrl;
}
// 下载原始图片
byte[] imageData = downloadImage(originalUrl);
if (imageData == null || imageData.length == 0) {
throw new IOException("下载图片失败或图片数据为空: " + originalUrl);
}
// 转换图片格式
BufferedImage bufferedImage = null;
try {
// 检查是否支持webp格式
boolean webpSupported = WebPImageIO.isWebPSupported();
if (webpSupported) {
// 如果支持webp尝试使用ImageIO读取
try (ByteArrayInputStream bais = new ByteArrayInputStream(imageData)) {
bufferedImage = ImageIO.read(bais);
}
// 如果ImageIO无法读取尝试使用WebP特定的读取器
if (bufferedImage == null) {
Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName("webp");
if (readers.hasNext()) {
ImageReader reader = readers.next();
try (ByteArrayInputStream bais = new ByteArrayInputStream(imageData);
ImageInputStream iis = ImageIO.createImageInputStream(bais)) {
reader.setInput(iis);
bufferedImage = reader.read(0);
} finally {
reader.dispose();
}
}
}
} else {
// 如果不支持webp尝试使用Thumbnailator转换
// 注意Thumbnailator内部也使用ImageIO所以通常也无法处理webp
try (ByteArrayInputStream bais = new ByteArrayInputStream(imageData)) {
bufferedImage = Thumbnails.of(bais)
.scale(1.0)
.asBufferedImage();
} catch (Exception e) {
log.debug("Thumbnailator无法读取webp图片: {}", e.getMessage());
// 如果无法转换抛出异常后续会返回原URL
throw new IOException("当前系统不支持webp格式转换。图片将保持原格式返回。", e);
}
}
if (bufferedImage == null) {
throw new IOException("无法读取webp图片格式。当前系统不支持webp格式转换。");
}
// 如果是RGBA格式转换为RGB
if (bufferedImage.getType() == BufferedImage.TYPE_4BYTE_ABGR ||
bufferedImage.getType() == BufferedImage.TYPE_INT_ARGB) {
BufferedImage rgbImage = new BufferedImage(
bufferedImage.getWidth(),
bufferedImage.getHeight(),
BufferedImage.TYPE_INT_RGB
);
rgbImage.createGraphics().drawImage(bufferedImage, 0, 0, null);
bufferedImage = rgbImage;
}
// 保存为jpg格式
boolean success = ImageIO.write(bufferedImage, "jpg", outputPath.toFile());
if (!success) {
throw new IOException("无法写入jpg格式");
}
String convertedUrl = generateConvertedUrl(fileName);
long fileSize = Files.size(outputPath);
// 保存转换记录
saveConversionRecord(originalUrl, convertedUrl, fileSize);
log.info("图片转换完成: {} -> {} (大小: {} bytes)", originalUrl, convertedUrl, fileSize);
return convertedUrl;
} finally {
if (bufferedImage != null) {
bufferedImage.flush();
}
}
}
/**
* 下载图片
*
* @param url 图片URL
* @return 图片字节数组
*/
private byte[] downloadImage(String url) {
try {
return HttpUtil.downloadBytes(url);
} catch (Exception e) {
log.error("下载图片失败: {}, 错误: {}", url, e.getMessage());
throw new RuntimeException("下载图片失败: " + e.getMessage(), e);
}
}
/**
* 生成文件名基于原URL的MD5
*
* @param originalUrl 原始URL
* @return 文件名(不含扩展名)
*/
private String generateFileName(String originalUrl) {
// 使用URL的MD5作为文件名避免重复和特殊字符问题
String md5 = cn.hutool.crypto.digest.DigestUtil.md5Hex(originalUrl);
return md5 + ".jpg";
}
/**
* 生成转换后的URL
*
* @param fileName 文件名
* @return 转换后的URL或本地路径
*/
private String generateConvertedUrl(String fileName) {
if (StrUtil.isNotBlank(baseUrl)) {
// 如果配置了baseUrl返回HTTP访问地址
return baseUrl.endsWith("/") ? baseUrl + fileName : baseUrl + "/" + fileName;
} else {
// 否则返回本地文件路径
return Paths.get(storagePath, fileName).toString();
}
}
/**
* 保存转换记录
*
* @param originalUrl 原始URL
* @param convertedUrl 转换后的URL
* @param fileSize 文件大小
*/
private void saveConversionRecord(String originalUrl, String convertedUrl, long fileSize) {
ImageConversion conversion = new ImageConversion();
conversion.setOriginalUrl(originalUrl);
conversion.setConvertedUrl(convertedUrl);
conversion.setFileSize(fileSize);
conversion.setConvertedAt(LocalDateTime.now());
try {
imageConversionRepository.save(conversion);
} catch (Exception e) {
log.warn("保存转换记录失败: {}, 错误: {}", originalUrl, e.getMessage());
// 不抛出异常,因为转换本身已成功
}
}
}