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…
Reference in New Issue
Block a user