封裝圖片處理工具類,實現圖片的裁剪、壓縮、圖片水印、文字水印、多行文字水印等功能

2023-07-12 12:00:26

目錄

一、前言

最近用結合thumbnailator和Graphics2D封裝了一個圖片工具類,目前可以實現圖片的裁剪、壓縮、新增圖片水印、文字水印、多行文字水印等功能,同時該工具類的實現使用了建造者模式、責任鏈模式、工廠模式、策略模式等多種設計模式,感覺圖片處理的功能有一定的通用性,所以這次寫一篇文章來分享一下這個工具類的使用方式和實現原理,程式碼不足之處還望大家指正,文末也會提供程式碼的github地址。

二、工具類的依賴和簡單介紹

1、新增依賴

		<dependency>
            <groupId>net.coobird</groupId>
            <artifactId>thumbnailator</artifactId>
            <version>0.4.20</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.15</version>
        </dependency>
        <dependency>
            <groupId>commons-chain</groupId>
            <artifactId>commons-chain</artifactId>
            <version>1.2</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
        </dependency>
        <!-- 僅僅為了支援MultipartFile型別檔案的載入,不載入MultipartFile這個依賴可以去掉-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>5.3.12</version>
            <scope>compile</scope>
        </dependency>

2、簡單的使用

首先這個處理類提供了一個建造簡化了工具列使用,下面一個簡單的使用範例,實現了圖的裁剪裁剪,從這個範例程式碼可以看出這個圖片處理工具類的建造者主要由load(載入需要處理的圖片)、addRule(新增圖片處理規則,可以多次新增)、toFile(輸出處理後的圖片)三部分組成。

ImageHandBuilder.load(imageResource)
                .addRule(ImageHandRuleFactory.regionRuleBuilder()
                        .regionType(RegionTypeEnum.SCALE_REGION)
                        .positions(Positions.TOP_CENTER)
                        .scale(0.5d).build())
                .toFile("D:\\test\\test-region-scale.jpg");

3、載入需要處理的圖片

載入需要處理的圖片的方法是load() 方法,建造者呼叫的第一個方法必須是這個load()方法。load()方法提供多個過載方法,支援以下傳參。

	/**
     * 載入圖片
     * @param absolutePath 圖片絕對路徑
     * @return Builder
     */
	Builder load(String absolutePath);

    /**
     * 載入圖片
     * @param file 圖片檔案
     * @return Builder
     */
    Builder load(File file);

	/**
     * 載入圖片
     * @param file 圖片檔案
     * @return Builder
     */
    Builder load(MultipartFile file);
    
    /**
     * 載入圖片
     * @param url 圖片url
     * @return Builder
     */
    Builder load(URL url);

 	/**
     * 載入圖片
     * @param inputStream 圖片輸入流
     * @return Builder
     */
    Builder load(InputStream inputStream);

	/**
     * 載入圖片
     * @param image 圖片
     * @param fileType 圖片型別
     * @return Builder
     */
    Builder load(BufferedImage image, String fileType);

4、新增圖片處理規則

新增圖片處理規則的方法是addRule(),支援RegionRule(裁剪規則)、CompressRule(壓縮規則)、ImageWatermarkRule(圖片水印規則)、TextWatermarkRule(文字水印規則)、MultipleTextWatermarkRule(多行文字水印規則)。規則的新增支援Builder方式和使用規則工廠的方法

4.1 Builder的方式

	@Test
    public void testBuilder() throws Exception {
        ClassLoader classLoader = ImageHandBuilderTest.class.getClassLoader();
        URL imageResource = classLoader.getResource("pic/example.jpg");
        ImageHandBuilder.load(imageResource)
                .addRule(RegionRule.builder()
                        .regionType(RegionTypeEnum.SCALE_REGION)
                        .positions(Positions.TOP_CENTER)
                        .scale(0.5d).build())
                .toFile("D:\\test\\test-region-scale.jpg");
    }

4.2 使用規則工廠的方式

	@Test
    public void testFactory() throws Exception {
        ClassLoader classLoader = ImageHandBuilderTest.class.getClassLoader();
        URL imageResource = classLoader.getResource("pic/example.jpg");
        ImageHandBuilder.load(imageResource)
                .addRule(ImageHandRuleFactory.regionRuleBuilder()
                        .regionType(RegionTypeEnum.SCALE_REGION)
                        .positions(Positions.TOP_CENTER)
                        .scale(0.5d).build())
                .toFile("D:\\test\\test-region-scale.jpg");
    }

5、輸出處理後的圖片

輸出處理後的圖片的方法是toFile()或toOutputStream()方法,是建造者最後需要呼叫的一個方法。輸出處理後的圖片的方法如下

		 /**
         * 將處理後的圖片匯出到檔案
         * @param file 圖片檔案
         */
         void toFile(File file);

        /**
         * 將處理後的圖片匯出到檔案
         * @param absolutePath 圖片絕對路徑
         */
         void toFile(String absolutePath);

        /**
         * 將處理後的圖片匯出到輸出流
         * @param out 輸出流
         */
         void toOutputStream(OutputStream out);

三、使用方式

下面我們使用同一張範例圖片依次演示一下RegionRule(裁剪規則)、CompressRule(壓縮規則)、ImageWatermarkRule(圖片水印規則)、TextWatermarkRule(文字水印規則)、MultipleTextWatermarkRule(多行文字水印規則)這五種規則的設定和實現效果,下圖處理前的範例原圖,圖片的原始比例為3840 X 2160:

1、圖片裁剪

圖片裁剪提供了按長寬裁剪和按比例裁剪兩種裁剪方式

圖片裁剪規則實體類

import com.fhey.common.file.imagehand.enums.RegionTypeEnum;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import net.coobird.thumbnailator.geometry.Positions;

/**
 * @author fhey
 * @date 2022-07-08 17:42:51
 * @description: 裁剪規則
 */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class RegionRule implements ImageHandRule {

    /**
     * 位置裁剪
     */
    private Positions positions;

    /**
     * 裁剪寬度
     */
    private Integer width;

    /**
     * 裁剪高度
     */
    private Integer height;

    /**
     * 裁剪比例
     */
    private Double scale;

    /**
     * 裁剪型別
     */
    private RegionTypeEnum regionType;
    
    @Override
    public boolean check() {
        if (null == regionType){
            throw new RuntimeException("裁剪型別(compressType)不能為空!");
        }
        if (RegionTypeEnum.SCALE_REGION.equals(regionType) && null == scale){
            throw new RuntimeException("按比列裁剪模式裁剪比例(scale)不能為空!");
        }
        if (RegionTypeEnum.WIDTH_HEIGHT_REGION.equals(regionType) && (null == width || null == height)){
            throw new RuntimeException("按寬高裁剪模式寬高不能為空!");
        }
        return true;
    }
}

