ImageCombiner设计源码详解

news2025/2/26 3:46:57

前言

在前面的博客中介绍了一款Java的海报生成器ImageCombiner,原文地址: 拿来就用的Java海报生成器ImageCombiner(一),在博文中简单介绍了一下代码以及一个真实的生成案例。但是对源码的介绍不多,本文就针对源码进行深入的讲解,便于用户在使用的过程当中可以知其然,知其所以然,了解它的内部架构,程序设计理念,相关类的具体实现,现有的不足,再此基础上可以扩展出符合自己业务的逻辑,进行二次开发改造。

一、源码分析

1、整体设计

ImageCombiner的源码比较简洁,主要包含以下三个包,element 海报组合要素,enums 海报样式、输出格式枚举,painter 具体的绘制器。

包图说明

整体类图

2、绘制器设计

从上面的类图可以清晰得看到,兑现绘制器和绘制工厂共同完成图像、文本、矩形绘制器三种。

图片绘制器与文本绘制器和矩形绘制器不一样的是多了两个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、海报元素类设计

海报元素可以分为三种元素,与绘制器一一对应,分别是图片元素、文本元素、矩形元素。这几个类都是CombineElement的子类。后面结合时序图来说明具体的元素绑定与注册及绘制过程。

4、海报生成器

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;
    }

二、生成时序解析

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();
        }
        
    }

通过以上的时序图可以清晰的看到系统的调用过程,一共包含了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;
    }

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的分析方法对类图、时序图结合源码进行说明,将海报生成器的核心代码做完整的剖析,想研究的朋友可以深入的学习,对于源码中不合理的地方可以进行修改。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/361660.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vs2022 x64 C/C++和汇编混编

vs2022环境x64 C/C和汇编混编vs64位程序不支持__asm内嵌汇编&#xff0c;需要单独编写汇编源文件示例如下1、新建空的win32项目&#xff0c;新建main.cpp&#xff0c;示例代码如下2、新建asm64.asm汇编源文件&#xff0c;示例代码如下3、编译器配置&#xff0c;选择x64&#xf…

【编程入门】应用市场(uni-app版)

背景 前面已输出多个系列&#xff1a; 《十余种编程语言做个计算器》 《十余种编程语言写2048小游戏》 《17种编程语言10种排序算法》 《十余种编程语言写博客系统》 《十余种编程语言写云笔记》 《N种编程语言做个记事本》 目标 为编程初学者打造入门学习项目&#xff0c;使…

华为OD机试 - 最短木板长度(C++) | 附带编码思路 【2023】

刷算法题之前必看 参加华为od机试,一定要注意不要完全背诵代码,需要理解之后模仿写出,通过率才会高。 华为 OD 清单查看地址:https://blog.csdn.net/hihell/category_12199283.html 华为OD详细说明:https://dream.blog.csdn.net/article/details/128980730 华为OD机试题…

PowerJob容器的前世,容器是如何产生,如何上传到Server上去的

这不仅仅是一篇PowerJob源码分析的文章&#xff0c;还有很多的java基础知识&#xff0c;在实践中学习效果更好&#xff0c;感兴趣就留下来交流一下吧。 power容器虽然说是容器&#xff0c;但并不是docker容器&#xff0c;仅仅就是java的jar包 &#xff0c;可以通过框架下载一套…

手工测试混了5年,年底接到了被裁员的消息....

大家都比较看好软件测试行业&#xff0c;只是因为表面上看起来&#xff1a;钱多事少加班少。其实这个都是针对个人运气好的童人才会有此待遇。在不同的阶段做好不同阶段的事情&#xff0c;才有可能离这个目标更近&#xff0c;作为一枚软件测试人员&#xff0c;也许下面才是我们…

阿里5年,一个女工对软件测试的理解

成为一个优秀的测试工程师需要具备哪些知识和经验&#xff1f; 针对这个问题&#xff0c;可以直接拆分以下三个小问题来详细说明&#xff1a; 1、优秀软件测试工程师的标准是什么&#xff1f; 2、一个合格的测试工程师需要具备哪些专业知识&#xff1f; 3、一个合格的测试工程…

介绍Kadence Elements元素模板:按您的方式设计网站

随着 Kadence Pro 1.0.4 和 Kadence Blocks Pro 1.5.8 的发布&#xff0c;Kadence 团队很高兴地宣布推出最强大的新方法&#xff0c;帮助网站所有者使用 Kadence Elements Templates 创建动态和高度定制的 WordPress 网站。如果您曾经创建过 WordPress 网站&#xff0c;并且发现…

Fiddler的简单使用

目录 1.断点应用 2.网络限速测试 2.1.为什么需要弱网测试 2.2.Fiddler弱网测试配置 1.断点应用 通过断点功能&#xff0c;可以在测试时方便的篡改request&#xff0c;response以达到测试的目的&#xff0c;如果&#xff1a;在请求头中的参数修改成错误的&#xff0c;或在响应…

强化学习DQN之俄罗斯方块

