一、需求概述

多人聊天(群组,讨论组,聊天室,以下统称: “群组” )生成一个拼接头像,需要把最先加入群组的几个人(最多4个人,以下简称:头部用户A、B、C、D)的头像拼凑成在一起。

群组创建后,A、B、C、D其中任何一个修改了自己的头像,需要 "异步" 更新群组头像。

以上是简单的需求描述。

本文使用.net core实现了N张图片拼接算法。

完整代码点击:https://github.com/night-king/ImageMerge

二、方案实现探讨

本需求可以在服务端实现,也可以在客户端实现。

(1)服务端实现:

群组创建时,下载A、B、C、D的头像,然后合并即可;

头部用户修改头像后,后台任务刷新群组头像。

(2)客户端实现:客户端包括iOS,Android,React,Web。

情况有些复杂,所以最后决定直接采用服务端实现。

Github上搜索无果,只能自己动手完成了。

先看效果:

采用四位Github大神头像作为源图片:

    

结果如下:

三、解决方案

群组2个用户,3个用户,4个以及以上用户头像不一样,总结如下:

    /// <summary>
/// 合并布局枚举
/// </summary>
public enum Merge2LayoutEnum
{
/// <summary>
/// 2张图片,上下各1个长方形
/// ———————————————————
/// | |
/// | R1 |
/// | |
/// ———————————————————
/// | |
/// | R2 |
/// | |
/// ———————————————————
/// </summary>
Merge2R1 = , /// <summary>
/// 2张图片,左右各1个长方形
/// ———————————————————
/// | | |
/// | | |
/// | | |
/// | R1 | R2 |
/// | | |
/// | | |
/// | | |
/// ———————————————————
/// </summary>
Merge2R2 =
} public enum Merge3LayoutEnum
{ /// <summary>
/// 3张图片, 上面一个长方形,下面2个正方形并排
/// ———————————————————
/// | |
/// | R1 |
/// | |
/// ———————————————————
/// | | |
/// | S1 | S2 |
/// | | |
/// ———————————————————
/// </summary>
Merge1R2S1 = , /// <summary>
/// 3张图片,上面2个正方形并排,下面一个长方形
/// ———————————————————
/// | | |
/// | S1 | S2 |
/// | | |
/// ———————————————————
/// | |
/// | R1 |
/// | |
/// ———————————————————
/// </summary>
Merge1R2S2 = , /// <summary>
/// 3张图片,上面2个正方形并排,下面一个长方形
/// ———————————————————
/// | | |
/// | | S1 |
/// | | |
/// | R1 |—————————
/// | | |
/// | | S2 |
/// | | |
/// ———————————————————
/// </summary>
Merge1R2S3 = , /// <summary>
/// 3张图片,上面2个正方形并排,下面一个长方形
/// ———————————————————
/// | | |
/// | S2 | |
/// | | |
/// |—————————| R1 |
/// | | |
/// | S2 | |
/// | | |
/// ———————————————————
/// </summary>
Merge1R2S4 =
} public enum Merge4LayoutEnum
{
/// <summary>
/// 4张图片,上面一个长方形,下面2个正方形并排
/// ———————————————————
/// | | |
/// | S1 | S2 |
/// | | |
/// ———————————————————
/// | | |
/// | S3 | S4 |
/// | | |
/// ———————————————————
/// </summary>
Merge4S = }

以下是2张图片实现代码:

        /// <summary>