1.1 按長寬裁剪

從圖片中間裁剪300 X 300的區域

1.1.1 範例程式碼

 @Test
    public void testWidthHeightRegion() throws Exception {
        ClassLoader classLoader = ImageHandBuilderTest.class.getClassLoader();
        URL imageResource = classLoader.getResource("pic/example.jpg");
        ImageHandBuilder.load(imageResource)
                .addRule(ImageHandRuleFactory.regionRuleBuilder()
                        .regionType(RegionTypeEnum.WIDTH_HEIGHT_REGION)
                        .positions(Positions.CENTER)
                        .width(300)
                        .height(300).build())
                .toFile("D:\\test\\test-region-widthHeight.jpg");
    }

1.1.2 實現效果

由下圖可以看出處理後的圖片正好是300 X 300,但是隻擷取了中間一小塊

1.2 按比例裁剪

從上中開始按圖片0.5的比列裁剪

1.2.1 範例程式碼

 @Test
    public void testScaleRegion() throws Exception {
        ClassLoader classLoader = ImageHandBuilderTest.class.getClassLoader();
        URL imageResource = classLoader.getResource("pic/example.jpg");
        ImageHandBuilder.load(imageResource)
                .addRule(ImageHandRuleFactory.regionRuleBuilder()
                        .regionType(RegionTypeEnum.SCALE_REGION)
                        .positions(Positions.TOP_CENTER)
                        .scale(0.5d).build())
                .toFile("D:\\test\\test-region-scale.jpg");
    }

1.2.2 實現效果

有下圖可以看出裁剪之後長寬都是原圖的一半,因為從上中開始裁剪所以原圖下方一些圖片被裁剪掉了

2、圖片壓縮

圖片壓縮提供了按長寬壓縮和按比例壓縮兩種壓縮方式,其中按長寬壓縮又提供了不保持比列、但寬度保持比列、自動保持比列。

圖片壓縮規則實體類

import com.fhey.common.file.imagehand.enums.CompressTypeEnum;
import com.fhey.common.file.imagehand.enums.KeepAspectRatioEnum;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author fhey
 * @date 2022-07-08 17:28:28
 * @description: 壓縮規則
 */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class CompressRule implements ImageHandRule {

    /**
     * 壓縮寬度
     */
    private Integer width;

    /**
     * 壓縮高度
     */
    private Integer height;

    /**
     * 壓縮比例
     */
    private Double scale;

    /**
     * 壓縮型別
     */
    private CompressTypeEnum compressType;

    /**
     * 是否保持寬高比
     */
    private KeepAspectRatioEnum keepAspectRatio;

    @Override
    public boolean check() {
        if (null == compressType){
            throw new RuntimeException("壓縮型別(compressType)不能為空!");
        }
        if (CompressTypeEnum.SCALE_COMPRESS.equals(compressType) && null == scale){
            throw new RuntimeException("按比列壓縮模式壓縮比例(scale)不能為空!");
        }
        if (CompressTypeEnum.WIDTH_HEIGHT_COMPRESS.equals(compressType) && (null == width || null == height)){
            throw new RuntimeException("按寬高壓縮模式寬高不能為空!");
        }
        return true;
    }
}

2.1 按長寬壓縮 不保持比例

2.1.1 範例程式碼

將圖片壓縮成300 X 300

@Test
    public void testWidthHeightCompressNoKeepAspectRatio() throws Exception {
        ClassLoader classLoader = ImageHandBuilderTest.class.getClassLoader();
        URL imageResource = classLoader.getResource("pic/example.jpg");
        ImageHandBuilder.load(imageResource)
                .addRule(ImageHandRuleFactory.compressRuleBuilder()
                        .compressType(CompressTypeEnum.WIDTH_HEIGHT_COMPRESS)
                        .width(300)
                        .height(300).build())
                .toFile("D:\\test\\test-compress-widthHeight-noKeep.jpg");
    }

2.1.2 實現效果

由下圖可以看出處理後的圖片是300 X300,但是圖片變形嚴重

2.2 按長寬壓縮 按寬度保持長寬比例

將圖片壓縮成300 X 300,這裡的縮 按寬度保持比例採用了thumbnailator提供的預設的保持長寬比列的方式

2.2.1 範例程式碼

@Test
    public void testWidthHeightCompressKeepAspectRatioByWidth() throws Exception {
        ClassLoader classLoader = ImageHandBuilderTest.class.getClassLoader();
        URL imageResource = classLoader.getResource("pic/example.jpg");
        ImageHandBuilder.load(imageResource)
                .addRule(ImageHandRuleFactory.compressRuleBuilder()
                        .compressType(CompressTypeEnum.WIDTH_HEIGHT_COMPRESS)
                        .width(300)
                        .height(300)
                        .keepAspectRatio(KeepAspectRatioEnum.KEEP_BY_WITH).build())
                .toFile("D:\\test\\test-compress-widthHeight-keep-byWidth.jpg");
    }

2.2.2 實現效果

由下圖可以看出thumbnailator預設的保持長寬比的做法,將寬度處理成了300,但是高度只有169,不到300了

2.3 按長寬壓縮 自動保持長寬比例

將圖片壓縮成300 X 300,因為thumbnailator預設的保持長寬比的做法在某些場景下不符合需求,所以我有自己擴充套件了自定義的一個保持長寬比例的做法,即在保持長寬比的情況下讓處理後的圖片寬高不會低於設定值。

2.3.1 範例程式碼

@Test
     public void testWidthHeightCompressKeepAspectRatioAuto() throws Exception {
        ClassLoader classLoader = ImageHandBuilderTest.class.getClassLoader();
        URL imageResource = classLoader.getResource("pic/example.jpg");
        ImageHandBuilder.load(imageResource)
                .addRule(ImageHandRuleFactory.compressRuleBuilder()
                        .compressType(CompressTypeEnum.WIDTH_HEIGHT_COMPRESS)
                        .width(300)
                        .height(300)
                        .keepAspectRatio(KeepAspectRatioEnum.KEEP_AUTO).build())
                .toFile("D:\\test\\test-compress-widthHeight-keep-auto.jpg");
    }

2.3.2 實現效果

由下圖可以看出處理後的圖片,寬度533,高度300

2.4 按比例壓縮

按圖片0.5的比列壓縮

2.4.1 範例程式碼

@Test
    public void testScaleCompress() throws Exception {
        ClassLoader classLoader = ImageHandBuilderTest.class.getClassLoader();
        URL imageResource = classLoader.getResource("pic/example.jpg");
        ImageHandBuilder.load(imageResource)
                .addRule(ImageHandRuleFactory.compressRuleBuilder()
                        .compressType(CompressTypeEnum.SCALE_COMPRESS)
                        .scale(0.5d).build())
                .toFile("D:\\test\\test-compress-scale.jpg");
    }

