Preliminary typesetting work.

This commit is contained in:
H. Utku Maden 2024-05-15 23:17:01 +03:00
parent 279e619c3b
commit bd69c0d93f
18 changed files with 597 additions and 167 deletions

@ -1,8 +1,9 @@
using System;
using System.Buffers;
using System.IO;
using Quik.FreeType;
using Quik.Media;
using Quik.Media.Color;
using Quik.Media.Font;
namespace Quik.Media.Defaults
{
@ -11,7 +12,7 @@ namespace Quik.Media.Defaults
private MemoryStream ms;
private FTFace face;
public override FontInfo Info => throw new NotImplementedException();
public override FontFace Face => throw new NotImplementedException();
public QFontFreeType(Stream stream)
{
@ -30,65 +31,39 @@ namespace Quik.Media.Defaults
return FT.GetCharIndex(face, (ulong)rune) != 0;
}
public override QFontPage RasterizePage(int codepage, float size, in FontRasterizerOptions options)
protected override QImage Render(out QGlyphMetrics metrics, int codepoint, 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)codepoint);
FT.LoadGlyph(face, index, FTLoadFlags.Default);
ref readonly FTGlyphMetrics ftmetrics = ref face.Glyph.Metrics;
metrics = new QGlyphMetrics(codepoint,
new QVec2(ftmetrics.Width/64f, ftmetrics.Height/64f),
new QVec2(ftmetrics.HorizontalBearingX/64f, ftmetrics.HorizontalBearingY/64f),
new QVec2(ftmetrics.VerticalBearingX/64f, ftmetrics.VerticalBearingY/64f),
new QVec2(ftmetrics.HorizontalAdvance/64f, ftmetrics.VerticalAdvance/64f)
);
FT.RenderGlyph(face.Glyph, options.Sdf ? FTRenderMode.Sdf : FTRenderMode.Normal);
ref readonly FTBitmap bitmap = ref face.Glyph.Bitmap;
if (bitmap.Width == 0 || bitmap.Pitch == 0 || bitmap.Buffer == IntPtr.Zero)
{
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));
return null;
}
int bits = Math.ILogB(pixels);
if (1 << bits != pixels)
QImageBuffer image = new QImageBuffer(QImageFormat.RedU8, (int)bitmap.Width, (int)bitmap.Rows);
image.LockBits2d(out QImageLock lk, QImageLockOptions.Default);
unsafe
{
pixels = 1 << bits + 1;
Buffer.MemoryCopy((void*)bitmap.Buffer, (void*)lk.ImagePtr, lk.Width * lk.Height, bitmap.Width * bitmap.Rows);
}
// 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);
image.UnlockBits();
return image;
}
protected override void Dispose(bool disposing)