/// 合并2张图片
/// </summary>
/// <param name="image1"></param>
/// <param name="image2"></param>
/// <param name="layout"></param>
/// <returns></returns>
public static Image Merge2Images(Image image1, Image image2, Merge2LayoutEnum layout = Merge2LayoutEnum.Merge2R1, int size = )
{
var width = size;
var height = size;
var pf = PixelFormat.Format32bppArgb;
using (var bg = new Bitmap(width, height, pf))
{
using (var g = Graphics.FromImage(bg))
{
g.FillRectangle((Brush)Brushes.White, , , width, height);//全幅背景为白色
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
switch (layout)
{
/// <summary>
/// 2张图片,上下各1个长方形
/// ———————————————————
/// | |
/// | R1 |
/// | |
/// ———————————————————
/// | |
/// | R2 |
/// | |
/// ———————————————————
/// </summary>
case Merge2LayoutEnum.Merge2R1:
{
using (var img1 = ZoomToSqure(image1, size))
{
var newHeight = height / ;// =125
var newWidth = width;// =250
var srcWidth = img1.Width;// =250
var srcHeight = img1.Height * newHeight / newWidth;//=250*125/250=125
g.DrawImage(img1, new Rectangle(, , newWidth, newHeight), new Rectangle(, , srcWidth, srcHeight), GraphicsUnit.Pixel);
}
using (var img2 = ZoomToSqure(image2, size))
{
var newHeight = height / ;// =125
var newWidth = width; // =250
var srcWidth = img2.Width;// =250
var srcHeight = img2.Height * newHeight / newWidth;//=250*125/250=125
g.DrawImage(img2, new Rectangle(, height / , newWidth, newHeight), new Rectangle(, , srcWidth, srcHeight), GraphicsUnit.Pixel);
}
}
break;
/// <summary>
/// 2张图片,左右各1个长方形
/// ———————————————————
/// | | |
/// | | |
/// | | |
/// | R1 | R2 |
/// | | |
/// | | |
/// | | |
/// ———————————————————
/// </summary>
case Merge2LayoutEnum.Merge2R2:
{
using (var img1 = ZoomToSqure(image1, size))
{
var newWidth = width / ;// =125
var newHeight = height; // =250
var srcWidth = img1.Width * newWidth / newHeight;//=250*125/250=125
var srcHeight = img1.Height;// =250
g.DrawImage(img1, new Rectangle(, , newWidth, newHeight), new Rectangle(, , srcWidth, srcHeight), GraphicsUnit.Pixel);
}
using (var img2 = ZoomToSqure(image2, size))
{
var newWidth = width / ;// =125
var newHeight = height; // =250
var srcWidth = img2.Width * newWidth / newHeight;//=500*125/250=250
var srcHeight = img2.Height;// =500
g.DrawImage(img2, new Rectangle(width / , , newWidth, newHeight), new Rectangle(, , srcWidth, srcHeight), GraphicsUnit.Pixel);
}
}
break;
}
g.Save();
image1.Dispose();
image2.Dispose();
} using (var ms = new MemoryStream())
{
bg.Save(ms, ImageFormat.Png);
var buffers = ms.ToArray();
return ConvertToImage(buffers);
}
}
}

以下是三种图片的生成算法

        /// <summary>
