前端vue 后端springboot如何实现图片去水印

先看效果:点透办公-图片去水印

前端支持框选,套索,笔刷

思路:前端用画布,后端对接大模型,出于成本考虑,目前本人对接的是阿里百炼大模型 wanx2.1-imageedit,价格在一张图片0.15元,其他的大模型都是偏贵的

前端框选后,后端根据框选坐标生成蒙层;再调用大模型去掉蒙层就行了

直接上代码吧

java 复制代码
package com.ruoyi.basicTool.imageTool.utils;

import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis;
import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisParam;
import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisResult;
import com.alibaba.dashscope.exception.ApiException;
import com.alibaba.dashscope.exception.NoApiKeyException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Path2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 图片去水印工具类 - 使用阿里通义万相SDK
 * 支持两种模式:
 * 1. 手动去水印:按掩码区域精确去除水印(使用Java标准库生成掩码)
 * 2. AI智能去水印:通过提示词描述要去除的内容(不需要掩码)
 */
@Slf4j
@Service
public class ImageReMarkQwenUtils {

    @Value("${tongyi.api.key:}")
    private String tongyiApiKey;

    private static final String DEFAULT_MODEL = "wanx2.1-imageedit";
    /** 全图去水印默认提示:避免笼统 "inpaint" 诱发涂抹感 */
    private static final String DEFAULT_PROMPT = "Remove watermarks and overlaid logos from the image while keeping the rest pixel-accurate";
    private static final int BUFFER_SIZE = 8192;
    /**
     * 掩码编辑:强调与周边像素一致的清晰度/纹理,减轻模糊、发灰、半透明蒙层感(wanx2.1-imageedit + description_edit_with_mask)。
     */
    private static final String WANX_MASK_INPAINT_HINT =
            "White mask = region to fix only. Erase watermark or overlay there completely. "
                    + "Rebuild by continuing the nearby background: same color temperature, contrast, sharpness, film grain/noise, and micro-texture as adjacent areas. "
                    + "The repaired pixels must look fully opaque and as crisp as the rest of the photo---no extra blur, no soft haze, no milky or foggy layer, no ghosting, no semi-transparent veil, no visible rectangular patch. "
                    + "Make mask boundaries invisible with natural transitions. "
                    + "Output canvas must stay exactly %dx%d pixels; do not resize or crop.";
    /**
     * 全图 remove_watermark:附在业务提示词后,约束修复区观感。
     */
    private static final String WANX_FULL_REMOVE_QUALITY_HINT =
            " Where content is removed, fill with surroundings-matched texture; keep full opacity and edge sharpness like the original---avoid blur, haze, or milky overlay.";
    /** 通义万相 wanx2.1-imageedit 等对输入边长的要求(与官方报错一致) */
    private static final int WANX_MIN_EDGE = 512;
    private static final int WANX_MAX_EDGE = 4096;

    /**
     * 手动去除图片水印(使用掩码区域)
     * 使用Java标准库生成掩码图像,按掩码区域精确去除水印
     *
     * @param imageBytes 原始图片字节数组
     * @param filename 文件名(可选)
     * @param areas 水印区域列表
     * @return 处理后的图片字节数组
     */
    public byte[] removeWatermark(byte[] imageBytes, String filename, List<WatermarkAreaDTO> areas) {
        validateApiKey();

        if (areas == null || areas.isEmpty()) {
            throw new IllegalArgumentException("水印区域列表不能为空");
        }

        try {
            // 读取原始图片
            BufferedImage src = ImageIO.read(new ByteArrayInputStream(imageBytes));
            if (src == null) {
                throw new RuntimeException("无法读取图片");
            }

            int originalWidth = src.getWidth();
            int originalHeight = src.getHeight();

            log.info("原图尺寸: {}x{}", originalWidth, originalHeight);

            // 创建纯黑白掩码(灰度图)
            // 黑色(0)表示保留区域,白色(255)表示要去除的区域
            BufferedImage mask = new BufferedImage(originalWidth, originalHeight, BufferedImage.TYPE_BYTE_GRAY);
            Graphics2D maskGraphics = mask.createGraphics();
            maskGraphics.setColor(Color.BLACK);  // 初始化为全黑
            maskGraphics.fillRect(0, 0, originalWidth, originalHeight);
            maskGraphics.setColor(Color.WHITE);  // 白色表示要去除的区域
            maskGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

            // 绘制所有水印区域到掩码上(白色区域表示要去除的部分)
            for (WatermarkAreaDTO area : areas) {
                drawMask(maskGraphics, mask, area);
            }
            maskGraphics.dispose();

            // 将原图和掩码编码为Base64
            ByteArrayOutputStream imageStream = new ByteArrayOutputStream();
            ImageIO.write(src, "png", imageStream);
            String imageBase64 = Base64.getEncoder().encodeToString(imageStream.toByteArray());

            ByteArrayOutputStream maskStream = new ByteArrayOutputStream();
            ImageIO.write(mask, "png", maskStream);
            String maskBase64 = Base64.getEncoder().encodeToString(maskStream.toByteArray());

            log.info("掩码图像已生成,大小: {} bytes", maskStream.size());

            int workW = originalWidth;
            int workH = originalHeight;
            if (needsWanxResize(originalWidth, originalHeight)) {
                int[] wh = resolveWanxWorkingSize(originalWidth, originalHeight);
                workW = wh[0];
                workH = wh[1];
                src = scaleImageToSize(src, workW, workH);
                mask = scaleMaskToSize(mask, workW, workH);
                imageStream.reset();
                ImageIO.write(src, "png", imageStream);
                imageBase64 = Base64.getEncoder().encodeToString(imageStream.toByteArray());
                maskStream.reset();
                ImageIO.write(mask, "png", maskStream);
                maskBase64 = Base64.getEncoder().encodeToString(maskStream.toByteArray());
                log.info("已按通义万相边长要求缩放原图与掩码: {}x{} -> {}x{}",
                        originalWidth, originalHeight, workW, workH);
            }

            byte[] resultBytes = callTongyiSDKWithMask(imageBase64, maskBase64, workW, workH);
            resultBytes = ensureImageSize(resultBytes, workW, workH);
            if (workW != originalWidth || workH != originalHeight) {
                resultBytes = ensureImageSize(resultBytes, originalWidth, originalHeight);
            }
            return resultBytes;

        } catch (Exception e) {
            log.error("手动去除水印失败: {}", e.getMessage(), e);
            throw new RuntimeException("处理图片失败: " + e.getMessage(), e);
        }
    }