2.4.2 實現效果

可以看出壓縮之後長寬都是原圖的一半

3、結合圖片壓縮和圖片裁剪生成縮圖

在需要展示圖片的列表中,比如電商中常見的商品列表,為了提升列表的效能,列表裡展示的圖片往往都是縮圖。但是通上面的圖片壓縮和圖片裁剪把圖片處理成300 X300可以發現處理後的圖片圖片變形嚴重要麼圖片細節缺失過多展示效果不好,那我們應該怎麼生成一張符合尺寸且保證展示效果的縮圖呢?方法就是將圖片壓縮和圖片裁剪結合起來使用,先根據縮圖的尺寸要求壓縮圖片(壓縮後的長寬都不能低於縮圖的尺寸),然後再根據縮圖的尺寸裁剪圖片。程式碼範例和展示效果如下。

3.1 錯誤的寫法

3.1.1 錯誤的程式碼範例:

		int width = 300;
        int height = 300;
        ClassLoader classLoader = ImageHandBuilderTest.class.getClassLoader();
        URL imageResource = classLoader.getResource("pic/example.jpg");
        ImageHandBuilder.load(imageResource)
                .addRule(ImageHandRuleFactory.compressRuleBuilder()
                        .compressType(CompressTypeEnum.WIDTH_HEIGHT_COMPRESS)
                        .width(width)
                        .height(height)
                        .keepAspectRatio(KeepAspectRatioEnum.KEEP_AUTO).build())
                .addRule(ImageHandRuleFactory.regionRuleBuilder()
                        .regionType(RegionTypeEnum.WIDTH_HEIGHT_REGION)
                        .positions(Positions.CENTER)
                        .width(width)
                        .height(height).build())
                .toFile("D:\\test\\test-compressAndRegion.jpg");

因為thumbnailator的裁剪都是對原圖的裁剪這樣使用只會使之前的壓縮規則實現,造成只會按原圖裁剪的情況。執行效果等同於

		int width = 300;
        int height = 300;
        ClassLoader classLoader = ImageHandBuilderTest.class.getClassLoader();
        URL imageResource = classLoader.getResource("pic/example.jpg");
        ImageHandBuilder.load(imageResource)
                .addRule(ImageHandRuleFactory.regionRuleBuilder()
                        .regionType(RegionTypeEnum.WIDTH_HEIGHT_REGION)
                        .positions(Positions.CENTER)
                        .width(width)
                        .height(height).build())
                .toFile("D:\\test\\test-compressAndRegion.jpg");

3.1.2 實現效果

3.2 正確的寫法

3.2.1 正確的程式碼範例:

所以正確的做法是需要壓縮圖片之後先輸出圖片,在第二次呼叫建造者載入圖片,再進行圖片裁剪。

正確的程式碼範例:

 @Test
    public void testCompressAndRegion() throws Exception {
        int width = 300;
        int height = 300;
        ClassLoader classLoader = ImageHandBuilderTest.class.getClassLoader();
        URL imageResource = classLoader.getResource("pic/example.jpg");
        ImageHandBuilder.load(imageResource)
                .addRule(ImageHandRuleFactory.compressRuleBuilder()
                        .compressType(CompressTypeEnum.WIDTH_HEIGHT_COMPRESS)
                        .width(width)
                        .height(height)
                        .keepAspectRatio(KeepAspectRatioEnum.KEEP_AUTO).build())
                .toFile("D:\\test\\test-compressAndRegion.jpg");
        ImageHandBuilder.load("D:\\test\\test-compressAndRegion.jpg")
                .addRule(ImageHandRuleFactory.regionRuleBuilder()
                        .regionType(RegionTypeEnum.WIDTH_HEIGHT_REGION)
                        .positions(Positions.CENTER)
                        .width(width)
                        .height(height).build())
                .toFile("D:\\test\\test-compressAndRegion.jpg");
    }

3.2.2 實現效果

4、新增圖片水印

水印規則基礎類別:

import com.fhey.common.file.imagehand.rule.ImageHandRule;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import net.coobird.thumbnailator.geometry.Positions;

@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public abstract class BaseImageWatermarkRule implements ImageHandRule {

    /**
     * 水印位置
     */
    private Positions positions;

    /**
     * 水印透明度
     */
    private Double alpha;

    @Override
    //設定水印可以重複設定
    public boolean canRepeat() {
        return true;
    }
}

新增圖片水印規則實體類

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.awt.image.BufferedImage;

/**
 * @author fhey
 * @date 2022-07-08 18:00:57
 * @description: 
 */
@Data
@SuperBuilder
@AllArgsConstructor
@NoArgsConstructor
public class ImageWatermarkRule extends BaseImageWatermarkRule {
    /**
     * 水印圖片
     */
    private BufferedImage waterImage;

    /**
     * 水印佔原圖寬高比
     */
    private Double proportion;


    @Override
    public boolean check() {
        if(null == waterImage){
            throw new RuntimeException("水印圖片(waterImage)不能為空!");
        }
        if(null == super.getPositions()){
            throw new RuntimeException("水印位置(positions)不能為空!");
        }
        return true;
    }
}

4.1 普通的新增圖片水印

4.1.1 範例程式碼

 @Test
    public void testImageWatermark() throws Exception {
        ClassLoader classLoader = ImageHandBuilderTest.class.getClassLoader();
        URL syResource = classLoader.getResource("pic/watermark.png");
        InputStream syInput = syResource.openStream();
        BufferedImage syImage = ImageIO.read(syInput);
        ;
        URL imageResource = classLoader.getResource("pic/example.jpg");
        ImageHandBuilder.load(imageResource)
                .addRule(ImageHandRuleFactory.waterRuleBuilder()
                        .imageBuilder()
                        .waterImage(syImage)
                        .alpha(0.5d)
                        .positions(Positions.BOTTOM_RIGHT).build())
                .toFile("D:\\test\\test-watermark-image.jpg");
    }

4.1.2 實現效果

4.2 新增圖片水印 按佔原圖寬高比

按佔原圖0.3寬高比新增圖片水印

4.2.1 範例程式碼

@Test
    public void testImageWatermarkProportion() throws Exception {
        ClassLoader classLoader = ImageHandBuilderTest.class.getClassLoader();
        URL syResource = classLoader.getResource("pic/watermark.png");
        InputStream syInput = syResource.openStream();
        BufferedImage syImage = ImageIO.read(syInput);
        URL imageResource = classLoader.getResource("pic/example.jpg");
        ImageHandBuilder.load(imageResource)
                .addRule(ImageHandRuleFactory.waterRuleBuilder()
                        .imageBuilder()
                        .waterImage(syImage)
                        .proportion(0.3d)
                        .alpha(0.5d)
                        .positions(Positions.BOTTOM_RIGHT).build())
                .toFile("D:\\test\\test-watermark-image-proportion.jpg");
    }

