Add font rendering.

This commit is contained in:
H. Utku Maden 2023-09-22 19:30:17 +03:00
parent 1f6a3a55e1
commit 118b50cee2
Signed by: themixedupstuff
GPG Key ID: 25A001B636F17843
12 changed files with 325 additions and 48 deletions

@ -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

@ -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);
}
}
}

@ -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

@ -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;