    /**
     * AI智能去除图片水印(不需要掩码)
     * 通过提示词描述要去除的内容
     *
     * @param imageBytes 原始图片字节数组
     * @param prompt 提示词,描述要去除的内容(如:Remove all watermarks and logos from the image)
     * @param contentType 图片的 Content-Type(如 image/jpeg, image/png 等),用于设置正确的 MIME 类型
     * @return 处理后的图片字节数组
     */
    public byte[] aiRemoveWatermark(byte[] imageBytes, String prompt, String contentType) {
        validateApiKey();

        try {
            // 读取原始图片以获取尺寸
            BufferedImage src = ImageIO.read(new ByteArrayInputStream(imageBytes));
            if (src == null) {
                throw new RuntimeException("无法读取图片");
            }

            int originalWidth = src.getWidth();
            int originalHeight = src.getHeight();
            log.info("原图尺寸: {}x{}", originalWidth, originalHeight);

            int workW = originalWidth;
            int workH = originalHeight;
            String workContentType = (contentType != null && !contentType.isEmpty()) ? contentType : "image/jpeg";
            String imageBase64;
            if (needsWanxResize(originalWidth, originalHeight)) {
                int[] wh = resolveWanxWorkingSize(originalWidth, originalHeight);
                workW = wh[0];
                workH = wh[1];
                BufferedImage scaled = scaleImageToSize(src, workW, workH);
                log.info("已按通义万相边长要求缩放后再调用 API: {}x{} -> {}x{}",
                        originalWidth, originalHeight, workW, workH);
                ByteArrayOutputStream enc = new ByteArrayOutputStream();
                ImageIO.write(scaled, "png", enc);
                imageBase64 = Base64.getEncoder().encodeToString(enc.toByteArray());
                workContentType = "image/png";
            } else {
                imageBase64 = Base64.getEncoder().encodeToString(imageBytes);
            }

            byte[] resultBytes = callTongyiSDKForAIRemove(imageBase64, prompt, workContentType, workW, workH);
            resultBytes = ensureImageSize(resultBytes, workW, workH);
            if (workW != originalWidth || workH != originalHeight) {
                resultBytes = ensureImageSize(resultBytes, originalWidth, originalHeight);
            }
            return resultBytes;

        } catch (Exception e) {
            log.error("AI去除水印失败: {}", e.getMessage(), e);
            throw new RuntimeException("AI去除水印失败: " + e.getMessage(), e);
        }
    }

    /**
     * 使用掩码调用通义万相SDK
     *
     * @param imageBase64 原图Base64编码
     * @param maskBase64 掩码Base64编码
     * @param originalWidth 原始图片宽度
     * @param originalHeight 原始图片高度
     * @return 处理后的图片字节数组(已调整为原始尺寸)
     */
    private byte[] callTongyiSDKWithMask(String imageBase64, String maskBase64, int originalWidth, int originalHeight) {
        String prompt = String.format(WANX_MASK_INPAINT_HINT, originalWidth, originalHeight);

        ImageSynthesisParam param = ImageSynthesisParam.builder()
                .apiKey(tongyiApiKey)
                .model(DEFAULT_MODEL)
                .function("description_edit_with_mask")
                .prompt(prompt)
                .baseImageUrl("data:image/png;base64," + imageBase64)
                .maskImageUrl("data:image/png;base64," + maskBase64)
                .n(1)
                .watermark(false)
                .build();

        log.info("调用通义万相SDK,使用掩码去水印,原始尺寸: {}x{}, 提示词: {}", originalWidth, originalHeight, prompt);

        // 执行API调用
        byte[] resultBytes = executeImageSynthesis(param);

        // 验证并调整返回图片的尺寸
        return ensureImageSize(resultBytes, originalWidth, originalHeight);
    }

    /**
     * 确保图片尺寸与原始尺寸一致,如果不一致则调整
     *
     * @param imageBytes 图片字节数组
     * @param targetWidth 目标宽度
     * @param targetHeight 目标高度
     * @return 调整后的图片字节数组
     */
    private byte[] ensureImageSize(byte[] imageBytes, int targetWidth, int targetHeight) {
        try {
            // 解码返回的图片
            BufferedImage resultImage = ImageIO.read(new ByteArrayInputStream(imageBytes));

            if (resultImage == null) {
                log.warn("无法解码返回的图片,返回原始数据");
                return imageBytes;
            }

            int resultWidth = resultImage.getWidth();
            int resultHeight = resultImage.getHeight();

            log.info("返回图片尺寸: {}x{}, 目标尺寸: {}x{}", resultWidth, resultHeight, targetWidth, targetHeight);

            // 如果尺寸不一致,调整回原始尺寸
            if (resultWidth != targetWidth || resultHeight != targetHeight) {
                double widthRatio = (double) targetWidth / resultWidth;
                double heightRatio = (double) targetHeight / resultHeight;

                log.warn("返回图片尺寸与原始尺寸不一致,正在调整: {}x{} -> {}x{} (比例: {:.2f}x, {:.2f}x)",
                        resultWidth, resultHeight, targetWidth, targetHeight, widthRatio, heightRatio);

                // 如果尺寸差异超过20%,给出警告
                if (Math.abs(widthRatio - 1.0) > 0.2 || Math.abs(heightRatio - 1.0) > 0.2) {
                    log.error("警告:返回图片尺寸与原始尺寸差异较大,调整后可能会有质量损失或变形!");
                }

                // 创建缩放后的图片
                BufferedImage resizedImage = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB);
                Graphics2D g2d = resizedImage.createGraphics();

                // 设置高质量渲染
                if (widthRatio > 1.0 || heightRatio > 1.0) {
                    // 放大时使用双三次插值(质量更好)
                    g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
                } else {
                    // 缩小时使用双线性插值(避免锯齿)
                    g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
                }
                g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
                g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

                // 如果是PNG格式(支持透明度),需要处理透明度
                if (resultImage.getColorModel().hasAlpha()) {
                    resizedImage = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_ARGB);
                    g2d = resizedImage.createGraphics();
                    g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
                            (widthRatio > 1.0 || heightRatio > 1.0)
                                    ? RenderingHints.VALUE_INTERPOLATION_BICUBIC
                                    : RenderingHints.VALUE_INTERPOLATION_BILINEAR);
                    g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
                    g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
                }

                // 绘制缩放后的图片
                g2d.drawImage(resultImage, 0, 0, targetWidth, targetHeight, null);
                g2d.dispose();

                // 编码调整后的图片
                ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                ImageIO.write(resizedImage, "png", outputStream);
                byte[] adjustedBytes = outputStream.toByteArray();