@ -4,14 +4,14 @@ using System.Collections;
using System.IO;
using System.Linq;
using System.Net;
using Quik.Media;
using Quik.Media.Font;
// WebRequest is obsolete but runs on .NET framework.
#pragma warning disable SYSLIB0014
namespace Quik.Media.Defaults
{
public class StbMediaLoader : MediaLoader<string>, MediaLoader<Uri>, MediaLoader<FileInfo>, MediaLoader<FontInfo>
public class StbMediaLoader : MediaLoader<string>, MediaLoader<Uri>, MediaLoader<FileInfo>, MediaLoader<FontFace>
{
public bool AllowRemoteTransfers { get; set; } = false;
private readonly ArrayPool<byte> ByteArrays = ArrayPool<byte>.Create();
@ -31,9 +31,9 @@ namespace Quik.Media.Defaults
{
return GetMedia((FileInfo)key, hint);
}
else if (t == typeof(FontInfo))
else if (t == typeof(FontFace))
{
return GetMedia((FontInfo)key, hint);
return GetMedia((FontFace)key, hint);
}
else
{
@ -56,7 +56,7 @@ namespace Quik.Media.Defaults
throw new NotImplementedException();
}
public IDisposable GetMedia(FontInfo key, MediaHint hint)
public IDisposable GetMedia(FontFace key, MediaHint hint)
{
throw new NotImplementedException();
}

@ -0,0 +1,151 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
#if false
namespace Quik.Media.Defaults.Win32
{
public class EnumerateFonts
{
private const byte DEFAULT_CHARSET = 1;
public static void Enumerate(FontFace font)
{
/* It's windows, just borrow the desktop window. */
IntPtr hdc = GetDC(GetDesktopWindow());
List<(LogFontA, TextMetricA)> list = new List<(LogFontA, TextMetricA)>();
LogFontA font2 = new LogFontA()
{
//FaceName = font.Family,
Weight = ((font.Style & FontStyle.Bold) != 0) ? FontWeight.Bold : FontWeight.Regular,
Italic = (font.Style & FontStyle.Italic) != 0,
CharSet = DEFAULT_CHARSET
};
Console.WriteLine(font2.FaceName);
EnumFontFamiliesExProc proc = (in LogFontA font, in TextMetricA metric, int type, IntPtr lparam) =>
{
list.Add((font, metric));
return 0;
};
EnumFontFamiliesExA(hdc, font2, proc, IntPtr.Zero, 0);
}
private const string gdi32 = "Gdi32.dll";
private const string user32 = "User32.dll";
[DllImport(gdi32)]
private static extern int EnumFontFamiliesExA(
IntPtr hdc,
in LogFontA font,
[MarshalAs(UnmanagedType.FunctionPtr)] EnumFontFamiliesExProc proc,
IntPtr lparam,
int flags /* Should be zero. */);
[DllImport(user32)]
private static extern IntPtr /* HWND */ GetDesktopWindow();
[DllImport(user32)]
private static extern IntPtr /* HDC */ GetDC(IntPtr hwnd);
private delegate int EnumFontFamiliesExProc(in LogFontA font, in TextMetricA metric, int fontType, IntPtr lParam);
private struct LogFontA
{
public long Height;
public long Width;
public long Escapement;
public long Orientation;
public FontWeight Weight;
[MarshalAs(UnmanagedType.U1)]
public bool Italic;
[MarshalAs(UnmanagedType.U1)]
public bool Underline;
[MarshalAs(UnmanagedType.U1)]
public bool StrikeOut;
public byte CharSet;
public byte OutPrecision;
public byte ClipPrecision;
public byte PitchAndFamily;
private unsafe fixed byte aFaceName[32];
public unsafe string FaceName
{
get
{
fixed (byte* str = aFaceName)
{
int len = 0;
for (; str[len] != 0 && len < 32; len++) ;
return Encoding.UTF8.GetString(str, len);
}
}
set
{
fixed (byte *str = aFaceName)
{
Span<byte> span = new Span<byte>(str, 32);
Encoding.UTF8.GetBytes(value, span);
span[31] = 0;
}
}
}
}
private struct TextMetricA
{
public long Height;
public long Ascent;
public long Descent;
public long InternalLeading;
public long ExternalLeading;
public long AveCharWidth;
public long MaxCharWidth;
public long Weight;
public long Overhang;
public long DigitizedAspectX;
public long DigitizedAspectY;
public byte FirstChar;
public byte LastChar;
public byte DefaultChar;
public byte BreakChar;
public byte Italic;
public byte Underlined;
public byte StruckOut;
public byte PitchAndFamily;
public byte CharSet;
}
private enum FontWeight : long
{
DontCare = 0,
Thin = 100,
ExtraLight = 200,
UltraLight = 200,
Light = 300,
Normal = 400,
Regular = 400,
Medium = 500,
Semibold = 600,
Demibold = 600,
Bold = 700,
Extrabold = 800,
Ultrabold = 800,
Heavy = 900,
Black = 900
}
}
}
#endif

@ -87,5 +87,18 @@ namespace Quik.OpenTK
public void PortShow(IQuikPortHandle port, bool shown = true) => ((OpenTKPort)port).Show(shown);
public void PortPaint(IQuikPortHandle port, CommandList commands) => ((OpenTKPort)port).Paint(commands);
public void GetMaximumImage(out int width, out int height)
{
GL.Get(GLEnum.GL_MAX_TEXTURE_SIZE, out int value);
width = height = value;
}
public void GetMaximumImage(out int width, out int height, out int depth)
{
GetMaximumImage(out width, out height);
GL.Get(GLEnum.GL_MAX_ARRAY_TEXTURE_LAYERS, out int value);
depth = value;
}
}
}

