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

来源:https://www.cnblogs.com/fhey/archive/2023/07/12/17544531.html
-Advertisement-
Play Games

最近用結合thumbnailator和Graphics2D封裝了一個圖片工具類,目前可以實現圖片的裁剪、壓縮、添加圖片水印、文字水印、多行文字水印等功能,同時該工具類的實現使用了建造者模式、責任鏈模式、工廠模式、策略模式等多種設計模式,感覺圖片處理的功能有一定的通用性,所以這次寫一篇文章來分享一下這... ...


目錄

一、前言

最近用結合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();
        }
 

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文旨在簡明扼要說明各回收器調優參數,如有疏漏歡迎指正。 #### 1、JDK版本 以下所有優化全部基於JDK8版本,強烈建議低版本升級到JDK8,並儘可能使用update_191以後版本。 #### 2、如何選擇垃圾回收器 響應優先應用:面向C端對響應時間敏感的應用,堆記憶體8G以上建議選擇G1,堆 ...
  • # 文件的上傳和下載 在上網的時候我們常常遇到文件上傳的情況,例如上傳頭像、上傳資料等:當然除了上傳,遇見下載的情況 也很多,接下來看看我們servlet中怎麼實現文件的上傳和下載。 ## 文件上傳 文件上傳涉及到前臺頁面的編寫和後臺伺服器端代碼的編寫,前臺發送文件,後臺接收並保存文件,這才是 一個 ...
  • # Django ## select_related 和 prefetch_related 函數 對 QuerySet 查詢的優化 在資料庫有外鍵的時候,使用 select_related() 和 prefetch_related() 能夠很好的減小資料庫請求的次數,從而提升性能。本文經過一個簡單的 ...
  • 一、內核驅動簽名初篇 1.大概聊一聊現有驅動情況 1.開啟安全啟動(Secure Boot) 1.使用微軟WHQL簽名 2.使用2013-2015年簽發的驅動簽名,已過期未吊銷未拉黑(不知道什麼時候打個補丁會修複). 2.關閉安全啟動(Secure Boot) 1.使用微軟WHQL簽名 2.使用過期 ...
  • 最近自動答題的外包很多,來給大家分享一下如何用Python來實現自動答題。 好了話不多說,我們開始操作。 首先你需要準備這些 環境使用 Python 3.8 解釋器 Pycharm 編輯器 模塊使用 import requests > 數據請求模塊 pip install requests impo ...
  • ## 多線程 ### 線程的實現方式 1. 繼承 Thread 類:一旦繼承了 Thread 類,就不能再繼承其他類了,可拓展性差 2. 實現 Runnable 介面:仍然可以繼承其他類,可拓展性較好 3. 使用線程池 #### 繼承Thread 類 ​ 不能通過線程對象調用 run() 方法,需要 ...
  • 與java類似,繼承的出現是為了提高代碼的重覆利用率,避免多次輸入同樣的代碼。而超類就是java中的父類。 # 1.繼承 要指定超類,可在定義類時,在class語句中的類名後加上超類名 * 基類就是超類,派生類就是子類 格式 ``` class Dog: # pass class Bobo(Dog) ...
  • 它來了它來了,最後一期終於來了。理論上該講的全都講完了,只剩下那個拖了好幾期的自定義控制項和一個比較沒有存在感的設置功能沒有講。所以這次就重點介紹它們倆吧。 首先我們快速瀏覽下設置的實現,上圖: 然後是控制器代碼: SettingsController.java package controllers ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...