java原生对于图片的编辑处理并没有特别友好,而且问题也有不少,那么作为一个java后端,如果要提供图片的编辑服务可以怎么办?也得想办法去支持业务需求,本片博文基于此进行展开
I. 调研
首先最容易想到的就是目前是不是已经有了相关的开源库,直接用不就很high了嘛,git上搜一下
1. thumbnailator
差不多四年都没有更新了,基于awt进行图片的编辑处理,目前提供了基本的图片编辑接口,开始用了一段时间,有几个绕不够去的坑,所以最后放弃了
使用姿势:
| 12
 3
 4
 5
 
 | <dependency><groupId>net.coobird</groupId>
 <artifactId>thumbnailator</artifactId>
 <version>0.4.8</version>
 </dependency>
 
 | 
一个使用case:
| 12
 3
 4
 5
 6
 
 | BufferedImage originalImage = ImageIO.read(new File("original.jpg"));
 BufferedImage thumbnail = Thumbnails.of(originalImage)
 .size(200, 200)
 .rotate(90)
 .asBufferedImage();
 
 | 
问题说明:
上面两个问题中,第二个精度丢失在某些对图片质量有要求的场景下比较严重,如果业务场景没那么将就的话,用这个库还是可以减少很多事情的,下面基于ImageMagic的接口设计,很大程度上参考了该工程的使用规范,因为使用起来(+阅读)确实特别顺畅
2. simpleimage
阿里的开源库,文档极其欠缺,而且良久没有人维护,没有实际使用过,感觉属于玩票的性质(个人猜测是KPI为导向下的产物)
如果想造轮子的话,参考它的源码,某些图片的处理方案还是不错的
3. imagemagic + im4java
ImageMagic/GraphicMagic 是c++的图象处理软件,很多服务基于此来搭建图片处理服务的
- 优点:稳定、性能高、支持接口多、开箱即用、靠谱
- 缺点:得提前配置环境,基本上改造不动,内部有问题也没辙
这个方法也是下面的主要讲述重点,放弃Thumbnailator选择imagemagic的原因如下:
- 支持更多的服务功能(比Thumbnailator多很多的接口)
- 没有精度丢失问题
- 没有图片失真问题(颜色变化,alpha值变化问题)
II. 环境准备
首先得安装ImageMagic环境,有不少的第三方依赖,下面提供linux和mac的安装过程
1. linux安装过程
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 
 | yum install libjpeg-devel
 yum install libpng-devel
 yum install libwebp-devel
 
 
 
 安装jpeg 包 `wget ftp://223.202.54.10/pub/web/php/libjpeg-6b.tar.gz`
 安装webp 包 `wget http://www.imagemagick.org/download/delegates/libwebp-0.5.1.tar.gz`
 安装png 包 `wget http://www.imagemagick.org/download/delegates/libpng-1.6.24.tar.gz`
 
 
 
 wget http://www.imagemagick.org/download/ImageMagick.tar.gz
 
 tar -zxvf ImageMagick.tar.gz
 cd ImageMagick-7.0.7-28
 ./configure; sudo make; sudo make install
 
 | 
安装完毕之后,进行测试
| 12
 3
 4
 5
 6
 7
 
 | $ convert --version
 Version: ImageMagick 7.0.7-28 Q16 x86_64 2018-04-17 http://www.imagemagick.org
 Copyright: © 1999-2018 ImageMagick Studio LLC
 License: http://www.imagemagick.org/script/license.php
 Features: Cipher DPC HDRI OpenMP
 Delegates (built-in): fontconfig freetype jng jpeg lzma png webp x xml zlib
 
 | 
2. mac安装过程
依赖安装
| 12
 3
 4
 5
 
 | sudo brew install jpegsudo brew install libpng
 sudo brew install libwebp
 sudo brew install GraphicsMagick
 sudo brew install ImageMagick
 
 | 
源码安装方式与上面一致
3. 问题及修复
如果安装完毕之后,可能会出现下面的问题
提示找不到png依赖:
执行 convert 提示linux shared libraries 不包含某个库
- 临时方案:export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
- 永久方案: | 12
 3
 
 | vi /etc/ld.so.conf在这个文件里加入:/usr/local/lib 来指明共享库的搜索位置
 然后再执行/sbin/ldconf
 
 |  
 
