Create a super basic test for fonts.

This commit is contained in:
H. Utku Maden 2023-07-16 18:31:51 +03:00
parent ecd6e8cab7
commit 6b8b3f2f0d
16 changed files with 532 additions and 31 deletions

@ -7,11 +7,6 @@
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="StbImageSharp" Version="2.27.13" />
<PackageReference Include="StbTrueTypeSharp" Version="1.26.11" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Quik\Quik.csproj" />
<ProjectReference Include="..\Quik.StbImage\Quik.StbImage.csproj" />

@ -11,4 +11,11 @@
<PackageReference Include="Quik.StbTrueType.redist" Version="1.0" />
</ItemGroup>
<ItemGroup>
<Content Include="runtimes/**">
<PackagePath>runtimes</PackagePath>
<Pack>true</Pack>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

355
Quik.StbTrueType/StbFont.cs Normal file

@ -0,0 +1,355 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
namespace Quik.Stb
{
public unsafe class StbFont : IDisposable
{
IntPtr _buffer;
stbtt_fontinfo* _info;
List<stbtt_kerningentry> _kerningTable;
public IntPtr FontBuffer => _buffer;
public ref stbtt_fontinfo FontInfo => ref *_info;
public IReadOnlyList<stbtt_kerningentry> KerningTable
{
get
{
if (_kerningTable != null)
return _kerningTable;
int count = Stbtt.GetKerningTableLength(_info);
if (count == 0)
{
return _kerningTable = new List<stbtt_kerningentry>();
}
else
{
stbtt_kerningentry[] array = new stbtt_kerningentry[count];
fixed (stbtt_kerningentry *ptr = array)
Stbtt.GetKerningTable(_info, ptr, count);
return _kerningTable = new List<stbtt_kerningentry>(array);
}
}
}
public int Ascend { get; }
public int Descend { get; }
public int VerticalLineGap { get; }
public int AscendOS2 { get; }
public int DescendOS2 { get; }
public int VerticalLineGapOS2 { get; }
public Box BoundingBox { get; }
private StbFont(IntPtr buffer, stbtt_fontinfo* info)
{
_buffer = buffer;
_info = info;
int a, b, c, d;
Stbtt.GetFontVMetrics(_info, &a, &b, &c);
Ascend = a;
Descend = b;
VerticalLineGap = c;
Stbtt.GetFontVMetricsOS2(_info, &a, &b, &c);
AscendOS2 = a;
DescendOS2 = b;
VerticalLineGapOS2 = c;
Stbtt.GetFontBoundingBox(_info, &a, &b, &c, &d);
BoundingBox = new Box(a, b, c, d);
}
~StbFont()
{
Dispose(false);
}
public int FindGlyphIndex(int codepoint)
{
return Stbtt.FindGlyphIndex(_info, codepoint);
}
public int FindGlyphIndex(Rune codepoint) => FindGlyphIndex(codepoint.Value);
public float ScaleForPixelHeight(float pixels)
{
return Stbtt.ScaleForPixelHeight(_info, pixels);
}
public float ScaleForMappingEmToPixels(float pixels)
{
return Stbtt.ScaleForMappingEmToPixels(_info, pixels);
}
public void GetCodepointHMetrics(int codepoint, out int advance, out int bearing)
{
int a, b;
Stbtt.GetCodepointHMetrics(_info, codepoint, &a, &b);
advance = a;
bearing = b;
}
public void GetCodepointHMetrics(Rune codepoint, out int advance, out int bearing)
=> GetCodepointHMetrics(codepoint.Value, out advance, out bearing);
public int GetCodepointKernAdvance(int cp1, int cp2)
{
return Stbtt.GetCodepointKernAdvance(_info, cp1, cp2);
}
public int GetCodepointKernAdvance(Rune cp1, Rune cp2) => GetCodepointKernAdvance(cp1.Value, cp2.Value);
public int GetCodepointBox(int codepoint, out Box box)
{
int x0, y0;
int x1, y1;
int rval;
rval = Stbtt.GetCodepointBox(_info, codepoint, &x0, &y0, &x1, &y1);
box = new Box(x0, y0, x1, y1);
return rval;
}
public void GetGlyphHMetrics(int glyph, out int advance, out int bearing)
{
int a, b;
Stbtt.GetGlyphHMetrics(_info, glyph, &a, &b);
advance = a;
bearing = b;
}
public int GetGlyphKernAdvance(int gl1, int gl2)
{
return Stbtt.GetGlyphKernAdvance(_info, gl1, gl2);
}
public int GetGlyphBox(int glyph, out Box box)
{
int x0, y0;
int x1, y1;
int rval;
rval = Stbtt.GetGlyphBox(_info, glyph, &x0, &y0, &x1, &y1);
box = new Box(x0, y0, x1, y1);
return rval;
}
public bool IsGlyphEmpty(int glyph)
{
return Stbtt.IsGlyphEmpty(_info, glyph) != 0;
}
public Bitmap GetCodepointBitmap(float scaleX, float scaleY, int codepoint, out int offsetX, out int offsetY)
{
int w, h, x, y;
void* ptr = Stbtt.GetCodepointBitmap(_info, scaleX, scaleY, codepoint, &w, &h, &x, &y);
offsetX = x;
offsetY = y;
return new Bitmap((IntPtr)ptr, w, h, FreeBitmap);
}
public Bitmap GetCodepointBitmap(float scaleX, float scaleY, Rune codepoint, out int offsetX, out int offsetY)
=> GetCodepointBitmap(scaleX, scaleY, codepoint.Value, out offsetX, out offsetY);
public Bitmap GetCodepointBitmapSubpixel(float scaleX, float scaleY, float shiftX, float shiftY, int codepoint, out int offsetX, out int offsetY)
{
int w, h, x, y;
void* ptr = Stbtt.GetCodepointBitmapSubpixel(_info, scaleX, scaleY, shiftX, shiftY, codepoint, &w, &h, &x, &y);
offsetX = x;
offsetY = y;
return new Bitmap((IntPtr)ptr, w, h, FreeBitmap);
}
public Bitmap GetCodepointBitmapSubpixel(float scaleX, float scaleY, float shiftX, float shiftY, Rune codepoint, out int offsetX, out int offsetY)
=> GetCodepointBitmapSubpixel(scaleX, scaleY, shiftX, shiftY, codepoint.Value, out offsetX, out offsetY);
public Bitmap GetGlyphBitmap(float scaleX, float scaleY, int glyph, out int offsetX, out int offsetY)
{
int w, h, x, y;
void* ptr = Stbtt.GetGlyphBitmap(_info, scaleX, scaleY, glyph, &w, &h, &x, &y);
offsetX = x;
offsetY = y;
return new Bitmap((IntPtr)ptr, w, h, FreeBitmap);
}
public Bitmap GetGlyphBitmapSubpixel(float scaleX, float scaleY, float shiftX, float shiftY, int glyph, out int offsetX, out int offsetY)
{
int w, h, x, y;
void* ptr = Stbtt.GetGlyphBitmapSubpixel(_info, scaleX, scaleY, shiftX, shiftY, glyph, &w, &h, &x, &y);
offsetX = x;
offsetY = y;
return new Bitmap((IntPtr)ptr, w, h, FreeBitmap);
}
public Bitmap GetGlyphSdf(float scale, int glyph, int padding, byte edgeValue, float pixelDistScale, out int offsetX, out int offsetY)
{
int w, h, x, y;
void *ptr = Stbtt.GetGlyphSDF(_info, scale, glyph, padding, edgeValue, pixelDistScale, &w, &h, &x, &y);
offsetX = x;
offsetY = y;
return new Bitmap((IntPtr)ptr, w, h, FreeSdf);
}
public Bitmap GetCodepointSdf(float scale, int codepoint, int padding, byte edgeValue, float pixelDistScale, out int offsetX, out int offsetY)
{
int w, h, x, y;
void *ptr = Stbtt.GetCodepointSDF(_info, scale, codepoint, padding, edgeValue, pixelDistScale, &w, &h, &x, &y);
offsetX = x;
offsetY = y;
return new Bitmap((IntPtr)ptr, w, h, FreeSdf);
}
public Bitmap GetCodepointSdf(float scale, Rune codepoint, int padding, byte edgeValue, float pixelDistScale, out int offsetX, out int offsetY)
=> GetCodepointSdf(scale, codepoint.Value, padding, edgeValue, pixelDistScale, out offsetX, out offsetY);
public void Dispose()
{
Dispose(true);
}
bool isDisposed = false;
private void Dispose(bool disposing)
{
if (isDisposed) return;
if (disposing)
{
GC.SuppressFinalize(this);
}
Marshal.FreeHGlobal(_buffer);
Marshal.FreeHGlobal((IntPtr)_info);
isDisposed = true;
}
public static bool TryLoad(Stream stream, out StbFont font)
{
byte* buffer = (byte*)Marshal.AllocHGlobal((int)stream.Length);
stbtt_fontinfo* fontInfo = (stbtt_fontinfo*)Marshal.AllocHGlobal(sizeof(stbtt_fontinfo));
stream.Read(new Span<byte>(buffer, (int)stream.Length));
int nfont = Stbtt.GetNumberOfFonts(buffer);
if (nfont == 0)
{
font = null;
return false;
}
int offset = Stbtt.GetFontOffsetForIndex(buffer, 0);
if (Stbtt.InitFont(fontInfo, (byte*)buffer, offset) == 0)
{
Marshal.FreeHGlobal((IntPtr)buffer);
Marshal.FreeHGlobal((IntPtr)fontInfo);
font = null;
return false;
}
font = new StbFont((IntPtr)buffer, fontInfo);
return true;
}
public static StbFont Load(Stream stream)
{
if (TryLoad(stream, out StbFont font))
{
return font;
}
throw new Exception("Could not load the font.");
}
private static void FreeBitmap(IntPtr buffer)
{
Stbtt.FreeBitmap((byte*)buffer, null);
}
private static void FreeSdf(IntPtr buffer)
{
Stbtt.FreeSDF((byte*)buffer, null);
}
public struct Box
{
public int X0;
public int Y0;
public int X1;
public int Y1;
public Box(int x0, int y0, int x1, int y1)
{
X0 = x0; Y0 = y0;
X1 = x1; Y1 = y1;
}
}
public class Bitmap : IDisposable
{
public IntPtr Buffer { get; }
public int Width { get; }
public int Height { get; }
private readonly Action<IntPtr> Destroy;
public Bitmap(IntPtr buffer, int width, int height, Action<IntPtr> destroy)
{
Buffer = buffer;
Width = width;
Height = height;
Destroy = destroy;
}
~Bitmap()
{
Dispose(false);
}
public void Dispose() => Dispose(true);
private bool isDiposed = false;
public void Dispose(bool disposing)
{
if (isDiposed) return;
if (disposing)
{
GC.SuppressFinalize(this);
}
Destroy(Buffer);
isDiposed = true;
}
}
}
}

