diff --git a/Quik.FreeType/Structures.cs b/Quik.FreeType/Structures.cs index 046b38f..6a4eca8 100644 --- a/Quik.FreeType/Structures.cs +++ b/Quik.FreeType/Structures.cs @@ -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 diff --git a/Quik.Media.Defaults/FTProvider.cs b/Quik.Media.Defaults/FTProvider.cs new file mode 100644 index 0000000..fd3a290 --- /dev/null +++ b/Quik.Media.Defaults/FTProvider.cs @@ -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); + } + } +} \ No newline at end of file diff --git a/Quik.Media.Defaults/QFontFreeType.cs b/Quik.Media.Defaults/QFontFreeType.cs new file mode 100644 index 0000000..a92dbab --- /dev/null +++ b/Quik.Media.Defaults/QFontFreeType.cs @@ -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; + } +} \ No newline at end of file diff --git a/Quik.Media.Defaults/QImageStbi.cs b/Quik.Media.Defaults/QImageStbi.cs index cbcd319..06207d4 100644 --- a/Quik.Media.Defaults/QImageStbi.cs +++ b/Quik.Media.Defaults/QImageStbi.cs @@ -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) diff --git a/Quik.Media.Defaults/Quik.Media.Defaults.csproj b/Quik.Media.Defaults/Quik.Media.Defaults.csproj index 75adbf8..0868f36 100644 --- a/Quik.Media.Defaults/Quik.Media.Defaults.csproj +++ b/Quik.Media.Defaults/Quik.Media.Defaults.csproj @@ -10,7 +10,7 @@ - + diff --git a/Quik.OpenTK/Quik.OpenTK.csproj b/Quik.OpenTK/Quik.OpenTK.csproj index 977354d..16f725e 100644 --- a/Quik.OpenTK/Quik.OpenTK.csproj +++ b/Quik.OpenTK/Quik.OpenTK.csproj @@ -7,7 +7,7 @@ - + diff --git a/Quik/Media/Color/ImageBuffer.cs b/Quik/Media/Color/ImageBuffer.cs index 9c9dfb0..44b861d 100644 --- a/Quik/Media/Color/ImageBuffer.cs +++ b/Quik/Media/Color/ImageBuffer.cs @@ -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(); } } } \ No newline at end of file diff --git a/Quik/Media/Extensions.cs b/Quik/Media/Extensions.cs index 64cff5f..8c06d6c 100644 --- a/Quik/Media/Extensions.cs +++ b/Quik/Media/Extensions.cs @@ -32,7 +32,7 @@ namespace Quik.Media } } - public static int BitsPerPixel(this QImageFormat format) + public static int BytesPerPixel(this QImageFormat format) { switch (format) { diff --git a/Quik/Media/FontInfo.cs b/Quik/Media/FontInfo.cs index 73632e6..cdc3653 100644 --- a/Quik/Media/FontInfo.cs +++ b/Quik/Media/FontInfo.cs @@ -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); } diff --git a/Quik/Media/ImageBuffer.cs b/Quik/Media/ImageBuffer.cs new file mode 100644 index 0000000..2a73040 --- /dev/null +++ b/Quik/Media/ImageBuffer.cs @@ -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(); + } + } +} \ No newline at end of file diff --git a/Quik/Media/QFont.cs b/Quik/Media/QFont.cs index 33295ad..493aa94 100644 --- a/Quik/Media/QFont.cs +++ b/Quik/Media/QFont.cs @@ -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(); + + 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(); + } + } + } } \ No newline at end of file diff --git a/Quik/Media/QGlyphMetrics.cs b/Quik/Media/QGlyphMetrics.cs index 3b4538a..84a5872 100644 --- a/Quik/Media/QGlyphMetrics.cs +++ b/Quik/Media/QGlyphMetrics.cs @@ -10,10 +10,10 @@ namespace Quik.Media /// public int Rune { get; } - /// - /// Location of the glyph on the atlas. - /// - public QRectangle Location { get; } + // /// + // /// Location of the glyph on the atlas. + // /// + // public QRectangle Location { get; } /// /// 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;