- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
最近用结合thumbnailator和Graphics2D封装了一个图片工具类,目前可以实现图片的裁剪、压缩、添加图片水印、文字水印、多行文字水印等功能,同时该工具类的实现使用了建造者模式、责任链模式、工厂模式、策略模式等多种设计模式,感觉图片处理的功能有一定的通用性,所以这次写一篇文章来分享一下这个工具类的使用方式和实现原理,代码不足之处还望大家指正,文末也会提供代码的github地址.
<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>
首先这个处理类提供了一个建造简化了工具栏使用,下面一个简单的使用示例,实现了图的裁剪裁剪,从这个示例代码可以看出这个图片处理工具类的建造者主要由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");
加载需要处理的图片的方法是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);
添加图片处理规则的方法是addRule(),支持RegionRule(裁剪规则)、CompressRule(压缩规则)、ImageWatermarkRule(图片水印规则)、TextWatermarkRule(文字水印规则)、MultipleTextWatermarkRule(多行文字水印规则)。规则的添加支持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");
}
@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");
}
输出处理后的图片的方法是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:
图片裁剪提供了按长宽裁剪和按比例裁剪两种裁剪方式 。
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;
}
}
从图片中间裁剪300 X 300的区域 。
@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");
}
由下图可以看出处理后的图片正好是300 X 300,但是只截取了中间一小块 。
从上中开始按图片0.5的比列裁剪 。
@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");
}
有下图可以看出裁剪之后长宽都是原图的一半,因为从上中开始裁剪所以原图下方一些图片被裁剪掉了 。
图片压缩提供了按长宽压缩和按比例压缩两种压缩方式,其中按长宽压缩又提供了不保持比列、但宽度保持比列、自动保持比列.
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;
}
}
将图片压缩成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");
}
由下图可以看出处理后的图片是300 X300,但是图片变形严重 。
将图片压缩成300 X 300,这里的缩 按宽度保持比例采用了thumbnailator提供的默认的保持长宽比列的方式 。
@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");
}
由下图可以看出thumbnailator默认的保持长宽比的做法,将宽度处理成了300,但是高度只有169,不到300了 。
将图片压缩成300 X 300,因为thumbnailator默认的保持长宽比的做法在某些场景下不符合需求,所以我有自己扩展了自定义的一个保持长宽比例的做法,即在保持长宽比的情况下让处理后的图片宽高不会低于设定值.
@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");
}
由下图可以看出处理后的图片,宽度533,高度300 。
按图片0.5的比列压缩 。
@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");
}
可以看出压缩之后长宽都是原图的一半 。
在需要展示图片的列表中,比如电商中常见的商品列表,为了提升列表的性能,列表里展示的图片往往都是缩略图。但是通上面的图片压缩和图片裁剪把图片处理成300 X300可以发现处理后的图片图片变形严重要么图片细节缺失过多展示效果不好,那我们应该怎么生成一张符合尺寸且保证展示效果的缩略图呢?方法就是将图片压缩和图片裁剪结合起来使用,先根据缩略图的尺寸要求压缩图片(压缩后的长宽都不能低于缩略图的尺寸),然后再根据缩略图的尺寸裁剪图片。代码示例和展示效果如下.
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");
所以正确的做法是需要压缩图片之后先输出图片,在第二次调用建造者加载图片,再进行图片裁剪.
正确的代码示例:
@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");
}
水印规则基类:
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;
}
}
@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");
}
按占原图0.3宽高比添加图片水印 。
@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");
}
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;
}
}
@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");
}
按占原图0.3宽高比添加文字水印 。
@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");
}
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;
}
}
@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");
}
@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");
}
建造者的实现的实现很简单在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);
}
}
}
工具类使用了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<>();
}
在调用添加图片处理规则方法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);
}
}
在最后执行的图片输出方法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;
}
图片裁剪的实现的实现很简单其实就是使用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;
}
}
按比例压缩图片使用thumbnailator的scale()方法 。
按长宽压缩图片使用thumbnailator的size()方法,因为thumbnailator提供的保持长宽比的方式和我的一些使用场景不一致,所以有自定义实现了一个保持长宽比的方式 。
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);
}
}
添加水印放入方式使用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().
添加图片水印的实现的获取水印方法很简单,只有图片处理规则里没有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);
}
}
}
因为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);
}
}
因为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) 。
最后 。
最后此篇关于封装图片处理工具类,实现图片的裁剪、压缩、图片水印、文字水印、多行文字水印等功能的文章就讲到这里了,如果你想了解更多关于封装图片处理工具类,实现图片的裁剪、压缩、图片水印、文字水印、多行文字水印等功能的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我有点想做 the reverse of this. 我不想解压缩并将收集文件添加到 S3 应用户要求: 生成一堆xml文件 使用一些图像(托管在 s3 上的预先存在的图像)压缩 xml 文件 下载
将此添加到域的虚拟主机后 AddOutputFilterByType DEFLATE application/javascript text/javascript text/css 响应头不包含任何内
在 Apache Im 中,通过将以下内容添加到我的 .htaccess 文件来启用输出压缩: # compress text, html, javascript, css, xml: AddOutp
是否可以以压缩格式将请求数据从浏览器发送到服务器? 如果是,我们该怎么做? 最佳答案 压缩从浏览器发送到服务器的数据是不受 native 支持 在浏览器中。 您必须找到一种解决方法,使用客户端语言(可
我正在寻找可以压缩JavaScript源代码的工具。我发现一些网络工具只能删除空格字符?但也许存在更好的工具,可以压缩用户的函数名称、字段名称、删除未使用的字段等。 最佳答案 经常用来压缩JS代码的工
使用赛马博彩场景,假设我有许多单独的投注来预测比赛的前 4 名选手 (superfecta)。 赌注如下... 1/2/3/4 1/2/3/5 1/2/4/3 1/2/4/5 1/2/5/3
我是一名实习生,被要求对 SQL 2008 数据压缩进行一些研究。我们想将 Outlook 电子邮件的几个部分存储在一个表中。问题是我们想将整个电子邮件正文存储在一个字段中,然后又想压缩它。使用 Ch
我目前有一个系统,用户可以在其中上传 MP4 文件,并且可以在移动设备上下载该文件。但有时,这些视频的大小超过 5MB,在我国,大多数人使用 2G。因此,下载大型视频通常需要 15-20 分钟。 有什
假设我有一个带有类型列的简单文档表: Documents Id Type 1 A 2 A 3 B 4 C 5 C 6 A 7 A 8 A 9 B 10 C 用户
我有一个较大字符串中的(子)字符串位置的 data.frame。数据包含(子)字符串的开头及其长度。可以很容易地计算出(子)字符串的结束位置。 data1 start length end #>
我想知道是否 文件加密算法可以设计成它也可以执行文件压缩的事件(任何活生生的例子?)。 我也可以将它集成到移动短信服务中,我的意思是短信吗? 另外我想知道二进制文件...如果纯文本文件以二进制编码
关闭。这个问题不满足Stack Overflow guidelines .它目前不接受答案。 想改善这个问题吗?更新问题,使其成为 on-topic对于堆栈溢出。 7年前关闭。 Improve thi
我们有几个具有大量 JavaScript 的 Java 项目,目前我们使用的是旧版本的 YUICompressor (2.4.2)。然而,我在这篇博文中发现 YUICompressor 正在 depr
从之前关于尝试提高网站性能的文章中,我一直在研究 HTTP 压缩。我读过有关在 IIS 中设置它的信息,但它似乎是所有 IIS 应用程序池的全局事物,我可能不允许这样做,因为还有另一个站点在其上运行。
我有一个 REST 服务,它返回一大块 XML,大约值(value) 150k。 例如http://xmlservice.com/services/RestService.svc/GetLargeXM
我正在尝试获取一个简单的 UglifyJS (v2.3.6) 示例来处理压缩。 具体来说,“未使用”选项,如果从未使用过,变量和函数将被删除。 这是我在命令行上的尝试: echo "function
我正在开发一个项目,如果我的磁盘出现问题,我将在使用 ZLIB 压缩内存块后将其发送到另一个磁盘。然后我计划下载该转储并用于进一步调试。这种压缩和上传将一次完成一个 block - 比如说 1024
LZW 压缩算法在压缩后增加了位大小: 这是压缩函数的代码: // compression void compress(FILE *inputFile, FILE *outputFile) {
我的问题与如何在 3D 地形上存储大量信息有关。这些信息应该是 secret 的,因为它们非常庞大,也应该被压缩。我选择了文件存储,现在我想知道将对象数据加密/压缩(或压缩/加密)到文件的最佳做法。
我使用以下代码来压缩我的文件并且效果很好,但我只想压缩子文件夹而不是在压缩文件中显示树的根。 public boolean zipFileAtPath(String sourcePath, Strin
我是一名优秀的程序员,十分优秀!