/// 合并3张图片
/// </summary>
/// <param name="image1"></param>
/// <param name="image2"></param>
/// <param name="image3"></param>
/// <param name="layout"></param>
/// <returns></returns>
public static Image Merge3Images(Image image1, Image image2, Image image3, Merge3LayoutEnum layout = Merge3LayoutEnum.Merge1R2S1, int size = )
{
var width = size;
var height = size;
var pf = PixelFormat.Format32bppArgb;
using (var bg = new Bitmap(width, height, pf))
{
using (var g = Graphics.FromImage(bg))
{
g.FillRectangle((Brush)Brushes.White, , , width, height);//全幅背景为白色
switch (layout)
{
/// <summary>
/// 3张图片, 上面一个长方形,下面2个正方形并排
/// ———————————————————
/// | |
/// | R1 |
/// | |
/// ———————————————————
/// | | |
/// | S1 | S2 |
/// | | |
/// ———————————————————
/// </summary>
case Merge3LayoutEnum.Merge1R2S1:
{
using (var img1 = ZoomToSqure(image1, size))
{
var newHeight = height / ;// =125
var newWidth = width;// =250
var srcWidth = img1.Width;// =250
var srcHeight = img1.Height * newHeight / newWidth;//=250*125/250=125
g.DrawImage(img1, new Rectangle(, , newWidth, newHeight), new Rectangle(, , srcWidth, srcHeight), GraphicsUnit.Pixel);
}
using (var img2 = ZoomToSqure(image2, size))
{
var newHeight = height / ;// =125
var newWidth = width / ; // =125
var srcWidth = img2.Width; // =250
var srcHeight = img2.Height;// =250
g.DrawImage(img2, new Rectangle(, height / , newWidth, newHeight), new Rectangle(, , srcWidth, srcHeight), GraphicsUnit.Pixel);
}
using (var img3 = ZoomToSqure(image3, size))
{
var newHeight = height / ; // =125
var newWidth = width / ; // =125
var srcWidth = img3.Width; // =250
var srcHeight = img3.Height;// =250
g.DrawImage(img3, new Rectangle(width / , height / , newWidth, newHeight), new Rectangle(, , srcWidth, srcHeight), GraphicsUnit.Pixel);
}
}
break;
/// <summary>
/// 3张图片,上面2个正方形并排,下面一个长方形
/// ———————————————————
/// | | |
/// | S1 | S2 |
/// | | |
/// ———————————————————
/// | |
/// | R1 |
/// | |
/// ———————————————————
/// </summary>
case Merge3LayoutEnum.Merge1R2S2:
{
using (var img1 = ZoomToSqure(image1, size))
{
var newHeight = height / ;// =125
var newWidth = width / ; // =125
var srcWidth = img1.Width; // =250
var srcHeight = img1.Height;// =250
g.DrawImage(img1, new Rectangle(, , newWidth, newHeight), new Rectangle(, , srcWidth, srcHeight), GraphicsUnit.Pixel);
}
using (var img2 = ZoomToSqure(image2, size))
{
var newHeight = height / ; // =125
var newWidth = width / ; // =125
var srcWidth = img2.Width; // =250
var srcHeight = img2.Height;// =250
g.DrawImage(img2, new Rectangle(width / , , newWidth, newHeight), new Rectangle(, , srcWidth, srcHeight), GraphicsUnit.Pixel);
}
using (var img3 = ZoomToSqure(image3, size))
{
var newHeight = height / ;// =125
var newWidth = width;// =250
var srcWidth = img3.Width;// =250
var srcHeight = img3.Height * newHeight / newWidth;//=250*125/250=125
g.DrawImage(img3, new Rectangle(, height / , newWidth, newHeight), new Rectangle(, , srcWidth, srcHeight), GraphicsUnit.Pixel);
}
}
break;
/// <summary>
/// 3张图片,上面2个正方形并排,下面一个长方形
/// ———————————————————
/// | | |
/// | | S1 |
/// | | |
/// | R1 |—————————
/// | | |
/// | | S2 |
/// | | |
/// ———————————————————
/// </summary>
case Merge3LayoutEnum.Merge1R2S3:
{
using (var img1 = ZoomToSqure(image1, size))
{
var newHeight = height;// =250
var newWidth = width / ;// =125
var srcHeight = img1.Height;//=250
var srcWidth = img1.Width * newWidth / newHeight;// =250*125/250=125;
g.DrawImage(img1, new Rectangle(, , newWidth, newHeight), new Rectangle(, , srcWidth, srcHeight), GraphicsUnit.Pixel);
}
using (var img2 = ZoomToSqure(image2, size))
{
var newHeight = height / ;// =125
var newWidth = width / ; // =125
var srcWidth = img2.Width; // =250
var srcHeight = img2.Height;// = 250
g.DrawImage(img2, new Rectangle(width / , , newWidth, newHeight), new Rectangle(, , srcWidth, srcHeight), GraphicsUnit.Pixel);
}
using (var img3 = ZoomToSqure(image3, size))
{
var newHeight = height / ; // =125
var newWidth = width / ; // =125
var srcWidth = img3.Width; // =250
var srcHeight = img3.Height;// =250
g.DrawImage(img3, new Rectangle(width / , height / , newWidth, newHeight), new Rectangle(, , srcWidth, srcHeight), GraphicsUnit.Pixel);
}
}
break;
/// <summary>
/// 3张图片,上面2个正方形并排,下面一个长方形
/// ———————————————————
/// | | |
/// | S2 | |
/// | | |
/// |—————————| R1 |
/// | | |
/// | S2 | |
/// | | |
/// ———————————————————
/// </summary>
case Merge3LayoutEnum.Merge1R2S4:
{
using (var img1 = ZoomToSqure(image1, size))
{
var newHeight = height / ;// =125
var newWidth = width / ; // =125
var srcWidth = img1.Width; // =250
var srcHeight = img1.Height;// =250
g.DrawImage(img1, new Rectangle(, , newWidth, newHeight), new Rectangle(, , srcWidth, srcHeight), GraphicsUnit.Pixel);
}
using (var img2 = ZoomToSqure(image2, size))
{
var newHeight = height / ; // =125
var newWidth = width / ; // =125
var srcWidth = img2.Width; // =250
var srcHeight = img2.Height;// =250
g.DrawImage(img2, new Rectangle(, height / , newWidth, newHeight), new Rectangle(, , srcWidth, srcHeight), GraphicsUnit.Pixel);
}
using (var img3 = ZoomToSqure(image3, size))
{
var newHeight = height;// =250
var newWidth = width / ;// =125
var srcHeight = img3.Height;//=250
var srcWidth = img3.Width * newWidth / newHeight;// =250*125/250=125;
g.DrawImage(img3, new Rectangle(width / , , newWidth, newHeight), new Rectangle(, , srcWidth, srcHeight), GraphicsUnit.Pixel);
}
}
break;
}
g.Save();
image1.Dispose();
image2.Dispose();
image3.Dispose();
}
using (var ms = new MemoryStream())
{
bg.Save(ms, ImageFormat.Png);
var buffers = ms.ToArray();
return ConvertToImage(buffers);
}
}
}

