using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.Drawing.Text; using System.Runtime.InteropServices; using System.Security.Cryptography; using XFESystem.Desktop; /// Offscreen host GDI+ rasterization of the actual pinned TTF. No browser or visible window. internal sealed class WindowsTextCanvas : IDesktopWebCanvas, IDisposable { private readonly PrivateFontCollection fonts = new(); private readonly Bitmap surface; private readonly Graphics graphics; private readonly Stack clips = []; private readonly StringFormat format = (StringFormat)StringFormat.GenericTypographic.Clone(); private readonly Dictionary<(int Size,int Weight),Font> fontCache = []; public WindowsTextCanvas(string fontPath,int width=800,int height=600) { fonts.AddFontFile(fontPath); FontSha256=Convert.ToHexStringLower(SHA256.HashData(File.ReadAllBytes(fontPath))); FontFamilyName=fonts.Families.Single().Name; surface=new Bitmap(width,height,PixelFormat.Format32bppArgb); graphics=Graphics.FromImage(surface);graphics.TextRenderingHint=TextRenderingHint.AntiAliasGridFit; graphics.SmoothingMode=SmoothingMode.AntiAlias;graphics.Clear(Color.Transparent); format.FormatFlags|=StringFormatFlags.MeasureTrailingSpaces; } public string FontSha256 {get;} public string FontFamilyName {get;} public long RasterCalls {get;private set;} public long RasterInkPixels {get;private set;} public long MeasureCalls {get;private set;} public int Presents {get;private set;} public int Width=>surface.Width; public int Height=>surface.Height; private Font Font(double size,int weight) { var key=((int)Math.Round(Math.Clamp(size,1,128)*64),weight>=600?700:400); if(!fontCache.TryGetValue(key,out var font)){ font=new Font(fonts.Families.Single(),key.Item1/64f,key.Item2>=600?FontStyle.Bold:FontStyle.Regular,GraphicsUnit.Pixel); fontCache.Add(key,font); } return font; } public double MeasureWebText(string text,double fontSize,string fontFamily,int fontWeight) { MeasureCalls++; return text.Length==0?0:graphics.MeasureString(text,Font(fontSize,fontWeight),int.MaxValue,format).Width; } public (int Width,int Height,byte[] Alpha) RasterizeWebText(string text,double fontSize,string fontFamily,int fontWeight) { if(text.Length>4096)throw new ArgumentOutOfRangeException(nameof(text)); var font=Font(fontSize,fontWeight); int width=Math.Max(1,(int)Math.Ceiling(MeasureWebText(text,fontSize,fontFamily,fontWeight))); int height=Math.Max(1,(int)Math.Ceiling(font.GetHeight(graphics))); using var bitmap=new Bitmap(width,height,PixelFormat.Format32bppArgb); using(var g=Graphics.FromImage(bitmap)){ g.TextRenderingHint=TextRenderingHint.AntiAliasGridFit;g.Clear(Color.Transparent); g.DrawString(text,font,Brushes.White,0,0,format); } var locked=bitmap.LockBits(new Rectangle(0,0,width,height),ImageLockMode.ReadOnly,PixelFormat.Format32bppArgb); var alpha=new byte[checked(width*height)]; try{ var row=new int[width]; for(int y=0;y>24);if(alpha[y*width+x]!=0)RasterInkPixels++;} } }finally{bitmap.UnlockBits(locked);} RasterCalls++;return(width,height,alpha); } public void Fill(uint rgb,int x,int y,int width,int height){using var brush=new SolidBrush(Color.FromArgb((int)(0xFF000000|rgb)));graphics.FillRectangle(brush,x,y,width,height);} public void Line(uint rgb,int x1,int y1,int x2,int y2){using var pen=new Pen(Color.FromArgb((int)(0xFF000000|rgb)));graphics.DrawLine(pen,x1,y1,x2,y2);} public void Text(string text,uint rgb,int x,int y)=>WebText(text,rgb,x,y,13,FontFamilyName,400); public void Present(){Presents++;} public void PushClip(DesktopBounds bounds){clips.Push(graphics.Save());graphics.SetClip(new Rectangle(bounds.X,bounds.Y,bounds.Width,bounds.Height),CombineMode.Intersect);} public void PopClip(){if(!clips.TryPop(out var state))throw new InvalidOperationException("Unbalanced probe clip");graphics.Restore(state);} public void WebText(string text,uint color,double x,double y,double fontSize,string fontFamily,int fontWeight,double opacity=1){ using var brush=new SolidBrush(Color.FromArgb((int)Math.Clamp(opacity*255,0,255),(int)((color>>16)&255),(int)((color>>8)&255),(int)(color&255))); graphics.DrawString(text,Font(fontSize,fontWeight),brush,(float)x,(float)y,format); } public void SavePng(string path)=>surface.Save(path,ImageFormat.Png); public void Dispose(){foreach(var font in fontCache.Values)font.Dispose();format.Dispose();graphics.Dispose();surface.Dispose();fonts.Dispose();} }