diff --git a/CADability.Forms/GdiFontProvider.cs b/CADability.Forms/GdiFontProvider.cs
new file mode 100644
index 00000000..846e3d9e
--- /dev/null
+++ b/CADability.Forms/GdiFontProvider.cs
@@ -0,0 +1,225 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Drawing.Drawing2D;
+using System.Linq;
+using System.Runtime.InteropServices;
+
+namespace CADability.Forms
+{
+ ///
+ /// A that gets the fonts from GDI and GDI+, as CADability did before issue #412: the glyph
+ /// outlines from , 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 to an instance of this class to use it;
+ /// the default is the .
+ ///
+ public class GdiFontProvider : FontProvider
+ {
+ private readonly Dictionary<(string family, bool bold, bool italic), GdiFontFace> faces = new Dictionary<(string, bool, bool), GdiFontFace>();
+ private HashSet familyNames; // upper case
+
+ ///
+ public override string[] GetFamilyNames()
+ {
+ return FontFamily.Families.Select(f => f.Name).ToArray();
+ }
+ ///
+ public override FontFace GetFace(string familyName, bool bold, bool italic)
+ {
+ if (string.IsNullOrEmpty(familyName)) return null;
+ lock (faces)
+ {
+ if (familyNames == null) familyNames = new HashSet(GetFamilyNames().Select(n => n.ToUpperInvariant()));
+ if (!familyNames.Contains(familyName.ToUpperInvariant())) return null;
+ if (!faces.TryGetValue((familyName.ToUpperInvariant(), bold, italic), out GdiFontFace face))
+ {
+ face = new GdiFontFace(new FontFamily(familyName), bold, italic);
+ faces[(familyName.ToUpperInvariant(), bold, italic)] = face;
+ }
+ return face;
+ }
+ }
+ ///
+ /// The generic sans serif family of GDI+, as before issue #412.
+ ///
+ public override FontFace GetDefaultFace(bool bold, bool italic)
+ {
+ using (FontFamily sansSerif = new FontFamily(System.Drawing.Text.GenericFontFamilies.SansSerif))
+ {
+ return GetFace(sansSerif.Name, bold, italic);
+ }
+ }
+ }
+
+ ///
+ /// A face of the .
+ ///
+ internal sealed class GdiFontFace : FontFace
+ {
+ private readonly FontFamily family;
+ private readonly FontStyle style; // bold and italic only
+ private readonly int em;
+ private readonly Dictionary widths = new Dictionary();
+ private Dictionary<(int, int), double> kerning;
+
+ public GdiFontFace(FontFamily family, bool bold, bool italic)
+ {
+ this.family = family;
+ FontStyle requested = (bold ? FontStyle.Bold : FontStyle.Regular) | (italic ? FontStyle.Italic : FontStyle.Regular);
+ style = requested;
+ if (!family.IsStyleAvailable(style))
+ {
+ if (family.IsStyleAvailable(FontStyle.Regular)) style = FontStyle.Regular;
+ else if (family.IsStyleAvailable(FontStyle.Bold)) style = FontStyle.Bold;
+ else if (family.IsStyleAvailable(FontStyle.Italic)) style = FontStyle.Italic;
+ }
+ em = family.GetEmHeight(style);
+ if (em == 0) em = 1000;
+ }
+ public override string FamilyName => family.Name;
+ public override bool IsBold => (style & FontStyle.Bold) != 0;
+ public override bool IsItalic => (style & FontStyle.Italic) != 0;
+ public override int EmHeight => em;
+ public override int CellAscent => family.GetCellAscent(style);
+ public override int CellDescent => family.GetCellDescent(style);
+ public override int LineSpacing => family.GetLineSpacing(style);
+
+ public override double GetAdvanceWidth(int codePoint)
+ {
+ lock (widths)
+ {
+ if (widths.TryGetValue(codePoint, out double width)) return width;
+ WithSelectedFont(hdc =>
+ {
+ if (codePoint <= 0xFFFF)
+ {
+ ABC[] abc = new ABC[1];
+ GetCharABCWidths(hdc, (uint)codePoint, (uint)codePoint, abc);
+ width = (abc[0].abcA + abc[0].abcB + abc[0].abcC) / (double)em;
+ }
+ else
+ { // GetCharABCWidths only takes UTF-16 chars: measure the surrogate pair as a string
+ string s = char.ConvertFromUtf32(codePoint);
+ GetTextExtentPoint32(hdc, s, s.Length, out SIZE size);
+ width = size.cx / (double)em;
+ }
+ });
+ widths[codePoint] = width;
+ return width;
+ }
+ }
+ public override double GetKerning(int left, int right)
+ {
+ lock (widths)
+ {
+ if (kerning == null)
+ {
+ Dictionary<(int, int), double> pairs = new Dictionary<(int, int), double>();
+ WithSelectedFont(hdc =>
+ {
+ int num = GetKerningPairs(hdc, 0, null);
+ if (num > 0)
+ {
+ KERNINGPAIR[] kp = new KERNINGPAIR[num];
+ GetKerningPairs(hdc, num, kp);
+ for (int i = 0; i < kp.Length; ++i) pairs[((ushort)kp[i].wFirst, (ushort)kp[i].wSecond)] = kp[i].iKernAmount / (double)em;
+ }
+ });
+ kerning = pairs;
+ }
+ kerning.TryGetValue((left, right), out double res);
+ return res;
+ }
+ }
+ public override void GetGlyphPath(int codePoint, bool underline, bool strikeout, out PointF[] points, out byte[] types)
+ {
+ FontStyle fs = style | (underline ? FontStyle.Underline : FontStyle.Regular) | (strikeout ? FontStyle.Strikeout : FontStyle.Regular);
+ using (GraphicsPath path = new GraphicsPath())
+ using (StringFormat sf = (StringFormat)StringFormat.GenericTypographic.Clone())
+ {
+ sf.LineAlignment = StringAlignment.Near;
+ sf.Alignment = StringAlignment.Near;
+ path.AddString(AsString(codePoint), family, (int)fs, 1.0f, new PointF(0.0f, 0.0f), sf);
+ if (path.PointCount == 0)
+ {
+ points = new PointF[0];
+ types = new byte[0];
+ }
+ else
+ {
+ points = path.PathPoints;
+ types = path.PathTypes;
+ }
+ }
+ }
+
+ // a lone surrogate (not a valid code point) is passed on as the char it is
+ private static string AsString(int codePoint) => codePoint >= 0xD800 && codePoint <= 0xDFFF ? ((char)codePoint).ToString() : char.ConvertFromUtf32(codePoint);
+
+ ///
+ /// Selects this face with the em height as pixel size into a device context for the duration of .
+ /// The device context only lives during the call, so nothing is left to a finalizer.
+ ///
+ private void WithSelectedFont(Action action)
+ {
+ IntPtr hdc = CreateCompatibleDC(IntPtr.Zero);
+ try
+ {
+ using (Font font = new Font(family, em, style, GraphicsUnit.Pixel))
+ {
+ IntPtr hfont = font.ToHfont();
+ IntPtr oldFont = SelectObject(hdc, hfont);
+ try
+ {
+ action(hdc);
+ }
+ finally
+ {
+ SelectObject(hdc, oldFont);
+ DeleteObject(hfont);
+ }
+ }
+ }
+ finally
+ {
+ DeleteDC(hdc);
+ }
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct ABC
+ {
+ public int abcA;
+ public uint abcB;
+ public int abcC;
+ }
+ [StructLayout(LayoutKind.Sequential)]
+ private struct KERNINGPAIR
+ {
+ public short wFirst;
+ public short wSecond;
+ public int iKernAmount;
+ }
+ [DllImport("gdi32.dll", EntryPoint = "GetCharABCWidthsW")]
+ private static extern bool GetCharABCWidths(IntPtr hdc, uint uFirstChar, uint uLastChar, [Out] ABC[] lpabc);
+ [StructLayout(LayoutKind.Sequential)]
+ private struct SIZE
+ {
+ public int cx;
+ public int cy;
+ }
+ [DllImport("gdi32.dll", EntryPoint = "GetTextExtentPoint32W", CharSet = CharSet.Unicode)]
+ private static extern bool GetTextExtentPoint32(IntPtr hdc, string lpString, int c, out SIZE psizl);
+ [DllImport("gdi32.dll", EntryPoint = "GetKerningPairsW")]
+ private static extern int GetKerningPairs(IntPtr hdc, int cPairs, [Out] KERNINGPAIR[] lpkrnpair);
+ [DllImport("gdi32.dll")]
+ private static extern IntPtr CreateCompatibleDC(IntPtr hdc);
+ [DllImport("gdi32.dll")]
+ private static extern bool DeleteDC(IntPtr hdc);
+ [DllImport("gdi32.dll")]
+ private static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);
+ [DllImport("gdi32.dll")]
+ private static extern bool DeleteObject(IntPtr hObject);
+ }
+}
diff --git a/CADability/DimensionStyle.cs b/CADability/DimensionStyle.cs
index a0d7e421..41fd15c1 100644
--- a/CADability/DimensionStyle.cs
+++ b/CADability/DimensionStyle.cs
@@ -1,6 +1,7 @@
using CADability.GeoObject;
using CADability.UserInterface;
using System;
+using System.Linq;
using System.Runtime.Serialization;
namespace CADability.Attribute
@@ -2056,9 +2057,7 @@ public override IPropertyEntry[] SubItems
options.AddSubEntry(new CheckProperty(this, "DimRadiusBend", "DimensionStyle.DimRadiusBend"));
ShowPropertyGroup font = new ShowPropertyGroup("DimensionStyle.Font");
- System.Drawing.FontFamily[] families = System.Drawing.FontFamily.Families;
- string[] choices = new string[families.Length];
- for (int i = 0; i < families.Length; i++) choices[i] = families[i].Name;
+ string[] choices = FontProvider.Current.GetFamilyNames().OrderBy(n => n, StringComparer.CurrentCultureIgnoreCase).ToArray();
MultipleChoiceProperty multipropfont = new MultipleChoiceProperty("DimensionStyle.TextFont", choices, TextFont);
multipropfont.ValueChangedEvent += new ValueChangedDelegate(FontValueChanged);
font.AddSubEntry(multipropfont);
diff --git a/CADability/FontProvider.cs b/CADability/FontProvider.cs
new file mode 100644
index 00000000..e0c00708
--- /dev/null
+++ b/CADability/FontProvider.cs
@@ -0,0 +1,320 @@
+using CADability.GeoObject;
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using Path = System.IO.Path;
+
+namespace CADability
+{
+ ///
+ /// Provides the fonts for and dimensions: the names of the font families, their metrics and the outlines
+ /// of the glyphs. The kernel gets all fonts from , which works without any user interface. The default
+ /// is a , 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.
+ ///
+ public abstract class FontProvider
+ {
+ private static FontProvider current;
+ ///
+ /// The font provider the kernel uses. Setting it clears the cached glyphs, so that texts are rebuilt with the new
+ /// provider, like : set it before texts are painted, or rebuild the display lists of
+ /// the views afterwards ().
+ ///
+ public static FontProvider Current
+ {
+ get => LazyInitializer.EnsureInitialized(ref current, () => new ManagedFontProvider());
+ set
+ {
+ current = value ?? throw new ArgumentNullException(nameof(value));
+ FontCache.GlobalFontCache.Clear();
+ Text.ResetFontFamilyNames();
+ }
+ }
+ ///
+ /// The names of all font families this provider knows, e.g. for a list to choose from.
+ ///
+ public abstract string[] GetFamilyNames();
+ ///
+ /// Returns the face of the font family (not case sensitive) that matches the requested
+ /// style best, or null if the provider does not know this family.
+ ///
+ /// The name of the font family, e.g. "Arial"
+ /// Bold requested
+ /// Italic requested
+ public abstract FontFace GetFace(string familyName, bool bold, bool italic);
+ ///
+ /// Returns the face to use for a family that does not know. The default implementation tries some
+ /// common sans serif fonts and then takes the first family. Returns null when there is no font at all.
+ ///
+ public virtual FontFace GetDefaultFace(bool bold, bool italic)
+ {
+ foreach (string name in new[] { "Microsoft Sans Serif", "Arial", "Liberation Sans", "DejaVu Sans", "Helvetica", "Noto Sans", "FreeSans" })
+ {
+ FontFace face = GetFace(name, bold, italic);
+ if (face != null) return face;
+ }
+ string first = GetFamilyNames().OrderBy(n => n, StringComparer.OrdinalIgnoreCase).FirstOrDefault();
+ return first == null ? null : GetFace(first, bold, italic);
+ }
+ }
+
+ ///
+ /// A face of a font family (e.g. "Arial Bold") as a provides it. Metrics are in font units,
+ /// widths and kerning in units of the em height, and the glyph outlines are Bezier curves, so that all providers produce
+ /// the same exact geometry.
+ ///
+ public abstract class FontFace
+ {
+ ///
+ /// The name of the font family.
+ ///
+ public abstract string FamilyName { get; }
+ ///
+ /// True if this face is bold.
+ ///
+ public abstract bool IsBold { get; }
+ ///
+ /// True if this face is italic.
+ ///
+ public abstract bool IsItalic { get; }
+ ///
+ /// The height of the em square in font units (e.g. 2048 for most TrueType fonts).
+ ///
+ public abstract int EmHeight { get; }
+ ///
+ /// The ascent of the character cell in font units.
+ ///
+ public abstract int CellAscent { get; }
+ ///
+ /// The descent of the character cell in font units, positive downwards.
+ ///
+ public abstract int CellDescent { get; }
+ ///
+ /// The distance between the baselines of two lines in font units.
+ ///
+ public abstract int LineSpacing { get; }
+ ///
+ /// The advance width of the character in units of the em height. Characters are Unicode
+ /// code points, so that characters outside the Basic Multilingual Plane (e.g. most emoji), which take two UTF-16
+ /// chars (a surrogate pair) in a string, are one character here.
+ ///
+ public abstract double GetAdvanceWidth(int codePoint);
+ ///
+ /// The kerning between two characters (Unicode code points) in units of the em height, usually negative; 0 if there
+ /// is none.
+ ///
+ public abstract double GetKerning(int left, int right);
+ ///
+ /// Returns the outline of the character (Unicode code point) in the layout of a GDI+ GraphicsPath after
+ /// AddString with an em size of 1 and StringFormat.GenericTypographic: in units of the em height, y
+ /// downwards, the top of the character cell at 0 and the baseline at /.
+ /// The point types are those of GDI+ (0 start, 1 line, 3 cubic Bezier, 0x80 closes the figure). Curves must be returned
+ /// as Bezier segments and not be flattened. Underline and strikeout are added as rectangles when requested.
+ ///
+ public abstract void GetGlyphPath(int codePoint, bool underline, bool strikeout, out PointF[] points, out byte[] types);
+ }
+
+ ///
+ /// The default : reads TrueType font files (.ttf, .ttc) itself, so it needs neither GDI nor any
+ /// other part of the operating system and works on Windows, Linux, macOS and in the browser. By default it uses the font
+ /// directories of the system; more files or directories can be added. OpenType fonts with CFF outlines are not supported
+ /// and are left out. A bold or italic face that a family does not have is not synthesized: the closest face is used.
+ ///
+ public class ManagedFontProvider : FontProvider
+ {
+ private sealed class Entry
+ {
+ public string FileName; // or
+ public byte[] Data;
+ public int Index; // in a collection
+ public string Family;
+ public bool Bold, Italic;
+ public ManagedFontFace Face; // loaded on demand
+ }
+ private readonly object locker = new object();
+ private readonly List directories = new List();
+ private readonly List added = new List();
+ private Dictionary> families; // upper case family name -> faces, built on demand
+
+ ///
+ /// Creates a provider for the fonts installed on the system.
+ ///
+ public ManagedFontProvider() : this(null, true)
+ {
+ }
+ ///
+ /// Creates a provider for the fonts in (with their subdirectories) and, if
+ /// is true, for the fonts installed on the system.
+ ///
+ public ManagedFontProvider(IEnumerable fontDirectories, bool includeSystemFonts)
+ {
+ if (fontDirectories != null) directories.AddRange(fontDirectories);
+ if (includeSystemFonts) directories.AddRange(SystemFontDirectories());
+ }
+ ///
+ /// The directories where the fonts of the system are installed on the current platform.
+ ///
+ public static IEnumerable SystemFontDirectories()
+ {
+ string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+ string fonts = Environment.GetFolderPath(Environment.SpecialFolder.Fonts); // Windows
+ if (!string.IsNullOrEmpty(fonts)) yield return fonts;
+ string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+ if (!string.IsNullOrEmpty(localAppData)) yield return Path.Combine(localAppData, "Microsoft", "Windows", "Fonts"); // fonts installed per user
+ yield return "/usr/share/fonts";
+ yield return "/usr/local/share/fonts";
+ yield return "/System/Library/Fonts";
+ yield return "/Library/Fonts";
+ if (!string.IsNullOrEmpty(home))
+ {
+ yield return Path.Combine(home, ".fonts");
+ yield return Path.Combine(home, ".local", "share", "fonts");
+ yield return Path.Combine(home, "Library", "Fonts");
+ }
+ }
+ ///
+ /// Adds the fonts of a font file (.ttf or .ttc).
+ ///
+ public void AddFontFile(string fileName)
+ {
+ TrueTypeFont[] fonts = TrueTypeFont.ReadHeaders(fileName);
+ lock (locker)
+ {
+ for (int i = 0; i < fonts.Length; ++i) AddEntry(new Entry { FileName = fileName, Index = i }, fonts[i], added);
+ families = null;
+ }
+ }
+ ///
+ /// Adds the fonts of a font file given as its content, e.g. an embedded resource.
+ ///
+ public void AddFontData(byte[] data)
+ {
+ TrueTypeFont[] fonts = TrueTypeFont.Load(data);
+ lock (locker)
+ {
+ for (int i = 0; i < fonts.Length; ++i) AddEntry(new Entry { Data = data, Index = i }, fonts[i], added);
+ families = null;
+ }
+ }
+ ///
+ public override string[] GetFamilyNames()
+ {
+ lock (locker)
+ {
+ return Families().Values.Select(l => l[0].Family).ToArray();
+ }
+ }
+ ///
+ public override FontFace GetFace(string familyName, bool bold, bool italic)
+ {
+ if (string.IsNullOrEmpty(familyName)) return null;
+ Entry best;
+ lock (locker)
+ {
+ if (!Families().TryGetValue(familyName.ToUpperInvariant(), out List 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)
+ ?? faces.FirstOrDefault(f => !f.Bold && !f.Italic)
+ ?? faces[0];
+ if (best.Face == null)
+ {
+ TrueTypeFont[] fonts = best.Data != null ? TrueTypeFont.Load(best.Data) : TrueTypeFont.Load(best.FileName);
+ best.Face = new ManagedFontFace(fonts[best.Index]);
+ }
+ }
+ return best.Face;
+ }
+
+ private static void AddEntry(Entry entry, TrueTypeFont font, List list)
+ {
+ if (!font.HasTrueTypeOutlines || string.IsNullOrEmpty(font.FamilyName)) return; // CFF or bitmap fonts cannot be used
+ entry.Family = font.FamilyName;
+ entry.Bold = font.IsBold;
+ entry.Italic = font.IsItalic;
+ list.Add(entry);
+ }
+ private Dictionary> Families()
+ {
+ if (families != null) return families;
+ List all = new List();
+ HashSet seen = new HashSet(StringComparer.OrdinalIgnoreCase);
+ foreach (string directory in directories)
+ {
+ if (!Directory.Exists(directory)) continue;
+ IEnumerable files;
+ try
+ {
+ files = Directory.EnumerateFiles(directory, "*", SearchOption.AllDirectories).ToArray();
+ }
+ catch (Exception e) when (e is IOException || e is UnauthorizedAccessException)
+ {
+ continue;
+ }
+ foreach (string file in files)
+ {
+ string ext = Path.GetExtension(file).ToLowerInvariant();
+ if (ext != ".ttf" && ext != ".ttc") continue;
+ if (!seen.Add(Path.GetFullPath(file))) continue;
+ try
+ {
+ TrueTypeFont[] fonts = TrueTypeFont.ReadHeaders(file);
+ for (int i = 0; i < fonts.Length; ++i) AddEntry(new Entry { FileName = file, Index = i }, fonts[i], all);
+ }
+ catch (Exception e) when (e is IOException || e is UnauthorizedAccessException || e is InvalidDataException || e is IndexOutOfRangeException || e is ArgumentException)
+ { // a broken or unreadable font file is left out
+ }
+ }
+ }
+ all.AddRange(added);
+ families = new Dictionary>();
+ foreach (Entry e in all)
+ {
+ string key = e.Family.ToUpperInvariant();
+ if (!families.TryGetValue(key, out List list)) families[key] = list = new List();
+ if (!list.Any(f => f.Bold == e.Bold && f.Italic == e.Italic)) list.Add(e); // the first file of a style wins
+ }
+ return families;
+ }
+ }
+
+ ///
+ /// A face read by the .
+ ///
+ internal sealed class ManagedFontFace : FontFace
+ {
+ private readonly TrueTypeFont font;
+ private readonly object locker = new object();
+ public ManagedFontFace(TrueTypeFont font)
+ {
+ this.font = font;
+ }
+ internal TrueTypeFont Font => font;
+ public override string FamilyName => font.FamilyName;
+ public override bool IsBold => font.IsBold;
+ public override bool IsItalic => font.IsItalic;
+ public override int EmHeight => font.UnitsPerEm;
+ public override int CellAscent => font.CellAscent;
+ public override int CellDescent => font.CellDescent;
+ public override int LineSpacing => font.LineSpacing;
+ public override double GetAdvanceWidth(int codePoint)
+ {
+ return font.GetAdvanceWidth(font.GetGlyphIndex(codePoint)) / (double)font.UnitsPerEm;
+ }
+ public override double GetKerning(int left, int right)
+ {
+ lock (locker)
+ { // the kerning pairs are read on the first call
+ return font.GetKerning(font.GetGlyphIndex(left), font.GetGlyphIndex(right)) / (double)font.UnitsPerEm;
+ }
+ }
+ public override void GetGlyphPath(int codePoint, bool underline, bool strikeout, out PointF[] points, out byte[] types)
+ {
+ font.GetPathData(font.GetGlyphIndex(codePoint), underline, strikeout, out points, out types);
+ }
+ }
+}
diff --git a/CADability/Text.cs b/CADability/Text.cs
index 1488360b..0e171b09 100644
--- a/CADability/Text.cs
+++ b/CADability/Text.cs
@@ -6,7 +6,6 @@
using System;
using System.Collections.Generic;
using System.Drawing;
-using System.Drawing.Drawing2D;
using System.Linq;
using System.Runtime.Serialization;
using System.Threading;
@@ -36,15 +35,7 @@ public ShowPropertyFont(Text t)
private MultipleChoiceProperty InitFontList()
{
- FontFamily[] families = FontFamily.Families;
- string[] choices = new string[families.Length];
- for (int i = 0; i < families.Length; i++)
- {
- choices[i] = families[i].Name;
- int emS = families[i].GetEmHeight(FontStyle.Regular);
- int asc = families[i].GetCellAscent(FontStyle.Regular);
- int desc = families[i].GetCellDescent(FontStyle.Regular);
- }
+ string[] choices = FontProvider.Current.GetFamilyNames().OrderBy(n => n, StringComparer.CurrentCultureIgnoreCase).ToArray();
MultipleChoiceProperty res = new MultipleChoiceProperty("Text.Font", choices, text.Font);
res.ValueChangedEvent += FontChanged;
return res;
@@ -404,9 +395,8 @@ string IGeoObjectShowProperty.GetContextMenuId()
internal class FontCache
{
- // Holds CompoundShapes for all characters requested so far
- // and a kerning table for all fonts used so far
- // Maintains a DeviceContext throughout its lifetime, where only one font is switched.
+ // Holds CompoundShapes for all characters requested so far
+ // and the font faces used so far, which come from FontProvider.Current (issue #412: no GDI here).
// The unit of the font is size 1.0, allowing easy work without scaling.
// However, the faces are only generated when a string is created.
// It might also be possible to keep a list of faces for each character in different sizes,
@@ -418,8 +408,8 @@ struct DictKey
{
public readonly string fontName;
public readonly int fontStyle;
- public readonly char c;
- public DictKey(string fontName, int fontStyle, char c)
+ public readonly int c; // a Unicode code point
+ public DictKey(string fontName, int fontStyle, int c)
{
this.fontName = fontName;
this.fontStyle = fontStyle;
@@ -427,7 +417,7 @@ public DictKey(string fontName, int fontStyle, char c)
}
public override int GetHashCode()
{
- return fontName.GetHashCode() ^ fontStyle ^ (int)c;
+ return fontName.GetHashCode() ^ fontStyle ^ c;
}
public override bool Equals(object obj)
{
@@ -462,28 +452,6 @@ public override bool Equals(object obj)
return false;
}
}
- struct KerningKey
- {
- public readonly string fontName;
- public readonly int fontStyle;
- public KerningKey(string fontName, int fontStyle)
- {
- this.fontName = fontName;
- this.fontStyle = fontStyle;
- }
- public override int GetHashCode()
- {
- return fontName.GetHashCode() ^ fontStyle;
- }
- public override bool Equals(object obj)
- {
- if (obj is KerningKey other)
- {
- return other.fontName == fontName && other.fontStyle == fontStyle;
- }
- return false;
- }
- }
struct DictVal
{
public CompoundShape shape; // hat die Splines statt der Polygone
@@ -508,8 +476,7 @@ struct FontCharacteristics
readonly Dictionary cache; // Cache von CompundShapes und Breiten für einzelne zeichen
readonly Dictionary centerLineCache; // Cache für die CenterLines, Größe 1
readonly Dictionary fontCharacteristicsCache; // Cache für die FontCharacteristics
- readonly Dictionary> kerning; // Kerningtabellen
- readonly IntPtr hDC; // fester DeviceContext (für die ganze Lebensdauer)
+ readonly Dictionary faces; // the faces of FontProvider.Current, per font name and bold/italic
public FontCache()
{
FontPrecision = Settings.GlobalSettings.GetIntValue("Font.Precision", 1); // |grob|mittel|fein
@@ -517,76 +484,58 @@ public FontCache()
cache = new Dictionary();
centerLineCache = new Dictionary();
fontCharacteristicsCache = new Dictionary();
- kerning = new Dictionary>();
- hDC = Gdi.CreateCompatibleDC(IntPtr.Zero);
+ faces = new Dictionary();
}
- ~FontCache()
+ ///
+ /// Returns the face of for a font name and a font style (the flags of
+ /// System.Drawing.FontStyle: 1 bold, 2 italic, 4 underline, 8 strikeout). An unknown font name gives the default face of
+ /// the provider.
+ ///
+ /// when the provider has no font at all
+ internal FontFace GetFace(string fontName, int fontStyle)
{
- // the constructor throws where there is no GDI (e.g. on Linux), and an exception in a finalizer ends the process
- if (hDC != IntPtr.Zero) Gdi.DeleteDC(hDC);
+ bool bold = (fontStyle & 1) != 0, italic = (fontStyle & 2) != 0;
+ FontKey key = new FontKey(fontName ?? "", fontStyle & 3);
+ lock (faces)
+ {
+ if (faces.TryGetValue(key, out FontFace face)) return face;
+ FontProvider provider = FontProvider.Current;
+ face = provider.GetFace(fontName, bold, italic) ?? provider.GetDefaultFace(bold, italic);
+ if (face == null) throw new InvalidOperationException("No font is available for texts. Install TrueType fonts, add font files to a ManagedFontProvider or set FontProvider.Current.");
+ faces[key] = face;
+ return face;
+ }
}
- // The caches and the device context are shared by all threads that measure or paint text, and Clear may empty the
- // caches at any time: every access goes through lock (this). The lock is reentrant, Get and GetCenterLines call GetOutline2D.
- public Path2D[] GetOutline2D(string fontName, int fontStyle, char c, out double width)
+ ///
+ /// Like , but returns null instead of throwing when there is no font at all: painting and the
+ /// extent of a text leave the text out then, instead of failing.
+ ///
+ internal FontFace TryGetFace(string fontName, int fontStyle)
+ {
+ try
+ {
+ return GetFace(fontName, fontStyle);
+ }
+ catch (InvalidOperationException)
+ {
+ return null;
+ }
+ }
+ // The caches are shared by all threads that measure or paint text, and Clear may empty them at any time: every access
+ // goes through lock (this). The lock is reentrant, Get and GetCenterLines call GetOutline2D. The faces are locked
+ // separately and always after this, so the two locks are taken in the same order everywhere.
+ public Path2D[] GetOutline2D(string fontName, int fontStyle, int c, out double width)
{
lock (this)
{
- return ReadOutline2D(fontName, fontStyle, c, out width);
- }
- }
- private Path2D[] ReadOutline2D(string fontName, int fontStyle, char c, out double width)
- {
- using GraphicsPath path = new GraphicsPath();
- // FontFamily wraps an unmanaged GDI+ object: dispose it at the end of the block instead of
- // leaving it to the finalizer (this code runs for every text on every repaint)
- using FontFamily ff = Text.FontFamilyNames.Contains(fontName.ToUpper()) ? new FontFamily(fontName) : new FontFamily(System.Drawing.Text.GenericFontFamilies.SansSerif);
- StringFormat sf = StringFormat.GenericTypographic.Clone() as StringFormat;
- sf.LineAlignment = StringAlignment.Near;
- sf.Alignment = StringAlignment.Near;
- if (!ff.IsStyleAvailable((FontStyle)fontStyle))
- {
- if (ff.IsStyleAvailable(FontStyle.Regular)) fontStyle = (int)FontStyle.Regular;
- if (ff.IsStyleAvailable(FontStyle.Bold)) fontStyle = (int)FontStyle.Bold;
- if (ff.IsStyleAvailable(FontStyle.Italic)) fontStyle = (int)FontStyle.Italic;
- if (ff.IsStyleAvailable(FontStyle.Strikeout)) fontStyle = (int)FontStyle.Strikeout;
- if (ff.IsStyleAvailable(FontStyle.Underline)) fontStyle = (int)FontStyle.Underline;
- }
- int fs = fontStyle;
- int em = ff.GetEmHeight((FontStyle)fs);
- if (em == 0) em = 1000;
- using Font font = new Font(ff, em, (FontStyle)fs, GraphicsUnit.Pixel);
- IntPtr hfont = font.ToHfont();
- IntPtr oldfont = Gdi.SelectObject(hDC, hfont);
- Gdi.ABC[] abc = new Gdi.ABC[1];
- Gdi.GetCharABCWidths(hDC, (uint)c, (uint)c, abc);
- //int[] widths = new int[1]; // liefert das selbe wie GetCharABCWidths, nur die Summe halt.
- //Gdi.GetCharWidth32(hDC, (uint)c, (uint)c, widths);
- if (!kerning.ContainsKey(new KerningKey(fontName, fontStyle)))
- {
- KerningKey kk = new KerningKey(fontName, fontStyle);
- Dictionary<(char Left, char Right), double> pairs = new Dictionary<(char Left, char Right), double>();
- kerning[kk] = pairs;
- int num = Gdi.GetKerningPairs(hDC, 0, null);
- if (num > 0)
- {
- Gdi.KERNINGPAIR[] kp = new Gdi.KERNINGPAIR[num];
- int ok = Gdi.GetKerningPairs(hDC, num, kp);
- for (int i = 0; i < kp.Length; ++i)
- {
- pairs[((char)kp[i].wFirst, (char)kp[i].wSecond)] = kp[i].iKernAmount / (double)em;
- }
- }
+ FontFace face = GetFace(fontName, fontStyle);
+ width = face.GetAdvanceWidth(c);
+ face.GetGlyphPath(c, (fontStyle & 4) != 0, (fontStyle & 8) != 0, out PointF[] points, out byte[] types);
+ if (points.Length == 0) return new Path2D[0];
+ return GlyphShapeBuilder.PathDataToPath2D(points, types, FontPrecision);
}
- Gdi.SelectObject(hDC, oldfont);
- Gdi.DeleteObject(hfont);
-
- width = (abc[0].abcA + abc[0].abcB + abc[0].abcC) / (double)em;
-
- path.AddString(c.ToString(), ff, fs, 1.0f, new PointF(0.0f, 0.0f), sf);
- if (path.PointCount == 0) return new Path2D[0];
- return GlyphShapeBuilder.PathDataToPath2D(path.PathPoints, path.PathTypes, FontPrecision);
}
- public CompoundShape Get(string font, int fontStyle, char c, out double width)
+ public CompoundShape Get(string font, int fontStyle, int c, out double width)
{ // soll verschwinden!!
DictVal found;
lock (this)
@@ -605,20 +554,14 @@ public CompoundShape Get(string font, int fontStyle, char c, out double width)
width = found.width;
return found.shape;
}
- public double GetKerning(string font, int fontStyle, char a, char b)
+ public double GetKerning(string font, int fontStyle, int a, int b)
{
- Dictionary<(char Left, char Right), double> pairs;
- double res = 0.0;
lock (this)
{
- if (kerning.TryGetValue(new KerningKey(font, fontStyle), out pairs))
- {
- pairs.TryGetValue((a, b), out res);
- }
+ return GetFace(font, fontStyle).GetKerning(a, b);
}
- return res;
}
- public IPaintTo3DList Get(string font, int fontStyle, char c, out double width, IPaintTo3D paintTo3D)
+ public IPaintTo3DList Get(string font, int fontStyle, int c, out double width, IPaintTo3D paintTo3D)
{
bool useLists = true;
if (paintTo3D != null) useLists = !paintTo3D.IsBitmap;
@@ -811,17 +754,17 @@ public void Clear()
cache.Clear();
centerLineCache.Clear();
fontCharacteristicsCache.Clear();
- kerning.Clear();
+ lock (faces) faces.Clear();
}
}
- public GeoObjectList GetCenterLines(string font, int fontStyle, char c, out double width)
+ public GeoObjectList GetCenterLines(string font, int fontStyle, int c, out double width)
{
lock (this)
{
return ReadCenterLines(font, fontStyle, c, out width);
}
}
- private GeoObjectList ReadCenterLines(string font, int fontStyle, char c, out double width)
+ private GeoObjectList ReadCenterLines(string font, int fontStyle, int c, out double width)
{
CenterLineVal found;
if (!centerLineCache.TryGetValue(new DictKey(font, fontStyle, c), out found))
@@ -996,9 +939,16 @@ public enum LineAlignMode
private LineAlignMode lineAlignment;
// private IGeoObject[] cachedDisplayItems;
private static HashSet fontFamilyNames;
+ ///
+ /// The upper case names of the font families of .
+ ///
internal static HashSet FontFamilyNames =>
LazyInitializer.EnsureInitialized(ref fontFamilyNames, () =>
- new HashSet(FontFamily.Families.Select(f => f.Name.ToUpper())));
+ new HashSet(FontProvider.Current.GetFamilyNames().Select(f => f.ToUpper())));
+ internal static void ResetFontFamilyNames()
+ {
+ fontFamilyNames = null;
+ }
#region polymorph construction
///
@@ -1057,37 +1007,29 @@ private void calcExtent()
{
try
{
- // FontFamily wraps an unmanaged GDI+ object: dispose it at the end of the block instead of
- // leaving it to the finalizer (this code runs for every text on every repaint)
- using FontFamily ff = FontFamilyNames.Contains(fontName.ToUpper()) ? new FontFamily(fontName) : new FontFamily(System.Drawing.Text.GenericFontFamilies.SansSerif);
- FontStyle fs = fontStyle;
- if (!ff.IsStyleAvailable(fs))
- {
- if (ff.IsStyleAvailable(FontStyle.Regular)) fs = FontStyle.Regular;
- else if (ff.IsStyleAvailable(FontStyle.Bold)) fs = FontStyle.Bold;
- else if (ff.IsStyleAvailable(FontStyle.Italic)) fs = FontStyle.Italic;
- else if (ff.IsStyleAvailable(FontStyle.Strikeout)) fs = FontStyle.Strikeout;
- else if (ff.IsStyleAvailable(FontStyle.Underline)) fs = FontStyle.Underline; // irgend einen muss es ja wohl geben, oder?
- }
- int em = ff.GetEmHeight(fs);
- int asc = ff.GetCellAscent(fs);
- int desc = ff.GetCellDescent(fs);
- int lsp = ff.GetLineSpacing(fs);
- double dbg = (double)(asc + desc) / (double)lsp;
+ int fs = (int)fontStyle;
+ FontFace ff = FontCache.GlobalFontCache.GetFace(fontName, fs); // FontProvider.Current, see issue #412
+ int em = ff.EmHeight;
+ int asc = ff.CellAscent;
+ int desc = ff.CellDescent;
+ int lsp = ff.LineSpacing;
// asc+desc kann mehr als em sein. ich nehme jedoch an, dass em für die Größe ausschlaggebend ist
// wenn man em als 1 annimmt, dann muss man um desc/em nach unten verschieben um auf die Grundlinie zu kommen
emSizeFactor = (double)(asc + desc) / (double)em;
emAscDescFactor = (double)(desc) / (double)asc;
emDescDiff = (double)(em - asc) / (double)em;
this.desc = (double)(desc) / (double)em;
- // the bitmap only serves as a device for measuring; it must be disposed as well, otherwise
- // 400 KB of unmanaged GDI+ memory stay alive per measured text until the finalizer runs
- using (Bitmap measureBitmap = new Bitmap(1000, 100))
- using (Graphics graphics = Graphics.FromImage(measureBitmap))
- using (Font font = new Font(fontName, (float)(glyphDirection.Length), GraphicsUnit.Pixel))
+ // the size of the text string with the font size glyphDirection.Length: the advance widths and the kerning
+ // of the characters and the line spacing (this used to be Graphics.MeasureString of GDI+)
+ double width = 0.0;
+ int[] chars = CodePoints(textString);
+ for (int i = 0; i < chars.Length; ++i)
{
- sizeExtent = graphics.MeasureString(textString, font);
+ width += ff.GetAdvanceWidth(chars[i]);
+ if (i > 0) width += ff.GetKerning(chars[i - 1], chars[i]);
}
+ double size = glyphDirection.Length;
+ sizeExtent = new SizeF((float)(width * size), (float)(size * lsp / em));
isValidExtent = true;
}
catch (Exception e)
@@ -1223,6 +1165,24 @@ private void AddToPath2D(List addto, List points, bool spl
}
points.RemoveRange(0, points.Count - 1); // den letzten als ersten drinlassen
}
+ ///
+ /// The characters of as Unicode code points: a surrogate pair (e.g. an emoji) is one character.
+ /// A lone surrogate is kept as the char it is.
+ ///
+ internal static int[] CodePoints(string s)
+ {
+ List res = new List(s.Length);
+ for (int i = 0; i < s.Length; ++i)
+ {
+ if (char.IsHighSurrogate(s[i]) && i + 1 < s.Length && char.IsLowSurrogate(s[i + 1]))
+ {
+ res.Add(char.ConvertToUtf32(s[i], s[i + 1]));
+ ++i;
+ }
+ else res.Add(s[i]);
+ }
+ return res.ToArray();
+ }
// Returns the string to display: textString, with additional line breaks inserted at word
// boundaries when ColumnWidth demands wrapping. Character widths are measured with the same
// FontCache the layout loops use, so the wrapped lines fit the column exactly.
@@ -1255,9 +1215,10 @@ private string GetDisplayString(int fs)
}
private void AppendWrappedParagraph(System.Text.StringBuilder result, string paragraph, double normalizedWidth, int fs)
{
- double MeasureWord(string w)
+ double MeasureWord(string word)
{
double total = 0.0;
+ int[] w = CodePoints(word);
for (int i = 0; i < w.Length; i++)
{
FontCache.GlobalFontCache.Get(fontName, fs, w[i], out double cw, null);
@@ -1306,22 +1267,12 @@ public virtual GeoObjectList GetOutline()
if (Bold) fs |= (int)FontStyle.Bold;
if (Strikeout) fs |= (int)FontStyle.Strikeout;
PlaneSurface pls = new PlaneSurface(location, lineDirection, glyphDirection, lineDirection ^ glyphDirection);
- // FontFamily wraps an unmanaged GDI+ object: dispose it at the end of the block instead of
- // leaving it to the finalizer (this code runs for every text on every repaint)
- using FontFamily ff = FontFamilyNames.Contains(fontName.ToUpper()) ? new FontFamily(fontName) : new FontFamily(System.Drawing.Text.GenericFontFamilies.SansSerif);
- if (!ff.IsStyleAvailable((FontStyle)fs))
- {
- if (ff.IsStyleAvailable(FontStyle.Regular)) fs = (int)FontStyle.Regular;
- else if (ff.IsStyleAvailable(FontStyle.Bold)) fs = (int)FontStyle.Bold;
- else if (ff.IsStyleAvailable(FontStyle.Italic)) fs = (int)FontStyle.Italic;
- else if (ff.IsStyleAvailable(FontStyle.Strikeout)) fs = (int)FontStyle.Strikeout;
- else if (ff.IsStyleAvailable(FontStyle.Underline)) fs = (int)FontStyle.Underline; // irgend einen muss es ja wohl geben, oder?
- }
- string txt = GetDisplayString(fs);
- int em = ff.GetEmHeight((FontStyle)fs);
- int ls = ff.GetLineSpacing((FontStyle)fs);
- int dc = ff.GetCellDescent((FontStyle)fs);
- int ac = ff.GetCellAscent((FontStyle)fs);
+ FontFace ff = FontCache.GlobalFontCache.GetFace(fontName, fs); // FontProvider.Current, see issue #412
+ int[] txt = CodePoints(GetDisplayString(fs));
+ int em = ff.EmHeight;
+ int ls = ff.LineSpacing;
+ int dc = ff.CellDescent;
+ int ac = ff.CellAscent;
double lsp = ls / em;
if (lineSpacing > 0.0) lsp = lineSpacing; // explicit baseline distance as factor of the text size
double dx = 0.0;
@@ -1463,22 +1414,12 @@ public CompoundShape[] GetShapes()
if (Bold) fs |= (int)FontStyle.Bold;
if (Strikeout) fs |= (int)FontStyle.Strikeout;
PlaneSurface pls = new PlaneSurface(location, lineDirection, glyphDirection, lineDirection ^ glyphDirection);
- // FontFamily wraps an unmanaged GDI+ object: dispose it at the end of the block instead of
- // leaving it to the finalizer (this code runs for every text on every repaint)
- using FontFamily ff = FontFamilyNames.Contains(fontName.ToUpper()) ? new FontFamily(fontName) : new FontFamily(System.Drawing.Text.GenericFontFamilies.SansSerif);
- if (!ff.IsStyleAvailable((FontStyle)fs))
- {
- if (ff.IsStyleAvailable(FontStyle.Regular)) fs = (int)FontStyle.Regular;
- else if (ff.IsStyleAvailable(FontStyle.Bold)) fs = (int)FontStyle.Bold;
- else if (ff.IsStyleAvailable(FontStyle.Italic)) fs = (int)FontStyle.Italic;
- else if (ff.IsStyleAvailable(FontStyle.Strikeout)) fs = (int)FontStyle.Strikeout;
- else if (ff.IsStyleAvailable(FontStyle.Underline)) fs = (int)FontStyle.Underline; // irgend einen muss es ja wohl geben, oder?
- }
- string txt = GetDisplayString(fs);
- int em = ff.GetEmHeight((FontStyle)fs);
- int ls = ff.GetLineSpacing((FontStyle)fs);
- int dc = ff.GetCellDescent((FontStyle)fs);
- int ac = ff.GetCellAscent((FontStyle)fs);
+ FontFace ff = FontCache.GlobalFontCache.GetFace(fontName, fs); // FontProvider.Current, see issue #412
+ int[] txt = CodePoints(GetDisplayString(fs));
+ int em = ff.EmHeight;
+ int ls = ff.LineSpacing;
+ int dc = ff.CellDescent;
+ int ac = ff.CellAscent;
double lsp = ls / em;
if (lineSpacing > 0.0) lsp = lineSpacing; // explicit baseline distance as factor of the text size
double dx = 0.0;
@@ -1606,22 +1547,12 @@ public virtual GeoObjectList GetCenterline()
if (Bold) fs |= (int)FontStyle.Bold;
if (Strikeout) fs |= (int)FontStyle.Strikeout;
PlaneSurface pls = new PlaneSurface(location, lineDirection, glyphDirection, lineDirection ^ glyphDirection);
- // FontFamily wraps an unmanaged GDI+ object: dispose it at the end of the block instead of
- // leaving it to the finalizer (this code runs for every text on every repaint)
- using FontFamily ff = FontFamilyNames.Contains(fontName.ToUpper()) ? new FontFamily(fontName) : new FontFamily(System.Drawing.Text.GenericFontFamilies.SansSerif);
- if (!ff.IsStyleAvailable((FontStyle)fs))
- {
- if (ff.IsStyleAvailable(FontStyle.Regular)) fs = (int)FontStyle.Regular;
- else if (ff.IsStyleAvailable(FontStyle.Bold)) fs = (int)FontStyle.Bold;
- else if (ff.IsStyleAvailable(FontStyle.Italic)) fs = (int)FontStyle.Italic;
- else if (ff.IsStyleAvailable(FontStyle.Strikeout)) fs = (int)FontStyle.Strikeout;
- else if (ff.IsStyleAvailable(FontStyle.Underline)) fs = (int)FontStyle.Underline; // irgend einen muss es ja wohl geben, oder?
- }
- string txt = GetDisplayString(fs);
- int em = ff.GetEmHeight((FontStyle)fs);
- int ls = ff.GetLineSpacing((FontStyle)fs);
- int dc = ff.GetCellDescent((FontStyle)fs);
- int ac = ff.GetCellAscent((FontStyle)fs);
+ FontFace ff = FontCache.GlobalFontCache.GetFace(fontName, fs); // FontProvider.Current, see issue #412
+ int[] txt = CodePoints(GetDisplayString(fs));
+ int em = ff.EmHeight;
+ int ls = ff.LineSpacing;
+ int dc = ff.CellDescent;
+ int ac = ff.CellAscent;
double lsp = ls / em;
if (lineSpacing > 0.0) lsp = lineSpacing; // explicit baseline distance as factor of the text size
double dx = 0.0;
@@ -2010,22 +1941,13 @@ private void CalcExtent()
{ // bei sehr kleinen lineDirection u.s.w. gibts hier ein Problem
pls = new PlaneSurface(location, GeoVector.XAxis, GeoVector.YAxis, GeoVector.ZAxis);
}
- // FontFamily wraps an unmanaged GDI+ object: dispose it at the end of the block instead of
- // leaving it to the finalizer (this code runs for every text on every repaint)
- using FontFamily ff = FontFamilyNames.Contains(fontName.ToUpper()) ? new FontFamily(fontName) : new FontFamily(System.Drawing.Text.GenericFontFamilies.SansSerif);
- if (!ff.IsStyleAvailable((FontStyle)fs))
- {
- if (ff.IsStyleAvailable(FontStyle.Regular)) fs = (int)FontStyle.Regular;
- else if (ff.IsStyleAvailable(FontStyle.Bold)) fs = (int)FontStyle.Bold;
- else if (ff.IsStyleAvailable(FontStyle.Italic)) fs = (int)FontStyle.Italic;
- else if (ff.IsStyleAvailable(FontStyle.Strikeout)) fs = (int)FontStyle.Strikeout;
- else if (ff.IsStyleAvailable(FontStyle.Underline)) fs = (int)FontStyle.Underline; // irgend einen muss es ja wohl geben, oder?
- }
- string txt = GetDisplayString(fs);
- int em = ff.GetEmHeight((FontStyle)fs);
- int ls = ff.GetLineSpacing((FontStyle)fs);
- int dc = ff.GetCellDescent((FontStyle)fs);
- int ac = ff.GetCellAscent((FontStyle)fs);
+ FontFace ff = FontCache.GlobalFontCache.TryGetFace(fontName, fs); // FontProvider.Current, see issue #412
+ if (ff == null) return; // no font at all: nothing to paint or measure
+ int[] txt = CodePoints(GetDisplayString(fs));
+ int em = ff.EmHeight;
+ int ls = ff.LineSpacing;
+ int dc = ff.CellDescent;
+ int ac = ff.CellAscent;
double lsp;
if (em > 0) lsp = (double)ls / em;
else lsp = 1.0;
@@ -2650,25 +2572,15 @@ public override void PrePaintTo3D(IPaintTo3D paintTo3D)
if (Strikeout) fs |= (int)FontStyle.Strikeout;
List res = new List();
PlaneSurface pls = new PlaneSurface(location, lineDirection, glyphDirection, lineDirection ^ glyphDirection);
- // FontFamily wraps an unmanaged GDI+ object: dispose it at the end of the block instead of
- // leaving it to the finalizer (this code runs for every text on every repaint)
- using FontFamily ff = FontFamilyNames.Contains(fontName.ToUpper()) ? new FontFamily(fontName) : new FontFamily(System.Drawing.Text.GenericFontFamilies.SansSerif);
- if (!ff.IsStyleAvailable((FontStyle)fs))
- {
- if (ff.IsStyleAvailable(FontStyle.Regular)) fs = (int)FontStyle.Regular;
- else if (ff.IsStyleAvailable(FontStyle.Bold)) fs = (int)FontStyle.Bold;
- else if (ff.IsStyleAvailable(FontStyle.Italic)) fs = (int)FontStyle.Italic;
- else if (ff.IsStyleAvailable(FontStyle.Strikeout)) fs = (int)FontStyle.Strikeout;
- else if (ff.IsStyleAvailable(FontStyle.Underline)) fs = (int)FontStyle.Underline; // there must be one
- }
- int em = ff.GetEmHeight((FontStyle)fs);
- int ls = ff.GetLineSpacing((FontStyle)fs);
- int dc = ff.GetCellDescent((FontStyle)fs);
- int ac = ff.GetCellAscent((FontStyle)fs);
- for (int i = 0; i < textString.Length; ++i)
+ FontFace ff = FontCache.GlobalFontCache.TryGetFace(fontName, fs); // FontProvider.Current, see issue #412
+ if (ff == null) return; // no font at all: nothing to paint or measure
+ int em = ff.EmHeight;
+ int ls = ff.LineSpacing;
+ int dc = ff.CellDescent;
+ int ac = ff.CellAscent;
+ foreach (int c in CodePoints(textString))
{
- double width;
- FontCache.GlobalFontCache.Get(fontName, fs, textString[i], out width, paintTo3D);
+ FontCache.GlobalFontCache.Get(fontName, fs, c, out double width, paintTo3D);
}
}
else
@@ -2719,22 +2631,13 @@ public override void PaintTo3D(IPaintTo3D paintTo3D)
if (Strikeout) fs |= (int)FontStyle.Strikeout;
List res = new List();
PlaneSurface pls = new PlaneSurface(location, lineDirection, glyphDirection, lineDirection ^ glyphDirection);
- // FontFamily wraps an unmanaged GDI+ object: dispose it at the end of the block instead of
- // leaving it to the finalizer (this code runs for every text on every repaint)
- using FontFamily ff = FontFamilyNames.Contains(fontName.ToUpper()) ? new FontFamily(fontName) : new FontFamily(System.Drawing.Text.GenericFontFamilies.SansSerif);
- if (!ff.IsStyleAvailable((FontStyle)fs))
- {
- if (ff.IsStyleAvailable(FontStyle.Regular)) fs = (int)FontStyle.Regular;
- else if (ff.IsStyleAvailable(FontStyle.Bold)) fs = (int)FontStyle.Bold;
- else if (ff.IsStyleAvailable(FontStyle.Italic)) fs = (int)FontStyle.Italic;
- else if (ff.IsStyleAvailable(FontStyle.Strikeout)) fs = (int)FontStyle.Strikeout;
- else if (ff.IsStyleAvailable(FontStyle.Underline)) fs = (int)FontStyle.Underline; // irgend einen muss es ja wohl geben, oder?
- }
- string txt = GetDisplayString(fs);
- int em = ff.GetEmHeight((FontStyle)fs);
- int ls = ff.GetLineSpacing((FontStyle)fs);
- int dc = ff.GetCellDescent((FontStyle)fs);
- int ac = ff.GetCellAscent((FontStyle)fs);
+ FontFace ff = FontCache.GlobalFontCache.TryGetFace(fontName, fs); // FontProvider.Current, see issue #412
+ if (ff == null) return; // no font at all: nothing to paint or measure
+ int[] txt = CodePoints(GetDisplayString(fs));
+ int em = ff.EmHeight;
+ int ls = ff.LineSpacing;
+ int dc = ff.CellDescent;
+ int ac = ff.CellAscent;
double lsp = ls / em;
if (lineSpacing > 0.0) lsp = lineSpacing; // explicit baseline distance as factor of the text size
if (lineSpacing > 0.0) lsp = lineSpacing; // explicit baseline distance as factor of the text size
diff --git a/CADability/TrueTypeFont.cs b/CADability/TrueTypeFont.cs
index 9ad4c236..7428e83d 100644
--- a/CADability/TrueTypeFont.cs
+++ b/CADability/TrueTypeFont.cs
@@ -64,6 +64,20 @@ internal sealed class TrueTypeFont
public int WinAscent { get; }
public int WinDescent { get; }
///
+ /// The top of the underline above the baseline in font units (negative: below), from the "post" table.
+ ///
+ public int UnderlinePosition { get; }
+ public int UnderlineThickness { get; }
+ ///
+ /// The top of the strikeout line above the baseline in font units, from the "OS/2" table.
+ ///
+ public int StrikeoutPosition { get; }
+ public int StrikeoutSize { get; }
+ ///
+ /// True for a font read by : it knows its names, style and metrics, but has no glyphs.
+ ///
+ public bool IsHeaderOnly { get; private set; }
+ ///
/// False for an OpenType font with CFF outlines and for a bitmap font, whose glyphs cannot be read.
///
public bool HasTrueTypeOutlines { get; }
@@ -123,14 +137,26 @@ private TrueTypeFont(byte[] data, int offset)
bool bold = (macStyle & 1) != 0, italic = (macStyle & 2) != 0;
WinAscent = Ascender;
WinDescent = -Descender;
+ // defaults like the ones GDI uses when the tables are missing
+ UnderlineThickness = Math.Max(1, UnitsPerEm / 14);
+ UnderlinePosition = -UnitsPerEm / 10;
+ StrikeoutSize = UnderlineThickness;
+ StrikeoutPosition = UnitsPerEm / 4;
if (tables.TryGetValue("OS/2", out var os2) && os2.length >= 78)
{
+ StrikeoutSize = S16(os2.offset + 26);
+ StrikeoutPosition = S16(os2.offset + 28);
int fsSelection = U16(os2.offset + 62);
italic = (fsSelection & 1) != 0;
bold = (fsSelection & 0x20) != 0;
WinAscent = U16(os2.offset + 74);
WinDescent = U16(os2.offset + 76);
}
+ if (tables.TryGetValue("post", out var post) && post.length >= 12)
+ {
+ UnderlinePosition = S16(post.offset + 8);
+ UnderlineThickness = S16(post.offset + 10);
+ }
IsBold = bold;
IsItalic = italic;
ReadNames(out string family, out string subfamily, out string full, out string typographicFamily);
@@ -160,6 +186,91 @@ public static TrueTypeFont[] Load(byte[] data)
return new TrueTypeFont[] { new TrueTypeFont(data, 0) };
}
///
+ /// Reads the names, styles and metrics of all fonts of a font file without reading the whole file: only the tables
+ /// "head", "hhea", "maxp", "OS/2", "post" and "name" are read. This is how a directory with hundreds of fonts is indexed
+ /// quickly. The fonts returned have set; load the file with to
+ /// get the glyphs.
+ ///
+ public static TrueTypeFont[] ReadHeaders(string fileName)
+ {
+ using (FileStream stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read))
+ {
+ byte[] head = ReadBytes(stream, 0, 12);
+ if (head[0] == 't' && head[1] == 't' && head[2] == 'c' && head[3] == 'f')
+ {
+ int count = (int)ReadU32(head, 8);
+ if (count <= 0 || count > 10000) throw new InvalidDataException("invalid font collection");
+ byte[] offsets = ReadBytes(stream, 12, 4 * count);
+ TrueTypeFont[] res = new TrueTypeFont[count];
+ for (int i = 0; i < count; ++i) res[i] = ReadHeader(stream, ReadU32(offsets, 4 * i));
+ return res;
+ }
+ return new TrueTypeFont[] { ReadHeader(stream, 0) };
+ }
+ }
+ private static TrueTypeFont ReadHeader(FileStream stream, uint offset)
+ {
+ // a small font made of the needed tables only, with the same table directory layout, so that the constructor reads it
+ byte[] directory = ReadBytes(stream, offset, 12);
+ int numTables = (directory[4] << 8) | directory[5];
+ if (numTables > 1000) throw new InvalidDataException("invalid table directory");
+ byte[] records = ReadBytes(stream, offset + 12, 16 * numTables);
+ string[] needed = { "head", "hhea", "maxp", "OS/2", "post", "name" };
+ List<(byte[] record, byte[] content)> copied = new List<(byte[], byte[])>();
+ for (int i = 0; i < numTables; ++i)
+ {
+ string tag = Encoding.ASCII.GetString(records, 16 * i, 4);
+ byte[] record = new byte[16];
+ Array.Copy(records, 16 * i, record, 0, 16);
+ if (Array.IndexOf(needed, tag) >= 0)
+ {
+ uint length = ReadU32(records, 16 * i + 12);
+ if (length > stream.Length) throw new InvalidDataException("table " + tag + " exceeds the file");
+ copied.Add((record, ReadBytes(stream, ReadU32(records, 16 * i + 8), (int)length)));
+ }
+ else if (tag == "glyf" || tag == "loca")
+ { // only their presence matters (HasTrueTypeOutlines)
+ for (int k = 8; k < 16; ++k) record[k] = 0;
+ copied.Add((record, new byte[0]));
+ }
+ }
+ int size = 12 + 16 * copied.Count;
+ foreach (var (_, content) in copied) size += content.Length;
+ byte[] data = new byte[size];
+ Array.Copy(directory, data, 12);
+ data[4] = (byte)(copied.Count >> 8);
+ data[5] = (byte)copied.Count;
+ int pos = 12 + 16 * copied.Count;
+ for (int i = 0; i < copied.Count; ++i)
+ {
+ byte[] record = copied[i].record, content = copied[i].content;
+ if (content.Length > 0)
+ {
+ record[8] = (byte)(pos >> 24); record[9] = (byte)(pos >> 16); record[10] = (byte)(pos >> 8); record[11] = (byte)pos;
+ }
+ Array.Copy(record, 0, data, 12 + 16 * i, 16);
+ Array.Copy(content, 0, data, pos, content.Length);
+ pos += content.Length;
+ }
+ TrueTypeFont res = new TrueTypeFont(data, 0);
+ res.IsHeaderOnly = true;
+ return res;
+ }
+ private static byte[] ReadBytes(FileStream stream, long offset, int count)
+ {
+ if (count < 0 || offset + count > stream.Length) throw new InvalidDataException("unexpected end of the font file");
+ byte[] res = new byte[count];
+ stream.Seek(offset, SeekOrigin.Begin);
+ int read = 0;
+ while (read < count)
+ {
+ int n = stream.Read(res, read, count - read);
+ if (n <= 0) throw new InvalidDataException("unexpected end of the font file");
+ read += n;
+ }
+ return res;
+ }
+ ///
/// Reads all fonts of the font file , see .
///
public static TrueTypeFont[] Load(string fileName)
@@ -213,6 +324,7 @@ public int GetKerning(int leftGlyph, int rightGlyph)
public List GetContours(int glyph)
{
if (!HasTrueTypeOutlines) throw new NotSupportedException("The outlines of the font \"" + FullName + "\" cannot be read: they are in CFF format or the font has bitmaps only");
+ if (IsHeaderOnly) throw new InvalidOperationException("The font \"" + FullName + "\" was read by ReadHeaders and has no glyphs, load it with Load");
List res = new List();
AddContours(glyph, res, 0);
return res;
@@ -226,6 +338,14 @@ public List GetContours(int glyph)
/// 0 start, 1 line, 3 Bezier, 0x80 closes the figure. This is the input of GlyphShapeBuilder.PathDataToPath2D.
///
public void GetPathData(int glyph, out PointF[] points, out byte[] types)
+ {
+ GetPathData(glyph, false, false, out points, out types);
+ }
+ ///
+ /// Like , and adds the underline and the strikeout line as
+ /// rectangles over the advance width of the glyph, as GDI+ does for an underlined or struck out font.
+ ///
+ public void GetPathData(int glyph, bool underline, bool strikeout, out PointF[] points, out byte[] types)
{
List pts = new List();
List tps = new List();
@@ -308,6 +428,17 @@ public void GetPathData(int glyph, out PointF[] points, out byte[] types)
}
tps[tps.Count - 1] |= 0x80;
}
+ void AddBar(int top, int thickness)
+ {
+ if (thickness <= 0) return;
+ double width = GetAdvanceWidth(glyph);
+ pts.Add(Map(0, top)); tps.Add(0);
+ pts.Add(Map(width, top)); tps.Add(1);
+ pts.Add(Map(width, top - thickness)); tps.Add(1);
+ pts.Add(Map(0, top - thickness)); tps.Add(0x81);
+ }
+ if (underline) AddBar(UnderlinePosition, UnderlineThickness);
+ if (strikeout) AddBar(StrikeoutPosition, StrikeoutSize);
points = pts.ToArray();
types = tps.ToArray();
}
diff --git a/tests/CADability.ImportTests/CADability.ImportTests.csproj b/tests/CADability.ImportTests/CADability.ImportTests.csproj
index 3ab5a792..bdea3af5 100644
--- a/tests/CADability.ImportTests/CADability.ImportTests.csproj
+++ b/tests/CADability.ImportTests/CADability.ImportTests.csproj
@@ -13,6 +13,13 @@
+
+
+
+ PreserveNewest
+
+
+
PreserveNewest
diff --git a/tests/CADability.ImportTests/DxfDimensionTest.cs b/tests/CADability.ImportTests/DxfDimensionTest.cs
index 32b4b706..4a37aa85 100644
--- a/tests/CADability.ImportTests/DxfDimensionTest.cs
+++ b/tests/CADability.ImportTests/DxfDimensionTest.cs
@@ -367,7 +367,6 @@ public void import_dxf_dimension_with_missing_block_is_drawn_from_its_definition
[TestMethod]
public void import_dxf_dimension_without_block_shows_its_measurement()
{
- RequireGdi();
string dxf = Header + Tables + EntitiesStart + AlignedDimension(null) + Epilogue;
Block block = AssertRegeneratedDimension(ImportDxf(dxf).GetActiveModel());
Text text = block.Children.OfType().SingleOrDefault();
@@ -759,7 +758,6 @@ public void export_dimension_writes_its_xdata()
[TestMethod]
public void export_dimension_round_trips_as_a_dxf_dimension()
{
- RequireGdi();
Project project = MakeProjectWithDimension(new GeoPoint(0, 0, 0), new GeoPoint(100, 0, 0));
Model reimported = WithChoice("DxfDwg.ExportDimension", 1, () => ExportAndImport(project));
@@ -1613,13 +1611,6 @@ private static Block SingleDimensionBlock(Model model)
return block;
}
- // Adding a Text to a model computes its extent through GDI, which only exists on Windows.
- private static void RequireGdi()
- {
- if (!System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows))
- Assert.Inconclusive("text needs GDI, which is only available on Windows");
- }
-
private Project ImportDxf(string dxf)
{
string file = this.TestContext.TestName + ".dxf";
diff --git a/tests/CADability.ImportTests/DxfOcsExportTest.cs b/tests/CADability.ImportTests/DxfOcsExportTest.cs
index 5e436801..3f81fd85 100644
--- a/tests/CADability.ImportTests/DxfOcsExportTest.cs
+++ b/tests/CADability.ImportTests/DxfOcsExportTest.cs
@@ -90,7 +90,6 @@ public void export_dxf_tilted_circle_and_arc_keep_position()
[TestMethod]
public void export_dxf_flipped_text_keeps_position()
{
- RequireGdi();
foreach (bool aligned in new[] { false, true })
{
Text text = MakeText("Flipped", new GeoPoint(10, 5, 2));
@@ -119,7 +118,6 @@ public void export_dxf_flipped_text_keeps_position()
[TestMethod]
public void export_dxf_tilted_text_keeps_position()
{
- RequireGdi();
Text text = MakeText("Tilted", new GeoPoint(-3, 8, 1));
text.Modify(ModOp.Rotate(new GeoPoint(2, 3, 4), new GeoVector(1, 1, 0.3), new SweepAngle(0.65)));
AssertSameText(text, ReimportSingle(Export(text)));
@@ -130,7 +128,6 @@ public void export_dxf_tilted_text_keeps_position()
[TestMethod]
public void export_dxf_flipped_and_tilted_mtext_keep_position()
{
- RequireGdi();
Text text = MakeText("first line\nsecond line", new GeoPoint(10, 5, 2));
text.Modify(ModOp.Rotate(GeoPoint.Origin, GeoVector.XAxis, new SweepAngle(Math.PI)));
string file = Export(text);
@@ -151,13 +148,6 @@ public void export_dxf_flipped_and_tilted_mtext_keep_position()
// --- helpers -------------------------------------------------------------------------
- // Adding a Text to a model computes its extent through GDI, which only exists on Windows.
- private static void RequireGdi()
- {
- if (!System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows))
- Assert.Inconclusive("text needs GDI, which is only available on Windows");
- }
-
private static Text MakeText(string value, GeoPoint location)
{
Text text = Text.Construct();
diff --git a/tests/CADability.ImportTests/TestFonts.cs b/tests/CADability.ImportTests/TestFonts.cs
new file mode 100644
index 00000000..1be6eb08
--- /dev/null
+++ b/tests/CADability.ImportTests/TestFonts.cs
@@ -0,0 +1,20 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace CADability.ImportTests
+{
+ ///
+ /// Texts get their fonts from FontProvider.Current. The tests add Liberation Sans (Files/Fonts) to the fonts of the system,
+ /// so that texts have a font on every machine, also on a build server without any fonts installed (issue #412).
+ ///
+ [TestClass]
+ public static class TestFonts
+ {
+ public static string Directory => System.IO.Path.Combine(AppContext.BaseDirectory, "Files", "Fonts");
+
+ [AssemblyInitialize]
+ public static void UseTestFonts(TestContext context)
+ {
+ FontProvider.Current = new ManagedFontProvider(new[] { Directory }, true);
+ }
+ }
+}
diff --git a/tests/CADability.Tests/FontCacheGlyphTests.cs b/tests/CADability.Tests/FontCacheGlyphTests.cs
index d0eaf115..04f64367 100644
--- a/tests/CADability.Tests/FontCacheGlyphTests.cs
+++ b/tests/CADability.Tests/FontCacheGlyphTests.cs
@@ -7,7 +7,8 @@ namespace CADability.Tests
{
///
/// Sends all printable ASCII characters of installed fonts through
- /// and checks that every glyph with a closed outline is painted with triangles. Needs Windows (GDI) and the font.
+ /// and checks that every glyph with a closed outline is painted with triangles. Needs the font: Liberation Sans comes with
+ /// the tests (see ), the others are Windows fonts.
///
[TestClass]
public class FontCacheGlyphTests
@@ -105,7 +106,6 @@ public void SetClip(Rectangle clipRectangle) { }
private static string CheckFontAvailable(string fontName)
{
- if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return "glyph outlines need GDI, which is only available on Windows";
if (!GeoObject.Text.FontFamilyNames.Contains(fontName.ToUpper()))
{
// name the installed families that start with the same word, which shows a misspelled or a wrong instance name
@@ -145,6 +145,7 @@ private static (List withoutTriangles, List withoutList) CheckFont(s
}
[DataTestMethod]
+ [DataRow("Liberation Sans")]
[DataRow("Arial")]
[DataRow("Calibri")]
[DataRow("Bahnschrift")]
@@ -179,18 +180,17 @@ public void ReportGlyphsWithoutTrianglesForAllInstalledFonts()
{
if (!System.Diagnostics.Debugger.IsAttached && string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CADABILITY_FONT_SURVEY")))
Assert.Inconclusive("survey over all installed fonts, takes a few minutes: start it with Debug Test or set CADABILITY_FONT_SURVEY=1");
- if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) Assert.Inconclusive("glyph outlines need GDI, which is only available on Windows");
int fontsWithProblems = 0;
- foreach (FontFamily ff in FontFamily.Families)
+ foreach (string family in FontProvider.Current.GetFamilyNames())
{
try
{
- var (withoutTriangles, withoutList) = CheckFont(ff.Name, (int)FontStyle.Regular, TestContext);
+ var (withoutTriangles, withoutList) = CheckFont(family, (int)FontStyle.Regular, TestContext);
if (withoutTriangles.Count + withoutList.Count > 0) ++fontsWithProblems;
}
catch (Exception ex)
{
- TestContext.WriteLine("{0}: exception {1}", ff.Name, ex.Message);
+ TestContext.WriteLine("{0}: exception {1}", family, ex.Message);
++fontsWithProblems;
}
}
diff --git a/tests/CADability.Tests/FontCodePointTests.cs b/tests/CADability.Tests/FontCodePointTests.cs
new file mode 100644
index 00000000..045025aa
--- /dev/null
+++ b/tests/CADability.Tests/FontCodePointTests.cs
@@ -0,0 +1,115 @@
+using CADability.GeoObject;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System.Drawing;
+
+namespace CADability.Tests
+{
+ ///
+ /// takes Unicode code points, so that characters outside the Basic Multilingual Plane, e.g. most
+ /// emoji, which are a surrogate pair of two chars in a string, reach the font as one character (issue #412).
+ ///
+ [TestClass]
+ public class FontCodePointTests
+ {
+ private const int Emoji = 0x1F600; // GRINNING FACE, "😀" in a string
+
+ ///
+ /// A face that records the characters it is asked for. Every character is a square from 0.1 to 0.4 with an advance
+ /// width of 0.5 (in units of the em height).
+ ///
+ private sealed class RecordingFace : FontFace
+ {
+ public readonly List Glyphs = new List(), Widths = new List();
+ public readonly List<(int, int)> KerningPairs = new List<(int, int)>();
+ public override string FamilyName => "Recording";
+ public override bool IsBold => false;
+ public override bool IsItalic => false;
+ public override int EmHeight => 1000;
+ public override int CellAscent => 800;
+ public override int CellDescent => 200;
+ public override int LineSpacing => 1000;
+ public override double GetAdvanceWidth(int codePoint)
+ {
+ lock (Widths) Widths.Add(codePoint);
+ return 0.5;
+ }
+ public override double GetKerning(int left, int right)
+ {
+ lock (KerningPairs) KerningPairs.Add((left, right));
+ return 0.0;
+ }
+ public override void GetGlyphPath(int codePoint, bool underline, bool strikeout, out PointF[] points, out byte[] types)
+ {
+ lock (Glyphs) Glyphs.Add(codePoint);
+ points = new[] { new PointF(0.1f, 0.2f), new PointF(0.4f, 0.2f), new PointF(0.4f, 0.6f), new PointF(0.1f, 0.6f) };
+ types = new byte[] { 0, 1, 1, 0x81 };
+ }
+ }
+ private sealed class RecordingProvider : FontProvider
+ {
+ public readonly RecordingFace Face = new RecordingFace();
+ public override string[] GetFamilyNames() => new[] { "Recording" };
+ public override FontFace GetFace(string familyName, bool bold, bool italic) => string.Equals(familyName, "Recording", StringComparison.OrdinalIgnoreCase) ? Face : null;
+ }
+
+ private static bool IsSurrogate(int c) => c >= 0xD800 && c <= 0xDFFF;
+
+ [TestMethod]
+ public void ASurrogatePairIsOneCodePoint()
+ {
+ CollectionAssert.AreEqual(new[] { 'a', Emoji, 'b' }, Text.CodePoints("a😀b"));
+ // a lone surrogate is no code point, it is kept as the char it is
+ CollectionAssert.AreEqual(new[] { 0xD83D, 'x' }, Text.CodePoints("\uD83Dx"));
+ CollectionAssert.AreEqual(new[] { 'x', 0xDE00 }, Text.CodePoints("x\uDE00"));
+ CollectionAssert.AreEqual(new int[0], Text.CodePoints(""));
+ }
+
+ [TestMethod]
+ [DoNotParallelize]
+ public void AnEmojiReachesTheFontAsOneCharacter()
+ {
+ RecordingProvider provider = new RecordingProvider();
+ try
+ {
+ FontProvider.Current = provider;
+ Text text = Text.Construct();
+ text.Font = "Recording";
+ text.TextString = "a😀b";
+ text.Location = GeoPoint.Origin;
+ text.LineDirection = GeoVector.XAxis;
+ text.GlyphDirection = GeoVector.YAxis;
+ text.TextSize = 1.0;
+
+ // three characters, not four as with the two halves of the surrogate pair as separate characters: the extent
+ // is the sum of the advance widths, the outline goes from the first square at 0.1 to the third at 2 * 0.5 + 0.4
+ BoundingCube extent = text.GetBoundingCube();
+ Assert.AreEqual(1.5, extent.Xmax - extent.Xmin, 1e-6, "the advance widths of three characters");
+ BoundingCube outline = text.GetOutline().GetExtent();
+ Assert.AreEqual(0.1, outline.Xmin, 1e-6);
+ Assert.AreEqual(1.4, outline.Xmax, 1e-6, "the outlines of three characters");
+
+ RecordingFace face = provider.Face;
+ CollectionAssert.AreEquivalent(new[] { 'a', Emoji, 'b' }, face.Glyphs.Distinct().ToArray(), "the outlines asked for");
+ Assert.IsTrue(face.Widths.Contains(Emoji), "the advance width of the emoji");
+ Assert.IsTrue(face.KerningPairs.Contains(('a', Emoji)) && face.KerningPairs.Contains((Emoji, 'b')), "the kerning around the emoji");
+ Assert.IsFalse(face.Glyphs.Concat(face.Widths).Any(IsSurrogate), "no half of a surrogate pair reaches the font");
+ }
+ finally
+ {
+ TestFonts.UseTestFonts(null);
+ }
+ }
+
+ [TestMethod]
+ public void TheManagedReaderMapsCodePointsAboveTheBmp()
+ {
+ // the face of the managed reader passes the code point on to the cmap, which supports format 12 (beyond U+FFFF);
+ // a font without the character gives the missing glyph, but no exception
+ FontFace face = new ManagedFontProvider(new[] { TestFonts.Directory }, false).GetFace("Liberation Sans", false, false);
+ Assert.IsNotNull(face);
+ Assert.AreEqual(face.GetAdvanceWidth(0xFFFF + 1), face.GetAdvanceWidth(0x10FFFF), 1e-12, "both are the missing glyph");
+ face.GetGlyphPath(Emoji, false, false, out PointF[] points, out byte[] types);
+ Assert.AreEqual(points.Length, types.Length);
+ }
+ }
+}
diff --git a/tests/CADability.Tests/FontProviderTests.cs b/tests/CADability.Tests/FontProviderTests.cs
new file mode 100644
index 00000000..8e2b7be0
--- /dev/null
+++ b/tests/CADability.Tests/FontProviderTests.cs
@@ -0,0 +1,170 @@
+using CADability.Curve2D;
+using CADability.GeoObject;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System.Runtime.CompilerServices;
+using Path = System.IO.Path;
+
+namespace CADability.Tests
+{
+ ///
+ /// Texts get their fonts from , a static service of the kernel, and no longer from GDI or
+ /// the frame (issue #412, step 6): they work without a frame and on every platform. The tests that replace the provider
+ /// do not run in parallel with other tests and restore the provider of .
+ ///
+ [TestClass]
+ public class FontProviderTests
+ {
+ private static string FontDirectory([CallerFilePath] string thisFile = "")
+ => Path.Combine(Path.GetDirectoryName(thisFile), "Files", "Fonts");
+
+ private static Text MakeText(string font, string textString, double size)
+ {
+ Text text = Text.Construct();
+ text.Font = font;
+ text.TextString = textString;
+ text.Location = GeoPoint.Origin;
+ text.LineDirection = GeoVector.XAxis;
+ text.GlyphDirection = GeoVector.YAxis;
+ text.TextSize = size;
+ return text;
+ }
+ private static void WithProvider(FontProvider provider, Action action)
+ {
+ try
+ {
+ FontProvider.Current = provider;
+ action();
+ }
+ finally
+ {
+ TestFonts.UseTestFonts(null);
+ }
+ }
+
+ [TestMethod]
+ [DoNotParallelize]
+ public void TextWorksWithoutFrameAndWithoutGdi()
+ {
+ Assert.IsNull(FrameImpl.MainFrame, "the tests run without a frame");
+ WithProvider(new ManagedFontProvider(new[] { FontDirectory() }, false), () =>
+ {
+ Text text = MakeText("Liberation Sans", "Hello", 10.0);
+ GeoObjectList outline = text.GetOutline();
+ Assert.IsTrue(outline.Count > 0, "the text has an outline");
+ BoundingCube ext = text.GetBoundingCube();
+ Assert.IsFalse(ext.IsEmpty);
+
+ // the width of the text is the sum of the advance widths and the kerning of the font
+ TrueTypeFont font = TrueTypeFont.Load(Path.Combine(FontDirectory(), "LiberationSans-Regular.ttf"))[0];
+ double advance = 0.0;
+ string s = "Hello";
+ for (int i = 0; i < s.Length; ++i)
+ {
+ advance += font.GetAdvanceWidth(font.GetGlyphIndex(s[i]));
+ if (i > 0) advance += font.GetKerning(font.GetGlyphIndex(s[i - 1]), font.GetGlyphIndex(s[i]));
+ }
+ double width = 10.0 * advance / font.UnitsPerEm;
+ Assert.AreEqual(width, text.GetBoundingCube().Xmax - text.GetBoundingCube().Xmin, 0.15 * 10.0, "the extent is about the advance width (the glyphs do not fill their whole advance)");
+ });
+ }
+
+ [TestMethod]
+ [DoNotParallelize]
+ public void UnknownFontUsesTheDefaultFace()
+ {
+ WithProvider(new ManagedFontProvider(new[] { FontDirectory() }, false), () =>
+ {
+ FontFace face = FontCache.GlobalFontCache.GetFace("A Font That Does Not Exist", 0);
+ Assert.AreEqual("Liberation Sans", face.FamilyName);
+ Text known = MakeText("Liberation Sans", "Ag", 1.0), unknown = MakeText("A Font That Does Not Exist", "Ag", 1.0);
+ Assert.AreEqual(known.GetOutline().GetExtent().Size, unknown.GetOutline().GetExtent().Size, 1e-9);
+ });
+ }
+
+ [TestMethod]
+ [DoNotParallelize]
+ public void WithoutAnyFontTheExceptionSaysWhatToDo()
+ {
+ string empty = Path.Combine(Path.GetTempPath(), "CADability-no-fonts-" + Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(empty);
+ try
+ {
+ WithProvider(new ManagedFontProvider(new[] { empty }, false), () =>
+ {
+ InvalidOperationException ex = Assert.ThrowsException(() => FontCache.GlobalFontCache.GetFace("Arial", 0));
+ StringAssert.Contains(ex.Message, "FontProvider.Current");
+ // the extent and the glyphs for painting are empty instead of failing, an explicit request for the outline throws
+ FontCache.GlobalFontCache.Get("Arial", 0, 'A', out double width, null);
+ Text text = MakeText("Arial", "no font", 1.0);
+ text.GetBoundingCube();
+ Assert.ThrowsException(() => text.GetOutline());
+ });
+ }
+ finally
+ {
+ Directory.Delete(empty, true);
+ }
+ }
+
+ [TestMethod]
+ [DoNotParallelize]
+ public void UnderlineAndStrikeoutAddBars()
+ {
+ WithProvider(new ManagedFontProvider(new[] { FontDirectory() }, false), () =>
+ {
+ Path2D[] plain = FontCache.GlobalFontCache.GetOutline2D("Liberation Sans", 0, 'o', out double w1);
+ Path2D[] underlined = FontCache.GlobalFontCache.GetOutline2D("Liberation Sans", 4, 'o', out double w2);
+ Path2D[] both = FontCache.GlobalFontCache.GetOutline2D("Liberation Sans", 4 | 8, 'o', out double w3);
+ Assert.AreEqual(plain.Length + 1, underlined.Length);
+ Assert.AreEqual(plain.Length + 2, both.Length);
+ Assert.AreEqual(w1, w2);
+ // the underline is below the baseline (y = 1 - CellAscent/em after PathDataToPath2D)
+ FontFace face = FontCache.GlobalFontCache.GetFace("Liberation Sans", 0);
+ double baseline = 1.0 - face.CellAscent / (double)face.EmHeight;
+ Assert.IsTrue(underlined.Last().GetExtent().Top < baseline);
+ });
+ }
+
+ [TestMethod]
+ [DoNotParallelize]
+ public void MissingStyleFallsBackToAnExistingFace()
+ {
+ ManagedFontProvider provider = new ManagedFontProvider(new[] { FontDirectory() }, false);
+ CollectionAssert.AreEqual(new[] { "Liberation Sans" }, provider.GetFamilyNames());
+ FontFace bold = provider.GetFace("liberation sans", true, false);
+ Assert.IsNotNull(bold, "the family name is not case sensitive");
+ Assert.IsFalse(bold.IsBold, "only the regular face is there");
+ Assert.IsNull(provider.GetFace("Arial", false, false));
+ Assert.AreSame(bold, provider.GetFace("Liberation Sans", false, false), "faces are loaded once");
+ }
+
+ [TestMethod]
+ public void HeadersAreReadWithoutLoadingTheGlyphs()
+ {
+ string file = Path.Combine(FontDirectory(), "LiberationSans-Regular.ttf");
+ TrueTypeFont header = TrueTypeFont.ReadHeaders(file).Single(), full = TrueTypeFont.Load(file).Single();
+ Assert.IsTrue(header.IsHeaderOnly);
+ Assert.IsFalse(full.IsHeaderOnly);
+ Assert.AreEqual(full.FamilyName, header.FamilyName);
+ Assert.AreEqual(full.SubfamilyName, header.SubfamilyName);
+ Assert.AreEqual(full.IsBold, header.IsBold);
+ Assert.AreEqual(full.IsItalic, header.IsItalic);
+ Assert.AreEqual(full.UnitsPerEm, header.UnitsPerEm);
+ Assert.AreEqual(full.CellAscent, header.CellAscent);
+ Assert.AreEqual(full.UnderlinePosition, header.UnderlinePosition);
+ Assert.IsTrue(header.HasTrueTypeOutlines);
+ Assert.ThrowsException(() => header.GetContours(5));
+ }
+
+ [TestMethod]
+ public void FontCacheHasNoFinalizerThatCanCrashTheProcess()
+ {
+ // the FontCache used to create a GDI device context in its constructor and delete it in its finalizer; without GDI
+ // the finalizer threw and ended the process (issue #412)
+ for (int i = 0; i < 10; ++i) new FontCache();
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+ }
+ }
+}
diff --git a/tests/CADability.Tests/ProjectTest.cs b/tests/CADability.Tests/ProjectTest.cs
index 9923a8a3..03274956 100644
--- a/tests/CADability.Tests/ProjectTest.cs
+++ b/tests/CADability.Tests/ProjectTest.cs
@@ -15,16 +15,6 @@ public class ProjectTest
public TestContext TestContext { get; set; }
- ///
- /// Text entities are measured and triangulated with GDI (System.Drawing and gdi32 via FontCache),
- /// which only exists on Windows. Tests that import or create text are inconclusive elsewhere.
- ///
- private static void RequiresGdiText()
- {
- if (!System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows))
- Assert.Inconclusive("text needs GDI, which is only available on Windows");
- }
-
///
/// Renders with OpenGL and compares it with the reference bitmap.
/// Rendering needs CADability.Forms (WinForms, OpenGL), so the check only runs on Windows.
@@ -103,7 +93,6 @@ public void import_step_issue101_succeds()
[TestMethod]
public void export_dxf_issue_129_succeds()
{
- RequiresGdiText();
// in master@82ffb34 exporting a text object to txt does not set the location,
// so all texts are all not in the correct location / orientation
@@ -172,7 +161,6 @@ public void import_dxf_issue171_succeds()
[DeploymentItem(@"Files/Dxf/Z273.dxf", nameof(import_dxf_Z273_succeeds))]
public void import_dxf_Z273_succeeds()
{
- RequiresGdiText();
// AC1009 (R12) DXF with no *Paper_Space block — importing this used to throw
// KeyNotFoundException when FillPaperSpace accessed the missing block record.
var file = Path.Combine(this.TestContext.DeploymentDirectory, this.TestContext.TestName, "Z273.dxf");
@@ -323,7 +311,6 @@ public void import_dxf_arc_crossing_zero_sweep_is_correct()
[TestMethod]
public void import_dxf_text_rotation_correct()
{
- RequiresGdiText();
// Regression: TextEntity.Rotation from ACadSharp is in RADIANS.
// Old code called Angle.Deg() on it, making 90° text appear at ~1.57° (nearly horizontal).
// A text at DXF rotation=90° must import with LineDirection ≈ (0, 1, 0).
@@ -509,7 +496,6 @@ public void import_dxf_0583667_succeeds()
[TestMethod]
public void import_dxf_mtext_topcenter_placement_correct()
{
- RequiresGdiText();
// Regression: CreateMText ignored AttachmentPoint so all MTEXT rendered as
// Left/Baseline. For table cells with attachment TopCenter (71=2) the text
// appears shifted right and down of where it should be.
@@ -567,7 +553,6 @@ public void import_dxf_mtext_topcenter_placement_correct()
[TestMethod]
public void import_dxf_mtext_bottomright_placement_correct()
{
- RequiresGdiText();
// MTEXT with attachment BottomRight (71=9) must have Right+Bottom alignment.
const string dxf = @" 0
SECTION
@@ -710,7 +695,6 @@ public void import_dxf_spline_degenerate_does_not_crash()
[TestMethod]
public void import_dxf_tolerance_fcf_imported_as_text()
{
- RequiresGdiText();
// DXF TOLERANCE (AcDbFcf / Feature Control Frame) entities were silently dropped
// because there was no case for ACadSharp.Entities.Tolerance in GeoObjectFromEntity.
// They must now be imported as Text objects at the insertion point.
diff --git a/tests/CADability.Tests/RendererRecoveryTests.cs b/tests/CADability.Tests/RendererRecoveryTests.cs
index 4487e7e8..5e7fdd3a 100644
--- a/tests/CADability.Tests/RendererRecoveryTests.cs
+++ b/tests/CADability.Tests/RendererRecoveryTests.cs
@@ -1,6 +1,5 @@
using System.Collections;
using System.Reflection;
-using System.Runtime.InteropServices;
using CADability.GeoObject;
using CADability.Substitutes;
// with UseWindowsForms (the Windows build of the tests), System.Windows.Forms has a PaintEventArgs as well
@@ -74,9 +73,9 @@ public void ViewPaintsAgainAfterAnException()
[TestMethod]
public void ClearingTheFontCacheDisposesTheGlyphLists()
{
- if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) Assert.Inconclusive("the font cache needs GDI, which is only available on Windows");
+ // the font cache gets its fonts from FontProvider.Current and no longer needs GDI (issue #412)
FontCache fontCache = new FontCache();
- string[] dictionaries = { "cache", "centerLineCache", "fontCharacteristicsCache", "kerning" };
+ string[] dictionaries = { "cache", "centerLineCache", "fontCharacteristicsCache", "faces" };
IDictionary cache = Dictionary(fontCache, "cache");
Type dictKey = typeof(FontCache).GetNestedType("DictKey", BindingFlags.NonPublic);
Type dictVal = typeof(FontCache).GetNestedType("DictVal", BindingFlags.NonPublic);
@@ -84,12 +83,12 @@ public void ClearingTheFontCacheDisposesTheGlyphLists()
object withList = Activator.CreateInstance(dictVal);
dictVal.GetField("list").SetValue(withList, list);
dictVal.GetField("listCreated").SetValue(withList, true);
- cache[Activator.CreateInstance(dictKey, "Arial", 0, 'A')] = withList;
+ cache[Activator.CreateInstance(dictKey, "Arial", 0, (int)'A')] = withList;
object withoutList = Activator.CreateInstance(dictVal); // e.g. a space: the list is null
dictVal.GetField("listCreated").SetValue(withoutList, true);
- cache[Activator.CreateInstance(dictKey, "Arial", 0, ' ')] = withoutList;
- Type kerningKey = typeof(FontCache).GetNestedType("KerningKey", BindingFlags.NonPublic);
- Dictionary(fontCache, "kerning")[Activator.CreateInstance(kerningKey, "Arial", 0)] = new Dictionary<(char Left, char Right), double>();
+ cache[Activator.CreateInstance(dictKey, "Arial", 0, (int)' ')] = withoutList;
+ Type fontKey = typeof(FontCache).GetNestedType("FontKey", BindingFlags.NonPublic);
+ Dictionary(fontCache, "faces")[Activator.CreateInstance(fontKey, "Arial", 0)] = null;
fontCache.Clear();
diff --git a/tests/CADability.Tests/TestFonts.cs b/tests/CADability.Tests/TestFonts.cs
new file mode 100644
index 00000000..ea18ad0f
--- /dev/null
+++ b/tests/CADability.Tests/TestFonts.cs
@@ -0,0 +1,20 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace CADability.Tests
+{
+ ///
+ /// Texts get their fonts from FontProvider.Current. The tests add Liberation Sans (Files/Fonts) to the fonts of the system,
+ /// so that texts have a font on every machine, also on a build server without any fonts installed (issue #412).
+ ///
+ [TestClass]
+ public static class TestFonts
+ {
+ public static string Directory => System.IO.Path.Combine(AppContext.BaseDirectory, "Files", "Fonts");
+
+ [AssemblyInitialize]
+ public static void UseTestFonts(TestContext context)
+ {
+ FontProvider.Current = new ManagedFontProvider(new[] { Directory }, true);
+ }
+ }
+}