@ -78,6 +78,7 @@ namespace Quik.OpenTK
if (!_glDriver.IsInit)
_glDriver.Init();
GL.Clear(GLEnum.GL_COLOR_BUFFER_BIT);
_glDriver.Draw(_vertexEngine.DrawQueue, view);
_window.Context.SwapBuffers();

@ -1,17 +1,19 @@
using System;
using System.Runtime.InteropServices;
namespace Quik.Media
namespace Quik.Media.Color
{
public class QImageBuffer : QImage
{
private byte[] buffer;
GCHandle handle;
private bool isSdf = false;
public override QImageFormat InternalFormat { get; }
public override int Width { get; }
public override int Height { get; }
public override int Depth { get; }
public override bool IsSdf => isSdf;
public QImageBuffer(QImageFormat format, int width, int height, int depth = 1)
{
@ -20,7 +22,7 @@ namespace Quik.Media
Height = height;
Depth = depth;
buffer = new byte[width * height * depth * format.BytesPerPixel()];
buffer = new byte[width * height * depth];
}
~QImageBuffer()
{
@ -29,7 +31,7 @@ namespace Quik.Media
private QImageLock Lock()
{
handle.Free();
if (handle.IsAllocated) handle.Free();
handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
IntPtr ptr = Marshal.UnsafeAddrOfPinnedArrayElement(buffer, 0);
return new QImageLock(InternalFormat, Width, Height, Depth, ptr);
@ -37,56 +39,32 @@ namespace Quik.Media
protected override void Dispose(bool disposing)
{
if (handle.IsAllocated) handle.Free();
buffer = null;
if (handle.IsAllocated) handle.Free();
GC.SuppressFinalize(this);
}
public override void LockBits2d(out QImageLock imageLock, QImageLockOptions options)
{
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);
imageLock = Lock();
}
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);
imageLock = Lock();
}
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()));
imageLock = Lock();
}
public override void UnlockBits()
{
if (handle.IsAllocated)
handle.Free();
handle.Free();
}
public void SetSdf(bool value = true) => isSdf = value;
}
}