以下是4张图片的生成是算法:

        /// <summary>
/// 合并4张图片
/// </summary>
/// <param name="image1"></param>
/// <param name="image2"></param>
/// <param name="image3"></param>
/// <param name="image4"></param>
/// <param name="layout"></param>
/// <returns></returns>
public static Image Merge4Images(Image image1, Image image2, Image image3, Image image4, Merge4LayoutEnum layout = Merge4LayoutEnum.Merge4S, int size = )
{
var width = size;
var height = size;
var pf = PixelFormat.Format32bppArgb;
using (var bg = new Bitmap(width, height, pf))
{
using (var g = Graphics.FromImage(bg))
{
g.FillRectangle((Brush)Brushes.White, , , width, height);//全幅背景为白色
switch (layout)
{
/// <summary>
/// 4张图片,上面一个长方形,下面2个正方形并排
/// ———————————————————
/// | | |
/// | S1 | S2 |
/// | | |
/// ———————————————————
/// | | |
/// | S3 | S4 |
/// | | |
/// ———————————————————
/// </summary>
case Merge4LayoutEnum.Merge4S:
{
using (var img1 = ZoomToSqure(image1, size))
{
var newHeight = height / ;// =125
var newWidth = width / ; // =125
var srcWidth = img1.Width; // =250
var srcHeight = img1.Height;// =250
g.DrawImage(img1, new Rectangle(, , newWidth, newHeight), new Rectangle(, , srcWidth, srcHeight), GraphicsUnit.Pixel);
}
using (var img2 = ZoomToSqure(image2, size))
{
var newHeight = height / ; // =125
var newWidth = width / ; // =125
var srcWidth = img2.Width; // =250
var srcHeight = img2.Height;// =250
g.DrawImage(img2, new Rectangle(width / , , newWidth, newHeight), new Rectangle(, , srcWidth, srcHeight), GraphicsUnit.Pixel);
}
using (var img3 = ZoomToSqure(image3, size))
{
var newHeight = height / ; // =125
var newWidth = width / ; // =125
var srcWidth = img3.Width; // =250
var srcHeight = img3.Height;// =250
g.DrawImage(img3, new Rectangle(, height / , newWidth, newHeight), new Rectangle(, , srcWidth, srcHeight), GraphicsUnit.Pixel);
}
using (var img4 = ZoomToSqure(image4, size))
{
var newHeight = height / ; // =125
var newWidth = width / ; // =125
var srcWidth = img4.Width; // =250
var srcHeight = img4.Height;// =250
g.DrawImage(img4, new Rectangle(width / , height / , newWidth, newHeight), new Rectangle(, , srcWidth, srcHeight), GraphicsUnit.Pixel);
}
}
break;
}
g.Save();
image1.Dispose();
image2.Dispose();
image3.Dispose();
image4.Dispose();
}
using (var ms = new MemoryStream())
{
bg.Save(ms, ImageFormat.Png);
var buffers = ms.ToArray();
return ConvertToImage(buffers);
}
}
}