4.2.2 實現效果

5、新增文字水印

新增文字水印實體類

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.awt.*;

/**
 * @author fhey
 * @date 2022-07-08 18:00:57
 * @description: 
 */
@Data
@SuperBuilder
@AllArgsConstructor
@NoArgsConstructor
public class TextWatermarkRule extends BaseImageWatermarkRule {
    /**
     * 水印文字
     */
    private String text;

    /**
     * 水印文字顏色
     */
    private Color color;

    /**
     * 水印文字字型
     */
    private Font font;

    /**
     * 水印佔原圖寬高比
     */
    private Double proportion;

    /**
     * 和邊緣的間距
     */
    private int margin;

    @Override
    public boolean check() {
        if(null == text){
            throw new RuntimeException("水印文字(水印文字)不能為空!");
        }
        if (null == font){
            throw new RuntimeException("水印文字字型(font)不能為空!");
        }
        if(null == super.getPositions()){
            throw new RuntimeException("水印位置(positions)不能為空!");
        }
        return true;
    }
}

5.1 普通的新增文字水印

5.1.1 範例程式碼

@Test
    public void testTextWatermark() throws Exception {
        ClassLoader classLoader = ImageHandBuilderTest.class.getClassLoader();
        URL imageResource = classLoader.getResource("pic/example.jpg");
        ImageHandBuilder.load(imageResource)
                .addRule(ImageHandRuleFactory.waterRuleBuilder()
                        .textBuilder()
                        .text("@知北遊")
                        .color(Color.WHITE)
                        .font(new Font("宋體", Font.BOLD, 100))
                        .margin(20)
                        .alpha(0.8d)
                        .positions(Positions.BOTTOM_RIGHT).build())
                .toFile("D:\\test\\test-watermark-text.jpg");
    }

5.1.2 實現效果

5.2 新增文字水印 按佔原圖寬高比

按佔原圖0.3寬高比新增文字水印

5.2.1 範例程式碼

@Test
    public void testTextWatermarkProportion() throws Exception {
        ClassLoader classLoader = ImageHandBuilderTest.class.getClassLoader();
        URL imageResource = classLoader.getResource("pic/example.jpg");
        ImageHandBuilder.load(imageResource)
                .addRule(ImageHandRuleFactory.waterRuleBuilder()
                        .textBuilder()
                        .text("@知北遊")
                        .color(Color.WHITE)
                        .font(new Font("宋體", Font.BOLD, 100))
                        .margin(20)
                        .proportion(0.3d)
                        .alpha(0.8d)
                        .positions(Positions.BOTTOM_RIGHT).build())
                .toFile("D:\\test\\test-watermark-text-proportion.jpg");
    }

5.2.2 實現效果

5、新增多行文字水印

新增多行文字水印規則實體類

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import net.coobird.thumbnailator.geometry.Positions;
import java.awt.*;

/**
 * @author fhey
 * @date 2022-07-08 18:00:57
 * @description: 
 */
@Data
@SuperBuilder
@AllArgsConstructor
@NoArgsConstructor
public class MultipleTextWatermarkRule extends BaseImageWatermarkRule {
    /**
     * 水印文字
     */
    private String text;

    /**
     * 水印文字顏色
     */
    private Color color;

    /**
     * 水印文字字型
     */
    private Font font;

    /**
     * 旋轉角度
     */
    private Double rotateDegree;

    /**
     * x軸間距
     */
    private Integer xSpace;

    /**
     * y軸間距
     */
    private Integer ySpace;

    @Override
    public boolean check() {
        super.setPositions(Positions.CENTER);
        if(null == text){
            throw new RuntimeException("水印文字(水印文字)不能為空!");
        }
        if (null == font){
            throw new RuntimeException("水印文字字型(font)不能為空!");
        }
        if (null == rotateDegree){
            throw new RuntimeException("水印透明度(rotateDegree)不能為空!");
        }
        if (null == rotateDegree){
            throw new RuntimeException("旋轉角度(rotateDegree)不能為空!");
        }
        if (null == xSpace){
            throw new RuntimeException("x軸間距(xSpace)不能為空!");
        }
        if (null == ySpace){
            throw new RuntimeException("y軸間距(ySpace)不能為空!");
        }
        return true;
    }
}

5.1 範例程式碼

@Test
    public void testMultipleTextWatermarkProportion() throws Exception {
        ClassLoader classLoader = ImageHandBuilderTest.class.getClassLoader();
        URL imageResource = classLoader.getResource("pic/example.jpg");
        ImageHandBuilder.load(imageResource)
                .addRule(ImageHandRuleFactory.waterRuleBuilder()
                        .multipleTextBuilder()
                        .rotateDegree(-35d)
                        .ySpace(500)
                        .xSpace(350)
                        .color(Color.RED)
                        .font(new Font("宋體", Font.BOLD, 30))
                        .text("三十功名塵與土,八千里路雲和月")
                        .alpha(0.7d)
                        .build())
                .toFile("D:\\test\\test-watermark-multipleText.jpg");
    }

5.2 實現效果

6、最後再來疊個buff,把幾種規則一起使用

5.1 範例程式碼

@Test
    public void testAddBuff() throws Exception {
        ClassLoader classLoader = ImageHandBuilderTest.class.getClassLoader();
        URL imageResource = classLoader.getResource("pic/example.jpg");
        URL syResource = classLoader.getResource("pic/watermark.png");
        InputStream syInput = syResource.openStream();
        BufferedImage syImage = ImageIO.read(syInput);
        ImageHandBuilder.load(imageResource)
                //按比例壓縮
                .addRule(ImageHandRuleFactory.compressRuleBuilder()
                        .compressType(CompressTypeEnum.SCALE_COMPRESS)
                        .scale(0.5d).build())
                //新增圖片水印
                .addRule(ImageHandRuleFactory.waterRuleBuilder()
                        .imageBuilder()
                        .waterImage(syImage)
                        .alpha(0.5d)
                        .positions(Positions.BOTTOM_RIGHT).build())
                //新增文字水印
                .addRule(ImageHandRuleFactory.waterRuleBuilder()
                        .textBuilder()
                        .text("@知北遊")
                        .color(Color.WHITE)
                        .font(new Font("宋體", Font.BOLD, 70))
                        .margin(20)
                        //.proportion(0.3d)
                        .alpha(0.8d)
                        .positions(Positions.BOTTOM_LEFT).build())
                //新增多行文字水印
                .addRule(ImageHandRuleFactory.waterRuleBuilder()
                        .multipleTextBuilder()
                        .rotateDegree(-35d)
                        .ySpace(300)
                        .xSpace(250)
                        .color(Color.RED)
                        .font(new Font("宋體", Font.BOLD, 30))
                        .text("三十功名塵與土,八千里路雲和月")
                        .alpha(0.7d)
                        .build())
                .toFile("D:\\test\\test-addBuff.jpg");
    }