@ -0,0 +1,63 @@
using System;
using System.Runtime.InteropServices;
using System.Reflection;
namespace Quik.Stb
{
public unsafe static partial class Stbtt
{
private delegate void FailedAssertProc(byte *expression, byte *file, int line, byte *function);
private static readonly string[] LibraryNames = new string[]
{
//FIXME: This is wrong on so many levels, but, i need to do this
// in order to get a change of this running.
"runtimes/win-x64/native/libstbtt.dll",
"runtimes/win-x86/native/libstbtt.dll",
"runtimes/linux-arm/native/libstbtt.so",
"runtimes/linux-arm64/native/libstbtt.so",
"runtimes/linux-x64/native/libstbtt.so",
"runtimes/native/libstbtt.dylib",
"libstbtt.dll",
"libstbtt.so",
"libstbtt.dylib",
};
static Stbtt()
{
NativeLibrary.SetDllImportResolver(Assembly.GetExecutingAssembly(), Resolver);
quik_stbtt_failed_assert_store(Marshal.GetFunctionPointerForDelegate<FailedAssertProc>(FailedAssert));
}
private static IntPtr Resolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
{
if (libraryName != "stbtt")
return IntPtr.Zero;
foreach (string name in LibraryNames)
{
if (NativeLibrary.TryLoad(name, assembly, searchPath, out IntPtr handle))
{
return handle;
}
}
return NativeLibrary.Load(libraryName);
}
private static void FailedAssert(byte *expression, byte *file, int line, byte *function)
{
string expr = expression == null ? string.Empty : Marshal.PtrToStringUTF8((IntPtr)expression);
string f = file == null ? string.Empty : Marshal.PtrToStringUTF8((IntPtr)file);
string func = function == null ? string.Empty : Marshal.PtrToStringUTF8((IntPtr)function);
Exception ex = new Exception("Assert failed in native stbtt code.");
ex.Data.Add("Expression", expr);
ex.Data.Add("File", f);
ex.Data.Add("Line", line);
ex.Data.Add("Function", func);
throw ex;
}
}
}

