Add font rendering.
This commit is contained in:
		
							parent
							
								
									1f6a3a55e1
								
							
						
					
					
						commit
						118b50cee2
					
				| @ -1,6 +1,9 @@ | ||||
| using System; | ||||
| using System.Runtime.InteropServices; | ||||
| 
 | ||||
| // Disable unused warnings for native types. | ||||
| #pragma warning disable CS0649 | ||||
| 
 | ||||
| namespace Quik.FreeType | ||||
| { | ||||
|     public struct FTLibrary | ||||
|  | ||||
							
								
								
									
										16
									
								
								Quik.Media.Defaults/FTProvider.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								Quik.Media.Defaults/FTProvider.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| using System; | ||||
| using Quik.FreeType; | ||||
| 
 | ||||
| namespace Quik.Media.Defaults | ||||
| { | ||||
|     public static class FTProvider | ||||
|     { | ||||
|         private static FTLibrary _ft; | ||||
|         public static FTLibrary Ft => _ft; | ||||
| 
 | ||||
|         static FTProvider() | ||||
|         { | ||||
|             FT.InitFreeType(out _ft); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										108
									
								
								Quik.Media.Defaults/QFontFreeType.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								Quik.Media.Defaults/QFontFreeType.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,108 @@ | ||||
| using System; | ||||
| using System.IO; | ||||
| using Quik.FreeType; | ||||
| using Quik.Media; | ||||
| using Quik.Media.Color; | ||||
| 
 | ||||
| namespace Quik.Media.Defaults | ||||
| { | ||||
|     public class QFontFreeType : QFont | ||||
|     { | ||||
|         private MemoryStream ms; | ||||
|         private FTFace face; | ||||
| 
 | ||||
|         public override FontInfo Info => throw new NotImplementedException(); | ||||
| 
 | ||||
|         public QFontFreeType(Stream stream) | ||||
|         { | ||||
|             ms = new MemoryStream(); | ||||
|             stream.CopyTo(ms); | ||||
| 
 | ||||
|             FTError e = FT.NewMemoryFace(Ft, ms.GetBuffer(), ms.Length, 0, out face); | ||||
|             if (e != FTError.None) | ||||
|             { | ||||
|                 throw new Exception("Could not load font face from stream."); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         public override bool HasRune(int rune) | ||||
|         { | ||||
|             return FT.GetCharIndex(face, (ulong)rune) != 0; | ||||
|         } | ||||
| 
 | ||||
|         public override QFontPage RasterizePage(int codepage, float size, in FontRasterizerOptions options) | ||||
|         { | ||||
|             FT.SetCharSize(face, 0, (long)Math.Round(64*size), 0, (uint)Math.Round(options.Resolution)); | ||||
|             QGlyphMetrics[] allMetrics = new QGlyphMetrics[256]; | ||||
| 
 | ||||
|             // Figure out the map size needed. | ||||
|             int pixels = 0; | ||||
|             for (int i = 0; i < 256; i++) | ||||
|             { | ||||
|                 uint index = FT.GetCharIndex(face, (ulong)(codepage + i)); | ||||
|                 FT.LoadGlyph(face, index, FTLoadFlags.Default); | ||||
| 
 | ||||
|                 ref readonly FTGlyphMetrics metrics = ref face.Glyph.Metrics; | ||||
|                 allMetrics[i] = new QGlyphMetrics( | ||||
|                     codepage + i, | ||||
|                     new QVec2(metrics.Width, metrics.Height), | ||||
|                     new QVec2(metrics.HorizontalBearingX/64f, metrics.HorizontalBearingY/64f), | ||||
|                     new QVec2(metrics.VerticalBearingX/64f, metrics.VerticalBearingY/64f), | ||||
|                     new QVec2(metrics.HorizontalAdvance/64f, metrics.VerticalAdvance/64f) | ||||
|                 ); | ||||
|                 pixels = (int)Math.Max(pixels, Math.Max(face.Glyph.Metrics.Width/64f, face.Glyph.Metrics.Height/64f)); | ||||
|             } | ||||
| 
 | ||||
|             int bits = Math.ILogB(pixels); | ||||
|             if (1 << bits != pixels) | ||||
|             { | ||||
|                 pixels = 1 << bits + 1; | ||||
|             } | ||||
| 
 | ||||
|             // Now we can create a bitmap and render our glyphs. | ||||
| 
 | ||||
|             QImageBuffer buffer = new QImageBuffer(QImageFormat.RedU8, (int)pixels, (int)pixels, 256); | ||||
| 
 | ||||
|             for (int i = 0; i < 256; i++) | ||||
|             { | ||||
|                 uint index = FT.GetCharIndex(face, (ulong)(codepage + i)); | ||||
|                 FT.LoadGlyph(face, index, FTLoadFlags.Default); | ||||
|                 FT.RenderGlyph(face.Glyph, options.Sdf ? FTRenderMode.Sdf : FTRenderMode.Mono); | ||||
| 
 | ||||
|                 ref readonly FTBitmap bitmap = ref face.Glyph.Bitmap; | ||||
| 
 | ||||
|                 buffer.LockBits3d(out QImageLock dst, QImageLockOptions.Default, i); | ||||
| 
 | ||||
|                 if (bitmap.Buffer != IntPtr.Zero) unsafe | ||||
|                 { | ||||
|                     for (int j = 0; j < bitmap.Rows; j++) | ||||
|                     { | ||||
|                         Buffer.MemoryCopy( | ||||
|                             (byte*)bitmap.Buffer + (j * bitmap.Pitch), | ||||
|                             (byte*)dst.ImagePtr  + (j * dst.Width), | ||||
|                             dst.Width, | ||||
|                             bitmap.Width); | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 buffer.UnlockBits(); | ||||
|             } | ||||
| 
 | ||||
|             return new QFontPage(this, codepage, size, options, buffer, allMetrics); | ||||
|         } | ||||
| 
 | ||||
|         protected override void Dispose(bool disposing) | ||||
|         { | ||||
|             base.Dispose(disposing); | ||||
| 
 | ||||
|             if (disposing) | ||||
|             { | ||||
|                 ms.Dispose(); | ||||
|             } | ||||
| 
 | ||||
|             FT.DoneFace(face); | ||||
|         } | ||||
| 
 | ||||
|         private static FTLibrary Ft => FTProvider.Ft; | ||||
|     } | ||||
| } | ||||
| @ -10,7 +10,7 @@ namespace Quik.Media.Defaults | ||||
|     public unsafe class QImageStbi : QImage | ||||
|     { | ||||
|         private readonly StbImage image; | ||||
|         private ImageBuffer buffer; | ||||
|         private QImageBuffer buffer; | ||||
| 
 | ||||
|         public override int Width => image.Width; | ||||
| 
 | ||||
| @ -47,8 +47,8 @@ namespace Quik.Media.Defaults | ||||
|             if (options.MipLevel > 0) throw new Exception("This image has no mip levels."); | ||||
| 
 | ||||
|             buffer?.Dispose(); | ||||
|             buffer = new ImageBuffer(options.Format, Width, Height); | ||||
|             QImageLock dst = buffer.Lock(); | ||||
|             buffer = new QImageBuffer(options.Format, Width, Height); | ||||
|             buffer.LockBits2d(out QImageLock dst, QImageLockOptions.Default); | ||||
| 
 | ||||
|             byte *srcPtr = (byte*)image.ImagePointer; | ||||
|             QImageLock src = new QImageLock(InternalFormat, Width, Height, 1, (IntPtr)srcPtr); | ||||
| @ -76,7 +76,7 @@ namespace Quik.Media.Defaults | ||||
| 
 | ||||
|         public override void UnlockBits() | ||||
|         { | ||||
|             buffer.Unlock(); | ||||
|             buffer.UnlockBits(); | ||||
|         } | ||||
| 
 | ||||
|         protected override void Dispose(bool disposing) | ||||
|  | ||||
| @ -10,7 +10,7 @@ | ||||
|   <ItemGroup> | ||||
|     <ProjectReference Include="..\Quik\Quik.csproj" /> | ||||
|     <ProjectReference Include="..\Quik.StbImage\Quik.StbImage.csproj" /> | ||||
|     <!--ProjectReference Include="..\Quik.FreeType\Quik.FreeType.csproj" /--> | ||||
|     <ProjectReference Include="..\Quik.FreeType\Quik.FreeType.csproj" /> | ||||
|   </ItemGroup> | ||||
| 
 | ||||
| </Project> | ||||
|  | ||||
| @ -7,7 +7,7 @@ | ||||
|   </PropertyGroup> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="OpenTK" Version="4.7.4" /> | ||||
|     <PackageReference Include="OpenTK" Version="4.8.0" /> | ||||
|   </ItemGroup> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|  | ||||
| @ -1,60 +1,92 @@ | ||||
| using System; | ||||
| using System.Runtime.InteropServices; | ||||
| 
 | ||||
| namespace Quik.Media.Color | ||||
| namespace Quik.Media | ||||
| { | ||||
|     public class ImageBuffer : IDisposable | ||||
|     public class QImageBuffer : QImage | ||||
|     { | ||||
|         private byte[] buffer; | ||||
|         GCHandle handle; | ||||
| 
 | ||||
|         public QImageFormat Format { get; } | ||||
|         public int Width { get; } | ||||
|         public int Height { get; } | ||||
|         public int Depth { get; } | ||||
|         public override QImageFormat InternalFormat { get; } | ||||
|         public override int Width { get; } | ||||
|         public override int Height { get; } | ||||
|         public override int Depth { get; } | ||||
| 
 | ||||
|         public ImageBuffer(QImageFormat format, int width, int height, int depth = 1) | ||||
|         public QImageBuffer(QImageFormat format, int width, int height, int depth = 1) | ||||
|         { | ||||
|             Format = format; | ||||
|             InternalFormat = format; | ||||
|             Width = width; | ||||
|             Height = height; | ||||
|             Depth = depth; | ||||
| 
 | ||||
|             buffer = new byte[width * height * depth]; | ||||
|             buffer = new byte[width * height * depth * format.BytesPerPixel()]; | ||||
|         } | ||||
|         ~ImageBuffer() | ||||
|         ~QImageBuffer() | ||||
|         { | ||||
|             Dispose(false); | ||||
|         } | ||||
| 
 | ||||
|         public QImageLock Lock() | ||||
|         private QImageLock Lock() | ||||
|         { | ||||
|             handle.Free(); | ||||
|             handle = GCHandle.Alloc(buffer, GCHandleType.Pinned); | ||||
|             IntPtr ptr = Marshal.UnsafeAddrOfPinnedArrayElement(buffer, 0); | ||||
|             return new QImageLock(Format, Width, Height, Depth, ptr); | ||||
|             return new QImageLock(InternalFormat, Width, Height, Depth, ptr); | ||||
|         } | ||||
| 
 | ||||
|         public void Unlock() | ||||
|         protected override void Dispose(bool disposing) | ||||
|         { | ||||
|             handle.Free(); | ||||
|         } | ||||
| 
 | ||||
|         private bool isDiposed = false; | ||||
|         private void Dispose(bool disposing) | ||||
|         { | ||||
|             if (isDiposed) return; | ||||
| 
 | ||||
|             if (handle.IsAllocated) handle.Free(); | ||||
|             buffer = null; | ||||
|             handle.Free(); | ||||
| 
 | ||||
|             isDiposed = true; | ||||
|             GC.SuppressFinalize(this); | ||||
|         } | ||||
| 
 | ||||
|         public void Dispose() | ||||
|         public override void LockBits2d(out QImageLock imageLock, QImageLockOptions options) | ||||
|         { | ||||
|             Dispose(true); | ||||
|             if (options.Format != options.Format) throw new InvalidOperationException("This image type cannot be converted."); | ||||
|             if (Depth > 1) throw new InvalidOperationException("This texture has a depth component."); | ||||
| 
 | ||||
|             UnlockBits(); | ||||
| 
 | ||||
|             handle = GCHandle.Alloc(buffer, GCHandleType.Pinned); | ||||
|             IntPtr ptr = Marshal.UnsafeAddrOfPinnedArrayElement(buffer, 0); | ||||
|             imageLock = new QImageLock(InternalFormat, Width, Height, Depth, ptr); | ||||
|         } | ||||
| 
 | ||||
|         public override void LockBits3d(out QImageLock imageLock, QImageLockOptions options) | ||||
|         { | ||||
|             if (options.Format != options.Format) throw new InvalidOperationException("This image type cannot be converted."); | ||||
|             UnlockBits(); | ||||
| 
 | ||||
|             handle = GCHandle.Alloc(buffer, GCHandleType.Pinned); | ||||
|             IntPtr ptr = Marshal.UnsafeAddrOfPinnedArrayElement(buffer, 0); | ||||
|             imageLock = new QImageLock(InternalFormat, Width, Height, Depth, ptr); | ||||
|         } | ||||
| 
 | ||||
|         public override void LockBits3d(out QImageLock imageLock, QImageLockOptions options, int depth) | ||||
|         { | ||||
|             if (options.Format != options.Format) throw new InvalidOperationException("This image type cannot be converted."); | ||||
|             if (depth < 0 || depth > Depth) | ||||
|                 throw new ArgumentOutOfRangeException(nameof(depth), "Depth must be in range."); | ||||
| 
 | ||||
|             UnlockBits(); | ||||
| 
 | ||||
|             handle = GCHandle.Alloc(buffer, GCHandleType.Pinned); | ||||
|             IntPtr ptr = Marshal.UnsafeAddrOfPinnedArrayElement(buffer, 0); | ||||
|             imageLock = new QImageLock( | ||||
|                 InternalFormat, | ||||
|                 Width, | ||||
|                 Height, | ||||
|                 1, | ||||
|                 ptr + (depth * Width * Height * InternalFormat.BytesPerPixel())); | ||||
|         } | ||||
| 
 | ||||
|         public override void UnlockBits() | ||||
|         { | ||||
|             if (handle.IsAllocated) | ||||
|                 handle.Free(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -32,7 +32,7 @@ namespace Quik.Media | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         public static int BitsPerPixel(this QImageFormat format) | ||||
|         public static int BytesPerPixel(this QImageFormat format) | ||||
|         { | ||||
|             switch (format) | ||||
|             { | ||||
|  | ||||
| @ -6,31 +6,27 @@ namespace Quik.Media | ||||
|     { | ||||
|         public string    Family { get; } | ||||
|         public FontStyle Style { get; } | ||||
|         public float     Size { get; } | ||||
| 
 | ||||
|         public override string ToString() | ||||
|         { | ||||
|             return $"{Family} {Style} {Size}"; | ||||
|             return $"{Family} {Style}"; | ||||
|         } | ||||
| 
 | ||||
|         public override int GetHashCode() | ||||
|         { | ||||
|             return Family.GetHashCode() ^ | ||||
|                    (Style.GetHashCode() * 3976061) ^ | ||||
|                    (Size.GetHashCode() * 9428791); | ||||
|                    (Style.GetHashCode() * 3976061); | ||||
|         } | ||||
| 
 | ||||
|         public static bool operator==(FontInfo a, FontInfo b) | ||||
|         { | ||||
|             return  (a.Style == b.Style)    && | ||||
|                     (a.Size == b.Size)      && | ||||
|                     (a.Family == a.Family); | ||||
|         } | ||||
| 
 | ||||
|         public static bool operator!=(FontInfo a, FontInfo b) | ||||
|         { | ||||
|             return  (a.Style != b.Style)    || | ||||
|                     (a.Size != b.Size)      || | ||||
|                     (a.Family != b.Family); | ||||
|         } | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										66
									
								
								Quik/Media/ImageBuffer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								Quik/Media/ImageBuffer.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,66 @@ | ||||
| using System; | ||||
| using System.Runtime.InteropServices; | ||||
| 
 | ||||
| namespace Quik.Media.Color | ||||
| { | ||||
|     public class QImageBuffer : QImage | ||||
|     { | ||||
|         private byte[] buffer; | ||||
|         GCHandle handle; | ||||
| 
 | ||||
|         public override QImageFormat InternalFormat { get; } | ||||
|         public override int Width { get; } | ||||
|         public override int Height { get; } | ||||
|         public override int Depth { get; } | ||||
| 
 | ||||
|         public QImageBuffer(QImageFormat format, int width, int height, int depth = 1) | ||||
|         { | ||||
|             InternalFormat = format; | ||||
|             Width = width; | ||||
|             Height = height; | ||||
|             Depth = depth; | ||||
| 
 | ||||
|             buffer = new byte[width * height * depth]; | ||||
|         } | ||||
|         ~QImageBuffer() | ||||
|         { | ||||
|             Dispose(false); | ||||
|         } | ||||
| 
 | ||||
|         private QImageLock Lock() | ||||
|         { | ||||
|             handle.Free(); | ||||
|             handle = GCHandle.Alloc(buffer, GCHandleType.Pinned); | ||||
|             IntPtr ptr = Marshal.UnsafeAddrOfPinnedArrayElement(buffer, 0); | ||||
|             return new QImageLock(InternalFormat, Width, Height, Depth, ptr); | ||||
|         } | ||||
| 
 | ||||
|         protected override void Dispose(bool disposing) | ||||
|         { | ||||
|             buffer = null; | ||||
|             handle.Free(); | ||||
| 
 | ||||
|             GC.SuppressFinalize(this); | ||||
|         } | ||||
| 
 | ||||
|         public override void LockBits2d(out QImageLock imageLock, QImageLockOptions options) | ||||
|         { | ||||
|             imageLock = Lock(); | ||||
|         } | ||||
| 
 | ||||
|         public override void LockBits3d(out QImageLock imageLock, QImageLockOptions options) | ||||
|         { | ||||
|             imageLock = Lock(); | ||||
|         } | ||||
| 
 | ||||
|         public override void LockBits3d(out QImageLock imageLock, QImageLockOptions options, int depth) | ||||
|         { | ||||
|             imageLock = Lock(); | ||||
|         } | ||||
| 
 | ||||
|         public override void UnlockBits() | ||||
|         { | ||||
|             handle.Free(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -10,12 +10,10 @@ namespace Quik.Media | ||||
|         public abstract FontInfo Info { get; } | ||||
|         public string Family => Info.Family; | ||||
|         public FontStyle Style => Info.Style; | ||||
|         public float Size => Info.Size; | ||||
| 
 | ||||
|         public abstract bool HasRune(int rune); | ||||
|         public abstract QImage RenderPage(int codepage, float resolution, bool sdf); | ||||
|         public abstract QGlyphMetrics GetMetricsForRune(int rune); | ||||
|         public abstract QGlyphMetrics[] GetMetricsForPage(int codepage); | ||||
|         public abstract QFontPage RasterizePage(int codepage, float size, in FontRasterizerOptions options); | ||||
|         public QFontPage RasterizePage(int codepage, float size) => RasterizePage(codepage, size, FontRasterizerOptions.Default); | ||||
| 
 | ||||
|         // IDisposable | ||||
|         private bool isDisposed = false; | ||||
| @ -30,4 +28,62 @@ namespace Quik.Media | ||||
|         protected virtual void Dispose(bool disposing) { } | ||||
|         public void Dispose() => DisposePrivate(true); | ||||
|     } | ||||
| 
 | ||||
|     public struct FontRasterizerOptions | ||||
|     { | ||||
|         public float Resolution { get; set; } | ||||
|         public bool Sdf { get; set; } | ||||
| 
 | ||||
|         public static readonly FontRasterizerOptions Default = new FontRasterizerOptions() | ||||
|         { | ||||
|             Resolution = 96.0f, | ||||
|             Sdf        = true | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     public class QFontPage : IDisposable | ||||
|     { | ||||
|         public QFont Font { get; } | ||||
|         public int CodePage { get; } | ||||
|         public float Size { get; } | ||||
|         public virtual QImage Image { get; } = null; | ||||
|         public virtual QGlyphMetrics[] Metrics { get; } = Array.Empty<QGlyphMetrics>(); | ||||
| 
 | ||||
|         public FontRasterizerOptions Options { get; } | ||||
|         public float Resolution => Options.Resolution; | ||||
|         public bool Sdf => Options.Sdf; | ||||
| 
 | ||||
|         public void Dispose() => DisposeInternal(false); | ||||
| 
 | ||||
|         protected QFontPage(QFont font, int codepage, float size, in FontRasterizerOptions options) | ||||
|         { | ||||
|             Font = font; | ||||
|             CodePage = codepage; | ||||
|             Size = size; | ||||
|             Options = options; | ||||
|         } | ||||
|         public QFontPage(QFont font, int codepage, float size, in FontRasterizerOptions options, QImage image, QGlyphMetrics[] metrics) | ||||
|             : this(font, codepage, size, options) | ||||
|         { | ||||
|             Image = image; | ||||
|             Metrics = metrics; | ||||
|         } | ||||
| 
 | ||||
|         private bool isDisposed = false; | ||||
|         private void DisposeInternal(bool disposing) | ||||
|         { | ||||
|             if (isDisposed) return; | ||||
| 
 | ||||
|             Dispose(disposing); | ||||
|             isDisposed = true; | ||||
|         } | ||||
| 
 | ||||
|         protected virtual void Dispose(bool disposing) | ||||
|         { | ||||
|             if (disposing) | ||||
|             { | ||||
|                 Image?.Dispose(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -10,10 +10,10 @@ namespace Quik.Media | ||||
|         /// </summary> | ||||
|         public int Rune { get; } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Location of the glyph on the atlas. | ||||
|         /// </summary> | ||||
|         public QRectangle Location { get; } | ||||
|         // /// <summary> | ||||
|         // /// Location of the glyph on the atlas. | ||||
|         // /// </summary> | ||||
|         // public QRectangle Location { get; } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Size of the glyph in units. | ||||
| @ -37,14 +37,14 @@ namespace Quik.Media | ||||
| 
 | ||||
|         public QGlyphMetrics( | ||||
|             int character, | ||||
|             QRectangle location, | ||||
|             // QRectangle location, | ||||
|             QVec2 size, | ||||
|             QVec2 horizontalBearing, | ||||
|             QVec2 verticalBearing, | ||||
|             QVec2 advance) | ||||
|         { | ||||
|             Rune = character; | ||||
|             Location = location; | ||||
|             // Location = location; | ||||
|             Size = size; | ||||
|             HorizontalBearing = horizontalBearing; | ||||
|             VerticalBearing = verticalBearing; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user