Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
be8329f
Add a managed TrueType reader for text on every platform (#412)
claude Oct 15, 2026
cb05b5b
Get the fonts of texts from a font service of the kernel instead of G…
claude Oct 15, 2026
fc5326e
Read the CFF outlines of OpenType fonts in the managed font reader (#…
claude Oct 15, 2026
5e7a0be
Read variable fonts: gvar, CFF2, avar and HVAR (#412)
claude Oct 15, 2026
4d939ef
Compare the managed font reader with GDI on Windows (#412)
claude Oct 15, 2026
e08061f
Find a family by the name GDI cuts to 31 characters (#412)
claude Oct 15, 2026
318a1ed
Lay out glyphs, metrics and decorations like GDI (#412)
claude Oct 15, 2026
191927d
Name and measure variable instances like Windows (#412)
claude Oct 15, 2026
b30fcd0
Correct the documentation of HasOutlines and HasCffOutlines for CFF2 …
claude Oct 15, 2026
1d19182
Merge branch 'feature/variable-fonts' into feature/gdi-glyph-comparison
claude Oct 15, 2026
3f1eb39
Merge master into feature/font-service
claude Oct 17, 2026
415fea4
Merge branch 'feature/font-service' into feature/cff-outlines
claude Oct 17, 2026
9a17a7b
Merge branch 'feature/cff-outlines' into feature/variable-fonts
claude Oct 17, 2026
95efb36
Merge branch 'feature/variable-fonts' into feature/gdi-glyph-comparison
claude Oct 17, 2026
8d22a3f
Adapt the font cache test of #497 to the font service (#412)
claude Oct 17, 2026
8e4900a
Merge branch 'feature/font-service' into feature/cff-outlines
claude Oct 17, 2026
1e56c2e
Merge branch 'feature/cff-outlines' into feature/variable-fonts
claude Oct 17, 2026
1496f3b
Merge branch 'feature/variable-fonts' into feature/gdi-glyph-comparison
claude Oct 17, 2026
83debf3
Merge remote-tracking branch 'origin/master' into feature/managed-fon…
claude Oct 19, 2026
830f757
Merge remote-tracking branch 'origin/feature/managed-font-provider' i…
claude Oct 19, 2026
3163749
Pass Unicode code points instead of chars to FontFace (#412)
claude Oct 19, 2026
3e640f2
Merge branch 'feature/font-service' into feature/cff-outlines
claude Oct 19, 2026
8a07d38
Merge branch 'feature/cff-outlines' into feature/variable-fonts
claude Oct 19, 2026
23aa919
Merge branch 'feature/variable-fonts' into feature/gdi-glyph-comparison
claude Oct 19, 2026
ac0ad17
Lock the shared sets of the parallel STEP import
claude Oct 19, 2026
c54ed31
Merge branch 'feature/managed-font-provider' into feature/font-service
claude Oct 19, 2026
5b325e3
Merge branch 'feature/font-service' into feature/cff-outlines
claude Oct 19, 2026
6b5e200
Merge branch 'feature/cff-outlines' into feature/variable-fonts
claude Oct 19, 2026
cfd97de
Merge branch 'feature/variable-fonts' into feature/gdi-glyph-comparison
claude Oct 19, 2026
cac8016
Make the GDI font provider the default in CADability.Forms (#503)
dsn27 Oct 19, 2026
74b0128
Merge branch 'master' into feature/gdi-glyph-comparison
claude Oct 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CADability.Forms/CadCanvas.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ private static CADability.Substitutes.PaintEventArgs Subst(System.Windows.Forms.
private string currenToolTip;
private string currentCursor;
private Action<int> callbackCollapsed;
// texts get their fonts from GDI as before issue #412, unless the application has chosen a font provider
static CadCanvas() { GdiFontProvider.MakeDefault(); }
public CadCanvas() : base()
{
toolTip = new ToolTip();
Expand Down
2 changes: 2 additions & 0 deletions CADability.Forms/CadControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public partial class CadControl : UserControl, ICommandHandler

#endregion PRIVATE FIELDS

// texts get their fonts from GDI as before issue #412, unless the application has chosen a font provider
static CadControl() { GdiFontProvider.MakeDefault(); }
/// <summary>
/// Default constructor
/// </summary>
Expand Down
3 changes: 3 additions & 0 deletions CADability.Forms/CadFrame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ namespace CADability.Forms
/// </summary>
public class CadFrame : FrameImpl, IUIService
{
// texts get their fonts from GDI as before issue #412, unless the application has chosen a font provider
static CadFrame() { GdiFontProvider.MakeDefault(); }

#region PRIVATE FIELDS

private ICommandHandler commandHandler;
Expand Down
22 changes: 18 additions & 4 deletions CADability.Forms/GdiFontProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,32 @@
using System.Drawing.Drawing2D;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;

namespace CADability.Forms
{
/// <summary>
/// A <see cref="FontProvider"/> that gets the fonts from GDI and GDI+, as CADability did before issue #412: the glyph
/// outlines from <see cref="GraphicsPath.AddString(string, FontFamily, int, float, PointF, StringFormat)"/>, the widths and
/// the kerning from gdi32. It knows every font Windows knows (also OpenType fonts with CFF outlines) and synthesizes bold
/// and italic faces a family does not have. Set <see cref="FontProvider.Current"/> to an instance of this class to use it;
/// the default is the <see cref="ManagedFontProvider"/>.
/// outlines from <see cref="GraphicsPath.AddString(string, FontFamily, int, float, PointF, StringFormat)"/>, the widths
/// and the kerning from gdi32. It knows every font Windows knows (also OpenType fonts with CFF outlines) and
/// synthesizes bold and italic faces a family does not have. As soon as one of the main classes of CADability.Forms is
/// used (<see cref="CadControl"/>, <see cref="CadCanvas"/>, <see cref="CadFrame"/>, <see cref="PaintToOpenGL"/> or
/// <see cref="PaintToGDI"/>), this provider becomes the default, so that Windows Forms applications show their texts
/// as before. An application that sets <see cref="FontProvider.Current"/> itself keeps its choice, e.g.
/// <c>FontProvider.Current = new ManagedFontProvider();</c> for the same fonts as on the other platforms.
/// </summary>
public class GdiFontProvider : FontProvider
{
private static int madeDefault;
/// <summary>
/// Makes a <see cref="GdiFontProvider"/> the default font provider, unless the application has chosen one. Called by the
/// static constructors of the main classes of CADability.Forms; only the first call does something.
/// </summary>
internal static void MakeDefault()
{
if (Interlocked.Exchange(ref madeDefault, 1) == 0) SetDefault(() => new GdiFontProvider());
}

private readonly Dictionary<(string family, bool bold, bool italic), GdiFontFace> faces = new Dictionary<(string, bool, bool), GdiFontFace>();
private HashSet<string> familyNames; // upper case

Expand Down
2 changes: 2 additions & 0 deletions CADability.Forms/PaintToGDI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ public void Dispose()
LazyInitializer.EnsureInitialized(ref fontFamilyNames, () =>
new HashSet<string>(FontFamily.Families.Select(f => f.Name.ToUpper())));

// texts get their fonts from GDI as before issue #412, unless the application has chosen a font provider
static PaintToGDI() { GdiFontProvider.MakeDefault(); }
public PaintToGDI(Projection projection, Graphics graphics)
: this()
{
Expand Down
3 changes: 3 additions & 0 deletions CADability.Forms/PaintToOpenGL.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ internal PaintToOpenGLException(String msg)
/// </summary>
public class PaintToOpenGL : IPaintTo3D, IPaintTo3DFlatText
{
// texts get their fonts from GDI as before issue #412, unless the application has chosen a font provider
static PaintToOpenGL() { GdiFontProvider.MakeDefault(); }

#region IPaintTo3D data
public static int debugNumTriangles = 0;
public static Thread MainThread = null;
Expand Down
68 changes: 59 additions & 9 deletions CADability/FontProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,63 @@ namespace CADability
/// Provides the fonts for <see cref="Text"/> and dimensions: the names of the font families, their metrics and the outlines
/// of the glyphs. The kernel gets all fonts from <see cref="Current"/>, which works without any user interface. The default
/// is a <see cref="ManagedFontProvider"/>, which reads the TrueType font files of the system itself and therefore works on
/// every platform. CADability.Forms contains a provider based on GDI (GdiFontProvider) for applications that want the
/// fonts exactly as Windows renders them.
/// every platform. CADability.Forms contains a provider based on GDI (GdiFontProvider), which gives the fonts exactly as
/// CADability showed them on Windows before; as soon as CADability.Forms is used, it makes that provider the default,
/// unless the application has set <see cref="Current"/> itself.
/// </summary>
public abstract class FontProvider
{
private static FontProvider current;
private static bool chosenByApplication;
private static readonly object sync = new object();
/// <summary>
/// The font provider the kernel uses. Setting it clears the cached glyphs, so that texts are rebuilt with the new
/// provider, like <see cref="Text.ClearFontCache"/>: set it before texts are painted, or rebuild the display lists of
/// the views afterwards (<see cref="ProjectedModel.InvalidateDisplayLists"/>).
/// the views afterwards (<see cref="ProjectedModel.InvalidateDisplayLists"/>). A provider set here is kept:
/// CADability.Forms only replaces the default with its GDI provider while the application has not set one.
/// </summary>
public static FontProvider Current
{
get => LazyInitializer.EnsureInitialized(ref current, () => new ManagedFontProvider());
get
{
FontProvider res = Volatile.Read(ref current);
if (res != null) return res;
lock (sync)
{
if (current == null) current = new ManagedFontProvider();
return current;
}
}
set
{
current = value ?? throw new ArgumentNullException(nameof(value));
FontCache.GlobalFontCache.Clear();
Text.ResetFontFamilyNames();
if (value == null) throw new ArgumentNullException(nameof(value));
lock (sync)
{
chosenByApplication = true;
current = value;
}
ClearCaches();
}
}
/// <summary>
/// Makes the provider made by <paramref name="create"/> the default, unless the application has set
/// <see cref="Current"/> itself. CADability.Forms calls this to use GDI for the fonts, as CADability did before issue #412.
/// </summary>
internal static void SetDefault(Func<FontProvider> create)
{
lock (sync)
{
if (chosenByApplication) return;
current = create();
}
ClearCaches();
}
private static void ClearCaches()
{ // outside the lock: the caches have locks of their own, and a thread holding one of them may ask for Current
FontCache.GlobalFontCache.Clear();
Text.ResetFontFamilyNames();
}
/// <summary>
/// The names of all font families this provider knows, e.g. for a list to choose from.
/// </summary>
public abstract string[] GetFamilyNames();
Expand Down Expand Up @@ -218,7 +253,7 @@ public override FontFace GetFace(string familyName, bool bold, bool italic)
Entry best;
lock (locker)
{
if (!Families().TryGetValue(familyName.ToUpperInvariant(), out List<Entry> faces)) return null;
if (!Families().TryGetValue(familyName.ToUpperInvariant(), out List<Entry> faces) && !TruncatedFamily(familyName, out faces)) return null;
// the exact style, then the same weight, then regular, then any face
best = faces.FirstOrDefault(f => f.Bold == bold && f.Italic == italic)
?? faces.FirstOrDefault(f => f.Bold == bold)
Expand Down Expand Up @@ -250,7 +285,7 @@ private static void AddEntry(Entry entry, TrueTypeFont font, List<Entry> list)
// a variable font: every named instance is a face, named like GDI names it
foreach (NamedInstance instance in instances)
{
InstanceFamily(font.TypographicFamilyName, instance.SubfamilyName, out string family, out bool bold, out bool italic);
InstanceFamily(font.TypographicFamilyName, font.InstanceName(instance), out string family, out bool bold, out bool italic);
italic |= font.IsItalic; // the instances of an italic font are italic, whether their names say so or not
list.Add(new Entry
{
Expand Down Expand Up @@ -281,6 +316,21 @@ internal static void InstanceFamily(string baseFamily, string instanceName, out
}
family = rest.Count == 0 ? baseFamily : baseFamily + " " + string.Join(" ", rest);
}
/// <summary>
/// GDI cuts family names to 31 characters (LF_FACESIZE), e.g. "Bahnschrift SemiBold SemiConden", and texts saved with
/// GDI keep that name. Such a name stands for the only family whose name starts with it.
/// </summary>
private bool TruncatedFamily(string familyName, out List<Entry> faces)
{
faces = null;
if (familyName.Length != GdiFaceNameLength) return false;
string prefix = familyName.ToUpperInvariant();
List<string> candidates = Families().Keys.Where(name => name.StartsWith(prefix, StringComparison.Ordinal)).Take(2).ToList();
if (candidates.Count != 1) return false;
faces = Families()[candidates[0]];
return true;
}
private const int GdiFaceNameLength = 31;
private Dictionary<string, List<Entry>> Families()
{
if (families != null) return families;
Expand Down
119 changes: 117 additions & 2 deletions CADability/FontVariations.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace CADability
{
Expand All @@ -25,8 +26,8 @@ internal struct NamedInstance
/// <summary>
/// The variation data of a variable OpenType font: the axes and named instances ("fvar"), the axis mapping ("avar"), the
/// glyph variations of TrueType outlines ("gvar", with the interpolation of untouched points), the advance width variations
/// ("HVAR") and the item variation store that CFF2 outlines use for their blend operator. Variations of the font wide metrics
/// ("MVAR") are not applied.
/// ("HVAR"), the variations of the font wide metrics ("MVAR"), the item variation store that CFF2 outlines use for their
/// blend operator and the names of the instances from the style attributes ("STAT").
/// </summary>
internal sealed class FontVariations
{
Expand All @@ -38,13 +39,18 @@ internal sealed class FontVariations
private readonly bool gvarLongOffsets;
private readonly ItemVariationStore hvarStore;
private readonly int hvarAdvanceMap = -1;
private readonly ItemVariationStore mvarStore;
private readonly Dictionary<string, (int outer, int inner)> mvarRecords = new Dictionary<string, (int, int)>();
private readonly int statOffset = -1;
private readonly Func<int, string> nameOf;

/// <param name="data">the font file</param>
/// <param name="tables">offset and length of the tables of the font</param>
/// <param name="nameOf">the name for a name ID</param>
public FontVariations(byte[] data, Dictionary<string, (int offset, int length)> tables, Func<int, string> nameOf)
{
this.data = data;
this.nameOf = nameOf;
var fvar = tables["fvar"];
int f = fvar.offset;
int axesOffset = f + U16(f + 4), axisCount = U16(f + 8), axisSize = U16(f + 10), instanceCount = U16(f + 12), instanceSize = U16(f + 14);
Expand Down Expand Up @@ -107,6 +113,115 @@ public FontVariations(byte[] data, Dictionary<string, (int offset, int length)>
uint map = U32(h + 8);
if (map != 0) hvarAdvanceMap = h + (int)map;
}
if (tables.TryGetValue("MVAR", out var mv) && mv.length >= 12)
{
int m = mv.offset;
int recordSize = U16(m + 6), recordCount = U16(m + 8), store = U16(m + 10);
if (store != 0 && recordSize >= 8)
{
mvarStore = new ItemVariationStore(data, m + store, axisCount);
for (int i = 0; i < recordCount; ++i)
{
int r = m + 12 + i * recordSize;
mvarRecords[System.Text.Encoding.ASCII.GetString(data, r, 4)] = (U16(r + 4), U16(r + 6));
}
}
}
if (tables.TryGetValue("STAT", out var st) && st.length >= 18) statOffset = st.offset;
}

/// <summary>
/// The change of a font wide metric from "MVAR" at the normalized coordinates, e.g. "strs" for the size of the strikeout
/// line, 0 if the font does not vary it.
/// </summary>
public double GetMetricDelta(string tag, double[] coords)
{
if (mvarStore == null || coords == null || !mvarRecords.TryGetValue(tag, out var record)) return 0.0;
return mvarStore.GetDelta(record.outer, record.inner, coords);
}

/// <summary>
/// The name of an instance (coordinates in user units) as the style attributes of "STAT" make it, e.g. "Display Light"
/// for wght 300 and opsz 36 of Segoe UI Variable: the names of the axis values of the instance in the axis ordering of
/// "STAT", without the elidable ones like "Regular". This is how Windows names the instances, while the subfamily name
/// in "fvar" may have another order ("Light Display"). Null if the font has no "STAT" table or an axis has no value
/// for the instance.
/// </summary>
public string StatInstanceName(double[] userCoordinates)
{
if (statOffset < 0) return null;
try
{
int t = statOffset;
int designAxisSize = U16(t + 4), designAxisCount = U16(t + 6), designAxes = t + (int)U32(t + 8);
int valueCount = U16(t + 12), valueOffsets = t + (int)U32(t + 14);
// the coordinate of the instance on every design axis (NaN for an axis "fvar" does not have)
double[] coordinate = new double[designAxisCount];
int[] ordering = new int[designAxisCount];
for (int i = 0; i < designAxisCount; ++i)
{
int a = designAxes + i * designAxisSize;
string tag = System.Text.Encoding.ASCII.GetString(data, a, 4);
ordering[i] = U16(a + 6);
coordinate[i] = double.NaN;
for (int k = 0; k < Axes.Length; ++k) if (Axes[k].Tag == tag) coordinate[i] = userCoordinates[k];
}
bool Matches(int axis, double value) => axis < designAxisCount && Math.Abs(coordinate[axis] - value) < 1e-3;
string[] names = new string[designAxisCount];
bool[] resolved = new bool[designAxisCount];
List<(int axis, int nameId, bool elidable)> single = new List<(int, int, bool)>();
for (int v = 0; v < valueCount; ++v)
{
int p = valueOffsets + U16(valueOffsets + 2 * v);
int format = U16(p);
if (format == 4)
{ // a combination of values on several axes, e.g. "Condensed Bold", which wins over the single values
int count = U16(p + 2), flags = U16(p + 4), nameId = U16(p + 6);
List<int> axes = new List<int>();
bool all = count > 0;
for (int k = 0; k < count; ++k)
{
int axis = U16(p + 8 + 6 * k);
if (!Matches(axis, Fixed(p + 10 + 6 * k))) all = false;
axes.Add(axis);
}
if (!all || axes.Any(axis => resolved[axis])) continue;
int first = axes.OrderBy(axis => ordering[axis]).First();
foreach (int axis in axes) resolved[axis] = true;
names[first] = (flags & 2) != 0 ? "" : nameOf(nameId);
continue;
}
if (format < 1 || format > 3) continue;
int ax = U16(p + 2), fl = U16(p + 4), id = U16(p + 6);
if (ax >= designAxisCount) continue;
bool match;
if (double.IsNaN(coordinate[ax])) match = false; // an axis without variation: see below
else if (format == 2) match = Fixed(p + 12) <= coordinate[ax] && coordinate[ax] <= Fixed(p + 16);
else match = Matches(ax, Fixed(p + 8));
if (match && !resolved[ax])
{
resolved[ax] = true;
names[ax] = (fl & 2) != 0 ? "" : nameOf(id);
}
single.Add((ax, id, (fl & 2) != 0));
}
for (int i = 0; i < designAxisCount; ++i)
{
if (resolved[i]) continue;
if (!double.IsNaN(coordinate[i])) return null; // an axis of "fvar" without a value for this instance
// an axis the font does not vary, e.g. "ital" of an upright font: its only value, if it has exactly one
List<(int axis, int nameId, bool elidable)> values = single.Where(x => x.axis == i).ToList();
if (values.Count == 1 && !values[0].elidable) names[i] = nameOf(values[0].nameId);
}
for (int i = 0; i < designAxisCount; ++i) if (resolved[i] && names[i] == null) return null; // a name is missing
IEnumerable<string> parts = Enumerable.Range(0, designAxisCount).OrderBy(i => ordering[i]).Select(i => names[i]).Where(n => !string.IsNullOrEmpty(n));
string res = string.Join(" ", parts);
return res.Length == 0 ? "Regular" : res;
}
catch (IndexOutOfRangeException)
{
return null; // a broken "STAT" table: the names of "fvar" are used
}
}

/// <summary>
Expand Down
Loading
Loading