@ -9,12 +9,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Quik.StbImage", "Quik.StbIm
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Quik.StbTrueType", "Quik.StbTrueType\Quik.StbTrueType.csproj", "{AD5B02B0-F957-4816-8CE9-5F278B856C70}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{5E87AF9C-AC12-4E48-99B1-CBEC0C97B624}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Quik.StbImage.Tests", "tests\Quik.StbImage.Tests\Quik.StbImage.Tests.csproj", "{AFF181CF-D51E-4E16-B3C6-38ED1E1FF615}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Quik.Media.Stb", "Quik.Media.Stb\Quik.Media.Stb.csproj", "{3D354BE0-42A7-45C4-AAEA-B0F8963A5745}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{AE05ADE5-A809-479F-97D5-BEAFE7604285}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Quik.Stb.Tests", "tests\Quik.Stb.Tests\Quik.Stb.Tests.csproj", "{BC7D3002-B79B-4141-B6CC-74FB2175B474}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -73,18 +73,6 @@ Global
{AD5B02B0-F957-4816-8CE9-5F278B856C70}.Release|x64.Build.0 = Release|Any CPU
{AD5B02B0-F957-4816-8CE9-5F278B856C70}.Release|x86.ActiveCfg = Release|Any CPU
{AD5B02B0-F957-4816-8CE9-5F278B856C70}.Release|x86.Build.0 = Release|Any CPU
{AFF181CF-D51E-4E16-B3C6-38ED1E1FF615}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AFF181CF-D51E-4E16-B3C6-38ED1E1FF615}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AFF181CF-D51E-4E16-B3C6-38ED1E1FF615}.Debug|x64.ActiveCfg = Debug|Any CPU
{AFF181CF-D51E-4E16-B3C6-38ED1E1FF615}.Debug|x64.Build.0 = Debug|Any CPU
{AFF181CF-D51E-4E16-B3C6-38ED1E1FF615}.Debug|x86.ActiveCfg = Debug|Any CPU
{AFF181CF-D51E-4E16-B3C6-38ED1E1FF615}.Debug|x86.Build.0 = Debug|Any CPU
{AFF181CF-D51E-4E16-B3C6-38ED1E1FF615}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AFF181CF-D51E-4E16-B3C6-38ED1E1FF615}.Release|Any CPU.Build.0 = Release|Any CPU
{AFF181CF-D51E-4E16-B3C6-38ED1E1FF615}.Release|x64.ActiveCfg = Release|Any CPU
{AFF181CF-D51E-4E16-B3C6-38ED1E1FF615}.Release|x64.Build.0 = Release|Any CPU
{AFF181CF-D51E-4E16-B3C6-38ED1E1FF615}.Release|x86.ActiveCfg = Release|Any CPU
{AFF181CF-D51E-4E16-B3C6-38ED1E1FF615}.Release|x86.Build.0 = Release|Any CPU
{3D354BE0-42A7-45C4-AAEA-B0F8963A5745}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3D354BE0-42A7-45C4-AAEA-B0F8963A5745}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3D354BE0-42A7-45C4-AAEA-B0F8963A5745}.Debug|x64.ActiveCfg = Debug|Any CPU
@ -97,8 +85,20 @@ Global
{3D354BE0-42A7-45C4-AAEA-B0F8963A5745}.Release|x64.Build.0 = Release|Any CPU
{3D354BE0-42A7-45C4-AAEA-B0F8963A5745}.Release|x86.ActiveCfg = Release|Any CPU
{3D354BE0-42A7-45C4-AAEA-B0F8963A5745}.Release|x86.Build.0 = Release|Any CPU
{BC7D3002-B79B-4141-B6CC-74FB2175B474}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BC7D3002-B79B-4141-B6CC-74FB2175B474}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BC7D3002-B79B-4141-B6CC-74FB2175B474}.Debug|x64.ActiveCfg = Debug|Any CPU
{BC7D3002-B79B-4141-B6CC-74FB2175B474}.Debug|x64.Build.0 = Debug|Any CPU
{BC7D3002-B79B-4141-B6CC-74FB2175B474}.Debug|x86.ActiveCfg = Debug|Any CPU
{BC7D3002-B79B-4141-B6CC-74FB2175B474}.Debug|x86.Build.0 = Debug|Any CPU
{BC7D3002-B79B-4141-B6CC-74FB2175B474}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BC7D3002-B79B-4141-B6CC-74FB2175B474}.Release|Any CPU.Build.0 = Release|Any CPU
{BC7D3002-B79B-4141-B6CC-74FB2175B474}.Release|x64.ActiveCfg = Release|Any CPU
{BC7D3002-B79B-4141-B6CC-74FB2175B474}.Release|x64.Build.0 = Release|Any CPU
{BC7D3002-B79B-4141-B6CC-74FB2175B474}.Release|x86.ActiveCfg = Release|Any CPU
{BC7D3002-B79B-4141-B6CC-74FB2175B474}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{AFF181CF-D51E-4E16-B3C6-38ED1E1FF615} = {5E87AF9C-AC12-4E48-99B1-CBEC0C97B624}
{BC7D3002-B79B-4141-B6CC-74FB2175B474} = {AE05ADE5-A809-479F-97D5-BEAFE7604285}
EndGlobalSection
EndGlobal

