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;
/// <summary>Offscreen host GDI+ rasterization of the actual pinned TTF. No browser or visible window.</summary>
internal sealed class WindowsTextCanvas : IDesktopWebCanvas, IDisposable
{
private readonly PrivateFontCollection fonts = new();
private readonly Bitmap surface;
private readonly Graphics graphics;
private readonly Stack<GraphicsState> 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<height;y++){
Marshal.Copy(locked.Scan0+y*locked.Stride,row,0,width);
for(int x=0;x<width;x++){alpha[y*width+x]=(byte)((uint)row[x]>>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();}
}
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;
/// <summary>Offscreen host GDI+ rasterization of the actual pinned TTF. No browser or visible window.</summary>
internal sealed class WindowsTextCanvas : IDesktopWebCanvas, IDisposable
{
private readonly PrivateFontCollection fonts = new();
private readonly Bitmap surface;
private readonly Graphics graphics;
private readonly Stack<GraphicsState> 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<height;y++){
Marshal.Copy(locked.Scan0+y*locked.Stride,row,0,width);
for(int x=0;x<width;x++){alpha[y*width+x]=(byte)((uint)row[x]>>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();}
}