C#自定義TemplateImage使用模板底圖,運行時根據用戶或產品資訊生成海報圖(1)

  • 2020 年 12 月 30 日
  • 筆記

由於經常需要基於固定的一個模板底圖,生成微信小程式分享用的海報圖,如果每次都調用繪圖函數,手動編寫每個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 = "//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

完整的程式碼詳見://github.com/kugarliyifan/Kugar.Core/blob/master/Kugar.Core.NetCore/Images/TemplateImage.cs