4. 常见Convert命令
imagemagic的场景使用命令如下
裁图 
- convert test.jpg -crop 640x960+0+0 output.jpg
旋转 
- convert test.jpg -rotate 90 output.jpg
缩放 
- convert test.jpg -resize 200x200 output.jpg
强制宽高缩放 
- convert test.jpg -resize 200x200! output.jpg
缩略图 
- convert -thumbnail 200x300 test.jpg thumb.jpg
上下翻转:
- convert -flip foo.png bar.png
左右翻转:
- convert -flop foo.png bar.png
水印:
- composite -gravity northwest -dissolve 100 -geometry +0+0 water.png temp.jpg out.jpg
添加边框 :
- convert -border 6x6 -bordercolor “#ffffff” test.jpg bord.jpg
去除边框 :
- convert -thumbnail 200x300 test.jpg thumb.jpg
III. 接口设计与实现
java调用ImageMagic的方式有两种,一个是基于命令行的,一种是基于JNI的,我们选则im4java来操作imagemagic的接口(基于命令行的操作)
目标:
对外的使用姿势尽可能如 Thumbnailtor,采用builder模式来设置参数,支持多种输入输出
1. im4java使用姿势
几个简单的case,演示下如何使用im4java实现图片的操作
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 
 | IMOperation op = new IMOperation();
 
 op.crop(operate.getWidth(), operate.getHeight(), operate.getX(), operate.getY());
 
 
 
 op.rotate(rotate);
 
 
 
 op.resize(operate.getWidth(), operate.getHeight());
 op.quality(operate.getQuality().doubleValue());
 
 
 
 op.flip();
 
 
 op.flop();
 
 
 op.geometry(operate.getWidth(), operate.getHeight(), operate.getX(), operate.getY()).composite();
 
 
 op.border(operate.getWidth(), operate.getHeight()).bordercolor(operate.getColor());
 
 
 
 op.addRawArgs("-resize", "!100x200");
 
 
 
 op.addImage(sourceFilename);
 
 op.addImage(outputFilename);
 
 
 
 ConvertCmd convert = new ConvertCmd();
 convert.run(op);
 
 | 
2. 使用姿势
在具体的设计接口之前,不妨先看一下最终的使用姿势,然后逆向的再看是如何设计的
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 
 | private static final String localFile = "blogInfoV2.png";
 
 
 
 
 @Test
 public void testOperate() {
 BufferedImage img;
 try {
 img = ImgWrapper.of(localFile)
 .board(10, 10, "red")
 .flip()
 .rotate(180)
 .crop(0, 0, 1200, 500)
 .asImg();
 System.out.println("--- " + img);
 } catch (Exception e) {
 e.printStackTrace();
 }
 }
 
 | 
上面这个方法,演示了图片的多个操作,首先是加个红色边框,然后翻转,然后旋转180°,再裁剪输出图片
所以这个封装,肯定是使用了Builder模式了,接下来看下配置参数
3. 接口设计
首先确定目前支持的几个方法:OperateType
其次就是相关的配置参数: Operate<T>
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 
 | @Datapublic static class Operate<T> {
 
 
 
 private OperateType operateType;
 
 
 
 
 private Integer width;
 
 
 
 private Integer height;
 
 
 
 private Integer x;
 
 
 
 private Integer y;
 
 
 
 private Double rotate;
 
 
 
 
 private Double radio;
 
 
 
 
 private Integer quality;
 
 
 
 
 private String color;
 
 
 
 
 private T water;
 
 
 
 
 private String waterImgType;
 
 
 
 
 private boolean forceScale;
 
 
 public boolean valid() {
 switch (operateType) {
 case CROP:
 return width != null && height != null && x != null && y != null;
 case SCALE:
 return width != null || height != null || radio != null;
 case ROTATE:
 return rotate != null;
 case WATER:
 
 return water != null;
 case BOARD:
 if (width == null) {
 width = 3;
 }
 if (height == null) {
 height = 3;
 }
 if (color == null) {
 color = "#ffffff";
 }
 case FLIP:
 case FLOP:
 return true;
 default:
 return false;
 }
 }
 
 
 
 
 
 
 public String getWaterFilename() throws ImgOperateException {
 try {
 return FileWriteUtil.saveFile(water, waterImgType).getAbsFile();
 } catch (Exception e) {
 e.printStackTrace();
 return null;
 }
 }
 }
 
 
 public enum OperateType {
 
 
 
 CROP,
 
 
 
 SCALE,
 
 
 
 ROTATE,
 
 
 
 WATER,
 
 
 
 
 FLIP,
 
 
 
 
 FLOP,
 
 
 
 BOARD;
 }
 
 | 
