前言
在前面的博客中介绍了一款Java的海报生成器ImageCombiner,原文地址: 拿来就用的Java海报生成器ImageCombiner(一),在博文中简单介绍了一下代码以及一个真实的生成案例。但是对源码的介绍不多,本文就针对源码进行深入的讲解,便于用户在使用的过程当中可以知其然,知其所以然,了解它的内部架构,程序设计理念,相关类的具体实现,现有的不足,再此基础上可以扩展出符合自己业务的逻辑,进行二次开发改造。一、源码分析
1、整体设计
ImageCombiner的源码比较简洁,主要包含以下三个包,element 海报组合要素,enums 海报样式、输出格式枚举,painter 具体的绘制器。
![](http://zhangshiyu.com/zb_users/upload/2023/04/20230402103606168040296698120.png)
包图说明
![](http://zhangshiyu.com/zb_users/upload/2023/04/20230402103606168040296632232.png)
整体类图
2、绘制器设计
从上面的类图可以清晰得看到,兑现绘制器和绘制工厂共同完成图像、文本、矩形绘制器三种。
![](http://zhangshiyu.com/zb_users/upload/2023/04/20230402103606168040296619672.png)
图片绘制器与文本绘制器和矩形绘制器不一样的是多了两个make的方法,一个是用于对图片进行高斯模糊处理的方法。高斯模糊的源码如下:
/** * 高斯模糊(毛玻璃效果) * * @param srcImage * @param radius * @return */ private BufferedImage makeBlur(BufferedImage srcImage, int radius) { if (radius < 1) { return srcImage; } int w = srcImage.getWidth(); int h = srcImage.getHeight(); int[] pix = new int[w * h]; srcImage.getRGB(0, 0, w, h, pix, 0, w); int wm = w - 1; int hm = h - 1; int wh = w * h; int div = radius + radius + 1; int r[] = new int[wh]; int g[] = new int[wh]; int b[] = new int[wh]; int rsum, gsum, bsum, x, y, i, p, yp, yi, yw; int vmin[] = new int[Math.max(w, h)]; int divsum = (div + 1) >> 1; divsum *= divsum; int dv[] = new int[256 * divsum]; for (i = 0; i < 256 * divsum; i++) { dv[i] = (i / divsum); } yw = yi = 0; int[][] stack = new int[div][3]; int stackpointer; int stackstart; int[] sir; int rbs; int r1 = radius + 1; int routsum, goutsum, boutsum; int rinsum, ginsum, binsum; for (y = 0; y < h; y++) { rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0; for (i = -radius; i <= radius; i++) { p = pix[yi + Math.min(wm, Math.max(i, 0))]; sir = stack[i + radius]; sir[0] = (p & 0xff0000) >> 16; sir[1] = (p & 0x00ff00) >> 8; sir[2] = (p & 0x0000ff); rbs = r1 - Math.abs(i); rsum += sir[0] * rbs; gsum += sir[1] * rbs; bsum += sir[2] * rbs; if (i > 0) { rinsum += sir[0]; ginsum += sir[1]; binsum += sir[2]; } else { routsum += sir[0]; goutsum += sir[1]; boutsum += sir[2]; } } stackpointer = radius; for (x = 0; x < w; x++) { r[yi] = dv[rsum]; g[yi] = dv[gsum]; b[yi] = dv[bsum]; rsum -= routsum; gsum -= goutsum; bsum -= boutsum; stackstart = stackpointer - radius + div; sir = stack[stackstart % div]; routsum -= sir[0]; goutsum -= sir[1]; boutsum -= sir[2]; if (y == 0) { vmin[x] = Math.min(x + radius + 1, wm); } p = pix[yw + vmin[x]]; sir[0] = (p & 0xff0000) >> 16; sir[1] = (p & 0x00ff00) >> 8; sir[2] = (p & 0x0000ff); rinsum += sir[0]; ginsum += sir[1]; binsum += sir[2]; rsum += rinsum; gsum += ginsum; bsum += binsum; stackpointer = (stackpointer + 1) % div; sir = stack[(stackpointer) % div]; routsum += sir[0]; goutsum += sir[1]; boutsum += sir[2]; rinsum -= sir[0]; ginsum -= sir[1]; binsum -= sir[2]; yi++; } yw += w; } for (x = 0; x < w; x++) { rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0; yp = -radius * w; for (i = -radius; i <= radius; i++) { yi = Math.max(0, yp) + x; sir = stack[i + radius]; sir[0] = r[yi]; sir[1] = g[yi]; sir[2] = b[yi]; rbs = r1 - Math.abs(i); rsum += r[yi] * rbs; gsum += g[yi] * rbs; bsum += b[yi] * rbs; if (i > 0) { rinsum += sir[0]; ginsum += sir[1]; binsum += sir[2]; } else { routsum += sir[0]; goutsum += sir[1]; boutsum += sir[2]; } if (i < hm) { yp += w; } } yi = x; stackpointer = radius; for (y = 0; y < h; y++) { pix[yi] = (0xff000000 & pix[yi]) | (dv[rsum] << 16) | (dv[gsum] << 8) | dv[bsum]; rsum -= routsum; gsum -= goutsum; bsum -= boutsum; stackstart = stackpointer - radius + div; sir = stack[stackstart % div]; routsum -= sir[0]; goutsum -= sir[1]; boutsum -= sir[2]; if (x == 0) { vmin[y] = Math.min(y + r1, hm) * w; } p = x + vmin[y]; sir[0] = r[p]; sir[1] = g[p]; sir[2] = b[p]; rinsum += sir[0]; ginsum += sir[1]; binsum += sir[2]; rsum += rinsum; gsum += ginsum; bsum += binsum; stackpointer = (stackpointer + 1) % div; sir = stack[stackpointer]; routsum += sir[0]; goutsum += sir[1]; boutsum += sir[2]; rinsum -= sir[0]; ginsum -= sir[1]; binsum -= sir[2]; yi += w; } } srcImage.setRGB(0, 0, w, h, pix, 0, w); return srcImage; }
在上述的代码,尤其是绘制器工厂的实现,信心的朋友通过阅读代码可以看到,工厂实现方式还是可以进行改造,由于目前的绘制对象也不多,不是有代码洁癖的也能接受。
package com.freewayso.image.combiner.painter;import com.freewayso.image.combiner.element.CombineElement;import com.freewayso.image.combiner.element.ImageElement;import com.freewayso.image.combiner.element.RectangleElement;import com.freewayso.image.combiner.element.TextElement;/** * @Author zhaoqing.chen * @Date 2020/8/21 * @Description */public class PainterFactory { private static ImagePainter imagePainter; //图片绘制器 private static TextPainter textPainter; //文本绘制器 private static RectanglePainter rectanglePainter; //矩形绘制器 public static IPainter createInstance(CombineElement element) throws Exception { //考虑到性能,这里用单件,先不lock了 if (element instanceof ImageElement) { if (imagePainter == null) { imagePainter = new ImagePainter(); } return imagePainter; } else if (element instanceof TextElement) { if (textPainter == null) { textPainter = new TextPainter(); } return textPainter; } else if (element instanceof RectangleElement) { if (rectanglePainter == null) { rectanglePainter = new RectanglePainter(); } return rectanglePainter; } else { throw new Exception("不支持的Painter类型"); } }}
在上述的代码中,通过if循环来进行具体的绘制图形实例生成,可以想象一下,针对上述的代码,如果未来要扩展一种新的绘制对象,该怎么进行代码优化。
3、海报元素类设计
![](http://zhangshiyu.com/zb_users/upload/2023/04/20230402103606168040296624786.png)
海报元素可以分为三种元素,与绘制器一一对应,分别是图片元素、文本元素、矩形元素。这几个类都是CombineElement的子类。后面结合时序图来说明具体的元素绑定与注册及绘制过程。
4、海报生成器
![](http://zhangshiyu.com/zb_users/upload/2023/04/20230402103606168040296623631.png)
ImageCombiner是最重要的海报生成器类,在ImageCombiner中,定义了一个海报元素类的集合用来保存需要在海报中添加的元素。同时定义了很多的以add开头的添加海报元素的方法,用以往List集合中添加新的元素对象。
/** * 添加图片元素 * * @param imgUrl 图片rul * @param x x坐标 * @param y y坐标 * @param width 宽度 * @param height 高度 * @param zoomMode 缩放模式 * @return */ public ImageElement addImageElement(String imgUrl, int x, int y, int width, int height, ZoomMode zoomMode) { ImageElement imageElement = new ImageElement(imgUrl, x, y, width, height, zoomMode); this.combineElements.add(imageElement); return imageElement; }
![](http://zhangshiyu.com/zb_users/upload/2023/04/20230402103607168040296717620.png)
二、生成时序解析
1、海报组装
public void test1() throws Exception { try { String bgImageUrl = "https://img.thebeastshop.com/combine_image/funny_topic/resource/bg_3x4.png"; //背景图(测试url形式) String qrCodeUrl = "http://imgtest.thebeastshop.com/file/combine_image/qrcodef3d132b46b474fe7a9cc6e76a511dfd5.jpg"; //二维码 String productImageUrl = "https://img.thebeastshop.com/combine_image/funny_topic/resource/product_3x4.png"; //商品图 BufferedImage waterMark = ImageIO.read(new URL("https://img.thebeastshop.com/combine_image/funny_topic/resource/water_mark.png")); //水印图(测试BufferedImage形式) BufferedImage avatar = ImageIO.read(new URL("https://img.thebeastshop.com/member/privilege/level-icon/level-three.jpg")); //头像 String title = "# 最爱的家居"; //标题文本 String content = "苏格拉底说:“如果没有那个桌子,可能就没有那个水壶”"; //内容文本 //合成器和背景图(整个图片的宽高和相关计算依赖于背景图,所以背景图的大小是个基准) ImageCombiner combiner = new ImageCombiner(bgImageUrl, OutputFormat.PNG); combiner.setBackgroundBlur(30); //设置背景高斯模糊(毛玻璃效果) combiner.setCanvasRoundCorner(100); //设置整图圆角(输出格式必须为PNG) //商品图(设置坐标、宽高和缩放模式,若按宽度缩放,则高度按比例自动计算) combiner.addImageElement(productImageUrl, 0, 160, 837, 0, ZoomMode.Width) .setRoundCorner(46) //设置圆角 .setCenter(true); //居中绘制,会忽略x坐标参数,改为自动计算 //标题(默认字体为“阿里巴巴普惠体”,也可以自己指定字体名称或Font对象) combiner.addTextElement(title, 55, 150, 1400); //内容(设置文本自动换行,需要指定最大宽度(超出则换行)、最大行数(超出则丢弃)、行高) combiner.addTextElement(content, "微软雅黑", 40, 150, 1480) .setAutoBreakLine(837, 2, 60); //头像(圆角设置一定的大小,可以把头像变成圆的) combiner.addImageElement(avatar, 200, 1200, 130, 130, ZoomMode.WidthHeight) .setRoundCorner(200) .setBlur(5); //高斯模糊,毛玻璃效果 //水印(设置透明度,0.0~1.0) combiner.addImageElement(waterMark, 630, 1200) .setAlpha(.8f) //透明度,0.0~1.0 .setRotate(15); //旋转,0~360,按中心点旋转 //二维码(强制按指定宽度、高度缩放) combiner.addImageElement(qrCodeUrl, 138, 1707, 186, 186, ZoomMode.WidthHeight); //元素对象也可以直接new,然后手动加入待绘制列表 TextElement textPrice = new TextElement("¥1290", 40, 600, 1400); textPrice.setStrikeThrough(true); //删除线 combiner.addElement(textPrice); //加入待绘制集合 //动态计算位置 int offsetPrice = textPrice.getX() + textPrice.getWidth() + 10; combiner.addTextElement("¥999", 60, offsetPrice, 1400) .setColor(Color.red); //执行图片合并 combiner.combine(); //保存文件 combiner.save("d://fullTest.png"); System.out.println(1/0); System.out.println("end..."); //或者获取流(并上传oss等) //InputStream is = combiner.getCombinedImageStream(); //String url = ossUtil.upload(is); } catch (Exception e) { e.printStackTrace(); } }
![](http://zhangshiyu.com/zb_users/upload/2023/04/20230402103607168040296761804.png)
![](http://zhangshiyu.com/zb_users/upload/2023/04/20230402103607168040296773771.png)
通过以上的时序图可以清晰的看到系统的调用过程,一共包含了24个步骤。通过时序图和代码,相信您可以很直观的看到相关的调用方式,这里不做过多的赘述。
2、combine海报合成
/** * 合成图片,返回图片对象 * * @throws Exception */ public BufferedImage combine() throws Exception { combinedImage = new BufferedImage(canvasWidth, canvasHeight, BufferedImage.TYPE_INT_RGB); Graphics2D g = combinedImage.createGraphics(); //PNG要做透明度处理,否则背景图透明部分会变黑 if (outputFormat == OutputFormat.PNG) { combinedImage = g.getDeviceConfiguration().createCompatibleImage(canvasWidth, canvasHeight, Transparency.TRANSLUCENT); g = combinedImage.createGraphics(); } //抗锯齿 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setColor(Color.white); //循环绘制各元素 for (CombineElement element : combineElements) { IPainter painter = PainterFactory.createInstance(element); if (element.isRepeat()) { //平铺绘制 painter.drawRepeat(g, element, canvasWidth, canvasHeight); } else { //正常绘制 painter.draw(g, element, canvasWidth); } } g.dispose(); //处理整图圆角 if (roundCorner != null) { combinedImage = this.makeRoundCorner(combinedImage, canvasWidth, canvasHeight, roundCorner); } return combinedImage; }
![](http://zhangshiyu.com/zb_users/upload/2023/04/20230402103607168040296765661.png)
![](http://zhangshiyu.com/zb_users/upload/2023/04/20230402103607168040296730114.png)
combine方法是最核心的生成方法,在第6步进入循环,将页面设置的不同的海报元素添加到生成器中,并调用绘制器工程生成相应的绘制器,通过Graphics2D来进行海报的生成。至此,海报生成器的相关类就全部讲解完毕,工具虽小,五脏俱全。
//循环绘制各元素for (CombineElement element : combineElements) { IPainter painter = PainterFactory.createInstance(element); if (element.isRepeat()) { //平铺绘制 painter.drawRepeat(g, element, canvasWidth, canvasHeight); } else { //正常绘制 painter.draw(g, element, canvasWidth); } }
三、总结
以上就是本文的主要内容,本文主要从海报生成器的源码和生成时序解析两个方面进行深度解析,使用UML的分析方法对类图、时序图结合源码进行说明,将海报生成器的核心代码做完整的剖析,想研究的朋友可以深入的学习,对于源码中不合理的地方可以进行修改。