diff --git a/CADability/CffOutlines.cs b/CADability/CffOutlines.cs new file mode 100644 index 00000000..d5bfeee2 --- /dev/null +++ b/CADability/CffOutlines.cs @@ -0,0 +1,505 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace CADability +{ + /// + /// Receives the outline of a glyph in font units: contours of lines and cubic Bezier segments. + /// + internal interface IGlyphOutlineSink + { + void MoveTo(double x, double y); + void LineTo(double x, double y); + void CurveTo(double x1, double y1, double x2, double y2, double x3, double y3); + void ClosePath(); + } + + /// + /// Reads the glyph outlines of the "CFF " table of an OpenType font: the Compact Font Format with Type 2 charstrings, as used + /// by many .otf files. Name-keyed and CID-keyed fonts (FDArray, FDSelect) are supported, with local and global subroutines, + /// hint masks and the flex operators. Hints are skipped, they do not change the outline. Not supported: CFF2 (variable fonts) + /// and the deprecated accent composition of endchar (seac), which modern fonts do not use. + /// + internal sealed class CffOutlines + { + private readonly byte[] data; + private readonly List<(int start, int length)> charStrings; + private readonly List<(int start, int length)> globalSubrs; + private readonly List> localSubrs = new List>(); // per font dict + private readonly byte[] fdSelect; // font dict index per glyph, null for a name-keyed font + private readonly double scale; // FontMatrix * units per em, 1 in almost all fonts + + /// + /// The number of glyphs (charstrings). + /// + public int GlyphCount => charStrings.Count; + /// + /// True for a CID-keyed font. + /// + public bool IsCid => fdSelect != null; + + /// the font file + /// the start of the "CFF " table + /// its length + /// from the "head" table, to check the FontMatrix + public CffOutlines(byte[] data, int offset, int length, int unitsPerEm) + { + this.data = data; + int end = offset + length; + if (length < 4 || data[offset] != 1) throw new InvalidDataException("not a CFF table (CFF2 is not supported)"); + int pos = offset + data[offset + 2]; // header size + ReadIndex(ref pos, end); // names + List<(int start, int length)> topDicts = ReadIndex(ref pos, end); + ReadIndex(ref pos, end); // strings + globalSubrs = ReadIndex(ref pos, end); + if (topDicts.Count == 0) throw new InvalidDataException("CFF table without a font"); + Dictionary top = ReadDict(topDicts[0].start, topDicts[0].length); + if (!top.TryGetValue(17, out double[] charStringsOffset)) throw new InvalidDataException("CFF font without charstrings"); + if (top.TryGetValue(1206, out double[] charstringType) && charstringType[0] != 2) throw new InvalidDataException("only Type 2 charstrings are supported"); + int csPos = offset + (int)charStringsOffset[0]; + charStrings = ReadIndex(ref csPos, end); + scale = 1.0; + if (top.TryGetValue(1207, out double[] fontMatrix) && fontMatrix.Length >= 4 && fontMatrix[0] != 0) + { + // the FontMatrix is stored rounded (e.g. 0.00048828 for 1/2048): a factor this close to 1 is 1 + double s = fontMatrix[0] * unitsPerEm; + if (Math.Abs(s - 1.0) > 1e-3) scale = s; + } + if (top.TryGetValue(1236, out double[] fdArrayOffset) && top.TryGetValue(1237, out double[] fdSelectOffset)) + { // CID-keyed: one private dict (with its own local subroutines) per font dict + int fdPos = offset + (int)fdArrayOffset[0]; + foreach (var fd in ReadIndex(ref fdPos, end)) localSubrs.Add(ReadLocalSubrs(offset, ReadDict(fd.start, fd.length), end)); + fdSelect = ReadFdSelect(offset + (int)fdSelectOffset[0], charStrings.Count, end); + } + else localSubrs.Add(ReadLocalSubrs(offset, top, end)); + } + + /// + /// Sends the outline of to . + /// + public void Draw(int glyph, IGlyphOutlineSink sink) + { + if (glyph < 0 || glyph >= charStrings.Count) return; + int fd = 0; + if (fdSelect != null) + { + fd = fdSelect[glyph]; + if (fd >= localSubrs.Count) throw new InvalidDataException("invalid FDSelect"); + } + new Interpreter(this, localSubrs[fd], scale == 1.0 ? sink : new ScalingSink(sink, scale)).Run(charStrings[glyph]); + } + + private List<(int start, int length)> ReadLocalSubrs(int cffOffset, Dictionary dict, int end) + { + if (!dict.TryGetValue(18, out double[] priv) || priv.Length < 2) return new List<(int, int)>(); + int privSize = (int)priv[0], privOffset = cffOffset + (int)priv[1]; + if (privSize <= 0) return new List<(int, int)>(); + Dictionary privateDict = ReadDict(privOffset, privSize); + if (!privateDict.TryGetValue(19, out double[] subrs)) return new List<(int, int)>(); + int pos = privOffset + (int)subrs[0]; // relative to the private dict + return ReadIndex(ref pos, end); + } + private byte[] ReadFdSelect(int pos, int numGlyphs, int end) + { + byte[] res = new byte[numGlyphs]; + int format = data[pos]; + if (format == 0) + { + if (pos + 1 + numGlyphs > end) throw new InvalidDataException("invalid FDSelect"); + Array.Copy(data, pos + 1, res, 0, numGlyphs); + } + else if (format == 3) + { + int nRanges = U16(pos + 1); + for (int i = 0; i < nRanges; ++i) + { + int r = pos + 3 + 3 * i; + int first = U16(r), fd = data[r + 2], next = U16(r + 3); + for (int g = first; g < next && g < numGlyphs; ++g) res[g] = (byte)fd; + } + } + else throw new InvalidDataException("unsupported FDSelect format " + format); + return res; + } + private List<(int start, int length)> ReadIndex(ref int pos, int end) + { + List<(int, int)> res = new List<(int, int)>(); + if (pos + 2 > end) throw new InvalidDataException("CFF INDEX exceeds the table"); + int count = U16(pos); + if (count == 0) + { + pos += 2; + return res; + } + int offSize = data[pos + 2]; + if (offSize < 1 || offSize > 4) throw new InvalidDataException("invalid CFF INDEX"); + int offsets = pos + 3; + int dataStart = offsets + (count + 1) * offSize - 1; // offsets count from 1 + int Offset(int i) + { + int v = 0; + for (int k = 0; k < offSize; ++k) v = (v << 8) | data[offsets + i * offSize + k]; + return v; + } + for (int i = 0; i < count; ++i) + { + int s = Offset(i), e = Offset(i + 1); + if (e < s || dataStart + e > end) throw new InvalidDataException("invalid CFF INDEX"); + res.Add((dataStart + s, e - s)); + } + pos = dataStart + Offset(count); + return res; + } + /// + /// Reads a DICT: operator (two-byte operators as 1200 + second byte) to its operands. + /// + private Dictionary ReadDict(int pos, int length) + { + Dictionary res = new Dictionary(); + List operands = new List(); + int end = pos + length; + if (end > data.Length) throw new InvalidDataException("CFF DICT exceeds the file"); + while (pos < end) + { + int b0 = data[pos++]; + if (b0 <= 21) + { + int op = b0; + if (b0 == 12) op = 1200 + data[pos++]; + res[op] = operands.ToArray(); + operands.Clear(); + } + else if (b0 == 28) { operands.Add((short)((data[pos] << 8) | data[pos + 1])); pos += 2; } + else if (b0 == 29) { operands.Add((data[pos] << 24) | (data[pos + 1] << 16) | (data[pos + 2] << 8) | data[pos + 3]); pos += 4; } + else if (b0 == 30) operands.Add(ReadReal(ref pos)); + else if (b0 >= 32 && b0 <= 246) operands.Add(b0 - 139); + else if (b0 >= 247 && b0 <= 250) operands.Add((b0 - 247) * 256 + data[pos++] + 108); + else if (b0 >= 251 && b0 <= 254) operands.Add(-(b0 - 251) * 256 - data[pos++] - 108); + else throw new InvalidDataException("invalid CFF DICT"); + } + return res; + } + private double ReadReal(ref int pos) + { + System.Text.StringBuilder sb = new System.Text.StringBuilder(); + while (true) + { + int b = data[pos++]; + foreach (int nibble in new[] { b >> 4, b & 0xF }) + { + if (nibble <= 9) sb.Append((char)('0' + nibble)); + else if (nibble == 0xA) sb.Append('.'); + else if (nibble == 0xB) sb.Append('E'); + else if (nibble == 0xC) sb.Append("E-"); + else if (nibble == 0xE) sb.Append('-'); + else if (nibble == 0xF) + { + double.TryParse(sb.ToString(), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out double v); + return v; + } + } + } + } + private int U16(int pos) => (data[pos] << 8) | data[pos + 1]; + private static int Bias(int count) => count < 1240 ? 107 : count < 33900 ? 1131 : 32768; + + private sealed class ScalingSink : IGlyphOutlineSink + { + private readonly IGlyphOutlineSink sink; + private readonly double s; + public ScalingSink(IGlyphOutlineSink sink, double s) { this.sink = sink; this.s = s; } + public void MoveTo(double x, double y) => sink.MoveTo(x * s, y * s); + public void LineTo(double x, double y) => sink.LineTo(x * s, y * s); + public void CurveTo(double x1, double y1, double x2, double y2, double x3, double y3) => sink.CurveTo(x1 * s, y1 * s, x2 * s, y2 * s, x3 * s, y3 * s); + public void ClosePath() => sink.ClosePath(); + } + + /// + /// The Type 2 charstring interpreter (Adobe Technical Note #5177). + /// + private sealed class Interpreter + { + private readonly CffOutlines font; + private readonly List<(int start, int length)> local; + private readonly IGlyphOutlineSink sink; + private readonly List stack = new List(48); + private readonly double[] transient = new double[32]; + private double x, y; + private int stems; + private bool haveWidth, open, finished; + private int depth; + private readonly Random random = new Random(0); + + public Interpreter(CffOutlines font, List<(int start, int length)> local, IGlyphOutlineSink sink) + { + this.font = font; + this.local = local; + this.sink = sink; + } + public void Run((int start, int length) charString) + { + Execute(charString.start, charString.start + charString.length); + if (open) sink.ClosePath(); + open = false; + } + + private void Execute(int pos, int end) + { + if (++depth > 12) throw new InvalidDataException("CFF subroutines nested too deeply"); + byte[] d = font.data; + while (pos < end && !finished) + { + int b0 = d[pos++]; + if (b0 >= 32 || b0 == 28) + { // an operand + if (b0 == 28) { stack.Add((short)((d[pos] << 8) | d[pos + 1])); pos += 2; } + else if (b0 <= 246) stack.Add(b0 - 139); + else if (b0 <= 250) stack.Add((b0 - 247) * 256 + d[pos++] + 108); + else if (b0 <= 254) stack.Add(-(b0 - 251) * 256 - d[pos++] - 108); + else { stack.Add(((d[pos] << 24) | (d[pos + 1] << 16) | (d[pos + 2] << 8) | d[pos + 3]) / 65536.0); pos += 4; } + continue; + } + switch (b0) + { + case 1: // hstem + case 3: // vstem + case 18: // hstemhm + case 23: // vstemhm + Stems(); + break; + case 19: // hintmask + case 20: // cntrmask + Stems(); // the operands of an implicit vstemhm + pos += (stems + 7) / 8; + break; + case 21: // rmoveto + Width(2); + MoveTo(x + Arg(0), y + Arg(1)); + stack.Clear(); + break; + case 22: // hmoveto + Width(1); + MoveTo(x + Arg(0), y); + stack.Clear(); + break; + case 4: // vmoveto + Width(1); + MoveTo(x, y + Arg(0)); + stack.Clear(); + break; + case 5: // rlineto + for (int i = 0; i + 1 < stack.Count; i += 2) LineTo(x + stack[i], y + stack[i + 1]); + stack.Clear(); + break; + case 6: // hlineto + case 7: // vlineto + { + bool horizontal = b0 == 6; + for (int i = 0; i < stack.Count; ++i, horizontal = !horizontal) + { + if (horizontal) LineTo(x + stack[i], y); + else LineTo(x, y + stack[i]); + } + stack.Clear(); + break; + } + case 8: // rrcurveto + for (int i = 0; i + 5 < stack.Count; i += 6) Curve(stack[i], stack[i + 1], stack[i + 2], stack[i + 3], stack[i + 4], stack[i + 5]); + stack.Clear(); + break; + case 24: // rcurveline + { + int i = 0; + for (; i + 7 < stack.Count; i += 6) Curve(stack[i], stack[i + 1], stack[i + 2], stack[i + 3], stack[i + 4], stack[i + 5]); + if (i + 1 < stack.Count) LineTo(x + stack[i], y + stack[i + 1]); + stack.Clear(); + break; + } + case 25: // rlinecurve + { + int i = 0; + for (; i + 7 < stack.Count; i += 2) LineTo(x + stack[i], y + stack[i + 1]); + if (i + 5 < stack.Count) Curve(stack[i], stack[i + 1], stack[i + 2], stack[i + 3], stack[i + 4], stack[i + 5]); + stack.Clear(); + break; + } + case 26: // vvcurveto + { + int i = 0; + double dx1 = 0; + if (stack.Count % 2 == 1) dx1 = stack[i++]; + for (; i + 3 < stack.Count; i += 4) + { + Curve(dx1, stack[i], stack[i + 1], stack[i + 2], 0, stack[i + 3]); + dx1 = 0; + } + stack.Clear(); + break; + } + case 27: // hhcurveto + { + int i = 0; + double dy1 = 0; + if (stack.Count % 2 == 1) dy1 = stack[i++]; + for (; i + 3 < stack.Count; i += 4) + { + Curve(stack[i], dy1, stack[i + 1], stack[i + 2], stack[i + 3], 0); + dy1 = 0; + } + stack.Clear(); + break; + } + case 30: // vhcurveto + case 31: // hvcurveto + { + bool horizontal = b0 == 31; + int i = 0; + while (i + 3 < stack.Count) + { + double extra = stack.Count - i == 5 ? stack[i + 4] : 0; // the last curve may have a fifth operand + if (horizontal) Curve(stack[i], 0, stack[i + 1], stack[i + 2], extra, stack[i + 3]); + else Curve(0, stack[i], stack[i + 1], stack[i + 2], stack[i + 3], extra); + horizontal = !horizontal; + i += 4; + } + stack.Clear(); + break; + } + case 10: // callsubr + case 29: // callgsubr + { + List<(int start, int length)> subrs = b0 == 10 ? local : font.globalSubrs; + int index = (int)Pop() + Bias(subrs.Count); + if (index < 0 || index >= subrs.Count) throw new InvalidDataException("invalid CFF subroutine index"); + Execute(subrs[index].start, subrs[index].start + subrs[index].length); + break; + } + case 11: // return + --depth; + return; + case 14: // endchar (the accent composition "seac" with four operands is not supported) + if (!haveWidth && (stack.Count == 1 || stack.Count == 5)) stack.RemoveAt(0); + haveWidth = true; + if (open) sink.ClosePath(); + open = false; + finished = true; + stack.Clear(); + break; + case 12: + Escape(d[pos++]); + break; + default: // reserved operators are ignored + stack.Clear(); + break; + } + } + --depth; + } + + private void Escape(int op) + { + switch (op) + { + case 35: // flex + Curve(Arg(0), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5)); + Curve(Arg(6), Arg(7), Arg(8), Arg(9), Arg(10), Arg(11)); + break; + case 34: // hflex + Curve(Arg(0), 0, Arg(1), Arg(2), Arg(3), 0); + Curve(Arg(4), 0, Arg(5), -Arg(2), Arg(6), 0); + break; + case 36: // hflex1 + { + double dy1 = Arg(1), dy2 = Arg(3), dy5 = Arg(7); + Curve(Arg(0), dy1, Arg(2), dy2, Arg(4), 0); + Curve(Arg(5), 0, Arg(6), dy5, Arg(8), -(dy1 + dy2 + dy5)); + break; + } + case 37: // flex1 + { + double dx = 0, dy = 0; + for (int i = 0; i < 10; i += 2) { dx += Arg(i); dy += Arg(i + 1); } + Curve(Arg(0), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5)); + if (Math.Abs(dx) > Math.Abs(dy)) Curve(Arg(6), Arg(7), Arg(8), Arg(9), Arg(10), -dy); + else Curve(Arg(6), Arg(7), Arg(8), Arg(9), -dx, Arg(10)); + break; + } + // arithmetic and storage operators, rarely used + case 3: { double b = Pop(), a = Pop(); stack.Add(a != 0 && b != 0 ? 1 : 0); return; } // and + case 4: { double b = Pop(), a = Pop(); stack.Add(a != 0 || b != 0 ? 1 : 0); return; } // or + case 5: stack.Add(Pop() == 0 ? 1 : 0); return; // not + case 9: stack.Add(Math.Abs(Pop())); return; // abs + case 10: { double b = Pop(), a = Pop(); stack.Add(a + b); return; } // add + case 11: { double b = Pop(), a = Pop(); stack.Add(a - b); return; } // sub + case 12: { double b = Pop(), a = Pop(); stack.Add(b == 0 ? 0 : a / b); return; } // div + case 14: stack.Add(-Pop()); return; // neg + case 15: { double b = Pop(), a = Pop(); stack.Add(a == b ? 1 : 0); return; } // eq + case 18: Pop(); return; // drop + case 20: { int i = (int)Pop(); double v = Pop(); if (i >= 0 && i < transient.Length) transient[i] = v; return; } // put + case 21: { int i = (int)Pop(); stack.Add(i >= 0 && i < transient.Length ? transient[i] : 0); return; } // get + case 22: { double v2 = Pop(), v1 = Pop(), s2 = Pop(), s1 = Pop(); stack.Add(v1 <= v2 ? s1 : s2); return; } // ifelse + case 23: stack.Add(random.NextDouble() + double.Epsilon); return; // random + case 24: { double b = Pop(), a = Pop(); stack.Add(a * b); return; } // mul + case 26: stack.Add(Math.Sqrt(Math.Abs(Pop()))); return; // sqrt + case 27: { double a = Pop(); stack.Add(a); stack.Add(a); return; } // dup + case 28: { double b = Pop(), a = Pop(); stack.Add(b); stack.Add(a); return; } // exch + case 29: { int i = (int)Pop(); stack.Add(i >= 0 && i < stack.Count ? stack[stack.Count - 1 - i] : stack.Count > 0 ? stack[stack.Count - 1] : 0); return; } // index + case 30: // roll + { + int j = (int)Pop(), n = (int)Pop(); + if (n > 0 && n <= stack.Count) + { + double[] part = stack.GetRange(stack.Count - n, n).ToArray(); + for (int i = 0; i < n; ++i) stack[stack.Count - n + ((i + j) % n + n) % n] = part[i]; + } + return; + } + } + stack.Clear(); + } + + private void Stems() + { + if (!haveWidth && stack.Count % 2 == 1) stack.RemoveAt(0); + haveWidth = true; + stems += stack.Count / 2; + stack.Clear(); + } + private void Width(int expected) + { + if (!haveWidth && stack.Count > expected) stack.RemoveAt(0); + haveWidth = true; + } + private double Arg(int i) => i < stack.Count ? stack[i] : 0; + private double Pop() + { + if (stack.Count == 0) return 0; + double v = stack[stack.Count - 1]; + stack.RemoveAt(stack.Count - 1); + return v; + } + private void MoveTo(double nx, double ny) + { + if (open) sink.ClosePath(); + x = nx; + y = ny; + sink.MoveTo(x, y); + open = true; + } + private void LineTo(double nx, double ny) + { + x = nx; + y = ny; + sink.LineTo(x, y); + } + private void Curve(double dx1, double dy1, double dx2, double dy2, double dx3, double dy3) + { + double x1 = x + dx1, y1 = y + dy1, x2 = x1 + dx2, y2 = y1 + dy2; + x = x2 + dx3; + y = y2 + dy3; + sink.CurveTo(x1, y1, x2, y2, x, y); + } + } + } +} diff --git a/CADability/FontProvider.cs b/CADability/FontProvider.cs index e0c00708..52a9c5de 100644 --- a/CADability/FontProvider.cs +++ b/CADability/FontProvider.cs @@ -119,10 +119,11 @@ public abstract class FontFace } /// - /// 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. + /// The default : 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. /// public class ManagedFontProvider : FontProvider { @@ -177,7 +178,7 @@ public static IEnumerable SystemFontDirectories() } } /// - /// Adds the fonts of a font file (.ttf or .ttc). + /// Adds the fonts of a font file (.ttf, .ttc, .otf or .otc). /// public void AddFontFile(string fileName) { @@ -232,7 +233,7 @@ public override FontFace GetFace(string familyName, bool bold, bool italic) 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 + 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; @@ -258,7 +259,7 @@ private Dictionary> 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 { diff --git a/CADability/TrueTypeFont.cs b/CADability/TrueTypeFont.cs index 7428e83d..01230fab 100644 --- a/CADability/TrueTypeFont.cs +++ b/CADability/TrueTypeFont.cs @@ -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 - /// ( is false). + /// OpenType fonts with CFF outlines (most .otf files) are read with : their cubic curves go into + /// the path data unchanged. /// internal sealed class TrueTypeFont { @@ -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. /// public bool HasTrueTypeOutlines { get; } + /// + /// True for an OpenType font with outlines in the "CFF " table (Compact Font Format, cubic curves). + /// + public bool HasCffOutlines { get; } + /// + /// True if the glyph outlines can be read: TrueType or CFF outlines. False for bitmap fonts and for CFF2 (variable + /// OpenType fonts). + /// + public bool HasOutlines => HasTrueTypeOutlines || HasCffOutlines; + private CffOutlines cff; // created on demand public int GlyphCount => numGlyphs; /// @@ -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; @@ -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])); } @@ -351,7 +362,8 @@ public void GetPathData(int glyph, bool underline, bool strikeout, out PointF[] List tps = new List(); 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; @@ -443,6 +455,78 @@ void AddBar(int top, int thickness) types = tps.ToArray(); } + /// + /// Sends the outline of a glyph of a font with CFF outlines to , in font units. + /// + 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); + } + /// + /// Collects an outline as GDI+ path data (see ). + /// + private sealed class PathDataSink : IGlyphOutlineSink + { + private readonly List pts; + private readonly List tps; + private readonly Func map; + private int start = -1; // index of the start point of the open figure + private double cx, cy; + public PathDataSink(List pts, List tps, Func 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 res, int depth) { if (glyph < 0 || glyph >= numGlyphs || depth > 8) return; // a deeper nesting is a broken (or malicious) font diff --git a/REUSE.toml b/REUSE.toml index 1532e096..d4b1bbae 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -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. diff --git a/tests/CADability.Tests/CffOutlinesTests.cs b/tests/CADability.Tests/CffOutlinesTests.cs new file mode 100644 index 00000000..6937bc68 --- /dev/null +++ b/tests/CADability.Tests/CffOutlinesTests.cs @@ -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 +{ + /// + /// 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. + /// + [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); + } + } + } +} diff --git a/tests/CADability.Tests/Files/Fonts/InterCffTest.otf b/tests/CADability.Tests/Files/Fonts/InterCffTest.otf new file mode 100644 index 00000000..8afd85e9 Binary files /dev/null and b/tests/CADability.Tests/Files/Fonts/InterCffTest.otf differ diff --git a/tests/CADability.Tests/Files/Fonts/InterCffTest.reference.json b/tests/CADability.Tests/Files/Fonts/InterCffTest.reference.json new file mode 100644 index 00000000..381571f5 --- /dev/null +++ b/tests/CADability.Tests/Files/Fonts/InterCffTest.reference.json @@ -0,0 +1 @@ +{"glyphs":[{"char":" ","glyph":104,"commands":[]},{"char":"!","glyph":74,"commands":[["moveTo",[204,479]],["lineTo",[383,479]],["lineTo",[396,1490]],["lineTo",[192,1490]],["closePath",[]],["moveTo",[295,-13]],["curveTo",[369,-13,429,47,429,121]],["curveTo",[429,196,369,256,295,256]],["curveTo",[220,256,160,196,160,121]],["curveTo",[160,47,220,-13,295,-13]],["closePath",[]]]},{"char":"\"","glyph":89,"commands":[["moveTo",[232,923]],["lineTo",[382,923]],["lineTo",[404,1490]],["lineTo",[210,1490]],["closePath",[]],["moveTo",[572,923]],["lineTo",[722,923]],["lineTo",[744,1490]],["lineTo",[550,1490]],["closePath",[]]]},{"char":"#","glyph":83,"commands":[["moveTo",[711,0]],["lineTo",[864,0]],["lineTo",[931,411]],["lineTo",[1150,411]],["lineTo",[1175,561]],["lineTo",[956,561]],["lineTo",[1016,927]],["lineTo",[1238,927]],["lineTo",[1261,1079]],["lineTo",[1041,1079]],["lineTo",[1108,1490]],["lineTo",[957,1490]],["lineTo",[889,1079]],["lineTo",[517,1079]],["lineTo",[584,1490]],["lineTo",[433,1490]],["lineTo",[366,1079]],["lineTo",[147,1079]],["lineTo",[122,927]],["lineTo",[341,927]],["lineTo",[281,561]],["lineTo",[59,561]],["lineTo",[34,411]],["lineTo",[256,411]],["lineTo",[189,0]],["lineTo",[340,0]],["lineTo",[407,411]],["lineTo",[779,411]],["closePath",[]],["moveTo",[432,561]],["lineTo",[492,927]],["lineTo",[864,927]],["lineTo",[804,561]],["closePath",[]]]},{"char":"$","glyph":61,"commands":[["moveTo",[597,-218]],["lineTo",[717,-218]],["lineTo",[717,-24]],["curveTo",[1024,-3,1198,177,1198,406]],["curveTo",[1198,684,936,774,776,818]],["lineTo",[717,834]],["lineTo",[717,1339]],["curveTo",[867,1324,977,1244,992,1114]],["lineTo",[1178,1114]],["curveTo",[1169,1327,986,1491,717,1509]],["lineTo",[717,1708]],["lineTo",[597,1708]],["lineTo",[597,1506]],["curveTo",[345,1481,158,1321,158,1091]],["curveTo",[158,893,298,770,538,702]],["lineTo",[597,685]],["lineTo",[597,147]],["curveTo",[448,163,324,241,311,390]],["lineTo",[116,390]],["curveTo",[132,156,309,-5,597,-24]],["closePath",[]],["moveTo",[717,147]],["lineTo",[717,651]],["lineTo",[718,651]],["curveTo",[877,606,1013,553,1013,409]],["curveTo",[1013,266,889,166,717,147]],["closePath",[]],["moveTo",[597,868]],["curveTo",[489,899,343,958,343,1100]],["curveTo",[343,1223,442,1317,597,1338]],["closePath",[]]]},{"char":"%","glyph":102,"commands":[["moveTo",[1490,-29]],["curveTo",[1679,-29,1781,110,1781,274]],["lineTo",[1781,352]],["curveTo",[1781,515,1682,654,1490,654]],["curveTo",[1302,654,1197,516,1197,352]],["lineTo",[1197,274]],["curveTo",[1197,110,1299,-29,1490,-29]],["closePath",[]],["moveTo",[523,831]],["curveTo",[712,831,814,970,814,1134]],["lineTo",[814,1212]],["curveTo",[814,1375,715,1514,523,1514]],["curveTo",[335,1514,230,1376,230,1212]],["lineTo",[230,1134]],["curveTo",[230,970,332,831,523,831]],["closePath",[]],["moveTo",[404,0]],["lineTo",[573,0]],["lineTo",[1597,1490]],["lineTo",[1428,1490]],["closePath",[]],["moveTo",[1490,102]],["curveTo",[1387,102,1350,185,1350,274]],["lineTo",[1350,352]],["curveTo",[1350,440,1390,524,1490,524]],["curveTo",[1594,524,1628,440,1628,352]],["lineTo",[1628,274]],["curveTo",[1628,185,1591,102,1490,102]],["closePath",[]],["moveTo",[523,962]],["curveTo",[420,962,383,1045,383,1134]],["lineTo",[383,1212]],["curveTo",[383,1300,423,1384,523,1384]],["curveTo",[627,1384,661,1300,661,1212]],["lineTo",[661,1134]],["curveTo",[661,1045,624,962,523,962]],["closePath",[]]]},{"char":"&","glyph":73,"commands":[["moveTo",[580,-20]],["curveTo",[716,-20,868,27,967,124]],["lineTo",[1070,0]],["lineTo",[1287,0]],["lineTo",[1079,252]],["curveTo",[1137,326,1199,450,1199,658]],["lineTo",[1026,658]],["curveTo",[1026,556,1003,466,965,391]],["lineTo",[646,778]],["lineTo",[754,858]],["curveTo",[870,944,952,1046,952,1183]],["curveTo",[952,1360,814,1504,590,1504]],["curveTo",[369,1504,225,1364,225,1167]],["curveTo",[225,1031,294,930,398,805]],["curveTo",[227,679,104,578,104,389]],["curveTo",[104,159,285,-20,580,-20]],["closePath",[]],["moveTo",[859,253]],["curveTo",[777,178,674,137,567,137]],["curveTo",[402,137,289,236,289,383]],["curveTo",[289,475,333,546,456,637]],["lineTo",[507,675]],["closePath",[]],["moveTo",[538,908]],["curveTo",[459,1004,406,1078,406,1168]],["curveTo",[406,1275,482,1349,595,1349]],["curveTo",[703,1349,771,1281,771,1186]],["curveTo",[771,1081,689,1020,635,980]],["closePath",[]]]},{"char":"'","glyph":88,"commands":[["moveTo",[232,923]],["lineTo",[382,923]],["lineTo",[404,1490]],["lineTo",[210,1490]],["closePath",[]]]},{"char":"(","glyph":76,"commands":[["moveTo",[218,607]],["curveTo",[218,315,306,10,476,-279]],["lineTo",[651,-279]],["curveTo",[479,76,401,342,401,607]],["curveTo",[401,899,496,1266,651,1581]],["lineTo",[476,1581]],["curveTo",[326,1334,218,932,218,607]],["closePath",[]]]},{"char":")","glyph":77,"commands":[["moveTo",[96,-279]],["lineTo",[271,-279]],["curveTo",[438,5,529,310,529,607]],["curveTo",[529,931,421,1335,271,1581]],["lineTo",[96,1581]],["curveTo",[251,1266,346,899,346,607]],["curveTo",[346,350,273,86,96,-279]],["closePath",[]]]},{"char":"*","glyph":101,"commands":[["moveTo",[441,652]],["lineTo",[585,652]],["lineTo",[573,969]],["lineTo",[840,798]],["lineTo",[912,924]],["lineTo",[630,1070]],["lineTo",[912,1216]],["lineTo",[840,1344]],["lineTo",[573,1173]],["lineTo",[585,1490]],["lineTo",[441,1490]],["lineTo",[454,1173]],["lineTo",[186,1344]],["lineTo",[114,1216]],["lineTo",[396,1070]],["lineTo",[114,924]],["lineTo",[186,798]],["lineTo",[454,969]],["closePath",[]]]},{"char":"+","glyph":97,"commands":[["moveTo",[593,100]],["lineTo",[762,100]],["lineTo",[762,504]],["lineTo",[1160,504]],["lineTo",[1160,663]],["lineTo",[762,663]],["lineTo",[762,1066]],["lineTo",[593,1066]],["lineTo",[593,663]],["lineTo",[195,663]],["lineTo",[195,504]],["lineTo",[593,504]],["closePath",[]]]},{"char":",","glyph":90,"commands":[["moveTo",[128,-359]],["lineTo",[266,-359]],["lineTo",[422,208]],["lineTo",[216,208]],["closePath",[]]]},{"char":"-","glyph":87,"commands":[["moveTo",[798,719]],["lineTo",[144,719]],["lineTo",[144,553]],["lineTo",[798,553]],["closePath",[]]]},{"char":".","glyph":91,"commands":[["moveTo",[295,-13]],["curveTo",[370,-13,430,47,430,122]],["curveTo",[430,197,370,257,295,257]],["curveTo",[220,257,160,197,160,122]],["curveTo",[160,47,220,-13,295,-13]],["closePath",[]]]},{"char":"/","glyph":84,"commands":[["moveTo",[692,1560]],["lineTo",[526,1560]],["lineTo",[46,-224]],["lineTo",[212,-224]],["closePath",[]]]},{"char":"0","glyph":63,"commands":[["moveTo",[646,-20]],["curveTo",[979,-20,1170,259,1170,744]],["curveTo",[1170,1227,976,1510,646,1510]],["curveTo",[316,1510,122,1227,122,744]],["curveTo",[122,258,314,-20,646,-20]],["closePath",[]],["moveTo",[646,146]],["curveTo",[428,146,305,366,305,744]],["curveTo",[305,1123,428,1345,646,1345]],["curveTo",[864,1345,987,1124,987,744]],["curveTo",[987,366,864,146,646,146]],["closePath",[]]]},{"char":"1","glyph":64,"commands":[["moveTo",[653,1490]],["lineTo",[420,1490]],["lineTo",[96,1251]],["lineTo",[96,1047]],["lineTo",[456,1314]],["lineTo",[466,1314]],["lineTo",[466,0]],["lineTo",[653,0]],["closePath",[]]]},{"char":"2","glyph":65,"commands":[["moveTo",[154,0]],["lineTo",[1103,0]],["lineTo",[1103,167]],["lineTo",[416,167]],["lineTo",[416,179]],["lineTo",[742,526]],["curveTo",[1005,806,1078,933,1078,1094]],["curveTo",[1078,1327,889,1510,623,1510]],["curveTo",[358,1510,158,1331,158,1067]],["lineTo",[340,1067]],["curveTo",[340,1235,448,1345,618,1345]],["curveTo",[777,1345,898,1248,898,1092]],["curveTo",[898,955,814,853,649,674]],["lineTo",[154,137]],["closePath",[]]]},{"char":"3","glyph":66,"commands":[["moveTo",[635,-20]],["curveTo",[927,-20,1143,161,1143,405]],["curveTo",[1143,593,1032,731,842,762]],["lineTo",[842,774]],["curveTo",[994,819,1089,942,1089,1110]],["curveTo",[1089,1322,919,1510,641,1510]],["curveTo",[379,1510,165,1349,157,1115]],["lineTo",[340,1115]],["curveTo",[346,1260,485,1345,637,1345]],["curveTo",[799,1345,903,1248,903,1100]],["curveTo",[903,946,783,846,609,846]],["lineTo",[488,846]],["lineTo",[488,681]],["lineTo",[609,681]],["curveTo",[830,681,955,568,955,407]],["curveTo",[955,252,819,147,632,147]],["curveTo",[462,147,328,234,317,376]],["lineTo",[125,376]],["curveTo",[135,140,343,-20,635,-20]],["closePath",[]]]},{"char":"4","glyph":67,"commands":[["moveTo",[120,303]],["lineTo",[821,303]],["lineTo",[821,0]],["lineTo",[1003,0]],["lineTo",[1003,303]],["lineTo",[1205,303]],["lineTo",[1205,470]],["lineTo",[1003,470]],["lineTo",[1003,1490]],["lineTo",[772,1490]],["lineTo",[120,458]],["closePath",[]],["moveTo",[822,470]],["lineTo",[323,470]],["lineTo",[323,482]],["lineTo",[810,1252]],["lineTo",[822,1252]],["closePath",[]]]},{"char":"5","glyph":68,"commands":[["moveTo",[626,-20]],["curveTo",[913,-20,1123,190,1123,476]],["curveTo",[1123,764,922,975,649,975]],["curveTo",[546,975,447,939,386,888]],["lineTo",[378,888]],["lineTo",[429,1323]],["lineTo",[1053,1323]],["lineTo",[1053,1490]],["lineTo",[269,1490]],["lineTo",[181,752]],["lineTo",[359,730]],["curveTo",[418,775,522,808,614,808]],["curveTo",[801,808,937,665,937,473]],["curveTo",[937,284,806,147,626,147]],["curveTo",[476,147,354,243,342,376]],["lineTo",[158,376]],["curveTo",[168,148,364,-20,626,-20]],["closePath",[]]]},{"char":"6","glyph":69,"commands":[["moveTo",[646,-20]],["curveTo",[950,-20,1148,203,1148,481]],["curveTo",[1148,769,936,974,681,974]],["curveTo",[526,974,394,900,314,776]],["lineTo",[302,776]],["curveTo",[302,1134,440,1342,669,1342]],["curveTo",[824,1342,917,1244,950,1115]],["lineTo",[1136,1115]],["curveTo",[1100,1349,919,1510,669,1510]],["curveTo",[331,1510,122,1212,122,688]],["curveTo",[122,139,397,-20,646,-20]],["closePath",[]],["moveTo",[646,147]],["curveTo",[463,147,329,303,329,479]],["curveTo",[329,653,470,809,651,809]],["curveTo",[831,809,964,663,964,481]],["curveTo",[964,295,825,147,646,147]],["closePath",[]]]},{"char":"7","glyph":70,"commands":[["moveTo",[200,0]],["lineTo",[400,0]],["lineTo",[1061,1313]],["lineTo",[1061,1490]],["lineTo",[98,1490]],["lineTo",[98,1323]],["lineTo",[862,1323]],["lineTo",[862,1311]],["closePath",[]]]},{"char":"8","glyph":71,"commands":[["moveTo",[633,-20]],["curveTo",[933,-20,1145,153,1145,396]],["curveTo",[1145,584,1016,744,854,774]],["lineTo",[854,782]],["curveTo",[995,819,1089,956,1089,1114]],["curveTo",[1089,1341,894,1510,633,1510]],["curveTo",[368,1510,176,1341,176,1114]],["curveTo",[176,956,268,819,413,782]],["lineTo",[413,774]],["curveTo",[246,744,122,584,122,396]],["curveTo",[122,153,330,-20,633,-20]],["closePath",[]],["moveTo",[633,145]],["curveTo",[432,145,309,248,309,406]],["curveTo",[309,571,445,689,633,689]],["curveTo",[817,689,955,571,955,406]],["curveTo",[955,248,830,145,633,145]],["closePath",[]],["moveTo",[633,848]],["curveTo",[472,848,360,949,360,1100]],["curveTo",[360,1249,468,1346,633,1346]],["curveTo",[795,1346,905,1249,905,1100]],["curveTo",[905,949,790,848,633,848]],["closePath",[]]]},{"char":"9","glyph":72,"commands":[["moveTo",[603,-20]],["curveTo",[940,-20,1148,277,1148,804]],["curveTo",[1148,1352,873,1506,628,1510]],["curveTo",[322,1515,122,1288,122,1010]],["curveTo",[122,723,334,517,591,517]],["curveTo",[747,517,878,594,958,716]],["lineTo",[970,716]],["curveTo",[970,355,830,148,603,148]],["curveTo",[446,148,353,247,322,379]],["lineTo",[134,379]],["curveTo",[169,141,350,-20,603,-20]],["closePath",[]],["moveTo",[619,683]],["curveTo",[439,683,308,828,308,1011]],["curveTo",[308,1195,445,1343,625,1343]],["curveTo",[808,1343,943,1186,943,1015]],["curveTo",[943,840,800,683,619,683]],["closePath",[]]]},{"char":":","glyph":92,"commands":[["moveTo",[295,-13]],["curveTo",[370,-13,430,47,430,122]],["curveTo",[430,197,370,257,295,257]],["curveTo",[220,257,160,197,160,122]],["curveTo",[160,47,220,-13,295,-13]],["closePath",[]],["moveTo",[295,796]],["curveTo",[370,796,430,856,430,931]],["curveTo",[430,1006,370,1066,295,1066]],["curveTo",[220,1066,160,1006,160,931]],["curveTo",[160,856,220,796,295,796]],["closePath",[]]]},{"char":";","glyph":93,"commands":[["moveTo",[128,-359]],["lineTo",[266,-359]],["lineTo",[422,208]],["lineTo",[216,208]],["closePath",[]],["moveTo",[323,796]],["curveTo",[398,796,458,856,458,931]],["curveTo",[458,1006,398,1066,323,1066]],["curveTo",[248,1066,188,1006,188,931]],["curveTo",[188,856,248,796,323,796]],["closePath",[]]]},{"char":"<","glyph":94,"commands":[["moveTo",[174,520]],["lineTo",[1118,44]],["lineTo",[1118,238]],["lineTo",[406,576]],["lineTo",[406,588]],["lineTo",[1118,926]],["lineTo",[1118,1122]],["lineTo",[174,646]],["closePath",[]]]},{"char":"=","glyph":96,"commands":[["moveTo",[226,283]],["lineTo",[1130,283]],["lineTo",[1130,449]],["lineTo",[226,449]],["closePath",[]],["moveTo",[226,717]],["lineTo",[1130,717]],["lineTo",[1130,883]],["lineTo",[226,883]],["closePath",[]]]},{"char":">","glyph":95,"commands":[["moveTo",[1181,520]],["lineTo",[1181,646]],["lineTo",[237,1122]],["lineTo",[237,926]],["lineTo",[949,588]],["lineTo",[949,576]],["lineTo",[237,238]],["lineTo",[237,44]],["closePath",[]]]},{"char":"?","glyph":75,"commands":[["moveTo",[383,449]],["lineTo",[564,449]],["lineTo",[564,460]],["curveTo",[564,637,613,686,742,765]],["curveTo",[871,844,945,956,945,1117]],["curveTo",[945,1345,777,1510,518,1510]],["curveTo",[282,1510,92,1366,82,1115]],["lineTo",[272,1115]],["curveTo",[283,1281,400,1351,518,1351]],["curveTo",[653,1351,761,1263,761,1124]],["curveTo",[761,1008,693,927,605,874]],["curveTo",[467,791,383,733,383,460]],["closePath",[]],["moveTo",[476,-13]],["curveTo",[550,-13,610,47,610,121]],["curveTo",[610,196,550,256,476,256]],["curveTo",[401,256,341,196,341,121]],["curveTo",[341,47,401,-13,476,-13]],["closePath",[]]]},{"char":"@","glyph":82,"commands":[["moveTo",[1044,-395]],["curveTo",[1195,-395,1384,-352,1476,-313]],["lineTo",[1433,-173]],["curveTo",[1374,-193,1210,-250,1051,-250]],["curveTo",[555,-250,289,28,289,520]],["curveTo",[289,1005,559,1294,1017,1294]],["curveTo",[1479,1294,1697,973,1697,642]],["curveTo",[1697,316,1662,171,1516,171]],["curveTo",[1430,171,1385,241,1385,321]],["lineTo",[1385,980]],["lineTo",[1227,980]],["lineTo",[1227,896]],["lineTo",[1217,896]],["curveTo",[1181,947,1093,1004,965,1004]],["curveTo",[726,1004,552,803,552,535]],["curveTo",[552,253,689,37,955,37]],["curveTo",[1106,37,1193,112,1228,198]],["lineTo",[1236,198]],["curveTo",[1254,74,1408,30,1512,30]],["curveTo",[1820,30,1856,355,1856,640]],["curveTo",[1856,1046,1571,1438,1023,1438]],["curveTo",[458,1438,122,1094,122,525]],["curveTo",[122,-63,449,-395,1044,-395]],["closePath",[]],["moveTo",[962,190]],["curveTo",[776,190,707,358,707,543]],["curveTo",[707,710,803,853,967,853]],["curveTo",[1141,853,1233,782,1233,543]],["curveTo",[1233,306,1155,190,962,190]],["closePath",[]]]},{"char":"A","glyph":1,"commands":[["moveTo",[52,0]],["lineTo",[254,0]],["lineTo",[398,416]],["lineTo",[1012,416]],["lineTo",[1161,0]],["lineTo",[1361,0]],["lineTo",[813,1490]],["lineTo",[593,1490]],["closePath",[]],["moveTo",[456,582]],["lineTo",[561,884]],["curveTo",[593,976,639,1118,702,1338]],["curveTo",[765,1122,809,984,845,884]],["lineTo",[953,582]],["closePath",[]]]},{"char":"B","glyph":3,"commands":[["moveTo",[180,0]],["lineTo",[725,0]],["curveTo",[1076,0,1224,172,1224,397]],["curveTo",[1224,631,1061,759,922,768]],["lineTo",[922,782]],["curveTo",[1052,817,1166,904,1166,1095]],["curveTo",[1166,1313,1019,1490,708,1490]],["lineTo",[180,1490]],["closePath",[]],["moveTo",[370,168]],["lineTo",[370,676]],["lineTo",[730,676]],["curveTo",[921,676,1041,548,1041,399]],["curveTo",[1041,270,952,168,720,168]],["closePath",[]],["moveTo",[370,836]],["lineTo",[370,1322]],["lineTo",[704,1322]],["curveTo",[897,1322,984,1220,984,1091]],["curveTo",[984,937,858,836,698,836]],["closePath",[]]]},{"char":"C","glyph":4,"commands":[["moveTo",[783,-20]],["curveTo",[1088,-20,1332,166,1384,469]],["lineTo",[1194,469]],["curveTo",[1152,266,978,156,783,156]],["curveTo",[516,156,307,363,307,744]],["curveTo",[307,1126,515,1334,783,1334]],["curveTo",[977,1334,1152,1225,1194,1021]],["lineTo",[1384,1021]],["curveTo",[1331,1328,1085,1510,783,1510]],["curveTo",[401,1510,122,1217,122,744]],["curveTo",[122,272,401,-20,783,-20]],["closePath",[]]]},{"char":"D","glyph":5,"commands":[["moveTo",[645,0]],["curveTo",[1099,0,1356,282,1356,748]],["curveTo",[1356,1210,1099,1490,664,1490]],["lineTo",[180,1490]],["lineTo",[180,0]],["closePath",[]],["moveTo",[370,168]],["lineTo",[370,1322]],["lineTo",[651,1322]],["curveTo",[996,1322,1172,1106,1172,748]],["curveTo",[1172,386,996,168,633,168]],["closePath",[]]]},{"char":"E","glyph":6,"commands":[["moveTo",[180,0]],["lineTo",[1097,0]],["lineTo",[1097,168]],["lineTo",[370,168]],["lineTo",[370,668]],["lineTo",[1039,668]],["lineTo",[1039,836]],["lineTo",[370,836]],["lineTo",[370,1322]],["lineTo",[1088,1322]],["lineTo",[1088,1490]],["lineTo",[180,1490]],["closePath",[]]]},{"char":"F","glyph":7,"commands":[["moveTo",[180,0]],["lineTo",[370,0]],["lineTo",[370,644]],["lineTo",[1013,644]],["lineTo",[1013,812]],["lineTo",[370,812]],["lineTo",[370,1322]],["lineTo",[1081,1322]],["lineTo",[1081,1490]],["lineTo",[180,1490]],["closePath",[]]]},{"char":"G","glyph":8,"commands":[["moveTo",[789,-20]],["curveTo",[1144,-20,1400,216,1400,586]],["lineTo",[1400,749]],["lineTo",[828,749]],["lineTo",[828,583]],["lineTo",[1214,583]],["curveTo",[1209,321,1038,156,789,156]],["curveTo",[515,156,307,364,307,744]],["curveTo",[307,1127,516,1334,778,1334]],["curveTo",[989,1334,1132,1214,1193,1023]],["lineTo",[1389,1023]],["curveTo",[1335,1312,1092,1510,777,1510]],["curveTo",[395,1510,122,1217,122,744]],["curveTo",[122,273,391,-20,789,-20]],["closePath",[]]]},{"char":"H","glyph":9,"commands":[["moveTo",[180,0]],["lineTo",[370,0]],["lineTo",[370,678]],["lineTo",[1152,678]],["lineTo",[1152,0]],["lineTo",[1342,0]],["lineTo",[1342,1490]],["lineTo",[1152,1490]],["lineTo",[1152,846]],["lineTo",[370,846]],["lineTo",[370,1490]],["lineTo",[180,1490]],["closePath",[]]]},{"char":"I","glyph":10,"commands":[["moveTo",[370,1490]],["lineTo",[180,1490]],["lineTo",[180,0]],["lineTo",[370,0]],["closePath",[]]]},{"char":"J","glyph":11,"commands":[["moveTo",[545,-20]],["curveTo",[808,-20,989,140,989,426]],["lineTo",[989,1490]],["lineTo",[799,1490]],["lineTo",[799,426]],["curveTo",[799,244,697,147,545,147]],["curveTo",[392,147,290,244,290,426]],["lineTo",[290,506]],["lineTo",[100,506]],["lineTo",[100,426]],["curveTo",[100,140,282,-20,545,-20]],["closePath",[]]]},{"char":"K","glyph":12,"commands":[["moveTo",[180,0]],["lineTo",[370,0]],["lineTo",[370,489]],["lineTo",[585,716]],["lineTo",[1094,0]],["lineTo",[1319,0]],["lineTo",[716,845]],["lineTo",[1318,1490]],["lineTo",[1069,1490]],["lineTo",[624,1009]],["curveTo",[533,911,450,813,367,706]],["lineTo",[370,980]],["lineTo",[370,1490]],["lineTo",[180,1490]],["closePath",[]]]},{"char":"L","glyph":13,"commands":[["moveTo",[180,0]],["lineTo",[1060,0]],["lineTo",[1060,168]],["lineTo",[370,168]],["lineTo",[370,1490]],["lineTo",[180,1490]],["closePath",[]]]},{"char":"M","glyph":14,"commands":[["moveTo",[180,0]],["lineTo",[361,0]],["lineTo",[361,851]],["curveTo",[361,966,357,1142,354,1284]],["curveTo",[413,1109,471,935,504,851]],["lineTo",[842,0]],["lineTo",[1007,0]],["lineTo",[1340,851]],["curveTo",[1374,938,1436,1117,1490,1287]],["curveTo",[1486,1132,1483,961,1483,851]],["lineTo",[1483,0]],["lineTo",[1670,0]],["lineTo",[1670,1490]],["lineTo",[1399,1490]],["lineTo",[1037,550]],["curveTo",[1008,475,962,328,927,211]],["curveTo",[892,332,845,478,817,550]],["lineTo",[450,1490]],["lineTo",[180,1490]],["closePath",[]]]},{"char":"N","glyph":15,"commands":[["moveTo",[180,0]],["lineTo",[371,0]],["lineTo",[371,850]],["curveTo",[371,922,364,1071,356,1262]],["curveTo",[472,1055,529,955,597,848]],["lineTo",[1137,0]],["lineTo",[1363,0]],["lineTo",[1363,1490]],["lineTo",[1173,1490]],["lineTo",[1173,576]],["curveTo",[1173,496,1174,369,1186,225]],["curveTo",[1118,360,1050,474,1010,537]],["lineTo",[404,1490]],["lineTo",[180,1490]],["closePath",[]]]},{"char":"O","glyph":16,"commands":[["moveTo",[784,-20]],["curveTo",[1166,-20,1444,272,1444,744]],["curveTo",[1444,1217,1166,1510,784,1510]],["curveTo",[402,1510,122,1217,122,744]],["curveTo",[122,272,402,-20,784,-20]],["closePath",[]],["moveTo",[784,156]],["curveTo",[516,156,307,364,307,744]],["curveTo",[307,1126,516,1334,784,1334]],["curveTo",[1050,1334,1259,1127,1259,744]],["curveTo",[1259,363,1050,156,784,156]],["closePath",[]]]},{"char":"P","glyph":18,"commands":[["moveTo",[180,0]],["lineTo",[370,0]],["lineTo",[370,539]],["lineTo",[691,539]],["curveTo",[1038,539,1198,750,1198,1016]],["curveTo",[1198,1281,1038,1490,690,1490]],["lineTo",[180,1490]],["closePath",[]],["moveTo",[370,706]],["lineTo",[370,1323]],["lineTo",[680,1323]],["curveTo",[918,1323,1009,1192,1009,1016]],["curveTo",[1009,839,918,706,682,706]],["closePath",[]]]},{"char":"Q","glyph":19,"commands":[["moveTo",[784,-20]],["curveTo",[880,-20,969,-2,1049,33]],["lineTo",[1178,-140]],["lineTo",[1376,-140]],["lineTo",[1184,115]],["curveTo",[1345,245,1444,462,1444,744]],["curveTo",[1444,1217,1166,1510,784,1510]],["curveTo",[402,1510,122,1217,122,744]],["curveTo",[122,272,402,-20,784,-20]],["closePath",[]],["moveTo",[722,466]],["lineTo",[938,181]],["curveTo",[890,165,838,156,784,156]],["curveTo",[516,156,307,364,307,744]],["curveTo",[307,1126,516,1334,784,1334]],["curveTo",[1050,1334,1259,1127,1259,744]],["curveTo",[1259,522,1188,359,1076,261]],["lineTo",[920,466]],["closePath",[]]]},{"char":"R","glyph":20,"commands":[["moveTo",[180,0]],["lineTo",[370,0]],["lineTo",[370,578]],["lineTo",[691,578]],["curveTo",[703,578,714,578,726,579]],["lineTo",[1036,0]],["lineTo",[1256,0]],["lineTo",[922,613]],["curveTo",[1110,679,1198,832,1198,1030]],["curveTo",[1198,1296,1038,1490,690,1490]],["lineTo",[180,1490]],["closePath",[]],["moveTo",[370,747]],["lineTo",[370,1323]],["lineTo",[680,1323]],["curveTo",[917,1323,1009,1207,1009,1030]],["curveTo",[1009,855,917,747,682,747]],["closePath",[]]]},{"char":"S","glyph":21,"commands":[["moveTo",[657,-26]],["curveTo",[1001,-26,1198,162,1198,406]],["curveTo",[1198,684,936,774,776,818]],["lineTo",[627,859]],["curveTo",[518,889,343,944,343,1100]],["curveTo",[343,1240,471,1342,664,1342]],["curveTo",[841,1342,975,1259,992,1114]],["lineTo",[1178,1114]],["curveTo",[1169,1339,964,1510,670,1510]],["curveTo",[382,1510,158,1343,158,1091]],["curveTo",[158,893,298,770,538,702]],["lineTo",[718,651]],["curveTo",[877,606,1013,553,1013,409]],["curveTo",[1013,250,859,144,657,144]],["curveTo",[482,144,326,221,311,390]],["lineTo",[116,390]],["curveTo",[133,140,334,-26,657,-26]],["closePath",[]]]},{"char":"T","glyph":22,"commands":[["moveTo",[98,1322]],["lineTo",[567,1322]],["lineTo",[567,0]],["lineTo",[757,0]],["lineTo",[757,1322]],["lineTo",[1224,1322]],["lineTo",[1224,1490]],["lineTo",[98,1490]],["closePath",[]]]},{"char":"U","glyph":23,"commands":[["moveTo",[763,-24]],["curveTo",[1114,-24,1344,200,1344,507]],["lineTo",[1344,1490]],["lineTo",[1154,1490]],["lineTo",[1154,522]],["curveTo",[1154,308,1006,152,763,152]],["curveTo",[519,152,370,308,370,522]],["lineTo",[370,1490]],["lineTo",[180,1490]],["lineTo",[180,507]],["curveTo",[180,200,410,-24,763,-24]],["closePath",[]]]},{"char":"V","glyph":25,"commands":[["moveTo",[600,0]],["lineTo",[819,0]],["lineTo",[1361,1490]],["lineTo",[1159,1490]],["lineTo",[852,606]],["curveTo",[820,514,773,372,710,154]],["curveTo",[648,367,600,514,567,606]],["lineTo",[252,1490]],["lineTo",[52,1490]],["closePath",[]]]},{"char":"W","glyph":26,"commands":[["moveTo",[458,0]],["lineTo",[680,0]],["lineTo",[935,944]],["curveTo",[964,1052,986,1153,1008,1275]],["curveTo",[1031,1144,1055,1039,1081,944]],["lineTo",[1337,0]],["lineTo",[1558,0]],["lineTo",[1966,1490]],["lineTo",[1770,1490]],["lineTo",[1535,576]],["curveTo",[1503,450,1473,321,1447,183]],["curveTo",[1419,321,1389,450,1356,576]],["lineTo",[1117,1490]],["lineTo",[901,1490]],["lineTo",[660,576]],["curveTo",[626,448,597,317,570,177]],["curveTo",[543,317,514,448,482,576]],["lineTo",[246,1490]],["lineTo",[52,1490]],["closePath",[]]]},{"char":"X","glyph":27,"commands":[["moveTo",[57,0]],["lineTo",[273,0]],["lineTo",[536,378]],["curveTo",[613,488,650,546,699,640]],["curveTo",[749,542,785,487,860,378]],["lineTo",[1121,0]],["lineTo",[1340,0]],["lineTo",[811,770]],["lineTo",[1312,1490]],["lineTo",[1097,1490]],["lineTo",[878,1174]],["curveTo",[790,1047,754,989,701,885]],["curveTo",[647,990,612,1046,525,1174]],["lineTo",[310,1490]],["lineTo",[90,1490]],["lineTo",[591,763]],["closePath",[]]]},{"char":"Y","glyph":28,"commands":[["moveTo",[600,0]],["lineTo",[790,0]],["lineTo",[790,610]],["lineTo",[1338,1490]],["lineTo",[1119,1490]],["lineTo",[825,1009]],["curveTo",[777,930,737,862,697,766]],["curveTo",[657,859,617,927,567,1009]],["lineTo",[272,1490]],["lineTo",[52,1490]],["lineTo",[600,610]],["closePath",[]]]},{"char":"Z","glyph":29,"commands":[["moveTo",[130,0]],["lineTo",[1166,0]],["lineTo",[1166,168]],["lineTo",[640,168]],["curveTo",[541,168,443,167,345,163]],["curveTo",[399,234,453,307,502,379]],["lineTo",[1158,1354]],["lineTo",[1158,1490]],["lineTo",[122,1490]],["lineTo",[122,1322]],["lineTo",[645,1322]],["curveTo",[744,1322,844,1323,943,1327]],["curveTo",[892,1260,841,1192,796,1125]],["lineTo",[130,134]],["closePath",[]]]},{"char":"[","glyph":78,"commands":[["moveTo",[226,-279]],["lineTo",[624,-279]],["lineTo",[624,-126]],["lineTo",[398,-126]],["lineTo",[398,1428]],["lineTo",[624,1428]],["lineTo",[624,1581]],["lineTo",[226,1581]],["closePath",[]]]},{"char":"\\","glyph":86,"commands":[["moveTo",[526,-224]],["lineTo",[692,-224]],["lineTo",[212,1560]],["lineTo",[46,1560]],["closePath",[]]]},{"char":"]","glyph":79,"commands":[["moveTo",[123,-279]],["lineTo",[521,-279]],["lineTo",[521,1581]],["lineTo",[123,1581]],["lineTo",[123,1428]],["lineTo",[349,1428]],["lineTo",[349,-126]],["lineTo",[123,-126]],["closePath",[]]]},{"char":"^","glyph":100,"commands":[["moveTo",[82,814]],["lineTo",[261,814]],["lineTo",[476,1285]],["lineTo",[489,1285]],["lineTo",[706,814]],["lineTo",[883,814]],["lineTo",[582,1443]],["lineTo",[383,1443]],["closePath",[]]]},{"char":"_","glyph":99,"commands":[["moveTo",[934,0]],["lineTo",[0,0]],["lineTo",[0,-166]],["lineTo",[934,-166]],["closePath",[]]]},{"char":"`","glyph":103,"commands":[["moveTo",[343,1264]],["lineTo",[491,1264]],["lineTo",[351,1558]],["lineTo",[150,1558]],["closePath",[]]]},{"char":"a","glyph":30,"commands":[["moveTo",[471,-26]],["curveTo",[660,-26,765,73,803,152]],["lineTo",[815,152]],["lineTo",[815,0]],["lineTo",[992,0]],["lineTo",[992,737]],["curveTo",[992,1086,723,1132,573,1132]],["curveTo",[400,1132,208,1070,122,867]],["lineTo",[295,810]],["curveTo",[330,888,417,974,578,974]],["curveTo",[733,974,812,894,812,755]],["lineTo",[812,749]],["curveTo",[812,666,724,672,525,647]],["curveTo",[318,621,90,574,90,318]],["curveTo",[90,98,259,-26,471,-26]],["closePath",[]],["moveTo",[502,133]],["curveTo",[368,133,271,193,271,309]],["curveTo",[271,436,386,478,517,495]],["curveTo",[586,504,782,524,812,559]],["lineTo",[812,404]],["curveTo",[812,267,701,133,502,133]],["closePath",[]]]},{"char":"b","glyph":32,"commands":[["moveTo",[677,-24]],["curveTo",[956,-24,1150,200,1150,556]],["curveTo",[1150,910,955,1132,676,1132]],["curveTo",[458,1132,389,998,352,939]],["lineTo",[338,939]],["lineTo",[338,1490]],["lineTo",[158,1490]],["lineTo",[158,0]],["lineTo",[332,0]],["lineTo",[332,173]],["lineTo",[352,173]],["curveTo",[389,111,464,-24,677,-24]],["closePath",[]],["moveTo",[650,137]],["curveTo",[447,137,336,309,336,558]],["curveTo",[336,805,444,971,650,971]],["curveTo",[862,971,967,790,967,558]],["curveTo",[967,323,859,137,650,137]],["closePath",[]]]},{"char":"c","glyph":33,"commands":[["moveTo",[613,-24]],["curveTo",[832,-24,1010,95,1070,294]],["lineTo",[898,343]],["curveTo",[865,222,763,137,613,137]],["curveTo",[390,137,287,332,287,552]],["curveTo",[287,775,390,971,613,971]],["curveTo",[760,971,860,889,893,774]],["lineTo",[1066,823]],["curveTo",[1005,1018,829,1132,613,1132]],["curveTo",[309,1132,104,902,104,552]],["curveTo",[104,207,309,-24,613,-24]],["closePath",[]]]},{"char":"d","glyph":34,"commands":[["moveTo",[577,-24]],["curveTo",[790,-24,865,111,902,173]],["lineTo",[922,173]],["lineTo",[922,0]],["lineTo",[1096,0]],["lineTo",[1096,1490]],["lineTo",[916,1490]],["lineTo",[916,939]],["lineTo",[902,939]],["curveTo",[865,998,796,1132,578,1132]],["curveTo",[299,1132,104,910,104,556]],["curveTo",[104,200,298,-24,577,-24]],["closePath",[]],["moveTo",[604,137]],["curveTo",[395,137,287,323,287,558]],["curveTo",[287,790,392,971,604,971]],["curveTo",[810,971,918,805,918,558]],["curveTo",[918,309,807,137,604,137]],["closePath",[]]]},{"char":"e","glyph":35,"commands":[["moveTo",[628,-24]],["curveTo",[857,-24,1024,91,1076,260]],["lineTo",[902,308]],["curveTo",[860,194,761,136,629,136]],["curveTo",[431,136,295,264,286,498]],["lineTo",[1094,498]],["lineTo",[1094,573]],["curveTo",[1094,986,844,1132,607,1132]],["curveTo",[302,1132,104,893,104,550]],["curveTo",[104,206,304,-24,628,-24]],["closePath",[]],["moveTo",[287,650]],["curveTo",[298,821,415,972,607,972]],["curveTo",[791,972,894,840,911,650]],["closePath",[]]]},{"char":"f","glyph":37,"commands":[["moveTo",[678,1118]],["lineTo",[420,1118]],["lineTo",[420,1227]],["curveTo",[420,1335,468,1393,579,1393]],["curveTo",[629,1393,657,1382,676,1376]],["lineTo",[726,1530]],["curveTo",[699,1542,643,1560,558,1560]],["curveTo",[395,1560,240,1461,240,1267]],["lineTo",[240,1118]],["lineTo",[20,1118]],["lineTo",[20,964]],["lineTo",[240,964]],["lineTo",[240,0]],["lineTo",[420,0]],["lineTo",[420,964]],["lineTo",[678,964]],["closePath",[]]]},{"char":"g","glyph":38,"commands":[["moveTo",[611,-442]],["curveTo",[883,-442,1098,-318,1098,-29]],["lineTo",[1098,1118]],["lineTo",[923,1118]],["lineTo",[923,939]],["lineTo",[906,939]],["curveTo",[868,999,796,1132,580,1132]],["curveTo",[301,1132,104,910,104,564]],["curveTo",[104,213,308,18,576,18]],["curveTo",[792,18,863,141,901,205]],["lineTo",[918,205]],["lineTo",[918,-19]],["curveTo",[918,-203,789,-285,611,-285]],["curveTo",[414,-285,344,-185,297,-120]],["lineTo",[151,-214]],["curveTo",[220,-337,363,-442,611,-442]],["closePath",[]],["moveTo",[606,178]],["curveTo",[396,178,287,338,287,568]],["curveTo",[287,791,393,971,606,971]],["curveTo",[812,971,920,805,920,568]],["curveTo",[920,324,809,178,606,178]],["closePath",[]]]},{"char":"h","glyph":39,"commands":[["moveTo",[338,670]],["curveTo",[338,861,459,970,620,970]],["curveTo",[776,970,872,867,872,695]],["lineTo",[872,0]],["lineTo",[1053,0]],["lineTo",[1053,710]],["curveTo",[1053,993,900,1132,674,1132]],["curveTo",[528,1132,411,1073,338,924]],["lineTo",[338,1490]],["lineTo",[158,1490]],["lineTo",[158,0]],["lineTo",[338,0]],["closePath",[]]]},{"char":"i","glyph":40,"commands":[["moveTo",[158,0]],["lineTo",[338,0]],["lineTo",[338,1118]],["lineTo",[158,1118]],["closePath",[]],["moveTo",[249,1301]],["curveTo",[318,1301,375,1354,375,1420]],["curveTo",[375,1486,318,1539,249,1539]],["curveTo",[181,1539,124,1486,124,1420]],["curveTo",[124,1354,181,1301,249,1301]],["closePath",[]]]},{"char":"j","glyph":41,"commands":[["moveTo",[157,1118]],["lineTo",[157,-80]],["curveTo",[157,-191,111,-252,7,-252]],["lineTo",[-26,-252]],["lineTo",[-26,-418]],["lineTo",[10,-418]],["curveTo",[227,-418,339,-293,338,-80]],["lineTo",[338,1118]],["closePath",[]],["moveTo",[248,1301]],["curveTo",[317,1301,374,1354,374,1420]],["curveTo",[374,1486,317,1539,248,1539]],["curveTo",[180,1539,123,1486,123,1420]],["curveTo",[123,1354,180,1301,248,1301]],["closePath",[]]]},{"char":"k","glyph":42,"commands":[["moveTo",[158,0]],["lineTo",[338,0]],["lineTo",[338,412]],["lineTo",[456,523]],["lineTo",[865,0]],["lineTo",[1096,0]],["lineTo",[593,638]],["lineTo",[1061,1118]],["lineTo",[838,1118]],["lineTo",[360,625]],["lineTo",[338,625]],["lineTo",[338,1490]],["lineTo",[158,1490]],["closePath",[]]]},{"char":"l","glyph":43,"commands":[["moveTo",[338,1490]],["lineTo",[158,1490]],["lineTo",[158,0]],["lineTo",[338,0]],["closePath",[]]]},{"char":"m","glyph":44,"commands":[["moveTo",[158,0]],["lineTo",[338,0]],["lineTo",[338,704]],["curveTo",[338,882,457,974,588,974]],["curveTo",[714,974,807,891,807,767]],["lineTo",[807,0]],["lineTo",[987,0]],["lineTo",[987,730]],["curveTo",[987,871,1079,974,1230,974]],["curveTo",[1350,974,1455,910,1455,749]],["lineTo",[1455,0]],["lineTo",[1636,0]],["lineTo",[1636,754]],["curveTo",[1636,1013,1478,1138,1284,1138]],["curveTo",[1122,1138,985,1051,932,910]],["curveTo",[892,1048,794,1138,647,1138]],["curveTo",[515,1138,389,1064,334,913]],["lineTo",[333,1118]],["lineTo",[158,1118]],["closePath",[]]]},{"char":"n","glyph":45,"commands":[["moveTo",[338,670]],["curveTo",[338,861,459,970,620,970]],["curveTo",[776,970,872,867,872,695]],["lineTo",[872,0]],["lineTo",[1052,0]],["lineTo",[1052,710]],["curveTo",[1052,993,900,1132,674,1132]],["curveTo",[524,1132,404,1069,332,911]],["lineTo",[331,1118]],["lineTo",[158,1118]],["lineTo",[158,0]],["lineTo",[338,0]],["closePath",[]]]},{"char":"o","glyph":46,"commands":[["moveTo",[613,-24]],["curveTo",[918,-24,1124,207,1124,552]],["curveTo",[1124,902,918,1132,613,1132]],["curveTo",[309,1132,104,902,104,552]],["curveTo",[104,207,309,-24,613,-24]],["closePath",[]],["moveTo",[613,137]],["curveTo",[390,137,287,332,287,552]],["curveTo",[287,775,390,971,613,971]],["curveTo",[839,971,942,774,942,552]],["curveTo",[942,332,839,137,613,137]],["closePath",[]]]},{"char":"p","glyph":48,"commands":[["moveTo",[158,-418]],["lineTo",[338,-418]],["lineTo",[338,173]],["lineTo",[352,173]],["curveTo",[389,111,464,-24,677,-24]],["curveTo",[956,-24,1150,200,1150,556]],["curveTo",[1150,910,955,1132,676,1132]],["curveTo",[458,1132,389,998,352,939]],["lineTo",[332,939]],["lineTo",[332,1118]],["lineTo",[158,1118]],["closePath",[]],["moveTo",[650,137]],["curveTo",[447,137,336,309,336,558]],["curveTo",[336,805,444,971,650,971]],["curveTo",[862,971,967,790,967,558]],["curveTo",[967,323,859,137,650,137]],["closePath",[]]]},{"char":"q","glyph":49,"commands":[["moveTo",[1096,-418]],["lineTo",[1096,1118]],["lineTo",[922,1118]],["lineTo",[922,939]],["lineTo",[902,939]],["curveTo",[865,998,796,1132,578,1132]],["curveTo",[299,1132,104,910,104,556]],["curveTo",[104,200,298,-24,577,-24]],["curveTo",[790,-24,865,111,902,173]],["lineTo",[916,173]],["lineTo",[916,-418]],["closePath",[]],["moveTo",[604,137]],["curveTo",[395,137,287,323,287,558]],["curveTo",[287,790,392,971,604,971]],["curveTo",[810,971,918,805,918,558]],["curveTo",[918,309,807,137,604,137]],["closePath",[]]]},{"char":"r","glyph":50,"commands":[["moveTo",[158,0]],["lineTo",[338,0]],["lineTo",[338,700]],["curveTo",[338,851,456,960,617,960]],["curveTo",[665,960,714,952,725,950]],["lineTo",[725,1131]],["curveTo",[704,1132,660,1134,634,1134]],["curveTo",[501,1134,385,1059,344,946]],["lineTo",[332,946]],["lineTo",[332,1118]],["lineTo",[158,1118]],["closePath",[]]]},{"char":"s","glyph":51,"commands":[["moveTo",[538,-24]],["curveTo",[800,-24,979,119,979,315]],["curveTo",[979,468,884,569,681,617]],["lineTo",[512,657]],["curveTo",[381,688,324,734,324,817]],["curveTo",[324,910,424,979,557,979]],["curveTo",[701,979,764,901,795,821]],["lineTo",[958,863]],["curveTo",[907,1020,786,1132,556,1132]],["curveTo",[318,1132,142,998,142,806]],["curveTo",[142,649,239,549,442,501]],["lineTo",[628,457]],["curveTo",[738,431,792,380,792,303]],["curveTo",[792,211,692,134,536,134]],["curveTo",[402,134,311,189,279,312]],["lineTo",[108,271]],["curveTo",[148,78,308,-24,538,-24]],["closePath",[]]]},{"char":"t","glyph":53,"commands":[["moveTo",[598,1118]],["lineTo",[368,1118]],["lineTo",[368,1384]],["lineTo",[188,1384]],["lineTo",[188,1118]],["lineTo",[20,1118]],["lineTo",[20,964]],["lineTo",[188,964]],["lineTo",[188,276]],["curveTo",[188,93,302,-14,495,-14]],["curveTo",[541,-14,592,-7,629,6]],["lineTo",[592,158]],["curveTo",[562,152,523,146,500,146]],["curveTo",[405,146,368,190,368,290]],["lineTo",[368,964]],["lineTo",[598,964]],["closePath",[]]]},{"char":"u","glyph":54,"commands":[["moveTo",[537,-14]],["curveTo",[684,-14,806,46,879,209]],["lineTo",[879,0]],["lineTo",[1053,0]],["lineTo",[1053,1118]],["lineTo",[872,1118]],["lineTo",[872,448]],["curveTo",[872,257,751,148,591,148]],["curveTo",[435,148,338,251,338,423]],["lineTo",[338,1118]],["lineTo",[158,1118]],["lineTo",[158,408]],["curveTo",[158,125,311,-14,537,-14]],["closePath",[]]]},{"char":"v","glyph":56,"commands":[["moveTo",[481,0]],["lineTo",[670,0]],["lineTo",[1097,1118]],["lineTo",[900,1118]],["lineTo",[662,455]],["curveTo",[627,358,601,260,576,167]],["curveTo",[550,260,524,358,489,455]],["lineTo",[251,1118]],["lineTo",[54,1118]],["closePath",[]]]},{"char":"w","glyph":57,"commands":[["moveTo",[409,0]],["lineTo",[588,0]],["lineTo",[733,506]],["curveTo",[769,631,804,771,838,913]],["curveTo",[872,770,906,634,943,506]],["lineTo",[1088,0]],["lineTo",[1267,0]],["lineTo",[1606,1118]],["lineTo",[1415,1118]],["lineTo",[1279,630]],["curveTo",[1245,508,1208,367,1171,196]],["curveTo",[1136,369,1100,511,1068,630]],["lineTo",[936,1118]],["lineTo",[744,1118]],["lineTo",[610,630]],["curveTo",[576,506,540,363,504,193]],["curveTo",[467,371,429,513,397,630]],["lineTo",[261,1118]],["lineTo",[70,1118]],["closePath",[]]]},{"char":"x","glyph":58,"commands":[["moveTo",[65,0]],["lineTo",[273,0]],["lineTo",[439,251]],["curveTo",[489,327,523,395,557,458]],["curveTo",[594,395,631,327,680,251]],["lineTo",[844,0]],["lineTo",[1053,0]],["lineTo",[661,564]],["lineTo",[1035,1118]],["lineTo",[829,1118]],["lineTo",[687,901]],["curveTo",[635,822,601,750,565,683]],["curveTo",[526,750,488,822,437,901]],["lineTo",[298,1118]],["lineTo",[88,1118]],["lineTo",[458,574]],["closePath",[]]]},{"char":"y","glyph":59,"commands":[["moveTo",[140,-405]],["curveTo",[159,-414,208,-426,272,-426]],["curveTo",[430,-426,538,-345,606,-167]],["lineTo",[1097,1118]],["lineTo",[901,1118]],["lineTo",[661,455]],["curveTo",[626,358,600,261,575,168]],["curveTo",[549,261,524,357,489,455]],["lineTo",[251,1118]],["lineTo",[54,1118]],["lineTo",[481,-6]],["lineTo",[449,-99]],["curveTo",[394,-259,315,-283,205,-254]],["lineTo",[186,-249]],["closePath",[]]]},{"char":"z","glyph":60,"commands":[["moveTo",[126,0]],["lineTo",[1005,0]],["lineTo",[1005,167]],["lineTo",[376,167]],["lineTo",[376,178]],["lineTo",[985,975]],["lineTo",[985,1118]],["lineTo",[146,1118]],["lineTo",[146,951]],["lineTo",[753,951]],["lineTo",[753,940]],["lineTo",[126,134]],["closePath",[]]]},{"char":"{","glyph":80,"commands":[["moveTo",[750,-279]],["lineTo",[750,-130]],["curveTo",[580,-130,528,-54,528,115]],["lineTo",[528,399]],["curveTo",[528,516,491,606,323,641]],["lineTo",[323,661]],["curveTo",[491,695,528,784,528,901]],["lineTo",[528,1187]],["curveTo",[528,1356,581,1431,750,1430]],["lineTo",[750,1581]],["curveTo",[464,1581,362,1463,362,1190]],["lineTo",[362,960]],["curveTo",[362,814,314,750,178,742]],["lineTo",[144,742]],["lineTo",[144,560]],["lineTo",[177,560]],["curveTo",[314,551,362,487,362,340]],["lineTo",[362,112]],["curveTo",[362,-162,464,-279,750,-279]],["closePath",[]]]},{"char":"|","glyph":85,"commands":[["moveTo",[419,1970]],["lineTo",[262,1970]],["lineTo",[262,-480]],["lineTo",[419,-480]],["closePath",[]]]},{"char":"}","glyph":81,"commands":[["moveTo",[123,-279]],["curveTo",[408,-279,510,-162,510,112]],["lineTo",[510,340]],["curveTo",[510,497,565,560,729,561]],["lineTo",[729,741]],["lineTo",[722,741]],["curveTo",[564,743,510,804,510,960]],["lineTo",[510,1190]],["curveTo",[510,1463,408,1581,123,1581]],["lineTo",[123,1430]],["curveTo",[293,1430,345,1355,345,1187]],["lineTo",[345,901]],["curveTo",[345,784,382,695,549,661]],["lineTo",[549,641]],["curveTo",[382,606,345,515,345,399]],["lineTo",[345,115]],["curveTo",[345,-54,293,-130,123,-130]],["closePath",[]]]},{"char":"~","glyph":98,"commands":[["moveTo",[168,439]],["lineTo",[332,439]],["curveTo",[330,566,387,616,465,616]],["curveTo",[515,616,548,594,620,532]],["curveTo",[723,443,796,400,897,400]],["curveTo",[1044,400,1190,493,1187,740]],["lineTo",[1023,740]],["curveTo",[1023,624,972,563,890,563]],["curveTo",[839,563,805,586,735,647]],["curveTo",[627,741,556,780,458,780]],["curveTo",[309,780,164,687,168,439]],["closePath",[]]]},{"char":"Ä","glyph":2,"commands":[["moveTo",[52,0]],["lineTo",[254,0]],["lineTo",[398,416]],["lineTo",[1012,416]],["lineTo",[1161,0]],["lineTo",[1361,0]],["lineTo",[813,1490]],["lineTo",[593,1490]],["closePath",[]],["moveTo",[456,582]],["lineTo",[561,884]],["curveTo",[593,976,639,1118,702,1338]],["curveTo",[765,1122,809,984,845,884]],["lineTo",[953,582]],["closePath",[]],["moveTo",[496,1673]],["curveTo",[561,1673,614,1727,614,1792]],["curveTo",[614,1857,561,1910,496,1910]],["curveTo",[430,1910,377,1857,377,1792]],["curveTo",[377,1727,430,1673,496,1673]],["closePath",[]],["moveTo",[917,1673]],["curveTo",[982,1673,1035,1727,1035,1792]],["curveTo",[1035,1857,982,1910,917,1910]],["curveTo",[851,1910,798,1857,798,1792]],["curveTo",[798,1727,851,1673,917,1673]],["closePath",[]]]},{"char":"Ö","glyph":17,"commands":[["moveTo",[784,-20]],["curveTo",[1166,-20,1444,272,1444,744]],["curveTo",[1444,1217,1166,1510,784,1510]],["curveTo",[402,1510,122,1217,122,744]],["curveTo",[122,272,402,-20,784,-20]],["closePath",[]],["moveTo",[784,156]],["curveTo",[516,156,307,364,307,744]],["curveTo",[307,1126,516,1334,784,1334]],["curveTo",[1050,1334,1259,1127,1259,744]],["curveTo",[1259,363,1050,156,784,156]],["closePath",[]],["moveTo",[574,1673]],["curveTo",[639,1673,692,1727,692,1792]],["curveTo",[692,1857,639,1910,574,1910]],["curveTo",[508,1910,455,1857,455,1792]],["curveTo",[455,1727,508,1673,574,1673]],["closePath",[]],["moveTo",[995,1673]],["curveTo",[1060,1673,1113,1727,1113,1792]],["curveTo",[1113,1857,1060,1910,995,1910]],["curveTo",[929,1910,876,1857,876,1792]],["curveTo",[876,1727,929,1673,995,1673]],["closePath",[]]]},{"char":"Ü","glyph":24,"commands":[["moveTo",[763,-24]],["curveTo",[1114,-24,1344,200,1344,507]],["lineTo",[1344,1490]],["lineTo",[1154,1490]],["lineTo",[1154,522]],["curveTo",[1154,308,1006,152,763,152]],["curveTo",[519,152,370,308,370,522]],["lineTo",[370,1490]],["lineTo",[180,1490]],["lineTo",[180,507]],["curveTo",[180,200,410,-24,763,-24]],["closePath",[]],["moveTo",[553,1673]],["curveTo",[618,1673,671,1727,671,1792]],["curveTo",[671,1857,618,1910,553,1910]],["curveTo",[487,1910,434,1857,434,1792]],["curveTo",[434,1727,487,1673,553,1673]],["closePath",[]],["moveTo",[974,1673]],["curveTo",[1039,1673,1092,1727,1092,1792]],["curveTo",[1092,1857,1039,1910,974,1910]],["curveTo",[908,1910,855,1857,855,1792]],["curveTo",[855,1727,908,1673,974,1673]],["closePath",[]]]},{"char":"ä","glyph":31,"commands":[["moveTo",[471,-26]],["curveTo",[660,-26,765,73,803,152]],["lineTo",[815,152]],["lineTo",[815,0]],["lineTo",[992,0]],["lineTo",[992,737]],["curveTo",[992,1086,723,1132,573,1132]],["curveTo",[400,1132,208,1070,122,867]],["lineTo",[295,810]],["curveTo",[330,888,417,974,578,974]],["curveTo",[733,974,812,894,812,755]],["lineTo",[812,749]],["curveTo",[812,666,724,672,525,647]],["curveTo",[318,621,90,574,90,318]],["curveTo",[90,98,259,-26,471,-26]],["closePath",[]],["moveTo",[502,133]],["curveTo",[368,133,271,193,271,309]],["curveTo",[271,436,386,478,517,495]],["curveTo",[586,504,782,524,812,559]],["lineTo",[812,404]],["curveTo",[812,267,701,133,502,133]],["closePath",[]],["moveTo",[348,1301]],["curveTo",[413,1301,466,1355,466,1420]],["curveTo",[466,1485,413,1538,348,1538]],["curveTo",[282,1538,229,1485,229,1420]],["curveTo",[229,1355,282,1301,348,1301]],["closePath",[]],["moveTo",[769,1301]],["curveTo",[834,1301,887,1355,887,1420]],["curveTo",[887,1485,834,1538,769,1538]],["curveTo",[703,1538,650,1485,650,1420]],["curveTo",[650,1355,703,1301,769,1301]],["closePath",[]]]},{"char":"ö","glyph":47,"commands":[["moveTo",[613,-24]],["curveTo",[918,-24,1124,207,1124,552]],["curveTo",[1124,902,918,1132,613,1132]],["curveTo",[309,1132,104,902,104,552]],["curveTo",[104,207,309,-24,613,-24]],["closePath",[]],["moveTo",[613,137]],["curveTo",[390,137,287,332,287,552]],["curveTo",[287,775,390,971,613,971]],["curveTo",[839,971,942,774,942,552]],["curveTo",[942,332,839,137,613,137]],["closePath",[]],["moveTo",[403,1301]],["curveTo",[468,1301,521,1355,521,1420]],["curveTo",[521,1485,468,1538,403,1538]],["curveTo",[337,1538,284,1485,284,1420]],["curveTo",[284,1355,337,1301,403,1301]],["closePath",[]],["moveTo",[824,1301]],["curveTo",[889,1301,942,1355,942,1420]],["curveTo",[942,1485,889,1538,824,1538]],["curveTo",[758,1538,705,1485,705,1420]],["curveTo",[705,1355,758,1301,824,1301]],["closePath",[]]]},{"char":"ü","glyph":55,"commands":[["moveTo",[537,-14]],["curveTo",[684,-14,806,46,879,209]],["lineTo",[879,0]],["lineTo",[1053,0]],["lineTo",[1053,1118]],["lineTo",[872,1118]],["lineTo",[872,448]],["curveTo",[872,257,751,148,591,148]],["curveTo",[435,148,338,251,338,423]],["lineTo",[338,1118]],["lineTo",[158,1118]],["lineTo",[158,408]],["curveTo",[158,125,311,-14,537,-14]],["closePath",[]],["moveTo",[395,1301]],["curveTo",[460,1301,513,1355,513,1420]],["curveTo",[513,1485,460,1538,395,1538]],["curveTo",[329,1538,276,1485,276,1420]],["curveTo",[276,1355,329,1301,395,1301]],["closePath",[]],["moveTo",[816,1301]],["curveTo",[881,1301,934,1355,934,1420]],["curveTo",[934,1485,881,1538,816,1538]],["curveTo",[750,1538,697,1485,697,1420]],["curveTo",[697,1355,750,1301,816,1301]],["closePath",[]]]},{"char":"ß","glyph":52,"commands":[["moveTo",[180,0]],["lineTo",[362,0]],["lineTo",[362,1104]],["curveTo",[362,1245,471,1343,621,1343]],["curveTo",[778,1343,875,1234,875,1097]],["curveTo",[875,965,785,841,641,841]],["lineTo",[550,841]],["lineTo",[550,671]],["lineTo",[679,671]],["curveTo",[854,671,964,563,964,412]],["curveTo",[964,269,864,167,709,167]],["lineTo",[544,167]],["lineTo",[544,0]],["lineTo",[709,0]],["curveTo",[973,0,1146,173,1146,410]],["curveTo",[1146,615,1016,756,823,775]],["lineTo",[823,789]],["curveTo",[965,838,1057,959,1057,1125]],["curveTo",[1057,1364,867,1510,616,1510]],["curveTo",[360,1510,180,1357,180,1107]],["closePath",[]]]},{"char":"€","glyph":62,"commands":[["moveTo",[1070,963]],["lineTo",[442,963]],["curveTo",[499,1206,654,1334,847,1334]],["curveTo",[975,1334,1081,1278,1170,1198]],["lineTo",[1243,1357]],["curveTo",[1135,1458,997,1510,847,1510]],["curveTo",[547,1510,320,1306,253,963]],["lineTo",[105,963]],["lineTo",[52,818]],["lineTo",[235,818]],["curveTo",[234,794,233,769,233,744]],["curveTo",[233,720,234,696,235,672]],["lineTo",[105,672]],["lineTo",[52,525]],["lineTo",[253,525]],["curveTo",[320,183,547,-20,847,-20]],["curveTo",[996,-20,1138,29,1239,125]],["lineTo",[1164,282]],["curveTo",[1082,213,976,156,847,156]],["curveTo",[655,156,500,283,443,525]],["lineTo",[876,525]],["lineTo",[943,672]],["lineTo",[421,672]],["curveTo",[420,695,419,719,419,744]],["curveTo",[419,769,420,794,421,818]],["lineTo",[1002,818]],["closePath",[]]]},{"char":"é","glyph":36,"commands":[["moveTo",[628,-24]],["curveTo",[857,-24,1024,91,1076,260]],["lineTo",[902,308]],["curveTo",[860,194,761,136,629,136]],["curveTo",[431,136,295,264,286,498]],["lineTo",[1094,498]],["lineTo",[1094,573]],["curveTo",[1094,986,844,1132,607,1132]],["curveTo",[302,1132,104,893,104,550]],["curveTo",[104,206,304,-24,628,-24]],["closePath",[]],["moveTo",[287,650]],["curveTo",[298,821,415,972,607,972]],["curveTo",[791,972,894,840,911,650]],["closePath",[]],["moveTo",[524,1264]],["lineTo",[671,1264]],["lineTo",[865,1558]],["lineTo",[664,1558]],["closePath",[]]]}]} \ No newline at end of file diff --git a/tests/CADability.Tests/Files/Fonts/InterCidTest.otf b/tests/CADability.Tests/Files/Fonts/InterCidTest.otf new file mode 100644 index 00000000..0e4fba29 Binary files /dev/null and b/tests/CADability.Tests/Files/Fonts/InterCidTest.otf differ diff --git a/tests/CADability.Tests/Files/Fonts/make_cff_test_fonts.py b/tests/CADability.Tests/Files/Fonts/make_cff_test_fonts.py new file mode 100644 index 00000000..f2625605 --- /dev/null +++ b/tests/CADability.Tests/Files/Fonts/make_cff_test_fonts.py @@ -0,0 +1,187 @@ +# Writes the CFF test fonts of TrueTypeFontTests and their reference data with fontTools (pip install fonttools): +# +# InterCffTest.otf name-keyed CFF font +# InterCidTest.otf the same glyphs as a CID-keyed CFF font with two font dicts (FDArray, FDSelect) +# InterCffTest.reference.json the outlines as fontTools draws them, the same for both fonts +# +# Both are made from a subset of Inter Regular (SIL Open Font License 1.1, https://rsms.me/inter), which uses only the +# general charstring operators and no subroutines. To test the whole Type 2 charstring interpreter, the charstrings are +# rewritten without changing the outlines: +# - the specialized operators (hvcurveto, vhcurveto, hhcurveto, vvcurveto, hlineto, vlineto, rcurveline, rlinecurve) +# by fontTools' specializer, +# - flex for two consecutive curves of some glyphs, +# - the body of a third of the glyphs moved into a local subroutine, of another third into a global one. +# +# Run from this directory with the path of Inter-Regular.otf: python make_cff_test_fonts.py /usr/share/fonts/opentype/inter/Inter-Regular.otf +import copy, json, sys +from fontTools.ttLib import TTFont +from fontTools.subset import Subsetter, Options +from fontTools.cffLib import specializer, SubrsIndex, FDArrayIndex, FontDict, FDSelect +from fontTools.misc.psCharStrings import T2CharString +from fontTools.pens.recordingPen import RecordingPen + +UNICODES = list(range(0x20, 0x7F)) + [0xC4, 0xD6, 0xDC, 0xE4, 0xF6, 0xFC, 0xDF, 0x20AC, 0xE9] + + +def bias(count): + return 107 if count < 1240 else 1131 if count < 33900 else 32768 + + +def subset(path): + options = Options() + options.layout_features = [] + font = TTFont(path) + subsetter = Subsetter(options) + subsetter.populate(unicodes=UNICODES) + subsetter.subset(font) + for tag in ('GDEF', 'GPOS', 'GSUB'): # they name glyphs, which the CID font renames + if tag in font: + del font[tag] + return font + + +def rewrite(program, index): + """The program with specialized operators and, for some glyphs, flex instead of two curves.""" + commands = specializer.programToCommands(program) + if index % 4 == 0: + for i in range(len(commands) - 1): + a, b = commands[i], commands[i + 1] + if a[0] == 'rrcurveto' and b[0] == 'rrcurveto' and len(a[1]) == 6 and len(b[1]) == 6: + commands[i:i + 2] = [('flex', a[1] + b[1] + [50])] + break + return specializer.specializeProgram(specializer.commandsToProgram(commands)) + + +def body(program): + assert program[-1] == 'endchar' + return program[:-1] + + +def draw_all(font): + glyphs = font.getGlyphSet() + cmap = font.getBestCmap() + result = [] + for code in UNICODES: + name = cmap[code] + pen = RecordingPen() + glyphs[name].draw(pen) + result.append({'char': chr(code), 'glyph': font.getGlyphID(name), + 'commands': [[op, [c for p in pts for c in p]] for op, pts in pen.value]}) + return result + + +def name_keyed(font): + cff = font['CFF '].cff + top = cff.topDictIndex[0] + private = top.Private + private.Subrs = SubrsIndex() + order = font.getGlyphOrder() + reference = draw_all(font) # before rewriting: the outlines must not change + local, glob = [], [] + for index, name in enumerate(order): + cs = top.CharStrings[name] + cs.decompile() + program = rewrite(cs.program, index) + if index % 3 == 1: + local.append(body(program) + ['return']) + program = [len(local) - 1, 'callsubr', 'endchar'] # bias is added below + elif index % 3 == 2: + glob.append(body(program) + ['return']) + program = [len(glob) - 1, 'callgsubr', 'endchar'] + cs.program = program + for p in local: + private.Subrs.append(T2CharString(program=p, private=private, globalSubrs=cff.GlobalSubrs)) + for p in glob: + cff.GlobalSubrs.append(T2CharString(program=p, private=private, globalSubrs=cff.GlobalSubrs)) + for name in order: # the subroutine numbers are stored without the bias + cs = top.CharStrings[name] + if len(cs.program) == 3 and cs.program[1] in ('callsubr', 'callgsubr'): + count = len(local) if cs.program[1] == 'callsubr' else len(glob) + cs.program[0] -= bias(count) + return reference + + +def to_cid(font): + """Converts the name-keyed font to a CID-keyed one with two font dicts: even glyphs use the first, odd glyphs the second, + each with its own private dict and local subroutines.""" + cmap_tables = font['cmap'].tables # read the tables that name glyphs before the glyphs are renamed + metrics = dict(font['hmtx'].metrics) + cff = font['CFF '].cff + top = cff.topDictIndex[0] + order = font.getGlyphOrder() + new_names = ['.notdef'] + ['cid%05d' % i for i in range(1, len(order))] + rename = dict(zip(order, new_names)) + programs = [] + for name in order: + cs = top.CharStrings[name] + cs.decompile() + programs.append(copy.copy(cs.program)) + old_private = top.Private + old_local = [s.program if s.program is not None else (s.decompile() or s.program) for s in old_private.Subrs] + fd_array = FDArrayIndex() + privates = [] + for k in range(2): + fd = FontDict() + private = copy.copy(old_private) + private.Subrs = SubrsIndex() + fd.Private = private + fd.FontName = 'InterCidTest-%d' % k + fd_array.append(fd) + privates.append(private) + # local subroutines move to the font dict of the glyph that calls them + local_bias = bias(len(old_local)) + for gid, program in enumerate(programs): + fd = gid % 2 + if len(program) == 3 and program[1] == 'callsubr': + subr = old_local[program[0] + local_bias] + privates[fd].Subrs.append(T2CharString(program=list(subr), private=privates[fd], globalSubrs=cff.GlobalSubrs)) + program[0] = len(privates[fd].Subrs) - 1 # bias added below + for gid, program in enumerate(programs): + fd = gid % 2 + if len(program) == 3 and program[1] == 'callsubr': + program[0] -= bias(len(privates[fd].Subrs)) + top.ROS = ('Adobe', 'Identity', 0) + top.CIDCount = len(order) + top.FDArray = fd_array + select = FDSelect() + select.format = 3 + select.gidArray = [gid % 2 for gid in range(len(order))] + top.FDSelect = select + del top.rawDict['Private'] + del top.Private + charstrings = top.CharStrings + new_dict = {} + for gid, name in enumerate(order): + cs = T2CharString(program=programs[gid], private=privates[gid % 2], globalSubrs=cff.GlobalSubrs) + cs.fdSelectIndex = gid % 2 + new_dict[rename[name]] = cs + charstrings.charStrings = new_dict + charstrings.charStringsAreIndexed = False + charstrings.charStringsIndex = None + top.charset = new_names + for table in cmap_tables: + table.cmap = {code: rename[name] for code, name in table.cmap.items()} + font['hmtx'].metrics = {rename[name]: value for name, value in metrics.items()} + font.setGlyphOrder(new_names) + font['post'].formatType = 3.0 + + +def main(path): + font = subset(path) + reference = name_keyed(font) + font.save('InterCffTest.otf') + check = TTFont('InterCffTest.otf') + assert draw_all(check) == reference, 'rewriting the charstrings changed the outlines' + json.dump({'glyphs': reference}, open('InterCffTest.reference.json', 'w', encoding='utf-8'), ensure_ascii=False, separators=(',', ':')) + + font = TTFont('InterCffTest.otf') + to_cid(font) + font.save('InterCidTest.otf') + check = TTFont('InterCidTest.otf') + top = check['CFF '].cff.topDictIndex[0] + assert hasattr(top, 'ROS') and len(top.FDArray) == 2, 'not a CID-keyed font with two font dicts' + assert [g['commands'] for g in draw_all(check)] == [g['commands'] for g in reference], 'the CID font has other outlines' + + +if __name__ == '__main__': + main(sys.argv[1]) diff --git a/tests/CADability.Tests/FontProviderTests.cs b/tests/CADability.Tests/FontProviderTests.cs index 8e2b7be0..c364a077 100644 --- a/tests/CADability.Tests/FontProviderTests.cs +++ b/tests/CADability.Tests/FontProviderTests.cs @@ -130,7 +130,7 @@ public void UnderlineAndStrikeoutAddBars() public void MissingStyleFallsBackToAnExistingFace() { ManagedFontProvider provider = new ManagedFontProvider(new[] { FontDirectory() }, false); - CollectionAssert.AreEqual(new[] { "Liberation Sans" }, provider.GetFamilyNames()); + CollectionAssert.Contains(provider.GetFamilyNames(), "Liberation Sans"); 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");