@ -1,5 +1,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using Quik.Media.Color;
namespace Quik.Media.Font
{
@ -17,11 +21,28 @@ namespace Quik.Media.Font
private readonly Dictionary<int, FontAtlasGlyphInfo> glyphs = new Dictionary<int, FontAtlasGlyphInfo>();
private int index = 0;
private AtlasPage last = null;
private bool isSdf = false;
private int expansion;
public FontAtlas(int width, int height)
public bool IsSdf
{
get => isSdf;
set
{
foreach (AtlasPage page in atlases)
{
(page.Image as QImageBuffer).SetSdf(value);
}
isSdf = value;
}
}
public FontAtlas(int width, int height, bool isSdf, int expansion = 4)
{
this.width = width;
this.height = height;
IsSdf = isSdf;
this.expansion = expansion;
}
public bool GetGlyph(int codepoint, out FontAtlasGlyphInfo info)
@ -33,7 +54,7 @@ namespace Quik.Media.Font
{
info = new FontAtlasGlyphInfo() { Codepoint = codepoint };
if (last == null || last.IsFull)
if (last == null || !last.WouldFit(source))
{
AddPage();
}
@ -51,7 +72,8 @@ namespace Quik.Media.Font
}
else
{
last = new AtlasPage(width, height);
last = new AtlasPage(width, height, expansion);
(last.Image as QImageBuffer).SetSdf(IsSdf);
atlases.Add(last);
}
}
@ -92,12 +114,14 @@ namespace Quik.Media.Font
public QImage Image;
public int PointerX, PointerY;
public int RowHeight;
public int Expansion;
public bool IsFull => PointerX > Image.Width || PointerY > Image.Height;
public AtlasPage(int width, int height)
public AtlasPage(int width, int height, int expansion)
{
Image = new QImageBuffer(QImageFormat.RedU8, width, height);
Expansion = expansion;
Reset();
}
@ -110,8 +134,8 @@ namespace Quik.Media.Font
src.CopyTo(dst, PointerX, PointerY);
Image.UnlockBits();
QVec2 min = new QVec2(PointerX/Image.Width, PointerY/Image.Width);
QVec2 size = new QVec2(src.Width/Image.Width, src.Height/Image.Height);
QVec2 min = new QVec2((float)PointerX/Image.Width, (float)PointerY/Image.Height);
QVec2 size = new QVec2((float)src.Width/Image.Width, (float)src.Height/Image.Height);
prototype.Image = Image;
prototype.UVs = new QRectangle(min + size, min);
@ -127,7 +151,7 @@ namespace Quik.Media.Font
public void AdvanceRow()
{
PointerX = 0;
PointerY += RowHeight;
PointerY += RowHeight + Expansion;
RowHeight = 0;
}
@ -135,7 +159,7 @@ namespace Quik.Media.Font
public void AdvanceColumn(int width, int height)
{
RowHeight = Math.Max(RowHeight, height);
PointerX += width;
PointerX += width + Expansion;
if (PointerX > Image.Width)
{
@ -153,6 +177,11 @@ namespace Quik.Media.Font
isDisposed = true;
}
internal bool WouldFit(QImageLock source)
{
return !IsFull || PointerX + source.Width > Image.Width || PointerY + source.Height > Image.Height;
}
}
}
}

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Quik.Media;
using Quik.Media.Font;
@ -17,8 +18,23 @@ namespace Quik.Media
public FontStretch Stretch => Face.Stretch;
public abstract bool HasRune(int rune);
public abstract QFontPage RasterizePage(int codepage, float size, in FontRasterizerOptions options);
public QFontPage RasterizePage(int codepage, float size) => RasterizePage(codepage, size, FontRasterizerOptions.Default);
protected abstract QImage Render(out QGlyphMetrics metrics, int codepoint, float size, in FontRasterizerOptions options);
private readonly Dictionary<float, SizedFontCollection> _atlasses = new Dictionary<float, SizedFontCollection>();
public void Get(int codepoint, float size, out FontGlyph glyph)
{
SizedFontCollection collection;
if (!_atlasses.TryGetValue(size, out collection))
{
collection = new SizedFontCollection(size);
_atlasses.Add(size, collection);
}
collection.Get(codepoint, out glyph, this);
}
// IDisposable
private bool isDisposed = false;
@ -32,6 +48,71 @@ namespace Quik.Media
}
protected virtual void Dispose(bool disposing) { }
public void Dispose() => DisposePrivate(true);
private class SizedFontCollection
{
public float Size { get; }
private readonly Dictionary<int, FontGlyph> glyphs = new Dictionary<int, FontGlyph>();
private readonly FontAtlas atlas;
public SizedFontCollection(float size)
{
Size = size;
QuikApplication.Current.Platform.GetMaximumImage(out int height, out int width);
// Do no allow to create a texture that is greater than 16 square characters at 200 DPI.
width = Math.Min(width, (int)(size * 200 * 16));
height = Math.Min(height, (int)(size * 200 * 16));
// width = height = 256;
atlas = new FontAtlas(width, height, QuikApplication.Current.FontProvider.RasterizerOptions.Sdf);
}
public void Get(int codepoint, out FontGlyph glyph, QFont font)
{
if (glyphs.TryGetValue(codepoint, out glyph))
return;
QImage image = font.Render(
out QGlyphMetrics metrics,
codepoint,
Size,
QuikApplication.Current.FontProvider.RasterizerOptions);
if (image != null)
{
image.LockBits2d(out QImageLock l, QImageLockOptions.Default);
atlas.PutGlyph(codepoint, l, out FontAtlasGlyphInfo glyphInfo);
image.UnlockBits();
image.Dispose();
glyph = new FontGlyph(codepoint, glyphInfo.Image, metrics, glyphInfo.UVs);
}
else
{
glyph = new FontGlyph(codepoint, null, metrics, default);
}
glyphs[codepoint] = glyph;
}
}
}
public readonly struct FontGlyph
{
public readonly int CodePoint;
public readonly QImage Image;
public readonly QGlyphMetrics Metrics;
public readonly QRectangle UVs;
public FontGlyph(int codepoint, QImage image, in QGlyphMetrics metrics, in QRectangle uvs)
{
CodePoint = codepoint;
Image = image;
Metrics = metrics;
UVs = uvs;
}
}
public struct FontRasterizerOptions
@ -42,53 +123,7 @@ namespace Quik.Media
public static readonly FontRasterizerOptions Default = new FontRasterizerOptions()
{
Resolution = 96.0f,
Sdf = true
Sdf = false
};
}
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();
}
}
}
}

