文章目录

  • 概述
  • 上代码实现
  • 主要使用方法讲解
  • 1. Graphics2D 类的 drawImage 方法
  • 2. BufferedImage 类的 getSubimage 方法
  • 3. ImageIO 类的 write 方法
  • 图片缩放插值算法选择
  • 常用插值算法及其优缺点


概述

很多时候,我们从不同地方获取到的图片,它的尺寸比例(宽高比)可能都是各种各样的参数,而我们想要切换成我们需要的比例,比如:9:16 / 16/9这种尺寸,这时候发现用工具很麻烦,且需要一个个的处理,所以用程序写一个能够实现批量处理。
效果:*将图片根据传入的宽高,进行缩放,然后从中心开始裁剪,生成满足你需要的图片尺寸

上代码实现

package com.xgf.file;

import com.xgf.common.LogUtil;
import com.xgf.constant.StringConstantUtil;
import com.xgf.constant.enumclass.FileTypeEnum;
import com.xgf.system.SystemUtil;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

/**
 * @author strive_day
 * @create 2023-04-08 1:00
 * @description 图片文件工具类
 */
public class ImageFileUtil {


    public static void main(String[] args) {
    	// 处理目录下的所有jpg格式文件,裁剪图片尺寸变为3840:2160
        String sourceImgDir = "F:\\wqq";
        String targetImgDir = "F:\\wqq\\11";
        Arrays.stream(new File(sourceImgDir).listFiles()).filter(p -> p.getName().endsWith(".jpg"))
                .forEach(p -> {
                    try {
                        imgDpiModify(p, new File(StringConstantUtil.defaultEndWith(targetImgDir, SystemUtil.getFileSeparator()) + p.getName()), 3840, 2160, Boolean.TRUE);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                });
    }


    public static void imgDpiModify(String sourceImgPath, String targetImgPath, Integer targetWidth, Integer targetHeight, Boolean dealOptimizeFlag) throws IOException {
        imgDpiModify(new File(sourceImgPath), new File(targetImgPath), targetWidth, targetHeight, dealOptimizeFlag);
    }

    /**
     * 图片按照中心点进行缩放和裁剪,达到需要的图片宽高像素比
     *
     * @param sourceImgFile 原图片文件
     * @param targetImgFile 目标图片文件
     * @param targetWidth 目标图片像素宽度
     * @param targetHeight 目标图片像素高度
     * @return true: 成功,false: 失败
     * @param dealOptimizeFlag 处理优化标识,true:处理优化,如果原图片宽高宽高比大,则自动替换宽和高的值
     *                         eg: 图片宽度: 1000,高度: 800,传入参数: targetWidth: 3840, targetHeight: 2160,那么会优化成:targetWidth: 2160, targetHeight: 3840,保证最佳展示
     * @throws IOException IO异常
     */
    public static boolean imgDpiModify(File sourceImgFile, File targetImgFile, Integer targetWidth, Integer targetHeight, Boolean dealOptimizeFlag) throws IOException {

        BufferedImage sourceBufImg = ImageIO.read(sourceImgFile);
        int originalWidth = sourceBufImg.getWidth();
        int originalHeight = sourceBufImg.getHeight();

        // 优化宽高比
        if (Boolean.TRUE.equals(dealOptimizeFlag)) {
            // 原图片宽度比高度大,那么应该满足: targetWidth >= targetHeight,如果不满足,则替换,反之,原图片宽度比高度小,那么应该满足: targetWidth <= targetHeight,否则替换
            if ((originalWidth > originalHeight && targetWidth < targetHeight)
                    || (originalWidth < originalHeight && targetWidth > targetHeight)) {
                Integer temp = targetWidth;
                targetWidth = targetHeight;
                targetHeight = temp;
            }
        }


        // 计算图片需要的缩放比例(宽高和原宽高做对比,取大值)【就是满足目标宽度和高度,需要对图片进行的缩放比】
        double imgScaleValue = Math.max((double) targetWidth / originalWidth, (double) targetHeight / originalHeight);
        // 缩放后的图片宽度
        int scaledImgWidth = (int) Math.round(originalWidth * imgScaleValue);
        // 缩放后的图片高度
        int scaledImgHeight = (int) Math.round(originalHeight * imgScaleValue);

        // 取中间区域的左上角坐标x,y开始截图下标【缩放后变更为要求的图片比例尺寸,进行截取的开始坐标】
        int xStartIndex = (scaledImgWidth - targetWidth) / 2;
        int yStartIndex = (scaledImgHeight - targetHeight) / 2;

        // 对图像进行缩放,获取缩放后的图片BufferedImage
        BufferedImage scaleBufImg = ImageScaleUtil.scale(sourceBufImg, scaledImgWidth, scaledImgHeight);

        // 对缩放后的图片,从中间区域开始截取(用于获取原始图像中指定区域的子图像),达到希望得到的目标图片的宽高比例
        // 参数分别是:截取图像左上角的 x 坐标、y 坐标, 截取图像的宽度、高度
        BufferedImage croppedTargetBufImg = scaleBufImg.getSubimage(xStartIndex, yStartIndex, targetWidth, targetHeight);

        // 将截取好的目标图像写入到目标文件中,参数1 (RenderedImage): 要写入文件或输出流的图像数据。
        // 参数2 (formatName): 指定要写入的图像格式的名称,如 jpg、png、bmp、gif,(这里默认使用JPG),告知 ImageIO 使用哪个图像编解码器来处理图像数据
        // 参数3 (output): 要写入的文件或输出流。可以是一个 File 对象或 OutputStream 对象
        boolean writeFlag = ImageIO.write(croppedTargetBufImg, FileTypeEnum.JPG.getCode(), targetImgFile);

        LogUtil.info("====== sourcePath = {}, targetPath = {}, result success = {}", sourceImgFile.getPath(), targetImgFile.getPath(), writeFlag);

        return writeFlag;
    }

    /**
     * 图片缩放工具类
     */
    public static class ImageScaleUtil {

        /**
         * 定义默认的最优插值算法映射表(根据图像类型自动选择最优插值算法,以获得最佳效果)
         *
         * 主要是三种插值算法:
         * 1. RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR(最近邻插值算法),特点: 像素值直接采用原来最近的像素值,优势和场景: 速度快,适用于图像放大倍数不超过2倍的情况
         * 2. RenderingHints.VALUE_INTERPOLATION_BILINEAR(双线性插值算法),特点:根据周围4个像素计算新像素值, 优势和场景:速度快,适用于图像放大倍数不超过2倍的情况
         * 3. RenderingHints.VALUE_INTERPOLATION_BICUBIC(双三次插值算法), 特点:根据周围16个像素计算新像素值,图像质量高,适用于图像放大倍数较大的情况
         */
        private static final Map<Integer, Object> DEFAULT_INTERPOLATION_MAP = new HashMap<>();

        static {
            // 将 BufferedImage.TYPE_BYTE_GRAY(表示一个 8 位灰度图像,每个像素值存储在 8 位无符号字节中) 类型的图像对应的最优插值算法设置为 NEAREST_NEIGHBOR(最近邻插值)
            DEFAULT_INTERPOLATION_MAP.put(BufferedImage.TYPE_BYTE_GRAY, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
            // 将 BufferedImage.TYPE_USHORT_GRAY(表示一个 16 位灰度图像,每个像素值存储在 16 位无符号 short 类型中) 类型的图像对应的最优插值算法设置为 NEAREST_NEIGHBOR(最近邻插值)
            DEFAULT_INTERPOLATION_MAP.put(BufferedImage.TYPE_USHORT_GRAY, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
            // 将 BufferedImage.TYPE_3BYTE_BGR(表示一个 8 位 BGR 颜色分量的图像,颜色存储在 24 位字节数组中) 类型的图像对应的最优插值算法设置为 BILINEAR(双线性插值)
            DEFAULT_INTERPOLATION_MAP.put(BufferedImage.TYPE_3BYTE_BGR, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
            // 将 BufferedImage.TYPE_4BYTE_ABGR(表示一个 8 位 ABGR 颜色分量的图像,颜色存储在 32 位字节数组中) 类型的图像对应的最优插值算法设置为 BILINEAR(双线性插值)
            DEFAULT_INTERPOLATION_MAP.put(BufferedImage.TYPE_4BYTE_ABGR, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
            // 将 BufferedImage.TYPE_INT_ARGB(表示一个 8 位 RGBA 颜色分量的图像,颜色存储在 32 位整数中) 类型的图像对应的最优插值算法设置为 BICUBIC(双三次插值)
            DEFAULT_INTERPOLATION_MAP.put(BufferedImage.TYPE_INT_ARGB, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
            // 将 BufferedImage.TYPE_INT_RGB(表示一个 8 位 RGB 颜色分量的图像,颜色存储在 32 位整数中) 类型的图像对应的最优插值算法设置为 BICUBIC(双三次插值)
            DEFAULT_INTERPOLATION_MAP.put(BufferedImage.TYPE_INT_RGB, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
            // 将 BufferedImage.TYPE_CUSTOM(表示一个自定义类型的图像,其颜色模型由开发人员指定) 类型的图像对应的最优插值算法设置为 BICUBIC(双三次插值)
            DEFAULT_INTERPOLATION_MAP.put(BufferedImage.TYPE_CUSTOM, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
        }

        /**
         * 对图像进行最优缩放,返回缩放后的图片
         *
         * @param sourceImg 原始图像 BufferedImage
         * @param width 缩放后的宽度
         * @param height 缩放后的高度
         * @return 缩放后的图像 BufferedImage
         */
        public static BufferedImage scale(BufferedImage sourceImg, int width, int height) {
            // 根据图像类型选择最优插值算法,默认为 RenderingHints.VALUE_INTERPOLATION_BICUBICC(双三次插值)
            RenderingHints renderingHints = new RenderingHints(RenderingHints.KEY_INTERPOLATION,
                    DEFAULT_INTERPOLATION_MAP.getOrDefault(sourceImg.getType(), RenderingHints.VALUE_INTERPOLATION_BICUBIC));

            // 创建 BufferedImage,对图片宽高进行缩放,并使用原始图像相同的颜色模型,参数分别为:图像宽度、图像高度、图像的颜色模型类型(这里使用原图像类型)
            BufferedImage scaledImage = new BufferedImage(width, height, sourceImg.getType());

            // 创建缩放图像上进行绘制操作的 Graphics2D 对象(图形上下文)
            Graphics2D graphics2D = scaledImage.createGraphics();
            try {
                // 设置 最优插值算法
                graphics2D.setRenderingHints(renderingHints);
                // 缩放裁剪图片,将指定的图像绘制在Graphics2D对象的当前坐标系中,在目标矩形区域内进行缩放和裁剪。
                // 就是:如果目标矩形区域和源图像的宽高比不同,则源图像将按照目标区域的宽高比例进行缩放。如果目标矩形区域超出了源图像的边界,则会进行裁剪
                // observer为null,表示使用默认的图像观察者(java.awt.image.ImageObserver)
                graphics2D.drawImage(sourceImg, 0, 0, width, height, null);
            } finally {
                // 释放 Graphics2D 资源
                graphics2D.dispose();
            }

            // 返回缩放完成后的图片内容
            return scaledImage;
        }
    }

}

主要使用方法讲解

1. Graphics2D 类的 drawImage 方法

public abstract boolean drawImage(Image img, int x, int y, int width, int height, ImageObserver observer);

方法含义:
对图像进行缩放和裁剪:将指定的图像绘制在 Graphics2D 对象的当前坐标系中,在目标矩形区域内进行缩放和裁剪
eg: 如果目标矩形区域和源图像的宽高比不同,则源图像将按照目标区域的宽高比例进行缩放。如果目标矩形区域超出了源图像的边界,则会进行裁剪

参数

  • img:要绘制的图像对象
  • x:目标矩形区域左上角的 x 坐标
  • y:目标矩形区域左上角的 y 坐标
  • width:目标矩形区域的宽度
  • height:目标矩形区域的高度
  • observer:是一个ImageObserver接口,用于接收有关加载和绘制图像的通知。可以自定义实现ImageObserver接口,该对象将被通知图像的加载和绘制进度。如果为null,表示使用默认的图像观察者(java.awt.image.ImageObserver默认实现)
2. BufferedImage 类的 getSubimage 方法

public BufferedImage getSubimage (int x, int y, int w, int h) { return new BufferedImage (colorModel, raster.createWritableChild(x, y, w, h, 0, 0, null), colorModel.isAlphaPremultiplied(), properties); }

方法含义:
java.awt.image.BufferedImage.BufferedImage 是一个带有可访问或可修改图像数据缓冲区的图像。getSubimage()方法,用于获取原始图像中指定区域的子图像

参数:

  • xStartIndex:子图像左上角的 x 坐标
  • yStartIndex:子图像左上角的 y 坐标
  • targetWidth:子图像的宽度
  • targetHeight:子图像的高度

【注意】使用 getSubimage() 方法时,要确保指定的子图像区域不超出原始图像的边界。否则,将抛出 RasterFormatException 异常。

3. ImageIO 类的 write 方法

public static boolean write(RenderedImage im, String formatName, File output) throws IOException {}

方法含义:
javax.imageio.ImageIO 类提供了一组静态方法,用于读取、写入和处理图像,write()方法用于将 BufferedImage 对象写入到目标文件中

参数:

  • RenderedImage:要写入目标文件的 BufferedImage 对象
  • formatName:目标文件格式,如jpg(jpeg)、png、bmp、gif
  • output:目标文件路径信息
    【注意】使用 ImageIO.write() 方法时,必须确保指定的 BufferedImage 对象已经完全加载到内存并且目标文件路径已经存在并且可以写入。如果图像过大或目标文件路径无法写入,则可能导致方法抛出 异常。

formatName取值,常用图片格式和使用场景:如果希望在保证图像质量的前提下,尽可能地减小图像文件大小,可以考虑使用

  • jpg:jpg/jpeg 格式可通过调整压缩比例来控制图像文件大小和质量,但是在高压缩比例下会出现失真问题
  • png:PNG格式支持透明度通道,并且压缩后文件大小相对较小,但是读写速度可能较慢,适用于 需要支持透明背景或者需要保存非常小的图像文件的场景
  • bmp:BMP格式对图像数据没有压缩,因此可以保留最高的图像质量,但是文件大小相对较大。如果要保存位图图像,则可以考虑使用BMP格式。
  • gif:GIF格式支持多帧动画和透明背景,并且文件大小相对较小,但是只支持256种颜色,不适用于复杂的图像。如果要保存动画或者简单的图形元素,则可以考虑使用GIF格式。

图片缩放插值算法选择

图像类型常量

图像类型常量描述

最佳插值算法常量

插值算法名称

BufferedImage.TYPE_BYTE_GRAY

一个8位灰度图像,每个像素值存储在8位无符号字节中

RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR

最近邻插值

BufferedImage.TYPE_USHORT_GRAY

一个16位灰度图像,每个像素值存储在16位无符号 short 类型中

RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR

BufferedImage.TYPE_3BYTE_BGR

一个8位BGR颜色分量的图像,颜色存储在24位字节数组中

RenderingHints.VALUE_INTERPOLATION_BILINEAR

双线性插值

BufferedImage.TYPE_4BYTE_ABGR

一个8位ABGR颜色分量的图像,颜色存储在32位字节数组中

RenderingHints.VALUE_INTERPOLATION_BILINEAR

BufferedImage.TYPE_INT_ARGB

一个8位RGBA颜色分量的图像,颜色存储在32位整数中

RenderingHints.VALUE_INTERPOLATION_BICUBIC

双三次插值

BufferedImage.TYPE_INT_RGB

一个8位RGB颜色分量的图像,颜色存储在32位整数中

RenderingHints.VALUE_INTERPOLATION_BICUBIC

BufferedImage.TYPE_CUSTOM

一个自定义类型的图像,其颜色模型由开发人员指定

RenderingHints.VALUE_INTERPOLATION_BICUBIC

常用插值算法及其优缺点

插值算法

算法描述

优缺点

主要应用场景

最近邻插值

(Nearest Neighbor Interpolation)

像素点直接采用原来最近的像素点的值

计算简单,速度快,但容易出现锯齿状走样效果

图像缩小、实时计算等场景

双线性插值

(Bilinear Interpolation)

在横向和纵向上分别进行一次线性插值

图像质量较高,但计算复杂度较高,可能会出现平滑效果不够好的情况

图像缩放、图像旋转等场景

双三次插值

(Bicubic Interpolation)

在横向和纵向上分别进行三次函数插值,使用周围 16 个像素计算新像素值

图像质量最高,能有效地解决图像缩放时出现的失真和锐度变化问题,但计算复杂度最高,避免图像出现锯齿状的伪影

图像放大、图像修复等对图像质量要求较高的场景

Lanczos 插值

(Lanczos resampling)

基于 Sinc 函数加窗并截断产生的样条插值方法

图像质量较高,能有效地处理图像拉伸、缩小等过程中出现的失真和锐度变化的问题,但计算复杂度较高

图像缩放、图像旋转等需要考虑图像质量的场景

立方卷积插值

(Cubic Convolution Interpolation

使用立方体函数进行插值

计算速度较快,能有效地处理图像缩放时出现的失真和锐度变化问题,但图像质量可能稍逊于双三次插值

图像缩放、图像修复等对图像质量要求不太高的场景

Spline 插值

基于样条函数进行插值,包括自然边界、非自然边界、周期性等多种类型

具有平滑效果较好、插值精度较高的优点,但计算复杂度较高

图像缩放、图像修复等需要保持连续性和平滑性的场景