5.2 實現效果

四、主要的程式碼實現

1、建造者的實現

建造者的實現的實現很簡單在ImageHandBuilder類裡建立一個Builder的內部類,通過呼叫靜態方法load()範例化Builder來實現建造者,具體程式碼如下:

package com.fhey.common.file.imagehand;

import cn.hutool.core.io.FileTypeUtil;
import com.fhey.common.file.imagehand.constants.ImageHandConstants;
import com.fhey.common.file.imagehand.rule.CompressRule;
import com.fhey.common.file.imagehand.rule.ImageHandRule;
import com.fhey.common.file.imagehand.base.ImageHandCommand;
import net.coobird.thumbnailator.Thumbnails;
import org.apache.commons.chain.Chain;
import org.apache.commons.chain.Context;
import org.apache.commons.chain.impl.ChainBase;
import org.apache.commons.chain.impl.ContextBase;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.URL;
import java.util.HashSet;
import java.util.Set;

/**
 * @author fhey
 * @date 2022-07-08 17:02:11
 * @description: 圖片處理器
 */
public final class ImageHandBuilder {

    /**
     * 載入圖片
     * @param absolutePath 圖片絕對路徑
     * @return Builder
     */
    public static Builder load(String absolutePath) {
        return load(new File(absolutePath));
    }

