图像透明处理

This commit is contained in:
spllzh
2025-10-16 23:21:39 +08:00
parent abd370beb5
commit cfcd04fe7e
33 changed files with 4658 additions and 23 deletions

View File

@@ -299,7 +299,7 @@ public class ImageModelServiceImpl extends ServiceImpl<ImageModelMapper, ImageMo
* 上传图片素材
*
* 实现图片素材上传的完整业务逻辑,包括:
* 1. 文件验证和格式检查
* 1. 文件验证和格式检查(根据阿里云参数要求)
* 2. 上传到MinIO存储
* 3. 生成永久URL和临时URL
* 4. 保存上传记录到数据库
@@ -307,17 +307,18 @@ public class ImageModelServiceImpl extends ServiceImpl<ImageModelMapper, ImageMo
* @param file 上传的图片文件
* @param ownerName 所属人姓名
* @param ownerPhone 所属人电话
* @param imageType 图片类型
* @param imageType 图片类型base_image主体图像 或 ref_image引导图像
* @param imageName 图像名称
* @return 图像模型记录包含上传结果和URL信息
*/
@Override
public ImageModel uploadImageMaterial(MultipartFile file, String ownerName, String ownerPhone, String imageType,String imageName) {
public ImageModel uploadImageMaterial(MultipartFile file, String ownerName, String ownerPhone, String imageType, String imageName) {
try {
log.info("开始上传图片素材,文件名:{},所属人:{},电话:{}",
file.getOriginalFilename(), ownerName, ownerPhone);
log.info("开始上传图片素材,文件名:{},所属人:{},电话:{},图像类型:{}",
file.getOriginalFilename(), ownerName, ownerPhone, imageType);
// 1. 文件验证
validateImageFile(file);
// 1. 文件验证(根据阿里云参数要求)
validateImageFile(file, imageType);
// 2. 生成唯一文件名
String originalFilename = file.getOriginalFilename();
@@ -340,9 +341,10 @@ public class ImageModelServiceImpl extends ServiceImpl<ImageModelMapper, ImageMo
imageModel.setCreateTime(LocalDateTime.now());
imageModel.setUpdateTime(LocalDateTime.now());
if ( imageName!=null && !imageName.trim().isEmpty()){
if (imageName != null && !imageName.trim().isEmpty()) {
imageModel.setImageName(imageName);
}
// 5. 保存到数据库
boolean saveResult = this.saveImageModel(imageModel);
if (!saveResult) {
@@ -368,14 +370,265 @@ public class ImageModelServiceImpl extends ServiceImpl<ImageModelMapper, ImageMo
}
/**
* 验证图片文件
* 上传引导图像
*
* 检查文件格式、大小等是否符合要求
* 专门用于上传引导图像的方法自动转换为带透明背景的RGBA四通道图像
* 如果上传的图像不是RGBA格式系统会自动转换为RGBA格式
*
* @param file 上传的引导图像文件
* @param ownerName 所属人姓名
* @param ownerPhone 所属人电话
* @param imageName 图像名称
* @return 图像模型记录包含上传结果和URL信息
*/
@Override
public ImageModel uploadRefImage(MultipartFile file, String ownerName, String ownerPhone, String imageName) {
try {
log.info("开始上传引导图像,文件名:{},所属人:{},电话:{}",
file.getOriginalFilename(), ownerName, ownerPhone);
// 1. 基础文件验证(不限制格式,因为会自动转换)
validateBasicImageFile(file);
// 2. 转换为RGBA格式
log.info("开始转换图像为RGBA格式");
byte[] rgbaImageBytes = convertToRGBA(file);
log.info("图像转换完成RGBA格式大小: {} bytes", rgbaImageBytes.length);
// 3. 生成唯一文件名固定为PNG格式
String uniqueFileName = generateUniqueFileName("png");
// 4. 上传转换后的RGBA图像到MinIO
log.info("开始上传转换后的RGBA图像到MinIO文件名{}", uniqueFileName);
String materialUrl = minIOService.uploadFile(rgbaImageBytes, uniqueFileName, "image/png");
String materialTempUrl = minIOService.generateTempUrl(uniqueFileName);
// 5. 构建图像模型记录
ImageModel imageModel = new ImageModel();
imageModel.setImageName(file.getOriginalFilename());
imageModel.setOwnerName(ownerName);
imageModel.setOwnerPhone(ownerPhone);
imageModel.setImageType("ref_image"); // 设置为引导图像类型
imageModel.setMaterialUrl(materialUrl);
imageModel.setMaterialTempUrl(materialTempUrl);
imageModel.setCreateTime(LocalDateTime.now());
imageModel.setUpdateTime(LocalDateTime.now());
if (imageName != null && !imageName.trim().isEmpty()) {
imageModel.setImageName(imageName);
}
// 6. 保存到数据库
boolean saveResult = this.saveImageModel(imageModel);
if (!saveResult) {
log.error("保存引导图像上传记录失败");
throw new RuntimeException("保存引导图像上传记录失败");
}
log.info("引导图像上传成功已转换为RGBA格式ID: {}永久URL: {}临时URL: {}",
imageModel.getUuid(), materialUrl, materialTempUrl);
return imageModel;
} catch (Exception e) {
log.error("上传引导图像异常", e);
throw new RuntimeException("上传引导图像失败: " + e.getMessage(), e);
}
}
/**
* 图像处理(支持主体图像和引导图像)
*
* 根据阿里云图像参数要求进行图像处理:
* - base_image_url: 主体图像URL必选PNG格式RGBA四通道长边不超过2048像素
* - ref_image_url: 引导图像URL可选PNG格式RGBA四通道长边不超过2048像素
*
* @param baseImageModel 主体图像模型记录
* @param refImageModel 引导图像模型记录(可选)
* @param prompt 处理提示词
* @param functionName 功能名称
* @return 处理后的图像模型记录
*/
@Override
public ImageModel processImageWithRef(ImageModel baseImageModel, ImageModel refImageModel,
String prompt, String functionName) {
try {
log.info("开始图像处理主体图像ID: {}引导图像ID: {},提示词: {},功能: {}",
baseImageModel.getUuid(),
refImageModel != null ? refImageModel.getUuid() : "",
prompt, functionName);
// 1. 验证主体图像
if (baseImageModel.getMaterialTempUrl() == null || baseImageModel.getMaterialTempUrl().trim().isEmpty()) {
throw new IllegalArgumentException("主体图像URL不能为空");
}
String baseImageUrl = baseImageModel.getMaterialTempUrl().trim();
if (!isPublicAccessibleUrl(baseImageUrl)) {
throw new IllegalArgumentException("主体图像URL必须是公网可访问的地址");
}
// 2. 验证引导图像(如果提供)
String refImageUrl = null;
if (refImageModel != null && refImageModel.getMaterialTempUrl() != null &&
!refImageModel.getMaterialTempUrl().trim().isEmpty()) {
refImageUrl = refImageModel.getMaterialTempUrl().trim();
if (!isPublicAccessibleUrl(refImageUrl)) {
throw new IllegalArgumentException("引导图像URL必须是公网可访问的地址");
}
}
// 3. 设置处理参数
baseImageModel.setPrompt(prompt);
baseImageModel.setModelName("wanx2.1-imageedit");
baseImageModel.setFunctionName(functionName);
// 4. 调用阿里云AI模型
log.info("调用阿里云AI模型进行图像处理主体图像URL: {}引导图像URL: {}",
baseImageUrl, refImageUrl);
String apiKey = System.getenv("DASHSCOPE_API_KEY");
Map<String, Object> parameters = new HashMap<>();
parameters.put("prompt_extend", true);
ImageSynthesisParam param = ImageSynthesisParam.builder()
.apiKey(apiKey)
.model(baseImageModel.getModelName())
.function(baseImageModel.getFunctionName())
.prompt(baseImageModel.getPrompt())
.baseImageUrl(baseImageUrl)
.n(1)
.size("1024*1024")
.parameters(parameters)
.build();
ImageSynthesis imageSynthesis = new ImageSynthesis();
ImageSynthesisResult result = null;
try {
log.info("开始同步调用阿里云AI模型参数: {}", param);
result = imageSynthesis.call(param);
log.info("AI模型调用完成结果: {}", JsonUtils.toJson(result));
// 处理结果
if (result != null && result.getOutput() != null && !result.getOutput().getResults().isEmpty()) {
String resultImageUrl = result.getOutput().getResults().get(0).get("url");
if (resultImageUrl != null && !resultImageUrl.trim().isEmpty()) {
log.info("开始下载AI处理结果图片: {}", resultImageUrl);
try {
// 下载图片到字节数组
byte[] imageBytes = downloadImageToBytes(resultImageUrl);
log.info("图片下载成功,大小: {} bytes", imageBytes.length);
// 生成唯一文件名
String resultFileName = generateUniqueFileName("jpg");
// 上传到MinIO
String resultImageMinIOUrl = minIOService.uploadFile(imageBytes, resultFileName, "image/jpeg");
log.info("图片上传到MinIO成功: {}", resultImageMinIOUrl);
// 生成7天临时访问链接
String resultImageTempUrl = minIOService.generateTempUrl(resultFileName);
log.info("生成7天临时访问链接: {}", resultImageTempUrl);
// 更新ImageModel对象
baseImageModel.setResultImageUrl(resultImageMinIOUrl);
baseImageModel.setResultImageTempUrl(resultImageTempUrl);
} catch (Exception e) {
log.error("下载或上传AI处理结果图片失败", e);
throw new RuntimeException("处理结果图片失败: " + e.getMessage(), e);
}
} else {
log.warn("AI处理结果中没有有效的图片URL");
}
} else {
log.warn("AI处理结果为空或无效");
}
} catch (ApiException | NoApiKeyException e) {
log.error("阿里云API调用失败", e);
throw new RuntimeException("AI模型调用失败: " + e.getMessage(), e);
}
// 5. 保存处理记录
boolean saveResult = this.updateById(baseImageModel);
if (!saveResult) {
log.error("保存图像处理记录失败");
throw new RuntimeException("保存图像处理记录失败");
}
log.info("图像处理完成ID: {}结果URL: {}",
baseImageModel.getUuid(), baseImageModel.getResultImageTempUrl());
return baseImageModel;
} catch (Exception e) {
log.error("图像处理异常", e);
throw new RuntimeException("图像处理失败: " + e.getMessage(), e);
}
}
/**
* 基础文件验证
*
* 验证文件的基本要求,不限制格式(用于引导图像自动转换)
*
* @param file 上传的文件
* @throws IllegalArgumentException 文件不符合要求时抛出异常
*/
private void validateImageFile(MultipartFile file) {
private void validateBasicImageFile(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("上传文件不能为空");
}
// 检查文件大小限制为10MB
long maxSize = 10 * 1024 * 1024; // 10MB
if (file.getSize() > maxSize) {
throw new IllegalArgumentException("文件大小不能超过10MB");
}
// 检查文件格式
String contentType = file.getContentType();
if (contentType == null || !contentType.startsWith("image/")) {
throw new IllegalArgumentException("只能上传图片文件");
}
// 检查文件扩展名
String originalFilename = file.getOriginalFilename();
if (originalFilename == null || originalFilename.trim().isEmpty()) {
throw new IllegalArgumentException("文件名不能为空");
}
// 支持常见图像格式
String extension = getFileExtension(originalFilename).toLowerCase();
String[] allowedExtensions = {"jpg", "jpeg", "png", "gif", "bmp", "webp"};
boolean isValidExtension = false;
for (String allowedExt : allowedExtensions) {
if (extension.equals(allowedExt)) {
isValidExtension = true;
break;
}
}
if (!isValidExtension) {
throw new IllegalArgumentException("不支持的图片格式支持的格式jpg, jpeg, png, gif, bmp, webp");
}
}
/**
* 验证图片文件
*
* 根据阿里云图像参数要求进行验证:
* - 主体图像必须为PNG格式带透明背景的RGBA四通道图像
* - 引导图像必须为PNG格式带透明背景的RGBA四通道图像
* - 图像长边不超过2048像素
*
* @param file 上传的文件
* @param imageType 图像类型base_image主体图像 或 ref_image引导图像
* @throws IllegalArgumentException 文件不符合要求时抛出异常
*/
private void validateImageFile(MultipartFile file, String imageType) {
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("上传文件不能为空");
}
@@ -399,17 +652,71 @@ public class ImageModelServiceImpl extends ServiceImpl<ImageModelMapper, ImageMo
}
String extension = getFileExtension(originalFilename).toLowerCase();
String[] allowedExtensions = {"jpg", "jpeg", "png", "gif", "bmp", "webp"};
boolean isValidExtension = false;
for (String allowedExt : allowedExtensions) {
if (extension.equals(allowedExt)) {
isValidExtension = true;
break;
// 根据图像类型进行不同的验证
if ("base_image".equals(imageType)) {
// 主体图像必须是PNG格式
if (!"png".equals(extension)) {
throw new IllegalArgumentException("主体图像必须是PNG格式且为带透明背景的RGBA四通道图像");
}
// 验证图像尺寸长边不超过2048像素
validateImageDimensions(file);
} else if ("ref_image".equals(imageType)) {
// 引导图像必须是PNG格式带透明背景的RGBA四通道图像
if (!"png".equals(extension)) {
throw new IllegalArgumentException("引导图像必须是PNG格式且为带透明背景的RGBA四通道图像");
}
// 验证图像尺寸长边不超过2048像素
validateImageDimensions(file);
} else {
// 默认验证(兼容原有逻辑)
String[] allowedExtensions = {"jpg", "jpeg", "png", "gif", "bmp", "webp"};
boolean isValidExtension = false;
for (String allowedExt : allowedExtensions) {
if (extension.equals(allowedExt)) {
isValidExtension = true;
break;
}
}
if (!isValidExtension) {
throw new IllegalArgumentException("不支持的图片格式支持的格式jpg, jpeg, png, gif, bmp, webp");
}
}
if (!isValidExtension) {
throw new IllegalArgumentException("不支持的图片格式支持的格式jpg, jpeg, png, gif, bmp, webp");
}
/**
* 验证图像尺寸
*
* 检查图像长边是否超过2048像素
*
* @param file 上传的文件
* @throws IllegalArgumentException 图像尺寸不符合要求时抛出异常
*/
private void validateImageDimensions(MultipartFile file) {
try {
// 使用BufferedImage读取图像尺寸
java.awt.image.BufferedImage image = javax.imageio.ImageIO.read(file.getInputStream());
if (image == null) {
throw new IllegalArgumentException("无法读取图像文件");
}
int width = image.getWidth();
int height = image.getHeight();
int maxDimension = Math.max(width, height);
if (maxDimension > 2048) {
throw new IllegalArgumentException("图像长边不能超过2048像素当前尺寸" + width + "x" + height);
}
log.info("图像尺寸验证通过:{}x{}", width, height);
} catch (Exception e) {
log.error("验证图像尺寸失败", e);
throw new IllegalArgumentException("验证图像尺寸失败:" + e.getMessage());
}
}
@@ -440,6 +747,113 @@ public class ImageModelServiceImpl extends ServiceImpl<ImageModelMapper, ImageMo
return timestamp + "_" + uuid + "." + extension;
}
/**
* 验证URL是否为公网可访问的地址
*
* 检查URL是否符合阿里云图像参数要求
* - 必须是HTTP或HTTPS协议
* - 必须是公网可访问的地址
*
* @param url 要验证的URL
* @return 是否为公网可访问的URL
*/
private boolean isPublicAccessibleUrl(String url) {
if (url == null || url.trim().isEmpty()) {
return false;
}
try {
// 检查协议
if (!url.startsWith("http://") && !url.startsWith("https://")) {
log.warn("URL协议不支持必须是HTTP或HTTPS: {}", url);
return false;
}
// 检查是否为内网地址(简单检查)
if (url.contains("localhost") || url.contains("127.0.0.1") ||
url.contains("192.168.") || url.contains("10.") || url.contains("172.")) {
log.warn("URL为内网地址不是公网可访问: {}", url);
return false;
}
// 尝试访问URL验证可访问性
URL urlObj = new URL(url);
try (InputStream inputStream = urlObj.openStream()) {
// 如果能打开流说明URL可访问
log.info("URL验证通过可公网访问: {}", url);
return true;
}
} catch (Exception e) {
log.warn("URL验证失败无法公网访问: {},错误: {}", url, e.getMessage());
return false;
}
}
/**
* 将图像转换为RGBA格式
*
* 将任意格式的图像转换为带透明背景的RGBA四通道PNG格式
*
* @param file 原始图像文件
* @return RGBA格式的图像字节数组
* @throws IOException 转换失败时抛出异常
*/
private byte[] convertToRGBA(MultipartFile file) throws IOException {
try {
log.info("开始转换图像为RGBA格式原始文件: {}", file.getOriginalFilename());
// 读取原始图像
java.awt.image.BufferedImage originalImage = javax.imageio.ImageIO.read(file.getInputStream());
if (originalImage == null) {
throw new IllegalArgumentException("无法读取图像文件");
}
int width = originalImage.getWidth();
int height = originalImage.getHeight();
// 验证图像尺寸
int maxDimension = Math.max(width, height);
if (maxDimension > 2048) {
throw new IllegalArgumentException("图像长边不能超过2048像素当前尺寸" + width + "x" + height);
}
log.info("原始图像尺寸: {}x{}", width, height);
// 创建RGBA格式的图像
java.awt.image.BufferedImage rgbaImage = new java.awt.image.BufferedImage(
width, height, java.awt.image.BufferedImage.TYPE_INT_ARGB);
// 获取图形上下文
java.awt.Graphics2D g2d = rgbaImage.createGraphics();
// 设置渲染提示以获得更好的质量
g2d.setRenderingHint(java.awt.RenderingHints.KEY_INTERPOLATION,
java.awt.RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.setRenderingHint(java.awt.RenderingHints.KEY_RENDERING,
java.awt.RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(java.awt.RenderingHints.KEY_ANTIALIASING,
java.awt.RenderingHints.VALUE_ANTIALIAS_ON);
// 绘制原始图像到RGBA图像上
g2d.drawImage(originalImage, 0, 0, null);
g2d.dispose();
// 将RGBA图像转换为字节数组
ByteArrayOutputStream baos = new ByteArrayOutputStream();
javax.imageio.ImageIO.write(rgbaImage, "PNG", baos);
byte[] rgbaBytes = baos.toByteArray();
log.info("图像转换完成RGBA格式大小: {} bytes", rgbaBytes.length);
return rgbaBytes;
} catch (Exception e) {
log.error("转换图像为RGBA格式失败", e);
throw new IOException("转换图像为RGBA格式失败: " + e.getMessage(), e);
}
}
/**
* 下载图片到字节数组
*