@ -78,7 +78,7 @@ namespace Quik.Media
int bpp = Format.BytesPerPixel();
for (int i = 0; i < Height; i++)
{
IntPtr srcPtr = (IntPtr)((long)ImagePtr + i * bpp);
IntPtr srcPtr = (IntPtr)((long)ImagePtr + i * Width * bpp);
long dstPos = x + i * destination.Width;
IntPtr dstPtr = (IntPtr)((long)destination.ImagePtr + dstPos * bpp);

@ -5,6 +5,7 @@ using Quik.VertexGenerator;
using static Quik.OpenGL.GLEnum;
using Quik.Media;
using System.Linq;
using System.Diagnostics;
namespace Quik.OpenGL
{
@ -105,7 +106,7 @@ namespace Quik.OpenGL
GL.UseProgram(program);
GL.Uniform1(fMaxZ, (float)(queue.ZDepth+1));
GL.UniformMatrix4(m4Transforms, false, in matrix);
GL.Uniform1(fSdfThreshold, 0.0f);
GL.Uniform1(fSdfThreshold, 0.5f);
GL.Uniform1(tx2d, 0);
GL.Enable(GL_BLEND);
GL.BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

@ -11,6 +11,9 @@ namespace Quik.OpenGL
GL_RENDERER = 0x1F01,
GL_VERSION = 0x1F02,
GL_EXTENSIONS = 0x1F03,
GL_MAX_TEXTURE_SIZE = 0x0D33,
GL_MAX_3D_TEXTURE_SIZE = 0x8073,
GL_MAX_ARRAY_TEXTURE_LAYERS = 0x88FF,
GL_MULTISAMPLE = 0x809D,
GL_BLEND = 0x0BE2,

@ -55,5 +55,7 @@ namespace Quik.PAL
void PortFocus(IQuikPortHandle port);
void PortShow(IQuikPortHandle port, bool shown = true);
void PortPaint(IQuikPortHandle port, CommandList commands);
void GetMaximumImage(out int width, out int height);
void GetMaximumImage(out int width, out int height, out int depth);
}
}

@ -0,0 +1,71 @@
using Quik.Media;
using Quik.Media.Font;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Quik.Typography
{
/// <summary>
/// The font provider is a caching object that provides fonts for typesetting classes.
/// </summary>
public class FontProvider : IDisposable
{
private Dictionary<FontFace, QFont> Fonts { get; } = new Dictionary<FontFace, QFont>();
private HashSet<QFont> UsedFonts { get; } = new HashSet<QFont>();
public readonly FontRasterizerOptions RasterizerOptions;
private readonly QuikApplication App;
public QFont this[FontFace info]
{
get
{
if (!Fonts.TryGetValue(info, out QFont font))
{
font = (QFont)App.GetMedia(info, MediaHint.Font);
Fonts[info] = font;
}
UsedFonts.Add(font);
return font;
}
}
public FontProvider(QuikApplication app, in FontRasterizerOptions options)
{
RasterizerOptions = options;
App = app;
}
public FontProvider(QuikApplication app)
: this(app, FontRasterizerOptions.Default)
{
}
/// <summary>
/// Tracks the use of fonts used by this typesetter and removes any that haven't been referenced since the last cycle.
/// </summary>
public void Collect()
{
// foreach (FontJar jar in Fonts.Values.ToArray())
// {
// if (!UsedFonts.Contains(jar))
// {
// Fonts.Remove(jar.Info);
// }
// }
// UsedFonts.Clear();
}
private bool isDisposed = false;
public void Dispose()
{
if (isDisposed) return;
isDisposed = true;
foreach (QFont font in Fonts.Values)
{
font.Dispose();
}
}
}
}