4. Builder实现
简化使用成本,因此针对图片裁剪、旋转等接口,封装了更友好的接口方式
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 
 | public static class Builder<T> {private T sourceFile;
 
 
 
 
 
 
 private String outputFormat = "jpg";
 
 private List<Operate> operates = new ArrayList<>();
 
 public Builder(T sourceFile) {
 this.sourceFile = sourceFile;
 }
 
 
 private static Builder<String> ofString(String str) {
 return new Builder<String>(ImgWrapper.class.getClassLoader().getResource(str).getFile());
 }
 
 
 private static Builder<URI> ofUrl(URI url) {
 return new Builder<URI>(url);
 }
 
 private static Builder<InputStream> ofStream(InputStream stream) {
 return new Builder<InputStream>(stream);
 }
 
 
 
 
 
 
 
 
 public Builder<T> setOutputFormat(String format) {
 this.outputFormat = format;
 return this;
 }
 
 
 private void updateOutputFormat(String originType) {
 if (this.outputFormat != null || originType == null) {
 return;
 }
 
 int index = originType.lastIndexOf(".");
 if (index <= 0) {
 return;
 }
 this.outputFormat = originType.substring(index + 1);
 }
 
 
 
 
 
 
 
 
 public Builder<T> scale(Integer width, Integer height, Integer quality) {
 return scale(width, height, quality, false);
 }
 
 
 public Builder<T> scale(Integer width, Integer height, Integer quality, boolean forceScale) {
 Operate operate = new Operate();
 operate.setOperateType(OperateType.SCALE);
 operate.setWidth(width);
 operate.setHeight(height);
 operate.setQuality(quality);
 operate.setForceScale(forceScale);
 operates.add(operate);
 return this;
 }
 
 
 
 
 
 
 
 public Builder<T> scale(Double radio, Integer quality) {
 Operate operate = new Operate();
 operate.setOperateType(OperateType.SCALE);
 operate.setRadio(radio);
 operate.setQuality(quality);
 operates.add(operate);
 return this;
 }
 
 
 
 
 
 
 
 
 
 
 
 public Builder<T> crop(int x, int y, int width, int height) {
 Operate operate = new Operate();
 operate.setOperateType(OperateType.CROP);
 operate.setWidth(width);
 operate.setHeight(height);
 operate.setX(x);
 operate.setY(y);
 operates.add(operate);
 return this;
 }
 
 
 
 
 
 
 
 
 public Builder<T> rotate(double rotate) {
 Operate operate = new Operate();
 operate.setOperateType(OperateType.ROTATE);
 operate.setRotate(rotate);
 operates.add(operate);
 return this;
 }
 
 
 
 
 
 
 public Builder<T> flip() {
 Operate operate = new Operate();
 operate.setOperateType(OperateType.FLIP);
 operates.add(operate);
 return this;
 }
 
 
 
 
 
 
 public Builder<T> flop() {
 Operate operate = new Operate();
 operate.setOperateType(OperateType.FLOP);
 operates.add(operate);
 return this;
 }
 
 
 
 
 
 
 
 
 
 public Builder<T> board(Integer width, Integer height, String color) {
 Operate args = new Operate();
 args.setOperateType(OperateType.BOARD);
 args.setWidth(width);
 args.setHeight(height);
 args.setColor(color);
 operates.add(args);
 return this;
 }
 
 
 
 
 
 
 
 
 
 
 public <U> Builder<T> water(U water, int x, int y) {
 return water(water, "png", x, y);
 }
 
 
 
 
 
 
 
 
 
 
 
 public <U> Builder<T> water(U water, String imgType, int x, int y) {
 Operate<U> operate = new Operate<>();
 operate.setOperateType(OperateType.WATER);
 operate.setX(x);
 operate.setY(y);
 operate.setWater(water);
 operate.setWaterImgType(imgType);
 operates.add(operate);
 return this;
 }
 
 
 
 
 
 
 
 
 public String toFile() throws Exception {
 return toFile(null);
 }
 
 
 
 
 
 
 
 
 
 public String toFile(String outputFilename) throws Exception {
 if (CollectionUtils.isEmpty(operates)) {
 throw new ImgOperateException("operates null!");
 }
 
 
 
 
 
 
 
 
 
 FileWriteUtil.FileInfo sourceFile = createFile();
 if (outputFilename == null) {
 outputFilename = FileWriteUtil.getTmpPath() + "/"
 + sourceFile.getFilename() + "_"
 + System.currentTimeMillis() + "_out." + outputFormat;
 }
 
 
 if (ImgBaseOperate.operate(operates, sourceFile.getAbsFile(), outputFilename)) {
 return outputFilename;
 } else {
 return null;
 }
 }
 
 
 
 
 
 
 
 public InputStream asStream() throws Exception {
 if (CollectionUtils.isEmpty(operates)) {
 throw new ImgOperateException("operate null!");
 }
 
 String outputFilename = this.toFile();
 if (StringUtils.isBlank(outputFilename)) {
 return null;
 }
 
 return new FileInputStream(new File(outputFilename));
 }
 
 
 public byte[] asBytes() throws Exception {
 if (CollectionUtils.isEmpty(operates)) {
 throw new ImgOperateException("operate null!");
 }
 
 String outputFilename = this.toFile();
 if (StringUtils.isBlank(outputFilename)) {
 return null;
 }
 
 
 return BytesTool.file2bytes(outputFilename);
 }
 
 
 public BufferedImage asImg() throws Exception {
 if (CollectionUtils.isEmpty(operates)) {
 throw new ImgOperateException("operate null!");
 }
 
 String outputFilename = this.toFile();
 if (StringUtils.isBlank(outputFilename)) {
 return null;
 }
 
 return ImageIO.read(new File(outputFilename));
 }
 
 
 private FileWriteUtil.FileInfo createFile() throws Exception {
 if (this.sourceFile instanceof String) {
 
 updateOutputFormat((String) this.sourceFile);
 } else if (this.sourceFile instanceof URI) {
 
 String urlPath = ((URI) this.sourceFile).getPath();
 updateOutputFormat(urlPath);
 }
 
 return FileWriteUtil.saveFile(this.sourceFile, outputFormat);
 }
 }
 
 | 
