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

@ -4,14 +4,14 @@ using System.Collections;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using Quik.Media; using Quik.Media.Font;
// WebRequest is obsolete but runs on .NET framework. // WebRequest is obsolete but runs on .NET framework.
#pragma warning disable SYSLIB0014 #pragma warning disable SYSLIB0014
namespace Quik.Media.Defaults 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; public bool AllowRemoteTransfers { get; set; } = false;
private readonly ArrayPool<byte> ByteArrays = ArrayPool<byte>.Create(); private readonly ArrayPool<byte> ByteArrays = ArrayPool<byte>.Create();
@ -31,9 +31,9 @@ namespace Quik.Media.Defaults
{ {
return GetMedia((FileInfo)key, hint); 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 else
{ {
@ -56,7 +56,7 @@ namespace Quik.Media.Defaults
throw new NotImplementedException(); throw new NotImplementedException();
} }
public IDisposable GetMedia(FontInfo key, MediaHint hint) public IDisposable GetMedia(FontFace key, MediaHint hint)
{ {
throw new NotImplementedException(); 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 PortShow(IQuikPortHandle port, bool shown = true) => ((OpenTKPort)port).Show(shown);
public void PortPaint(IQuikPortHandle port, CommandList commands) => ((OpenTKPort)port).Paint(commands); 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) if (!_glDriver.IsInit)
_glDriver.Init(); _glDriver.Init();
GL.Clear(GLEnum.GL_COLOR_BUFFER_BIT);
_glDriver.Draw(_vertexEngine.DrawQueue, view); _glDriver.Draw(_vertexEngine.DrawQueue, view);
_window.Context.SwapBuffers(); _window.Context.SwapBuffers();

@ -1,17 +1,19 @@
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Quik.Media namespace Quik.Media.Color
{ {
public class QImageBuffer : QImage public class QImageBuffer : QImage
{ {
private byte[] buffer; private byte[] buffer;
GCHandle handle; GCHandle handle;
private bool isSdf = false;
public override QImageFormat InternalFormat { get; } public override QImageFormat InternalFormat { get; }
public override int Width { get; } public override int Width { get; }
public override int Height { get; } public override int Height { get; }
public override int Depth { get; } public override int Depth { get; }
public override bool IsSdf => isSdf;
public QImageBuffer(QImageFormat format, int width, int height, int depth = 1) public QImageBuffer(QImageFormat format, int width, int height, int depth = 1)
{ {
@ -20,7 +22,7 @@ namespace Quik.Media
Height = height; Height = height;
Depth = depth; Depth = depth;
buffer = new byte[width * height * depth * format.BytesPerPixel()]; buffer = new byte[width * height * depth];
} }
~QImageBuffer() ~QImageBuffer()
{ {
@ -29,7 +31,7 @@ namespace Quik.Media
private QImageLock Lock() private QImageLock Lock()
{ {
handle.Free(); if (handle.IsAllocated) handle.Free();
handle = GCHandle.Alloc(buffer, GCHandleType.Pinned); handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
IntPtr ptr = Marshal.UnsafeAddrOfPinnedArrayElement(buffer, 0); IntPtr ptr = Marshal.UnsafeAddrOfPinnedArrayElement(buffer, 0);
return new QImageLock(InternalFormat, Width, Height, Depth, ptr); return new QImageLock(InternalFormat, Width, Height, Depth, ptr);
@ -37,56 +39,32 @@ namespace Quik.Media
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
if (handle.IsAllocated) handle.Free();
buffer = null; buffer = null;
if (handle.IsAllocated) handle.Free();
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
public override void LockBits2d(out QImageLock imageLock, QImageLockOptions options) public override void LockBits2d(out QImageLock imageLock, QImageLockOptions options)
{ {
if (options.Format != options.Format) throw new InvalidOperationException("This image type cannot be converted."); imageLock = Lock();
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) public override void LockBits3d(out QImageLock imageLock, QImageLockOptions options)
{ {
if (options.Format != options.Format) throw new InvalidOperationException("This image type cannot be converted."); imageLock = Lock();
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) 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."); imageLock = Lock();
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() 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using Quik.Media.Color;
namespace Quik.Media.Font namespace Quik.Media.Font
{ {
@ -17,11 +21,28 @@ namespace Quik.Media.Font
private readonly Dictionary<int, FontAtlasGlyphInfo> glyphs = new Dictionary<int, FontAtlasGlyphInfo>(); private readonly Dictionary<int, FontAtlasGlyphInfo> glyphs = new Dictionary<int, FontAtlasGlyphInfo>();
private int index = 0; private int index = 0;
private AtlasPage last = null; 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.width = width;
this.height = height; this.height = height;
IsSdf = isSdf;
this.expansion = expansion;
} }
public bool GetGlyph(int codepoint, out FontAtlasGlyphInfo info) public bool GetGlyph(int codepoint, out FontAtlasGlyphInfo info)
@ -33,7 +54,7 @@ namespace Quik.Media.Font
{ {
info = new FontAtlasGlyphInfo() { Codepoint = codepoint }; info = new FontAtlasGlyphInfo() { Codepoint = codepoint };
if (last == null || last.IsFull) if (last == null || !last.WouldFit(source))
{ {
AddPage(); AddPage();
} }
@ -51,7 +72,8 @@ namespace Quik.Media.Font
} }
else else
{ {
last = new AtlasPage(width, height); last = new AtlasPage(width, height, expansion);
(last.Image as QImageBuffer).SetSdf(IsSdf);
atlases.Add(last); atlases.Add(last);
} }
} }
@ -92,12 +114,14 @@ namespace Quik.Media.Font
public QImage Image; public QImage Image;
public int PointerX, PointerY; public int PointerX, PointerY;
public int RowHeight; public int RowHeight;
public int Expansion;
public bool IsFull => PointerX > Image.Width || PointerY > Image.Height; 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); Image = new QImageBuffer(QImageFormat.RedU8, width, height);
Expansion = expansion;
Reset(); Reset();
} }
@ -110,8 +134,8 @@ namespace Quik.Media.Font
src.CopyTo(dst, PointerX, PointerY); src.CopyTo(dst, PointerX, PointerY);
Image.UnlockBits(); Image.UnlockBits();
QVec2 min = new QVec2(PointerX/Image.Width, PointerY/Image.Width); QVec2 min = new QVec2((float)PointerX/Image.Width, (float)PointerY/Image.Height);
QVec2 size = new QVec2(src.Width/Image.Width, src.Height/Image.Height); QVec2 size = new QVec2((float)src.Width/Image.Width, (float)src.Height/Image.Height);
prototype.Image = Image; prototype.Image = Image;
prototype.UVs = new QRectangle(min + size, min); prototype.UVs = new QRectangle(min + size, min);
@ -127,7 +151,7 @@ namespace Quik.Media.Font
public void AdvanceRow() public void AdvanceRow()
{ {
PointerX = 0; PointerX = 0;
PointerY += RowHeight; PointerY += RowHeight + Expansion;
RowHeight = 0; RowHeight = 0;
} }
@ -135,7 +159,7 @@ namespace Quik.Media.Font
public void AdvanceColumn(int width, int height) public void AdvanceColumn(int width, int height)
{ {
RowHeight = Math.Max(RowHeight, height); RowHeight = Math.Max(RowHeight, height);
PointerX += width; PointerX += width + Expansion;
if (PointerX > Image.Width) if (PointerX > Image.Width)
{ {
@ -153,6 +177,11 @@ namespace Quik.Media.Font
isDisposed = true; 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.InteropServices;
using Quik.Media; using Quik.Media;
using Quik.Media.Font; using Quik.Media.Font;
@ -17,8 +18,23 @@ namespace Quik.Media
public FontStretch Stretch => Face.Stretch; public FontStretch Stretch => Face.Stretch;
public abstract bool HasRune(int rune); public abstract bool HasRune(int rune);
public abstract QFontPage RasterizePage(int codepage, float size, in FontRasterizerOptions options); protected abstract QImage Render(out QGlyphMetrics metrics, int codepoint, float size, in FontRasterizerOptions options);
public QFontPage RasterizePage(int codepage, float size) => RasterizePage(codepage, size, FontRasterizerOptions.Default);
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 // IDisposable
private bool isDisposed = false; private bool isDisposed = false;
@ -32,6 +48,71 @@ namespace Quik.Media
} }
protected virtual void Dispose(bool disposing) { } protected virtual void Dispose(bool disposing) { }
public void Dispose() => DisposePrivate(true); 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 public struct FontRasterizerOptions
@ -42,53 +123,7 @@ namespace Quik.Media
public static readonly FontRasterizerOptions Default = new FontRasterizerOptions() public static readonly FontRasterizerOptions Default = new FontRasterizerOptions()
{ {
Resolution = 96.0f, 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(); int bpp = Format.BytesPerPixel();
for (int i = 0; i < Height; i++) 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; long dstPos = x + i * destination.Width;
IntPtr dstPtr = (IntPtr)((long)destination.ImagePtr + dstPos * bpp); IntPtr dstPtr = (IntPtr)((long)destination.ImagePtr + dstPos * bpp);

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

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

@ -55,5 +55,7 @@ namespace Quik.PAL
void PortFocus(IQuikPortHandle port); void PortFocus(IQuikPortHandle port);
void PortShow(IQuikPortHandle port, bool shown = true); void PortShow(IQuikPortHandle port, bool shown = true);
void PortPaint(IQuikPortHandle port, CommandList commands); 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; pen.Y -= PostSpace;
group.BoundingBox = new QRectangle(width, 0, 0, pen.Y); group.BoundingBox = new QRectangle(width, pen.Y, 0, 0);
group.Translate(-pen); group.Translate(-pen);
} }
@ -353,13 +353,13 @@ namespace Quik.Typography
public struct TypesetCharacter public struct TypesetCharacter
{ {
public int Character; public int Character;
public QuikTexture Texture; public QImage Texture;
public QRectangle Position; public QRectangle Position;
public QRectangle UV; public QRectangle UV;
public TypesetCharacter( public TypesetCharacter(
int chr, int chr,
QuikTexture texture, QImage texture,
in QRectangle position, in QRectangle position,
in QRectangle uv) 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); DrawQueue.AddVertex(vertex);
vertex.Position = new QVec2(rect.Right, rect.Top); 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.AddVertex(vertex);
DrawQueue.AddElement(0); DrawQueue.AddElement(2); DrawQueue.AddElement(3); DrawQueue.AddElement(0); DrawQueue.AddElement(2); DrawQueue.AddElement(3);

@ -13,9 +13,8 @@ out vec4 fragColor;
uniform int iEnableSdf; uniform int iEnableSdf;
uniform int iEnableTexture; uniform int iEnableTexture;
uniform int iAlphaDiscard; uniform int iAlphaDiscard;
uniform float fSdfThreshold; uniform float fSdfThreshold = 0.5;
uniform sampler2D tx2d; uniform sampler2D tx2d;
uniform sampler2DArray tx2dArray;
const float fAlphaThreshold = 0.01; const float fAlphaThreshold = 0.01;
@ -23,7 +22,13 @@ vec4 getTexture()
{ {
if (iEnableTexture == 3) 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 else
{ {
@ -41,12 +46,7 @@ void main(void)
if (iEnableSdf != 0) if (iEnableSdf != 0)
{ {
float a = max(value.r, value.a); value = vec4(vec3(1.0), smoothstep(fSdfThreshold-0.1, fSdfThreshold+0.1, value.r));
value =
(a >= fSdfThreshold) ?
vec4(1.0,1.0,1.0,1.0) :
vec4(0.0,0.0,0.0,0.0);
} }
if (iAlphaDiscard != 0 && value.a <= fAlphaThreshold) if (iAlphaDiscard != 0 && value.a <= fAlphaThreshold)

@ -1,8 +1,12 @@
using Quik; using System;
using Quik;
using Quik.CommandMachine; using Quik.CommandMachine;
using Quik.Controls; using Quik.Controls;
using Quik.OpenTK; using Quik.OpenTK;
using Quik.Media.Defaults;
using Quik.Media;
using Quik.Typography;
using Quik.PAL;
namespace QuikDemo namespace QuikDemo
{ {
@ -18,11 +22,23 @@ namespace QuikDemo
public class EmptyView : View public class EmptyView : View
{ {
protected override void PaintBegin(CommandQueue cmd) private QFont font;
protected override void PaintBegin(CommandList cmd)
{ {
base.PaintBegin(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);
} }
} }
} }