@ -0,0 +1,72 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.IO;
using System.Runtime.InteropServices;
using Quik.Stb;
namespace Quik.Stb
{
[TestClass]
[TestCategory("Load Font")]
public class LoadFont
{
StbFont? font;
[TestInitialize]
public void Initialize()
{
using (Stream? str = GetType().Assembly.GetManifestResourceStream("Quik.Stb.Tests.res.Varicka.ttf"))
{
Assert.IsNotNull(str, "Test font file not packed.");
font = StbFont.Load(str);
}
}
[TestCleanup]
public void Deinitialize()
{
font?.Dispose();
}
[TestMethod]
public void AscendIsValid()
{
Assert.AreNotEqual(-1, font!.Ascend);
}
[TestMethod]
public void DescendIsValid()
{
Assert.AreNotEqual(-1, font!.Descend);
}
[TestMethod]
public void VLineGapIsValid()
{
Assert.AreNotEqual(-1, font!.VerticalLineGap);
}
[TestMethod]
public void BBoxIsValid()
{
Assert.AreNotEqual(default, font!.BoundingBox);
}
[TestMethod]
public void KerningTableIsValid()
{
Assert.IsNotNull(font!.KerningTable);
}
[TestMethod]
public void GetGlyphsForAscii()
{
for (int i = 0; i < 128; i++)
{
int glyph = font!.FindGlyphIndex(i);
Assert.AreNotEqual(-1, glyph);
}
}
}
}

