Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
505 changes: 505 additions & 0 deletions CADability/CffOutlines.cs

Large diffs are not rendered by default.

15 changes: 8 additions & 7 deletions CADability/FontProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,11 @@ public abstract class FontFace
}

/// <summary>
/// The default <see cref="FontProvider"/>: 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.
/// The default <see cref="FontProvider"/>: reads TrueType and OpenType font files (.ttf, .ttc, .otf, .otc) 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. Bitmap fonts and variable
/// OpenType fonts with CFF2 outlines are left out. A bold or italic face that a family does not have is not synthesized:
/// the closest face is used.
/// </summary>
public class ManagedFontProvider : FontProvider
{
Expand Down Expand Up @@ -177,7 +178,7 @@ public static IEnumerable<string> SystemFontDirectories()
}
}
/// <summary>
/// Adds the fonts of a font file (.ttf or .ttc).
/// Adds the fonts of a font file (.ttf, .ttc, .otf or .otc).
/// </summary>
public void AddFontFile(string fileName)
{
Expand Down Expand Up @@ -232,7 +233,7 @@ public override FontFace GetFace(string familyName, bool bold, bool italic)

private static void AddEntry(Entry entry, TrueTypeFont font, List<Entry> list)
{
if (!font.HasTrueTypeOutlines || string.IsNullOrEmpty(font.FamilyName)) return; // CFF or bitmap fonts cannot be used
if (!font.HasOutlines || string.IsNullOrEmpty(font.FamilyName)) return; // bitmap fonts and CFF2 cannot be used
entry.Family = font.FamilyName;
entry.Bold = font.IsBold;
entry.Italic = font.IsItalic;
Expand All @@ -258,7 +259,7 @@ private Dictionary<string, List<Entry>> Families()
foreach (string file in files)
{
string ext = Path.GetExtension(file).ToLowerInvariant();
if (ext != ".ttf" && ext != ".ttc") continue;
if (ext != ".ttf" && ext != ".ttc" && ext != ".otf" && ext != ".otc") continue;
if (!seen.Add(Path.GetFullPath(file))) continue;
try
{
Expand Down
94 changes: 89 additions & 5 deletions CADability/TrueTypeFont.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ public GlyphPoint(double x, double y, bool onCurve)
/// Reads a TrueType font (.ttf, or one font of a .ttc collection) without any help of the operating system, so that text
/// works on every platform (issue #412). It reads the glyph outlines (exact quadratic curves from the "glyf" table,
/// composite glyphs included), the advance widths, the metrics, the kerning pairs of the "kern" table and the names.
/// OpenType fonts with CFF outlines (some .otf files) are recognized, but their outlines cannot be read
/// (<see cref="HasTrueTypeOutlines"/> is false).
/// OpenType fonts with CFF outlines (most .otf files) are read with <see cref="CffOutlines"/>: their cubic curves go into
/// the path data unchanged.
/// </summary>
internal sealed class TrueTypeFont
{
Expand Down Expand Up @@ -81,6 +81,16 @@ internal sealed class TrueTypeFont
/// False for an OpenType font with CFF outlines and for a bitmap font, whose glyphs cannot be read.
/// </summary>
public bool HasTrueTypeOutlines { get; }
/// <summary>
/// True for an OpenType font with outlines in the "CFF " table (Compact Font Format, cubic curves).
/// </summary>
public bool HasCffOutlines { get; }
/// <summary>
/// True if the glyph outlines can be read: TrueType or CFF outlines. False for bitmap fonts and for CFF2 (variable
/// OpenType fonts).
/// </summary>
public bool HasOutlines => HasTrueTypeOutlines || HasCffOutlines;
private CffOutlines cff; // created on demand
public int GlyphCount => numGlyphs;

/// <summary>
Expand Down Expand Up @@ -123,6 +133,7 @@ private TrueTypeFont(byte[] data, int offset)
}
// a TrueType font without outlines is a bitmap font (e.g. color emoji)
HasTrueTypeOutlines = trueType && tables.ContainsKey("glyf") && tables.ContainsKey("loca");
HasCffOutlines = !trueType && tables.ContainsKey("CFF ");
int head = Table("head");
UnitsPerEm = U16(head + 18);
if (UnitsPerEm == 0) UnitsPerEm = 1000;
Expand Down Expand Up @@ -228,8 +239,8 @@ private static TrueTypeFont ReadHeader(FileStream stream, uint offset)
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)
else if (tag == "glyf" || tag == "loca" || tag == "CFF ")
{ // only their presence matters (HasTrueTypeOutlines, HasCffOutlines)
for (int k = 8; k < 16; ++k) record[k] = 0;
copied.Add((record, new byte[0]));
}
Expand Down Expand Up @@ -351,7 +362,8 @@ public void GetPathData(int glyph, bool underline, bool strikeout, out PointF[]
List<byte> tps = new List<byte>();
double scale = 1.0 / UnitsPerEm;
PointF Map(double x, double y) => new PointF((float)(x * scale), (float)((CellAscent - y) * scale));
foreach (GlyphPoint[] contour in GetContours(glyph))
if (HasCffOutlines) DrawCff(glyph, new PathDataSink(pts, tps, Map));
else foreach (GlyphPoint[] contour in GetContours(glyph))
{
if (contour.Length < 2) continue;
int n = contour.Length;
Expand Down Expand Up @@ -443,6 +455,78 @@ void AddBar(int top, int thickness)
types = tps.ToArray();
}

/// <summary>
/// Sends the outline of a glyph of a font with CFF outlines to <paramref name="sink"/>, in font units.
/// </summary>
internal void DrawCff(int glyph, IGlyphOutlineSink sink)
{
if (IsHeaderOnly) throw new InvalidOperationException("The font \"" + FullName + "\" was read by ReadHeaders and has no glyphs, load it with Load");
if (!HasCffOutlines) throw new NotSupportedException("The font \"" + FullName + "\" has no CFF outlines");
if (cff == null)
{
var t = tables["CFF "];
cff = new CffOutlines(data, t.offset, t.length, UnitsPerEm);
}
cff.Draw(glyph, sink);
}
/// <summary>
/// Collects an outline as GDI+ path data (see <see cref="GetPathData(int, out PointF[], out byte[])"/>).
/// </summary>
private sealed class PathDataSink : IGlyphOutlineSink
{
private readonly List<PointF> pts;
private readonly List<byte> tps;
private readonly Func<double, double, PointF> map;
private int start = -1; // index of the start point of the open figure
private double cx, cy;
public PathDataSink(List<PointF> pts, List<byte> tps, Func<double, double, PointF> map)
{
this.pts = pts;
this.tps = tps;
this.map = map;
}
public void MoveTo(double x, double y)
{
ClosePath();
start = pts.Count;
pts.Add(map(x, y));
tps.Add(0);
cx = x;
cy = y;
}
public void LineTo(double x, double y)
{
if (start < 0 || (x == cx && y == cy)) return;
pts.Add(map(x, y));
tps.Add(1);
cx = x;
cy = y;
}
public void CurveTo(double x1, double y1, double x2, double y2, double x3, double y3)
{
if (start < 0) return;
if (x1 == cx && y1 == cy && x2 == cx && y2 == cy && x3 == cx && y3 == cy) return; // no length
pts.Add(map(x1, y1));
pts.Add(map(x2, y2));
pts.Add(map(x3, y3));
tps.Add(3);
tps.Add(3);
tps.Add(3);
cx = x3;
cy = y3;
}
public void ClosePath()
{
if (start < 0) return;
if (pts.Count - start < 2)
{ // nothing but the start point
pts.RemoveRange(start, pts.Count - start);
tps.RemoveRange(start, tps.Count - start);
}
else tps[tps.Count - 1] |= 0x80;
start = -1;
}
}
private void AddContours(int glyph, List<GlyphPoint[]> res, int depth)
{
if (glyph < 0 || glyph >= numGlyphs || depth > 8) return; // a deeper nesting is a broken (or malicious) font
Expand Down
9 changes: 9 additions & 0 deletions REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ SPDX-FileCopyrightText = [
]
SPDX-License-Identifier = "OFL-1.1"

[[annotations]]
path = [
"tests/CADability.Tests/Files/Fonts/InterCffTest.otf",
"tests/CADability.Tests/Files/Fonts/InterCidTest.otf",
"tests/CADability.Tests/Files/Fonts/InterCffTest.reference.json",
]
SPDX-FileCopyrightText = "2016 The Inter Project Authors"
SPDX-License-Identifier = "OFL-1.1"

[[annotations]]
# Test drawings of R10 and R11/R12 from https://github.com/michal-josef-spacek/CAD-Format-DWG-AC1006
# and https://github.com/michal-josef-spacek/CAD-Format-DWG-AC1009, renamed after their content.
Expand Down
125 changes: 125 additions & 0 deletions tests/CADability.Tests/CffOutlinesTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using CADability.GeoObject;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Drawing;
using System.Runtime.CompilerServices;
using System.Text.Json;
using Path = System.IO.Path;

namespace CADability.Tests
{
/// <summary>
/// The outlines of OpenType fonts with CFF outlines (issue #412, step 6). InterCffTest.otf (name-keyed) and InterCidTest.otf
/// (CID-keyed, two font dicts) are made by make_cff_test_fonts.py from a subset of Inter so that they use subroutines,
/// the specialized curve and line operators, flex and hint masks; InterCffTest.reference.json holds the outlines as fontTools
/// draws them, the same for both fonts.
/// </summary>
[TestClass]
public class CffOutlinesTests
{
private static string FontFile(string name, [CallerFilePath] string thisFile = "")
=> Path.Combine(Path.GetDirectoryName(thisFile), "Files", "Fonts", name);

private sealed class Recorder : IGlyphOutlineSink
{
public readonly List<(string op, double[] args)> Commands = new List<(string, double[])>();
public void MoveTo(double x, double y) => Commands.Add(("moveTo", new[] { x, y }));
public void LineTo(double x, double y) => Commands.Add(("lineTo", new[] { x, y }));
public void CurveTo(double x1, double y1, double x2, double y2, double x3, double y3) => Commands.Add(("curveTo", new[] { x1, y1, x2, y2, x3, y3 }));
public void ClosePath() => Commands.Add(("closePath", new double[0]));
}

private static void AssertOutlinesMatchFontTools(string fontFile)
{
TrueTypeFont font = TrueTypeFont.Load(FontFile(fontFile)).Single();
Assert.IsTrue(font.HasCffOutlines);
Assert.IsFalse(font.HasTrueTypeOutlines);
Assert.IsTrue(font.HasOutlines);
JsonElement glyphs = JsonDocument.Parse(File.ReadAllText(FontFile("InterCffTest.reference.json"))).RootElement.GetProperty("glyphs");
int compared = 0;
foreach (JsonElement expected in glyphs.EnumerateArray())
{
string c = expected.GetProperty("char").GetString();
int glyph = font.GetGlyphIndex(c[0]);
Assert.AreEqual(expected.GetProperty("glyph").GetInt32(), glyph, $"glyph index of '{c}'");
Recorder recorder = new Recorder();
font.DrawCff(glyph, recorder);
JsonElement commands = expected.GetProperty("commands");
Assert.AreEqual(commands.GetArrayLength(), recorder.Commands.Count, $"number of drawing commands of '{c}'");
int i = 0;
foreach (JsonElement command in commands.EnumerateArray())
{
(string op, double[] args) = recorder.Commands[i++];
Assert.AreEqual(command[0].GetString(), op, $"command {i} of '{c}'");
double[] expectedArgs = command[1].EnumerateArray().Select(v => v.GetDouble()).ToArray();
Assert.AreEqual(expectedArgs.Length, args.Length, $"arguments of command {i} of '{c}'");
for (int k = 0; k < args.Length; ++k) Assert.AreEqual(expectedArgs[k], args[k], 1e-9, $"argument {k} of command {i} of '{c}'");
}
++compared;
}
Assert.AreEqual(104, compared);
}

[TestMethod]
public void NameKeyedOutlinesMatchFontTools()
{
AssertOutlinesMatchFontTools("InterCffTest.otf");
}

[TestMethod]
public void CidKeyedOutlinesMatchFontTools()
{
AssertOutlinesMatchFontTools("InterCidTest.otf");
}

[TestMethod]
public void PathDataOfACffGlyph()
{
TrueTypeFont font = TrueTypeFont.Load(FontFile("InterCffTest.otf")).Single();
Recorder recorder = new Recorder();
int glyph = font.GetGlyphIndex('B');
font.DrawCff(glyph, recorder);
font.GetPathData(glyph, out PointF[] points, out byte[] types);
Assert.AreEqual(recorder.Commands.Count(c => c.op == "closePath"), types.Count(t => (t & 0x80) != 0), "one closed figure per contour");
Assert.IsTrue(types.All(t => new[] { 0, 1, 3 }.Contains(t & 0x7F)));
Assert.AreEqual(recorder.Commands.Count(c => c.op == "curveTo") * 3, types.Count(t => (t & 0x7F) == 3), "the cubic curves go in unchanged");
Assert.AreEqual(font.CellAscent / (double)font.UnitsPerEm, points.Max(p => p.Y), 1e-6, "the B stands on the baseline");
}

[TestMethod]
public void HeadersOfAnOpenTypeFontAreRead()
{
TrueTypeFont header = TrueTypeFont.ReadHeaders(FontFile("InterCidTest.otf")).Single();
Assert.IsTrue(header.IsHeaderOnly);
Assert.IsTrue(header.HasCffOutlines);
Assert.AreEqual("Inter", header.FamilyName);
}

[TestMethod]
[DoNotParallelize]
public void TextWithAnOpenTypeFont()
{
string directory = Path.Combine(Path.GetTempPath(), "CADability-otf-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(directory);
try
{
File.Copy(FontFile("InterCffTest.otf"), Path.Combine(directory, "InterCffTest.otf"));
FontProvider.Current = new ManagedFontProvider(new[] { directory }, false);
FontFace face = FontCache.GlobalFontCache.GetFace("Inter", 0);
Assert.AreEqual("Inter", face.FamilyName);
Text text = Text.Construct();
text.Font = "Inter";
text.TextString = "CFF";
text.Location = GeoPoint.Origin;
text.LineDirection = GeoVector.XAxis;
text.GlyphDirection = GeoVector.YAxis;
text.TextSize = 1.0;
Assert.IsTrue(text.GetOutline().Count > 0);
}
finally
{
TestFonts.UseTestFonts(null);
Directory.Delete(directory, true);
}
}
}
}
Binary file not shown.

Large diffs are not rendered by default.

Binary file not shown.
Loading
Loading