                log.info("图片尺寸已调整完成,新尺寸: {}x{}", targetWidth, targetHeight);
                return adjustedBytes;
            } else {
                log.info("返回图片尺寸与原始尺寸一致,无需调整");
                return imageBytes;
            }

        } catch (Exception e) {
            log.error("调整图片尺寸失败: {}", e.getMessage(), e);
            // 如果调整失败,返回原始数据
            return imageBytes;
        }
    }

    /**
     * AI去水印调用通义万相SDK
     * 
     * @param imageBase64 原图Base64编码
     * @param prompt 提示词
     * @param contentType 图片Content-Type
     * @param originalWidth 原始图片宽度
     * @param originalHeight 原始图片高度
     * @return 处理后的图片字节数组
     */
    private byte[] callTongyiSDKForAIRemove(String imageBase64, String prompt, String contentType, 
                                             int originalWidth, int originalHeight) {
        String baseImageInput = buildDataUri(imageBase64, contentType);
        
        // 构建更精确的提示词,强调保持原图不变
        String finalPrompt = buildEnhancedPrompt(prompt, originalWidth, originalHeight);

        log.info("调用通义万相SDK,AI去水印,原始尺寸: {}x{}, 提示词: {}", originalWidth, originalHeight, finalPrompt);

        ImageSynthesisParam param = ImageSynthesisParam.builder()
                .apiKey(tongyiApiKey)
                .model(DEFAULT_MODEL)
                .function("remove_watermark")
                .prompt(finalPrompt)
                .baseImageUrl(baseImageInput)
                .n(1)
                .watermark(false)
                .build();

        return executeImageSynthesis(param);
    }

    /**
     * 构建增强的提示词,强调保持原图不变
     * 
     * @param originalPrompt 原始提示词
     * @param originalWidth 原始图片宽度
     * @param originalHeight 原始图片高度
     * @return 增强后的提示词
     */
    private String buildEnhancedPrompt(String originalPrompt, int originalWidth, int originalHeight) {
        String finalPrompt = (originalPrompt != null && !originalPrompt.trim().isEmpty())
                ? originalPrompt.trim()
                : DEFAULT_PROMPT;

        // 检查提示词是否已经包含了保持原图不变的说明
        String lowerPrompt = finalPrompt.toLowerCase();
        boolean hasPreserveInstruction = lowerPrompt.contains("keep") || 
                                        lowerPrompt.contains("preserve") || 
                                        lowerPrompt.contains("unchanged") ||
                                        lowerPrompt.contains("保持") ||
                                        lowerPrompt.contains("不变");

        // 如果提示词中没有强调保持原图不变,则添加
        if (!hasPreserveInstruction) {
            finalPrompt = finalPrompt + 
                ". IMPORTANT: Only remove the specified content, keep everything else in the image completely unchanged. " +
                "Do not modify, regenerate, or change any other parts of the image. " +
                "Maintain the exact original image dimensions (" + originalWidth + "x" + originalHeight + ") " +
                "and preserve all other visual elements exactly as they are.";
        } else {
            // 即使有保持说明,也添加尺寸要求
            if (!lowerPrompt.contains("dimension") && !lowerPrompt.contains("size") && !lowerPrompt.contains("尺寸")) {
                finalPrompt = finalPrompt + 
                    " Maintain the exact original image dimensions (" + originalWidth + "x" + originalHeight + ").";
            }
        }

        finalPrompt = finalPrompt + WANX_FULL_REMOVE_QUALITY_HINT;
        return finalPrompt;
    }

    /**
     * 执行图片合成请求(统一处理逻辑)
     */
    private byte[] executeImageSynthesis(ImageSynthesisParam param) {
        try {
            ImageSynthesis imageSynthesis = new ImageSynthesis();
            log.info("开始调用 DashScope API,模型: {}, function: {}",
                    param.getModel(), param.getFunction());

            ImageSynthesisResult result = imageSynthesis.call(param);

            validateResult(result);

            String imageUrl = extractImageUrl(result);
            if (imageUrl == null || imageUrl.isEmpty()) {
                throw new RuntimeException("未找到输出图片URL,完整响应: " + result);
            }

            log.info("DashScope API 调用成功,结果 URL: {}", imageUrl);
            return downloadImageFromUrl(imageUrl);

        } catch (NoApiKeyException e) {
            log.error("API Key未配置或无效: {}", e.getMessage());
            throw new RuntimeException("服务正忙,请稍后再试!");
        } catch (ApiException e) {
            log.error("通义万相API调用失败: {}", e.getMessage(), e);
            throw new RuntimeException("服务正忙,请稍后再试!");
        } catch (Exception e) {
            log.error("调用通义万相SDK失败: {}", e.getMessage(), e);
            throw new RuntimeException("服务正忙,请稍后再试!");
        }
    }

    /**
     * 验证API调用结果(含异步任务 taskStatus=FAILED 时的 Output.message)
     */
    private void validateResult(ImageSynthesisResult result) {
        if (result == null) {
            throw new RuntimeException("调用失败,响应为空");
        }

        if (result.getCode() != null && !result.getCode().isEmpty() && !"Success".equals(result.getCode())) {
            String errorMsg = result.getMessage() != null ? result.getMessage() : "调用失败";
            throw new RuntimeException("通义万相API调用失败: " + errorMsg);
        }

        if (result.getOutput() == null) {
            throw new RuntimeException("调用失败,响应Output为空");
        }

        try {
            java.lang.reflect.Method getTaskStatusMethod = result.getOutput().getClass().getMethod("getTaskStatus");
            Object taskStatusObj = getTaskStatusMethod.invoke(result.getOutput());
            if (taskStatusObj != null) {
                String taskStatus = taskStatusObj.toString();
                if ("FAILED".equalsIgnoreCase(taskStatus)) {
                    String errorCode = "";
                    String errorMessage = "";
                    try {
                        java.lang.reflect.Method getCodeMethod = result.getOutput().getClass().getMethod("getCode");
                        Object codeObj = getCodeMethod.invoke(result.getOutput());
                        if (codeObj != null) {
                            errorCode = codeObj.toString();
                        }
                    } catch (Exception ignored) {
                    }
                    try {
                        java.lang.reflect.Method getMessageMethod = result.getOutput().getClass().getMethod("getMessage");
                        Object messageObj = getMessageMethod.invoke(result.getOutput());
                        if (messageObj != null) {
                            errorMessage = messageObj.toString();
                        }
                    } catch (Exception ignored) {
                    }
                    String detail = (errorMessage != null && !errorMessage.isEmpty())
                            ? appendWanxSizeHint(errorMessage)
                            : (errorCode + " " + errorMessage).trim();
                    throw new RuntimeException("通义万相任务失败: " + detail);
                }
                if ("PENDING".equalsIgnoreCase(taskStatus) || "RUNNING".equalsIgnoreCase(taskStatus)) {
                    throw new RuntimeException("通义万相任务处理中,请稍后重试。状态: " + taskStatus);
                }
            }
        } catch (NoSuchMethodException e) {
            // 同步返回无 taskStatus
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            log.warn("检查任务状态失败: {}", e.getMessage());
        }

        if (result.getOutput().getResults() == null || result.getOutput().getResults().isEmpty()) {
            throw new RuntimeException("调用成功但未找到输出结果,完整响应: " + result);
        }
    }

    private static String appendWanxSizeHint(String apiMessage) {
        if (apiMessage != null && apiMessage.contains("512") && apiMessage.contains("4096")) {
            return apiMessage + "(通义万相要求宽、高均在 512~4096 像素之间)";
        }
        return apiMessage;
    }

    private static boolean needsWanxResize(int w, int h) {
        int min = Math.min(w, h);
        int max = Math.max(w, h);
        return min < WANX_MIN_EDGE || max > WANX_MAX_EDGE;
    }

    /**
     * 在保持宽高比的前提下,得到同时满足 min 边 ≥512、max 边 ≤4096 的尺寸;若长宽比超过 8:1 则无法兼顾。
     */
    private static int[] resolveWanxWorkingSize(int w, int h) {
        int minE = Math.min(w, h);
        int maxE = Math.max(w, h);
        double sMin = (double) WANX_MIN_EDGE / minE;
        double sMax = (double) WANX_MAX_EDGE / maxE;
        if (sMin > sMax + 1e-12) {
            throw new RuntimeException(
                    "图片长宽比过大,无法在保持比例的同时满足通义万相对边长 512~4096 像素的要求,请裁剪后重试");
        }
        double s = sMin;
        int nw = (int) Math.round(w * s);
        int nh = (int) Math.round(h * s);
        if (Math.min(nw, nh) < WANX_MIN_EDGE) {
            double fix = (double) WANX_MIN_EDGE / Math.min(nw, nh);
            nw = (int) Math.round(nw * fix);
            nh = (int) Math.round(nh * fix);
        }
        if (Math.max(nw, nh) > WANX_MAX_EDGE) {
            double fix = (double) WANX_MAX_EDGE / Math.max(nw, nh);
            nw = (int) Math.round(nw * fix);
            nh = (int) Math.round(nh * fix);
        }
        return new int[]{nw, nh};
    }

    private static BufferedImage scaleImageToSize(BufferedImage src, int tw, int th) {
        int type = src.getColorModel().hasAlpha() ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB;
        BufferedImage dst = new BufferedImage(tw, th, type);
        Graphics2D g = dst.createGraphics();
        g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
        g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
        g.drawImage(src, 0, 0, tw, th, null);
        g.dispose();
        return dst;
    }

    private static BufferedImage scaleMaskToSize(BufferedImage mask, int tw, int th) {
        BufferedImage dst = new BufferedImage(tw, th, BufferedImage.TYPE_BYTE_GRAY);
        Graphics2D g = dst.createGraphics();
        g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        g.drawImage(mask, 0, 0, tw, th, null);
        g.dispose();
        return dst;
    }

    /**
     * 从结果中提取图片URL(支持多种方式)
     */
    private String extractImageUrl(ImageSynthesisResult result) {
        Object firstResult = result.getOutput().getResults().get(0);

        // 方式1: 如果是Map类型
        if (firstResult instanceof Map) {
            Map<?, ?> map = (Map<?, ?>) firstResult;
            Object urlObj = map.get("url");
            if (urlObj != null) {
                return urlObj.toString();
            }
        }

        // 方式2: 尝试反射调用getUrl方法
        try {
            java.lang.reflect.Method getUrlMethod = firstResult.getClass().getMethod("getUrl");
            Object urlObj = getUrlMethod.invoke(firstResult);
            if (urlObj != null) {
                return urlObj.toString();
            }
        } catch (NoSuchMethodException e) {
            // 方式3: 尝试getImageUrl方法
            try {
                java.lang.reflect.Method getImageUrlMethod = firstResult.getClass().getMethod("getImageUrl");
                Object urlObj = getImageUrlMethod.invoke(firstResult);
                if (urlObj != null) {
                    return urlObj.toString();
                }
            } catch (Exception ex) {
                log.debug("无法通过反射获取URL: {}", ex.getMessage());
            }
        } catch (Exception e) {
            log.debug("反射调用getUrl失败: {}", e.getMessage());
        }

        // 方式4: 从toString中提取URL(备选方案)
        String resultStr = firstResult.toString();
        if (resultStr.contains("http://") || resultStr.contains("https://")) {
            int httpIndex = resultStr.indexOf("http");
            int endIndex = resultStr.indexOf(" ", httpIndex);
            if (endIndex == -1) {
                endIndex = resultStr.length();
            }
            return resultStr.substring(httpIndex, endIndex);
        }

        return null;
    }

    /**
     * 构建Data URI格式的图片输入
     */
    private String buildDataUri(String imageBase64, String contentType) {
        if (imageBase64.startsWith("data:")) {
            return imageBase64;
        }

        String mimeType = getMimeType(contentType);
        return "data:" + mimeType + ";base64," + imageBase64;
    }

    /**
     * 根据Content-Type获取MIME类型
     */
    private String getMimeType(String contentType) {
        if (contentType == null) {
            return "image/png";
        }

        String lowerContentType = contentType.toLowerCase();
        if (lowerContentType.contains("jpeg") || lowerContentType.contains("jpg")) {
            return "image/jpeg";
        } else if (lowerContentType.contains("png")) {
            return "image/png";
        } else if (lowerContentType.contains("gif")) {
            return "image/gif";
        } else if (lowerContentType.contains("webp")) {
            return "image/webp";
        } else if (lowerContentType.contains("bmp")) {
            return "image/bmp";
        }

        return "image/png"; // 默认
    }

    /**
     * 从URL下载图片
     */
    private byte[] downloadImageFromUrl(String imageUrl) {
        try {
            log.info("开始下载处理后的图片: {}", imageUrl);
            URL url = new URL(imageUrl);

            try (InputStream inputStream = url.openStream();
                 ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {

                byte[] buffer = new byte[BUFFER_SIZE];
                int bytesRead;
                while ((bytesRead = inputStream.read(buffer)) != -1) {
                    outputStream.write(buffer, 0, bytesRead);
                }

                byte[] imageBytes = outputStream.toByteArray();
                log.info("图片下载成功,大小: {} bytes", imageBytes.length);
                return imageBytes;
            }
        } catch (Exception e) {
            log.error("下载图片失败: {}", e.getMessage(), e);
            throw new RuntimeException("下载图片失败: " + e.getMessage(), e);
        }
    }

    /**
     * 在掩码上绘制标记区域
     */
    private void drawMask(Graphics2D maskGraphics, BufferedImage mask, WatermarkAreaDTO area) {
        String type = area.getType();

        if ("rect".equals(type)) {
            drawRectMask(maskGraphics, mask, area);
        } else if ("brush".equals(type) && area.getPoints() != null) {
            drawBrushMask(maskGraphics, mask, area);
        } else if ("lasso".equals(type) && area.getPoints() != null) {
            drawLassoMask(maskGraphics, mask, area);
        }
    }

    /**
     * 绘制矩形掩码
     * 使用纯白色填充矩形区域
     */
    private void drawRectMask(Graphics2D maskGraphics, BufferedImage mask, WatermarkAreaDTO area) {
        // 确保坐标在有效范围内
        int x = Math.max(0, Math.min(area.getX(), mask.getWidth() - 1));
        int y = Math.max(0, Math.min(area.getY(), mask.getHeight() - 1));
        int width = Math.min(area.getWidth(), mask.getWidth() - x);
        int height = Math.min(area.getHeight(), mask.getHeight() - y);

        if (width <= 0 || height <= 0) {
            log.warn("矩形区域超出图片范围,已跳过: x={}, y={}, width={}, height={}",
                    area.getX(), area.getY(), area.getWidth(), area.getHeight());
            return;
        }

        maskGraphics.fillRect(x, y, width, height);
        log.debug("绘制矩形掩码: ({}, {}) -> ({}, {})", x, y, x + width, y + height);
    }

    /**
     * 绘制笔刷掩码
     * 使用纯白色填充圆形区域,确保连续绘制
     */
    private void drawBrushMask(Graphics2D maskGraphics, BufferedImage mask, WatermarkAreaDTO area) {
        int radius = area.getBrushSize() != null ? Math.max(1, area.getBrushSize() / 2) : 10;
        java.awt.Point prevPoint = null;
        int pointCount = 0;

        for (WatermarkAreaDTO.Point point : area.getPoints()) {
            // 确保坐标在有效范围内
            int x = Math.max(radius, Math.min(point.getX(), mask.getWidth() - radius));
            int y = Math.max(radius, Math.min(point.getY(), mask.getHeight() - radius));

            java.awt.Point center = new java.awt.Point(x, y);
            // 绘制实心圆
            Ellipse2D circle = new Ellipse2D.Double(
                    center.x - radius, center.y - radius,
                    radius * 2, radius * 2
            );
            maskGraphics.fill(circle);

            // 连接相邻点,确保连续绘制
            if (prevPoint != null) {
                double distance = Math.sqrt(
                        Math.pow(center.x - prevPoint.x, 2) +
                                Math.pow(center.y - prevPoint.y, 2)
                );

                if (distance > radius * 0.5) {
                    int steps = Math.max(2, (int) Math.ceil(distance / (radius * 0.5)));
                    for (int i = 1; i < steps; i++) {
                        double t = (double) i / steps;
                        double midX = prevPoint.x + (center.x - prevPoint.x) * t;
                        double midY = prevPoint.y + (center.y - prevPoint.y) * t;
                        // 确保中间点也在有效范围内
                        midX = Math.max(radius, Math.min(midX, mask.getWidth() - radius));
                        midY = Math.max(radius, Math.min(midY, mask.getHeight() - radius));
                        Ellipse2D midCircle = new Ellipse2D.Double(
                                midX - radius, midY - radius,
                                radius * 2, radius * 2
                        );
                        maskGraphics.fill(midCircle);
                    }
                }
            }
            prevPoint = center;
            pointCount++;
        }

        log.debug("绘制笔刷掩码: {} 个点, 笔刷半径: {}", pointCount, radius);
    }

    /**
     * 绘制套索掩码
     * 使用纯白色填充多边形区域
     */
    private void drawLassoMask(Graphics2D maskGraphics, BufferedImage mask, WatermarkAreaDTO area) {
        if (area.getPoints().size() < 3) {
            log.warn("套索区域点数不足(需要至少3个点),已跳过");
            return;
        }

        Path2D polygon = new Path2D.Double();
        WatermarkAreaDTO.Point firstPoint = area.getPoints().get(0);
        // 确保第一个点在有效范围内
        int firstX = Math.max(0, Math.min(firstPoint.getX(), mask.getWidth() - 1));
        int firstY = Math.max(0, Math.min(firstPoint.getY(), mask.getHeight() - 1));
        polygon.moveTo(firstX, firstY);

        for (int i = 1; i < area.getPoints().size(); i++) {
            WatermarkAreaDTO.Point p = area.getPoints().get(i);
            // 确保所有点都在有效范围内
            int x = Math.max(0, Math.min(p.getX(), mask.getWidth() - 1));
            int y = Math.max(0, Math.min(p.getY(), mask.getHeight() - 1));
            polygon.lineTo(x, y);
        }
        polygon.closePath();

        maskGraphics.fill(polygon);
        log.debug("绘制套索掩码: {} 个点", area.getPoints().size());
    }

    /**
     * 验证API Key
     */
    private void validateApiKey() {
        if (tongyiApiKey == null || tongyiApiKey.isEmpty() || "YOUR_TONGYI_API_KEY_HERE".equals(tongyiApiKey)) {
            throw new RuntimeException("通义万相API Key未配置,请在application.properties中设置tongyi.api.key");
        }
    }

    /**
     * 根据 Content-Type 获取 MediaType
     */
    public MediaType getMediaType(String contentType) {
        if (contentType == null) {
            return MediaType.IMAGE_PNG;
        }

        try {
            return MediaType.parseMediaType(contentType);
        } catch (Exception e) {
            // 如果解析失败,根据常见图片类型返回
            String lowerContentType = contentType.toLowerCase();
            if (lowerContentType.contains("jpeg") || lowerContentType.contains("jpg")) {
                return MediaType.IMAGE_JPEG;
            } else if (lowerContentType.contains("png")) {
                return MediaType.IMAGE_PNG;
            } else if (lowerContentType.contains("gif")) {
                return MediaType.IMAGE_GIF;
            } else if (lowerContentType.contains("webp")) {
                return MediaType.parseMediaType("image/webp");
            } else {
                return MediaType.IMAGE_PNG;
            }
        }
    }

    /**
     * 构建提示词
     * 支持精确指定要删除的内容,如特定文字、特定图标等
     */
    public String buildPrompt(String contentType, String contentTypeText, String customDescription) {
        // 优先使用自定义描述(如果提供),无论 contentType 是什么
        // 这样可以支持用户精确指定要删除的特定文字或图标
        if (customDescription != null && !customDescription.trim().isEmpty()) {
            String desc = customDescription.trim();
            
            // 如果同时提供了 contentTypeText,组合使用以提供更多上下文
            if (contentTypeText != null && !contentTypeText.trim().isEmpty()) {
                // 如果自定义描述已经包含了完整的指令,直接使用
                if (desc.toLowerCase().startsWith("remove") || 
                    desc.toLowerCase().startsWith("删除") ||
                    desc.toLowerCase().contains("from the image") ||
                    desc.toLowerCase().contains("从图片")) {
                    // 确保包含保持原图不变的说明
                    if (!desc.toLowerCase().contains("keep") && 
                        !desc.toLowerCase().contains("preserve") && 
                        !desc.toLowerCase().contains("unchanged") &&
                        !desc.toLowerCase().contains("保持") &&
                        !desc.toLowerCase().contains("不变")) {
                        return desc + " Only remove the specified content, keep everything else in the image completely unchanged. Do not modify or regenerate any other parts.";
                    }
                    return desc;
                }
                // 否则组合使用,提供更精确的描述
                return String.format("Remove %s (%s) from the image. IMPORTANT: Only remove the specified content, keep everything else in the image completely unchanged. Do not modify, regenerate, or change any other parts of the image.", 
                    contentTypeText.trim(), desc);
            }
            
            // 如果自定义描述已经是一个完整的提示词,直接返回
            if (desc.toLowerCase().startsWith("remove") || 
                desc.toLowerCase().startsWith("删除") ||
                desc.toLowerCase().contains("from the image") ||
                desc.toLowerCase().contains("从图片")) {
                // 确保包含保持原图不变的说明
                if (!desc.toLowerCase().contains("keep") && 
                    !desc.toLowerCase().contains("preserve") && 
                    !desc.toLowerCase().contains("unchanged") &&
                    !desc.toLowerCase().contains("保持") &&
                    !desc.toLowerCase().contains("不变")) {
                    return desc + " Only remove the specified content, keep everything else in the image completely unchanged. Do not modify or regenerate any other parts.";
                }
                return desc;
            }
            
            // 否则包装成完整的提示词,强调保持原图不变
            return String.format("Remove %s from the image. IMPORTANT: Only remove the specified content, keep everything else in the image completely unchanged. Do not modify, regenerate, or change any other parts of the image.", desc);
        }

        // 使用预设类型文字(如果提供了具体描述)
        if (contentTypeText != null && !contentTypeText.trim().isEmpty()) {
            String text = contentTypeText.trim();
            // 检查是否包含引号,表示是特定的文字内容(支持英文和中文引号)
            if (text.contains("\"") || text.contains("'") || text.contains("\u201C") || text.contains("\u201D") || text.contains("\u2018") || text.contains("\u2019")) {
                return String.format("Remove only the text %s from the image. IMPORTANT: Keep all other text and content completely unchanged. Do not modify or regenerate any other parts of the image.", text);
            }
            // 检查是否是位置描述(如 "top right", "bottom left" 等)
            if (text.toLowerCase().matches(".*(top|bottom|left|right|center|corner|upper|lower).*")) {
                return String.format("Remove %s from the image. IMPORTANT: Only remove the specified element, keep everything else in the image completely unchanged. Do not modify or regenerate any other parts.", text);
            }
            // 默认处理
            return String.format("Remove %s from the image. IMPORTANT: Only remove the specified content, keep everything else in the image completely unchanged. Do not modify, regenerate, or change any other parts of the image.", text);
        }

        // 根据 contentType 生成提示词
        if (contentType != null) {
            Map<String, String> promptMap = new HashMap<>();
            promptMap.put("watermark", "Remove all watermarks and logos from the image");
            promptMap.put("text", "Remove text content from the image");
            promptMap.put("people", "Remove people from the image");
            promptMap.put("glasses_reflection", "Remove glasses reflection from the image");
            promptMap.put("tattoo", "Remove tattoo from the image");
            promptMap.put("sticker", "Remove stickers and emojis from the image");
            promptMap.put("acne", "Remove acne from the image");
            promptMap.put("glasses", "Remove glasses from the image");

            String prompt = promptMap.get(contentType);
            if (prompt != null) {
                return prompt;
            }
        }

        // 默认提示词
        return DEFAULT_PROMPT;
    }

    /**
     * 生成输出文件名
     */
    public String getOutputFileName(String originalFilename) {
        if (originalFilename == null) {
            return "no_watermark.png";
        }

        int lastDot = originalFilename.lastIndexOf('.');
        if (lastDot > 0) {
            String nameWithoutExt = originalFilename.substring(0, lastDot);
            String ext = originalFilename.substring(lastDot);
            return nameWithoutExt + "_no_watermark" + ext;
        }

        return originalFilename + "_no_watermark.png";
    }
}

想要完整前后端实例代码的可以点击 https://office.zjdiante.com/imageTool/imageRemoveWater 找到API对接,上面有本人联系方式

相关推荐
whuhewei1 小时前
React搜索框组件
前端·javascript·react.js
姓王者1 小时前
Cloudflare Pages自定义依赖安装实践 | 姓王者的博客
前端
spmcor1 小时前
前端 RBAC 权限控制实战:从零实现动态路由与细粒度按钮权限
vue.js
stringwu1 小时前
Flutter 开发的 AI 三件套:壮汉、法师、实习生
前端
spmcor1 小时前
Vue 2 vs Vue 3:核心差异深度剖析与迁移指南
vue.js
代码搬运媛1 小时前
BFF 架构浅析:再也不用求后端改接口了
前端
ZC跨境爬虫1 小时前
跟着 MDN 学 HTML day_50:(深入理解 DOM 中的 Text 节点)
前端·javascript·microsoft·ui·html·媒体
ZC跨境爬虫1 小时前
跟着 MDN 学 HTML day_51:(深入理解 XPathEvaluator 接口)
前端·javascript·ui·html·音视频
wjykp1 小时前
5.cypher语句组合与复杂操作
linux·前端·javascript