@ -1,11 +1,7 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.IO;
using System.Runtime.InteropServices;
using Quik.Stb;
using Image = Quik.Stb.StbImage;
namespace Quik.StbImage.Tests
namespace Quik.Stb.Tests
{
[TestClass]
[TestCategory("Load Image")]
@ -27,7 +23,7 @@ namespace Quik.StbImage.Tests
private unsafe void TestImage(string path, int width, int height)
{
Image image = Image.Load(GetImage(path));
StbImage image = StbImage.Load(GetImage(path));
Assert.IsNotNull(image);
@ -41,9 +37,9 @@ namespace Quik.StbImage.Tests
const int HEIGHT = 512;
[TestMethod("Load a single frame GIF")]
public unsafe void LoadGif() => TestImage("Quik.StbImage.Tests.res.kodim.kodim23.gif", WIDTH, HEIGHT);
public unsafe void LoadGif() => TestImage("Quik.Stb.Tests.res.kodim.kodim23.gif", WIDTH, HEIGHT);
[TestMethod("Load a JPEG")]
public unsafe void LoadJpg() => TestImage("Quik.StbImage.Tests.res.kodim.kodim23.jpg", WIDTH, HEIGHT);
[TestMethod("Load a PNG")] public unsafe void LoadPng() => TestImage("Quik.StbImage.Tests.res.kodim.kodim23.png", WIDTH, HEIGHT);
public unsafe void LoadJpg() => TestImage("Quik.Stb.Tests.res.kodim.kodim23.jpg", WIDTH, HEIGHT);
[TestMethod("Load a PNG")] public unsafe void LoadPng() => TestImage("Quik.Stb.Tests.res.kodim.kodim23.png", WIDTH, HEIGHT);
}
}

@ -23,6 +23,7 @@
<ItemGroup>
<ProjectReference Include="..\..\Quik.StbImage\Quik.StbImage.csproj" />
<ProjectReference Include="..\..\Quik.StbTrueType\Quik.StbTrueType.csproj" />
</ItemGroup>
</Project>

Binary file not shown.

@ -0,0 +1,12 @@
Varicka (Truetype and Opentype with no OT features)
Based on 'Varick', from "Decorative Condensed Alphabets", by Dan Solo, Page 94.
Letter spacing is set to balance the letterforms themselves; the space between most adjacent letters is identical to the horizontal space inside such letters as 'O' and 'H'. Kerning is supplied as needed for certain letter combinations, particularly those that have a letter with a projection on the left, such as A, E, F, H, K, P, and R.
Varicka is superficially similar to Red Rooster's Triple Gothic Condensed, but the Solo book's font has different features and some very different letterforms.
This font is free and available for all use, personal and commercial, with no restrictions.
Character
February 13, 2010

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 156 KiB

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Before

Width:  |  Height:  |  Size: 544 KiB

After

Width:  |  Height:  |  Size: 544 KiB

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB