C#自定义TemplateImage使用模板底图,运行时根据用户或产品信息生成海报图(1)
由于经常需要基于固定的一个模板底图,生成微信小程序分享用的海报图,如果每次都调用绘图函数,手动编写每个placeholder的填充,重复而且容易出错,因此,封装一个TemplateImage,用于填充每个需要画上数据的地方,
先看看调用的方式:
_homeShareTemplate.Generate(new TemplateItem[] //Generate返回新的Bitmap { new StringTemplateItem() //日期 { Location = new Point(80 * 2, 78*2), Font = new Font("宋体", 42, FontStyle.Bold, GraphicsUnit.Pixel), Color = Color.FromArgb(0x8e, 0x1a, 0x22), Value = DateTime.Now.ToString("yyyy.MM.dd"), Horizontal = HorizontalPosition.Center }, new StringTemplateItem() //农历 { Location = new Point(230*2, 166*2), //MaxWidth = 15, Font = new Font("宋体", 22, FontStyle.Bold, GraphicsUnit.Pixel), Color = Color.FromArgb(0x8e, 0x1a, 0x22), StringFormat = new StringFormat(StringFormatFlags.DirectionVertical), Value = GetMonthCalendar(DateTime.Now) }, new StringTemplateItem() //星期 { Location = new Point(256*2, 175*2), //MaxWidth = 15, Font = new Font("宋体", 24, FontStyle.Bold, GraphicsUnit.Pixel), Color = Color.FromArgb(0x8e, 0x1a, 0x22), StringFormat = new StringFormat(StringFormatFlags.DirectionVertical), Value = GetWeekName(DateTime.Now) }, new ImageTemplateItem() //图片 { Image = (Bitmap) Bitmap.FromFile(Path.Join(Directory.GetCurrentDirectory(),weather.MainImageUrl)), Location = new Point(81*2, 108*2), Size = new Size(132*2, 133*2) }, new StringTemplateItem() { Location = new Point(88*2, 257*2), MaxWidth = 125*2, Font = new Font("楷体", 30, FontStyle.Bold, GraphicsUnit.Pixel), Color = Color.FromArgb(0x17, 0x14, 0x0e), Value = weather.Content.Left(44) }, new StringTemplateItem() //宜 { Location = new Point(35*2+3,294*2), Color = Color.FromArgb(0x8f, 0x1A, 0x22), Font = new Font("宋体", 38, FontStyle.Bold, GraphicsUnit.Pixel), StringFormat = new StringFormat(StringFormatFlags.DirectionVertical), //MaxWidth = 14, Value = weather.Yi.Left(4) }, new StringTemplateItem() //忌 { Location = new Point(228*2+3,294*2), Color = Color.FromArgb(0x8f, 0x1A, 0x22), Font = new Font("宋体", 38, FontStyle.Bold, GraphicsUnit.Pixel), StringFormat = new StringFormat(StringFormatFlags.DirectionVertical), //MaxWidth = 14, Value = weather.Ji.Left(4) }, new QrCodeTemplateItem() //二维码 { Location = new Point(188*2, 421*2), Size = new Size(73*2, 72*2), QrCode = "http://ssssss.com/sdfsdfsdfs/sss" } });
输出的效果如下:
完整的功能由一个TemplateImage作为模板图管理的类+N个根据需要输出的各种数据处理类,可根据实际需求进行扩展不同的类型,默认有:String,Image,QrCode三种:
单个模板图管理类的定义:
public class TemplateImage:IDisposable { private Bitmap _templateSource = null; private Stream _sourceStream = null; private FileSystemWatcher _wather = null; public TemplateImage(Bitmap templateSource) { _templateSource = templateSource; } /// <summary> /// 模板图片的构造函数 /// </summary> /// <param name="templatePath">模板图片文件绝对路径</param> /// <param name="isWatchFileModify">是否自动监控文件,当文件有变动时,自动重新加载模板文件 /// </param> public TemplateImage(string templatePath,bool isWatchFileModify=true) { if (!File.Exists(templatePath)) { throw new FileNotFoundException(nameof(templatePath)); } //打开模板文件路径,在跳出构造函数后,自动释放file对象,防止长久占用文件,导致无法替换模板文件 using var file = File.OpenRead(templatePath); var data = file.ReadAllBytes(); var s1 = new ByteStream(data); //这里s1肯定不能关闭,否则,再调用Bitmap.Clone函数的时候,会报错 _sourceStream = s1; _templateSource = (Bitmap) Bitmap.FromStream(s1); if (isWatchFileModify) //如果启用文件监控,则自动监控模板图片文件 { _wather = new FileSystemWatcher(templatePath); _wather.EnableRaisingEvents = true; _wather.Changed += wather_changed; } } private void wather_changed(object sender, FileSystemEventArgs e) { if (e.ChangeType == WatcherChangeTypes.Changed || e.ChangeType== WatcherChangeTypes.Created ) { using var file = File.OpenRead(e.FullPath); var data = file.ReadAllBytes(); var oldValue = _sourceStream; var templateSource = _templateSource; var s1 = new ByteStream(data); var newTemplateSource = (Bitmap) Bitmap.FromStream(s1); _sourceStream = s1; _templateSource = newTemplateSource; oldValue.Close(); oldValue.Dispose(); templateSource.Dispose(); } } public SmoothingMode SmoothingMode { set; get; } = SmoothingMode.AntiAlias; public TextRenderingHint TextRenderingHint { set; get; } = TextRenderingHint.AntiAlias; public CompositingQuality CompositingQuality { set; get; } = CompositingQuality.HighQuality; /// <summary> /// 根据传入的数据,套入模板图片,生成新的图片 /// </summary> /// <param name="settings"></param> /// <returns></returns> public Bitmap Generate(TemplateItemBase[] settings) { //Clone一个新的Bitmap对象 var newImg = (Bitmap)_templateSource.Clone(); var g1 = Graphics.FromImage(_templateSource); try { using (var g = Graphics.FromImage(newImg)) { g.SmoothingMode = SmoothingMode; g.TextRenderingHint = TextRenderingHint; g.CompositingQuality = CompositingQuality; foreach (var item in settings) { item.Draw(g, newImg.Size); //调用每个Item的Draw画入新的数据 } return newImg; } } catch (Exception e) { Console.WriteLine(e); throw; } } public void Dispose() { _templateSource.Dispose(); _sourceStream?.Close(); _sourceStream?.Dispose(); } }
至此,一个模板图片类已定义完成,接下来需要定义一个Placeholder的基类:
1 public abstract class TemplateItemBase 2 { 3 /// <summary> 4 /// 水平方向对其方式,默认为Custom,使用Location定位 5 /// </summary> 6 public HorizontalPosition Horizontal { set; get; } = HorizontalPosition.Custom; 7 8 /// <summary> 9 /// 垂直方向对其方式,默认为Custom,使用Location定位 10 /// </summary> 11 public VerticalPosition Vertical { set; get; } = VerticalPosition.Custom; 12 13 /// <summary> 14 /// 输出项定位 15 /// </summary> 16 public Point Location { set; get; } 17 18 public abstract void Draw(Graphics graphics,Size newBitmapSize); 19 20 }
这个基类定义了每个placeholder的定位方式,Custom表示使用Location自定义位置.
然后开始来定义每个不同类型的TemplateItem:
1.String类型:
1 /// <summary> 2 /// 普通字符串项 3 /// </summary> 4 public class StringTemplateItem : TemplateItemBase 5 { 6 /// <summary> 7 /// 文本字符串值 8 /// </summary> 9 public string Value { set; get; } 10 11 /// <summary> 12 /// 字体信息 13 /// </summary> 14 public Font Font { set; get; } 15 16 /// <summary> 17 /// 字体颜色 18 /// </summary> 19 public Color Color { set; get; }= Color.Black; 20 21 /// <summary> 22 /// 文本输出的最大宽度,如果为0,则自动,,如果非0,则只用最大宽度,并自动根据最大宽度修改计算字符串所需高度 23 /// </summary> 24 public int MaxWidth { set; get; } = 0; 25 26 /// <summary> 27 /// 字符串输出参数 28 /// </summary> 29 /// <example> 30 /// 如纵向输出: 31 /// new StringFormat(StringFormatFlags.DirectionVertical) 32 /// 33 /// </example> 34 public StringFormat StringFormat { set; get; } 35 36 public override void Draw(Graphics graphics,Size newBitmapSize) 37 { 38 var location = this.Location; 39 SizeF size=default(Size); 40 if (this.Horizontal== HorizontalPosition.Center || this.Vertical== VerticalPosition.Middle) 41 { 42 location = new Point(this.Location.X,this.Location.Y); 43 44 if (this.MaxWidth>0) 45 { 46 size = graphics.MeasureString(this.Value, this.Font,this.MaxWidth); 47 } 48 else 49 { 50 size = graphics.MeasureString(this.Value, this.Font); 51 } 52 53 if (this.Horizontal== HorizontalPosition.Center) 54 { 55 var newx = newBitmapSize.Width / 2 - (int)(size.Width / 2); 56 location.X = newx; 57 } 58 59 if (this.Vertical== VerticalPosition.Middle) 60 { 61 var newy= newBitmapSize.Height / 2 - (int)(size.Height / 2); 62 location.Y = newy; 63 } 64 } 65 else if(MaxWidth>0) 66 { 67 size = graphics.MeasureString(this.Value, this.Font,this.MaxWidth); 68 } 69 70 if (MaxWidth>0) 71 { 72 graphics.DrawString(this.Value, this.Font,new SolidBrush(this.Color), new RectangleF(location,size),StringFormat); 73 } 74 else 75 { 76 graphics.DrawString(this.Value, this.Font,new SolidBrush(this.Color), location,StringFormat); 77 } 78 79 80 } 81 }
2.纯图片类型:
1 /// <summary> 2 /// 传入一个图片 3 /// </summary> 4 public class ImageTemplateItem:TemplateItemBase 5 { 6 /// <summary> 7 /// 图片数据 8 /// </summary> 9 public Bitmap Image { set; get; } 10 11 /// <summary> 12 /// 图片输出到模板图的时候的大小 13 /// </summary> 14 public Size Size { set; get; } 15 16 public override void Draw(Graphics graphics,Size newBitmapSize) 17 { 18 var location = this.Location; 19 20 //计算垂直居中或水平居中的情况下的定位 21 if (this.Horizontal== HorizontalPosition.Center || this.Vertical== VerticalPosition.Middle) 22 { 23 location = new Point(this.Location.X,this.Location.Y); 24 25 if (this.Horizontal== HorizontalPosition.Center) 26 { 27 var newx = newBitmapSize.Width / 2 - this.Size.Width / 2; 28 29 location.X = newx; 30 } 31 32 if (this.Vertical== VerticalPosition.Middle) 33 { 34 var newy= newBitmapSize.Height / 2 - this.Size.Height / 2; 35 location.Y = newy; 36 } 37 } 38 39 //此处后续可优化为使用Lockbits的方式 40 graphics.DrawImage(Image,new Rectangle(location,this.Size),new Rectangle(0,0,this.Image.Width,this.Image.Height),GraphicsUnit.Pixel); 41 42 } 43 }
3.QrCode的方式,使用QRCoder类库:
1 /// <summary> 2 /// 二维码项 3 /// </summary> 4 public class QrCodeTemplateItem : TemplateItemBase 5 { 6 /// <summary> 7 /// 二维码内实际存储的字符数据 8 /// </summary> 9 public string QrCode { set; get; } 10 11 /// <summary> 12 /// 二维码中心的icon图标 13 /// </summary> 14 public Bitmap Icon { set; get; } 15 16 /// <summary> 17 /// 二维码尺寸 18 /// </summary> 19 public Size Size { set; get; } 20 21 /// <summary> 22 /// 容错级别,默认为M 23 /// </summary> 24 public QRCodeGenerator.ECCLevel ECCLevel { set; get; } = QRCodeGenerator.ECCLevel.M; 25 26 public override void Draw(Graphics graphics,Size newBitmapSize) 27 { 28 var location = this.Location; 29 30 if (this.Horizontal== HorizontalPosition.Center || this.Vertical== VerticalPosition.Middle) 31 { 32 location = new Point(this.Location.X,this.Location.Y); 33 34 if (this.Horizontal== HorizontalPosition.Center) 35 { 36 var newx = newBitmapSize.Width / 2 - this.Size.Width / 2; 37 38 location.X = newx; 39 } 40 41 if (this.Vertical== VerticalPosition.Middle) 42 { 43 var newy= newBitmapSize.Height / 2 - this.Size.Height / 2; 44 location.Y = newy; 45 } 46 } 47 48 using (QRCodeGenerator qrGenerator = new QRCodeGenerator()) 49 using (QRCodeData qrCodeData = qrGenerator.CreateQrCode(QrCode,ECCLevel)) 50 using (QRCode qrCode = new QRCode(qrCodeData)) 51 using (Bitmap qrCodeImage = qrCode.GetGraphic(20,Color.Black,Color.White,Icon)) 52 { 53 graphics.DrawImage(qrCodeImage,new Rectangle(location,this.Size),new Rectangle(0,0,qrCodeImage.Width,qrCodeImage.Height),GraphicsUnit.Pixel); 54 55 } 56 } 57 }
后续的优化:
1.Image画入的优化处理,考虑是否可以用Lockbits进行优化
2.增加不同类型的新的Item
完整的代码详见:https://github.com/kugarliyifan/Kugar.Core/blob/master/Kugar.Core.NetCore/Images/TemplateImage.cs