    /**
     * 載入圖片
     * @param file 圖片檔案
     * @return Builder
     */
    public static Builder load(File file) {
        try {
            return load(new FileInputStream(file));
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 載入圖片
     * @param file 圖片檔案
     * @return Builder
     */
    public static Builder load(MultipartFile file) {
        try {
            return load(file.getInputStream());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 載入圖片
     * @param url 圖片url
     * @return Builder
     */
    public static Builder load(URL url){
        try {
            return load(url.openStream());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 載入圖片
     * @param inputStream 圖片輸入流
     * @return Builder
     */
    public static Builder load(InputStream inputStream) {
        try (ByteArrayOutputStream byteArrayOutputStream = cloneInputStream(inputStream);
             InputStream inputStream1 = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
             InputStream inputStream2 = new ByteArrayInputStream(byteArrayOutputStream.toByteArray())){
            String fileType = FileTypeUtil.getType(inputStream1);
            BufferedImage image = ImageIO.read(inputStream2);
            inputStream.close();
            return load(image, fileType);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 載入圖片
     * @param image 圖片
     * @param fileType 圖片型別
     * @return Builder
     */
    public static Builder load(BufferedImage image, String fileType) {
        return new Builder(image, fileType);
    }

    private static ByteArrayOutputStream cloneInputStream(InputStream input) {
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int len;
            while ((len = input.read(buffer)) > -1) {
                baos.write(buffer, 0, len);
            }
            baos.flush();
            return baos;
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    public static class Builder {
        /**
         * 圖片處理責任鏈
         */
        private Chain execChain;

        /**
         * 檔案型別
         */
        private String fileType;

        /**
         * 需要處理的圖片
         */
        private BufferedImage image;

        private  Thumbnails.Builder<? extends BufferedImage> thumbnails;

        private Set<Class<? extends ImageHandRule>> ruleSet;

        public Builder(BufferedImage image, String fileType) {
            this.image = image;
            this.fileType = fileType.toUpperCase();
            this.thumbnails = Thumbnails.of(image);
            this.execChain = new ChainBase();
            this.ruleSet = new HashSet<>();
        }

        /**
         * 新增圖片處理規則
         * @param rule 圖片處理規則
         * @return Builder
         */
        public Builder addRule(ImageHandRule rule) {
            //檢查規則
            rule.check();
            //檢查規則是否重複
            if(!rule.canRepeat()){
                if(this.ruleSet.contains(rule.getClass())){
                    throw new RuntimeException("規則:" + rule.getClass().getSimpleName() + "不能重複新增");
                }
                this.ruleSet.add(rule.getClass());
            }
            //給圖片處理責任鏈新增任務
            ImageHandCommand instance = ImageHandCommand.getInstance(rule);
            execChain.addCommand(instance);
            return this;
        }

        /**
         * 返回Thumbnails.Builder 方便自定義處理
         * @return Thumbnails.Builder
         */
        public Thumbnails.Builder<? extends BufferedImage> getThumbnails() {
            return this.thumbnails;
        }

        /**
         * 開始處理圖片
         * @return Builder
         */
        private Builder process() throws Exception {
            Context context = new ContextBase();
            context.put(ImageHandConstants.PROCESS_IMAGE_KEY, this.image);
            context.put(ImageHandConstants.TARGET_KEY, this.thumbnails);
            //開始執行圖片處理責任鏈
            execChain.execute(context);
            thumbnails.outputQuality(1f).outputFormat(this.fileType);
            //如果沒有壓縮規則,則不壓縮
            if(!ruleSet.contains(CompressRule.class)){
                thumbnails.scale(1f);
            }
            return this;
        }

        /**
         * 將處理後的圖片匯出到檔案
         * @param file 圖片檔案
         */
        public void toFile(File file) throws Exception {
            this.process();
            this.thumbnails.toFile(file);
        }

        /**
         * 將處理後的圖片匯出到檔案
         * @param absolutePath 圖片絕對路徑
         */
        public void toFile(String absolutePath) throws Exception {
            this.process();
            this.thumbnails.toFile(absolutePath);
        }

        /**
         * 將處理後的圖片匯出到輸出流
         * @param out 輸出流
         */
        public void toOutputStream(OutputStream out) throws Exception {
            this.process();
            this.thumbnails.toOutputStream(out);
        }
    }
}

2、責任鏈的使用

2.1 初始化責任鏈

工具類使用了apache包下的責任鏈ChainBase。在呼叫load())初始Builder類是建立了一個責任鏈

 	public static Builder load(BufferedImage image, String fileType) {
     	return new Builder(image, fileType);
  	}
  
  	public Builder(BufferedImage image, String fileType) {
         this.image = image;
         this.fileType = fileType.toUpperCase();
         this.thumbnails = Thumbnails.of(image);
         this.execChain = new ChainBase();
         this.ruleSet = new HashSet<>();
  	}

2.2 為責任鏈新增任務

在呼叫新增圖片處理規則方法addRule()時會將圖片處理規則封裝成責任鏈命令並往初始化後的責任鏈裡新增任務

 /**
         * 新增圖片處理規則
         * @param rule 圖片處理規則
         * @return Builder
         */
        public Builder addRule(ImageHandRule rule) {
            //檢查規則
            rule.check();
            //檢查規則是否重複
            if(!rule.canRepeat()){
                if(this.ruleSet.contains(rule.getClass())){
                    throw new RuntimeException("規則:" + rule.getClass().getSimpleName() + "不能重複新增");
                }
                this.ruleSet.add(rule.getClass());
            }
            //給圖片處理責任鏈新增任務
            ImageHandCommand instance = ImageHandCommand.getInstance(rule);
            execChain.addCommand(instance);
            return this;
        }

責任鏈任務範例工廠:

import com.fhey.common.file.imagehand.constants.ImageHandConstants;
import com.fhey.common.file.imagehand.hand.BaseImageHand;
import com.fhey.common.file.imagehand.rule.ImageHandRule;
import lombok.Data;
import net.coobird.thumbnailator.Thumbnails;
import org.apache.commons.chain.Command;
import org.apache.commons.chain.Context;

import java.awt.image.BufferedImage;
import java.io.InputStream;

@Data
public
class ImageHandCommand implements Command {
    private ImageHandCommand(){}

    /**
     * 獲取圖片處理器責任鏈任務範例
     * @param rule 圖片處理規則
     * @return 圖片處理器
     */
    public static ImageHandCommand getInstance(ImageHandRule rule) {
        ImageHandCommand imageHandCommand = new ImageHandCommand();
        imageHandCommand.setRule(rule);
        BaseImageHand imageHand = ImageHandConstants.getImageHand(rule.getClass());
        if (imageHand == null) {
            throw new RuntimeException("規則:" + rule.getClass().getSimpleName() + "未找到對應的處理器");
        }
        imageHandCommand.setImageHand(imageHand);
        return imageHandCommand;
    }

    /**
     * 圖片處理規則
     */
    private ImageHandRule rule;

    /**
     * 圖片處理器
     */
    private BaseImageHand imageHand;


    @Override
    public boolean execute(Context context) {
        BufferedImage image =(BufferedImage) context.get(ImageHandConstants.PROCESS_IMAGE_KEY);
        Thumbnails.Builder<? extends InputStream> thumbnails = (Thumbnails.Builder<? extends InputStream>) context.get(ImageHandConstants.TARGET_KEY);
        return this.imageHand.hand(thumbnails, this.rule, image);
    }
}

圖片處理靜態類:

/**
 * @author fhey
 * @date 2022-07-08 17:20:29
 * @description: 
 */
public class ImageHandConstants {

    public final  static  String PROCESS_IMAGE_KEY = "image";

    public final  static  String TARGET_KEY = "target";

    private final static Map<Class<? extends ImageHandRule>, BaseImageHand> ruleHandMap = new HashMap<>();

    static {
        ruleHandMap.put(RegionRule.class, new RegionHandImageHand());
        ruleHandMap.put(CompressRule.class, new CompressHandImageHand());
        ruleHandMap.put(ImageWatermarkRule.class, new ImageHandWatermarkHand());
        ruleHandMap.put(TextWatermarkRule.class, new TextImageHandWatermarkHand());
        ruleHandMap.put(MultipleTextWatermarkRule.class, new MultipleTextWatermarkHand());
    }

    public static BaseImageHand getImageHand(Class<? extends ImageHandRule> ruleClass) {
        return ruleHandMap.get(ruleClass);
    }
}

3、執行責任鏈

在最後執行的圖片輸出方法toFile()、toOutputStream()中都會呼叫process()方法

		/**
         * 將處理後的圖片匯出到檔案
         * @param file 圖片檔案
         */
        public void toFile(File file) throws Exception {
            this.process();
            this.thumbnails.toFile(file);
        }

        /**
         * 將處理後的圖片匯出到檔案
         * @param absolutePath 圖片絕對路徑
         */
        public void toFile(String absolutePath) throws Exception {
            this.process();
            this.thumbnails.toFile(absolutePath);
        }

        /**
         * 將處理後的圖片匯出到輸出流
         * @param out 輸出流
         */
        public void toOutputStream(OutputStream out) throws Exception {
            this.process();
            this.thumbnails.toOutputStream(out);
        }

而在process()方法中執行了執行責任鏈

		/**
         * 開始處理圖片
         * @return Builder
         */
        private Builder process() throws Exception {
            Context context = new ContextBase();
            context.put(ImageHandConstants.PROCESS_IMAGE_KEY, this.image);
            context.put(ImageHandConstants.TARGET_KEY, this.thumbnails);
            //開始執行圖片處理責任鏈
            execChain.execute(context);
            thumbnails.outputQuality(1f).outputFormat(this.fileType);
            //如果沒有壓縮規則,則不壓縮
            if(!ruleSet.contains(CompressRule.class)){
                thumbnails.scale(1f);
            }
            return this;
        }

4、圖片裁剪的實現

圖片裁剪的實現的實現很簡單其實就是使用thumbnailator的sourceRegion()方法,所謂按比例裁剪其實就是用原圖的長寬 * 比例算出需要裁剪的長寬,再進行裁剪。

實現程式碼

public class RegionHandImageHand extends BaseImageHand<RegionRule> {
    @Override
    public boolean hand(Thumbnails.Builder<? extends InputStream> thumbnails, RegionRule rule, BufferedImage image) {
        if (RegionTypeEnum.SCALE_REGION.equals(rule.getRegionType())) {
            //按比例裁剪
            Double scale = rule.getScale();
            thumbnails.sourceRegion(rule.getPositions(), (int) (image.getWidth() * scale), (int) (image.getHeight() * scale));
        } else if (RegionTypeEnum.WIDTH_HEIGHT_REGION.equals(rule.getRegionType())) {
            //按寬高裁剪
            thumbnails.sourceRegion(rule.getPositions(), rule.getWidth(), rule.getHeight());
        }
        return false;
    }
}

5、圖片壓縮的實現

5.1 按比例壓縮圖片

按比例壓縮圖片使用thumbnailator的scale()方法

5.2 按長寬壓縮圖片

按長寬壓縮圖片使用thumbnailator的size()方法,因為thumbnailator提供的保持長寬比的方式和我的一些使用場景不一致,所以有自定義實現了一個保持長寬比的方式

5.3 實現程式碼

import com.fhey.common.file.imagehand.enums.CompressTypeEnum;
import com.fhey.common.file.imagehand.enums.KeepAspectRatioEnum;
import com.fhey.common.file.imagehand.rule.CompressRule;
import net.coobird.thumbnailator.Thumbnails;

import java.awt.image.BufferedImage;
import java.io.InputStream;
import java.math.BigDecimal;

/**
 * @author fhey
 * @date 2022-07-08 17:02:11
 * @description:
 */
public class CompressHandImageHand extends BaseImageHand<CompressRule> {

    @Override
    public boolean hand(Thumbnails.Builder<? extends InputStream> thumbnails, CompressRule rule, BufferedImage image) {
        if (CompressTypeEnum.SCALE_COMPRESS.equals(rule.getCompressType())){//按比例縮放
            thumbnails.scale(rule.getScale());
        } else if (CompressTypeEnum.WIDTH_HEIGHT_COMPRESS.equals(rule.getCompressType())) {//按寬高縮放
            KeepAspectRatioEnum keepAspectRatio = rule.getKeepAspectRatio();
            if(null == keepAspectRatio){
                keepAspectRatio = KeepAspectRatioEnum.NO_KEEP;
            }
            switch (keepAspectRatio){
                case KEEP_BY_WITH:
                    thumbnails.size(rule.getWidth(), rule.getHeight()).keepAspectRatio(true);
                    break;
                case KEEP_AUTO:
                    compressKeepAspectRatio(thumbnails, rule, image);
                    break;
                default:
                    thumbnails.size(rule.getWidth(), rule.getHeight()).keepAspectRatio(false);
                    break;
            }
        }
        return false;
    }

    private void compressKeepAspectRatio(Thumbnails.Builder<? extends InputStream> thumbnails, CompressRule rule, BufferedImage image) {
        int width;
        int height;
        BigDecimal aspectRatio = BigDecimal.valueOf(image.getWidth()).divide(BigDecimal.valueOf(image.getHeight()),2,BigDecimal.ROUND_HALF_UP);
        BigDecimal aspectRatioWidth = BigDecimal.valueOf(rule.getHeight()).multiply(aspectRatio);
        BigDecimal aspectRatioHeight = BigDecimal.valueOf(rule.getWidth()).divide(aspectRatio,2,BigDecimal.ROUND_HALF_UP);

        if(aspectRatioHeight.compareTo(BigDecimal.valueOf(rule.getHeight())) < 0) {
            height = rule.getHeight();
            width = aspectRatioWidth.intValue();
        } else {
            width = rule.getWidth();
            height = aspectRatioHeight.intValue();
        }
        thumbnails.size(width, height).keepAspectRatio(true);
    }
}

6、新增水印的實現

6.1 新增水印通用程式碼

新增水印放入方式使用thumbnailator的watermark()方法,因為thumbnailator只支援新增圖片水印,所以新增文字水印和多行文字水印都是先使用Graphics2D繪製一張圖片在呼叫watermark()方法新增水印的。所有工具類先建立了新增水印的規則基礎類別BaseImageWatermarkRule和新增水印的處理類基礎類別AbstractWatermarkHand

BaseImageWatermarkRule:

import com.fhey.common.file.imagehand.rule.ImageHandRule;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import net.coobird.thumbnailator.geometry.Positions;

@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public abstract class BaseImageWatermarkRule implements ImageHandRule {

    /**
     * 水印位置
     */
    private Positions positions;

    /**
     * 水印透明度
     */
    private Double alpha;

    @Override
    //設定水印可以重複設定
    public boolean canRepeat() {
        return true;
    }
}

AbstractWatermarkHand:

import com.fhey.common.file.imagehand.rule.watermark.BaseImageWatermarkRule;
import com.fhey.common.file.imagehand.hand.BaseImageHand;
import net.coobird.thumbnailator.Thumbnails;

import java.awt.image.BufferedImage;
import java.io.InputStream;

public abstract class AbstractWatermarkHand<T extends BaseImageWatermarkRule> extends BaseImageHand<T> {
    @Override
    public boolean hand(Thumbnails.Builder<? extends InputStream> thumbnails, T rule, BufferedImage image) {
        if(rule.getAlpha() == null){
            rule.setAlpha(1d);
        }
        BufferedImage waterImg = getWaterImg(image, rule);
        thumbnails.watermark(rule.getPositions(), waterImg, rule.getAlpha().floatValue());
        return false;
    }

    /**
     * 獲取水印圖片
     * @param srcImg 原圖
     * @param rule 水印規則
     * @return Builder
     */
     abstract BufferedImage getWaterImg(BufferedImage srcImg, T rule);
}

ImageHandWatermarkHand(新增圖片水印處理類)、TextHandWatermarkHand(新增文字水印處理類)、MultipleTextWatermarkHand(新增多行文字水印處理類)繼承新增水印的處理類基礎類別AbstractWatermarkHand後只需要實現獲取水印圖片的方法getWaterImg()。

6.2 新增圖片水印的實現

新增圖片水印的實現的獲取水印方法很簡單,只有圖片處理規則裡沒有proportion(水印佔原圖寬高比),就直接返回水印圖片,如果傳了則需要調整一下水印的長寬再返回。

程式碼實現如下:

import com.fhey.common.file.imagehand.rule.watermark.ImageWatermarkRule;
import net.coobird.thumbnailator.Thumbnails;
import java.awt.image.BufferedImage;
import java.io.IOException;

/**
 * @author fhey
 * @date 2022-07-08 18:31:09
 * @description: 
 */
public class ImageHandWatermarkHand extends AbstractWatermarkHand<ImageWatermarkRule> {
    /**
     * 獲取水印圖片
     * @param srcImg 原圖
     * @param rule 圖片水印規則
     * @return Builder
     */
    @Override
    public BufferedImage getWaterImg(BufferedImage srcImg, ImageWatermarkRule rule){
        try {
            if (rule.getProportion() == null){
                return rule.getWaterImage();
            }
            BufferedImage waterImg = rule.getWaterImage();
            int waterWidth = (int) (srcImg.getWidth() * rule.getProportion());
            int waterHeight = waterWidth * waterImg.getHeight() / waterImg.getWidth();
            return Thumbnails.of(waterImg).size(waterWidth, waterHeight).keepAspectRatio(true).asBufferedImage();
        } catch (IOException e){
           throw new RuntimeException(e);
        }
    }
}

6.3 新增文字水印的實現

因為thumbnailator的watermark()方法只支援新增圖片水印,所以先使用Graphics2D繪製一張圖片。

程式碼實現如下:

import cn.hutool.core.lang.Tuple;
import com.fhey.common.file.imagehand.rule.watermark.TextWatermarkRule;
import net.coobird.thumbnailator.geometry.Positions;
import java.awt.*;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;

/**
 * @author fhey
 * @date 2022-07-08 18:31:39
 * @description: 
 */
public class TextHandWatermarkHand extends AbstractWatermarkHand<TextWatermarkRule> {
    /**
     * 獲取水印圖片
     * @param srcImg 原圖
     * @param rule 文字水印規則
     * @return Builder
     */
    //@Override
    public BufferedImage getWaterImg(BufferedImage srcImg, TextWatermarkRule rule) {
        int srcWidth = srcImg.getWidth();
        int srcHeight = srcImg.getHeight();
        int margin = rule.getMargin();
        String text = rule.getText();
        ///確定字型
        Font font;
        if (rule.getProportion() != null) {
            double fontLength = srcWidth * rule.getProportion();
            Integer frontSize = (int) (fontLength / text.length());
            font = new Font(rule.getFont().getName(), rule.getFont().getStyle(), frontSize);
        } else {
            font = rule.getFont();
        }
        FontMetrics metrics = new FontMetrics(font) {};
        Rectangle2D bounds = metrics.getStringBounds(text, null);
        //效果一樣
//        FontRenderContext frc = new FontRenderContext(null, true, true);
//        Rectangle2D bounds = font.getStringBounds(text, frc);
        // 字元寬度
        int textWidth = (int) bounds.getWidth();
        // 字元高度
        int textHeight = (int) bounds.getHeight();
         // 根據字元寬度字、符高度設定畫布大小
        BufferedImage sysImage = new BufferedImage(textWidth + margin, textHeight + margin, BufferedImage.TYPE_INT_RGB);
        // 獲取畫筆物件
        Graphics2D graphics = sysImage.createGraphics();
        //設定圖片透明
        sysImage = graphics.getDeviceConfiguration().createCompatibleImage(textWidth + margin, textHeight + margin, Transparency.TRANSLUCENT);
        graphics = sysImage.createGraphics();
        //設定字型
        graphics.setFont(font);
        //設定水印字型顏色
        Color color;
        if (rule.getColor() != null) {
            color = rule.getColor();
        } else {
            color = new Color(0, 0, 0, 3);
        }
        graphics.setColor(color);
        // 消除java.awt.Font字型的鋸齒
        graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        // 設定水印透明度
        graphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, rule.getAlpha().floatValue()));
        Tuple marginInfo = getMarginInfo(rule.getPositions(), margin);
        graphics.drawString(text,  marginInfo.get(0) , metrics.getAscent() + (int) marginInfo.get(1));
        graphics.dispose();
        return sysImage;
    }

    public Tuple getMarginInfo(Positions positions, Integer margin) {
        int xMargin = 0;
        int yMargin = 0;
        switch (positions){
            case TOP_LEFT:
                xMargin = margin;
                yMargin = margin;
                break;
            case TOP_CENTER:
                xMargin = margin/2;
                yMargin = margin;
                break;
            case TOP_RIGHT:
                xMargin = 0;
                yMargin = margin;
                break;
            case CENTER_LEFT:
                xMargin = margin;
                yMargin = margin/2;
                break;
            case CENTER:
                xMargin = margin/2;
                yMargin = margin/2;
                break;
            case CENTER_RIGHT:
                xMargin = 0;
                yMargin = margin/2;
                break;
            case BOTTOM_LEFT:
                xMargin = margin;
                yMargin = 0;
                break;
            case BOTTOM_CENTER:
                xMargin = margin/2;
                yMargin = 0;
                break;
            case BOTTOM_RIGHT:
                xMargin = 0;
                yMargin = 0;
                break;
        }
        return new Tuple(xMargin, yMargin);
    }
}

6.4 新增多行文字水印的實現

因為thumbnailator的watermark()方法只支援新增圖片水印,所以先使用Graphics2D繪製一張圖片。

程式碼實現如下:

import com.fhey.common.file.imagehand.rule.watermark.MultipleTextWatermarkRule;
import net.coobird.thumbnailator.geometry.Positions;
import java.awt.*;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.Optional;

public class MultipleTextWatermarkHand extends AbstractWatermarkHand<MultipleTextWatermarkRule> {

    /**
     * 獲取全螢幕水印圖片
     * @param srcImg 原圖
     * @param rule 文字水印規則
     * @return Builder
     */
    @Override
    public BufferedImage getWaterImg(BufferedImage srcImg, MultipleTextWatermarkRule rule) {
        rule.setPositions(Positions.CENTER);
        int srcWidth = srcImg.getWidth();
        int srcHeight = srcImg.getHeight();
        String text = rule.getText();
        Optional<MultipleTextWatermarkRule> multipleTextRuleOptional = Optional.ofNullable(rule);
        // 將畫布大小繪製成原圖大小
        BufferedImage sysImage= new BufferedImage(srcWidth, srcHeight, BufferedImage.TYPE_INT_RGB);
        // 獲取畫筆物件
        Graphics2D graphics = sysImage.createGraphics();
        //清除矩形區域
        graphics.clearRect(0, 0, srcWidth, srcHeight);
        // 設定繪圖區域透明
        sysImage = graphics.getDeviceConfiguration().createCompatibleImage(srcWidth, srcHeight, Transparency.TRANSLUCENT);
        graphics = sysImage.createGraphics();
        FontMetrics metrics = new FontMetrics(rule.getFont()) {};
        Rectangle2D bounds = metrics.getStringBounds(text, null);
        // 字元寬度
        int textWidth = (int) bounds.getWidth();
        // 字元高度
        int textHeight = (int) bounds.getHeight();
        //設定字型
        graphics.setFont( rule.getFont());
        //設定水印字型顏色
        Color color;
        if (rule.getColor() != null) {
            color = rule.getColor();
        } else {
            color = new Color(0, 0, 0);
        }
        graphics.setColor(color);
        // 消除java.awt.Font字型的鋸齒
        graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        // 設定水印透明度
        graphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, rule.getAlpha().floatValue()));
        // 設定傾斜角度
        graphics.rotate(Math.toRadians(multipleTextRuleOptional.map(MultipleTextWatermarkRule::getRotateDegree).orElse(0d))
                , (double) srcImg.getWidth() / 2
                , (double) srcImg.getHeight() / 2);
        int xCoordinate = -srcWidth / 2, yCoordinate;
        // 迴圈新增水印
        while (xCoordinate < srcWidth * 1.5) {
            yCoordinate = -srcHeight / 2;
            while (yCoordinate < srcHeight * 1.5) {
                graphics.drawString(rule.getText(), xCoordinate, yCoordinate);
                yCoordinate += textHeight + multipleTextRuleOptional.map(MultipleTextWatermarkRule::getYSpace).orElse(textHeight);
            }
            xCoordinate += textWidth + multipleTextRuleOptional.map(MultipleTextWatermarkRule::getXSpace).orElse(textWidth / text.length() * 2);
        }
        graphics.dispose();
        return sysImage;
    }
}

總結

​ 到此這篇圖片處理工具類的使用方式和實現原理都已經介紹完了,這個工具類實現圖片裁剪、圖片壓縮、新增圖片、新增文字水印和新增多行文字水印功能。因為第一次封裝這樣的工具類難免有些簡陋,甚至可能有些錯誤,還望各位大佬海涵和指正。

程式碼的git倉庫如下

fhey223/fhey-common-imagehand (github.com)

最後