参数的设置相关的比较清晰,唯一需要注意的是输出asFile(),这个里面实现了一些有意思的东西
- 保存原图片(将网络/二进制的原图,保存到本地)
- 生成临时输出文件
- 命令执行
上面前两个,主要是借助辅助工具 FileWriteUtil实现,与主题的关联不大,但是内部东西还是很有意思的,推荐查看:
命令执行的封装如下(就是解析Operate参数,翻译成对应的IMOperation)
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 
 | 
 
 
 
 
 
 
 
 public static boolean operate(List<ImgWrapper.Builder.Operate> operates, String sourceFilename, String outputFilename) throws ImgOperateException {
 try {
 IMOperation op = new IMOperation();
 boolean operateTag = false;
 String waterFilename = null;
 for (ImgWrapper.Builder.Operate operate : operates) {
 if (!operate.valid()) {
 continue;
 }
 
 if (operate.getOperateType() == ImgWrapper.Builder.OperateType.CROP) {
 op.crop(operate.getWidth(), operate.getHeight(), operate.getX(), operate.getY());
 
 
 
 
 operateTag = true;
 } else if (operate.getOperateType() == ImgWrapper.Builder.OperateType.ROTATE) {
 
 double rotate = operate.getRotate();
 if (Math.abs((rotate % 360) - 180) <= 0.005) {
 rotate += 0.01;
 }
 op.rotate(rotate);
 operateTag = true;
 } else if (operate.getOperateType() == ImgWrapper.Builder.OperateType.SCALE) {
 if (operate.getRadio() == null) {
 if (operate.isForceScale()) {
 StringBuilder builder = new StringBuilder();
 builder.append("!").append(operate.getWidth() == null ? "" : operate.getWidth()).append("x");
 builder.append(operate.getHeight() == null ? "" : operate.getHeight());
 op.addRawArgs("-resize", builder.toString());
 } else {
 op.resize(operate.getWidth(), operate.getHeight());
 }
 } else if(Math.abs(operate.getRadio() - 1) > 0.005) {
 
 op.addRawArgs("-resize", "%" + (operate.getRadio() * 100));
 }
 
 if (operate.getQuality() != null && operate.getQuality() > 0) {
 op.quality(operate.getQuality().doubleValue());
 }
 operateTag = true;
 } else if (operate.getOperateType() == ImgWrapper.Builder.OperateType.FLIP) {
 op.flip();
 operateTag = true;
 } else if (operate.getOperateType() == ImgWrapper.Builder.OperateType.FLOP) {
 op.flop();
 operateTag = true;
 } else if (operate.getOperateType() == ImgWrapper.Builder.OperateType.WATER && waterFilename == null) {
 
 op.geometry(operate.getWidth(), operate.getHeight(), operate.getX(), operate.getY())
 .composite();
 waterFilename = operate.getWaterFilename();
 operateTag = true;
 } else if (operate.getOperateType() == ImgWrapper.Builder.OperateType.BOARD) {
 op.border(operate.getWidth(), operate.getHeight()).bordercolor(operate.getColor());
 operateTag = true;
 }
 }
 
 if (!operateTag) {
 throw new ImgOperateException("operate illegal! operates: " + operates);
 }
 op.addImage(sourceFilename);
 if (waterFilename != null) {
 op.addImage(waterFilename);
 }
 op.addImage(outputFilename);
 
 ConvertCmd convert = new ConvertCmd();
 convert.run(op);
 } catch (IOException e) {
 log.error("file read error!, e: {}", e);
 return false;
 } catch (InterruptedException e) {
 log.error("interrupt exception! e: {}", e);
 return false;
 } catch (IM4JavaException e) {
 log.error("im4java exception! e: {}", e);
 return false;
 }
 return true;
 }
 
 | 
