首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java Web 圖片上傳裁剪完整方案:JCrop 前端 + 後端縮放裁剪實現

     編輯:Java 相關知識

整體架構

  1. 前端:JCrop (jQuery 插件) 選取裁剪區域 → 獲取座標 (x, y, w, h)。
  2. 上傳:原圖上傳 → 後端等比縮放生成預覽圖 → 返回預覽 URL。
  3. 裁剪:前端在預覽圖上選區 → 提交座標 + 原圖標識 → 後端按原圖比例映射座標 → 裁剪生成最終圖片。
  4. 存儲:裁剪後圖片上傳文件服務器/對象存儲 → 返回業務 URL。

後端核心工具類

1. 圖片縮放(生成預覽圖)

public class ImageUtils {
    public static BufferedImage scale(BufferedImage src, int targetWidth, int targetHeight, boolean keepRatio) {
        int srcW = src.getWidth(), srcH = src.getHeight();
        double scale = keepRatio ? 
            Math.min((double) targetWidth / srcW, (double) targetHeight / srcH) :
            1.0;
        int w = (int) (srcW * scale);
        int h = (int) (srcH * scale);
        
        BufferedImage dst = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
        Graphics2D g = dst.createGraphics();
        g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        g.drawImage(src, 0, 0, w, h, null);
        g.dispose();
        return dst;
    }

    public static void writeJpg(BufferedImage img, OutputStream out, float quality) throws IOException {
        ImageWriter writer = ImageIO.getImageWritersByFormatName("jpg").next();
        ImageWriteParam param = writer.getDefaultWriteParam();
        param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
        param.setCompressionQuality(quality); // 0.7-0.9 平衡體積/質量
        writer.setOutput(new MemoryCacheImageOutputStream(out));
        writer.write(null, new IIOImage(img, null, null), param);
        writer.dispose();
    }
}

2. 圖片裁剪(核心邏輯:座標映射)

public class CropUtils {
    public record CropResult(File file, int width, int height) {}

    public static CropResult crop(File srcFile, int previewW, int previewH, 
                                  int cropX, int cropY, int cropW, int cropH, 
                                  File outputDir, String prefix) throws IOException {
        BufferedImage src = ImageIO.read(srcFile);
        int origW = src.getWidth(), origH = src.getHeight();
        
        // 座標映射:預覽圖座標 -> 原圖座標
        double scaleX = (double) origW / previewW;
        double scaleY = (double) origH / previewH;
        int srcX = (int) Math.round(cropX * scaleX);
        int srcY = (int) Math.round(cropY * scaleY);
        int srcW = (int) Math.round(cropW * scaleX);
        int srcH = (int) Math.round(cropH * scaleY);
        
        // 邊界保護
        srcX = Math.max(0, Math.min(srcX, origW - 1));
        srcY = Math.max(0, Math.min(srcY, origH - 1));
        srcW = Math.min(srcW, origW - srcX);
        srcH = Math.min(srcH, origH - srcY);
        
        // 執行裁剪
        BufferedImage cropped = src.getSubimage(srcX, srcY, srcW, srcH);
        
        // 輸出文件
        File outFile = new File(outputDir, prefix + "_" + System.currentTimeMillis() + ".jpg");
        ImageUtils.writeJpg(cropped, new FileOutputStream(outFile), 0.85f);
        return new CropResult(outFile, srcW, srcH);
    }
}

Spring MVC 控制器

@RestController
@RequestMapping("/api/avatar")
public class AvatarController {
    private static final String TEMP_DIR = System.getProperty("java.io.tmpdir") + "/avatar_uploads/";
    private final FileStorageService storageService;

    @PostMapping("/upload")
    public Result upload(@RequestParam("file") MultipartFile file) {
        // 1. 校驗文件類型/大小
        if (!isImage(file)) return Result.error("僅支援 JPG/PNG");
        if (file.getSize() > 5 * 1024 * 1024) return Result.error("文件不超過 5MB");

        // 2. 保存原圖到臨時目錄
        File tempDir = new File(TEMP_DIR);
        tempDir.mkdirs();
        String originalName = UUID.randomUUID() + "_" + file.getOriginalFilename();
        File originalFile = new File(tempDir, originalName);
        file.transferTo(originalFile);

        // 3. 生成預覽圖(寬 400px 等比縮放)
        BufferedImage src = ImageIO.read(originalFile);
        BufferedImage preview = ImageUtils.scale(src, 400, 300, true);
        String previewName = "preview_" + originalName;
        File previewFile = new File(tempDir, previewName);
        ImageUtils.writeJpg(preview, new FileOutputStream(previewFile), 0.8f);

        // 4. 返回預覽信息供前端 JCrop 使用
        return Result.success(Map.of(
            "originalId", originalName,
            "previewUrl", "/api/avatar/preview/" + previewName,
            "previewWidth", preview.getWidth(),
            "previewHeight", preview.getHeight()
        ));
    }