@ -226,7 +226,7 @@ namespace Quik.Typography
pen.Y -= PostSpace;
group.BoundingBox = new QRectangle(width, 0, 0, pen.Y);
group.BoundingBox = new QRectangle(width, pen.Y, 0, 0);
group.Translate(-pen);
}
@ -353,13 +353,13 @@ namespace Quik.Typography
public struct TypesetCharacter
{
public int Character;
public QuikTexture Texture;
public QImage Texture;
public QRectangle Position;
public QRectangle UV;
public TypesetCharacter(
int chr,
QuikTexture texture,
QImage texture,
in QRectangle position,
in QRectangle uv)
{

@ -0,0 +1,155 @@
using Quik.CommandMachine;
using Quik.Media;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Quik.Typography
{
public static class Typesetter
{
private ref struct LineEnumerator
{
private ReadOnlySpan<char> Entire, Segment;
private bool Final;
public ReadOnlySpan<char> Current => Segment;
public LineEnumerator(ReadOnlySpan<char> value)
{
Entire = value;
Segment = ReadOnlySpan<char>.Empty;
Final = false;
}
public void Reset()
{
Segment = ReadOnlySpan<char>.Empty;
Final = false;
}
public bool MoveNext()
{
if (Final)
{
return false;
}
else if (Segment == ReadOnlySpan<char>.Empty)
{
int index = Entire.IndexOf('\n');
if (index == -1)
{
Segment = Entire;
}
else
{
Segment = Entire.Slice(0, index);
}
return true;
}
else
{
Entire.Overlaps(Segment, out int offset);
if (offset + Segment.Length >= Entire.Length)
{
return false;
}
ReadOnlySpan<char> rest = Entire.Slice(offset + Segment.Length + 1);
int index = rest.IndexOf('\n');
if (index == -1)
{
Segment = rest;
Final = true;
}
else
{
Segment = rest.Slice(0, index);
}
return true;
}
}
}
public static void TypesetHorizontalDirect(this CommandList list, string str, QVec2 origin, float size, QFont font)
{
Dictionary<QImage, FontDrawInfo> drawInfo = new Dictionary<QImage, FontDrawInfo>();
var enumerator = new LineEnumerator(str.AsSpan());
QVec2 pen = origin;
while (enumerator.MoveNext())
{
ReadOnlySpan<char> line = enumerator.Current;
float rise = 0.0f;
float fall = 0.0f;
// Find out all the code pages required, and the line height.
foreach (Rune r in line.EnumerateRunes())
{
int codepoint = r.Value;
font.Get(codepoint, size, out FontGlyph glyph);
float crise = glyph.Metrics.HorizontalBearing.Y;
float cfall = glyph.Metrics.Size.Y - crise;
rise = Math.Max(crise, rise);
fall = Math.Max(cfall, fall);
}
pen += new QVec2(0, rise);
foreach (Rune r in line.EnumerateRunes())
{
FontDrawInfo info;
int codepoint = r.Value;
font.Get(codepoint, size, out FontGlyph glyph);
ref readonly QGlyphMetrics metrics = ref glyph.Metrics;
QImage image = glyph.Image;
if (image == null)
{
pen += new QVec2(metrics.Advance.X, 0);
continue;
}
if (!drawInfo.TryGetValue(image, out info))
{
info = new FontDrawInfo();
info.Image = image;
info.rectangles = new List<QRectangle>();
drawInfo[image] = info;
}
QRectangle dest = new QRectangle(
pen + new QVec2(metrics.HorizontalBearing.X + metrics.Size.X, metrics.Size.Y - metrics.HorizontalBearing.Y),
pen + new QVec2(metrics.HorizontalBearing.X, -metrics.HorizontalBearing.Y));
info.rectangles.Add(dest);
info.rectangles.Add(glyph.UVs);
pen.X += metrics.Advance.X;
}
pen.X = origin.X;
pen.Y += fall;
}
// Now for each rectangle we can dispatch draw calls.
foreach (FontDrawInfo info in drawInfo.Values)
{
list.Image(info.Image, info.rectangles.ToArray(), true);
}
}
private struct FontDrawInfo
{
public QImage Image;
public List<QRectangle> rectangles;
}
}
}

@ -1063,7 +1063,7 @@ namespace Quik.VertexGenerator
DrawQueue.AddVertex(vertex);
vertex.Position = new QVec2(rect.Right, rect.Top);
vertex.TextureCoordinates = new QVec2(uvs.Right, uvs.Right);
vertex.TextureCoordinates = new QVec2(uvs.Right, uvs.Top);
DrawQueue.AddVertex(vertex);
DrawQueue.AddElement(0); DrawQueue.AddElement(2); DrawQueue.AddElement(3);

@ -13,9 +13,8 @@ out vec4 fragColor;
uniform int iEnableSdf;
uniform int iEnableTexture;
uniform int iAlphaDiscard;
uniform float fSdfThreshold;
uniform float fSdfThreshold = 0.5;
uniform sampler2D tx2d;
uniform sampler2DArray tx2dArray;
const float fAlphaThreshold = 0.01;
@ -23,7 +22,13 @@ vec4 getTexture()
{
if (iEnableTexture == 3)
{
return texture(tx2dArray, vec3(fv2TexPos, ffTexLayer));
// return texture(tx2dArray, vec3(fv2TexPos, ffTexLayer));
}
else if (iEnableSdf == 1)
{
vec2 texelSz = 1.0/vec2(textureSize(tx2d, 0));
vec2 txCoord2 = fv2TexPos + texelSz * (1 - mod(fv2TexPos, texelSz));
return texture(tx2d, txCoord2);
}
else
{
@ -41,12 +46,7 @@ void main(void)
if (iEnableSdf != 0)
{
float a = max(value.r, value.a);
value =
(a >= fSdfThreshold) ?
vec4(1.0,1.0,1.0,1.0) :
vec4(0.0,0.0,0.0,0.0);
value = vec4(vec3(1.0), smoothstep(fSdfThreshold-0.1, fSdfThreshold+0.1, value.r));
}
if (iAlphaDiscard != 0 && value.a <= fAlphaThreshold)

@ -1,8 +1,12 @@
using Quik;
using System;
using Quik;
using Quik.CommandMachine;
using Quik.Controls;
using Quik.OpenTK;
using Quik.Media.Defaults;
using Quik.Media;
using Quik.Typography;
using Quik.PAL;
namespace QuikDemo
{
@ -18,11 +22,23 @@ namespace QuikDemo
public class EmptyView : View
{
protected override void PaintBegin(CommandQueue cmd)
private QFont font;
protected override void PaintBegin(CommandList cmd)
{
base.PaintBegin(cmd);
cmd.Rectangle(new QRectangle(0, 0, 16, 16));
if (font == null)
{
IFontDataBase db = FontDataBaseProvider.Instance;
font = new QFontFreeType(db.FontFileInfo(db.Sans).OpenRead());
}
cmd.Rectangle(new QRectangle(16, 16, 0, 0));
cmd.TypesetHorizontalDirect(
"The quick brown fox jumps over the lazy dog.\n" +
"hi?",
new QVec2(64.33f, 0.77f), 9, font);
}
}
}