From 0e3aa32113b5456f0e483306964e684ad5602f57 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 25 Apr 2022 00:31:01 +1000 Subject: [PATCH 01/10] Begin shaper --- .../GlyphSubstitutionCollection.cs | 32 +- .../Shapers/HangulShaper.cs | 306 ++++++++++++++++++ 2 files changed, 336 insertions(+), 2 deletions(-) create mode 100644 src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs diff --git a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs index 509f0d7e..1c9a1d9c 100644 --- a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs +++ b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs @@ -155,8 +155,6 @@ public void Replace(int index, ushort glyphId) public void Replace(int index, ReadOnlySpan removalIndices, ushort glyphId, int ligatureId) { // Remove the glyphs at each index. - // TODO: We will have to offset these indices by the leading index of the collection - // that the current shaper is working against. int codePointCount = 0; for (int i = removalIndices.Length - 1; i >= 0; i--) { @@ -178,6 +176,36 @@ public void Replace(int index, ReadOnlySpan removalIndices, ushort glyphId, current.CursiveAttachment = -1; } + /// + /// Performs a 1:1 replacement of a glyph id at the given position while removing a series of glyph ids. + /// + /// The zero-based index of the element to replace. + /// The number of glyphs to remove. + /// The replacement glyph id. + public void Replace(int index, int count, ushort glyphId) + { + // Remove the glyphs at each index. + int codePointCount = 0; + for (int i = count - 1; i >= 0; i--) + { + int match = index + i; + int matchOffset = this.offsets[match]; + codePointCount += this.glyphs[matchOffset].CodePointCount; + this.glyphs.Remove(matchOffset); + this.offsets.RemoveAt(match); + } + + // Assign our new id at the index. + int offset = this.offsets[index]; + GlyphShapingData current = this.glyphs[offset]; + current.CodePointCount += codePointCount; + current.GlyphIds = new[] { glyphId }; + current.LigatureId = 0; + current.LigatureComponent = -1; + current.MarkAttachment = -1; + current.CursiveAttachment = -1; + } + /// /// Replaces a single glyph id with a collection of glyph ids. /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs new file mode 100644 index 00000000..3842fd09 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs @@ -0,0 +1,306 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers +{ + /// + /// This is a shaper for the Hangul script, used by the Korean language. + /// The shaping state machine was ported from fontkit. + /// + /// + internal sealed class HangulShaper : DefaultShaper + { + private static readonly Tag LjmoTag = Tag.Parse("ljmo"); + + private static readonly Tag VjmoTag = Tag.Parse("vjmo"); + + private static readonly Tag TjmoTag = Tag.Parse("tjmo"); + + private const int HANGUL_BASE = 0xac00; + private const int HANGUL_END = 0xd7a4; + private const int HANGUL_COUNT = HANGUL_END - HANGUL_BASE + 1; + private const int L_BASE = 0x1100; // lead + private const int V_BASE = 0x1161; // vowel + private const int T_BASE = 0x11a7; // trail + private const int L_COUNT = 19; + private const int V_COUNT = 21; + private const int T_COUNT = 28; + private const int L_END = L_BASE + L_COUNT - 1; + private const int V_END = V_BASE + V_COUNT - 1; + private const int T_END = T_BASE + T_COUNT - 1; + private const int DOTTED_CIRCLE = 0x25cc; + + // Character categories + private const byte X = 0; // Other character + private const byte L = 1; // Leading consonant + private const byte V = 2; // Medial vowel + private const byte T = 3; // Trailing consonant + private const byte LV = 4; // Composed syllable + private const byte LVT = 5; // Composed syllable + private const byte M = 6; // Tone mark + + // State machine actions + private const byte None = 0; + private const byte Decompose = 1; + private const byte Compose = 2; + private const byte ToneMark = 4; + private const byte Invalid = 5; + + // Build a state machine that accepts valid syllables, and applies actions along the way. + // The logic this is implementing is documented at the top of the file. + private static readonly byte[,][] StateTable = + { + // # X L V T LV LVT M + // State 0: start state + { new byte[] { None, 0 }, new byte[] { None, 1 }, new byte[] { None, 0 }, new byte[] { None, 0 }, new byte[] { Decompose, 2 }, new byte[] { Decompose, 3 }, new byte[] { Invalid, 0 } }, + + // State 1: + { new byte[] { None, 0 }, new byte[] { None, 1 }, new byte[] { Compose, 2 }, new byte[] { None, 0 }, new byte[] { Decompose, 2 }, new byte[] { Decompose, 3 }, new byte[] { Invalid, 0 } }, + + // State 2: or + { new byte[] { None, 0 }, new byte[] { None, 1 }, new byte[] { None, 0 }, new byte[] { Compose, 3 }, new byte[] { Decompose, 2 }, new byte[] { Decompose, 3 }, new byte[] { ToneMark, 0 } }, + + // State 3: or + { new byte[] { None, 0 }, new byte[] { None, 1 }, new byte[] { None, 0 }, new byte[] { None, 0 }, new byte[] { Decompose, 2 }, new byte[] { Decompose, 3 }, new byte[] { ToneMark, 0 } }, + }; + + public HangulShaper(TextOptions textOptions) + : base(MarkZeroingMode.None, textOptions) + { + } + + public override void AssignFeatures(IGlyphShapingCollection collection, int index, int count) + { + this.AddFeature(collection, index, count, LjmoTag, false); + this.AddFeature(collection, index, count, VjmoTag, false); + this.AddFeature(collection, index, count, TjmoTag, false); + + base.AssignFeatures(collection, index, count); + + // Apply the state machine to map glyphs to features. + if (collection is GlyphSubstitutionCollection substitutionCollection) + { + // GSub + int state = 0; + for (int i = 0; i < count; i++) + { + GlyphShapingData data = substitutionCollection.GetGlyphShapingData(i + index); + CodePoint codePoint = data.CodePoint; + int type = GetType(codePoint); + byte[] actionsWithState = StateTable[state, type]; + byte action = actionsWithState[0]; + state = actionsWithState[1]; + switch (action) + { + case Decompose: + + // Decompose the composed syllable if it is not supported by the font. + if (!data.TextRun.Font!.FontMetrics.TryGetGlyphId(codePoint, out ushort _)) + { + i = this.DecomposeGlyph(substitutionCollection, data, i); + } + + break; + + case Compose: + + // Found a decomposed syllable. Try to compose if supported by the font. + i = this.ComposeGlyph(substitutionCollection, data, i, type); + break; + + case ToneMark: + + // Got a valid syllable, followed by a tone mark. Move the tone mark to the beginning of the syllable. + this.ReOrderToneMark(data, i); + break; + + case Invalid: + // Tone mark has no valid syllable to attach to, so insert a dotted circle + i = this.InsertDottedCircle(data, i); + break; + } + } + } + else + { + // GPos + // Simply loop and enable based on type. + // Glyph substitution has handled [de]composition. + } + } + + private static int GetType(CodePoint codePoint) + { + GraphemeClusterClass type = CodePoint.GetGraphemeClusterClass(codePoint); + int value = codePoint.Value; + + return type switch + { + GraphemeClusterClass.HangulLead => L, + GraphemeClusterClass.HangulVowel => V, + GraphemeClusterClass.HangulTail => T, + GraphemeClusterClass.HangulLeadVowel => LV, + GraphemeClusterClass.HangulLeadVowelTail => LVT, + + // HANGUL SINGLE DOT TONE MARK + // HANGUL DOUBLE DOT TONE MARK + _ => value is >= 0x302E and <= 0x302F ? M : X, + }; + } + + private int DecomposeGlyph(GlyphSubstitutionCollection collection, GlyphShapingData data, int index) + { + // Decompose the syllable into a sequence of glyphs. + int s = data.CodePoint.Value - HANGUL_BASE; + int t = T_BASE + (s % T_COUNT); + s = (s / T_COUNT) | 0; + int l = (L_BASE + (s / V_COUNT)) | 0; + int v = V_BASE + (s % V_COUNT); + + FontMetrics metrics = data.TextRun.Font!.FontMetrics; + + // Don't decompose if all of the components are not available + if (!metrics.TryGetGlyphId(new(l), out ushort _) || + !metrics.TryGetGlyphId(new(v), out ushort _) || + (t != T_BASE && !metrics.TryGetGlyphId(new(t), out ushort _))) + { + return index; + } + + // Replace the current glyph with decomposed L, V, and T glyphs, + // and apply the proper OpenType features to each component. + GlyphShapingData ljmo = collection.GetGlyphShapingData(l); + collection.EnableShapingFeature(l, LjmoTag); + + GlyphShapingData vjmo = collection.GetGlyphShapingData(v); + collection.EnableShapingFeature(v, VjmoTag); + + if (t <= T_BASE) + { + Span ii = stackalloc ushort[2]; + ii[0] = ljmo.GlyphIds[0]; + ii[1] = vjmo.GlyphIds[0]; + + collection.Replace(index, ii); + return index + 1; + } + + GlyphShapingData tjmo = collection.GetGlyphShapingData(t); + collection.EnableShapingFeature(t, TjmoTag); + Span iii = stackalloc ushort[3]; + iii[0] = ljmo.GlyphIds[0]; + iii[1] = vjmo.GlyphIds[0]; + iii[2] = tjmo.GlyphIds[0]; + + collection.Replace(index, iii); + return index + 2; + } + + private int ComposeGlyph(GlyphSubstitutionCollection collection, GlyphShapingData data, int i, int type) + { + GlyphShapingData prev = collection.GetGlyphShapingData(i - 1); + CodePoint prevCodePoint = prev.CodePoint; + int prevType = GetType(prevCodePoint); + + // Figure out what type of syllable we're dealing with + CodePoint lv = default; + GlyphShapingData? ljmo = null, vjmo = null, tjmo = null; + + if (prevType == LV && type == T) + { + // + lv = prevCodePoint; + tjmo = data; + } + else + { + if (type == V) + { + // + ljmo = prev; + vjmo = data; + } + else + { + // + ljmo = collection.GetGlyphShapingData(i - 2); + vjmo = prev; + tjmo = data; + } + + CodePoint l = ljmo.CodePoint; + CodePoint v = vjmo.CodePoint; + + // Make sure L and V are combining characters + if (isCombiningL(l) && isCombiningV(v)) + { + lv = new CodePoint(HANGUL_BASE + ((((l.Value - L_BASE) * V_COUNT) + (v.Value - V_BASE)) * T_COUNT)); + } + + CodePoint t = tjmo?.CodePoint ?? new CodePoint(T_BASE); + if ((lv != default) && (t.Value == T_BASE || isCombiningT(t))) + { + CodePoint s = new(lv.Value + (t.Value - T_BASE)); + + // Replace with a composed glyph if supported by the font, + // otherwise apply the proper OpenType features to each component. + FontMetrics metrics = data.TextRun.Font!.FontMetrics; + if (metrics.TryGetGlyphId(s, out ushort id)) + { + int del = prevType == V ? 3 : 2; + int idx = i - del + 1; + collection.Replace(idx, del, id); + return idx; + } + } + } + + // Didn't compose (either a non-combining component or unsupported by font). + if (ljmo != null) + { + collection.EnableShapingFeature(ljmo.GlyphIds[0], LjmoTag); + } + + if (vjmo != null) + { + collection.EnableShapingFeature(vjmo.GlyphIds[0], VjmoTag); + } + + if (tjmo != null) + { + collection.EnableShapingFeature(tjmo.GlyphIds[0], TjmoTag); + } + + if (prevType == LV) + { + // Sequence was originally , which got combined earlier. + // Either the T was non-combining, or the LVT glyph wasn't supported. + // Decompose the glyph again and apply OT features. + data = collection.GetGlyphShapingData(i - 1); + this.DecomposeGlyph(collection, data, i - 1); + return i + 1; + } + + return i; + } + + private int ReOrderToneMark(GlyphShapingData data, int i) + { + throw new NotImplementedException(); + } + + private int InsertDottedCircle(GlyphShapingData data, int i) + { + throw new NotImplementedException(); + } + + private static bool isCombiningL(CodePoint code) => UnicodeUtility.IsInRangeInclusive((uint)code.Value, L_BASE, L_END); + + private static bool isCombiningV(CodePoint code) => UnicodeUtility.IsInRangeInclusive((uint)code.Value, V_BASE, V_END); + + private static bool isCombiningT(CodePoint code) => UnicodeUtility.IsInRangeInclusive((uint)code.Value, T_BASE + 1, T_END); + } +} From 68fa451006b24de4590729a43f89c0ad4ca01ac2 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 28 Apr 2022 00:05:24 +1000 Subject: [PATCH 02/10] Fix some shaper issues --- .../GlyphSubstitutionCollection.cs | 145 +++++++++++++----- .../Shapers/HangulShaper.cs | 122 +++++++++------ 2 files changed, 185 insertions(+), 82 deletions(-) diff --git a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs index 1c9a1d9c..1c94caed 100644 --- a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs +++ b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs @@ -77,8 +77,7 @@ public void AddShapingFeature(int index, TagEntry feature) /// public void EnableShapingFeature(int index, Tag feature) { - List features = this.glyphs[this.offsets[index]].Features; - foreach (TagEntry tagEntry in features) + foreach (TagEntry tagEntry in this.glyphs[this.offsets[index]].Features) { if (tagEntry.Tag == feature) { @@ -107,6 +106,41 @@ public void AddGlyph(ushort glyphId, CodePoint codePoint, TextDirection directio this.offsets.Add(offset); } + /// + /// Removes the glyph at the given index. + /// + /// The zero-based index of the element to remove. + public void RemoveGlyph(int index) + { + this.glyphs.Remove(this.offsets[index]); + this.offsets.RemoveAt(index); + } + + public void MoveGlyph(int fromIndex, int toIndex) + { + GlyphShapingData data = this.GetGlyphShapingData(fromIndex); + this.RemoveGlyph(fromIndex); + + if (fromIndex > toIndex) + { + int idx = fromIndex; + while (idx > toIndex) + { + this.glyphs[this.offsets[idx]] = this.glyphs[this.offsets[idx - 1]]; + } + } + else + { + int idx = toIndex; + while (idx > fromIndex) + { + this.glyphs[this.offsets[idx - 1]] = this.glyphs[this.offsets[idx]]; + } + } + + this.glyphs[this.offsets[toIndex]] = data; + } + /// /// Removes all elements from the collection. /// @@ -154,26 +188,36 @@ public void Replace(int index, ushort glyphId) /// The ligature id. public void Replace(int index, ReadOnlySpan removalIndices, ushort glyphId, int ligatureId) { - // Remove the glyphs at each index. - int codePointCount = 0; - for (int i = removalIndices.Length - 1; i >= 0; i--) + if (removalIndices.Length > 0) { - int match = removalIndices[i]; - int matchOffset = this.offsets[match]; - codePointCount += this.glyphs[matchOffset].CodePointCount; - this.glyphs.Remove(matchOffset); - this.offsets.RemoveAt(match); - } + // Remove the glyphs at each index. + int codePointCount = 0; + for (int i = removalIndices.Length - 1; i >= 0; i--) + { + int match = removalIndices[i]; + int matchOffset = this.offsets[match]; + codePointCount += this.glyphs[matchOffset].CodePointCount; + this.glyphs.Remove(matchOffset); + this.offsets.RemoveAt(match); + } - // Assign our new id at the index. - int offset = this.offsets[index]; - GlyphShapingData current = this.glyphs[offset]; - current.CodePointCount += codePointCount; - current.GlyphIds = new[] { glyphId }; - current.LigatureId = ligatureId; - current.LigatureComponent = -1; - current.MarkAttachment = -1; - current.CursiveAttachment = -1; + // Assign our new id at the index. + int offset = this.offsets[index]; + GlyphShapingData current = this.glyphs[offset]; + current.CodePointCount += codePointCount; + current.GlyphIds = new[] { glyphId }; + current.LigatureId = ligatureId; + current.LigatureComponent = -1; + current.MarkAttachment = -1; + current.CursiveAttachment = -1; + } + else + { + // Spec disallows removal of glyphs in this manner but it's common enough practice to allow it. + // https://github.com/MicrosoftDocs/typography-issues/issues/673 + this.glyphs.Remove(this.offsets[index]); + this.offsets.RemoveAt(index); + } } /// @@ -184,26 +228,36 @@ public void Replace(int index, ReadOnlySpan removalIndices, ushort glyphId, /// The replacement glyph id. public void Replace(int index, int count, ushort glyphId) { - // Remove the glyphs at each index. - int codePointCount = 0; - for (int i = count - 1; i >= 0; i--) + if (count > 0) { - int match = index + i; - int matchOffset = this.offsets[match]; - codePointCount += this.glyphs[matchOffset].CodePointCount; - this.glyphs.Remove(matchOffset); - this.offsets.RemoveAt(match); - } + // Remove the glyphs at each index. + int codePointCount = 0; + for (int i = count - 1; i >= 0; i--) + { + int match = index + i; + int matchOffset = this.offsets[match]; + codePointCount += this.glyphs[matchOffset].CodePointCount; + this.glyphs.Remove(matchOffset); + this.offsets.RemoveAt(match); + } - // Assign our new id at the index. - int offset = this.offsets[index]; - GlyphShapingData current = this.glyphs[offset]; - current.CodePointCount += codePointCount; - current.GlyphIds = new[] { glyphId }; - current.LigatureId = 0; - current.LigatureComponent = -1; - current.MarkAttachment = -1; - current.CursiveAttachment = -1; + // Assign our new id at the index. + int offset = this.offsets[index]; + GlyphShapingData current = this.glyphs[offset]; + current.CodePointCount += codePointCount; + current.GlyphIds = new[] { glyphId }; + current.LigatureId = 0; + current.LigatureComponent = -1; + current.MarkAttachment = -1; + current.CursiveAttachment = -1; + } + else + { + // Spec disallows removal of glyphs in this manner but it's common enough practice to allow it. + // https://github.com/MicrosoftDocs/typography-issues/issues/673 + this.glyphs.Remove(this.offsets[index]); + this.offsets.RemoveAt(index); + } } /// @@ -213,6 +267,8 @@ public void Replace(int index, int count, ushort glyphId) /// The collection of replacement glyph ids. public void Replace(int index, ReadOnlySpan glyphIds) { + // TODO: + // Features most likely need to be bound to each glyph index. // TODO: FontKit stores the ids in sequence with increasing ligature component values. int offset = this.offsets[index]; GlyphShapingData current = this.glyphs[offset]; @@ -221,5 +277,18 @@ public void Replace(int index, ReadOnlySpan glyphIds) current.MarkAttachment = -1; current.CursiveAttachment = -1; } + + private class OffsetGlyphDataPair + { + public OffsetGlyphDataPair(int offset, GlyphShapingData data) + { + this.Offset = offset; + this.Data = data; + } + + public int Offset { get; set; } + + public GlyphShapingData Data { get; set; } + } } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs index 3842fd09..e711478c 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs @@ -89,7 +89,7 @@ public override void AssignFeatures(IGlyphShapingCollection collection, int inde { GlyphShapingData data = substitutionCollection.GetGlyphShapingData(i + index); CodePoint codePoint = data.CodePoint; - int type = GetType(codePoint); + int type = GetSyllableType(codePoint); byte[] actionsWithState = StateTable[state, type]; byte action = actionsWithState[0]; state = actionsWithState[1]; @@ -114,7 +114,7 @@ public override void AssignFeatures(IGlyphShapingCollection collection, int inde case ToneMark: // Got a valid syllable, followed by a tone mark. Move the tone mark to the beginning of the syllable. - this.ReOrderToneMark(data, i); + this.ReOrderToneMark(substitutionCollection, data, i); break; case Invalid: @@ -132,7 +132,7 @@ public override void AssignFeatures(IGlyphShapingCollection collection, int inde } } - private static int GetType(CodePoint codePoint) + private static int GetSyllableType(CodePoint codePoint) { GraphemeClusterClass type = CodePoint.GetGraphemeClusterClass(codePoint); int value = codePoint.Value; @@ -151,6 +151,15 @@ private static int GetType(CodePoint codePoint) }; } + private static int GetSyllableLength(CodePoint codePoint) + => GetSyllableType(codePoint) switch + { + LV or LVT => 1, + V => 2, + T => 3, + _ => 0, + }; + private int DecomposeGlyph(GlyphSubstitutionCollection collection, GlyphShapingData data, int index) { // Decompose the syllable into a sequence of glyphs. @@ -163,47 +172,52 @@ private int DecomposeGlyph(GlyphSubstitutionCollection collection, GlyphShapingD FontMetrics metrics = data.TextRun.Font!.FontMetrics; // Don't decompose if all of the components are not available - if (!metrics.TryGetGlyphId(new(l), out ushort _) || - !metrics.TryGetGlyphId(new(v), out ushort _) || - (t != T_BASE && !metrics.TryGetGlyphId(new(t), out ushort _))) + if (!metrics.TryGetGlyphId(new(l), out ushort ljmo) || + !metrics.TryGetGlyphId(new(v), out ushort vjmo) || + (!metrics.TryGetGlyphId(new(t), out ushort tjmo) && t != T_BASE)) { return index; } + // TODO: Check the insertion here. + // We likely need to add the features separately to each of the newly + // embedded glyph ids. + // // Replace the current glyph with decomposed L, V, and T glyphs, // and apply the proper OpenType features to each component. - GlyphShapingData ljmo = collection.GetGlyphShapingData(l); - collection.EnableShapingFeature(l, LjmoTag); - - GlyphShapingData vjmo = collection.GetGlyphShapingData(v); - collection.EnableShapingFeature(v, VjmoTag); + collection.EnableShapingFeature(index, LjmoTag); + collection.EnableShapingFeature(index, VjmoTag); if (t <= T_BASE) { Span ii = stackalloc ushort[2]; - ii[0] = ljmo.GlyphIds[0]; - ii[1] = vjmo.GlyphIds[0]; + ii[0] = ljmo; + ii[1] = vjmo; collection.Replace(index, ii); - return index + 1; + return index; } - GlyphShapingData tjmo = collection.GetGlyphShapingData(t); - collection.EnableShapingFeature(t, TjmoTag); + collection.EnableShapingFeature(index, TjmoTag); Span iii = stackalloc ushort[3]; - iii[0] = ljmo.GlyphIds[0]; - iii[1] = vjmo.GlyphIds[0]; - iii[2] = tjmo.GlyphIds[0]; + iii[0] = ljmo; + iii[1] = vjmo; + iii[2] = tjmo; collection.Replace(index, iii); - return index + 2; + return index; } - private int ComposeGlyph(GlyphSubstitutionCollection collection, GlyphShapingData data, int i, int type) + private int ComposeGlyph(GlyphSubstitutionCollection collection, GlyphShapingData data, int index, int type) { - GlyphShapingData prev = collection.GetGlyphShapingData(i - 1); + if (index == 0) + { + return index; + } + + GlyphShapingData prev = collection.GetGlyphShapingData(index - 1); CodePoint prevCodePoint = prev.CodePoint; - int prevType = GetType(prevCodePoint); + int prevType = GetSyllableType(prevCodePoint); // Figure out what type of syllable we're dealing with CodePoint lv = default; @@ -226,7 +240,7 @@ private int ComposeGlyph(GlyphSubstitutionCollection collection, GlyphShapingDat else { // - ljmo = collection.GetGlyphShapingData(i - 2); + ljmo = collection.GetGlyphShapingData(index - 2); vjmo = prev; tjmo = data; } @@ -239,22 +253,22 @@ private int ComposeGlyph(GlyphSubstitutionCollection collection, GlyphShapingDat { lv = new CodePoint(HANGUL_BASE + ((((l.Value - L_BASE) * V_COUNT) + (v.Value - V_BASE)) * T_COUNT)); } + } - CodePoint t = tjmo?.CodePoint ?? new CodePoint(T_BASE); - if ((lv != default) && (t.Value == T_BASE || isCombiningT(t))) - { - CodePoint s = new(lv.Value + (t.Value - T_BASE)); + CodePoint t = tjmo?.CodePoint ?? new CodePoint(T_BASE); + if ((lv != default) && (t.Value == T_BASE || isCombiningT(t))) + { + CodePoint s = new(lv.Value + (t.Value - T_BASE)); - // Replace with a composed glyph if supported by the font, - // otherwise apply the proper OpenType features to each component. - FontMetrics metrics = data.TextRun.Font!.FontMetrics; - if (metrics.TryGetGlyphId(s, out ushort id)) - { - int del = prevType == V ? 3 : 2; - int idx = i - del + 1; - collection.Replace(idx, del, id); - return idx; - } + // Replace with a composed glyph if supported by the font, + // otherwise apply the proper OpenType features to each component. + FontMetrics metrics = data.TextRun.Font!.FontMetrics; + if (metrics.TryGetGlyphId(s, out ushort id)) + { + int del = prevType == V ? 3 : 2; + int idx = index - del + 1; + collection.Replace(idx, del, id); + return idx; } } @@ -279,22 +293,42 @@ private int ComposeGlyph(GlyphSubstitutionCollection collection, GlyphShapingDat // Sequence was originally , which got combined earlier. // Either the T was non-combining, or the LVT glyph wasn't supported. // Decompose the glyph again and apply OT features. - data = collection.GetGlyphShapingData(i - 1); - this.DecomposeGlyph(collection, data, i - 1); - return i + 1; + data = collection.GetGlyphShapingData(index - 1); + this.DecomposeGlyph(collection, data, index - 1); } - return i; + return index; } - private int ReOrderToneMark(GlyphShapingData data, int i) + private int ReOrderToneMark(GlyphSubstitutionCollection collection, GlyphShapingData data, int index) { + if (index == 0) + { + return index; + } + + // Move tone mark to the beginning of the previous syllable, unless it is zero width + // We don't have access to the glyphs metrics as an array when substituting so we have to loop. + FontMetrics metrics = data.TextRun.Font!.FontMetrics; + foreach (GlyphMetrics gm in metrics.GetGlyphMetrics(data.CodePoint, collection.TextOptions.ColorFontSupport)) + { + if (gm.Width == 0) + { + return index; + } + } + + GlyphShapingData prev = collection.GetGlyphShapingData(index - 1); + int len = GetSyllableLength(prev.CodePoint); + collection.MoveGlyph(index, index - len); throw new NotImplementedException(); } private int InsertDottedCircle(GlyphShapingData data, int i) { - throw new NotImplementedException(); + // TODO: Implement. + // We need to find a way to do this that doesn't require increasing the length of the collection. + return i; } private static bool isCombiningL(CodePoint code) => UnicodeUtility.IsInRangeInclusive((uint)code.Value, L_BASE, L_END); From 94603237be18560e3af7273925d0d4a4db0dc3be Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 28 Apr 2022 00:22:37 +1000 Subject: [PATCH 03/10] Simplify GlyphSubstitutionCollection --- .../GlyphSubstitutionCollection.cs | 112 ++++++------------ 1 file changed, 35 insertions(+), 77 deletions(-) diff --git a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs index 1c94caed..d6219ec9 100644 --- a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs +++ b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs @@ -15,14 +15,9 @@ namespace SixLabors.Fonts internal sealed class GlyphSubstitutionCollection : IGlyphShapingCollection { /// - /// Contains a map between the index of a map within the collection and its offset. + /// Contains a map the index of a map within the collection, non-sequential codepoint offsets, and their glyph ids. /// - private readonly List offsets = new(); - - /// - /// Contains a map between non-sequential codepoint offsets and their glyph ids. - /// - private readonly Dictionary glyphs = new(); + private readonly List glyphs = new(); /// /// Initializes a new instance of the class. @@ -38,7 +33,7 @@ public GlyphSubstitutionCollection(TextOptions textOptions) /// Gets the number of glyphs ids contained in the collection. /// This may be more or less than original input codepoint count (due to substitution process). /// - public int Count => this.offsets.Count; + public int Count => this.glyphs.Count; /// public bool IsVerticalLayoutMode { get; } @@ -52,11 +47,11 @@ public GlyphSubstitutionCollection(TextOptions textOptions) public int LigatureId { get; set; } = 1; /// - public ReadOnlySpan this[int index] => this.glyphs[this.offsets[index]].GlyphIds; + public ReadOnlySpan this[int index] => this.glyphs[index].Data.GlyphIds; /// public GlyphShapingData GetGlyphShapingData(int index) - => this.glyphs[this.offsets[index]]; + => this.glyphs[index].Data; /// /// Gets the shaping data at the specified position. @@ -66,18 +61,19 @@ public GlyphShapingData GetGlyphShapingData(int index) /// The . internal GlyphShapingData GetGlyphShapingData(int index, out int offset) { - offset = this.offsets[index]; - return this.glyphs[offset]; + OffsetGlyphDataPair pair = this.glyphs[index]; + offset = pair.Offset; + return pair.Data; } /// public void AddShapingFeature(int index, TagEntry feature) - => this.glyphs[this.offsets[index]].Features.Add(feature); + => this.glyphs[index].Data.Features.Add(feature); /// public void EnableShapingFeature(int index, Tag feature) { - foreach (TagEntry tagEntry in this.glyphs[this.offsets[index]].Features) + foreach (TagEntry tagEntry in this.glyphs[index].Data.Features) { if (tagEntry.Tag == feature) { @@ -96,57 +92,22 @@ public void EnableShapingFeature(int index, Tag feature) /// The text run this glyph belongs to. /// The zero-based index within the input codepoint collection. public void AddGlyph(ushort glyphId, CodePoint codePoint, TextDirection direction, TextRun textRun, int offset) - { - this.glyphs.Add(offset, new(textRun) + => this.glyphs.Add(new(offset, new(textRun) { CodePoint = codePoint, Direction = direction, GlyphIds = new[] { glyphId }, - }); - this.offsets.Add(offset); - } - - /// - /// Removes the glyph at the given index. - /// - /// The zero-based index of the element to remove. - public void RemoveGlyph(int index) - { - this.glyphs.Remove(this.offsets[index]); - this.offsets.RemoveAt(index); - } + })); + // TODO: This can be made specific to the Hangul shaper. public void MoveGlyph(int fromIndex, int toIndex) - { - GlyphShapingData data = this.GetGlyphShapingData(fromIndex); - this.RemoveGlyph(fromIndex); - - if (fromIndex > toIndex) - { - int idx = fromIndex; - while (idx > toIndex) - { - this.glyphs[this.offsets[idx]] = this.glyphs[this.offsets[idx - 1]]; - } - } - else - { - int idx = toIndex; - while (idx > fromIndex) - { - this.glyphs[this.offsets[idx - 1]] = this.glyphs[this.offsets[idx]]; - } - } - - this.glyphs[this.offsets[toIndex]] = data; - } + => this.glyphs[toIndex].Data = this.glyphs[fromIndex].Data; /// /// Removes all elements from the collection. /// public void Clear() { - this.offsets.Clear(); this.glyphs.Clear(); this.LigatureId = 1; } @@ -165,7 +126,17 @@ public void Clear() /// for the specified offset; otherwise, . /// public bool TryGetGlyphShapingDataAtOffset(int offset, [NotNullWhen(true)] out GlyphShapingData? data) - => this.glyphs.TryGetValue(offset, out data); + { + OffsetGlyphDataPair? pair = this.glyphs.Find(x => x.Offset == offset); + if (pair is null) + { + data = null; + return false; + } + + data = pair.Data; + return true; + } /// /// Performs a 1:1 replacement of a glyph id at the given position. @@ -173,11 +144,7 @@ public bool TryGetGlyphShapingDataAtOffset(int offset, [NotNullWhen(true)] out G /// The zero-based index of the element to replace. /// The replacement glyph id. public void Replace(int index, ushort glyphId) - { - int offset = this.offsets[index]; - GlyphShapingData current = this.glyphs[offset]; - current.GlyphIds = new[] { glyphId }; - } + => this.glyphs[index].Data.GlyphIds = new[] { glyphId }; /// /// Performs a 1:1 replacement of a glyph id at the given position while removing a series of glyph ids at the given positions within the sequence. @@ -195,15 +162,12 @@ public void Replace(int index, ReadOnlySpan removalIndices, ushort glyphId, for (int i = removalIndices.Length - 1; i >= 0; i--) { int match = removalIndices[i]; - int matchOffset = this.offsets[match]; - codePointCount += this.glyphs[matchOffset].CodePointCount; - this.glyphs.Remove(matchOffset); - this.offsets.RemoveAt(match); + codePointCount += this.glyphs[match].Data.CodePointCount; + this.glyphs.RemoveAt(match); } // Assign our new id at the index. - int offset = this.offsets[index]; - GlyphShapingData current = this.glyphs[offset]; + GlyphShapingData current = this.glyphs[index].Data; current.CodePointCount += codePointCount; current.GlyphIds = new[] { glyphId }; current.LigatureId = ligatureId; @@ -215,8 +179,7 @@ public void Replace(int index, ReadOnlySpan removalIndices, ushort glyphId, { // Spec disallows removal of glyphs in this manner but it's common enough practice to allow it. // https://github.com/MicrosoftDocs/typography-issues/issues/673 - this.glyphs.Remove(this.offsets[index]); - this.offsets.RemoveAt(index); + this.glyphs.RemoveAt(index); } } @@ -235,15 +198,12 @@ public void Replace(int index, int count, ushort glyphId) for (int i = count - 1; i >= 0; i--) { int match = index + i; - int matchOffset = this.offsets[match]; - codePointCount += this.glyphs[matchOffset].CodePointCount; - this.glyphs.Remove(matchOffset); - this.offsets.RemoveAt(match); + codePointCount += this.glyphs[match].Data.CodePointCount; + this.glyphs.RemoveAt(match); } // Assign our new id at the index. - int offset = this.offsets[index]; - GlyphShapingData current = this.glyphs[offset]; + GlyphShapingData current = this.glyphs[index].Data; current.CodePointCount += codePointCount; current.GlyphIds = new[] { glyphId }; current.LigatureId = 0; @@ -255,8 +215,7 @@ public void Replace(int index, int count, ushort glyphId) { // Spec disallows removal of glyphs in this manner but it's common enough practice to allow it. // https://github.com/MicrosoftDocs/typography-issues/issues/673 - this.glyphs.Remove(this.offsets[index]); - this.offsets.RemoveAt(index); + this.glyphs.RemoveAt(index); } } @@ -270,8 +229,7 @@ public void Replace(int index, ReadOnlySpan glyphIds) // TODO: // Features most likely need to be bound to each glyph index. // TODO: FontKit stores the ids in sequence with increasing ligature component values. - int offset = this.offsets[index]; - GlyphShapingData current = this.glyphs[offset]; + GlyphShapingData current = this.glyphs[index].Data; current.GlyphIds = glyphIds.ToArray(); current.LigatureComponent = 0; current.MarkAttachment = -1; From 8c901511d4db9d45c98644e29f2c06b97bb35b06 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 28 Apr 2022 23:15:45 +1000 Subject: [PATCH 04/10] Wire up shaper --- src/SixLabors.Fonts/GlyphMetrics.cs | 40 +++- .../GlyphPositioningCollection.cs | 13 ++ .../GlyphSubstitutionCollection.cs | 41 ++++- .../IGlyphShapingCollection.cs | 7 + .../Tables/AdvancedTypographic/GSubTable.cs | 5 + .../Shapers/DefaultShaper.cs | 42 ++--- .../Shapers/HangulShaper.cs | 131 ++++++++----- .../Shapers/ShaperFactory.cs | 1 + src/SixLabors.Fonts/Unicode/UnicodeUtility.cs | 174 ++++++++++++++++++ src/UnicodeTrieGenerator/Rules/README.md | 2 +- 10 files changed, 386 insertions(+), 70 deletions(-) diff --git a/src/SixLabors.Fonts/GlyphMetrics.cs b/src/SixLabors.Fonts/GlyphMetrics.cs index 2f23484b..40681182 100644 --- a/src/SixLabors.Fonts/GlyphMetrics.cs +++ b/src/SixLabors.Fonts/GlyphMetrics.cs @@ -237,6 +237,12 @@ internal FontRectangle GetBoundingBox(Vector2 origin, float scaledPointSize) /// Too many control points internal void RenderTo(IGlyphRenderer surface, float pointSize, Vector2 location, TextOptions options) { + // https://www.unicode.org/faq/unsup_char.html + if (ShouldSkipGlyphRendering(this.CodePoint)) + { + return; + } + float dpi = options.Dpi; location *= dpi; float scaledPPEM = dpi * pointSize; @@ -255,7 +261,7 @@ internal void RenderTo(IGlyphRenderer surface, float pointSize, Vector2 location if (surface.BeginGlyph(box, parameters)) { - if (!CodePoint.IsWhiteSpace(this.CodePoint)) + if (!ShouldRenderWhiteSpaceOnly(this.CodePoint)) { if (this.GlyphColor.HasValue && surface is IColorGlyphRenderer colorSurface) { @@ -452,6 +458,38 @@ void SetDecoration(TextDecorations decorationType, float thickness, float positi surface.EndGlyph(); } + private static bool ShouldSkipGlyphRendering(CodePoint codePoint) + { + uint value = (uint)codePoint.Value; + return UnicodeUtility.IsDefaultIgnorableCodePoint(value) && !ShouldRenderWhiteSpaceOnly(codePoint); + } + + private static bool ShouldRenderWhiteSpaceOnly(CodePoint codePoint) + { + if (CodePoint.IsWhiteSpace(codePoint)) + { + return true; + } + + // Note: While U+115F, U+1160, U+3164 and U+FFA0 are Default_Ignorable, + // we do NOT want to hide them, as the way Uniscribe has implemented them + // is with regular spacing glyphs, and that's the way fonts are made to work. + // As such, we make exceptions for those four. + // Also ignoring U+1BCA0..1BCA3. https://github.com/harfbuzz/harfbuzz/issues/503 + uint value = (uint)codePoint.Value; + if (value is 0x115F or 0x1160 or 0x3164 or 0xFFA0) + { + return true; + } + + if (UnicodeUtility.IsInRangeInclusive(value, 0x1BCA0, 0x1BCA3)) + { + return true; + } + + return false; + } + private static void AlignToGrid(ref Vector2 point) { var floorPoint = new Vector2(MathF.Floor(point.X), MathF.Floor(point.Y)); diff --git a/src/SixLabors.Fonts/GlyphPositioningCollection.cs b/src/SixLabors.Fonts/GlyphPositioningCollection.cs index 11a68bdc..57eaf664 100644 --- a/src/SixLabors.Fonts/GlyphPositioningCollection.cs +++ b/src/SixLabors.Fonts/GlyphPositioningCollection.cs @@ -72,6 +72,19 @@ public void EnableShapingFeature(int index, Tag feature) } } + /// + public void DisableShapingFeature(int index, Tag feature) + { + foreach (TagEntry tagEntry in this.glyphs[index].Features) + { + if (tagEntry.Tag == feature) + { + tagEntry.Enabled = false; + break; + } + } + } + /// /// Gets the glyph metrics at the given codepoint offset. /// diff --git a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs index d6219ec9..35a532f2 100644 --- a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs +++ b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs @@ -83,6 +83,19 @@ public void EnableShapingFeature(int index, Tag feature) } } + /// + public void DisableShapingFeature(int index, Tag feature) + { + foreach (TagEntry tagEntry in this.glyphs[index].Data.Features) + { + if (tagEntry.Tag == feature) + { + tagEntry.Enabled = false; + break; + } + } + } + /// /// Adds the glyph id and the codepoint it represents to the collection. /// @@ -99,9 +112,33 @@ public void AddGlyph(ushort glyphId, CodePoint codePoint, TextDirection directio GlyphIds = new[] { glyphId }, })); - // TODO: This can be made specific to the Hangul shaper. + /// + /// Moves the specified glyph to the specified position. + /// + /// The index to move from. + /// The index to move to. public void MoveGlyph(int fromIndex, int toIndex) - => this.glyphs[toIndex].Data = this.glyphs[fromIndex].Data; + { + GlyphShapingData data = this.GetGlyphShapingData(fromIndex); + if (fromIndex > toIndex) + { + int idx = fromIndex; + while (idx > toIndex) + { + this.glyphs[idx].Data = this.glyphs[idx - 1].Data; + } + } + else + { + int idx = toIndex; + while (idx > fromIndex) + { + this.glyphs[idx - 1].Data = this.glyphs[idx].Data; + } + } + + this.glyphs[toIndex].Data = data; + } /// /// Removes all elements from the collection. diff --git a/src/SixLabors.Fonts/IGlyphShapingCollection.cs b/src/SixLabors.Fonts/IGlyphShapingCollection.cs index 06e00821..0ce54627 100644 --- a/src/SixLabors.Fonts/IGlyphShapingCollection.cs +++ b/src/SixLabors.Fonts/IGlyphShapingCollection.cs @@ -53,5 +53,12 @@ internal interface IGlyphShapingCollection /// The zero-based index of the element. /// The feature to enable. void EnableShapingFeature(int index, Tag feature); + + /// + /// Disables a previously added shaping feature. + /// + /// The zero-based index of the element. + /// The feature to disable. + void DisableShapingFeature(int index, Tag feature); } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs index cb10582e..f3691f69 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; using System.Collections.Generic; using SixLabors.Fonts.Tables.AdvancedTypographic.GSub; using SixLabors.Fonts.Tables.AdvancedTypographic.Shapers; @@ -125,6 +126,10 @@ public void ApplySubstitution(FontMetrics fontMetrics, GlyphSubstitutionCollecti // Assign Substitution features to each glyph. shaper.AssignFeatures(collection, index, count); + + // Shapers can adjust the count. + count = (ushort)Math.Min(count, collection.Count - index); + IEnumerable stageFeatures = shaper.GetShapingStageFeatures(); int currentCount = collection.Count; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs index 1e189a6f..7e49d3f9 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs @@ -12,47 +12,47 @@ namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers /// internal class DefaultShaper : BaseShaper { - private static readonly Tag RvnrTag = Tag.Parse("rvrn"); + protected static readonly Tag RvnrTag = Tag.Parse("rvrn"); - private static readonly Tag LtraTag = Tag.Parse("ltra"); + protected static readonly Tag LtraTag = Tag.Parse("ltra"); - private static readonly Tag LtrmTag = Tag.Parse("ltrm"); + protected static readonly Tag LtrmTag = Tag.Parse("ltrm"); - private static readonly Tag RtlaTag = Tag.Parse("rtla"); + protected static readonly Tag RtlaTag = Tag.Parse("rtla"); - private static readonly Tag RtlmTag = Tag.Parse("rtlm"); + protected static readonly Tag RtlmTag = Tag.Parse("rtlm"); - private static readonly Tag FracTag = Tag.Parse("frac"); + protected static readonly Tag FracTag = Tag.Parse("frac"); - private static readonly Tag NumrTag = Tag.Parse("numr"); + protected static readonly Tag NumrTag = Tag.Parse("numr"); - private static readonly Tag DnomTag = Tag.Parse("dnom"); + protected static readonly Tag DnomTag = Tag.Parse("dnom"); - private static readonly Tag CcmpTag = Tag.Parse("ccmp"); + protected static readonly Tag CcmpTag = Tag.Parse("ccmp"); - private static readonly Tag LoclTag = Tag.Parse("locl"); + protected static readonly Tag LoclTag = Tag.Parse("locl"); - private static readonly Tag RligTag = Tag.Parse("rlig"); + protected static readonly Tag RligTag = Tag.Parse("rlig"); - private static readonly Tag MarkTag = Tag.Parse("mark"); + protected static readonly Tag MarkTag = Tag.Parse("mark"); - private static readonly Tag MkmkTag = Tag.Parse("mkmk"); + protected static readonly Tag MkmkTag = Tag.Parse("mkmk"); - private static readonly Tag CaltTag = Tag.Parse("calt"); + protected static readonly Tag CaltTag = Tag.Parse("calt"); - private static readonly Tag CligTag = Tag.Parse("clig"); + protected static readonly Tag CligTag = Tag.Parse("clig"); - private static readonly Tag LigaTag = Tag.Parse("liga"); + protected static readonly Tag LigaTag = Tag.Parse("liga"); - private static readonly Tag RcltTag = Tag.Parse("rclt"); + protected static readonly Tag RcltTag = Tag.Parse("rclt"); - private static readonly Tag CursTag = Tag.Parse("curs"); + protected static readonly Tag CursTag = Tag.Parse("curs"); - private static readonly Tag KernTag = Tag.Parse("kern"); + protected static readonly Tag KernTag = Tag.Parse("kern"); - private static readonly Tag VertTag = Tag.Parse("vert"); + protected static readonly Tag VertTag = Tag.Parse("vert"); - private static readonly Tag VKernTag = Tag.Parse("vkrn"); + protected static readonly Tag VKernTag = Tag.Parse("vkrn"); private static readonly CodePoint FractionSlash = new(0x2044); diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs index e711478c..8ccceef0 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs @@ -19,19 +19,19 @@ internal sealed class HangulShaper : DefaultShaper private static readonly Tag TjmoTag = Tag.Parse("tjmo"); - private const int HANGUL_BASE = 0xac00; - private const int HANGUL_END = 0xd7a4; - private const int HANGUL_COUNT = HANGUL_END - HANGUL_BASE + 1; - private const int L_BASE = 0x1100; // lead - private const int V_BASE = 0x1161; // vowel - private const int T_BASE = 0x11a7; // trail - private const int L_COUNT = 19; - private const int V_COUNT = 21; - private const int T_COUNT = 28; - private const int L_END = L_BASE + L_COUNT - 1; - private const int V_END = V_BASE + V_COUNT - 1; - private const int T_END = T_BASE + T_COUNT - 1; - private const int DOTTED_CIRCLE = 0x25cc; + private const int HangulBase = 0xac00; + private const int HangulEnd = 0xd7a4; + private const int HangulCount = HangulEnd - HangulBase + 1; + private const int LBase = 0x1100; // lead + private const int VBase = 0x1161; // vowel + private const int TBase = 0x11a7; // trail + private const int LCount = 19; + private const int VCount = 21; + private const int TCount = 28; + private const int LEnd = LBase + LCount - 1; + private const int VEnd = VBase + VCount - 1; + private const int TEnd = TBase + TCount - 1; + private const int DottedCircle = 0x25cc; // Character categories private const byte X = 0; // Other character @@ -80,6 +80,14 @@ public override void AssignFeatures(IGlyphShapingCollection collection, int inde base.AssignFeatures(collection, index, count); + for (int i = index; i < count; i++) + { + // Uniscribe does not apply 'calt' for Hangul, and certain fonts + // (Noto Sans CJK, Source Sans Han, etc) apply all of jamo lookups + // in calt, which is not desirable. + collection.DisableShapingFeature(i, CaltTag); + } + // Apply the state machine to map glyphs to features. if (collection is GlyphSubstitutionCollection substitutionCollection) { @@ -87,6 +95,11 @@ public override void AssignFeatures(IGlyphShapingCollection collection, int inde int state = 0; for (int i = 0; i < count; i++) { + if (i + index >= substitutionCollection.Count) + { + break; + } + GlyphShapingData data = substitutionCollection.GetGlyphShapingData(i + index); CodePoint codePoint = data.CodePoint; int type = GetSyllableType(codePoint); @@ -98,7 +111,7 @@ public override void AssignFeatures(IGlyphShapingCollection collection, int inde case Decompose: // Decompose the composed syllable if it is not supported by the font. - if (!data.TextRun.Font!.FontMetrics.TryGetGlyphId(codePoint, out ushort _)) + if (data.GlyphIds[0] == 0) { i = this.DecomposeGlyph(substitutionCollection, data, i); } @@ -129,6 +142,34 @@ public override void AssignFeatures(IGlyphShapingCollection collection, int inde // GPos // Simply loop and enable based on type. // Glyph substitution has handled [de]composition. + for (int i = 0; i < count; i++) + { + if (i + index >= collection.Count) + { + break; + } + + GlyphShapingData data = collection.GetGlyphShapingData(i + index); + CodePoint codePoint = data.CodePoint; + switch (GetSyllableType(codePoint)) + { + case L: + collection.EnableShapingFeature(i, LjmoTag); + break; + case V: + collection.EnableShapingFeature(i, VjmoTag); + break; + case T: + collection.EnableShapingFeature(i, TjmoTag); + break; + case LV: + collection.EnableShapingFeature(i, LjmoTag); + collection.EnableShapingFeature(i, VjmoTag); + break; + default: + break; + } + } } } @@ -163,18 +204,18 @@ private static int GetSyllableLength(CodePoint codePoint) private int DecomposeGlyph(GlyphSubstitutionCollection collection, GlyphShapingData data, int index) { // Decompose the syllable into a sequence of glyphs. - int s = data.CodePoint.Value - HANGUL_BASE; - int t = T_BASE + (s % T_COUNT); - s = (s / T_COUNT) | 0; - int l = (L_BASE + (s / V_COUNT)) | 0; - int v = V_BASE + (s % V_COUNT); + int s = data.CodePoint.Value - HangulBase; + int t = TBase + (s % TCount); + s = (s / TCount) | 0; + int l = (LBase + (s / VCount)) | 0; + int v = VBase + (s % VCount); FontMetrics metrics = data.TextRun.Font!.FontMetrics; // Don't decompose if all of the components are not available if (!metrics.TryGetGlyphId(new(l), out ushort ljmo) || !metrics.TryGetGlyphId(new(v), out ushort vjmo) || - (!metrics.TryGetGlyphId(new(t), out ushort tjmo) && t != T_BASE)) + (!metrics.TryGetGlyphId(new(t), out ushort tjmo) && t != TBase)) { return index; } @@ -188,7 +229,7 @@ private int DecomposeGlyph(GlyphSubstitutionCollection collection, GlyphShapingD collection.EnableShapingFeature(index, LjmoTag); collection.EnableShapingFeature(index, VjmoTag); - if (t <= T_BASE) + if (t <= TBase) { Span ii = stackalloc ushort[2]; ii[0] = ljmo; @@ -221,44 +262,44 @@ private int ComposeGlyph(GlyphSubstitutionCollection collection, GlyphShapingDat // Figure out what type of syllable we're dealing with CodePoint lv = default; - GlyphShapingData? ljmo = null, vjmo = null, tjmo = null; + int ljmo = -1, vjmo = -1, tjmo = -1; if (prevType == LV && type == T) { // lv = prevCodePoint; - tjmo = data; + tjmo = index; } else { if (type == V) { // - ljmo = prev; - vjmo = data; + ljmo = index - 1; + vjmo = index; } else { // - ljmo = collection.GetGlyphShapingData(index - 2); - vjmo = prev; - tjmo = data; + ljmo = index - 2; + vjmo = index - 1; + tjmo = index; } - CodePoint l = ljmo.CodePoint; - CodePoint v = vjmo.CodePoint; + CodePoint l = collection.GetGlyphShapingData(ljmo).CodePoint; + CodePoint v = collection.GetGlyphShapingData(vjmo).CodePoint; // Make sure L and V are combining characters - if (isCombiningL(l) && isCombiningV(v)) + if (IsCombiningL(l) && IsCombiningV(v)) { - lv = new CodePoint(HANGUL_BASE + ((((l.Value - L_BASE) * V_COUNT) + (v.Value - V_BASE)) * T_COUNT)); + lv = new CodePoint(HangulBase + ((((l.Value - LBase) * VCount) + (v.Value - VBase)) * TCount)); } } - CodePoint t = tjmo?.CodePoint ?? new CodePoint(T_BASE); - if ((lv != default) && (t.Value == T_BASE || isCombiningT(t))) + CodePoint t = tjmo >= 0 ? collection.GetGlyphShapingData(tjmo).CodePoint : new CodePoint(TBase); + if ((lv != default) && (t.Value == TBase || IsCombiningT(t))) { - CodePoint s = new(lv.Value + (t.Value - T_BASE)); + CodePoint s = new(lv.Value + (t.Value - TBase)); // Replace with a composed glyph if supported by the font, // otherwise apply the proper OpenType features to each component. @@ -267,25 +308,25 @@ private int ComposeGlyph(GlyphSubstitutionCollection collection, GlyphShapingDat { int del = prevType == V ? 3 : 2; int idx = index - del + 1; - collection.Replace(idx, del, id); + collection.Replace(idx, del - 1, id); return idx; } } // Didn't compose (either a non-combining component or unsupported by font). - if (ljmo != null) + if (ljmo >= 0) { - collection.EnableShapingFeature(ljmo.GlyphIds[0], LjmoTag); + collection.EnableShapingFeature(ljmo, LjmoTag); } - if (vjmo != null) + if (vjmo >= 0) { - collection.EnableShapingFeature(vjmo.GlyphIds[0], VjmoTag); + collection.EnableShapingFeature(vjmo, VjmoTag); } - if (tjmo != null) + if (tjmo >= 0) { - collection.EnableShapingFeature(tjmo.GlyphIds[0], TjmoTag); + collection.EnableShapingFeature(tjmo, TjmoTag); } if (prevType == LV) @@ -331,10 +372,10 @@ private int InsertDottedCircle(GlyphShapingData data, int i) return i; } - private static bool isCombiningL(CodePoint code) => UnicodeUtility.IsInRangeInclusive((uint)code.Value, L_BASE, L_END); + private static bool IsCombiningL(CodePoint code) => UnicodeUtility.IsInRangeInclusive((uint)code.Value, LBase, LEnd); - private static bool isCombiningV(CodePoint code) => UnicodeUtility.IsInRangeInclusive((uint)code.Value, V_BASE, V_END); + private static bool IsCombiningV(CodePoint code) => UnicodeUtility.IsInRangeInclusive((uint)code.Value, VBase, VEnd); - private static bool isCombiningT(CodePoint code) => UnicodeUtility.IsInRangeInclusive((uint)code.Value, T_BASE + 1, T_END); + private static bool IsCombiningT(CodePoint code) => UnicodeUtility.IsInRangeInclusive((uint)code.Value, TBase + 1, TEnd); } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShaperFactory.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShaperFactory.cs index 47e2bd1e..7cc28117 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShaperFactory.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShaperFactory.cs @@ -24,6 +24,7 @@ or ScriptClass.PhagsPa or ScriptClass.Mandaic or ScriptClass.Manichaean or ScriptClass.PsalterPahlavi => new ArabicShaper(textOptions), + ScriptClass.Hangul => new HangulShaper(textOptions), _ => new DefaultShaper(textOptions), }; } diff --git a/src/SixLabors.Fonts/Unicode/UnicodeUtility.cs b/src/SixLabors.Fonts/Unicode/UnicodeUtility.cs index a6edeb2d..53e052bf 100644 --- a/src/SixLabors.Fonts/Unicode/UnicodeUtility.cs +++ b/src/SixLabors.Fonts/Unicode/UnicodeUtility.cs @@ -230,6 +230,180 @@ public static bool IsCJKCodePoint(uint value) return false; } + /// + /// Returns if is a Default Ignorable Code Point. + /// + /// + /// + /// + /// + public static bool IsDefaultIgnorableCodePoint(uint value) + { + // SOFT HYPHEN + if (value == 0x00AD) + { + return true; + } + + // COMBINING GRAPHEME JOINER + if (value == 0x034F) + { + return true; + } + + // COMBINING GRAPHEME JOINER + if (value == 0x061C) + { + return true; + } + + // HANGUL CHOSEONG FILLER..HANGUL JUNGSEONG FILLER + if (IsInRangeInclusive(value, 0x115F, 0x1160)) + { + return true; + } + + // KHMER VOWEL INHERENT AQ..KHMER VOWEL INHERENT AA + if (IsInRangeInclusive(value, 0x17B4, 0x17B5)) + { + return true; + } + + // MONGOLIAN FREE VARIATION SELECTOR ONE..MONGOLIAN FREE VARIATION SELECTOR THREE + if (IsInRangeInclusive(value, 0x180B, 0x180D)) + { + return true; + } + + // MONGOLIAN VOWEL SEPARATOR + if (value == 0x180E) + { + return true; + } + + // MONGOLIAN FREE VARIATION SELECTOR FOUR + if (value == 0x180F) + { + return true; + } + + // ZERO WIDTH SPACE..RIGHT-TO-LEFT MARK + if (IsInRangeInclusive(value, 0x200B, 0x200F)) + { + return true; + } + + // LEFT-TO-RIGHT EMBEDDING..RIGHT-TO-LEFT OVERRIDE + if (IsInRangeInclusive(value, 0x202A, 0x202E)) + { + return true; + } + + // WORD JOINER..INVISIBLE PLUS + if (IsInRangeInclusive(value, 0x2060, 0x2064)) + { + return true; + } + + // + if (value == 0x2065) + { + return true; + } + + // LEFT-TO-RIGHT ISOLATE..NOMINAL DIGIT SHAPES + if (IsInRangeInclusive(value, 0x2066, 0x206F)) + { + return true; + } + + // HANGUL FILLER + if (value == 0x3164) + { + return true; + } + + // VARIATION SELECTOR-1..VARIATION SELECTOR-16 + if (IsInRangeInclusive(value, 0xFE00, 0xFE0F)) + { + return true; + } + + // ZERO WIDTH NO-BREAK SPACE + if (value == 0xFEFF) + { + return true; + } + + // HALFWIDTH HANGUL FILLER + if (value == 0xFFA0) + { + return true; + } + + // .. + if (IsInRangeInclusive(value, 0xFFF0, 0xFFF8)) + { + return true; + } + + // SHORTHAND FORMAT LETTER OVERLAP..SHORTHAND FORMAT UP STEP + if (IsInRangeInclusive(value, 0x1BCA0, 0x1BCA3)) + { + return true; + } + + // MUSICAL SYMBOL BEGIN BEAM..MUSICAL SYMBOL END PHRASE + if (IsInRangeInclusive(value, 0x1D173, 0x1D17A)) + { + return true; + } + + // + if (value == 0xE0000) + { + return true; + } + + // LANGUAGE TAG + if (value == 0xE0001) + { + return true; + } + + // .. + if (IsInRangeInclusive(value, 0xE0002, 0xE001F)) + { + return true; + } + + // TAG SPACE..CANCEL TAG + if (IsInRangeInclusive(value, 0xE0020, 0xE007F)) + { + return true; + } + + // .. + if (IsInRangeInclusive(value, 0xE0080, 0xE00FF)) + { + return true; + } + + // VARIATION SELECTOR-17..VARIATION SELECTOR-256 + if (IsInRangeInclusive(value, 0xE0100, 0xE01EF)) + { + return true; + } + + // .. + if (IsInRangeInclusive(value, 0xE01F0, 0xE0FFF)) + { + return true; + } + + return false; + } + /// /// Returns the Unicode plane (0 through 16, inclusive) which contains this code point. /// diff --git a/src/UnicodeTrieGenerator/Rules/README.md b/src/UnicodeTrieGenerator/Rules/README.md index 26282114..c8aa4e2e 100644 --- a/src/UnicodeTrieGenerator/Rules/README.md +++ b/src/UnicodeTrieGenerator/Rules/README.md @@ -1,3 +1,3 @@ Files sourced from: -https://www.unicode.org/Public/13.0.0/ucd/ +https://www.unicode.org/Public/14.0.0/ucd/ From 862d458789796c3d2a65adc71dfb82bf74c9b2ce Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 28 Apr 2022 23:21:03 +1000 Subject: [PATCH 05/10] Update ArabicShaper.cs --- .../Tables/AdvancedTypographic/Shapers/ArabicShaper.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs index 55c4829c..7c1c2c7e 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs @@ -12,10 +12,6 @@ namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers /// internal sealed class ArabicShaper : DefaultShaper { - private static readonly Tag CcmpTag = Tag.Parse("ccmp"); - - private static readonly Tag LoclTag = Tag.Parse("locl"); - private static readonly Tag MsetTag = Tag.Parse("mset"); private static readonly Tag FinaTag = Tag.Parse("fina"); From 94e612c1a7ce1c7849a7f9b48cfc59b709533780 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 29 Apr 2022 15:50:58 +1000 Subject: [PATCH 06/10] Handle offsets withing id collections --- src/SixLabors.Fonts/GlyphMetrics.cs | 3 + .../GlyphPositioningCollection.cs | 40 ++++++- src/SixLabors.Fonts/GlyphShapingData.cs | 8 +- .../GlyphSubstitutionCollection.cs | 104 +++++++++--------- .../GSub/LookupType2SubTable.cs | 4 +- .../Shapers/HangulShaper.cs | 59 +++++++--- 6 files changed, 142 insertions(+), 76 deletions(-) diff --git a/src/SixLabors.Fonts/GlyphMetrics.cs b/src/SixLabors.Fonts/GlyphMetrics.cs index 40681182..ada1e482 100644 --- a/src/SixLabors.Fonts/GlyphMetrics.cs +++ b/src/SixLabors.Fonts/GlyphMetrics.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Numerics; +using System.Runtime.CompilerServices; using SixLabors.Fonts.Tables.General; using SixLabors.Fonts.Tables.General.Glyphs; using SixLabors.Fonts.Unicode; @@ -458,12 +459,14 @@ void SetDecoration(TextDecorations decorationType, float thickness, float positi surface.EndGlyph(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool ShouldSkipGlyphRendering(CodePoint codePoint) { uint value = (uint)codePoint.Value; return UnicodeUtility.IsDefaultIgnorableCodePoint(value) && !ShouldRenderWhiteSpaceOnly(codePoint); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool ShouldRenderWhiteSpaceOnly(CodePoint codePoint) { if (CodePoint.IsWhiteSpace(codePoint)) diff --git a/src/SixLabors.Fonts/GlyphPositioningCollection.cs b/src/SixLabors.Fonts/GlyphPositioningCollection.cs index 57eaf664..05eab8d0 100644 --- a/src/SixLabors.Fonts/GlyphPositioningCollection.cs +++ b/src/SixLabors.Fonts/GlyphPositioningCollection.cs @@ -145,6 +145,8 @@ public bool TryUpdate(Font font, GlyphSubstitutionCollection collection) ushort[] glyphIds = data.GlyphIds; var m = new List(glyphIds.Length); + ushort shiftXY = 0; + bool doShift = data.OffsetGlyphs; foreach (ushort id in glyphIds) { // Perform a semi-deep clone (FontMetrics is not cloned) so we can continue to @@ -159,9 +161,25 @@ public bool TryUpdate(Font font, GlyphSubstitutionCollection collection) break; } + // Clone and offset the glyph based on it's position in the glyphId array. // We slip the text run in here while we clone so we have // it available to the renderer. - m.Add(GlyphMetrics.CloneForRendering(gm, data.TextRun, codePoint)); + var clone = GlyphMetrics.CloneForRendering(gm, data.TextRun, codePoint); + if (doShift) + { + if (!this.IsVerticalLayoutMode) + { + clone.ApplyOffset((short)shiftXY, 0); + shiftXY += clone.AdvanceWidth; + } + else + { + clone.ApplyOffset(0, (short)shiftXY); + shiftXY += clone.AdvanceHeight; + } + } + + m.Add(clone); } } @@ -205,6 +223,8 @@ public bool TryAdd(Font font, GlyphSubstitutionCollection collection) ushort[] glyphIds = data.GlyphIds; var m = new List(glyphIds.Length); + ushort shiftXY = 0; + bool doShift = data.OffsetGlyphs; foreach (ushort id in glyphIds) { // Perform a semi-deep clone (FontMetrics is not cloned) so we can continue to @@ -216,9 +236,25 @@ public bool TryAdd(Font font, GlyphSubstitutionCollection collection) hasFallBacks = true; } + // Clone and offset the glyph based on it's position in the glyphId array. // We slip the text run in here while we clone so we have // it available to the renderer. - m.Add(GlyphMetrics.CloneForRendering(gm, data.TextRun, codePoint)); + var clone = GlyphMetrics.CloneForRendering(gm, data.TextRun, codePoint); + if (doShift) + { + if (!this.IsVerticalLayoutMode) + { + clone.ApplyOffset((short)shiftXY, 0); + shiftXY += clone.AdvanceWidth; + } + else + { + clone.ApplyOffset(0, (short)shiftXY); + shiftXY += clone.AdvanceHeight; + } + } + + m.Add(clone); } } diff --git a/src/SixLabors.Fonts/GlyphShapingData.cs b/src/SixLabors.Fonts/GlyphShapingData.cs index 3019b476..c9758ef4 100644 --- a/src/SixLabors.Fonts/GlyphShapingData.cs +++ b/src/SixLabors.Fonts/GlyphShapingData.cs @@ -36,6 +36,7 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) this.LigatureComponent = data.LigatureComponent; this.MarkAttachment = data.MarkAttachment; this.CursiveAttachment = data.CursiveAttachment; + this.OffsetGlyphs = data.OffsetGlyphs; if (!clearFeatures) { @@ -100,8 +101,13 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) /// public GlyphShapingBounds Bounds { get; set; } = new(0, 0, 0, 0); + /// + /// Gets or sets a value indicating whether individual glyph in the collection should be offset from the preceding glyph. + /// + public bool OffsetGlyphs { get; set; } + private string DebuggerDisplay => FormattableString - .Invariant($"{this.CodePoint.ToDebuggerDisplay()} : {CodePoint.GetScriptClass(this.CodePoint)} : {this.Direction} : {this.TextRun.TextAttributes} : {this.LigatureId} : {this.LigatureComponent} : [{string.Join(",", this.GlyphIds)}]"); + .Invariant($"{this.CodePoint.ToDebuggerDisplay()} : {CodePoint.GetScriptClass(this.CodePoint)} : {this.Direction} : {this.TextRun.TextAttributes} : {this.LigatureId} : {this.LigatureComponent} : [{string.Join(",", this.GlyphIds)}] : {this.OffsetGlyphs}"); } } diff --git a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs index 35a532f2..36ffd1b2 100644 --- a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs +++ b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs @@ -126,6 +126,7 @@ public void MoveGlyph(int fromIndex, int toIndex) while (idx > toIndex) { this.glyphs[idx].Data = this.glyphs[idx - 1].Data; + idx--; } } else @@ -134,6 +135,7 @@ public void MoveGlyph(int fromIndex, int toIndex) while (idx > fromIndex) { this.glyphs[idx - 1].Data = this.glyphs[idx].Data; + idx--; } } @@ -192,32 +194,23 @@ public void Replace(int index, ushort glyphId) /// The ligature id. public void Replace(int index, ReadOnlySpan removalIndices, ushort glyphId, int ligatureId) { - if (removalIndices.Length > 0) + // Remove the glyphs at each index. + int codePointCount = 0; + for (int i = removalIndices.Length - 1; i >= 0; i--) { - // Remove the glyphs at each index. - int codePointCount = 0; - for (int i = removalIndices.Length - 1; i >= 0; i--) - { - int match = removalIndices[i]; - codePointCount += this.glyphs[match].Data.CodePointCount; - this.glyphs.RemoveAt(match); - } - - // Assign our new id at the index. - GlyphShapingData current = this.glyphs[index].Data; - current.CodePointCount += codePointCount; - current.GlyphIds = new[] { glyphId }; - current.LigatureId = ligatureId; - current.LigatureComponent = -1; - current.MarkAttachment = -1; - current.CursiveAttachment = -1; - } - else - { - // Spec disallows removal of glyphs in this manner but it's common enough practice to allow it. - // https://github.com/MicrosoftDocs/typography-issues/issues/673 - this.glyphs.RemoveAt(index); + int match = removalIndices[i]; + codePointCount += this.glyphs[match].Data.CodePointCount; + this.glyphs.RemoveAt(match); } + + // Assign our new id at the index. + GlyphShapingData current = this.glyphs[index].Data; + current.CodePointCount += codePointCount; + current.GlyphIds = new[] { glyphId }; + current.LigatureId = ligatureId; + current.LigatureComponent = -1; + current.MarkAttachment = -1; + current.CursiveAttachment = -1; } /// @@ -228,25 +221,43 @@ public void Replace(int index, ReadOnlySpan removalIndices, ushort glyphId, /// The replacement glyph id. public void Replace(int index, int count, ushort glyphId) { - if (count > 0) + // Remove the glyphs at each index. + int codePointCount = 0; + for (int i = count - 1; i >= 0; i--) { - // Remove the glyphs at each index. - int codePointCount = 0; - for (int i = count - 1; i >= 0; i--) - { - int match = index + i; - codePointCount += this.glyphs[match].Data.CodePointCount; - this.glyphs.RemoveAt(match); - } + int match = index + i; + codePointCount += this.glyphs[match].Data.CodePointCount; + this.glyphs.RemoveAt(match); + } + + // Assign our new id at the index. + GlyphShapingData current = this.glyphs[index].Data; + current.CodePointCount += codePointCount; + current.GlyphIds = new[] { glyphId }; + current.LigatureId = 0; + current.LigatureComponent = -1; + current.MarkAttachment = -1; + current.CursiveAttachment = -1; + } - // Assign our new id at the index. + /// + /// Replaces a single glyph id with a collection of glyph ids. + /// + /// The zero-based index of the element to replace. + /// The collection of replacement glyph ids. + /// Whether individual glyph in the collection should be offset from the preceding glyph. + public void Replace(int index, ReadOnlySpan glyphIds, bool offset) + { + if (glyphIds.Length > 0) + { + // TODO: Features most likely need to be bound to each glyph index. + // TODO: FontKit stores the ids in sequence with increasing ligature component values. GlyphShapingData current = this.glyphs[index].Data; - current.CodePointCount += codePointCount; - current.GlyphIds = new[] { glyphId }; - current.LigatureId = 0; - current.LigatureComponent = -1; + current.GlyphIds = glyphIds.ToArray(); + current.LigatureComponent = 0; current.MarkAttachment = -1; current.CursiveAttachment = -1; + current.OffsetGlyphs = offset; } else { @@ -256,23 +267,6 @@ public void Replace(int index, int count, ushort glyphId) } } - /// - /// Replaces a single glyph id with a collection of glyph ids. - /// - /// The zero-based index of the element to replace. - /// The collection of replacement glyph ids. - public void Replace(int index, ReadOnlySpan glyphIds) - { - // TODO: - // Features most likely need to be bound to each glyph index. - // TODO: FontKit stores the ids in sequence with increasing ligature component values. - GlyphShapingData current = this.glyphs[index].Data; - current.GlyphIds = glyphIds.ToArray(); - current.LigatureComponent = 0; - current.MarkAttachment = -1; - current.CursiveAttachment = -1; - } - private class OffsetGlyphDataPair { public OffsetGlyphDataPair(int offset, GlyphShapingData data) diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs index 24501114..9d0173e9 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs @@ -101,9 +101,7 @@ public override bool TrySubstitution( if (offset > -1) { - // TODO: Looks like we should remove the glyph if the substitutes - // length = 0; - collection.Replace(index, this.sequenceTables[offset].SubstituteGlyphs); + collection.Replace(index, this.sequenceTables[offset].SubstituteGlyphs, false); return true; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs index 8ccceef0..eb19fe38 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs @@ -20,8 +20,6 @@ internal sealed class HangulShaper : DefaultShaper private static readonly Tag TjmoTag = Tag.Parse("tjmo"); private const int HangulBase = 0xac00; - private const int HangulEnd = 0xd7a4; - private const int HangulCount = HangulEnd - HangulBase + 1; private const int LBase = 0x1100; // lead private const int VBase = 0x1161; // vowel private const int TBase = 0x11a7; // trail @@ -131,8 +129,9 @@ public override void AssignFeatures(IGlyphShapingCollection collection, int inde break; case Invalid: - // Tone mark has no valid syllable to attach to, so insert a dotted circle - i = this.InsertDottedCircle(data, i); + + // Tone mark has no valid syllable to attach to, so insert a dotted circle. + this.InsertDottedCircle(substitutionCollection, data, i); break; } } @@ -166,6 +165,11 @@ public override void AssignFeatures(IGlyphShapingCollection collection, int inde collection.EnableShapingFeature(i, LjmoTag); collection.EnableShapingFeature(i, VjmoTag); break; + case LVT: + collection.EnableShapingFeature(i, LjmoTag); + collection.EnableShapingFeature(i, VjmoTag); + collection.EnableShapingFeature(i, TjmoTag); + break; default: break; } @@ -235,7 +239,7 @@ private int DecomposeGlyph(GlyphSubstitutionCollection collection, GlyphShapingD ii[0] = ljmo; ii[1] = vjmo; - collection.Replace(index, ii); + collection.Replace(index, ii, true); return index; } @@ -245,7 +249,7 @@ private int DecomposeGlyph(GlyphSubstitutionCollection collection, GlyphShapingD iii[1] = vjmo; iii[2] = tjmo; - collection.Replace(index, iii); + collection.Replace(index, iii, true); return index; } @@ -335,17 +339,17 @@ private int ComposeGlyph(GlyphSubstitutionCollection collection, GlyphShapingDat // Either the T was non-combining, or the LVT glyph wasn't supported. // Decompose the glyph again and apply OT features. data = collection.GetGlyphShapingData(index - 1); - this.DecomposeGlyph(collection, data, index - 1); + return this.DecomposeGlyph(collection, data, index - 1); } return index; } - private int ReOrderToneMark(GlyphSubstitutionCollection collection, GlyphShapingData data, int index) + private void ReOrderToneMark(GlyphSubstitutionCollection collection, GlyphShapingData data, int index) { if (index == 0) { - return index; + return; } // Move tone mark to the beginning of the previous syllable, unless it is zero width @@ -355,21 +359,46 @@ private int ReOrderToneMark(GlyphSubstitutionCollection collection, GlyphShaping { if (gm.Width == 0) { - return index; + return; } } GlyphShapingData prev = collection.GetGlyphShapingData(index - 1); int len = GetSyllableLength(prev.CodePoint); collection.MoveGlyph(index, index - len); - throw new NotImplementedException(); } - private int InsertDottedCircle(GlyphShapingData data, int i) + private void InsertDottedCircle(GlyphSubstitutionCollection collection, GlyphShapingData data, int index) { - // TODO: Implement. - // We need to find a way to do this that doesn't require increasing the length of the collection. - return i; + bool after = false; + FontMetrics metrics = data.TextRun.Font!.FontMetrics; + + if (metrics.TryGetGlyphId(new(DottedCircle), out ushort id)) + { + foreach (GlyphMetrics gm in metrics.GetGlyphMetrics(data.CodePoint, collection.TextOptions.ColorFontSupport)) + { + if (gm.Width != 0) + { + after = true; + break; + } + } + + // If the tone mark is zero width, insert the dotted circle before, otherwise after + Span glyphs = stackalloc ushort[2]; + if (after) + { + glyphs[0] = data.GlyphIds[0]; + glyphs[1] = id; + } + else + { + glyphs[0] = id; + glyphs[1] = data.GlyphIds[0]; + } + + collection.Replace(index, glyphs, true); + } } private static bool IsCombiningL(CodePoint code) => UnicodeUtility.IsInRangeInclusive((uint)code.Value, LBase, LEnd); From 3729204527a8d7f5f9e4e0f756e84520de1878f8 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 6 May 2022 22:47:13 +1000 Subject: [PATCH 07/10] Allow increasing collection length --- .../GlyphPositioningCollection.cs | 260 +++++++++--------- src/SixLabors.Fonts/GlyphShapingData.cs | 22 +- .../GlyphSubstitutionCollection.cs | 79 ++++-- .../IGlyphShapingCollection.cs | 7 +- .../AdvancedTypographicUtils.cs | 8 +- .../GPos/LookupType1SubTable.cs | 4 +- .../GPos/LookupType2SubTable.cs | 8 +- .../GPos/LookupType3SubTable.cs | 8 +- .../GPos/LookupType4SubTable.cs | 6 +- .../GPos/LookupType5SubTable.cs | 6 +- .../GPos/LookupType6SubTable.cs | 4 +- .../GPos/LookupType7SubTable.cs | 6 +- .../GPos/LookupType8SubTable.cs | 6 +- .../Tables/AdvancedTypographic/GPosTable.cs | 2 +- .../GSub/LookupType1SubTable.cs | 4 +- .../GSub/LookupType2SubTable.cs | 2 +- .../GSub/LookupType3SubTable.cs | 2 +- .../GSub/LookupType4SubTable.cs | 4 +- .../GSub/LookupType5SubTable.cs | 6 +- .../GSub/LookupType6SubTable.cs | 6 +- .../GSub/LookupType8SubTable.cs | 6 +- .../Shapers/HangulShaper.cs | 34 ++- .../Tables/AdvancedTypographic/TagEntry.cs | 2 +- .../Tables/General/KerningTable.cs | 4 +- .../Tables/SkippingGlyphIterator.cs | 2 +- src/SixLabors.Fonts/TextLayout.cs | 14 +- 26 files changed, 274 insertions(+), 238 deletions(-) diff --git a/src/SixLabors.Fonts/GlyphPositioningCollection.cs b/src/SixLabors.Fonts/GlyphPositioningCollection.cs index 05eab8d0..aa45c62b 100644 --- a/src/SixLabors.Fonts/GlyphPositioningCollection.cs +++ b/src/SixLabors.Fonts/GlyphPositioningCollection.cs @@ -1,9 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using SixLabors.Fonts.Tables.AdvancedTypographic; using SixLabors.Fonts.Unicode; @@ -15,20 +15,9 @@ namespace SixLabors.Fonts internal sealed class GlyphPositioningCollection : IGlyphShapingCollection { /// - /// Contains a map between the index of a map within the collection, it's codepoint - /// and glyph ids. + /// Contains a map the index of a map within the collection, non-sequential codepoint offsets, and their glyph ids, point size, and mtrics. /// - private readonly List glyphs = new(); - - /// - /// Contains a map between the index of a map within the collection and its offset. - /// - private readonly List offsets = new(); - - /// - /// Contains a map between non-sequential codepoint offsets and their glyphs. - /// - private readonly Dictionary map = new(); + private readonly List glyphs = new(); /// /// Initializes a new instance of the class. @@ -41,7 +30,7 @@ public GlyphPositioningCollection(TextOptions textOptions) } /// - public int Count => this.offsets.Count; + public int Count => this.glyphs.Count; /// public bool IsVerticalLayoutMode { get; } @@ -50,23 +39,31 @@ public GlyphPositioningCollection(TextOptions textOptions) public TextOptions TextOptions { get; } /// - public ReadOnlySpan this[int index] => this.glyphs[index].GlyphIds; + public ushort this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.glyphs[index].Data.GlyphId; + } /// - public GlyphShapingData GetGlyphShapingData(int index) => this.glyphs[index]; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public GlyphShapingData GetGlyphShapingData(int index) => this.glyphs[index].Data; /// public void AddShapingFeature(int index, TagEntry feature) - => this.glyphs[index].Features.Add(feature); + => this.glyphs[index].Data.Features.Add(feature); /// public void EnableShapingFeature(int index, Tag feature) { - foreach (TagEntry tagEntry in this.glyphs[index].Features) + List features = this.glyphs[index].Data.Features; + for (int i = 0; i < features.Count; i++) { + TagEntry tagEntry = features[i]; if (tagEntry.Tag == feature) { tagEntry.Enabled = true; + features[i] = tagEntry; break; } } @@ -75,11 +72,14 @@ public void EnableShapingFeature(int index, Tag feature) /// public void DisableShapingFeature(int index, Tag feature) { - foreach (TagEntry tagEntry in this.glyphs[index].Features) + List features = this.glyphs[index].Data.Features; + for (int i = 0; i < features.Count; i++) { + TagEntry tagEntry = features[i]; if (tagEntry.Tag == feature) { tagEntry.Enabled = false; + features[i] = tagEntry; break; } } @@ -96,18 +96,27 @@ public void DisableShapingFeature(int index, Tag feature) /// This parameter is passed uninitialized. /// /// The metrics. - public bool TryGetGlyphMetricsAtOffset(int offset, out float pointSize, [NotNullWhen(true)] out GlyphMetrics[]? metrics) + public bool TryGetGlyphMetricsAtOffset(int offset, out float pointSize, [NotNullWhen(true)] out IReadOnlyList? metrics) { - if (this.map.TryGetValue(offset, out PointSizeMetricsPair? entry)) + List match = new(); + pointSize = 0; + for (int i = 0; i < this.glyphs.Count; i++) { - pointSize = entry.PointSize; - metrics = entry.Metrics; - return true; + if (this.glyphs[i].Offset == offset) + { + GlyphPositioningData glyph = this.glyphs[i]; + pointSize = glyph.PointSize; + match.AddRange(glyph.Metrics); + } + else if (match.Count > 0) + { + // Offsets, though non-sequential, are sorted, so we can stop searching. + break; + } } - pointSize = 0; - metrics = null; - return false; + metrics = match; + return match.Count > 0; } /// @@ -122,85 +131,77 @@ public bool TryUpdate(Font font, GlyphSubstitutionCollection collection) FontMetrics fontMetrics = font.FontMetrics; ColorFontSupport colorFontSupport = this.TextOptions.ColorFontSupport; bool hasFallBacks = false; - List orphans = new(); - for (int i = 0; i < this.offsets.Count; i++) + for (int i = 0; i < this.glyphs.Count; i++) { - int offset = this.offsets[i]; - if (!collection.TryGetGlyphShapingDataAtOffset(offset, out GlyphShapingData? data)) - { - // If a font had glyphs but a follow up font also has them and can substitute. e.g ligatures - // then we end up with orphaned fallbacks. We need to remove them. - orphans.Add(i); - continue; - } - - PointSizeMetricsPair pair = this.map[offset]; - if (pair.Metrics[0].GlyphType != GlyphType.Fallback) + GlyphPositioningData current = this.glyphs[i]; + if (current.Metrics[0].GlyphType != GlyphType.Fallback) { // We've already got the correct glyph. continue; } - CodePoint codePoint = data.CodePoint; - ushort[] glyphIds = data.GlyphIds; - var m = new List(glyphIds.Length); - - ushort shiftXY = 0; - bool doShift = data.OffsetGlyphs; - foreach (ushort id in glyphIds) + int offset = current.Offset; + float pointSize = current.PointSize; + if (collection.TryGetGlyphShapingDataAtOffset(offset, out IReadOnlyList? data)) { - // Perform a semi-deep clone (FontMetrics is not cloned) so we can continue to - // cache the original in the font metrics and only update our collection. - foreach (GlyphMetrics gm in fontMetrics.GetGlyphMetrics(codePoint, id, colorFontSupport)) + ushort shiftXY = 0; + int replacementCount = 0; + for (int j = 0; j < data.Count; j++) { - if (gm.GlyphType == GlyphType.Fallback && !CodePoint.IsControl(codePoint)) - { - // If the glyphs are fallbacks we don't want them as - // we've already captured them on the first run. - hasFallBacks = true; - break; - } - - // Clone and offset the glyph based on it's position in the glyphId array. - // We slip the text run in here while we clone so we have - // it available to the renderer. - var clone = GlyphMetrics.CloneForRendering(gm, data.TextRun, codePoint); - if (doShift) + GlyphShapingData shape = data[j]; + ushort id = shape.GlyphId; + CodePoint codePoint = shape.CodePoint; + bool doShift = shape.OffsetGlyph; + + // Perform a semi-deep clone (FontMetrics is not cloned) so we can continue to + // cache the original in the font metrics and only update our collection. + var metrics = new List(data.Count); + foreach (GlyphMetrics gm in fontMetrics.GetGlyphMetrics(codePoint, id, colorFontSupport)) { - if (!this.IsVerticalLayoutMode) + if (gm.GlyphType == GlyphType.Fallback && !CodePoint.IsControl(codePoint)) { - clone.ApplyOffset((short)shiftXY, 0); - shiftXY += clone.AdvanceWidth; + // If the glyphs are fallbacks we don't want them as + // we've already captured them on the first run. + hasFallBacks = true; + break; } - else + + // Clone and offset the glyph based on it's position in the offset group. + // We slip the text run in here while we clone so we have it available to the renderer. + var clone = GlyphMetrics.CloneForRendering(gm, shape.TextRun, codePoint); + if (doShift) { - clone.ApplyOffset(0, (short)shiftXY); - shiftXY += clone.AdvanceHeight; + if (!this.IsVerticalLayoutMode) + { + clone.ApplyOffset((short)shiftXY, 0); + shiftXY += clone.AdvanceWidth; + } + else + { + clone.ApplyOffset(0, (short)shiftXY); + shiftXY += clone.AdvanceHeight; + } } + + metrics.Add(clone); } - m.Add(clone); - } - } + if (metrics.Count > 0) + { + if (j == 0) + { + // There should only be a single fallback glyph at this position from the previous collection. + this.glyphs.RemoveAt(i); + } - if (m.Count > 0) - { - GlyphMetrics[] gm = m.ToArray(); - this.map[offset] = new(pair.PointSize, gm); - this.glyphs[i] = new(data, true) { Bounds = new(0, 0, gm[0].AdvanceWidth, gm[0].AdvanceHeight) }; - this.offsets[i] = offset; + // Track the number of inserted glyphs at the offset so we can correctly increment our position. + this.glyphs.Insert(i += replacementCount, new(offset, new(shape, true) { Bounds = new(0, 0, metrics[0].AdvanceWidth, metrics[0].AdvanceHeight) }, pointSize, metrics.ToArray())); + replacementCount++; + } + } } } - // Remove any orphans. - for (int i = orphans.Count - 1; i >= 0; i--) - { - int idx = orphans[i]; - this.map.Remove(this.offsets[idx]); - this.offsets.RemoveAt(idx); - this.glyphs.RemoveAt(idx); - } - return !hasFallBacks; } @@ -220,58 +221,53 @@ public bool TryAdd(Font font, GlyphSubstitutionCollection collection) { GlyphShapingData data = collection.GetGlyphShapingData(i, out int offset); CodePoint codePoint = data.CodePoint; - ushort[] glyphIds = data.GlyphIds; - var m = new List(glyphIds.Length); + ushort id = data.GlyphId; + List metrics = new(); ushort shiftXY = 0; - bool doShift = data.OffsetGlyphs; - foreach (ushort id in glyphIds) + bool doShift = data.OffsetGlyph; + + // Perform a semi-deep clone (FontMetrics is not cloned) so we can continue to + // cache the original in the font metrics and only update our collection. + foreach (GlyphMetrics gm in fontMetrics.GetGlyphMetrics(codePoint, id, colorFontSupport)) { - // Perform a semi-deep clone (FontMetrics is not cloned) so we can continue to - // cache the original in the font metrics and only update our collection. - foreach (GlyphMetrics gm in fontMetrics.GetGlyphMetrics(codePoint, id, colorFontSupport)) + if (gm.GlyphType == GlyphType.Fallback && !CodePoint.IsControl(codePoint)) { - if (gm.GlyphType == GlyphType.Fallback && !CodePoint.IsControl(codePoint)) + hasFallBacks = true; + } + + // Clone and offset the glyph based on it's position in the glyphId array. + // We slip the text run in here while we clone so we have + // it available to the renderer. + var clone = GlyphMetrics.CloneForRendering(gm, data.TextRun, codePoint); + if (doShift) + { + if (!this.IsVerticalLayoutMode) { - hasFallBacks = true; + clone.ApplyOffset((short)shiftXY, 0); + shiftXY += clone.AdvanceWidth; } - - // Clone and offset the glyph based on it's position in the glyphId array. - // We slip the text run in here while we clone so we have - // it available to the renderer. - var clone = GlyphMetrics.CloneForRendering(gm, data.TextRun, codePoint); - if (doShift) + else { - if (!this.IsVerticalLayoutMode) - { - clone.ApplyOffset((short)shiftXY, 0); - shiftXY += clone.AdvanceWidth; - } - else - { - clone.ApplyOffset(0, (short)shiftXY); - shiftXY += clone.AdvanceHeight; - } + clone.ApplyOffset(0, (short)shiftXY); + shiftXY += clone.AdvanceHeight; } - - m.Add(clone); } + + metrics.Add(clone); } - if (m.Count > 0) + if (metrics.Count > 0) { - GlyphMetrics[] gm = m.ToArray(); - this.map[offset] = new(font.Size, gm); + GlyphMetrics[] gm = metrics.ToArray(); if (this.IsVerticalLayoutMode) { - this.glyphs.Add(new(data, true) { Bounds = new(0, 0, 0, gm[0].AdvanceHeight) }); + this.glyphs.Add(new(offset, new(data, true) { Bounds = new(0, 0, 0, gm[0].AdvanceHeight) }, font.Size, gm)); } else { - this.glyphs.Add(new(data, true) { Bounds = new(0, 0, gm[0].AdvanceWidth, 0) }); + this.glyphs.Add(new(offset, new(data, true) { Bounds = new(0, 0, gm[0].AdvanceWidth, 0) }, font.Size, gm)); } - - this.offsets.Add(offset); } } @@ -293,8 +289,8 @@ public void UpdatePosition(FontMetrics fontMetrics, ushort index) return; } - ushort glyphId = data.GlyphIds[0]; - foreach (GlyphMetrics m in this.map[this.offsets[index]].Metrics) + ushort glyphId = data.GlyphId; + foreach (GlyphMetrics m in this.glyphs[index].Metrics) { if (m.GlyphId == glyphId && fontMetrics == m.FontMetrics) { @@ -323,7 +319,7 @@ public void UpdatePosition(FontMetrics fontMetrics, ushort index) /// The delta y-advance. public void Advance(FontMetrics fontMetrics, ushort index, ushort glyphId, short dx, short dy) { - foreach (GlyphMetrics m in this.map[this.offsets[index]].Metrics) + foreach (GlyphMetrics m in this.glyphs[index].Metrics) { if (m.GlyphId == glyphId && fontMetrics == m.FontMetrics) { @@ -336,19 +332,25 @@ public void Advance(FontMetrics fontMetrics, ushort index, ushort glyphId, short /// Returns a value indicating whether the element at the given index should be processed. /// /// The font face with metrics. - /// The zero-based index of the elements to offset. + /// The zero-based index of the elements to position. /// if the element should be processed; otherwise, . public bool ShouldProcess(FontMetrics fontMetrics, ushort index) - => this.map[this.offsets[index]].Metrics[0].FontMetrics == fontMetrics; + => this.glyphs[index].Metrics[0].FontMetrics == fontMetrics; - private class PointSizeMetricsPair + private class GlyphPositioningData { - public PointSizeMetricsPair(float pointSize, GlyphMetrics[] metrics) + public GlyphPositioningData(int offset, GlyphShapingData data, float pointSize, GlyphMetrics[] metrics) { + this.Offset = offset; + this.Data = data; this.PointSize = pointSize; this.Metrics = metrics; } + public int Offset { get; set; } + + public GlyphShapingData Data { get; set; } + public float PointSize { get; set; } public GlyphMetrics[] Metrics { get; set; } diff --git a/src/SixLabors.Fonts/GlyphShapingData.cs b/src/SixLabors.Fonts/GlyphShapingData.cs index c9758ef4..f67d2ede 100644 --- a/src/SixLabors.Fonts/GlyphShapingData.cs +++ b/src/SixLabors.Fonts/GlyphShapingData.cs @@ -27,25 +27,30 @@ internal class GlyphShapingData /// Whether to clear features. public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) { + this.GlyphId = data.GlyphId; this.CodePoint = data.CodePoint; this.CodePointCount = data.CodePointCount; this.Direction = data.Direction; this.TextRun = data.TextRun; - this.GlyphIds = data.GlyphIds; this.LigatureId = data.LigatureId; this.LigatureComponent = data.LigatureComponent; this.MarkAttachment = data.MarkAttachment; this.CursiveAttachment = data.CursiveAttachment; - this.OffsetGlyphs = data.OffsetGlyphs; + this.OffsetGlyph = data.OffsetGlyph; if (!clearFeatures) { - this.Features = data.Features; + this.Features = new(data.Features); } this.Bounds = data.Bounds; } + /// + /// Gets or sets the glyph id. + /// + public ushort GlyphId { get; set; } + /// /// Gets or sets the leading codepoint. /// @@ -66,11 +71,6 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) /// public TextRun TextRun { get; set; } - /// - /// Gets or sets the collection of glyph ids. - /// - public ushort[] GlyphIds { get; set; } = Array.Empty(); - /// /// Gets or sets the id of any ligature this glyph is a member of. /// @@ -102,12 +102,12 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) public GlyphShapingBounds Bounds { get; set; } = new(0, 0, 0, 0); /// - /// Gets or sets a value indicating whether individual glyph in the collection should be offset from the preceding glyph. + /// Gets or sets a value indicating whether this glyph should be positioned at the advance of the preceding glyph at the same codepoint offset. /// - public bool OffsetGlyphs { get; set; } + public bool OffsetGlyph { get; set; } private string DebuggerDisplay => FormattableString - .Invariant($"{this.CodePoint.ToDebuggerDisplay()} : {CodePoint.GetScriptClass(this.CodePoint)} : {this.Direction} : {this.TextRun.TextAttributes} : {this.LigatureId} : {this.LigatureComponent} : [{string.Join(",", this.GlyphIds)}] : {this.OffsetGlyphs}"); + .Invariant($" {this.GlyphId} : {this.CodePoint.ToDebuggerDisplay()} : {CodePoint.GetScriptClass(this.CodePoint)} : {this.Direction} : {this.TextRun.TextAttributes} : {this.LigatureId} : {this.LigatureComponent} : {this.OffsetGlyph}"); } } diff --git a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs index 36ffd1b2..0f8ec6be 100644 --- a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs +++ b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using SixLabors.Fonts.Tables.AdvancedTypographic; using SixLabors.Fonts.Unicode; @@ -47,11 +48,15 @@ public GlyphSubstitutionCollection(TextOptions textOptions) public int LigatureId { get; set; } = 1; /// - public ReadOnlySpan this[int index] => this.glyphs[index].Data.GlyphIds; + public ushort this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.glyphs[index].Data.GlyphId; + } /// - public GlyphShapingData GetGlyphShapingData(int index) - => this.glyphs[index].Data; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public GlyphShapingData GetGlyphShapingData(int index) => this.glyphs[index].Data; /// /// Gets the shaping data at the specified position. @@ -73,11 +78,14 @@ public void AddShapingFeature(int index, TagEntry feature) /// public void EnableShapingFeature(int index, Tag feature) { - foreach (TagEntry tagEntry in this.glyphs[index].Data.Features) + List features = this.glyphs[index].Data.Features; + for (int i = 0; i < features.Count; i++) { + TagEntry tagEntry = features[i]; if (tagEntry.Tag == feature) { tagEntry.Enabled = true; + features[i] = tagEntry; break; } } @@ -86,11 +94,14 @@ public void EnableShapingFeature(int index, Tag feature) /// public void DisableShapingFeature(int index, Tag feature) { - foreach (TagEntry tagEntry in this.glyphs[index].Data.Features) + List features = this.glyphs[index].Data.Features; + for (int i = 0; i < features.Count; i++) { + TagEntry tagEntry = features[i]; if (tagEntry.Tag == feature) { tagEntry.Enabled = false; + features[i] = tagEntry; break; } } @@ -109,7 +120,7 @@ public void AddGlyph(ushort glyphId, CodePoint codePoint, TextDirection directio { CodePoint = codePoint, Direction = direction, - GlyphIds = new[] { glyphId }, + GlyphId = glyphId, })); /// @@ -164,17 +175,24 @@ public void Clear() /// if the contains glyph ids /// for the specified offset; otherwise, . /// - public bool TryGetGlyphShapingDataAtOffset(int offset, [NotNullWhen(true)] out GlyphShapingData? data) + public bool TryGetGlyphShapingDataAtOffset(int offset, [NotNullWhen(true)] out IReadOnlyList? data) { - OffsetGlyphDataPair? pair = this.glyphs.Find(x => x.Offset == offset); - if (pair is null) + List match = new(); + for (int i = 0; i < this.glyphs.Count; i++) { - data = null; - return false; + if (this.glyphs[i].Offset == offset) + { + match.Add(this.glyphs[i].Data); + } + else if (match.Count > 0) + { + // Offsets, though non-sequential, are sorted, so we can stop searching. + break; + } } - data = pair.Data; - return true; + data = match; + return match.Count > 0; } /// @@ -183,7 +201,7 @@ public bool TryGetGlyphShapingDataAtOffset(int offset, [NotNullWhen(true)] out G /// The zero-based index of the element to replace. /// The replacement glyph id. public void Replace(int index, ushort glyphId) - => this.glyphs[index].Data.GlyphIds = new[] { glyphId }; + => this.glyphs[index].Data.GlyphId = glyphId; /// /// Performs a 1:1 replacement of a glyph id at the given position while removing a series of glyph ids at the given positions within the sequence. @@ -206,7 +224,7 @@ public void Replace(int index, ReadOnlySpan removalIndices, ushort glyphId, // Assign our new id at the index. GlyphShapingData current = this.glyphs[index].Data; current.CodePointCount += codePointCount; - current.GlyphIds = new[] { glyphId }; + current.GlyphId = glyphId; current.LigatureId = ligatureId; current.LigatureComponent = -1; current.MarkAttachment = -1; @@ -223,7 +241,7 @@ public void Replace(int index, int count, ushort glyphId) { // Remove the glyphs at each index. int codePointCount = 0; - for (int i = count - 1; i >= 0; i--) + for (int i = count; i > 0; i--) { int match = index + i; codePointCount += this.glyphs[match].Data.CodePointCount; @@ -233,7 +251,7 @@ public void Replace(int index, int count, ushort glyphId) // Assign our new id at the index. GlyphShapingData current = this.glyphs[index].Data; current.CodePointCount += codePointCount; - current.GlyphIds = new[] { glyphId }; + current.GlyphId = glyphId; current.LigatureId = 0; current.LigatureComponent = -1; current.MarkAttachment = -1; @@ -245,19 +263,34 @@ public void Replace(int index, int count, ushort glyphId) /// /// The zero-based index of the element to replace. /// The collection of replacement glyph ids. - /// Whether individual glyph in the collection should be offset from the preceding glyph. + /// Whether the additional glyphs should be positioned at the advance of the preceding glyph at the same codepoint offset. public void Replace(int index, ReadOnlySpan glyphIds, bool offset) { if (glyphIds.Length > 0) { - // TODO: Features most likely need to be bound to each glyph index. - // TODO: FontKit stores the ids in sequence with increasing ligature component values. - GlyphShapingData current = this.glyphs[index].Data; - current.GlyphIds = glyphIds.ToArray(); + OffsetGlyphDataPair pair = this.glyphs[index]; + GlyphShapingData current = pair.Data; + current.GlyphId = glyphIds[0]; current.LigatureComponent = 0; current.MarkAttachment = -1; current.CursiveAttachment = -1; - current.OffsetGlyphs = offset; + current.OffsetGlyph = offset; + + // Add additional glyphs to the end of the sequence. + if (glyphIds.Length > 1) + { + glyphIds = glyphIds.Slice(1); + for (int i = 0; i < glyphIds.Length; i++) + { + GlyphShapingData data = new(current, false); + data.GlyphId = glyphIds[i]; + data.LigatureComponent = i + 1; + + // TODO: We may have to shift the offset of the following glyphs. + int o = offset ? pair.Offset + i + 1 : pair.Offset; + this.glyphs.Insert(++index, new(o, data)); + } + } } else { diff --git a/src/SixLabors.Fonts/IGlyphShapingCollection.cs b/src/SixLabors.Fonts/IGlyphShapingCollection.cs index 0ce54627..96bdd7a1 100644 --- a/src/SixLabors.Fonts/IGlyphShapingCollection.cs +++ b/src/SixLabors.Fonts/IGlyphShapingCollection.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -using System; using SixLabors.Fonts.Tables.AdvancedTypographic; namespace SixLabors.Fonts @@ -27,11 +26,11 @@ internal interface IGlyphShapingCollection TextOptions TextOptions { get; } /// - /// Gets the glyph ids at the specified index. + /// Gets the glyph id at the specified index. /// /// The zero-based index of the elements to get. - /// The . - ReadOnlySpan this[int index] { get; } + /// The . + ushort this[int index] { get; } /// /// Gets the shaping data at the specified position. diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs index ffd2dfe8..f356d990 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs @@ -86,7 +86,7 @@ public static bool MatchInputSequence(SkippingGlyphIterator iterator, Tag featur return false; } - return component == data.GlyphIds[0]; + return component == data.GlyphId; }, matches); @@ -108,7 +108,7 @@ public static bool MatchSequence(SkippingGlyphIterator iterator, int increment, increment, sequence, iterator, - (component, data) => component == data.GlyphIds[0], + (component, data) => component == data.GlyphId, default); public static bool MatchClassSequence( @@ -120,7 +120,7 @@ public static bool MatchClassSequence( increment, sequence, iterator, - (component, data) => component == classDefinitionTable.ClassIndexOf(data.GlyphIds[0]), + (component, data) => component == classDefinitionTable.ClassIndexOf(data.GlyphId), default); public static bool MatchCoverageSequence( @@ -131,7 +131,7 @@ public static bool MatchCoverageSequence( increment, coverageTable, iterator, - (component, data) => component.CoverageIndexOf(data.GlyphIds[0]) >= 0, + (component, data) => component.CoverageIndexOf(data.GlyphId) >= 0, default); public static bool ApplyChainedSequenceRule(SkippingGlyphIterator iterator, ChainedSequenceRuleTable rule) diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType1SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType1SubTable.cs index 32d05f0a..0075b7e1 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType1SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType1SubTable.cs @@ -74,7 +74,7 @@ public override bool TryUpdatePosition( ushort index, int count) { - ushort glyphId = collection[index][0]; + ushort glyphId = collection[index]; if (glyphId == 0) { return false; @@ -145,7 +145,7 @@ public override bool TryUpdatePosition( ushort index, int count) { - ushort glyphId = collection[index][0]; + ushort glyphId = collection[index]; if (glyphId == 0) { return false; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType2SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType2SubTable.cs index 0e9f9a55..2664e873 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType2SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType2SubTable.cs @@ -101,7 +101,7 @@ public override bool TryUpdatePosition( return false; } - ushort glyphId = collection[index][0]; + ushort glyphId = collection[index]; if (glyphId == 0) { return false; @@ -111,7 +111,7 @@ public override bool TryUpdatePosition( if (coverage > -1) { PairSetTable pairSet = this.pairSets[coverage]; - ushort glyphId2 = collection[index + 1][0]; + ushort glyphId2 = collection[index + 1]; if (glyphId2 == 0) { return false; @@ -266,7 +266,7 @@ public override bool TryUpdatePosition( return false; } - ushort glyphId = collection[index][0]; + ushort glyphId = collection[index]; if (glyphId == 0) { return false; @@ -276,7 +276,7 @@ public override bool TryUpdatePosition( if (coverage > -1) { int classDef1 = this.classDefinitionTable1.ClassIndexOf(glyphId); - ushort glyphId2 = collection[index + 1][0]; + ushort glyphId2 = collection[index + 1]; if (glyphId2 == 0) { return false; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType3SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType3SubTable.cs index acb0a214..acd4fdf8 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType3SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType3SubTable.cs @@ -88,14 +88,14 @@ public override bool TryUpdatePosition( // Implements Cursive Attachment Positioning Subtable: // https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#lookup-type-3-cursive-attachment-positioning-subtable - ushort glyphId = collection[index][0]; + ushort glyphId = collection[index]; if (glyphId == 0) { return false; } ushort nextIndex = (ushort)(index + 1); - ushort nextGlyphId = collection[nextIndex][0]; + ushort nextGlyphId = collection[nextIndex]; if (nextGlyphId == 0) { return false; @@ -180,9 +180,7 @@ public override bool TryUpdatePosition( int yOffset = entryXY.YCoordinate - exitXY.YCoordinate; if (this.LookupFlags.HasFlag(LookupFlags.RightToLeft)) { - int temp = child; - child = parent; - parent = temp; + (parent, child) = (child, parent); xOffset = -xOffset; yOffset = -yOffset; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType4SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType4SubTable.cs index 62e61585..9e1bb997 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType4SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType4SubTable.cs @@ -91,7 +91,7 @@ public override bool TryUpdatePosition( { // Mark-to-Base Attachment Positioning Subtable. // Implements: https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#lookup-type-4-mark-to-base-attachment-positioning-subtable - ushort glyphId = collection[index][0]; + ushort glyphId = collection[index]; if (glyphId == 0) { return false; @@ -108,7 +108,7 @@ public override bool TryUpdatePosition( while (--baseGlyphIndex >= 0) { GlyphShapingData data = collection.GetGlyphShapingData(baseGlyphIndex); - if (!AdvancedTypographicUtils.IsMarkGlyph(fontMetrics, data.GlyphIds[0], data) && !(data.LigatureComponent > 0)) + if (!AdvancedTypographicUtils.IsMarkGlyph(fontMetrics, data.GlyphId, data) && !(data.LigatureComponent > 0)) { break; } @@ -119,7 +119,7 @@ public override bool TryUpdatePosition( return false; } - ushort baseGlyphId = collection[baseGlyphIndex][0]; + ushort baseGlyphId = collection[baseGlyphIndex]; int baseIndex = this.baseCoverage.CoverageIndexOf(baseGlyphId); if (baseIndex < 0) { diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType5SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType5SubTable.cs index 0c040a2d..1d20ac3c 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType5SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType5SubTable.cs @@ -95,7 +95,7 @@ public override bool TryUpdatePosition( { // Mark-to-Ligature Attachment Positioning. // Implements: https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#lookup-type-5-mark-to-ligature-attachment-positioning-subtable - ushort glyphId = collection[index][0]; + ushort glyphId = collection[index]; if (glyphId == 0) { return false; @@ -112,7 +112,7 @@ public override bool TryUpdatePosition( while (--baseGlyphIndex >= 0) { GlyphShapingData data = collection.GetGlyphShapingData(baseGlyphIndex); - if (!AdvancedTypographicUtils.IsMarkGlyph(fontMetrics, data.GlyphIds[0], data)) + if (!AdvancedTypographicUtils.IsMarkGlyph(fontMetrics, data.GlyphId, data)) { break; } @@ -123,7 +123,7 @@ public override bool TryUpdatePosition( return false; } - ushort baseGlyphId = collection[baseGlyphIndex][0]; + ushort baseGlyphId = collection[baseGlyphIndex]; int ligatureIndex = this.ligatureCoverage.CoverageIndexOf(baseGlyphId); if (ligatureIndex < 0) { diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType6SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType6SubTable.cs index 9ca304ba..61c2a4a6 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType6SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType6SubTable.cs @@ -93,7 +93,7 @@ public override bool TryUpdatePosition( { // Mark to mark positioning. // Implements: https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#lookup-type-6-mark-to-mark-attachment-positioning-subtable - ushort glyphId = collection[index][0]; + ushort glyphId = collection[index]; if (glyphId == 0) { return false; @@ -112,7 +112,7 @@ public override bool TryUpdatePosition( } int prevIdx = index - 1; - ushort prevGlyphId = collection[prevIdx][0]; + ushort prevGlyphId = collection[prevIdx]; GlyphShapingData prevGlyph = collection.GetGlyphShapingData(prevIdx); if (!AdvancedTypographicUtils.IsMarkGlyph(fontMetrics, prevGlyphId, prevGlyph)) { diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType7SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType7SubTable.cs index 9b9612fe..c90495b3 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType7SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType7SubTable.cs @@ -55,7 +55,7 @@ public override bool TryUpdatePosition( ushort index, int count) { - ushort glyphId = collection[index][0]; + ushort glyphId = collection[index]; if (glyphId == 0) { return false; @@ -134,7 +134,7 @@ public override bool TryUpdatePosition( ushort index, int count) { - ushort glyphId = collection[index][0]; + ushort glyphId = collection[index]; if (glyphId == 0) { return false; @@ -210,7 +210,7 @@ public override bool TryUpdatePosition( ushort index, int count) { - ushort glyphId = collection[index][0]; + ushort glyphId = collection[index]; if (glyphId == 0) { return false; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType8SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType8SubTable.cs index 5e85fd34..ad9e57dc 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType8SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType8SubTable.cs @@ -59,7 +59,7 @@ public override bool TryUpdatePosition( { // Implements Chained Contexts Substitution, Format 1: // https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#61-chained-contexts-substitution-format-1-simple-glyph-contexts - ushort glyphId = collection[index][0]; + ushort glyphId = collection[index]; if (glyphId == 0) { return false; @@ -160,7 +160,7 @@ public override bool TryUpdatePosition( { // Implements Chained Contexts Substitution for Format 2: // https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#62-chained-contexts-substitution-format-2-class-based-glyph-contexts - ushort glyphId = collection[index][0]; + ushort glyphId = collection[index]; if (glyphId == 0) { return false; @@ -252,7 +252,7 @@ public override bool TryUpdatePosition( ushort index, int count) { - ushort glyphId = collection[index][0]; + ushort glyphId = collection[index]; if (glyphId == 0) { return false; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs index d3537c99..9db7de54 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs @@ -320,7 +320,7 @@ private static void ZeroMarkAdvances(FontMetrics fontMetrics, GlyphPositioningCo { int currentIndex = i + index; GlyphShapingData data = collection.GetGlyphShapingData(currentIndex); - if (AdvancedTypographicUtils.IsMarkGlyph(fontMetrics, data.GlyphIds[0], data)) + if (AdvancedTypographicUtils.IsMarkGlyph(fontMetrics, data.GlyphId, data)) { data.Bounds.Width = 0; data.Bounds.Height = 0; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs index 30006653..bf688f84 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs @@ -68,7 +68,7 @@ public override bool TrySubstitution( ushort index, int count) { - ushort glyphId = collection[index][0]; + ushort glyphId = collection[index]; if (glyphId == 0) { return false; @@ -127,7 +127,7 @@ public override bool TrySubstitution( ushort index, int count) { - ushort glyphId = collection[index][0]; + ushort glyphId = collection[index]; if (glyphId == 0) { return false; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs index 9d0173e9..f171edad 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs @@ -91,7 +91,7 @@ public override bool TrySubstitution( ushort index, int count) { - ushort glyphId = collection[index][0]; + ushort glyphId = collection[index]; if (glyphId == 0) { return false; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs index 0974bb23..76fbef37 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs @@ -90,7 +90,7 @@ public override bool TrySubstitution( ushort index, int count) { - ushort glyphId = collection[index][0]; + ushort glyphId = collection[index]; if (glyphId == 0) { return false; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs index e88ce93c..4b61cea2 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs @@ -120,7 +120,7 @@ public override bool TrySubstitution( ushort index, int count) { - ushort glyphId = collection[index][0]; + ushort glyphId = collection[index]; if (glyphId == 0) { return false; @@ -183,7 +183,7 @@ public override bool TrySubstitution( for (int j = 0; j < matches.Length && isMarkLigature; j++) { GlyphShapingData match = collection.GetGlyphShapingData(matches[j]); - if (!AdvancedTypographicUtils.IsMarkGlyph(fontMetrics, match.GlyphIds[0], match)) + if (!AdvancedTypographicUtils.IsMarkGlyph(fontMetrics, match.GlyphId, match)) { isBaseLigature = false; isMarkLigature = false; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs index c5c5eebe..f06f5036 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs @@ -54,7 +54,7 @@ public override bool TrySubstitution( ushort index, int count) { - ushort glyphId = collection[index][0]; + ushort glyphId = collection[index]; if (glyphId == 0) { return false; @@ -133,7 +133,7 @@ public override bool TrySubstitution( ushort index, int count) { - ushort glyphId = collection[index][0]; + ushort glyphId = collection[index]; if (glyphId == 0) { return false; @@ -211,7 +211,7 @@ public override bool TrySubstitution( ushort index, int count) { - ushort glyphId = collection[index][0]; + ushort glyphId = collection[index]; if (glyphId == 0) { return false; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs index b507840b..9aab6bc4 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs @@ -55,7 +55,7 @@ public override bool TrySubstitution( { // Implements Chained Contexts Substitution, Format 1: // https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#61-chained-contexts-substitution-format-1-simple-glyph-contexts - ushort glyphId = collection[index][0]; + ushort glyphId = collection[index]; if (glyphId == 0) { return false; @@ -147,7 +147,7 @@ public override bool TrySubstitution( { // Implements Chained Contexts Substitution for Format 2: // https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#62-chained-contexts-substitution-format-2-class-based-glyph-contexts - ushort glyphId = collection[index][0]; + ushort glyphId = collection[index]; if (glyphId == 0) { return false; @@ -234,7 +234,7 @@ public override bool TrySubstitution( ushort index, int count) { - ushort glyphId = collection[index][0]; + ushort glyphId = collection[index]; if (glyphId == 0) { return false; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType8SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType8SubTable.cs index 11b33d9f..aea52ebc 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType8SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType8SubTable.cs @@ -107,7 +107,7 @@ public override bool TrySubstitution( int count) { // https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#81-reverse-chaining-contextual-single-substitution-format-1-coverage-based-glyph-contexts - ushort glyphId = collection[index][0]; + ushort glyphId = collection[index]; if (glyphId == 0) { return false; @@ -121,7 +121,7 @@ public override bool TrySubstitution( for (int i = 0; i < this.backtrackCoverageTables.Length; ++i) { - ushort id = collection[index - 1 - i][0]; + ushort id = collection[index - 1 - i]; if (id == 0 || this.backtrackCoverageTables[i].CoverageIndexOf(id) < 0) { return false; @@ -130,7 +130,7 @@ public override bool TrySubstitution( for (int i = 0; i < this.lookaheadCoverageTables.Length; ++i) { - ushort id = collection[index + i][0]; + ushort id = collection[index + i]; if (id == 0 || this.lookaheadCoverageTables[i].CoverageIndexOf(id) < 0) { return false; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs index eb19fe38..1566d18b 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs @@ -20,6 +20,8 @@ internal sealed class HangulShaper : DefaultShaper private static readonly Tag TjmoTag = Tag.Parse("tjmo"); private const int HangulBase = 0xac00; + private const int HangulEnd = 0xd7a4; + private const int HangulCount = HangulEnd - HangulBase + 1; private const int LBase = 0x1100; // lead private const int VBase = 0x1161; // vowel private const int TBase = 0x11a7; // trail @@ -109,7 +111,7 @@ public override void AssignFeatures(IGlyphShapingCollection collection, int inde case Decompose: // Decompose the composed syllable if it is not supported by the font. - if (data.GlyphIds[0] == 0) + if (data.GlyphId == 0) { i = this.DecomposeGlyph(substitutionCollection, data, i); } @@ -224,15 +226,8 @@ private int DecomposeGlyph(GlyphSubstitutionCollection collection, GlyphShapingD return index; } - // TODO: Check the insertion here. - // We likely need to add the features separately to each of the newly - // embedded glyph ids. - // // Replace the current glyph with decomposed L, V, and T glyphs, // and apply the proper OpenType features to each component. - collection.EnableShapingFeature(index, LjmoTag); - collection.EnableShapingFeature(index, VjmoTag); - if (t <= TBase) { Span ii = stackalloc ushort[2]; @@ -240,17 +235,21 @@ private int DecomposeGlyph(GlyphSubstitutionCollection collection, GlyphShapingD ii[1] = vjmo; collection.Replace(index, ii, true); - return index; + collection.EnableShapingFeature(index, LjmoTag); + collection.EnableShapingFeature(index + 1, VjmoTag); + return index + 1; } - collection.EnableShapingFeature(index, TjmoTag); Span iii = stackalloc ushort[3]; iii[0] = ljmo; iii[1] = vjmo; iii[2] = tjmo; collection.Replace(index, iii, true); - return index; + collection.EnableShapingFeature(index, LjmoTag); + collection.EnableShapingFeature(index + 1, VjmoTag); + collection.EnableShapingFeature(index + 2, TjmoTag); + return index + 2; } private int ComposeGlyph(GlyphSubstitutionCollection collection, GlyphShapingData data, int index, int type) @@ -313,6 +312,7 @@ private int ComposeGlyph(GlyphSubstitutionCollection collection, GlyphShapingDat int del = prevType == V ? 3 : 2; int idx = index - del + 1; collection.Replace(idx, del - 1, id); + collection.GetGlyphShapingData(idx).CodePoint = s; return idx; } } @@ -339,7 +339,8 @@ private int ComposeGlyph(GlyphSubstitutionCollection collection, GlyphShapingDat // Either the T was non-combining, or the LVT glyph wasn't supported. // Decompose the glyph again and apply OT features. data = collection.GetGlyphShapingData(index - 1); - return this.DecomposeGlyph(collection, data, index - 1); + this.DecomposeGlyph(collection, data, index - 1); + return index + 1; } return index; @@ -368,7 +369,7 @@ private void ReOrderToneMark(GlyphSubstitutionCollection collection, GlyphShapin collection.MoveGlyph(index, index - len); } - private void InsertDottedCircle(GlyphSubstitutionCollection collection, GlyphShapingData data, int index) + private int InsertDottedCircle(GlyphSubstitutionCollection collection, GlyphShapingData data, int index) { bool after = false; FontMetrics metrics = data.TextRun.Font!.FontMetrics; @@ -388,17 +389,20 @@ private void InsertDottedCircle(GlyphSubstitutionCollection collection, GlyphSha Span glyphs = stackalloc ushort[2]; if (after) { - glyphs[0] = data.GlyphIds[0]; + glyphs[0] = data.GlyphId; glyphs[1] = id; } else { glyphs[0] = id; - glyphs[1] = data.GlyphIds[0]; + glyphs[1] = data.GlyphId; } collection.Replace(index, glyphs, true); + return index + 1; } + + return index; } private static bool IsCombiningL(CodePoint code) => UnicodeUtility.IsInRangeInclusive((uint)code.Value, LBase, LEnd); diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/TagEntry.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/TagEntry.cs index aafd4dca..f8e4a9ee 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/TagEntry.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/TagEntry.cs @@ -6,7 +6,7 @@ namespace SixLabors.Fonts.Tables.AdvancedTypographic { [DebuggerDisplay("Tag: {Tag}, Enabled: {Enabled}")] - internal class TagEntry + internal struct TagEntry { public TagEntry(Tag tag, bool enabled) { diff --git a/src/SixLabors.Fonts/Tables/General/KerningTable.cs b/src/SixLabors.Fonts/Tables/General/KerningTable.cs index da870fea..ea39f8b9 100644 --- a/src/SixLabors.Fonts/Tables/General/KerningTable.cs +++ b/src/SixLabors.Fonts/Tables/General/KerningTable.cs @@ -59,8 +59,8 @@ public static KerningTable Load(BigEndianBinaryReader reader) public void UpdatePositions(FontMetrics fontMetrics, GlyphPositioningCollection collection, ushort left, ushort right) { - ushort previous = collection[left][0]; - ushort current = collection[right][0]; + ushort previous = collection[left]; + ushort current = collection[right]; if (previous == 0 || current == 0) { return; diff --git a/src/SixLabors.Fonts/Tables/SkippingGlyphIterator.cs b/src/SixLabors.Fonts/Tables/SkippingGlyphIterator.cs index 1bfef134..4747e9d0 100644 --- a/src/SixLabors.Fonts/Tables/SkippingGlyphIterator.cs +++ b/src/SixLabors.Fonts/Tables/SkippingGlyphIterator.cs @@ -73,7 +73,7 @@ private void Move(int direction) private bool ShouldIgnore(int index) { GlyphShapingData data = this.Collection.GetGlyphShapingData(index); - GlyphShapingClass shapingClass = AdvancedTypographicUtils.GetGlyphShapingClass(this.fontMetrics, data.GlyphIds[0], data); + GlyphShapingClass shapingClass = AdvancedTypographicUtils.GetGlyphShapingClass(this.fontMetrics, data.GlyphId, data); return (this.ignoreMarks && shapingClass.IsMark) || (this.ignoreBaseGlypghs && shapingClass.IsBase) || (this.ignoreLigatures && shapingClass.IsLigature) || diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index bf6f5468..d249c02c 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -713,7 +713,7 @@ private static TextBox BreakLines( var codePointEnumerator = new SpanCodePointEnumerator(graphemeEnumerator.Current); while (codePointEnumerator.MoveNext()) { - if (!positionings.TryGetGlyphMetricsAtOffset(codePointIndex, out float pointSize, out GlyphMetrics[]? metrics)) + if (!positionings.TryGetGlyphMetricsAtOffset(codePointIndex, out float pointSize, out IReadOnlyList? metrics)) { // Codepoint was skipped during original enumeration. codePointIndex++; @@ -736,7 +736,7 @@ private static TextBox BreakLines( { glyphAdvance *= options.TabWidth; } - else if (metrics.Length == 1 && (CodePoint.IsZeroWidthJoiner(codePoint) || CodePoint.IsZeroWidthNonJoiner(codePoint))) + else if (metrics.Count == 1 && (CodePoint.IsZeroWidthJoiner(codePoint) || CodePoint.IsZeroWidthNonJoiner(codePoint))) { // The zero-width joiner characters should be ignored when determining word or // line break boundaries so are safe to skip here. Any existing instances are the result of font error @@ -749,7 +749,7 @@ private static TextBox BreakLines( // Standard text. Use the largest advance for the metrics. if (isHorizontal) { - for (int i = 1; i < metrics.Length; i++) + for (int i = 1; i < metrics.Count; i++) { float a = metrics[i].AdvanceWidth; if (a > glyphAdvance) @@ -760,7 +760,7 @@ private static TextBox BreakLines( } else { - for (int i = 1; i < metrics.Length; i++) + for (int i = 1; i < metrics.Count; i++) { float a = metrics[i].AdvanceHeight; if (a > glyphAdvance) @@ -959,7 +959,7 @@ internal sealed class TextLine public GlyphLayoutData this[int index] => this.data[index]; public void Add( - GlyphMetrics[] metrics, + IReadOnlyList metrics, float pointSize, float scaledAdvance, float scaledLineHeight, @@ -1213,7 +1213,7 @@ private static OrderedBidiRun LinearReOrder(OrderedBidiRun? line) internal readonly struct GlyphLayoutData { public GlyphLayoutData( - GlyphMetrics[] metrics, + IReadOnlyList metrics, float pointSize, float scaledAdvance, float scaledLineHeight, @@ -1238,7 +1238,7 @@ public GlyphLayoutData( public CodePoint CodePoint => this.Metrics[0].CodePoint; - public GlyphMetrics[] Metrics { get; } + public IReadOnlyList Metrics { get; } public float PointSize { get; } From 53e6d751eb41f465643b4050a6e0c4952cf5afd7 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 6 May 2022 23:43:02 +1000 Subject: [PATCH 08/10] Correctly handle glyph decomposition --- .../GlyphPositioningCollection.cs | 28 ++++++++++++------- src/SixLabors.Fonts/GlyphShapingData.cs | 8 +++--- .../GlyphSubstitutionCollection.cs | 12 +++----- .../GSub/LookupType2SubTable.cs | 2 +- .../Shapers/HangulShaper.cs | 8 +++--- src/SixLabors.Fonts/TextLayout.cs | 17 ++++++++--- 6 files changed, 44 insertions(+), 31 deletions(-) diff --git a/src/SixLabors.Fonts/GlyphPositioningCollection.cs b/src/SixLabors.Fonts/GlyphPositioningCollection.cs index aa45c62b..edccab85 100644 --- a/src/SixLabors.Fonts/GlyphPositioningCollection.cs +++ b/src/SixLabors.Fonts/GlyphPositioningCollection.cs @@ -90,21 +90,24 @@ public void DisableShapingFeature(int index, Tag feature) /// /// The zero-based index within the input codepoint collection. /// The font size in PT units of the font containing this glyph. + /// Whether the glyph is the result of a decomposition substitution. /// /// When this method returns, contains the glyph metrics associated with the specified offset, /// if the value is found; otherwise, the default value for the type of the metrics parameter. /// This parameter is passed uninitialized. /// /// The metrics. - public bool TryGetGlyphMetricsAtOffset(int offset, out float pointSize, [NotNullWhen(true)] out IReadOnlyList? metrics) + public bool TryGetGlyphMetricsAtOffset(int offset, out float pointSize, out bool isDecomposed, [NotNullWhen(true)] out IReadOnlyList? metrics) { List match = new(); pointSize = 0; + isDecomposed = false; for (int i = 0; i < this.glyphs.Count; i++) { if (this.glyphs[i].Offset == offset) { GlyphPositioningData glyph = this.glyphs[i]; + isDecomposed = glyph.Data.IsDecomposed; pointSize = glyph.PointSize; match.AddRange(glyph.Metrics); } @@ -151,7 +154,7 @@ public bool TryUpdate(Font font, GlyphSubstitutionCollection collection) GlyphShapingData shape = data[j]; ushort id = shape.GlyphId; CodePoint codePoint = shape.CodePoint; - bool doShift = shape.OffsetGlyph; + bool isDecomposed = shape.IsDecomposed; // Perform a semi-deep clone (FontMetrics is not cloned) so we can continue to // cache the original in the font metrics and only update our collection. @@ -166,10 +169,11 @@ public bool TryUpdate(Font font, GlyphSubstitutionCollection collection) break; } - // Clone and offset the glyph based on it's position in the offset group. + // Clone and offset the glyph for rendering. + // If the glyph is the result of a decomposition substitution we need to offset it. // We slip the text run in here while we clone so we have it available to the renderer. var clone = GlyphMetrics.CloneForRendering(gm, shape.TextRun, codePoint); - if (doShift) + if (isDecomposed) { if (!this.IsVerticalLayoutMode) { @@ -217,6 +221,7 @@ public bool TryAdd(Font font, GlyphSubstitutionCollection collection) bool hasFallBacks = false; FontMetrics fontMetrics = font.FontMetrics; ColorFontSupport colorFontSupport = this.TextOptions.ColorFontSupport; + ushort shiftXY = 0; for (int i = 0; i < collection.Count; i++) { GlyphShapingData data = collection.GetGlyphShapingData(i, out int offset); @@ -224,8 +229,11 @@ public bool TryAdd(Font font, GlyphSubstitutionCollection collection) ushort id = data.GlyphId; List metrics = new(); - ushort shiftXY = 0; - bool doShift = data.OffsetGlyph; + bool isDecomposed = data.IsDecomposed; + if (!isDecomposed) + { + shiftXY = 0; + } // Perform a semi-deep clone (FontMetrics is not cloned) so we can continue to // cache the original in the font metrics and only update our collection. @@ -236,11 +244,11 @@ public bool TryAdd(Font font, GlyphSubstitutionCollection collection) hasFallBacks = true; } - // Clone and offset the glyph based on it's position in the glyphId array. - // We slip the text run in here while we clone so we have - // it available to the renderer. + // Clone and offset the glyph for rendering. + // If the glyph is the result of a decomposition substitution we need to offset it. + // We slip the text run in here while we clone so we have it available to the renderer. var clone = GlyphMetrics.CloneForRendering(gm, data.TextRun, codePoint); - if (doShift) + if (isDecomposed) { if (!this.IsVerticalLayoutMode) { diff --git a/src/SixLabors.Fonts/GlyphShapingData.cs b/src/SixLabors.Fonts/GlyphShapingData.cs index f67d2ede..717ce8ff 100644 --- a/src/SixLabors.Fonts/GlyphShapingData.cs +++ b/src/SixLabors.Fonts/GlyphShapingData.cs @@ -36,7 +36,7 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) this.LigatureComponent = data.LigatureComponent; this.MarkAttachment = data.MarkAttachment; this.CursiveAttachment = data.CursiveAttachment; - this.OffsetGlyph = data.OffsetGlyph; + this.IsDecomposed = data.IsDecomposed; if (!clearFeatures) { @@ -102,12 +102,12 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) public GlyphShapingBounds Bounds { get; set; } = new(0, 0, 0, 0); /// - /// Gets or sets a value indicating whether this glyph should be positioned at the advance of the preceding glyph at the same codepoint offset. + /// Gets or sets a value indicating whether this glyph is the result of a decomposition substitution /// - public bool OffsetGlyph { get; set; } + public bool IsDecomposed { get; set; } private string DebuggerDisplay => FormattableString - .Invariant($" {this.GlyphId} : {this.CodePoint.ToDebuggerDisplay()} : {CodePoint.GetScriptClass(this.CodePoint)} : {this.Direction} : {this.TextRun.TextAttributes} : {this.LigatureId} : {this.LigatureComponent} : {this.OffsetGlyph}"); + .Invariant($" {this.GlyphId} : {this.CodePoint.ToDebuggerDisplay()} : {CodePoint.GetScriptClass(this.CodePoint)} : {this.Direction} : {this.TextRun.TextAttributes} : {this.LigatureId} : {this.LigatureComponent} : {this.IsDecomposed}"); } } diff --git a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs index 0f8ec6be..b6ed4159 100644 --- a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs +++ b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs @@ -263,8 +263,7 @@ public void Replace(int index, int count, ushort glyphId) /// /// The zero-based index of the element to replace. /// The collection of replacement glyph ids. - /// Whether the additional glyphs should be positioned at the advance of the preceding glyph at the same codepoint offset. - public void Replace(int index, ReadOnlySpan glyphIds, bool offset) + public void Replace(int index, ReadOnlySpan glyphIds) { if (glyphIds.Length > 0) { @@ -274,9 +273,9 @@ public void Replace(int index, ReadOnlySpan glyphIds, bool offset) current.LigatureComponent = 0; current.MarkAttachment = -1; current.CursiveAttachment = -1; - current.OffsetGlyph = offset; + current.IsDecomposed = true; - // Add additional glyphs to the end of the sequence. + // Add additional glyphs from the rest of the sequence. if (glyphIds.Length > 1) { glyphIds = glyphIds.Slice(1); @@ -285,10 +284,7 @@ public void Replace(int index, ReadOnlySpan glyphIds, bool offset) GlyphShapingData data = new(current, false); data.GlyphId = glyphIds[i]; data.LigatureComponent = i + 1; - - // TODO: We may have to shift the offset of the following glyphs. - int o = offset ? pair.Offset + i + 1 : pair.Offset; - this.glyphs.Insert(++index, new(o, data)); + this.glyphs.Insert(++index, new(pair.Offset, data)); } } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs index f171edad..c83a7965 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs @@ -101,7 +101,7 @@ public override bool TrySubstitution( if (offset > -1) { - collection.Replace(index, this.sequenceTables[offset].SubstituteGlyphs, false); + collection.Replace(index, this.sequenceTables[offset].SubstituteGlyphs); return true; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs index 1566d18b..0f834da9 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs @@ -133,7 +133,7 @@ public override void AssignFeatures(IGlyphShapingCollection collection, int inde case Invalid: // Tone mark has no valid syllable to attach to, so insert a dotted circle. - this.InsertDottedCircle(substitutionCollection, data, i); + i = this.InsertDottedCircle(substitutionCollection, data, i); break; } } @@ -234,7 +234,7 @@ private int DecomposeGlyph(GlyphSubstitutionCollection collection, GlyphShapingD ii[0] = ljmo; ii[1] = vjmo; - collection.Replace(index, ii, true); + collection.Replace(index, ii); collection.EnableShapingFeature(index, LjmoTag); collection.EnableShapingFeature(index + 1, VjmoTag); return index + 1; @@ -245,7 +245,7 @@ private int DecomposeGlyph(GlyphSubstitutionCollection collection, GlyphShapingD iii[1] = vjmo; iii[2] = tjmo; - collection.Replace(index, iii, true); + collection.Replace(index, iii); collection.EnableShapingFeature(index, LjmoTag); collection.EnableShapingFeature(index + 1, VjmoTag); collection.EnableShapingFeature(index + 2, TjmoTag); @@ -398,7 +398,7 @@ private int InsertDottedCircle(GlyphSubstitutionCollection collection, GlyphShap glyphs[1] = data.GlyphId; } - collection.Replace(index, glyphs, true); + collection.Replace(index, glyphs); return index + 1; } diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index d249c02c..a41cc91b 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -713,7 +713,7 @@ private static TextBox BreakLines( var codePointEnumerator = new SpanCodePointEnumerator(graphemeEnumerator.Current); while (codePointEnumerator.MoveNext()) { - if (!positionings.TryGetGlyphMetricsAtOffset(codePointIndex, out float pointSize, out IReadOnlyList? metrics)) + if (!positionings.TryGetGlyphMetricsAtOffset(codePointIndex, out float pointSize, out bool isDecomposed, out IReadOnlyList? metrics)) { // Codepoint was skipped during original enumeration. codePointIndex++; @@ -746,13 +746,18 @@ private static TextBox BreakLines( } else if (!CodePoint.IsNewLine(codePoint)) { - // Standard text. Use the largest advance for the metrics. + // Standard text. + // If decomposed we need to add the advance; otherwise, use the largest advance for the metrics. if (isHorizontal) { for (int i = 1; i < metrics.Count; i++) { float a = metrics[i].AdvanceWidth; - if (a > glyphAdvance) + if (isDecomposed) + { + glyphAdvance += a; + } + else if (a > glyphAdvance) { glyphAdvance = a; } @@ -763,7 +768,11 @@ private static TextBox BreakLines( for (int i = 1; i < metrics.Count; i++) { float a = metrics[i].AdvanceHeight; - if (a > glyphAdvance) + if (isDecomposed) + { + glyphAdvance += a; + } + else if (a > glyphAdvance) { glyphAdvance = a; } From dc4fd0e028fec593e36ba8172b30d009ed5fd350 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 7 May 2022 13:24:47 +1000 Subject: [PATCH 09/10] Add tests --- .../Shapers/HangulShaper.cs | 2 - src/SixLabors.Fonts/TextLayout.cs | 13 +- .../Gsub/GSubTableTests.Hangul.cs | 192 ++++++++++++++++++ .../Gsub/GSubTableTests.cs | 2 +- 4 files changed, 203 insertions(+), 6 deletions(-) create mode 100644 tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.Hangul.cs diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs index 0f834da9..9b49be12 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs @@ -20,8 +20,6 @@ internal sealed class HangulShaper : DefaultShaper private static readonly Tag TjmoTag = Tag.Parse("tjmo"); private const int HangulBase = 0xac00; - private const int HangulEnd = 0xd7a4; - private const int HangulCount = HangulEnd - HangulBase + 1; private const int LBase = 0x1100; // lead private const int VBase = 0x1161; // vowel private const int TBase = 0x11a7; // trail diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index a41cc91b..01c38bb9 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -884,10 +884,17 @@ private static TextBox BreakLines( GlyphMetrics metric = metrics[0]; float scaleY = pointSize / metric.ScaleFactor.Y; float ascender = metric.FontMetrics.Ascender * scaleY; - if (metric.TopSideBearing < 0) + + // Adjust ascender for glyphs with a negative tsb. e.g. emoji to prevent cutoff. + short tsbOffset = 0; + for (int i = 0; i < metrics.Count; i++) + { + tsbOffset = Math.Min(tsbOffset, metrics[i].TopSideBearing); + } + + if (tsbOffset < 0) { - // Adjust for glyphs with a negative tsb. e.g. emoji. - ascender -= metric.TopSideBearing * scaleY; + ascender -= tsbOffset * scaleY; } float descender = Math.Abs(metric.FontMetrics.Descender * scaleY); diff --git a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.Hangul.cs b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.Hangul.cs new file mode 100644 index 00000000..aa08069c --- /dev/null +++ b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.Hangul.cs @@ -0,0 +1,192 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using Xunit; + +namespace SixLabors.Fonts.Tests.Tables.AdvancedTypographic.Gsub +{ + /// + /// Tests adapted from . + /// + public partial class GSubTableTests + { + // TODO: Switch to NotoSansKR-Regular when we have CFF support. +#if OS_WINDOWS + private readonly Font hangulFont = SystemFonts.CreateFont("Malgun Gothic", 12); + + [Fact] + public void ShouldUseComposedSyllables() + { + // arrange + const string input = "\uD734\uAC00\u0020\uAC00\u002D\u002D\u0020\u0028\uC624\u002D\u002D\u0029"; + ColorGlyphRenderer renderer = new(); + int[] expectedGlyphIndices = { 2953, 636, 3, 636, 16, 16, 3, 11, 2077, 16, 16, 12 }; + + // act + TextRenderer.RenderTextTo(renderer, input, new TextOptions(this.hangulFont)); + + // assert + Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); + for (int i = 0; i < expectedGlyphIndices.Length; i++) + { + Assert.Equal(expectedGlyphIndices[i], renderer.GlyphKeys[i].GlyphIndex); + } + } + + [Fact] + public void ShouldComposeDecomposedSyllables() + { + // arrange + const string input = "\u1112\u1172\u1100\u1161\u0020\u1100\u1161\u002D\u002D\u0020\u0028\u110B\u1169\u002D\u002D\u0029"; + ColorGlyphRenderer renderer = new(); + int[] expectedGlyphIndices = { 2953, 636, 3, 636, 16, 16, 3, 11, 2077, 16, 16, 12 }; + + // act + TextRenderer.RenderTextTo(renderer, input, new TextOptions(this.hangulFont)); + + // assert + Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); + for (int i = 0; i < expectedGlyphIndices.Length; i++) + { + Assert.Equal(expectedGlyphIndices[i], renderer.GlyphKeys[i].GlyphIndex); + } + } + + [Fact] + public void ShouldUseOTFeaturesForNonCombining_L_V_T() + { + // arrange + const string input = "\ua960\ud7b0\ud7cb"; + ColorGlyphRenderer renderer = new(); + int[] expectedGlyphIndices = { 21150, 21436, 21569 }; + + // act + TextRenderer.RenderTextTo(renderer, input, new TextOptions(this.hangulFont)); + + // assert + Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); + for (int i = 0; i < expectedGlyphIndices.Length; i++) + { + Assert.Equal(expectedGlyphIndices[i], renderer.GlyphKeys[i].GlyphIndex); + } + } + + [Fact] + public void ShouldDecompose_LV_T_To_L_V_T_If_LVT_IsNotSupported() + { + // combine at first, but the T is non-combining, so this + // tests that the gets decomposed again in this case. + + // arrange + const string input = "\u1100\u1161\ud7cb"; + ColorGlyphRenderer renderer = new(); + int[] expectedGlyphIndices = { 20667, 21294, 21569 }; + + // act + TextRenderer.RenderTextTo(renderer, input, new TextOptions(this.hangulFont)); + + // assert + Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); + for (int i = 0; i < expectedGlyphIndices.Length; i++) + { + Assert.Equal(expectedGlyphIndices[i], renderer.GlyphKeys[i].GlyphIndex); + } + } + + [Fact] + public void ShouldReorderToneMarksToBeginningOf_L_V_Syllables() + { + // arrange + const string input = "\ua960\ud7b0\u302f"; + ColorGlyphRenderer renderer = new(); + int[] expectedGlyphIndices = { 20665, 21150, 21435 }; + + // act + TextRenderer.RenderTextTo(renderer, input, new TextOptions(this.hangulFont)); + + // assert + Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); + for (int i = 0; i < expectedGlyphIndices.Length; i++) + { + Assert.Equal(expectedGlyphIndices[i], renderer.GlyphKeys[i].GlyphIndex); + } + } + + [Fact] + public void ShouldReorderToneMarksToBeginningOf_L_V_T_Syllables() + { + // arrange + const string input = "\ua960\ud7b0\ud7cb\u302f"; + ColorGlyphRenderer renderer = new(); + int[] expectedGlyphIndices = { 20665, 21150, 21436, 21569 }; + + // act + TextRenderer.RenderTextTo(renderer, input, new TextOptions(this.hangulFont)); + + // assert + Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); + for (int i = 0; i < expectedGlyphIndices.Length; i++) + { + Assert.Equal(expectedGlyphIndices[i], renderer.GlyphKeys[i].GlyphIndex); + } + } + + [Fact] + public void ShouldReorderToneMarksToBeginningOf_LV_Syllables() + { + // arrange + const string input = "\uac00\u302f"; + ColorGlyphRenderer renderer = new(); + int[] expectedGlyphIndices = { 20665, 636 }; + + // act + TextRenderer.RenderTextTo(renderer, input, new TextOptions(this.hangulFont)); + + // assert + Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); + for (int i = 0; i < expectedGlyphIndices.Length; i++) + { + Assert.Equal(expectedGlyphIndices[i], renderer.GlyphKeys[i].GlyphIndex); + } + } + + [Fact] + public void ShouldReorderToneMarksToBeginningOf_LVT_Syllables() + { + // arrange + const string input = "\uac01\u302f"; + ColorGlyphRenderer renderer = new(); + int[] expectedGlyphIndices = { 20665, 637 }; + + // act + TextRenderer.RenderTextTo(renderer, input, new TextOptions(this.hangulFont)); + + // assert + Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); + for (int i = 0; i < expectedGlyphIndices.Length; i++) + { + Assert.Equal(expectedGlyphIndices[i], renderer.GlyphKeys[i].GlyphIndex); + } + } + + [Fact] + public void ShouldInsertDottedCircleForInvalidToneMarks() + { + // arrange + const string input = "\u1100\u302f\u1161"; + ColorGlyphRenderer renderer = new(); + int[] expectedGlyphIndices = { 2986, 20665, 21620, 3078 }; + + // act + TextRenderer.RenderTextTo(renderer, input, new TextOptions(this.hangulFont)); + + // assert + Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); + for (int i = 0; i < expectedGlyphIndices.Length; i++) + { + Assert.Equal(expectedGlyphIndices[i], renderer.GlyphKeys[i].GlyphIndex); + } + } +#endif + } +} diff --git a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.cs b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.cs index b8e40d78..db3867c6 100644 --- a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.cs +++ b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.cs @@ -7,7 +7,7 @@ namespace SixLabors.Fonts.Tests.Tables.AdvancedTypographic.GSub { - public class GSubTableTests + public partial class GSubTableTests { [Theory] [InlineData("ا", 139)] From 83f730a6e445031c7e59229f963f8fa2ff7e557f Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 7 May 2022 14:13:21 +1000 Subject: [PATCH 10/10] Update UnicodeUtilityTests.cs --- .../Unicode/UnicodeUtilityTests.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/SixLabors.Fonts.Tests/Unicode/UnicodeUtilityTests.cs b/tests/SixLabors.Fonts.Tests/Unicode/UnicodeUtilityTests.cs index b0959cff..ed4b66c4 100644 --- a/tests/SixLabors.Fonts.Tests/Unicode/UnicodeUtilityTests.cs +++ b/tests/SixLabors.Fonts.Tests/Unicode/UnicodeUtilityTests.cs @@ -39,5 +39,51 @@ public void NoFalsePositiveCJKCodePoints(uint min, uint max) Assert.False(UnicodeUtility.IsCJKCodePoint(i)); } } + + [Theory] + [InlineData(0x00AD, 0x00AD)] + [InlineData(0x034F, 0x034F)] + [InlineData(0x061C, 0x061C)] + [InlineData(0x115F, 0x1160)] + [InlineData(0x17B4, 0x17B5)] + [InlineData(0x180B, 0x180D)] + [InlineData(0x180E, 0x180E)] + [InlineData(0x180F, 0x180F)] + [InlineData(0x200B, 0x200F)] + [InlineData(0x202A, 0x202E)] + [InlineData(0x2060, 0x2064)] + [InlineData(0x2065, 0x2065)] + [InlineData(0x2066, 0x206F)] + [InlineData(0x3164, 0x3164)] + [InlineData(0xFE00, 0xFE0F)] + [InlineData(0xFEFF, 0xFEFF)] + [InlineData(0xFFA0, 0xFFA0)] + [InlineData(0xFFF0, 0xFFF8)] + [InlineData(0x1BCA0, 0x1BCA3)] + [InlineData(0x1D173, 0x1D17A)] + [InlineData(0xE0000, 0xE0000)] + [InlineData(0xE0001, 0xE0001)] + [InlineData(0xE0002, 0xE001F)] + [InlineData(0xE0020, 0xE007F)] + [InlineData(0xE0080, 0xE00FF)] + [InlineData(0xE0100, 0xE01EF)] + [InlineData(0xE01F0, 0xE0FFF)] + public void CanDetectDefaultIgnorableCodePoint(uint min, uint max) + { + for (uint i = min; i <= max; i++) + { + Assert.True(UnicodeUtility.IsDefaultIgnorableCodePoint(i)); + } + } + + [Theory] + [InlineData(0x0u, 0x7Fu)] // ASCII + public void NoFalsePositiveDefaultIgnorableCodePoint(uint min, uint max) + { + for (uint i = min; i <= max; i++) + { + Assert.False(UnicodeUtility.IsDefaultIgnorableCodePoint(i)); + } + } } }