强化学习DQN之俄罗斯方块强化学习DQN之俄罗斯方块算法流程文件目录结构模型结构游戏环境训练代码测试代码结果展示强化学习DQN之俄罗斯方块 算法流程 本项目目的是训练一个基于深度强化学习的俄罗斯方块。具体来说&#xff0c;这个代码通过以下步骤实现训练&#xff1a; 首先…

2023最新网络工程师HCIA-Datacom“1000”道题库,光速刷题拿证

HCIA认证是华为认证体系的初级认证&#xff0c;可以说是网工进入IT行业的一张从业资格证&#xff01; HCIA-Datacom考试覆盖数通基础知识 包括 TCP/IP 协议栈基础知识&#xff0c;OSPF 路由协议基本原理以及在华为路由器中的配置实现&#xff0c;以太网技术、生成树、VLAN 原…

【Java 面试合集】final 以及finally 不同

final 以及finally 不同 1. 概述 嗨&#xff0c;大家好【Java 面试合集】又来了。今天给大家分享的主题是final 以及finally 不同. 很简单&#xff0c;但是确实很细节哦&#xff0c;好了废话不多说&#xff0c;让我们开始吧 2. final 首先我们要知道final 是一个修饰符&#x…

WordPress主题:知更鸟Begin5.2免授权版

内容目录一、详细介绍二、效果展示1.部分代码2.效果图展示三、学习资料下载一、详细介绍 HTML5CSS3响应式设计兼容IE8、Firefox、Chrome等现代浏览器。杂志布局、图片布局和博客布局后台切换 Wordpress主题 知更鸟Begin主题 无授权无加密独立页面模板 博客布局、随机文章、友…

总结:电容在电路35个基本常识

1 电压源正负端接了一个电容&#xff0c;与电路并联&#xff0c;用于整流电路时&#xff0c;具有很好的滤波作用&#xff0c;当电压交变时&#xff0c;由于电容的充电作用&#xff0c;两端的电压不能突变&#xff0c;就保证了电压的平稳。 当用于电池电源时&#xff0c;具有交流…

Python 四大主流 Web 编程框架

目前Python的网络编程框架已经多达几十个&#xff0c;逐个学习它们显然不现实。但这些框架在系统架构和运行环境中有很多共通之处&#xff0c;本文带领读者学习基于Python网络框架开发的常用知识,及目前的4种主流Python网络框架&#xff1a;Django、Tornado、Flask、Twisted。 …

【Git】Git是什么?简单说说Git的工作机制?Git的常用命令有那些?

目录 一、Git是什么? 二、简单说说Git的工作机制&#xff1f; 三、Git的常用命令有那些&#xff1f; &#x1f49f; 创作不易&#xff0c;不妨点赞&#x1f49a;评论❤️收藏&#x1f499;一下 一、Git是什么? Git 是一个免费的、开源的分布式版本控制系统&#xff0c;可…

若依(微服务版)-0 运行(踩坑日记)

本次运行采用本地运行程序&#xff0c;其他中间件全部使用docker安装在云服务器上 本次运行的若依为目前最新版本3.6.2&#xff0c;nacos/nacos-server:v2.0.4&#xff0c;mysql8.0&#xff0c;redis和nginx最新版就行。 关于nacos这里会有好几个坑&#xff0c;详情请看naocs…

【C语言】浮点型数据在内存中的存储

&#x1f680;&#x1f680;&#x1f680; 如果文章对你有帮助不要忘记点赞关注收藏哦&#x1f680;&#x1f680;&#x1f680; 文章目录⭐浮点数在内存中的存储1.1 &#x1f913;举个例子:1.2浮点数存储规则&#x1f308;&#xff1a;对于M与E有一些特别规定1.3解释前面题目&…

内存映射(1)

内存映射 将磁盘文件中的数据映射到内存&#xff0c;用户通过修改内存就能修改磁盘文件 相关的系统调用&#xff1a; void *mmap() 功能&#xff1a;将一个文件或设备的数据映射到内存中 参数&#xff1a; void *addr : NULL 由内核指定length : 要映射的数据长度&#xff0c;…

端口扫描介绍

文章目录1、端口的基本概念2、端口的常见分类3、端口扫描原理1、端口的基本概念 “端口” 在计算机网络领域中是个非常重要的概念它是专门为计算机通信而设计的&#xff0c;它不是硬件&#xff0c;不同于计算机中的“插槽”&#xff0c;可以说是个“软端口”端口是由计算机的通…

MacBook Pro 恢复出厂设置

目录1.恢复出厂设置1.1 按Command-R 键1.2 macOS 实用工具1.3 从 macOS 恢复功能的实用工具窗口中选择“磁盘工具”&#xff0c;然后点按“继续”1.4 在“磁盘工具”边栏中选择您的设备或宗卷。1.5 点按“抹掉”按钮或标签页1.6 抹掉OS X HD - 数据 完成1.7 抹掉 OS X HD1.8 查…