以下是用到的Helper方法:

        /// <summary>
/// 下载图片
/// </summary>
/// <param name="imageUrl"></param>
/// <returns></returns>
public static byte[] Download(string imageUrl)
{
if (imageUrl.StartsWith("http"))
{
using (var ms = new MemoryStream())
{
var request = (HttpWebRequest)HttpWebRequest.Create(imageUrl);// 打开网络连接
using (var rs = request.GetResponse().GetResponseStream())// 向服务器请求,获得服务器的回应数据流
{
byte[] btArray = new byte[];// 定义一个字节数据,用来向readStream读取内容和向writeStream写入内容
int size = rs.Read(btArray, , btArray.Length);// 向远程文件读第一次 while (size > )// 如果读取长度大于零则继续读
{
ms.Write(btArray, , size);// 写入本地文件
size = rs.Read(btArray, , btArray.Length);// 继续向远程文件读取
}
return ms.ToArray();
}
}
}
else
{
using (var ms = new MemoryStream())
{
var img = Image.FromFile(imageUrl);
img.Save(ms, img.RawFormat);
return ms.ToArray();
}
}
} /// <summary>
/// 将byte数组转化为Image
/// </summary>
/// <param name="buffer"></param>
/// <returns></returns>
public static Image ConvertToImage(byte[] buffer)
{
using (MemoryStream ms = new MemoryStream(buffer))
{
return Image.FromStream(ms);
}
} /// <summary>
/// 将Image转化为byte数组
/// </summary>
/// <param name="image"></param>
/// <returns></returns>
public static byte[] ConvertToByte(Image image)
{
using (MemoryStream ms = new MemoryStream())
{
image.Save(ms, image.RawFormat);
return ms.ToArray();
}
} /// <summary>
/// 等比缩放/放大成正方形图片
/// </summary>
/// <param name="orginal">原始图片</param>
/// <param name="size">目标宽高</param>
/// <param name="cute">超出部分是否剪裁</param>
/// <returns></returns>
public static Image ZoomToSqure(Image orginal, int size, bool cute = true)
{
var width = 0d;//图片宽度
var height = 0d;//图片高度
if (orginal.Width > orginal.Height)//原始图片宽度大于高度
{
height = size;
width = cute ? size : (orginal.Width * height / orginal.Height);
}
else if (orginal.Width < orginal.Height)//原始图片高度大于宽度
{
width = size;
height = cute ? size : (orginal.Height * width / orginal.Width);
}
else//原始图片是正方形,刚好
{
width = size;
height = size;
}
var board = new Bitmap((int)width, (int)height);
using (var g = Graphics.FromImage(board))
{
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;//设置质量
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;//设置质量
g.Clear(Color.White);//置背景色
g.DrawImage(orginal, new Rectangle(, , board.Width, board.Height), new Rectangle(, , orginal.Width, orginal.Height), System.Drawing.GraphicsUnit.Pixel); //画图
orginal.Dispose();//释放原图
return board;
}
}

完整代码点击:https://github.com/night-king/ImageMerge