5. 接口封装
包装一个对外使用的方式
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 
 | public class ImgWrapper {
 
 
 
 
 
 public static Builder<String> of(String file) {
 checkForNull(file, "Cannot specify null for input file.");
 if (file.startsWith("http")) {
 throw new IllegalArgumentException("file should not be URI resources! file: " + file);
 }
 return Builder.ofString(file);
 }
 
 public static Builder<URI> of(URI uri) {
 checkForNull(uri, "Cannot specify null for input uri.");
 return Builder.ofUrl(uri);
 }
 
 public static Builder<InputStream> of(InputStream inputStream) {
 checkForNull(inputStream, "Cannot specify null for InputStream.");
 return Builder.ofStream(inputStream);
 }
 
 
 private static void checkForNull(Object o, String message) {
 if (o == null) {
 throw new NullPointerException(message);
 }
 }
 }
 
 | 
IV. 测试
上面基本上完成了整个接口的设计与实现,接下来就是接口测试了
给出几个使用姿势演示,更多可以查看:ImgWrapperTest
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 
 | private static final String url = "http://a.hiphotos.baidu.com/image/pic/item/14ce36d3d539b6006a6cc5d0e550352ac65cb733.jpg";private static final String localFile = "blogInfoV2.png";
 
 @Test
 public void testCutImg() {
 
 try {
 
 ImgWrapper.of(URI.create(url))
 .crop(10, 20, 500, 500)
 .toFile();
 } catch (Exception e) {
 e.printStackTrace();
 }
 }
 
 
 @Test
 public void testRotateImg() {
 try {
 InputStream stream = FileReadUtil.getStreamByFileName(localFile);
 BufferedImage img = ImgWrapper.of(stream).rotate(90).asImg();
 System.out.println("----" + img);
 } catch (Exception e) {
 e.printStackTrace();
 }
 }
 
 
 @Test
 public void testWater() {
 BufferedImage img;
 try {
 img = ImgWrapper.of(URI.create(url))
 .board(10, 10, "red")
 .water(localFile, 100, 100)
 .asImg();
 System.out.println("--- " + img);
 } catch (Exception e) {
 e.printStackTrace();
 }
 }
 
 | 
V. 其他
项目:
GitHub:
Gitee:
基于hexo + github pages搭建的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
声明
尽信书则不如,已上内容,纯属一家之言,因本人能力一般,见识有限,如发现bug或者有更好的建议,随时欢迎批评指正
扫描关注