    @GetMapping("/preview/{filename}")
    public void preview(@PathVariable String filename, HttpServletResponse resp) throws IOException {
        File file = new File(TEMP_DIR, filename);
        if (!file.exists()) { resp.setStatus(404); return; }
        resp.setContentType("image/jpeg");
        Files.copy(file.toPath(), resp.getOutputStream());
    }

    @PostMapping("/crop")
    public Result crop(@RequestParam String originalId,
                               @RequestParam int x, @RequestParam int y,
                               @RequestParam int w, @RequestParam int h) {
        File originalFile = new File(TEMP_DIR, originalId);
        if (!originalFile.exists()) return Result.error("原圖已過期,請重新上傳");

        // 獲取預覽圖尺寸(需前端傳回或服務端緩存)
        int previewW = 400; // 示例固定值,實際應從緩存/數據庫獲取
        int previewH = 300;

        CropUtils.CropResult result = CropUtils.crop(originalFile, previewW, previewH, 
            x, y, w, h, new File(TEMP_DIR), "cropped");
        
        // 上傳到永久存儲(OSS/S3/文件服務器)
        String permanentUrl = storageService.upload(result.file(), "avatar/");
        
        // 清理臨時文件
        originalFile.delete();
        new File(TEMP_DIR, "preview_" + originalId).delete();
        result.file().delete();

        return Result.success(permanentUrl);
    }
}

前端關鍵點 (JCrop 初始化)

$('#originalImg').Jcrop({
    aspectRatio: 4/3,           // 固定比例 400:300
    setSelect: [0, 0, 400, 300], // 預設選區
    onChange: updateCoords,
    onSelect: updateCoords,
    boxWidth: 500,              // 容器寬度限制
    boxHeight: 400
}, function() {
    jcropApi = this;
});

function updateCoords(c) {
    $('#x').val(c.x);
    $('#y').val(c.y);
    $('#w').val(c.w);
    $('#h').val(c.h);
}

// 提交裁剪
$('#saveImage').click(function() {
    $.post('/api/avatar/crop', {
        originalId: $('#originalId').val(),
        x: $('#x').val(), y: $('#y').val(),
        w: $('#w').val(), h: $('#h').val()
    }, function(res) {
        if (res.success) {
            $('#finalAvatar').attr('src', res.data);
            alert('頭像更新成功');
        }
    });
});

常見坑與解決方案

問題原因解決
裁剪位置偏移預覽圖縮放比例與原圖不一致,或未正確映射座標後端記錄並返回預覽圖實際寬高,裁剪時按 原圖尺寸/預覽尺寸 精確映射
圖片變形/模糊非等比縮放、JPG 質量過低、多次重編碼強制等比縮放、質量 0.8-0.9、原圖→預覽→裁剪鏈路中僅最終輸出編碼一次
大圖 OOMBufferedImage 全量載入堆內存限制上傳大小(<5MB)、使用 ImageIO.createImageInputStream() 流式讀取、或引入 TwelveMonkeys ImageIO 支援大圖分塊處理
臨時文件洩漏異常分支未清理、定時任務缺失try-finally 清理、獨立定時掃描臨時目錄刪除過期文件(>1小時)
並發文件名衝突UUID 機率極低但理論存在UUID + timestamp + 原始文件名哈希,或使用数据库序列/雪花算法 ID

生產級擴展建議

  • 異步處理:上傳返回預覽 URL,裁剪提交進消息隊列,Worker 處理後推送 WebSocket 通知前端。
  • 多尺寸輸出:一次裁剪生成多種規格(大頭像 200x200、中 100x100、小 40x40),響應式顯示。
  • 圖片審核:接入內容安全 API(色情/暴恐/廣告識別)在存儲前同步檢測。
  • CDN 緩存:裁剪後圖片上傳 OSS 並配置 CDN,URL 帶版本號實現緩存更新。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved