353 lines
12 KiB
Java
353 lines
12 KiB
Java
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());
|
||
// 不抛出异常,因为转换本身已成功
|
||
}
|
||
}
|
||
}
|
||
|