IM即时通讯群组头像拼接.net core 解决方案的更多相关文章

  1. 一行实现QQ群组头像,微信群组,圆角等效果. 并支持url直接加载图片

    说点题外话. Coding中我们总是经历着这么几个过程. 学会使用: 不管是API也好, 开源库也好. 总是在最开始的学会去用. 了解实现原理: 可能会因为一些不兼容, 代码的异常状态的处理不够完美等 ...

  2. layim即时通讯实例各功能整合

    一.系统演示1.1 聊天窗体主界面演示 1.2 模拟两人在线聊天(点击图片查看演示视频) 1.3 在线演示> 在线演示,点击进入系统到这里,若是您想要的,接下来听我娓娓道来二.开发工具开发软件: ...

  3. Activiti6.0 工作流引擎 websocket即时聊天发图片文字 好友群组 SSM源码

    即时通讯:支持好友,群组,发图片.文件,消息声音提醒,离线消息,保留聊天记录 (即时聊天功能支持手机端,详情下面有截图) 工作流模块---------------------------------- ...

  4. java工作流引擎 Activiti6.0 websocket 即时聊天发图片文字 好友群组 SSM源码

    时通讯:支持好友,群组,发图片.文件,消息声音提醒,离线消息,保留聊天记录 工作流模块--------------------------------------------------------- ...

  5. java ssm 后台框架平台 项目源码 websocket即时聊天发图片文字 好友群组 SSM源码

    官网 http://www.fhadmin.org/D 集成安全权限框架shiro  Shiro 是一个用 Java 语言实现的框架,通过一个简单易用的 API 提供身份验证和授权,更安全,更可靠E ...

  6. mui初级入门教程(五)— 聊聊即时通讯(IM),基于环信 web im SDK

    文章来源:小青年原创发布时间:2016-06-15关键词:mui,环信 web im,html5+,im,页面传值,缓存转载需标注本文原始地址: http://zhaomenghuan.github. ...

  7. iOS:融云即时通讯快速集成

    一.介绍 即时通讯在众多社交软件.生活软件以及教育软件中已经是必备的功能了,在当前国内,即时通讯SDK做的比较不错的有那么几家,例如环信SDK.融云SDK...,这两家做的都很不错,各有千秋吧,要是真 ...

  8. GIM企业即时通讯

    GIM企业即时通讯是笔者Garfield(QQ:3674571)采用.NetFramework4.0+SQL2008R2开发的一套企业内网/外网 通用的即时通讯(IM)软件,分为服务器端和客户端,通讯 ...

  9. apicloud+融云实现即时通讯

    请尊重作者的辛勤劳动!!! 使用apicloud开发已经快2个月了,起初的目的就是为了实现安卓和苹果的兼容,属于一个试验项目,究竟apicloud是否能够满足公司的要求?最 终看来还是不错的,使用ap ...

随机推荐

  1. FreeMarker的基础语法使用 && 心得和技巧

    FreeMarker语言 FreeMarker语言概述 FreeMarker是一个模板引擎,一个基于模板生成文本输出的通用工具,使用纯Java编写. FreeMarker被设计用来生成HTML Web ...

  2. OpenResty安装与hello world

    安装环境:CentOS 7.0 1. 安装编译工具.依赖库 yum -y install readline-devel pcre-devel openssl-devel gcc 2. 下载openre ...

  3. laravel5.2总结--数据填充

      1 生成一个seeder文件 你可以通过 make:seeder artisan命令来生成一个 Seeder.所有通过框架生成的 Seeder 都将被放置在 database/seeds 路径: ...

  4. 微信小程序-----校园头条详细开发之列表展示数据

    1.分类列表数据展示功能的实现 1.1 结构 1.2 代码实现 1.2.1  列表显示数据,.每次界面显示6条数据,发请求获取数据,动态存放 var app = getApp() Page({ dat ...

  5. Wordpress 作者模板页中的自定义帖子类型分页问题

    <?php // 获取当前页面的页数,函数的参数为 paged $paged = (get_query_var('paged')) ? get_query_var('paged') : 1; $ ...

  6. 基于单层决策树的AdaBoost算法源码

    基于单层决策树的AdaBoost算法源码 Mian.py # -*- coding: utf-8 -*- # coding: UTF-8 import numpy as np from AdaBoos ...

  7. linux命令之grep、cut

    输入: ifconfig eth0 eth0表示主机的第一块网卡. 输出: eth0: flags=<UP,BROADCAST,RUNNING,MULTICAST> mtu inet 19 ...

  8. JVM虚拟机系列(三)Class文件格式

  9. 整合SpringMVC与Mybatis

    第一步.导包 第二步.配置springmvc springmvc.xml <?xml version="1.0" encoding="UTF-8"?> ...

  10. Log4j官方文档翻译(八、文件输出)

    使用org.apache.log4j.FileAppender可以把日志写到文件中: FileAppender配置 immediateFlush 这个标志默认为true,是否每次有消息产生都自动flu ...