From 4d2f1e12208fde57b27132a465c65fcfcaf03f6c Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 23 Jul 2026 19:18:46 +1000 Subject: [PATCH 01/94] Add TextShaper and optimize OT shaping Introduces a new shaping API (`TextShaper.Shape`) and `ShapedGlyph` output model so callers can get logical-order shaped glyph streams directly from the layout pipeline. Adds `TextOptions.Culture` and rewires GSUB/GPOS language-system selection to use resolved OpenType language tags (including `dflt` handling), with updated tag generation/mappings aligned to HarfBuzz behavior. Internally, replaces per-glyph feature collections with shared bitmask-based feature tracking (`ShapingFeatureMap`) and adds `GlyphSetDigest` prefilters to skip lookups that cannot match current glyph sets. This reduces shaping overhead while preserving correctness. The change also updates glyph metric cloning/caching behavior for lower allocation cost, adds shaping/language regression tests (including HarfBuzz fixture coverage), and adds new shaping benchmarks. --- src/SixLabors.Fonts/FontGlyphMetrics.cs | 8 +- .../GlyphPositioningCollection.cs | 142 ++++++------ src/SixLabors.Fonts/GlyphShapingData.cs | 34 +-- .../GlyphSubstitutionCollection.cs | 92 +++++--- .../IGlyphShapingCollection.cs | 33 +++ src/SixLabors.Fonts/ShapedGlyph.cs | 103 +++++++++ src/SixLabors.Fonts/ShapingProbe.cs | 81 +++++++ .../AdvancedTypographicUtils.cs | 29 +-- .../AdvancedTypographic/CoverageTable.cs | 30 +++ .../GPos/LookupListTable.cs | 24 ++ .../GPos/LookupType1SubTable.cs | 6 + .../GPos/LookupType2SubTable.cs | 6 + .../GPos/LookupType3SubTable.cs | 3 + .../GPos/LookupType4SubTable.cs | 3 + .../GPos/LookupType5SubTable.cs | 3 + .../GPos/LookupType6SubTable.cs | 3 + .../GPos/LookupType7SubTable.cs | 20 ++ .../GPos/LookupType8SubTable.cs | 20 ++ .../Tables/AdvancedTypographic/GPosTable.cs | 80 ++++++- .../GSub/LookupListTable.cs | 24 ++ .../GSub/LookupType1SubTable.cs | 10 +- .../GSub/LookupType2SubTable.cs | 5 +- .../GSub/LookupType3SubTable.cs | 5 +- .../GSub/LookupType4SubTable.cs | 5 +- .../GSub/LookupType5SubTable.cs | 26 ++- .../GSub/LookupType6SubTable.cs | 26 ++- .../GSub/LookupType8SubTable.cs | 5 +- .../Tables/AdvancedTypographic/GSubTable.cs | 79 ++++++- .../AdvancedTypographic/GlyphSetDigest.cs | 114 ++++++++++ .../OpenTypeLanguageTagMap.Generated.cs | 130 +++++++---- .../OpenTypeLanguageTagMap.cs | 182 +++++++++++---- .../Shapers/IndicShaper.cs | 19 +- .../AdvancedTypographic/Shapers/ThaiShaper.cs | 4 +- .../Shapers/UniversalShaper.cs | 3 +- .../AdvancedTypographic/ShapingFeatureMap.cs | 92 ++++++++ .../Tables/TrueType/TrueTypeGlyphMetrics.cs | 14 +- src/SixLabors.Fonts/TextLayout.cs | 32 ++- src/SixLabors.Fonts/TextOptions.cs | 17 ++ src/SixLabors.Fonts/TextShaper.cs | 91 ++++++++ .../Generator.OpenTypeLanguageTags.cs | 139 ++++++++++-- tests/Fonts/HarfBuzz-LanguageTags.ttf | 3 + .../SixLabors.Fonts.Benchmarks/Config.cs | 57 +++++ .../MeasureTextBenchmark.cs | 1 + .../ShapeTextBenchmark.cs | 156 +++++++++++++ .../SixLabors.Fonts.Benchmarks.csproj | 3 + .../OpenTypeLanguageTagMapTests.cs | 134 ++++++++--- tests/SixLabors.Fonts.Tests/TestFonts.cs | 6 + .../TextLayoutTestUtilities.cs | 1 + .../SixLabors.Fonts.Tests/TextLayoutTests.cs | 35 ++- .../SixLabors.Fonts.Tests/TextShaperTests.cs | 214 ++++++++++++++++++ 50 files changed, 2032 insertions(+), 320 deletions(-) create mode 100644 src/SixLabors.Fonts/ShapedGlyph.cs create mode 100644 src/SixLabors.Fonts/ShapingProbe.cs create mode 100644 src/SixLabors.Fonts/Tables/AdvancedTypographic/GlyphSetDigest.cs create mode 100644 src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapingFeatureMap.cs create mode 100644 src/SixLabors.Fonts/TextShaper.cs create mode 100644 tests/Fonts/HarfBuzz-LanguageTags.ttf create mode 100644 tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/Config.cs create mode 100644 tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/ShapeTextBenchmark.cs create mode 100644 tests/SixLabors.Fonts.Tests/TextShaperTests.cs diff --git a/src/SixLabors.Fonts/FontGlyphMetrics.cs b/src/SixLabors.Fonts/FontGlyphMetrics.cs index 688938039..8cb94b96f 100644 --- a/src/SixLabors.Fonts/FontGlyphMetrics.cs +++ b/src/SixLabors.Fonts/FontGlyphMetrics.cs @@ -231,8 +231,12 @@ internal FontGlyphMetrics( public TextDecorations TextDecorations { get; } /// - /// Performs a semi-deep clone (FontMetrics are not cloned) for rendering - /// This allows caching the original in the font metrics. + /// Performs a shallow positioning clone: the returned instance carries its own + /// mutable scalar state (advances, offset, text run) so glyph positioning can write + /// to it, while immutable loaded data (font metrics, outline or charstring data) is + /// shared with the cached original. Implementations must not copy outline data: the + /// raw outline is immutable after load and every consumer that transforms it clones + /// it first. /// /// The current text run this glyph belongs to. /// The new . diff --git a/src/SixLabors.Fonts/GlyphPositioningCollection.cs b/src/SixLabors.Fonts/GlyphPositioningCollection.cs index 1bb4b918d..d843a4d60 100644 --- a/src/SixLabors.Fonts/GlyphPositioningCollection.cs +++ b/src/SixLabors.Fonts/GlyphPositioningCollection.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Runtime.CompilerServices; using SixLabors.Fonts.Tables.AdvancedTypographic; using SixLabors.Fonts.Unicode; @@ -18,12 +19,24 @@ internal sealed class GlyphPositioningCollection : IGlyphShapingCollection /// 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 = []; + private GlyphSetDigest glyphDigest; /// /// Initializes a new instance of the class. /// /// The text options. - public GlyphPositioningCollection(TextOptions textOptions) => this.TextOptions = textOptions; + /// The feature bit assignment shared by the shaping pass. + public GlyphPositioningCollection(TextOptions textOptions, ShapingFeatureMap featureMap) + { + this.TextOptions = textOptions; + this.FeatureMap = featureMap; + + // A null culture takes the ambient current culture, mirroring the reference + // shaping engine model where an unset buffer language is guessed from the + // locale. CultureInfo.InvariantCulture expresses no language preference. + CultureInfo culture = textOptions.Culture ?? CultureInfo.CurrentCulture; + this.LanguageTags = OpenTypeLanguageTagMap.TryGetTags(culture, out Tag[] tags) ? tags : []; + } /// public int Count => this.glyphs.Count; @@ -31,6 +44,15 @@ internal sealed class GlyphPositioningCollection : IGlyphShapingCollection /// public TextOptions TextOptions { get; } + /// + public Tag[] LanguageTags { get; } + + /// + public GlyphSetDigest GlyphDigest => this.glyphDigest; + + /// + public ShapingFeatureMap FeatureMap { get; } + /// public GlyphShapingData this[int index] { @@ -38,53 +60,52 @@ public GlyphShapingData this[int index] get => this.glyphs[index].Data; } + /// + public void SetGlyphId(int index, ushort glyphId) + { + this.glyphDigest.Add(glyphId); + this.glyphs[index].Data.GlyphId = glyphId; + } + /// public void AddShapingFeature(int index, TagEntry feature) { + // Registration only ever accumulates: adding a disabled entry for an already + // enabled feature must not clear the enabled bit, matching the list model this + // replaced where a disabled duplicate left earlier enabled entries in force. GlyphShapingData data = this.glyphs[index].Data; - data.Features.Add(feature); + ulong mask = this.FeatureMap.GetOrAddMask(feature.Tag); + data.RegisteredFeatureMask |= mask; if (feature.Enabled) { - data.EnabledFeatureTags.Add(feature.Tag); + data.FeatureMask |= mask; } } /// public void EnableShapingFeature(int index, Tag feature) { + // Intersecting with the registered mask preserves the contract that enabling a + // feature a shaper never added for this glyph is a no-op. GlyphShapingData data = this.glyphs[index].Data; - List features = data.Features; - for (int i = 0; i < features.Count; i++) - { - TagEntry tagEntry = features[i]; - if (tagEntry.Tag == feature) - { - tagEntry.Enabled = true; - features[i] = tagEntry; - data.EnabledFeatureTags.Add(feature); - break; - } - } + data.FeatureMask |= data.RegisteredFeatureMask & this.FeatureMap.GetMask(feature); } /// public void DisableShapingFeature(int index, Tag feature) { + // An unregistered tag yields a zero mask whose complement clears nothing. GlyphShapingData data = this.glyphs[index].Data; - List features = data.Features; - for (int i = 0; i < features.Count; i++) - { - TagEntry tagEntry = features[i]; - if (tagEntry.Tag == feature) - { - tagEntry.Enabled = false; - features[i] = tagEntry; - data.EnabledFeatureTags.Remove(feature); - break; - } - } + data.FeatureMask &= ~this.FeatureMap.GetMask(feature); } + /// + /// Gets the full positioning data at the given index. + /// + /// The zero-based index of the element. + /// The positioning data. + public GlyphPositioningData GetPositioningData(int index) => this.glyphs[index]; + /// /// Gets the glyph metrics at the given codepoint offset. /// @@ -118,9 +139,7 @@ public bool TryGetGlyphMetricsAtOffset( isVerticalSubstitution = false; isDecomposed = false; - Tag vert = KnownFeatureTags.VerticalAlternates; - Tag vrt2 = KnownFeatureTags.VerticalAlternatesAndRotation; - Tag vrtr = KnownFeatureTags.VerticalAlternatesForRotation; + ulong verticalMask = this.GetVerticalFeatureMask(); for (int i = startIndex; i < this.glyphs.Count; i++) { @@ -136,14 +155,7 @@ public bool TryGetGlyphMetricsAtOffset( { isSubstituted = glyph.Data.IsSubstituted; isDecomposed = glyph.Data.IsDecomposed; - - foreach (Tag feature in glyph.Data.AppliedFeatures) - { - isVerticalSubstitution |= feature == vert; - isVerticalSubstitution |= feature == vrt2; - isVerticalSubstitution |= feature == vrtr; - } - + isVerticalSubstitution |= (glyph.Data.AppliedFeatureMask & verticalMask) != 0; pointSize = glyph.PointSize; } @@ -175,9 +187,7 @@ public bool TryUpdate(Font font, GlyphSubstitutionCollection collection) bool hasFallBacks = false; List orphans = []; - Tag vert = KnownFeatureTags.VerticalAlternates; - Tag vrt2 = KnownFeatureTags.VerticalAlternatesAndRotation; - Tag vrtr = KnownFeatureTags.VerticalAlternatesForRotation; + ulong verticalMask = this.GetVerticalFeatureMask(); for (int i = 0; i < this.glyphs.Count; i++) { @@ -204,13 +214,8 @@ public bool TryUpdate(Font font, GlyphSubstitutionCollection collection) TextAttributes textAttributes = shape.TextRun.TextAttributes; TextDecorations textDecorations = shape.TextRun.TextDecorations; - bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(codePoint, layoutMode); - foreach (Tag feature in shape.AppliedFeatures) - { - isVertical |= feature == vert; - isVertical |= feature == vrt2; - isVertical |= feature == vrtr; - } + bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(codePoint, layoutMode) + || (shape.AppliedFeatureMask & verticalMask) != 0; FontGlyphMetrics metrics = fontMetrics.GetGlyphMetrics(codePoint, id, textAttributes, textDecorations, layoutMode, colorFontSupport); { @@ -236,6 +241,7 @@ public bool TryUpdate(Font font, GlyphSubstitutionCollection collection) : new(0, 0, metrics.AdvanceWidth, 0); // Track the number of inserted glyphs at the offset so we can correctly increment our position. + this.glyphDigest.Add(metrics.GlyphId); this.glyphs.Insert(i += replacementCount, new(offset, new(shape, true) { Bounds = bounds }, font, pointSize, metrics.CloneForRendering(shape.TextRun))); replacementCount++; } @@ -272,9 +278,7 @@ public bool TryAdd(Font font, GlyphSubstitutionCollection collection) LayoutMode layoutMode = this.TextOptions.LayoutMode; ColorFontSupport colorFontSupport = this.TextOptions.ColorFontSupport; - Tag vert = KnownFeatureTags.VerticalAlternates; - Tag vrt2 = KnownFeatureTags.VerticalAlternatesAndRotation; - Tag vrtr = KnownFeatureTags.VerticalAlternatesForRotation; + ulong verticalMask = this.GetVerticalFeatureMask(); for (int i = 0; i < collection.Count; i++) { @@ -307,6 +311,7 @@ public bool TryAdd(Font font, GlyphSubstitutionCollection collection) IsPositioned = true }; + this.glyphDigest.Add(placeholderMetrics.GlyphId); this.glyphs.Add(new(offset, placeholderData, font, font.Size, placeholderMetrics)); continue; } @@ -316,13 +321,8 @@ public bool TryAdd(Font font, GlyphSubstitutionCollection collection) TextAttributes textAttributes = data.TextRun.TextAttributes; TextDecorations textDecorations = data.TextRun.TextDecorations; - bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(codePoint, layoutMode); - foreach (Tag feature in data.AppliedFeatures) - { - isVertical |= feature == vert; - isVertical |= feature == vrt2; - isVertical |= feature == vrtr; - } + bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(codePoint, layoutMode) + || (data.AppliedFeatureMask & verticalMask) != 0; FontGlyphMetrics metrics = fontMetrics.GetGlyphMetrics(codePoint, id, textAttributes, textDecorations, layoutMode, colorFontSupport); @@ -336,6 +336,7 @@ public bool TryAdd(Font font, GlyphSubstitutionCollection collection) ? new(0, 0, 0, metrics.AdvanceHeight) : new(0, 0, metrics.AdvanceWidth, 0); + this.glyphDigest.Add(metrics.GlyphId); this.glyphs.Add(new(offset, new(data, true) { Bounds = bounds }, font, font.Size, metrics.CloneForRendering(data.TextRun))); } @@ -391,23 +392,13 @@ public void UpdatePosition(FontMetrics fontMetrics, int index) public void Advance(FontMetrics fontMetrics, int index, ushort glyphId, short dx, short dy) { LayoutMode layoutMode = this.TextOptions.LayoutMode; - Tag vert = KnownFeatureTags.VerticalAlternates; - Tag vrt2 = KnownFeatureTags.VerticalAlternatesAndRotation; - Tag vrtr = KnownFeatureTags.VerticalAlternatesForRotation; - GlyphPositioningData glyph = this.glyphs[index]; FontGlyphMetrics m = glyph.Metrics; if (m.GlyphId == glyphId && fontMetrics == m.FontMetrics) { - bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(m.CodePoint, layoutMode); - - foreach (Tag feature in glyph.Data.AppliedFeatures) - { - isVertical |= feature == vert; - isVertical |= feature == vrt2; - isVertical |= feature == vrtr; - } + bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(m.CodePoint, layoutMode) + || (glyph.Data.AppliedFeatureMask & this.GetVerticalFeatureMask()) != 0; m.ApplyAdvance(dx, isVertical ? dy : (short)0); } @@ -430,6 +421,17 @@ public bool ShouldProcess(FontMetrics fontMetrics, int index) return data.Metrics.FontMetrics == fontMetrics; } + /// + /// Gets the combined mask of the three vertical alternate features. Computed from + /// the shared feature map so it stays valid for applied bits written during + /// substitution and read here after the copy into this collection. + /// + /// The combined mask, or zero when no vertical feature was registered. + private ulong GetVerticalFeatureMask() + => this.FeatureMap.GetMask(KnownFeatureTags.VerticalAlternates) + | this.FeatureMap.GetMask(KnownFeatureTags.VerticalAlternatesAndRotation) + | this.FeatureMap.GetMask(KnownFeatureTags.VerticalAlternatesForRotation); + [DebuggerDisplay("{DebuggerDisplay,nq}")] public class GlyphPositioningData { diff --git a/src/SixLabors.Fonts/GlyphShapingData.cs b/src/SixLabors.Fonts/GlyphShapingData.cs index 910cc8954..adf4f76d0 100644 --- a/src/SixLabors.Fonts/GlyphShapingData.cs +++ b/src/SixLabors.Fonts/GlyphShapingData.cs @@ -65,17 +65,11 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) if (!clearFeatures) { - this.Features.AddRange(data.Features); - foreach (Tag tag in data.EnabledFeatureTags) - { - this.EnabledFeatureTags.Add(tag); - } + this.RegisteredFeatureMask = data.RegisteredFeatureMask; + this.FeatureMask = data.FeatureMask; } - foreach (Tag feature in data.AppliedFeatures) - { - this.AppliedFeatures.Add(feature); - } + this.AppliedFeatureMask = data.AppliedFeatureMask; this.Bounds = data.Bounds; this.CachedShapingClass = data.CachedShapingClass; @@ -155,20 +149,28 @@ public ushort GlyphId public int CursiveAttachment { get; set; } = -1; /// - /// Gets or sets the collection of features. + /// Gets or sets the mask of features a shaper has registered for this glyph, enabled + /// or not. Bits are assigned by the shaping pass's . + /// Enabling a feature only ever reveals a registered bit; a feature that was never + /// registered for the glyph cannot be enabled. /// - public List Features { get; set; } = []; + public ulong RegisteredFeatureMask { get; set; } /// - /// Gets the set of feature tags that are currently enabled, maintained - /// in sync with for O(1) lookup. + /// Gets or sets the mask of features currently enabled for this glyph: the subset of + /// a lookup application gate tests with a single + /// bitwise AND. /// - internal HashSet EnabledFeatureTags { get; } = []; + public ulong FeatureMask { get; set; } /// - /// Gets or sets the collection of applied features. + /// Gets or sets the mask of features whose lookups actually changed this glyph. + /// Read after shaping, for example to detect that a vertical alternate was + /// substituted. Survives the copy into the positioning collection, which is why the + /// substitution and positioning collections must share one + /// . /// - public HashSet AppliedFeatures { get; set; } = []; + public ulong AppliedFeatureMask { get; set; } /// /// Gets or sets the shaping bounds. diff --git a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs index 23895fa2d..9755ce04f 100644 --- a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs +++ b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Runtime.CompilerServices; using SixLabors.Fonts.Tables.AdvancedTypographic; using SixLabors.Fonts.Unicode; @@ -18,12 +19,24 @@ internal sealed class GlyphSubstitutionCollection : IGlyphShapingCollection /// Contains a map the index of a map within the collection, non-sequential codepoint offsets, and their glyph ids. /// private readonly List glyphs = []; + private GlyphSetDigest glyphDigest; /// /// Initializes a new instance of the class. /// /// The text options. - public GlyphSubstitutionCollection(TextOptions textOptions) => this.TextOptions = textOptions; + /// The feature bit assignment shared by the shaping pass. + public GlyphSubstitutionCollection(TextOptions textOptions, ShapingFeatureMap featureMap) + { + this.TextOptions = textOptions; + this.FeatureMap = featureMap; + + // A null culture takes the ambient current culture, mirroring the reference + // shaping engine model where an unset buffer language is guessed from the + // locale. CultureInfo.InvariantCulture expresses no language preference. + CultureInfo culture = textOptions.Culture ?? CultureInfo.CurrentCulture; + this.LanguageTags = OpenTypeLanguageTagMap.TryGetTags(culture, out Tag[] tags) ? tags : []; + } /// /// Gets the number of glyphs ids contained in the collection. @@ -34,6 +47,15 @@ internal sealed class GlyphSubstitutionCollection : IGlyphShapingCollection /// public TextOptions TextOptions { get; } + /// + public Tag[] LanguageTags { get; } + + /// + public GlyphSetDigest GlyphDigest => this.glyphDigest; + + /// + public ShapingFeatureMap FeatureMap { get; } + /// /// Gets or sets the running id of any ligature glyphs contained withing this collection are a member of. /// @@ -62,48 +84,33 @@ internal GlyphShapingData GetGlyphShapingData(int index, out int offset) /// public void AddShapingFeature(int index, TagEntry feature) { + // Registration only ever accumulates: adding a disabled entry for an already + // enabled feature must not clear the enabled bit, matching the list model this + // replaced where a disabled duplicate left earlier enabled entries in force. GlyphShapingData data = this.glyphs[index].Data; - data.Features.Add(feature); + ulong mask = this.FeatureMap.GetOrAddMask(feature.Tag); + data.RegisteredFeatureMask |= mask; if (feature.Enabled) { - data.EnabledFeatureTags.Add(feature.Tag); + data.FeatureMask |= mask; } } /// public void EnableShapingFeature(int index, Tag feature) { + // Intersecting with the registered mask preserves the contract that enabling a + // feature a shaper never added for this glyph is a no-op. GlyphShapingData data = this.glyphs[index].Data; - List features = data.Features; - for (int i = 0; i < features.Count; i++) - { - TagEntry tagEntry = features[i]; - if (tagEntry.Tag == feature) - { - tagEntry.Enabled = true; - features[i] = tagEntry; - data.EnabledFeatureTags.Add(feature); - break; - } - } + data.FeatureMask |= data.RegisteredFeatureMask & this.FeatureMap.GetMask(feature); } /// public void DisableShapingFeature(int index, Tag feature) { + // An unregistered tag yields a zero mask whose complement clears nothing. GlyphShapingData data = this.glyphs[index].Data; - List features = data.Features; - for (int i = 0; i < features.Count; i++) - { - TagEntry tagEntry = features[i]; - if (tagEntry.Tag == feature) - { - tagEntry.Enabled = false; - features[i] = tagEntry; - data.EnabledFeatureTags.Remove(feature); - break; - } - } + data.FeatureMask &= ~this.FeatureMap.GetMask(feature); } /// @@ -112,7 +119,10 @@ public void DisableShapingFeature(int index, Tag feature) /// The data. /// The zero-based index within the input codepoint collection. public void AddGlyph(GlyphShapingData data, int offset) - => this.glyphs.Add(new(offset, new(data, false))); + { + this.glyphDigest.Add(data.GlyphId); + this.glyphs.Add(new(offset, new(data, false))); + } /// /// Adds the glyph id and the codepoint it represents to the collection. @@ -123,12 +133,15 @@ public void AddGlyph(GlyphShapingData data, int offset) /// 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(new(offset, new(textRun) + { + this.glyphDigest.Add(glyphId); + this.glyphs.Add(new(offset, new(textRun) { CodePoint = codePoint, Direction = direction, GlyphId = glyphId, })); + } /// /// Adds an atomic inline placeholder to the collection. @@ -241,6 +254,13 @@ public void Sort(int startIndex, int endIndex, Comparison comp } } + /// + public void SetGlyphId(int index, ushort glyphId) + { + this.glyphDigest.Add(glyphId); + this.glyphs[index].Data.GlyphId = glyphId; + } + /// /// Removes all elements from the collection. /// @@ -292,13 +312,14 @@ public bool TryGetGlyphShapingDataAtOffset(int offset, [NotNullWhen(true)] out I public void Replace(int index, ushort glyphId, Tag feature) { GlyphShapingData current = this.glyphs[index].Data; + this.glyphDigest.Add(glyphId); current.GlyphId = glyphId; current.LigatureId = 0; current.LigatureComponent = -1; current.MarkAttachment = -1; current.CursiveAttachment = -1; current.IsSubstituted = true; - current.AppliedFeatures.Add(feature); + current.AppliedFeatureMask |= this.FeatureMap.GetOrAddMask(feature); } /// @@ -338,6 +359,7 @@ public void Replace(int index, ReadOnlySpan removalIndices, ushort glyphId, } current.CodePointCount += codePointCount; + this.glyphDigest.Add(glyphId); current.GlyphId = glyphId; current.LigatureId = ligatureId; current.IsLigated = true; @@ -345,7 +367,7 @@ public void Replace(int index, ReadOnlySpan removalIndices, ushort glyphId, current.MarkAttachment = -1; current.CursiveAttachment = -1; current.IsSubstituted = true; - current.AppliedFeatures.Add(feature); + current.AppliedFeatureMask |= this.FeatureMap.GetOrAddMask(feature); } /// @@ -384,13 +406,14 @@ public void Replace(int index, int count, ushort glyphId, Tag feature) } current.CodePointCount += codePointCount; + this.glyphDigest.Add(glyphId); current.GlyphId = glyphId; current.LigatureId = 0; current.LigatureComponent = -1; current.MarkAttachment = -1; current.CursiveAttachment = -1; current.IsSubstituted = true; - current.AppliedFeatures.Add(feature); + current.AppliedFeatureMask |= this.FeatureMap.GetOrAddMask(feature); } /// @@ -405,6 +428,7 @@ public void Replace(int index, ReadOnlySpan glyphIds, Tag feature) { OffsetGlyphDataPair pair = this.glyphs[index]; GlyphShapingData current = pair.Data; + this.glyphDigest.Add(glyphIds[0]); current.GlyphId = glyphIds[0]; current.LigatureComponent = 0; current.MarkAttachment = -1; @@ -424,7 +448,9 @@ public void Replace(int index, ReadOnlySpan glyphIds, Tag feature) LigatureComponent = i + 1 }; - data.AppliedFeatures.Add(feature); + this.glyphDigest.Add(glyphIds[i]); + + data.AppliedFeatureMask |= this.FeatureMap.GetOrAddMask(feature); this.glyphs.Insert(++index, new(pair.Offset, data)); } diff --git a/src/SixLabors.Fonts/IGlyphShapingCollection.cs b/src/SixLabors.Fonts/IGlyphShapingCollection.cs index 9a56fd64e..780b8df7a 100644 --- a/src/SixLabors.Fonts/IGlyphShapingCollection.cs +++ b/src/SixLabors.Fonts/IGlyphShapingCollection.cs @@ -20,6 +20,29 @@ internal interface IGlyphShapingCollection /// public TextOptions TextOptions { get; } + /// + /// Gets the candidate OpenType language system tags resolved from + /// , most specific first, or an empty array when the + /// culture expresses no language preference. Resolved once per shaping pass. + /// + public Tag[] LanguageTags { get; } + + /// + /// Gets the feature bit assignment shared by every collection of the shaping pass. + /// See for the mask model and why the instance must + /// be shared across the substitution and positioning collections. + /// + public ShapingFeatureMap FeatureMap { get; } + + /// + /// Gets the approximate membership filter over every glyph id the collection has + /// ever contained. The digest only grows: substituted-away ids remain, keeping a + /// definitive negative from sound while + /// lookups mutate the collection mid-application. Every glyph id write must funnel + /// through the collection so the digest observes it; see . + /// + public GlyphSetDigest GlyphDigest { get; } + /// /// Gets the glyph shaping data at the specified index. /// @@ -27,6 +50,16 @@ internal interface IGlyphShapingCollection /// The . public GlyphShapingData this[int index] { get; } + /// + /// Sets the glyph id at the specified index, recording the id in + /// . Callers outside the collection must use this rather + /// than writing directly, which would leave + /// the digest unaware of the new id. + /// + /// The zero-based index of the element. + /// The glyph id to set. + public void SetGlyphId(int index, ushort glyphId); + /// /// Adds the shaping feature to the collection which should be applied to the glyph at a specified index. /// diff --git a/src/SixLabors.Fonts/ShapedGlyph.cs b/src/SixLabors.Fonts/ShapedGlyph.cs new file mode 100644 index 000000000..3e3689bbe --- /dev/null +++ b/src/SixLabors.Fonts/ShapedGlyph.cs @@ -0,0 +1,103 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts; + +/// +/// Represents a single shaped glyph: the result of substitution and positioning, before +/// line breaking, visual reordering, or scaling. +/// +/// +/// Advances and offsets are expressed in font design units for the glyph's +/// ; multiply by the font size over +/// to convert to pixel units. +/// +public readonly struct ShapedGlyph +{ + /// + /// Initializes a new instance of the struct. + /// + /// The font face that resolved the glyph. + /// The glyph identifier within the font face. + /// The leading codepoint the glyph represents. + /// The codepoint index into the source text. + /// The number of codepoints the glyph represents. + /// The horizontal advance in font design units. + /// The vertical advance in font design units. + /// The placement offset in font design units. + internal ShapedGlyph( + Font font, + ushort glyphId, + CodePoint codePoint, + int codePointIndex, + int codePointCount, + ushort advanceWidth, + ushort advanceHeight, + Vector2 offset) + { + this.Font = font; + this.GlyphId = glyphId; + this.CodePoint = codePoint; + this.CodePointIndex = codePointIndex; + this.CodePointCount = codePointCount; + this.AdvanceWidth = advanceWidth; + this.AdvanceHeight = advanceHeight; + this.Offset = offset; + } + + /// + /// Gets the font face that resolved the glyph: the primary font, a text run override, + /// or a fallback font. + /// + public Font Font { get; } + + /// + /// Gets the glyph identifier within . Glyph id 0 is the font's + /// missing glyph, produced when the font cannot map the codepoint. + /// + public ushort GlyphId { get; } + + /// + /// Gets the leading codepoint the glyph represents: the first codepoint for + /// ligatures. + /// + public CodePoint CodePoint { get; } + + /// + /// Gets the codepoint index into the source text of the first codepoint this glyph + /// represents, following the indexing convention. + /// + /// + /// Ligatures emit one glyph indexed at the first codepoint with + /// covering the rest; marks and decompositions emit + /// multiple glyphs sharing one index. + /// + public int CodePointIndex { get; } + + /// + /// Gets the number of codepoints the glyph represents. + /// + public int CodePointCount { get; } + + /// + /// Gets the horizontal advance in font design units, after positioning features have + /// been applied. + /// + public ushort AdvanceWidth { get; } + + /// + /// Gets the vertical advance in font design units, after positioning features have + /// been applied. + /// + public ushort AdvanceHeight { get; } + + /// + /// Gets the placement offset in font design units, in Y-up font space. The offset + /// positions the glyph outline relative to its pen position and does not contribute + /// to the advance. + /// + public Vector2 Offset { get; } +} diff --git a/src/SixLabors.Fonts/ShapingProbe.cs b/src/SixLabors.Fonts/ShapingProbe.cs new file mode 100644 index 000000000..6b850ae06 --- /dev/null +++ b/src/SixLabors.Fonts/ShapingProbe.cs @@ -0,0 +1,81 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// TEMPORARY diagnostic probe for shaping performance attribution. Delete before commit. +using System.Diagnostics; + +namespace SixLabors.Fonts; + +#pragma warning disable CS1591, SA1600 + +public static class ShapingProbe +{ + public const int BuildTextRuns = 0; + public const int Bidi = 1; + public const int Populate = 2; + public const int Mirrors = 3; + public const int Substitution = 4; + public const int MetricsAdd = 5; + public const int Positioning = 6; + public const int Projection = 7; + public const int LookupResolve = 8; + + private static readonly string[] Names = + [ + "BuildTextRuns", + "Bidi", + "Populate (glyph ids)", + "Bidi mirrors", + "GSUB substitution", + "Metrics add (clones)", + "GPOS positioning", + "Projection (ShapedGlyph)", + " of which lookup resolve", + ]; + + private static readonly long[] Ticks = new long[Names.Length]; + private static readonly long[] Bytes = new long[Names.Length]; + + public static bool Enabled { get; set; } + + public static (long Ticks, long Bytes) Enter() + => Enabled ? (Stopwatch.GetTimestamp(), GC.GetAllocatedBytesForCurrentThread()) : default; + + public static void Exit(int phase, (long Ticks, long Bytes) entry) + { + if (!Enabled) + { + return; + } + + Bytes[phase] += GC.GetAllocatedBytesForCurrentThread() - entry.Bytes; + Ticks[phase] += Stopwatch.GetTimestamp() - entry.Ticks; + } + + public static void Reset() + { + Array.Clear(Ticks); + Array.Clear(Bytes); + } + + public static void Print(int iterations) + { + long totalTicks = 0; + long totalBytes = 0; + for (int i = 0; i < Names.Length; i++) + { + totalTicks += Ticks[i]; + totalBytes += Bytes[i]; + } + + Console.WriteLine($"{"Phase",-26} {"us/op",10} {"%time",7} {"B/op",10} {"%alloc",7}"); + for (int i = 0; i < Names.Length; i++) + { + double us = Ticks[i] * 1_000_000.0 / Stopwatch.Frequency / iterations; + double bytes = (double)Bytes[i] / iterations; + Console.WriteLine($"{Names[i],-26} {us,10:F2} {(totalTicks > 0 ? Ticks[i] * 100.0 / totalTicks : 0),6:F1}% {bytes,10:F0} {(totalBytes > 0 ? Bytes[i] * 100.0 / totalBytes : 0),6:F1}%"); + } + + Console.WriteLine($"{"TOTAL",-26} {totalTicks * 1_000_000.0 / Stopwatch.Frequency / iterations,10:F2} {(double)totalBytes / iterations,10:F0}"); + } +} diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs index 4a3a9c476..0f7373953 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs @@ -187,13 +187,16 @@ public static bool ApplyLookupList( /// A span to store matched glyph indices, or default if not needed. /// if the entire sequence was matched; otherwise, . public static bool MatchInputSequence(SkippingGlyphIterator iterator, Tag feature, ushort increment, ushort[] sequence, Span matches) - => Match( + { + ulong featureMask = iterator.Collection.FeatureMap.GetMask(feature); + + return Match( increment, sequence, iterator, (component, data) => { - if (!ContainsFeatureTag(data.Features, feature)) + if ((data.FeatureMask & featureMask) == 0) { return false; } @@ -201,24 +204,6 @@ public static bool MatchInputSequence(SkippingGlyphIterator iterator, Tag featur return component == data.GlyphId; }, matches); - - /// - /// Determines whether the feature list contains the specified feature tag in an enabled state. - /// - /// The list of tag entries to search. - /// The feature tag to find. - /// if the feature is present and enabled; otherwise, . - private static bool ContainsFeatureTag(List featureList, Tag feature) - { - foreach (TagEntry tagEntry in featureList) - { - if (tagEntry.Tag == feature && tagEntry.Enabled) - { - return true; - } - } - - return false; } /// @@ -470,7 +455,7 @@ public static void ApplyAnchor( markData.Bounds.X = baseXY.XCoordinate - markXY.XCoordinate; markData.Bounds.Y = baseXY.YCoordinate - markXY.YCoordinate; markData.MarkAttachment = baseGlyphIndex; - markData.AppliedFeatures.Add(feature); + markData.AppliedFeatureMask |= collection.FeatureMap.GetOrAddMask(feature); } /// @@ -503,7 +488,7 @@ public static void ApplyPosition( current.Bounds.Height += (short)MathF.Round(fontMetrics.GetGDefVariationDelta(record.YAdvanceVariation)); } - current.AppliedFeatures.Add(feature); + current.AppliedFeatureMask |= collection.FeatureMap.GetOrAddMask(feature); } /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/CoverageTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/CoverageTable.cs index 2b83c6a35..8ef7808c9 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/CoverageTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/CoverageTable.cs @@ -22,6 +22,12 @@ internal abstract class CoverageTable /// The zero-based coverage index, or -1 if not found. public abstract int CoverageIndexOf(ushort glyphId); + /// + /// Adds every glyph this table covers to the digest. + /// + /// The digest to add to. + public abstract void CollectDigest(ref GlyphSetDigest digest); + /// /// Loads a from the binary reader at the specified offset. /// @@ -83,6 +89,15 @@ public override int CoverageIndexOf(ushort glyphId) return n < 0 ? -1 : n; } + /// + public override void CollectDigest(ref GlyphSetDigest digest) + { + foreach (ushort glyphId in this.glyphArray) + { + digest.Add(glyphId); + } + } + /// /// Loads a from the binary reader. /// The format identifier has already been read. @@ -150,6 +165,15 @@ public override int CoverageIndexOf(ushort glyphId) return -1; } + /// + public override void CollectDigest(ref GlyphSetDigest digest) + { + foreach (CoverageRangeRecord record in this.records) + { + digest.AddRange(record.StartGlyphId, record.EndGlyphId); + } + } + /// /// Loads a from the binary reader. /// The format identifier has already been read. @@ -209,5 +233,11 @@ private EmptyCoverageTable() /// public override int CoverageIndexOf(ushort glyphId) => -1; + + /// + public override void CollectDigest(ref GlyphSetDigest digest) + { + // This table never matches a glyph, so contributing nothing is exact. + } } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupListTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupListTable.cs index fc0f6a60d..a28dc7569 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupListTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupListTable.cs @@ -92,6 +92,17 @@ private LookupTable( this.LookupFlags = lookupFlags; this.MarkFilteringSet = markFilteringSet; this.LookupSubTables = lookupSubTables; + + // The union of every subtable's gating coverage: a glyph outside the digest + // cannot be affected by any subtable of this lookup, so application skips it + // without touching the subtables. See GlyphSetDigest for the accuracy contract. + GlyphSetDigest digest = default; + for (int i = 0; i < lookupSubTables.Length; i++) + { + lookupSubTables[i].CollectDigest(ref digest); + } + + this.Digest = digest; } /// @@ -114,6 +125,11 @@ private LookupTable( /// public LookupSubTable[] LookupSubTables { get; } + /// + /// Gets the approximate membership filter for the glyphs this lookup can affect. + /// + public GlyphSetDigest Digest { get; } + /// /// Loads the from the specified reader at the given offset. /// @@ -245,6 +261,14 @@ protected LookupSubTable(LookupFlags lookupFlags, ushort markFilteringSet) /// public ushort MarkFilteringSet { get; } + /// + /// Adds the coverage that gates this subtable's applicability to the digest. + /// The default adds every glyph so the lookup is always attempted, the correct + /// conservative behavior for subtables whose gating coverage is unknown. + /// + /// The digest to add to. + public virtual void CollectDigest(ref GlyphSetDigest digest) => digest.AddAll(); + /// /// Attempts to update the position of glyphs in the collection at the specified index. /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType1SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType1SubTable.cs index b60e4c1aa..4f24f9fb2 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType1SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType1SubTable.cs @@ -90,6 +90,9 @@ public static LookupType1Format1SubTable Load(BigEndianBinaryReader reader, long return new LookupType1Format1SubTable(valueRecord, coverageTable, lookupFlags, markFilteringSet); } + /// + public override void CollectDigest(ref GlyphSetDigest digest) => this.coverageTable.CollectDigest(ref digest); + /// public override bool TryUpdatePosition( FontMetrics fontMetrics, @@ -181,6 +184,9 @@ public static LookupType1Format2SubTable Load(BigEndianBinaryReader reader, long return new LookupType1Format2SubTable(valueRecords, coverageTable, lookupFlags, markFilteringSet); } + /// + public override void CollectDigest(ref GlyphSetDigest digest) => this.coverageTable.CollectDigest(ref digest); + /// public override bool TryUpdatePosition( FontMetrics fontMetrics, diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType2SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType2SubTable.cs index f67b69b4f..3369781b1 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType2SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType2SubTable.cs @@ -113,6 +113,9 @@ public static LookupType2Format1SubTable Load(BigEndianBinaryReader reader, long return new LookupType2Format1SubTable(coverageTable, pairSets, lookupFlags, markFilteringSet); } + /// + public override void CollectDigest(ref GlyphSetDigest digest) => this.coverageTable.CollectDigest(ref digest); + /// public override bool TryUpdatePosition( FontMetrics fontMetrics, @@ -336,6 +339,9 @@ public static LookupType2Format2SubTable Load(BigEndianBinaryReader reader, long return new LookupType2Format2SubTable(coverageTable, class1Records, classDefTable1, classDefTable2, lookupFlags, markFilteringSet); } + /// + public override void CollectDigest(ref GlyphSetDigest digest) => this.coverageTable.CollectDigest(ref digest); + /// public override bool TryUpdatePosition( FontMetrics fontMetrics, diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType3SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType3SubTable.cs index 38f02f164..ac6b481c9 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType3SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType3SubTable.cs @@ -101,6 +101,9 @@ public static LookupType3Format1SubTable Load(BigEndianBinaryReader reader, long return new LookupType3Format1SubTable(coverageTable, entryExitAnchors, lookupFlags, markFilteringSet); } + /// + public override void CollectDigest(ref GlyphSetDigest digest) => this.coverageTable.CollectDigest(ref digest); + /// public override bool TryUpdatePosition( FontMetrics fontMetrics, diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType4SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType4SubTable.cs index 5e32c5ff4..8aa71b6d5 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType4SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType4SubTable.cs @@ -109,6 +109,9 @@ public static LookupType4Format1SubTable Load(BigEndianBinaryReader reader, long return new LookupType4Format1SubTable(markCoverage, baseCoverage, markArrayTable, baseArrayTable, lookupFlags, markFilteringSet); } + /// + public override void CollectDigest(ref GlyphSetDigest digest) => this.markCoverage.CollectDigest(ref digest); + /// public override bool TryUpdatePosition( FontMetrics fontMetrics, diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType5SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType5SubTable.cs index 3ccd01b93..bc9402360 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType5SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType5SubTable.cs @@ -112,6 +112,9 @@ public static LookupType5Format1SubTable Load(BigEndianBinaryReader reader, long return new LookupType5Format1SubTable(markCoverage, ligatureCoverage, markArrayTable, ligatureArrayTable, lookupFlags, markFilteringSet); } + /// + public override void CollectDigest(ref GlyphSetDigest digest) => this.markCoverage.CollectDigest(ref digest); + /// public override bool TryUpdatePosition( FontMetrics fontMetrics, diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType6SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType6SubTable.cs index 92a7110df..fc3bae2b3 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType6SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType6SubTable.cs @@ -111,6 +111,9 @@ public static LookupType6Format1SubTable Load(BigEndianBinaryReader reader, long return new LookupType6Format1SubTable(mark1Coverage, mark2Coverage, mark1ArrayTable, mark2ArrayTable, lookupFlags, markFilteringSet); } + /// + public override void CollectDigest(ref GlyphSetDigest digest) => this.mark1Coverage.CollectDigest(ref digest); + /// public override bool TryUpdatePosition( FontMetrics fontMetrics, diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType7SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType7SubTable.cs index c92ce602e..4c9bf5435 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType7SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType7SubTable.cs @@ -74,6 +74,9 @@ public static LookupType7Format1SubTable Load(BigEndianBinaryReader reader, long return new LookupType7Format1SubTable(coverageTable, seqRuleSets, lookupFlags, markFilteringSet); } + /// + public override void CollectDigest(ref GlyphSetDigest digest) => this.coverageTable.CollectDigest(ref digest); + /// public override bool TryUpdatePosition( FontMetrics fontMetrics, @@ -180,6 +183,9 @@ public static LookupType7Format2SubTable Load(BigEndianBinaryReader reader, long return new LookupType7Format2SubTable(coverageTable, classDefTable, classSeqRuleSets, lookupFlags, markFilteringSet); } + /// + public override void CollectDigest(ref GlyphSetDigest digest) => this.coverageTable.CollectDigest(ref digest); + /// public override bool TryUpdatePosition( FontMetrics fontMetrics, @@ -281,6 +287,20 @@ public static LookupType7Format3SubTable Load(BigEndianBinaryReader reader, long return new LookupType7Format3SubTable(coverageTables, seqLookupRecords, lookupFlags, markFilteringSet); } + /// + public override void CollectDigest(ref GlyphSetDigest digest) + { + if (this.coverageTables.Length == 0) + { + // Degenerate table data: without a first position coverage the gate + // cannot be known, so the lookup is always attempted. + digest.AddAll(); + return; + } + + this.coverageTables[0].CollectDigest(ref digest); + } + /// public override bool TryUpdatePosition( FontMetrics fontMetrics, diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType8SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType8SubTable.cs index bac0b6d63..00d76178e 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType8SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType8SubTable.cs @@ -76,6 +76,9 @@ public static LookupType8Format1SubTable Load(BigEndianBinaryReader reader, long return new LookupType8Format1SubTable(coverageTable, seqRuleSets, lookupFlags, markFilteringSet); } + /// + public override void CollectDigest(ref GlyphSetDigest digest) => this.coverageTable.CollectDigest(ref digest); + /// public override bool TryUpdatePosition( FontMetrics fontMetrics, @@ -207,6 +210,9 @@ public static LookupType8Format2SubTable Load(BigEndianBinaryReader reader, long markFilteringSet); } + /// + public override void CollectDigest(ref GlyphSetDigest digest) => this.coverageTable.CollectDigest(ref digest); + /// public override bool TryUpdatePosition( FontMetrics fontMetrics, @@ -333,6 +339,20 @@ public static LookupType8Format3SubTable Load(BigEndianBinaryReader reader, long markFilteringSet); } + /// + public override void CollectDigest(ref GlyphSetDigest digest) + { + if (this.inputCoverageTables.Length == 0) + { + // Degenerate table data: without a first position coverage the gate + // cannot be known, so the lookup is always attempted. + digest.AddAll(); + return; + } + + this.inputCoverageTables[0].CollectDigest(ref digest); + } + /// public override bool TryUpdatePosition( FontMetrics fontMetrics, diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs index d2241c8d5..1b7598593 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs @@ -25,6 +25,11 @@ internal class GPosTable : Table /// private static readonly Tag VKernTag = Tag.Parse("vkrn"); + /// + /// The invalid but widely shipped language system record tag 'dflt'. + /// + private static readonly Tag DefaultLangSysTag = Tag.Parse("dflt"); + /// /// The OpenType table tag for the GPOS table. /// @@ -222,12 +227,28 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas stage.PreProcessFeature(collection, index, count); Tag featureTag = stage.FeatureTag; - if (this.TryGetFeatureLookups(fontMetrics, in featureTag, current, out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? lookups)) + var lookupProbe = ShapingProbe.Enter(); + bool found = this.TryGetFeatureLookups(fontMetrics, in featureTag, current, collection.LanguageTags, out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? lookups); + ShapingProbe.Exit(ShapingProbe.LookupResolve, lookupProbe); + if (found && lookups is not null) { // Apply features in order. foreach ((Tag Feature, ushort Index, LookupTable LookupTable) featureLookup in lookups) { Tag feature = featureLookup.Feature; + + // Skip the whole lookup when its coverage cannot intersect any + // glyph id the collection has ever contained; most fonts carry + // many lookups for glyphs a given text never produces. + if (!featureLookup.LookupTable.Digest.MightIntersect(collection.GlyphDigest)) + { + continue; + } + + // Resolve the feature's mask bit once per lookup; the per-glyph + // gate below is then a single bitwise AND against the glyph's + // enabled mask. + ulong featureMask = collection.FeatureMap.GetMask(feature); LookupTable featureLookupTable = featureLookup.LookupTable; iterator.Reset(index, featureLookupTable.LookupFlags, featureLookupTable.MarkFilteringSet); @@ -239,7 +260,7 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas goto EndLookups; } - if (!collection[iterator.Index].EnabledFeatureTags.Contains(feature)) + if ((collection[iterator.Index].FeatureMask & featureMask) == 0) { iterator.Next(); continue; @@ -276,17 +297,22 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas } /// - /// Tries to get the feature lookups for the given stage feature and script. + /// Tries to get the feature lookups for the given stage feature, script, and language. /// /// The font metrics. /// The feature tag for the current shaping stage. /// The script class. + /// + /// The candidate OpenType language system tags, most specific first. An empty array + /// selects the default language system. + /// /// When this method returns, contains the list of feature lookups if found. /// if lookups were found; otherwise, . private bool TryGetFeatureLookups( FontMetrics fontMetrics, in Tag stageFeature, ScriptClass script, + Tag[] languageTags, [NotNullWhen(true)] out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? value) { if (this.ScriptList is null) @@ -299,6 +325,9 @@ private bool TryGetFeatureLookups( FeatureTableSubstitutionRecord[]? substitutions = this.FeatureVariations ?.FindMatchingSubstitutions(fontMetrics.GetNormalizedCoordinates()); + // Step 1: script selection. Map the Unicode script class onto the font's + // script table, falling back to the font's first script when the font does not + // declare the script. ScriptListTable scriptListTable = this.ScriptList.Default(); Tag[] tags = UnicodeScriptTagMap.Instance[script]; for (int i = 0; i < tags.Length; i++) @@ -310,6 +339,42 @@ private bool TryGetFeatureLookups( } } + // Step 2: language selection. Walk the culture's candidate tags in priority + // order, most specific first, scanning the script's named language systems for + // a tag match; the first candidate the font declares wins, so a zh-HK run + // selects ZHH before ZHT. A match commits even when the selected language + // system lacks this stage feature: per the specification a LangSys table is the + // complete feature set for its language, so falling through to the default + // below would merge two languages' features, exactly the output language + // systems exist to prevent. An empty candidate array skips this step entirely. + LangSysTable[] langSysTables = scriptListTable.LangSysTables; + for (int i = 0; i < languageTags.Length; i++) + { + uint language = languageTags[i].Value; + for (int j = 0; j < langSysTables.Length; j++) + { + if (langSysTables[j].LangSysTag == language) + { + value = this.GetFeatureLookups(stageFeature, substitutions, langSysTables[j]); + return value.Count > 0; + } + } + } + + // Step 3: no culture, or no candidate the font declares. A language system + // record explicitly tagged dflt is preferred over the true default: the tag is + // invalid per the specification, but fonts built from old documentation typos + // carry one, and the reference engines honor it. + LangSysTable[] langSysRecords = scriptListTable.LangSysTables; + for (int i = 0; i < langSysRecords.Length; i++) + { + if (langSysRecords[i].LangSysTag == DefaultLangSysTag.Value) + { + value = this.GetFeatureLookups(stageFeature, substitutions, langSysRecords[i]); + return value.Count > 0; + } + } + LangSysTable? defaultLangSysTable = scriptListTable.DefaultLangSysTable; if (defaultLangSysTable != null) { @@ -317,8 +382,13 @@ private bool TryGetFeatureLookups( return value.Count > 0; } - value = this.GetFeatureLookups(stageFeature, substitutions, scriptListTable.LangSysTables); - return value.Count > 0; + // Step 4: no default language system either. Nothing applies: the font scoped + // every feature to specific languages, and the reference engines agree that no + // language system means no lookups. Features such as SimSun's vertical + // alternates, which live only under its Chinese language systems, are reached by + // setting TextOptions.Culture to a Chinese culture. + value = null; + return false; } /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs index b221bc9aa..5d7cdf146 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs @@ -92,6 +92,17 @@ private LookupTable( this.LookupFlags = lookupFlags; this.MarkFilteringSet = markFilteringSet; this.LookupSubTables = lookupSubTables; + + // The union of every subtable's gating coverage: a glyph outside the digest + // cannot be affected by any subtable of this lookup, so application skips it + // without touching the subtables. See GlyphSetDigest for the accuracy contract. + GlyphSetDigest digest = default; + for (int i = 0; i < lookupSubTables.Length; i++) + { + lookupSubTables[i].CollectDigest(ref digest); + } + + this.Digest = digest; } /// @@ -115,6 +126,11 @@ private LookupTable( /// public LookupSubTable[] LookupSubTables { get; } + /// + /// Gets the approximate membership filter for the glyphs this lookup can affect. + /// + public GlyphSetDigest Digest { get; } + /// /// Loads the from the binary reader at the given offset. /// @@ -252,6 +268,14 @@ protected LookupSubTable(LookupFlags lookupFlags, ushort markFilteringSet) /// public ushort MarkFilteringSet { get; } + /// + /// Adds the coverage that gates this subtable's applicability to the digest. + /// The default adds every glyph so the lookup is always attempted, the correct + /// conservative behavior for subtables whose gating coverage is unknown. + /// + /// The digest to add to. + public virtual void CollectDigest(ref GlyphSetDigest digest) => digest.AddAll(); + /// /// Attempts to perform a glyph substitution at the specified index in the collection. /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs index 8b83cac8f..6a8b9735a 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs @@ -93,7 +93,10 @@ public static LookupType1Format1SubTable Load(BigEndianBinaryReader reader, long return new LookupType1Format1SubTable(deltaGlyphId, coverageTable, lookupFlags, markFilteringSet); } - /// + /// + public override void CollectDigest(ref GlyphSetDigest digest) => this.coverageTable.CollectDigest(ref digest); + + /// public override bool TrySubstitution( FontMetrics fontMetrics, GSubTable table, @@ -180,7 +183,10 @@ public static LookupType1Format2SubTable Load(BigEndianBinaryReader reader, long return new LookupType1Format2SubTable(substituteGlyphIds, coverageTable, lookupFlags, markFilteringSet); } - /// + /// + public override void CollectDigest(ref GlyphSetDigest digest) => this.coverageTable.CollectDigest(ref digest); + + /// public override bool TrySubstitution( FontMetrics fontMetrics, GSubTable table, diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs index 28b04e07d..737bd3426 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs @@ -115,7 +115,10 @@ public static LookupType2Format1SubTable Load(BigEndianBinaryReader reader, long return new LookupType2Format1SubTable(sequenceTables, coverageTable, lookupFlags, markFilteringSet); } - /// + /// + public override void CollectDigest(ref GlyphSetDigest digest) => this.coverageTable.CollectDigest(ref digest); + + /// public override bool TrySubstitution( FontMetrics fontMetrics, GSubTable table, diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs index 8cae811f1..78cf9649f 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs @@ -114,7 +114,10 @@ public static LookupType3Format1SubTable Load(BigEndianBinaryReader reader, long return new LookupType3Format1SubTable(alternateTables, coverageTable, lookupFlags, markFilteringSet); } - /// + /// + public override void CollectDigest(ref GlyphSetDigest digest) => this.coverageTable.CollectDigest(ref digest); + + /// public override bool TrySubstitution( FontMetrics fontMetrics, GSubTable table, diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs index 94dee6362..cd052f175 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs @@ -145,7 +145,10 @@ public static LookupType4Format1SubTable Load(BigEndianBinaryReader reader, long return new LookupType4Format1SubTable(ligatureSetTables, coverageTable, lookupFlags, markFilteringSet); } - /// + /// + public override void CollectDigest(ref GlyphSetDigest digest) => this.coverageTable.CollectDigest(ref digest); + + /// public override bool TrySubstitution( FontMetrics fontMetrics, GSubTable table, diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs index ef772d12f..ad2bb34e2 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs @@ -79,7 +79,10 @@ public static LookupType5Format1SubTable Load(BigEndianBinaryReader reader, long return new LookupType5Format1SubTable(coverageTable, seqRuleSets, lookupFlags, markFilteringSet); } - /// + /// + public override void CollectDigest(ref GlyphSetDigest digest) => this.coverageTable.CollectDigest(ref digest); + + /// public override bool TrySubstitution( FontMetrics fontMetrics, GSubTable table, @@ -193,7 +196,10 @@ public static LookupType5Format2SubTable Load(BigEndianBinaryReader reader, long return new LookupType5Format2SubTable(classSeqRuleSets, classDefTable, coverageTable, lookupFlags, markFilteringSet); } - /// + /// + public override void CollectDigest(ref GlyphSetDigest digest) => this.coverageTable.CollectDigest(ref digest); + + /// public override bool TrySubstitution( FontMetrics fontMetrics, GSubTable table, @@ -309,7 +315,21 @@ public static LookupType5Format3SubTable Load(BigEndianBinaryReader reader, long return new LookupType5Format3SubTable(coverageTables, seqLookupRecords, lookupFlags, markFilteringSet); } - /// + /// + public override void CollectDigest(ref GlyphSetDigest digest) + { + if (this.coverageTables.Length == 0) + { + // Degenerate table data: without a first position coverage the gate + // cannot be known, so the lookup is always attempted. + digest.AddAll(); + return; + } + + this.coverageTables[0].CollectDigest(ref digest); + } + + /// public override bool TrySubstitution( FontMetrics fontMetrics, GSubTable table, diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs index 8b377b959..80f843183 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs @@ -82,7 +82,10 @@ public static LookupType6Format1SubTable Load(BigEndianBinaryReader reader, long return new LookupType6Format1SubTable(coverageTable, seqRuleSets, lookupFlags, markFilteringSet); } - /// + /// + public override void CollectDigest(ref GlyphSetDigest digest) => this.coverageTable.CollectDigest(ref digest); + + /// public override bool TrySubstitution( FontMetrics fontMetrics, GSubTable table, @@ -226,7 +229,10 @@ public static LookupType6Format2SubTable Load(BigEndianBinaryReader reader, long markFilteringSet); } - /// + /// + public override void CollectDigest(ref GlyphSetDigest digest) => this.coverageTable.CollectDigest(ref digest); + + /// public override bool TrySubstitution( FontMetrics fontMetrics, GSubTable table, @@ -362,7 +368,21 @@ public static LookupType6Format3SubTable Load(BigEndianBinaryReader reader, long markFilteringSet); } - /// + /// + public override void CollectDigest(ref GlyphSetDigest digest) + { + if (this.inputCoverageTables.Length == 0) + { + // Degenerate table data: without a first position coverage the gate + // cannot be known, so the lookup is always attempted. + digest.AddAll(); + return; + } + + this.inputCoverageTables[0].CollectDigest(ref digest); + } + + /// public override bool TrySubstitution( FontMetrics fontMetrics, GSubTable table, diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType8SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType8SubTable.cs index e2e42fe84..beca5b323 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType8SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType8SubTable.cs @@ -148,7 +148,10 @@ public static LookupType8Format1SubTable Load(BigEndianBinaryReader reader, long markFilteringSet); } - /// + /// + public override void CollectDigest(ref GlyphSetDigest digest) => this.coverageTable.CollectDigest(ref digest); + + /// public override bool TrySubstitution( FontMetrics fontMetrics, GSubTable table, diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs index 2bc7d3aa3..83d264ab6 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs @@ -20,6 +20,11 @@ internal class GSubTable : Table /// internal const string TableName = "GSUB"; + /// + /// The invalid but widely shipped language system record tag 'dflt'. + /// + private static readonly Tag DefaultLangSysTag = Tag.Parse("dflt"); + /// /// Initializes a new instance of the class. /// @@ -260,12 +265,27 @@ internal void ApplyFeature( int maxOperationsCount, ref int currentOperations) { - if (this.TryGetFeatureLookups(fontMetrics, in featureTag, current, out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? lookups)) + var lookupProbe = ShapingProbe.Enter(); + bool found = this.TryGetFeatureLookups(fontMetrics, in featureTag, current, collection.LanguageTags, out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? lookups); + ShapingProbe.Exit(ShapingProbe.LookupResolve, lookupProbe); + if (found && lookups is not null) { // Apply features in order. foreach ((Tag Feature, ushort Index, LookupTable LookupTable) featureLookup in lookups) { Tag feature = featureLookup.Feature; + + // Skip the whole lookup when its coverage cannot intersect any glyph id + // the collection has ever contained; most fonts carry many lookups for + // glyphs a given text never produces. + if (!featureLookup.LookupTable.Digest.MightIntersect(collection.GlyphDigest)) + { + continue; + } + + // Resolve the feature's mask bit once per lookup; the per-glyph gate + // below is then a single bitwise AND against the glyph's enabled mask. + ulong featureMask = collection.FeatureMap.GetMask(feature); LookupTable featureLookupTable = featureLookup.LookupTable; iterator.Reset(index, featureLookupTable.LookupFlags, featureLookupTable.MarkFilteringSet); @@ -276,7 +296,7 @@ internal void ApplyFeature( return; } - if (!collection[iterator.Index].EnabledFeatureTags.Contains(feature)) + if ((collection[iterator.Index].FeatureMask & featureMask) == 0) { iterator.Next(); continue; @@ -296,17 +316,22 @@ internal void ApplyFeature( } /// - /// Tries to get the feature lookups for the given stage feature and script. + /// Tries to get the feature lookups for the given stage feature, script, and language. /// /// The font metrics. /// The feature tag for the current shaping stage. /// The script class. + /// + /// The candidate OpenType language system tags, most specific first. An empty array + /// selects the default language system. + /// /// When this method returns, contains the list of feature lookups if found. /// if lookups were found; otherwise, . internal bool TryGetFeatureLookups( FontMetrics fontMetrics, in Tag stageFeature, ScriptClass script, + Tag[] languageTags, [NotNullWhen(true)] out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? value) { if (this.ScriptList is null) @@ -319,6 +344,9 @@ internal bool TryGetFeatureLookups( FeatureTableSubstitutionRecord[]? substitutions = this.FeatureVariations ?.FindMatchingSubstitutions(fontMetrics.GetNormalizedCoordinates()); + // Step 1: script selection. Map the Unicode script class onto the font's + // script table, falling back to the font's first script when the font does not + // declare the script. ScriptListTable scriptListTable = this.ScriptList.Default(); Tag[] tags = UnicodeScriptTagMap.Instance[script]; for (int i = 0; i < tags.Length; i++) @@ -330,6 +358,42 @@ internal bool TryGetFeatureLookups( } } + // Step 2: language selection. Walk the culture's candidate tags in priority + // order, most specific first, scanning the script's named language systems for + // a tag match; the first candidate the font declares wins, so a zh-HK run + // selects ZHH before ZHT. A match commits even when the selected language + // system lacks this stage feature: per the specification a LangSys table is the + // complete feature set for its language, so falling through to the default + // below would merge two languages' features, exactly the output language + // systems exist to prevent. An empty candidate array skips this step entirely. + LangSysTable[] langSysTables = scriptListTable.LangSysTables; + for (int i = 0; i < languageTags.Length; i++) + { + uint language = languageTags[i].Value; + for (int j = 0; j < langSysTables.Length; j++) + { + if (langSysTables[j].LangSysTag == language) + { + value = this.GetFeatureLookups(stageFeature, substitutions, langSysTables[j]); + return value.Count > 0; + } + } + } + + // Step 3: no culture, or no candidate the font declares. A language system + // record explicitly tagged dflt is preferred over the true default: the tag is + // invalid per the specification, but fonts built from old documentation typos + // carry one, and the reference engines honor it. + LangSysTable[] langSysRecords = scriptListTable.LangSysTables; + for (int i = 0; i < langSysRecords.Length; i++) + { + if (langSysRecords[i].LangSysTag == DefaultLangSysTag.Value) + { + value = this.GetFeatureLookups(stageFeature, substitutions, langSysRecords[i]); + return value.Count > 0; + } + } + LangSysTable? defaultLangSysTable = scriptListTable.DefaultLangSysTable; if (defaultLangSysTable != null) { @@ -337,8 +401,13 @@ internal bool TryGetFeatureLookups( return value.Count > 0; } - value = this.GetFeatureLookups(stageFeature, substitutions, scriptListTable.LangSysTables); - return value.Count > 0; + // Step 4: no default language system either. Nothing applies: the font scoped + // every feature to specific languages, and the reference engines agree that no + // language system means no lookups. Features such as SimSun's vertical + // alternates, which live only under its Chinese language systems, are reached by + // setting TextOptions.Culture to a Chinese culture. + value = null; + return false; } /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GlyphSetDigest.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GlyphSetDigest.cs new file mode 100644 index 000000000..8de30b1e5 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GlyphSetDigest.cs @@ -0,0 +1,114 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.AdvancedTypographic; + +/// +/// An approximate glyph membership filter for lookup coverage. +/// +/// +/// +/// Conceptually a tiny Bloom filter tuned for glyph coverage queries: three 64 bit +/// words, each indexing the glyph id at a different shift so ids cluster into different +/// buckets per word. A glyph "may" be in the set only when all three words contain its +/// bucket bit. False positives fall through to the lookup's exact coverage test, so they +/// cost only what the pre-digest code always paid; false negatives cannot occur. +/// +/// +/// The filter is highly accurate when a lookup covers a local cluster of glyph ids, +/// which is the common case for real fonts, and degrades to always-maybe when coverage +/// is spread across the id space. The three shifts bucket runs of 16 ids, single ids, +/// and runs of 64 ids respectively. +/// +/// +internal struct GlyphSetDigest +{ + private const int BitsMinusOne = 63; + private ulong mask0; + private ulong mask1; + private ulong mask2; + + /// + /// Adds a single glyph id to the digest. + /// + /// The glyph id. + public void Add(ushort glyphId) + { + this.mask0 |= 1UL << ((glyphId >> 4) & BitsMinusOne); + this.mask1 |= 1UL << (glyphId & BitsMinusOne); + this.mask2 |= 1UL << ((glyphId >> 6) & BitsMinusOne); + } + + /// + /// Adds an inclusive range of glyph ids to the digest. + /// + /// The first glyph id in the range. + /// The last glyph id in the range. + public void AddRange(ushort start, ushort end) + { + this.mask0 = AddRange(this.mask0, start, end, 4); + this.mask1 = AddRange(this.mask1, start, end, 0); + this.mask2 = AddRange(this.mask2, start, end, 6); + } + + /// + /// Marks the digest as containing every glyph, used when a subtable's gating + /// coverage is unknown so the lookup is always attempted, matching the behavior + /// before digests existed. + /// + public void AddAll() + { + this.mask0 = ulong.MaxValue; + this.mask1 = ulong.MaxValue; + this.mask2 = ulong.MaxValue; + } + + /// + /// Gets a value indicating whether the glyph may be a member of the digested set. + /// A false result is definitive; a true result must be confirmed by the lookup's + /// exact coverage test. + /// + /// The glyph id. + /// when the glyph is definitely absent. + public readonly bool MightContain(ushort glyphId) + => (this.mask0 & (1UL << ((glyphId >> 4) & BitsMinusOne))) != 0 + && (this.mask1 & (1UL << (glyphId & BitsMinusOne))) != 0 + && (this.mask2 & (1UL << ((glyphId >> 6) & BitsMinusOne))) != 0; + + /// + /// Gets a value indicating whether this digest and may + /// share a member: every word pair must share at least one bucket bit. A false + /// result proves the underlying sets are disjoint; a true result is approximate. + /// + /// The other digest. + /// when the sets are definitely disjoint. + public readonly bool MightIntersect(in GlyphSetDigest other) + => (this.mask0 & other.mask0) != 0 + && (this.mask1 & other.mask1) != 0 + && (this.mask2 & other.mask2) != 0; + + /// + /// Sets every bucket bit from through + /// at the given shift, saturating the word when the range spans all buckets. + /// + /// The current word. + /// The first glyph id in the range. + /// The last glyph id in the range. + /// The bucket shift for this word. + /// The updated word. + private static ulong AddRange(ulong mask, ushort start, ushort end, int shift) + { + if ((end >> shift) - (start >> shift) >= BitsMinusOne) + { + return ulong.MaxValue; + } + + // Sets the contiguous bucket bits from start through end inclusive, wrapping + // within the word: with mb >= ma the expression is (mb << 1) - ma, the bits + // ma..mb; with mb < ma the unsigned wrap of (mb - ma) plus the borrowed 1 + // produces the two runs ma..63 and 0..mb. + ulong ma = 1UL << ((start >> shift) & BitsMinusOne); + ulong mb = 1UL << ((end >> shift) & BitsMinusOne); + return mask | unchecked(mb + (mb - ma) - (mb < ma ? 1UL : 0UL)); + } +} diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/OpenTypeLanguageTagMap.Generated.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/OpenTypeLanguageTagMap.Generated.cs index 94205a53a..a99b0fbb5 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/OpenTypeLanguageTagMap.Generated.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/OpenTypeLanguageTagMap.Generated.cs @@ -27,7 +27,7 @@ private static Dictionary CreateIsoLanguageMap() { "abq", new[] { Tag.Parse("ABA ") } }, // Abaza { "abs", new[] { Tag.Parse("CPP ") } }, // Creoles { "abv", new[] { Tag.Parse("ARA ") } }, // Arabic - { "acf", new[] { Tag.Parse("CPP "), Tag.Parse("FAN ") } }, // Creoles; French Antillean + { "acf", new[] { Tag.Parse("FAN "), Tag.Parse("CPP ") } }, // French Antillean; Creoles { "ach", new[] { Tag.Parse("ACH ") } }, // Acholi { "acm", new[] { Tag.Parse("ARA ") } }, // Arabic { "acq", new[] { Tag.Parse("ARA ") } }, // Arabic @@ -181,11 +181,11 @@ private static Dictionary CreateIsoLanguageMap() { "btd", new[] { Tag.Parse("BTD "), Tag.Parse("BTK ") } }, // Batak Dairi (Pakpak); Batak languages { "btj", new[] { Tag.Parse("MLY ") } }, // Malay { "btk", new[] { Tag.Parse("BTK ") } }, // Batak languages - { "btm", new[] { Tag.Parse("BTK "), Tag.Parse("BTM ") } }, // Batak languages; Batak Mandailing + { "btm", new[] { Tag.Parse("BTM "), Tag.Parse("BTK ") } }, // Batak Mandailing; Batak languages { "bto", new[] { Tag.Parse("BIK ") } }, // Bikol - { "bts", new[] { Tag.Parse("BTK "), Tag.Parse("BTS ") } }, // Batak languages; Batak Simalungun - { "btx", new[] { Tag.Parse("BTK "), Tag.Parse("BTX ") } }, // Batak languages; Batak Karo - { "btz", new[] { Tag.Parse("BTK "), Tag.Parse("BTZ ") } }, // Batak languages; Batak Alas-Kluet + { "bts", new[] { Tag.Parse("BTS "), Tag.Parse("BTK ") } }, // Batak Simalungun; Batak languages + { "btx", new[] { Tag.Parse("BTX "), Tag.Parse("BTK ") } }, // Batak Karo; Batak languages + { "btz", new[] { Tag.Parse("BTZ "), Tag.Parse("BTK ") } }, // Batak Alas-Kluet; Batak languages { "bug", new[] { Tag.Parse("BUG ") } }, // Bugis { "bul", new[] { Tag.Parse("BGR ") } }, // Bulgarian { "bum", new[] { Tag.Parse("BTI ") } }, // Beti @@ -197,13 +197,13 @@ private static Dictionary CreateIsoLanguageMap() { "bxp", new[] { Tag.Parse("BTI ") } }, // Beti { "bxr", new[] { Tag.Parse("RBU ") } }, // Russian Buriat { "byn", new[] { Tag.Parse("BIL ") } }, // Bilen - { "byv", new[] { Tag.Parse("BML "), Tag.Parse("BYV ") } }, // Bamileke; Medumba + { "byv", new[] { Tag.Parse("BYV "), Tag.Parse("BML ") } }, // Medumba; Bamileke { "bzc", new[] { Tag.Parse("MLG ") } }, // Malagasy { "bzj", new[] { Tag.Parse("CPP ") } }, // Creoles { "bzk", new[] { Tag.Parse("CPP ") } }, // Creoles { "caa", new[] { Tag.Parse("MYN ") } }, // Mayan { "cac", new[] { Tag.Parse("MYN ") } }, // Mayan - { "caf", new[] { Tag.Parse("ATH "), Tag.Parse("CRR ") } }, // Athapaskan languages; Carrier + { "caf", new[] { Tag.Parse("CRR "), Tag.Parse("ATH ") } }, // Carrier; Athapaskan languages { "cak", new[] { Tag.Parse("CAK "), Tag.Parse("MYN ") } }, // Kaqchikel; Mayan { "cat", new[] { Tag.Parse("CAT ") } }, // Catalan { "cay", new[] { Tag.Parse("CAY ") } }, // Cayuga @@ -214,6 +214,7 @@ private static Dictionary CreateIsoLanguageMap() { "ccm", new[] { Tag.Parse("CPP ") } }, // Creoles { "cco", new[] { Tag.Parse("CCHN") } }, // Chinantec { "ccq", new[] { Tag.Parse("ARK ") } }, // Rakhine + { "cdo", new[] { Tag.Parse("ZHS ") } }, // Chinese, Simplified { "ceb", new[] { Tag.Parse("CEB ") } }, // Cebuano { "cek", new[] { Tag.Parse("QIN ") } }, // Chin { "ces", new[] { Tag.Parse("CSY ") } }, // Czech @@ -227,7 +228,7 @@ private static Dictionary CreateIsoLanguageMap() { "chk", new[] { Tag.Parse("CHK0") } }, // Chuukese { "chn", new[] { Tag.Parse("CPP ") } }, // Creoles { "cho", new[] { Tag.Parse("CHO ") } }, // Choctaw - { "chp", new[] { Tag.Parse("ATH "), Tag.Parse("CHP "), Tag.Parse("SAY ") } }, // Athapaskan languages; Chipewyan; Sayisi + { "chp", new[] { Tag.Parse("CHP "), Tag.Parse("SAY "), Tag.Parse("ATH ") } }, // Chipewyan; Sayisi; Athapaskan languages { "chq", new[] { Tag.Parse("CCHN") } }, // Chinantec { "chr", new[] { Tag.Parse("CHR ") } }, // Cherokee { "chu", new[] { Tag.Parse("CSL ") } }, // Church Slavonic @@ -237,6 +238,7 @@ private static Dictionary CreateIsoLanguageMap() { "ciw", new[] { Tag.Parse("OJB ") } }, // Ojibway { "cja", new[] { Tag.Parse("CJA ") } }, // Western Cham { "cjm", new[] { Tag.Parse("CJM ") } }, // Eastern Cham + { "cjy", new[] { Tag.Parse("ZHS ") } }, // Chinese, Simplified { "cka", new[] { Tag.Parse("QIN ") } }, // Chin { "ckb", new[] { Tag.Parse("KUR ") } }, // Kurdish { "ckn", new[] { Tag.Parse("QIN ") } }, // Chin @@ -255,6 +257,7 @@ private static Dictionary CreateIsoLanguageMap() { "cnh", new[] { Tag.Parse("QIN ") } }, // Chin { "cnk", new[] { Tag.Parse("QIN ") } }, // Chin { "cnl", new[] { Tag.Parse("CCHN") } }, // Chinantec + { "cnp", new[] { Tag.Parse("ZHS ") } }, // Chinese, Simplified { "cnr", new[] { Tag.Parse("SRB ") } }, // Serbian { "cnt", new[] { Tag.Parse("CCHN") } }, // Chinantec { "cnu", new[] { Tag.Parse("BBR ") } }, // Berber @@ -271,6 +274,7 @@ private static Dictionary CreateIsoLanguageMap() { "cpf", new[] { Tag.Parse("CPP ") } }, // Creoles { "cpi", new[] { Tag.Parse("CPP ") } }, // Creoles { "cpp", new[] { Tag.Parse("CPP ") } }, // Creoles + { "cpx", new[] { Tag.Parse("ZHS ") } }, // Chinese, Simplified { "cqd", new[] { Tag.Parse("HMN ") } }, // Hmong { "cqu", new[] { Tag.Parse("QUH ") } }, // Quechua (Bolivia) { "cre", new[] { Tag.Parse("CRE ") } }, // Cree @@ -282,12 +286,13 @@ private static Dictionary CreateIsoLanguageMap() { "crm", new[] { Tag.Parse("LCR "), Tag.Parse("MCR ") } }, // L-Cree; Moose Cree { "crp", new[] { Tag.Parse("CPP ") } }, // Creoles { "crs", new[] { Tag.Parse("CPP ") } }, // Creoles - { "crx", new[] { Tag.Parse("ATH "), Tag.Parse("CRR ") } }, // Athapaskan languages; Carrier + { "crx", new[] { Tag.Parse("CRR "), Tag.Parse("ATH ") } }, // Carrier; Athapaskan languages { "csa", new[] { Tag.Parse("CCHN") } }, // Chinantec { "csb", new[] { Tag.Parse("CSB ") } }, // Kashubian { "csh", new[] { Tag.Parse("QIN ") } }, // Chin { "csj", new[] { Tag.Parse("QIN ") } }, // Chin { "cso", new[] { Tag.Parse("CCHN") } }, // Chinantec + { "csp", new[] { Tag.Parse("ZHS ") } }, // Chinese, Simplified { "csv", new[] { Tag.Parse("QIN ") } }, // Chin { "csw", new[] { Tag.Parse("NCR "), Tag.Parse("NHC ") } }, // N-Cree; Norway House Cree { "csy", new[] { Tag.Parse("QIN ") } }, // Chin @@ -306,6 +311,8 @@ private static Dictionary CreateIsoLanguageMap() { "cvn", new[] { Tag.Parse("CCHN") } }, // Chinantec { "cwd", new[] { Tag.Parse("DCR "), Tag.Parse("TCR ") } }, // Woods Cree; TH-Cree { "cym", new[] { Tag.Parse("WEL ") } }, // Welsh + { "czh", new[] { Tag.Parse("ZHS ") } }, // Chinese, Simplified + { "czo", new[] { Tag.Parse("ZHS ") } }, // Chinese, Simplified { "czt", new[] { Tag.Parse("QIN ") } }, // Chin { "dag", new[] { Tag.Parse("DAG ") } }, // Dagbani { "dan", new[] { Tag.Parse("DAN ") } }, // Danish @@ -314,7 +321,7 @@ private static Dictionary CreateIsoLanguageMap() { "dar", new[] { Tag.Parse("DAR ") } }, // Dargwa { "dax", new[] { Tag.Parse("DAX ") } }, // Dayi { "dcr", new[] { Tag.Parse("CPP ") } }, // Creoles - { "den", new[] { Tag.Parse("ATH "), Tag.Parse("SLA ") } }, // Athapaskan languages; Slavey + { "den", new[] { Tag.Parse("SLA "), Tag.Parse("ATH ") } }, // Slavey; Athapaskan languages { "dep", new[] { Tag.Parse("CPP ") } }, // Creoles { "deu", new[] { Tag.Parse("DEU ") } }, // German { "dgo", new[] { Tag.Parse("DGO ") } }, // Dogri (individual language) @@ -335,11 +342,13 @@ private static Dictionary CreateIsoLanguageMap() { "dng", new[] { Tag.Parse("DUN ") } }, // Dungan { "dnj", new[] { Tag.Parse("DNJ ") } }, // Dan { "doi", new[] { Tag.Parse("DGR ") } }, // Dogri (macrolanguage) + { "drh", new[] { Tag.Parse("MNG ") } }, // Mongolian { "drw", new[] { Tag.Parse("DRI ") } }, // Dari { "dsb", new[] { Tag.Parse("LSB ") } }, // Lower Sorbian { "dty", new[] { Tag.Parse("NEP ") } }, // Nepali { "duj", new[] { Tag.Parse("DUJ ") } }, // Dhuwal { "dup", new[] { Tag.Parse("MLY ") } }, // Malay + { "dwk", new[] { Tag.Parse("KUI ") } }, // Kui { "dwu", new[] { Tag.Parse("DUJ ") } }, // Dhuwal { "dwy", new[] { Tag.Parse("DUJ ") } }, // Dhuwal { "dyu", new[] { Tag.Parse("JUL ") } }, // Jula @@ -372,13 +381,13 @@ private static Dictionary CreateIsoLanguageMap() { "fan", new[] { Tag.Parse("FAN0") } }, // Fang { "fao", new[] { Tag.Parse("FOS ") } }, // Faroese { "fas", new[] { Tag.Parse("FAR ") } }, // Persian - { "fat", new[] { Tag.Parse("AKA "), Tag.Parse("FAT ") } }, // Akan; Fanti + { "fat", new[] { Tag.Parse("FAT "), Tag.Parse("AKA ") } }, // Fanti; Akan { "fbl", new[] { Tag.Parse("BIK ") } }, // Bikol { "ffm", new[] { Tag.Parse("FUL ") } }, // Fulah { "fij", new[] { Tag.Parse("FJI ") } }, // Fijian { "fil", new[] { Tag.Parse("PIL ") } }, // Filipino { "fin", new[] { Tag.Parse("FIN ") } }, // Finnish - { "fmp", new[] { Tag.Parse("BML "), Tag.Parse("FMP ") } }, // Bamileke; Feโ€™feโ€™ + { "fmp", new[] { Tag.Parse("FMP "), Tag.Parse("BML ") } }, // Feโ€™feโ€™; Bamileke { "fng", new[] { Tag.Parse("CPP ") } }, // Creoles { "fon", new[] { Tag.Parse("FON ") } }, // Fon { "fpe", new[] { Tag.Parse("CPP ") } }, // Creoles @@ -399,6 +408,7 @@ private static Dictionary CreateIsoLanguageMap() { "gaa", new[] { Tag.Parse("GAD ") } }, // Ga { "gac", new[] { Tag.Parse("CPP ") } }, // Creoles { "gag", new[] { Tag.Parse("GAG ") } }, // Gagauz + { "gan", new[] { Tag.Parse("ZHS ") } }, // Chinese, Simplified { "gax", new[] { Tag.Parse("ORO ") } }, // Oromo { "gaz", new[] { Tag.Parse("ORO ") } }, // Oromo { "gbm", new[] { Tag.Parse("GAW ") } }, // Garhwali @@ -408,6 +418,7 @@ private static Dictionary CreateIsoLanguageMap() { "gcr", new[] { Tag.Parse("CPP ") } }, // Creoles { "gda", new[] { Tag.Parse("RAJ ") } }, // Rajasthani { "gez", new[] { Tag.Parse("GEZ ") } }, // Geez + { "ggo", new[] { Tag.Parse("GON ") } }, // Gondi { "gha", new[] { Tag.Parse("BBR ") } }, // Berber { "ghc", new[] { Tag.Parse("IRT ") } }, // Irish Traditional { "ghk", new[] { Tag.Parse("KRN ") } }, // Karen @@ -452,8 +463,9 @@ private static Dictionary CreateIsoLanguageMap() { "haa", new[] { Tag.Parse("ATH ") } }, // Athapaskan languages { "hae", new[] { Tag.Parse("ORO ") } }, // Oromo { "hai", new[] { Tag.Parse("HAI0") } }, // Haida + { "hak", new[] { Tag.Parse("ZHS ") } }, // Chinese, Simplified { "har", new[] { Tag.Parse("HRI ") } }, // Harari - { "hat", new[] { Tag.Parse("CPP "), Tag.Parse("HAI ") } }, // Creoles; Haitian (Haitian Creole) + { "hat", new[] { Tag.Parse("HAI "), Tag.Parse("CPP ") } }, // Haitian (Haitian Creole); Creoles { "hau", new[] { Tag.Parse("HAU ") } }, // Hausa { "haw", new[] { Tag.Parse("HAW ") } }, // Hawaiian { "hax", new[] { Tag.Parse("HAI0") } }, // Haida @@ -480,7 +492,7 @@ private static Dictionary CreateIsoLanguageMap() { "hml", new[] { Tag.Parse("HMN ") } }, // Hmong { "hmm", new[] { Tag.Parse("HMN ") } }, // Hmong { "hmn", new[] { Tag.Parse("HMN ") } }, // Hmong - { "hmo", new[] { Tag.Parse("CPP "), Tag.Parse("HMO ") } }, // Creoles; Hiri Motu + { "hmo", new[] { Tag.Parse("HMO "), Tag.Parse("CPP ") } }, // Hiri Motu; Creoles { "hmp", new[] { Tag.Parse("HMN ") } }, // Hmong { "hmq", new[] { Tag.Parse("HMN ") } }, // Hmong { "hmr", new[] { Tag.Parse("QIN ") } }, // Chin @@ -491,6 +503,7 @@ private static Dictionary CreateIsoLanguageMap() { "hnd", new[] { Tag.Parse("HND ") } }, // Hindko { "hne", new[] { Tag.Parse("CHH ") } }, // Chattisgarhi { "hnj", new[] { Tag.Parse("HMN ") } }, // Hmong + { "hnm", new[] { Tag.Parse("ZHS ") } }, // Chinese, Simplified { "hno", new[] { Tag.Parse("HND ") } }, // Hindko { "hoc", new[] { Tag.Parse("HO ") } }, // Ho { "hoi", new[] { Tag.Parse("ATH ") } }, // Athapaskan languages @@ -500,13 +513,14 @@ private static Dictionary CreateIsoLanguageMap() { "hrm", new[] { Tag.Parse("HMN ") } }, // Hmong { "hrv", new[] { Tag.Parse("HRV ") } }, // Croatian { "hsb", new[] { Tag.Parse("USB ") } }, // Upper Sorbian + { "hsn", new[] { Tag.Parse("ZHS ") } }, // Chinese, Simplified { "huj", new[] { Tag.Parse("HMN ") } }, // Hmong { "hun", new[] { Tag.Parse("HUN ") } }, // Hungarian { "hup", new[] { Tag.Parse("ATH ") } }, // Athapaskan languages { "hur", new[] { Tag.Parse("HUR ") } }, // Halkomelem { "hus", new[] { Tag.Parse("MYN ") } }, // Mayan { "hwc", new[] { Tag.Parse("CPP ") } }, // Creoles - { "hye", new[] { Tag.Parse("HYE "), Tag.Parse("HYE0") } }, // Armenian; Armenian East + { "hye", new[] { Tag.Parse("HYE0"), Tag.Parse("HYE ") } }, // Armenian East; Armenian { "hyw", new[] { Tag.Parse("HYE ") } }, // Armenian { "iba", new[] { Tag.Parse("IBA ") } }, // Iban { "ibb", new[] { Tag.Parse("IBB ") } }, // Ibibio @@ -544,7 +558,7 @@ private static Dictionary CreateIsoLanguageMap() { "ixl", new[] { Tag.Parse("MYN ") } }, // Mayan { "jac", new[] { Tag.Parse("MYN ") } }, // Mayan { "jak", new[] { Tag.Parse("MLY ") } }, // Malay - { "jam", new[] { Tag.Parse("CPP "), Tag.Parse("JAM ") } }, // Creoles; Jamaican Creole + { "jam", new[] { Tag.Parse("JAM "), Tag.Parse("CPP ") } }, // Jamaican Creole; Creoles { "jav", new[] { Tag.Parse("JAV ") } }, // Javanese { "jax", new[] { Tag.Parse("MLY ") } }, // Malay { "jbe", new[] { Tag.Parse("BBR ") } }, // Berber @@ -560,7 +574,7 @@ private static Dictionary CreateIsoLanguageMap() { "jvd", new[] { Tag.Parse("CPP ") } }, // Creoles { "jw", new[] { Tag.Parse("JAV ") } }, // Javanese { "kaa", new[] { Tag.Parse("KRK ") } }, // Karakalpak - { "kab", new[] { Tag.Parse("BBR "), Tag.Parse("KAB0") } }, // Berber; Kabyle + { "kab", new[] { Tag.Parse("KAB0"), Tag.Parse("BBR ") } }, // Kabyle; Berber { "kal", new[] { Tag.Parse("GRN ") } }, // Greenlandic { "kam", new[] { Tag.Parse("KMB ") } }, // Kamba { "kan", new[] { Tag.Parse("KAN ") } }, // Kannada @@ -578,7 +592,7 @@ private static Dictionary CreateIsoLanguageMap() { "kde", new[] { Tag.Parse("KDE ") } }, // Makonde { "kdr", new[] { Tag.Parse("KRM ") } }, // Karaim { "kdt", new[] { Tag.Parse("KUY ") } }, // Kuy - { "kea", new[] { Tag.Parse("CPP "), Tag.Parse("KEA ") } }, // Creoles; Kabuverdianu (Crioulo) + { "kea", new[] { Tag.Parse("KEA "), Tag.Parse("CPP ") } }, // Kabuverdianu (Crioulo); Creoles { "kek", new[] { Tag.Parse("KEK "), Tag.Parse("MYN ") } }, // Kekchi; Mayan { "kex", new[] { Tag.Parse("KKN ") } }, // Kokni { "kfa", new[] { Tag.Parse("KOD ") } }, // Kodagu @@ -619,23 +633,24 @@ private static Dictionary CreateIsoLanguageMap() { "kok", new[] { Tag.Parse("KOK ") } }, // Konkani { "kom", new[] { Tag.Parse("KOM ") } }, // Komi { "kon", new[] { Tag.Parse("KON0") } }, // Kongo - { "kor", new[] { Tag.Parse("KOH "), Tag.Parse("KOR ") } }, // Korean Old Hangul; Korean + { "kor", new[] { Tag.Parse("KOR "), Tag.Parse("KOH ") } }, // Korean; Korean Old Hangul { "kos", new[] { Tag.Parse("KOS ") } }, // Kosraean { "koy", new[] { Tag.Parse("ATH ") } }, // Athapaskan languages { "kpe", new[] { Tag.Parse("KPL ") } }, // Kpelle + { "kpp", new[] { Tag.Parse("KRN ") } }, // Karen { "kpv", new[] { Tag.Parse("KOZ ") } }, // Komi-Zyrian { "kpy", new[] { Tag.Parse("KYK ") } }, // Koryak { "kqs", new[] { Tag.Parse("KIS ") } }, // Kisii { "kqy", new[] { Tag.Parse("KRT ") } }, // Koorete { "krc", new[] { Tag.Parse("BAL "), Tag.Parse("KAR ") } }, // Balkar; Karachay - { "kri", new[] { Tag.Parse("CPP "), Tag.Parse("KRI ") } }, // Creoles; Krio + { "kri", new[] { Tag.Parse("KRI "), Tag.Parse("CPP ") } }, // Krio; Creoles { "krl", new[] { Tag.Parse("KRL ") } }, // Karelian { "krt", new[] { Tag.Parse("KNR ") } }, // Kanuri { "kru", new[] { Tag.Parse("KUU ") } }, // Kurukh { "ksh", new[] { Tag.Parse("KSH0") } }, // Ripuarian { "kss", new[] { Tag.Parse("KIS ") } }, // Kisii { "ksu", new[] { Tag.Parse("KSU ") } }, // Khamyang - { "ksw", new[] { Tag.Parse("KRN "), Tag.Parse("KSW ") } }, // Karen; Sโ€™gaw Karen + { "ksw", new[] { Tag.Parse("KSW "), Tag.Parse("KRN ") } }, // Sโ€™gaw Karen; Karen { "ktb", new[] { Tag.Parse("KEB ") } }, // Kebena { "ktu", new[] { Tag.Parse("KON ") } }, // Kikongo { "ktw", new[] { Tag.Parse("ATH ") } }, // Athapaskan languages @@ -646,7 +661,7 @@ private static Dictionary CreateIsoLanguageMap() { "kuw", new[] { Tag.Parse("BAD0") } }, // Banda { "kvb", new[] { Tag.Parse("MLY ") } }, // Malay { "kvl", new[] { Tag.Parse("KRN ") } }, // Karen - { "kvq", new[] { Tag.Parse("KRN "), Tag.Parse("KVQ ") } }, // Karen; Geba Karen + { "kvq", new[] { Tag.Parse("KVQ "), Tag.Parse("KRN ") } }, // Geba Karen; Karen { "kvr", new[] { Tag.Parse("MLY ") } }, // Malay { "kvt", new[] { Tag.Parse("KRN ") } }, // Karen { "kvu", new[] { Tag.Parse("KRN ") } }, // Karen @@ -660,7 +675,7 @@ private static Dictionary CreateIsoLanguageMap() { "kxk", new[] { Tag.Parse("KRN ") } }, // Karen { "kxl", new[] { Tag.Parse("KUU ") } }, // Kurukh { "kxu", new[] { Tag.Parse("KUI ") } }, // Kui - { "kyu", new[] { Tag.Parse("KRN "), Tag.Parse("KYU ") } }, // Karen; Western Kayah + { "kyu", new[] { Tag.Parse("KYU "), Tag.Parse("KRN ") } }, // Western Kayah; Karen { "lac", new[] { Tag.Parse("MYN ") } }, // Mayan { "lad", new[] { Tag.Parse("JUD ") } }, // Ladino { "lao", new[] { Tag.Parse("LAO ") } }, // Lao @@ -709,6 +724,7 @@ private static Dictionary CreateIsoLanguageMap() { "lua", new[] { Tag.Parse("LUA ") } }, // Luba-Lulua { "lub", new[] { Tag.Parse("LUB ") } }, // Luba-Katanga { "lug", new[] { Tag.Parse("LUG ") } }, // Ganda + { "luh", new[] { Tag.Parse("ZHS ") } }, // Chinese, Simplified { "luo", new[] { Tag.Parse("LUO ") } }, // Luo { "lus", new[] { Tag.Parse("MIZ "), Tag.Parse("QIN ") } }, // Mizo; Chin { "lut", new[] { Tag.Parse("LUT ") } }, // Lushootseed @@ -716,6 +732,7 @@ private static Dictionary CreateIsoLanguageMap() { "luz", new[] { Tag.Parse("LRC ") } }, // Luri { "lvs", new[] { Tag.Parse("LVI ") } }, // Latvian { "lwg", new[] { Tag.Parse("LUH ") } }, // Luyia + { "lzh", new[] { Tag.Parse("ZHT ") } }, // Chinese, Traditional { "lzz", new[] { Tag.Parse("LAZ ") } }, // Laz { "mad", new[] { Tag.Parse("MAD ") } }, // Madura { "mag", new[] { Tag.Parse("MAG ") } }, // Magahi @@ -740,7 +757,7 @@ private static Dictionary CreateIsoLanguageMap() { "mev", new[] { Tag.Parse("MEV ") } }, // Mano { "mfa", new[] { Tag.Parse("MFA ") } }, // Pattani Malay { "mfb", new[] { Tag.Parse("MLY ") } }, // Malay - { "mfe", new[] { Tag.Parse("CPP "), Tag.Parse("MFE ") } }, // Creoles; Morisyen + { "mfe", new[] { Tag.Parse("MFE "), Tag.Parse("CPP ") } }, // Morisyen; Creoles { "mfp", new[] { Tag.Parse("CPP ") } }, // Creoles { "mga", new[] { Tag.Parse("SGA ") } }, // Old Irish { "mhc", new[] { Tag.Parse("MYN ") } }, // Mayan @@ -759,6 +776,7 @@ private static Dictionary CreateIsoLanguageMap() { "mnh", new[] { Tag.Parse("BAD0") } }, // Banda { "mni", new[] { Tag.Parse("MNI ") } }, // Manipuri { "mnk", new[] { Tag.Parse("MND "), Tag.Parse("MNK ") } }, // Mandinka; Maninka + { "mnp", new[] { Tag.Parse("ZHS ") } }, // Chinese, Simplified { "mns", new[] { Tag.Parse("MAN ") } }, // Mansi { "mnw", new[] { Tag.Parse("MON "), Tag.Parse("MONT") } }, // Mon; Thailand Mon { "mo", new[] { Tag.Parse("ROM ") } }, // Romanian @@ -799,11 +817,12 @@ private static Dictionary CreateIsoLanguageMap() { "mzb", new[] { Tag.Parse("BBR ") } }, // Berber { "mzn", new[] { Tag.Parse("MZN ") } }, // Mazanderani { "mzs", new[] { Tag.Parse("CPP ") } }, // Creoles - { "nag", new[] { Tag.Parse("CPP "), Tag.Parse("NAG ") } }, // Creoles; Naga-Assamese + { "nag", new[] { Tag.Parse("NAG "), Tag.Parse("CPP ") } }, // Naga-Assamese; Creoles { "nah", new[] { Tag.Parse("NAH ") } }, // Nahuatl + { "nan", new[] { Tag.Parse("ZHS ") } }, // Chinese, Simplified { "nap", new[] { Tag.Parse("NAP ") } }, // Neapolitan { "nau", new[] { Tag.Parse("NAU ") } }, // Nauruan - { "nav", new[] { Tag.Parse("ATH "), Tag.Parse("NAV ") } }, // Athapaskan languages; Navajo + { "nav", new[] { Tag.Parse("NAV "), Tag.Parse("ATH ") } }, // Navajo; Athapaskan languages { "naz", new[] { Tag.Parse("NAH ") } }, // Nahuatl { "nbl", new[] { Tag.Parse("NDB ") } }, // Ndebele { "nch", new[] { Tag.Parse("NAH ") } }, // Nahuatl @@ -848,6 +867,7 @@ private static Dictionary CreateIsoLanguageMap() { "nla", new[] { Tag.Parse("BML ") } }, // Bamileke { "nld", new[] { Tag.Parse("NLD ") } }, // Dutch { "nle", new[] { Tag.Parse("LUH ") } }, // Luyia + { "nln", new[] { Tag.Parse("NAH ") } }, // Nahuatl { "nlv", new[] { Tag.Parse("NAH ") } }, // Nahuatl { "nnh", new[] { Tag.Parse("BML ") } }, // Bamileke { "nno", new[] { Tag.Parse("NYN ") } }, // Norwegian Nynorsk (Nynorsk, Norwegian) @@ -902,7 +922,7 @@ private static Dictionary CreateIsoLanguageMap() { "pag", new[] { Tag.Parse("PAG ") } }, // Pangasinan { "pam", new[] { Tag.Parse("PAM ") } }, // Pampangan { "pan", new[] { Tag.Parse("PAN ") } }, // Punjabi - { "pap", new[] { Tag.Parse("CPP "), Tag.Parse("PAP0") } }, // Creoles; Papiamentu + { "pap", new[] { Tag.Parse("PAP0"), Tag.Parse("CPP ") } }, // Papiamentu; Creoles { "pau", new[] { Tag.Parse("PAU ") } }, // Palauan { "pbt", new[] { Tag.Parse("PAS ") } }, // Pashto { "pbu", new[] { Tag.Parse("PAS ") } }, // Pashto @@ -919,7 +939,7 @@ private static Dictionary CreateIsoLanguageMap() { "pey", new[] { Tag.Parse("CPP ") } }, // Creoles { "pga", new[] { Tag.Parse("CPP ") } }, // Creoles { "phk", new[] { Tag.Parse("PHK ") } }, // Phake - { "pih", new[] { Tag.Parse("CPP "), Tag.Parse("PIH ") } }, // Creoles; Norfolk + { "pih", new[] { Tag.Parse("PIH "), Tag.Parse("CPP ") } }, // Norfolk; Creoles { "pis", new[] { Tag.Parse("CPP ") } }, // Creoles { "pkh", new[] { Tag.Parse("QIN ") } }, // Chin { "pko", new[] { Tag.Parse("KAL ") } }, // Kalenjin @@ -934,7 +954,7 @@ private static Dictionary CreateIsoLanguageMap() { "pmy", new[] { Tag.Parse("CPP ") } }, // Creoles { "pnb", new[] { Tag.Parse("PNB ") } }, // Western Panjabi { "poc", new[] { Tag.Parse("MYN ") } }, // Mayan - { "poh", new[] { Tag.Parse("MYN "), Tag.Parse("POH ") } }, // Mayan; Pocomchi + { "poh", new[] { Tag.Parse("POH "), Tag.Parse("MYN ") } }, // Pocomchi; Mayan { "pol", new[] { Tag.Parse("PLK ") } }, // Polish { "pon", new[] { Tag.Parse("PON ") } }, // Pohnpeian { "por", new[] { Tag.Parse("PTG ") } }, // Portuguese @@ -949,16 +969,26 @@ private static Dictionary CreateIsoLanguageMap() { "pub", new[] { Tag.Parse("QIN ") } }, // Chin { "pus", new[] { Tag.Parse("PAS ") } }, // Pashto { "puz", new[] { Tag.Parse("QIN ") } }, // Chin - { "pwo", new[] { Tag.Parse("KRN "), Tag.Parse("PWO ") } }, // Karen; Western Pwo Karen + { "pwo", new[] { Tag.Parse("PWO "), Tag.Parse("KRN ") } }, // Western Pwo Karen; Karen { "pww", new[] { Tag.Parse("KRN ") } }, // Karen - { "quc", new[] { Tag.Parse("MYN "), Tag.Parse("QUC ") } }, // Mayan; Kโ€™icheโ€™ + { "quc", new[] { Tag.Parse("QUC "), Tag.Parse("MYN ") } }, // Kโ€™icheโ€™; Mayan + { "que", new[] { Tag.Parse("QUZ ") } }, // Quechua + { "quf", new[] { Tag.Parse("QUZ ") } }, // Quechua { "quh", new[] { Tag.Parse("QUH ") } }, // Quechua (Bolivia) + { "quk", new[] { Tag.Parse("QUZ ") } }, // Quechua { "qum", new[] { Tag.Parse("MYN ") } }, // Mayan { "quv", new[] { Tag.Parse("MYN ") } }, // Mayan + { "quy", new[] { Tag.Parse("QUZ ") } }, // Quechua { "quz", new[] { Tag.Parse("QUZ ") } }, // Quechua + { "qvc", new[] { Tag.Parse("QUZ ") } }, // Quechua + { "qve", new[] { Tag.Parse("QUZ ") } }, // Quechua { "qvi", new[] { Tag.Parse("QVI ") } }, // Quechua (Ecuador) + { "qvs", new[] { Tag.Parse("QUZ ") } }, // Quechua + { "qwc", new[] { Tag.Parse("QUZ ") } }, // Quechua { "qwh", new[] { Tag.Parse("QWH ") } }, // Quechua (Peru) { "qwt", new[] { Tag.Parse("ATH ") } }, // Athapaskan languages + { "qxp", new[] { Tag.Parse("QUZ ") } }, // Quechua + { "qxu", new[] { Tag.Parse("QUZ ") } }, // Quechua { "rag", new[] { Tag.Parse("LUH ") } }, // Luyia { "raj", new[] { Tag.Parse("RAJ ") } }, // Rajasthani { "ral", new[] { Tag.Parse("QIN ") } }, // Chin @@ -969,7 +999,7 @@ private static Dictionary CreateIsoLanguageMap() { "rej", new[] { Tag.Parse("REJ ") } }, // Rejang { "rhg", new[] { Tag.Parse("RHG ") } }, // Rohingya { "ria", new[] { Tag.Parse("RIA ") } }, // Riang - { "rif", new[] { Tag.Parse("BBR "), Tag.Parse("RIF ") } }, // Berber; Tarifit + { "rif", new[] { Tag.Parse("RIF "), Tag.Parse("BBR ") } }, // Tarifit; Berber { "rit", new[] { Tag.Parse("RIT ") } }, // Ritarungo { "rki", new[] { Tag.Parse("ARK ") } }, // Rakhine { "rkw", new[] { Tag.Parse("RKW ") } }, // Arakwal @@ -1004,7 +1034,7 @@ private static Dictionary CreateIsoLanguageMap() { "sck", new[] { Tag.Parse("SAD ") } }, // Sadri { "scn", new[] { Tag.Parse("SCN ") } }, // Sicilian { "sco", new[] { Tag.Parse("SCO ") } }, // Scots - { "scs", new[] { Tag.Parse("ATH "), Tag.Parse("SCS "), Tag.Parse("SLA ") } }, // Athapaskan languages; North Slavey; Slavey + { "scs", new[] { Tag.Parse("SCS "), Tag.Parse("SLA "), Tag.Parse("ATH ") } }, // North Slavey; Slavey; Athapaskan languages { "sdc", new[] { Tag.Parse("SRD ") } }, // Sardinian { "sdh", new[] { Tag.Parse("KUR ") } }, // Kurdish { "sdn", new[] { Tag.Parse("SRD ") } }, // Sardinian @@ -1019,7 +1049,7 @@ private static Dictionary CreateIsoLanguageMap() { "sgc", new[] { Tag.Parse("KAL ") } }, // Kalenjin { "sgs", new[] { Tag.Parse("SGS ") } }, // Samogitian { "sgw", new[] { Tag.Parse("CHG ") } }, // Chaha Gurage - { "shi", new[] { Tag.Parse("BBR "), Tag.Parse("SHI ") } }, // Berber; Tachelhit + { "shi", new[] { Tag.Parse("SHI "), Tag.Parse("BBR ") } }, // Tachelhit; Berber { "shl", new[] { Tag.Parse("QIN ") } }, // Chin { "shn", new[] { Tag.Parse("SHN ") } }, // Shan { "shu", new[] { Tag.Parse("ARA ") } }, // Arabic @@ -1028,6 +1058,7 @@ private static Dictionary CreateIsoLanguageMap() { "sin", new[] { Tag.Parse("SNH ") } }, // Sinhala (Sinhalese) { "siz", new[] { Tag.Parse("BBR ") } }, // Berber { "sja", new[] { Tag.Parse("SJA ") } }, // Epena + { "sjc", new[] { Tag.Parse("ZHS ") } }, // Chinese, Simplified { "sjd", new[] { Tag.Parse("KSM ") } }, // Kildin Sami { "sje", new[] { Tag.Parse("SJE ") } }, // Pite Sami { "sjo", new[] { Tag.Parse("SIB ") } }, // Sibe @@ -1085,15 +1116,15 @@ private static Dictionary CreateIsoLanguageMap() { "swn", new[] { Tag.Parse("BBR ") } }, // Berber { "swv", new[] { Tag.Parse("MAW ") } }, // Marwari { "sxu", new[] { Tag.Parse("SXU ") } }, // Upper Saxon - { "syc", new[] { Tag.Parse("SYR "), Tag.Parse("SYRE"), Tag.Parse("SYRJ"), Tag.Parse("SYRN") } }, // Syriac; Syriac, Estrangela script-variant (equivalent to ISO 15924 'Syre'); Syriac, Western script-variant (equivalent to ISO 15924 'Syrj'); Syriac, Eastern script-variant (equivalent to ISO 15924 'Syrn') + { "syc", new[] { Tag.Parse("SYR ") } }, // Syriac { "syl", new[] { Tag.Parse("SYL ") } }, // Sylheti - { "syr", new[] { Tag.Parse("SYR "), Tag.Parse("SYRE"), Tag.Parse("SYRJ"), Tag.Parse("SYRN") } }, // Syriac; Syriac, Estrangela script-variant (equivalent to ISO 15924 'Syre'); Syriac, Western script-variant (equivalent to ISO 15924 'Syrj'); Syriac, Eastern script-variant (equivalent to ISO 15924 'Syrn') + { "syr", new[] { Tag.Parse("SYR ") } }, // Syriac { "szl", new[] { Tag.Parse("SZL ") } }, // Silesian { "taa", new[] { Tag.Parse("ATH ") } }, // Athapaskan languages { "tab", new[] { Tag.Parse("TAB ") } }, // Tabasaran { "tah", new[] { Tag.Parse("THT ") } }, // Tahitian { "tam", new[] { Tag.Parse("TAM ") } }, // Tamil - { "taq", new[] { Tag.Parse("BBR "), Tag.Parse("TAQ "), Tag.Parse("TMH ") } }, // Berber; Tamasheq; Tamashek + { "taq", new[] { Tag.Parse("TAQ "), Tag.Parse("TMH "), Tag.Parse("BBR ") } }, // Tamasheq; Tamashek; Berber { "tas", new[] { Tag.Parse("CPP ") } }, // Creoles { "tat", new[] { Tag.Parse("TAT ") } }, // Tatar { "tau", new[] { Tag.Parse("ATH ") } }, // Athapaskan languages @@ -1122,8 +1153,8 @@ private static Dictionary CreateIsoLanguageMap() { "tha", new[] { Tag.Parse("THA ") } }, // Thai { "thp", new[] { Tag.Parse("THP ") } }, // Thompson { "tht", new[] { Tag.Parse("ATH ") } }, // Athapaskan languages - { "thv", new[] { Tag.Parse("BBR "), Tag.Parse("THV "), Tag.Parse("TMH ") } }, // Berber; Tahaggart Tamahaq; Tamashek - { "thz", new[] { Tag.Parse("BBR "), Tag.Parse("THZ "), Tag.Parse("TMH ") } }, // Berber; Tayart Tamajeq; Tamashek + { "thv", new[] { Tag.Parse("THV "), Tag.Parse("TMH "), Tag.Parse("BBR ") } }, // Tahaggart Tamahaq; Tamashek; Berber + { "thz", new[] { Tag.Parse("THZ "), Tag.Parse("TMH "), Tag.Parse("BBR ") } }, // Tayart Tamajeq; Tamashek; Berber { "tia", new[] { Tag.Parse("BBR ") } }, // Berber { "tig", new[] { Tag.Parse("TGR ") } }, // Tigre { "tir", new[] { Tag.Parse("TGY ") } }, // Tigrinya @@ -1134,7 +1165,7 @@ private static Dictionary CreateIsoLanguageMap() { "tli", new[] { Tag.Parse("TLI ") } }, // Tlingit { "tly", new[] { Tag.Parse("TLY ") } }, // Talysh { "tmg", new[] { Tag.Parse("CPP ") } }, // Creoles - { "tmh", new[] { Tag.Parse("BBR "), Tag.Parse("TMH ") } }, // Berber; Tamashek + { "tmh", new[] { Tag.Parse("TMH "), Tag.Parse("BBR ") } }, // Tamashek; Berber { "tmw", new[] { Tag.Parse("MLY ") } }, // Malay { "tnf", new[] { Tag.Parse("DRI ") } }, // Dari { "tod", new[] { Tag.Parse("TOD0") } }, // Toma @@ -1143,15 +1174,15 @@ private static Dictionary CreateIsoLanguageMap() { "tol", new[] { Tag.Parse("ATH ") } }, // Athapaskan languages { "ton", new[] { Tag.Parse("TGN ") } }, // Tongan { "tor", new[] { Tag.Parse("BAD0") } }, // Banda - { "tpi", new[] { Tag.Parse("CPP "), Tag.Parse("TPI ") } }, // Creoles; Tok Pisin + { "tpi", new[] { Tag.Parse("TPI "), Tag.Parse("CPP ") } }, // Tok Pisin; Creoles { "trf", new[] { Tag.Parse("CPP ") } }, // Creoles - { "tru", new[] { Tag.Parse("SYR "), Tag.Parse("TUA ") } }, // Syriac; Turoyo Aramaic + { "tru", new[] { Tag.Parse("TUA "), Tag.Parse("SYR ") } }, // Turoyo Aramaic; Syriac { "tsj", new[] { Tag.Parse("TSJ ") } }, // Tshangla { "tsn", new[] { Tag.Parse("TNA ") } }, // Tswana { "tso", new[] { Tag.Parse("TSG ") } }, // Tsonga { "ttc", new[] { Tag.Parse("MYN ") } }, // Mayan { "ttm", new[] { Tag.Parse("ATH ") } }, // Athapaskan languages - { "ttq", new[] { Tag.Parse("BBR "), Tag.Parse("TMH "), Tag.Parse("TTQ ") } }, // Berber; Tamashek; Tawallammat Tamajaq + { "ttq", new[] { Tag.Parse("TTQ "), Tag.Parse("TMH "), Tag.Parse("BBR ") } }, // Tawallammat Tamajaq; Tamashek; Berber { "tuk", new[] { Tag.Parse("TKM ") } }, // Turkmen { "tum", new[] { Tag.Parse("TUM ") } }, // Tumbuka { "tur", new[] { Tag.Parse("TRK ") } }, // Turkish @@ -1161,19 +1192,20 @@ private static Dictionary CreateIsoLanguageMap() { "tvl", new[] { Tag.Parse("TVL ") } }, // Tuvalu { "tvy", new[] { Tag.Parse("CPP ") } }, // Creoles { "tw", new[] { Tag.Parse("AKA ") } }, // Akan - { "twi", new[] { Tag.Parse("AKA "), Tag.Parse("TWI ") } }, // Akan; Twi + { "twi", new[] { Tag.Parse("TWI "), Tag.Parse("AKA ") } }, // Twi; Akan { "txc", new[] { Tag.Parse("ATH ") } }, // Athapaskan languages { "txy", new[] { Tag.Parse("MLG ") } }, // Malagasy { "tyv", new[] { Tag.Parse("TUV ") } }, // Tuvin { "tyz", new[] { Tag.Parse("TYZ ") } }, // Tร y { "tzh", new[] { Tag.Parse("MYN ") } }, // Mayan { "tzj", new[] { Tag.Parse("MYN ") } }, // Mayan - { "tzm", new[] { Tag.Parse("BBR "), Tag.Parse("TZM ") } }, // Berber; Tamazight - { "tzo", new[] { Tag.Parse("MYN "), Tag.Parse("TZO ") } }, // Mayan; Tzotzil + { "tzm", new[] { Tag.Parse("TZM "), Tag.Parse("BBR ") } }, // Tamazight; Berber + { "tzo", new[] { Tag.Parse("TZO "), Tag.Parse("MYN ") } }, // Tzotzil; Mayan { "ubl", new[] { Tag.Parse("BIK ") } }, // Bikol { "udi", new[] { Tag.Parse("UDI ") } }, // Udi { "udm", new[] { Tag.Parse("UDM ") } }, // Udmurt { "uig", new[] { Tag.Parse("UYG ") } }, // Uyghur + { "uki", new[] { Tag.Parse("KUI ") } }, // Kui { "ukr", new[] { Tag.Parse("UKR ") } }, // Ukrainian { "uln", new[] { Tag.Parse("CPP ") } }, // Creoles { "umb", new[] { Tag.Parse("UMB ") } }, // Umbundu @@ -1215,6 +1247,7 @@ private static Dictionary CreateIsoLanguageMap() { "wry", new[] { Tag.Parse("MAW ") } }, // Marwari { "wsg", new[] { Tag.Parse("GON ") } }, // Gondi { "wtm", new[] { Tag.Parse("WTM ") } }, // Mewati + { "wuu", new[] { Tag.Parse("ZHS ") } }, // Chinese, Simplified { "wyn", new[] { Tag.Parse("WYN ") } }, // Wyandot { "xal", new[] { Tag.Parse("KLM "), Tag.Parse("TOD ") } }, // Kalmyk; Todo { "xan", new[] { Tag.Parse("SEK ") } }, // Sekota @@ -1230,11 +1263,12 @@ private static Dictionary CreateIsoLanguageMap() { "xnr", new[] { Tag.Parse("DGR ") } }, // Dogri (macrolanguage) { "xog", new[] { Tag.Parse("XOG ") } }, // Soga { "xpe", new[] { Tag.Parse("XPE ") } }, // Kpelle (Liberia) - { "xsl", new[] { Tag.Parse("ATH "), Tag.Parse("SLA "), Tag.Parse("SSL ") } }, // Athapaskan languages; Slavey; South Slavey + { "xsl", new[] { Tag.Parse("SSL "), Tag.Parse("SLA "), Tag.Parse("ATH ") } }, // South Slavey; Slavey; Athapaskan languages { "xst", new[] { Tag.Parse("SIG ") } }, // Silte Gurage { "xub", new[] { Tag.Parse("XUB ") } }, // Bette Kuruma { "xuj", new[] { Tag.Parse("XUJ ") } }, // Jennu Kuruma { "xup", new[] { Tag.Parse("ATH ") } }, // Athapaskan languages + { "xwo", new[] { Tag.Parse("TOD ") } }, // Todo { "yaj", new[] { Tag.Parse("BAD0") } }, // Banda { "yao", new[] { Tag.Parse("YAO ") } }, // Yao { "yap", new[] { Tag.Parse("YAP ") } }, // Yapese @@ -1257,7 +1291,7 @@ private static Dictionary CreateIsoLanguageMap() { "zeh", new[] { Tag.Parse("ZHA ") } }, // Zhuang { "zen", new[] { Tag.Parse("BBR ") } }, // Berber { "zgb", new[] { Tag.Parse("ZHA ") } }, // Zhuang - { "zgh", new[] { Tag.Parse("BBR "), Tag.Parse("ZGH ") } }, // Berber; Standard Moroccan Tamazight + { "zgh", new[] { Tag.Parse("ZGH "), Tag.Parse("BBR ") } }, // Standard Moroccan Tamazight; Berber { "zgm", new[] { Tag.Parse("ZHA ") } }, // Zhuang { "zgn", new[] { Tag.Parse("ZHA ") } }, // Zhuang { "zha", new[] { Tag.Parse("ZHA ") } }, // Zhuang diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/OpenTypeLanguageTagMap.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/OpenTypeLanguageTagMap.cs index a9e86d5e5..c07de4710 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/OpenTypeLanguageTagMap.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/OpenTypeLanguageTagMap.cs @@ -12,9 +12,10 @@ namespace SixLabors.Fonts.Tables.AdvancedTypographic; /// /// /// Candidates are ordered most specific first; callers select the first tag the font's -/// script table declares and fall back to the default language system otherwise. -/// The registry data half of this class is generated; see the UnicodeTrieGenerator -/// project. +/// script table declares and fall back to the default language system otherwise. The +/// resolution rules mirror HarfBuzz's hb_ot_tags_from_language and are pinned against +/// its test corpus. The registry data half of this class is generated; see the +/// UnicodeTrieGenerator project. /// internal sealed partial class OpenTypeLanguageTagMap { @@ -24,10 +25,10 @@ internal sealed partial class OpenTypeLanguageTagMap private static readonly Lazy> LazyMap = new(CreateIsoLanguageMap, isThreadSafe: true); /// - /// Maps BCP 47 variant subtags to the language system tags the registry defines by - /// cross reference rather than by ISO 639 code. + /// Maps BCP 47 variant and script subtags to the language system tags the registry + /// defines by cross reference rather than by ISO 639 code. /// - private static readonly Dictionary VariantTagMap = new(StringComparer.Ordinal) + private static readonly Dictionary SubtagTagMap = new(StringComparer.Ordinal) { { "fonipa", Tag.Parse("IPPH") }, // Phonetic transcription, IPA conventions { "fonnapa", Tag.Parse("APPH") }, // Phonetic transcription, Americanist conventions @@ -37,6 +38,7 @@ internal sealed partial class OpenTypeLanguageTagMap { "syre", Tag.Parse("SYRE") }, // Syriac, Estrangela script variant { "syrj", Tag.Parse("SYRJ") }, // Syriac, Western script variant { "syrn", Tag.Parse("SYRN") }, // Syriac, Eastern script variant + { "latg", Tag.Parse("IRT ") }, // Irish Traditional, Latin Gaelic script { "arevmda", Tag.Parse("HYE ") }, // Western Armenian }; @@ -46,6 +48,10 @@ internal sealed partial class OpenTypeLanguageTagMap private static readonly Tag ZhhTag = Tag.Parse("ZHH "); + private static readonly Tag ZhtmTag = Tag.Parse("ZHTM"); + + private static readonly Tag MolTag = Tag.Parse("MOL "); + /// /// Prevents a default instance of the class /// from being created. @@ -76,61 +82,159 @@ public static bool TryGetTags(CultureInfo? culture, out Tag[] tags) List candidates = []; string[] subtags = culture.Name.ToLowerInvariant().Split('-'); + string threeLetter = culture.ThreeLetterISOLanguageName.ToLowerInvariant(); - // BCP 47 variant subtags override the language mapping entirely: the registry - // defines these tags by variant, not by ISO code. + // BCP 47 variant and script subtags override the language mapping entirely: the + // registry defines these tags by subtag, not by ISO code. for (int i = 1; i < subtags.Length; i++) { - if (VariantTagMap.TryGetValue(subtags[i], out Tag variantTag)) + if (SubtagTagMap.TryGetValue(subtags[i], out Tag subtagTag)) { - AddDistinct(candidates, variantTag); + AddDistinct(candidates, subtagTag); } } - string threeLetter = culture.ThreeLetterISOLanguageName.ToLowerInvariant(); - - if (subtags[0] == "zh" || threeLetter is "zho" or "cmn") + if (subtags[0] is "zh" or "yue" or "cmn" || threeLetter is "zho" or "cmn" or "yue") { - // Chinese language system tags encode script and region rather than - // language: Hong Kong and Macao conventions first where they apply, then - // traditional for Hant or Taiwan, then simplified, with the remaining - // registry tags as fallbacks below. - bool traditional = false; - bool hongKong = false; - for (int i = 1; i < subtags.Length; i++) - { - traditional |= subtags[i] is "hant" or "tw"; - hongKong |= subtags[i] is "hk" or "mo"; - } + AddChineseCandidates(candidates, subtags); + } + else if ((subtags[0] == "ro" || threeLetter == "ron") && HasSubtag(subtags, "md")) + { + // Moldavian keeps its own registered tag for Romanian in Moldova; the + // Romanian tag follows from the general lookup below. + AddDistinct(candidates, MolTag); + } - if (hongKong) + // The registry lists ISO 639-3 codes; older rows also carry two letter forms. + // The runtime reports an empty or echoed code for languages it has no data for, + // so the two letter form doubles as the raw subtag for unknown languages. + string twoLetter = culture.TwoLetterISOLanguageName.ToLowerInvariant(); + Dictionary map = LazyMap.Value; + if (map.TryGetValue(threeLetter, out Tag[]? mapped) || map.TryGetValue(twoLetter, out mapped)) + { + foreach (Tag tag in mapped) { - AddDistinct(candidates, ZhhTag); - AddDistinct(candidates, ZhtTag); + AddDistinct(candidates, tag); } - else if (traditional) + } + else if (TryGetSynthesisSource(threeLetter, twoLetter, out string source)) + { + // The specification directs fonts supporting languages without registered + // tags to use the uppercase ISO 639-3 code as the language system tag, so an + // unmapped code synthesizes that candidate, matching HarfBuzz. + Span synthesized = ['\0', '\0', '\0', ' ']; + for (int i = 0; i < 3; i++) { - AddDistinct(candidates, ZhtTag); + synthesized[i] = char.ToUpperInvariant(source[i]); } - else + + AddDistinct(candidates, Tag.Parse(new string(synthesized))); + } + + tags = [.. candidates]; + return tags.Length > 0; + } + + /// + /// Adds the Chinese language system candidates. The Chinese tags encode script and + /// region rather than language, so resolution mirrors HarfBuzz: an explicit + /// simplified script wins over any region, an explicit traditional script defers to + /// the Macao and Hong Kong regional conventions, then regions decide, and bare + /// Cantonese defaults to the Hong Kong conventions while any other bare Chinese + /// defaults to simplified. Traditional regional conventions fall back to the general + /// traditional tag. + /// + /// The candidate list to add to. + /// The lowercase culture name subtags. + private static void AddChineseCandidates(List candidates, string[] subtags) + { + bool simplified = HasSubtag(subtags, "hans"); + bool traditional = HasSubtag(subtags, "hant"); + bool taiwan = HasSubtag(subtags, "tw"); + bool hongKong = HasSubtag(subtags, "hk"); + bool macao = HasSubtag(subtags, "mo"); + + if (simplified) + { + AddDistinct(candidates, ZhsTag); + } + else if (macao) + { + // Macao's dedicated tag is rare in fonts; the Hong Kong conventions are the + // regional fallback before the general traditional tag, matching HarfBuzz's + // language-tags shaping expectations. + AddDistinct(candidates, ZhtmTag); + AddDistinct(candidates, ZhhTag); + AddDistinct(candidates, ZhtTag); + } + else if (hongKong || subtags[0] == "yue") + { + // Cantonese without an explicit simplified script or region uses the Hong + // Kong conventions, including with an explicit traditional script. + AddDistinct(candidates, ZhhTag); + AddDistinct(candidates, ZhtTag); + } + else if (traditional || taiwan) + { + AddDistinct(candidates, ZhtTag); + } + else + { + AddDistinct(candidates, ZhsTag); + } + } + + /// + /// Selects the ISO 639-3 code an unmapped language synthesizes its tag from, + /// preferring the resolved three letter code and falling back to the raw subtag the + /// runtime echoes for languages it has no data for. + /// + /// The lowercase three letter ISO language name. + /// The lowercase two letter ISO language name. + /// The selected code. + /// if a three letter code is available; otherwise . + private static bool TryGetSynthesisSource(string threeLetter, string twoLetter, out string source) + { + if (threeLetter.Length == 3 && threeLetter != "und" && IsAsciiLetters(threeLetter)) + { + source = threeLetter; + return true; + } + + if (twoLetter.Length == 3 && twoLetter != "und" && IsAsciiLetters(twoLetter)) + { + source = twoLetter; + return true; + } + + source = string.Empty; + return false; + } + + private static bool HasSubtag(string[] subtags, string value) + { + for (int i = 1; i < subtags.Length; i++) + { + if (subtags[i] == value) { - AddDistinct(candidates, ZhsTag); + return true; } } - // The registry lists ISO 639-3 codes; older rows also carry two letter forms. - Dictionary map = LazyMap.Value; - if (map.TryGetValue(threeLetter, out Tag[]? mapped) - || map.TryGetValue(culture.TwoLetterISOLanguageName.ToLowerInvariant(), out mapped)) + return false; + } + + private static bool IsAsciiLetters(string value) + { + foreach (char c in value) { - foreach (Tag tag in mapped) + if (c is < 'a' or > 'z') { - AddDistinct(candidates, tag); + return false; } } - tags = [.. candidates]; - return tags.Length > 0; + return true; } private static void AddDistinct(List candidates, Tag tag) diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs index cfbe1a169..8bfa1e803 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs @@ -318,8 +318,11 @@ private void InitialReorder(IGlyphShapingCollection collection, int index, int c } // Create a reusable temporary substitution collection and buffer to allow checking whether - // certain combinations will be substituted. - GlyphSubstitutionCollection tempCollection = new(this.textOptions); + // certain combinations will be substituted. The probe collection MUST share the + // pass's feature map: the probed glyphs are copies of real glyphs whose feature + // masks were assigned by it, and a private map would resolve the probed feature + // to a different bit, turning the enable into a no-op. + GlyphSubstitutionCollection tempCollection = new(this.textOptions, substitutionCollection.FeatureMap); Span tempBuffer = new GlyphShapingData[3]; ShapingConfiguration indicConfiguration = this.indicConfiguration; @@ -434,7 +437,7 @@ private void InitialReorder(IGlyphShapingCollection collection, int index, int c // base consonants. if (start + 3 <= end && indicConfiguration.RephPosition != Positions.Ra_To_Become_Reph && - gSubTable?.TryGetFeatureLookups(fontMetrics, in RphfTag, this.ScriptClass, out _) == true && + gSubTable?.TryGetFeatureLookups(fontMetrics, in RphfTag, this.ScriptClass, substitutionCollection.LanguageTags, out _) == true && ((indicConfiguration.RephMode == RephMode.Implicit && !IsJoiner(substitutionCollection[start + 2])) || (indicConfiguration.RephMode == RephMode.Explicit && substitutionCollection[start + 2].IndicShapingEngineInfo?.Category == Categories.ZWJ))) { @@ -832,7 +835,7 @@ private void InitialReorder(IGlyphShapingCollection collection, int index, int c const int prefLen = 2; if (basePosition + prefLen < end && - gSubTable?.TryGetFeatureLookups(fontMetrics, in PrefTag, this.ScriptClass, out _) == true) + gSubTable?.TryGetFeatureLookups(fontMetrics, in PrefTag, this.ScriptClass, substitutionCollection.LanguageTags, out _) == true) { // Find a Halant,Ra sequence and mark it for pre-base reordering processing. for (int i = basePosition + 1; i + prefLen - 1 < end; i++) @@ -851,7 +854,7 @@ private void InitialReorder(IGlyphShapingCollection collection, int index, int c // This allows distinguishing the following cases with MS Khmer fonts: // U+1784,U+17D2,U+179A,U+17D2,U+1782 // U+1784,U+17D2,U+1782,U+17D2,U+179A - if (gSubTable.TryGetFeatureLookups(fontMetrics, in CfarTag, this.ScriptClass, out _)) + if (gSubTable.TryGetFeatureLookups(fontMetrics, in CfarTag, this.ScriptClass, substitutionCollection.LanguageTags, out _)) { while (i < end) { @@ -1056,7 +1059,7 @@ private void FinalReorder(IGlyphShapingCollection collection, int index, int cou // applied (see below), the shaping engine performs some final glyph // reordering before applying all the remaining font features to the entire // cluster. - bool tryPref = gSubTable?.TryGetFeatureLookups(fontMetrics, in PrefTag, this.ScriptClass, out _) == true; + bool tryPref = gSubTable?.TryGetFeatureLookups(fontMetrics, in PrefTag, this.ScriptClass, substitutionCollection.LanguageTags, out _) == true; // Find base consonant again. int basePosition = start; @@ -1069,7 +1072,7 @@ private void FinalReorder(IGlyphShapingCollection collection, int index, int cou for (int i = basePosition + 1; i < end; i++) { GlyphShapingData current = substitutionCollection[i]; - if (current.Features.FindIndex(x => x.Tag == PrefTag && x.Enabled) >= 0) + if ((current.FeatureMask & substitutionCollection.FeatureMap.GetMask(PrefTag)) != 0) { if (!current.IsSubstituted && current.IsLigated && !current.IsDecomposed) { @@ -1378,7 +1381,7 @@ private void FinalReorder(IGlyphShapingCollection collection, int index, int cou for (int i = basePosition + 1; i < end; i++) { GlyphShapingData current = substitutionCollection[i]; - if (current.Features.FindIndex(x => x.Tag == PrefTag && x.Enabled) >= 0) + if ((current.FeatureMask & substitutionCollection.FeatureMap.GetMask(PrefTag)) != 0) { // 1. Only reorder a glyph produced by substitution during application // of the feature. (Note that a font may shape a Ra consonant with diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ThaiShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ThaiShaper.cs index e1d197ea5..f561941d1 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ThaiShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ThaiShaper.cs @@ -309,7 +309,7 @@ private static void DoThaiPuaShaping(GlyphSubstitutionCollection collection, Fon if (puaCp != baseCp && fontMetrics.TryGetGlyphId(new CodePoint(puaCp), out ushort puaId)) { collection[baseIndex].CodePoint = new CodePoint(puaCp); - collection[baseIndex].GlyphId = puaId; + collection.SetGlyphId(baseIndex, puaId); } } else if (action != PuaAction.NOP) @@ -318,7 +318,7 @@ private static void DoThaiPuaShaping(GlyphSubstitutionCollection collection, Fon if (puaCp != codepoint && fontMetrics.TryGetGlyphId(new CodePoint(puaCp), out ushort puaId)) { collection[i].CodePoint = new CodePoint(puaCp); - collection[i].GlyphId = puaId; + collection.SetGlyphId(i, puaId); } } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs index fda9bc50b..0b46f7231 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs @@ -264,10 +264,11 @@ private static void RecordRhpf(IGlyphShapingCollection collection, int index, in } int end = index + count; + ulong rphfMask = substitutionCollection.FeatureMap.GetMask(RphfTag); for (int i = index; i < end; i++) { GlyphShapingData data = substitutionCollection[i]; - if (data.IsSubstituted && data.Features.Any(x => x.Tag == RphfTag)) + if (data.IsSubstituted && (data.RegisteredFeatureMask & rphfMask) != 0) { // Mark a substituted repha. if (data.UniversalShapingEngineInfo != null) diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapingFeatureMap.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapingFeatureMap.cs new file mode 100644 index 000000000..d7033d302 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapingFeatureMap.cs @@ -0,0 +1,92 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.AdvancedTypographic; + +/// +/// Assigns each OpenType feature tag touched during a single shaping pass a bit within a +/// 64 bit mask, so per-glyph feature state can be stored and tested as plain bitwise +/// operations instead of per-glyph collections. +/// +/// +/// +/// Every glyph carries mask words instead of per-glyph collections, and lookup +/// application gates a glyph with one bitwise AND. The glyph side lives in the three +/// mask fields on : +/// +/// +/// : features a +/// shaper added for the glyph, enabled or not. Enabling is only ever an unhide of a +/// registered feature, never an introduction. +/// : the enabled subset, +/// the analog of hb_glyph_info_t.mask. This is the word the per-glyph application gate +/// tests. +/// : features whose +/// lookups actually changed the glyph, read after shaping, for example to detect that a +/// vertical alternate was substituted. +/// +/// +/// One instance is shared by the substitution and positioning collections of a shaping +/// pass. The sharing is load bearing: applied bits are written while substituting, the +/// glyph data is then copied into the positioning collection, and the positioning stages +/// and the layout walk read those bits later. A per-collection map would renumber the +/// bits across that copy and silently corrupt the applied state. +/// +/// +/// Bits are assigned first come, in registration order. A pass touches the shaper's +/// stage features, the font's required features, and the caller's +/// ; the largest shaper plans sit far below the 64 +/// available bits, so exhaustion indicates a defect rather than a workload and throws +/// loudly instead of shaping incorrectly. +/// +/// +internal sealed class ShapingFeatureMap +{ + /// + /// The registered tag values, indexed by assigned bit position. Stored as raw + /// values rather than so lookups take the + /// runtime's vectorized primitive search path, which custom structs never qualify + /// for. + /// + private readonly List tags = new(16); + + /// + /// Gets the mask bit for the given feature tag, or zero when the tag has not been + /// registered. A zero result is safe at every consumption site: testing it enables + /// or matches nothing and clearing it clears nothing. + /// + /// The feature tag. + /// The single-bit mask, or zero. + public ulong GetMask(Tag tag) + { + int index = this.tags.IndexOf(tag.Value); + return index < 0 ? 0 : 1UL << index; + } + + /// + /// Gets the mask bit for the given feature tag, assigning the next free bit when the + /// tag is new to this pass. + /// + /// The feature tag. + /// The single-bit mask. + /// + /// Thrown when a single shaping pass registers more than 64 distinct features. + /// + public ulong GetOrAddMask(Tag tag) + { + ulong mask = this.GetMask(tag); + if (mask != 0) + { + return mask; + } + + if (this.tags.Count == 64) + { + throw new InvalidOperationException( + "A single shaping pass registered more than 64 distinct OpenType features."); + } + + this.tags.Add(tag.Value); + return 1UL << (this.tags.Count - 1); + } +} diff --git a/src/SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.cs b/src/SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.cs index 98ceef5be..55c80d50e 100644 --- a/src/SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.cs +++ b/src/SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.cs @@ -16,7 +16,13 @@ public partial class TrueTypeGlyphMetrics : FontGlyphMetrics { private static readonly Vector2 YInverter = new(1, -1); private readonly GlyphVector vector; - private readonly ConcurrentDictionary scaledVectorCache = new(); + + /// + /// Scaled, hinted outline copies keyed by ppem. Allocated on first render: shaping + /// and measurement clone metrics without ever rendering them, so an eager cache + /// would cost a dictionary per glyph per shaping pass. + /// + private ConcurrentDictionary? scaledVectorCache; /// /// Initializes a new instance of the class. @@ -114,7 +120,7 @@ internal override FontGlyphMetrics CloneForRendering(TextRun textRun) this.FontMetrics, this.GlyphId, this.CodePoint, - GlyphVector.DeepClone(this.vector), + this.vector, this.AdvanceWidth, this.AdvanceHeight, this.LeftSideBearing, @@ -140,7 +146,9 @@ internal override void RenderOutlineTo( HintingMode hintingMode) { Matrix3x2 transform = this.GetOutlineTransform(mode); - GlyphVector scaledVector = this.scaledVectorCache.GetOrAdd(scaledPPEM, _ => + ConcurrentDictionary cache = + LazyInitializer.EnsureInitialized(ref this.scaledVectorCache, static () => new()); + GlyphVector scaledVector = cache.GetOrAdd(scaledPPEM, _ => { // Create a scaled deep copy of the vector so that we do not alter // the globally cached instance. diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index d6ea9118b..67a818cbe 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -127,8 +127,15 @@ public static ShapedText ShapeText(ReadOnlySpan text, TextOptions options) : []; LayoutMode layoutMode = options.LayoutMode; - GlyphSubstitutionCollection substitutions = new(options); - GlyphPositioningCollection positionings = new(options); + + // One feature bit assignment for the whole pass: applied feature bits written + // while substituting are read after the glyph data is copied into the + // positioning collection, so both collections must agree on bit meaning. + ShapingFeatureMap featureMap = new(); + GlyphSubstitutionCollection substitutions = new(options, featureMap); + GlyphPositioningCollection positionings = new(options, featureMap); + + var probe = ShapingProbe.Enter(); // Analyse the text for bidi directional runs. BidiAlgorithm bidi = BidiAlgorithm.Instance.Value!; @@ -169,9 +176,13 @@ or BidiCharacterType.FirstStrongIsolate // Get the list of directional runs BidiRun[] bidiRuns = [.. BidiRun.CoalesceLevels(bidi.ResolvedLevels)]; Dictionary bidiMap = []; + ShapingProbe.Exit(ShapingProbe.Bidi, probe); + + probe = ShapingProbe.Enter(); // Incrementally build out collection of glyphs. IReadOnlyList textRuns = BuildTextRuns(text, options); + ShapingProbe.Exit(ShapingProbe.BuildTextRuns, probe); // First do multiple font runs using the individual text runs. bool complete = true; @@ -258,6 +269,7 @@ or BidiCharacterType.FirstStrongIsolate // Update the positions of the glyphs in the completed collection. // Each set of metrics is associated with single font and will only be updated // by that font so it's safe to use a single collection. + probe = ShapingProbe.Enter(); Font? lastFont = null; for (int i = 0; i < textRuns.Count; i++) { @@ -278,6 +290,8 @@ or BidiCharacterType.FirstStrongIsolate font.FontMetrics.UpdatePositions(positionings); } + ShapingProbe.Exit(ShapingProbe.Positioning, probe); + return new ShapedText(positionings, bidiRuns, bidiMap, layoutMode); } @@ -1607,6 +1621,8 @@ private static bool DoFontRun( // overwriting the glyph ids. substitutions.Clear(); + var probe = ShapingProbe.Enter(); + // Enumerate through each grapheme in the text. int graphemeIndex = start; SpanGraphemeEnumerator graphemeEnumerator = new(text); @@ -1675,14 +1691,24 @@ private static bool DoFontRun( graphemeIndex++; } + ShapingProbe.Exit(ShapingProbe.Populate, probe); + // Apply the simple and complex substitutions. // TODO: Investigate HarfBuzz normalizer. + probe = ShapingProbe.Enter(); SubstituteBidiMirrors(font.FontMetrics, substitutions); + ShapingProbe.Exit(ShapingProbe.Mirrors, probe); + + probe = ShapingProbe.Enter(); font.FontMetrics.ApplySubstitution(substitutions); + ShapingProbe.Exit(ShapingProbe.Substitution, probe); - return !isFallbackRun + probe = ShapingProbe.Enter(); + bool result = !isFallbackRun ? positionings.TryAdd(font, substitutions) : positionings.TryUpdate(font, substitutions); + ShapingProbe.Exit(ShapingProbe.MetricsAdd, probe); + return result; } /// diff --git a/src/SixLabors.Fonts/TextOptions.cs b/src/SixLabors.Fonts/TextOptions.cs index fffd4d8a3..b22d06a45 100644 --- a/src/SixLabors.Fonts/TextOptions.cs +++ b/src/SixLabors.Fonts/TextOptions.cs @@ -1,6 +1,7 @@ ๏ปฟ// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Globalization; using System.Numerics; using SixLabors.Fonts.Tables.AdvancedTypographic; using SixLabors.Fonts.Unicode; @@ -59,6 +60,7 @@ public TextOptions(TextOptions options) this.Tracking = options.Tracking; this.ColorFontSupport = options.ColorFontSupport; this.FeatureTags = new List(options.FeatureTags); + this.Culture = options.Culture; this.TextRuns = new List(options.TextRuns); this.DecorationPositioningMode = options.DecorationPositioningMode; this.TextDecorationSkipInk = options.TextDecorationSkipInk; @@ -294,6 +296,21 @@ public float LineSpacing /// public IReadOnlyList FeatureTags { get; set; } = Array.Empty(); + /// + /// Gets or sets the culture used to select language specific glyph shaping. + /// + /// + /// Fonts can substitute and position glyphs differently per language, for example the + /// Turkish dotless i or Serbian italic letterforms. When set, shaping selects the + /// matching language system within each script's OpenType feature tables, falling back + /// to the font's default language system when the font does not distinguish the + /// language. When the current culture applies, matching the + /// ambient language model of the reference shaping engines; use + /// to express no language preference and + /// always shape with the font's default language system. + /// + public CultureInfo? Culture { get; set; } + /// /// Gets or sets an optional collection of text runs to apply to the body of text. /// diff --git a/src/SixLabors.Fonts/TextShaper.cs b/src/SixLabors.Fonts/TextShaper.cs new file mode 100644 index 000000000..d327e4c8c --- /dev/null +++ b/src/SixLabors.Fonts/TextShaper.cs @@ -0,0 +1,91 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts; + +/// +/// Encapsulates logic for shaping text into a positioned glyph stream. +/// +/// +/// +/// Shaping runs the pipeline text layout uses: bidi analysis, font and text run +/// itemization, fallback font resolution, and the font's substitution and positioning +/// features. The result is the glyph stream in logical (source) order, before line +/// breaking, visual reordering, or scaling. Advances and offsets are expressed in font +/// design units; see for the conversion to pixel units. +/// +/// +/// External text stacks that itemize runs themselves shape one run per call: set a +/// single with no +/// so unmapped codepoints produce the +/// font's missing glyph, and pre-resolve the direction with +/// and . +/// Shaping is context sensitive, so a caller shaping a slice of a larger paragraph +/// passes the containing text and keeps the glyphs whose +/// falls inside the slice. +/// +/// +public static class TextShaper +{ + /// + public static IReadOnlyList Shape(string text, TextOptions options) + { + Guard.NotNull(text, nameof(text)); + + return Shape(text.AsSpan(), options); + } + + /// + /// Shapes the text into a positioned glyph stream using the supplied options. + /// + /// The text to shape. + /// + /// The text options. Shaping honors the font selection members + /// (, , + /// ), and + /// , , + /// , , and + /// . Layout members such as + /// , , wrapping, and + /// alignment do not affect shaping. + /// + /// The shaped glyphs in logical order. + public static IReadOnlyList Shape(ReadOnlySpan text, TextOptions options) + { + Guard.NotNull(options, nameof(options)); + + if (text.IsEmpty) + { + return []; + } + + GlyphPositioningCollection positionings = TextLayout.ShapeText(text, options).Positionings; + + var probe = ShapingProbe.Enter(); + List glyphs = new(positionings.Count); + for (int i = 0; i < positionings.Count; i++) + { + GlyphPositioningCollection.GlyphPositioningData data = positionings.GetPositioningData(i); + if (data.Data.IsPlaceholder) + { + // Placeholder runs reserve layout space for inline objects; they carry + // no glyph. + continue; + } + + FontGlyphMetrics metrics = data.Metrics; + glyphs.Add(new ShapedGlyph( + data.Font, + metrics.GlyphId, + data.Data.CodePoint, + data.Offset, + data.Data.CodePointCount, + metrics.AdvanceWidth, + metrics.AdvanceHeight, + metrics.Offset)); + } + + ShapingProbe.Exit(ShapingProbe.Projection, probe); + return glyphs; + } +} diff --git a/src/UnicodeTrieGenerator/Generator.OpenTypeLanguageTags.cs b/src/UnicodeTrieGenerator/Generator.OpenTypeLanguageTags.cs index f7874b95e..077598268 100644 --- a/src/UnicodeTrieGenerator/Generator.OpenTypeLanguageTags.cs +++ b/src/UnicodeTrieGenerator/Generator.OpenTypeLanguageTags.cs @@ -46,11 +46,21 @@ public static void GenerateOpenTypeLanguageTagMap() { List entries = ParseOpenTypeLanguageTagRegistry(); - // Key the registry rows by each ISO 639 code they list. Rows are alphabetical by - // language system name; within one code deprecated tags order after current ones. + // The Syriac script-variant tags are equivalents of ISO 15924 script codes, not + // languages: they are selected by BCP 47 script subtag in the hand-written + // resolver, and keying them by the ISO codes their rows list would outrank the + // plain Syriac tag for every Syriac language. + string[] scriptVariantTags = ["SYRE", "SYRJ", "SYRN"]; + + // Key the registry rows by each ISO 639 code they list. Dictionary> map = new(StringComparer.Ordinal); foreach (LanguageTagEntry entry in entries) { + if (scriptVariantTags.Contains(entry.Tag)) + { + continue; + } + foreach (string code in entry.IsoCodes) { if (!map.TryGetValue(code, out List? list)) @@ -59,17 +69,31 @@ public static void GenerateOpenTypeLanguageTagMap() map[code] = list; } - if (list.Any(x => x.Tag == entry.Tag)) + if (!list.Any(x => x.Tag == entry.Tag)) { - continue; + list.Add(entry); } - - int insertAt = entry.Deprecated ? list.Count : list.FindIndex(x => x.Deprecated); - list.Insert(insertAt < 0 ? list.Count : insertAt, entry); } } + // Order each code's candidates the way HarfBuzz disambiguates: current tags + // before deprecated ones, then tags registered for fewer languages first because + // they are the more specific claim (HYE0 "Armenian East" lists only hye, so it + // outranks HYE "Armenian" which also covers hyw), then registry order. + foreach (List list in map.Values) + { + List ordered = [.. list + .Select((entry, index) => (Entry: entry, Index: index)) + .OrderBy(x => x.Entry.Deprecated) + .ThenBy(x => x.Entry.IsoCodes.Count) + .ThenBy(x => x.Index) + .Select(x => x.Entry)]; + list.Clear(); + list.AddRange(ordered); + } + AddIanaAliases(map); + AddCuratedOverrides(map, entries); StringBuilder sb = new(); sb.AppendLine("// Copyright (c) Six Labors."); @@ -158,6 +182,84 @@ private static List ParseOpenTypeLanguageTagRegistry() return entries; } + /// + /// Applies the curated rows for which the two registries alone do not determine + /// the mapping. These follow the Uniscribe and DirectWrite behavior the OpenType + /// ecosystem is built around: Chinese macrolanguage members shape as simplified + /// Chinese with Literary Chinese as traditional, the Quechua family leads with + /// Cusco Quechua, and retired codes the IANA registry split without a preferred + /// value keep their historically assigned tag. The deliberate exception is + /// Serbo-Croatian (sh), which the runtime canonicalizes to Serbian before + /// resolution and therefore maps to SRB. + /// + /// The map keyed by ISO 639 code. + /// The parsed registry entries, for tag lookup. + private static void AddCuratedOverrides( + Dictionary> map, + List entries) + { + (string Code, string[] Tags)[] overrides = + [ + + // Chinese macrolanguage members. + ("cdo", ["ZHS "]), + ("cjy", ["ZHS "]), + ("cnp", ["ZHS "]), + ("cpx", ["ZHS "]), + ("csp", ["ZHS "]), + ("czh", ["ZHS "]), + ("czo", ["ZHS "]), + ("gan", ["ZHS "]), + ("hak", ["ZHS "]), + ("hnm", ["ZHS "]), + ("hsn", ["ZHS "]), + ("luh", ["ZHS "]), + ("lzh", ["ZHT "]), + ("mnp", ["ZHS "]), + ("nan", ["ZHS "]), + ("sjc", ["ZHS "]), + ("wuu", ["ZHS "]), + + // The Quechua family maps to Cusco Quechua; the registry defines no + // general Quechua tag. + ("que", ["QUZ "]), + ("quf", ["QUZ "]), + ("quk", ["QUZ "]), + ("quy", ["QUZ "]), + ("qvc", ["QUZ "]), + ("qve", ["QUZ "]), + ("qvs", ["QUZ "]), + ("qwc", ["QUZ "]), + ("qxp", ["QUZ "]), + ("qxu", ["QUZ "]), + + // Retired codes split by IANA without a preferred value. + ("dwk", ["KUI "]), + ("uki", ["KUI "]), + ("ggo", ["GON "]), + ("kpp", ["KRN "]), + ("nln", ["NAH "]), + ("xwo", ["TOD "]), + ]; + + Dictionary entriesByTag = []; + foreach (LanguageTagEntry entry in entries) + { + entriesByTag.TryAdd(entry.Tag, entry); + } + + foreach ((string code, string[] tags) in overrides) + { + List list = []; + foreach (string tag in tags) + { + list.Add(entriesByTag[tag]); + } + + map[code] = list; + } + } + /// /// Adds map keys for macrolanguages' sublanguages and retired codes' replacements /// from the IANA BCP 47 subtag registry, so lookups by those codes resolve to the @@ -217,16 +319,25 @@ private static void AddIanaAliases(Dictionary> ma } } - foreach ((string subtag, string target) in aliases.OrderBy(x => x.Subtag, StringComparer.Ordinal)) + // Resolve to a fixpoint so chained redirects land: a retired code can point at a + // sublanguage whose tags only exist through its macrolanguage, for example + // drh -> khk -> mn resolves to the Mongolian tags on the second pass. + bool added = true; + while (added) { - if (map.ContainsKey(subtag)) + added = false; + foreach ((string subtag, string target) in aliases.OrderBy(x => x.Subtag, StringComparer.Ordinal)) { - continue; - } + if (map.ContainsKey(subtag)) + { + continue; + } - if (TryResolveEntries(map, target, out List? entries)) - { - map[subtag] = entries; + if (TryResolveEntries(map, target, out List? entries)) + { + map[subtag] = entries; + added = true; + } } } } diff --git a/tests/Fonts/HarfBuzz-LanguageTags.ttf b/tests/Fonts/HarfBuzz-LanguageTags.ttf new file mode 100644 index 000000000..9078ad7dc --- /dev/null +++ b/tests/Fonts/HarfBuzz-LanguageTags.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8440df3446a0724e2498ba62980d55ad0b0ad5e568cdc0ad4efd85c7f8d4b455 +size 5596 diff --git a/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/Config.cs b/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/Config.cs new file mode 100644 index 000000000..4ffd1b34a --- /dev/null +++ b/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/Config.cs @@ -0,0 +1,57 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +#if OS_WINDOWS +using System.Security.Principal; +using BenchmarkDotNet.Diagnostics.Windows; +#endif +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Environments; +using BenchmarkDotNet.Exporters; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Loggers; +using BenchmarkDotNet.Reports; + +namespace SixLabors.Fonts.Benchmarks; + +public class Config : ManualConfig +{ + public Config() + { + this.AddLogger(ConsoleLogger.Default); + this.AddColumnProvider(DefaultColumnProviders.Instance); + this.AddExporter(MarkdownExporter.GitHub, DefaultExporters.Html, DefaultExporters.Csv); + this.AddDiagnoser(MemoryDiagnoser.Default); + +#if OS_WINDOWS + if (this.IsElevated) + { + this.AddDiagnoser(new NativeMemoryProfiler()); + } +#endif + + this.SummaryStyle = SummaryStyle.Default.WithMaxParameterColumnWidth(50); + } + + public class Standard : Config + { + public Standard() => this.AddJob( + Job.Default.WithRuntime(CoreRuntime.Core80).WithArguments([new MsBuildArgument("/p:DebugType=portable")])); + } + + public class Short : Config + { + public Short() => this.AddJob( + Job.Default.WithRuntime(CoreRuntime.Core80) + .WithLaunchCount(1) + .WithWarmupCount(3) + .WithIterationCount(3) + .WithArguments([new MsBuildArgument("/p:DebugType=portable")])); + } + +#if OS_WINDOWS + private bool IsElevated => new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator); +#endif +} diff --git a/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/MeasureTextBenchmark.cs b/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/MeasureTextBenchmark.cs index b5726cd7b..91f70687d 100644 --- a/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/MeasureTextBenchmark.cs +++ b/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/MeasureTextBenchmark.cs @@ -69,6 +69,7 @@ public void Dispose() this.arialTypeface.Dispose(); this.font.Dispose(); this.paint.Dispose(); + GC.SuppressFinalize(this); } [Params("a", "Hello world", "The quick brown fox jumps over the lazy dog", LoremIpsum)] diff --git a/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/ShapeTextBenchmark.cs b/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/ShapeTextBenchmark.cs new file mode 100644 index 000000000..fffd9dc92 --- /dev/null +++ b/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/ShapeTextBenchmark.cs @@ -0,0 +1,156 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using BenchmarkDotNet.Attributes; +using HarfBuzzSharp; +using HBBuffer = HarfBuzzSharp.Buffer; +using HBFace = HarfBuzzSharp.Face; +using HBFont = HarfBuzzSharp.Font; + +namespace SixLabors.Fonts.Benchmarks; + +/// +/// Defines the text shape used by . +/// +public enum ShapeTextBenchmarkScenario +{ + /// + /// Latin text with standard ligature opportunities, shaped with Open Sans. + /// + Latin, + + /// + /// Arabic text exercising joining forms and mandatory ligatures, shaped with Dubai. + /// + Arabic +} + +/// +/// Compares end to end single run text shaping between and +/// HarfBuzz via HarfBuzzSharp. Both sides shape identical font file bytes and walk the +/// resulting glyph stream, summing the advances so the output is fully consumed. +/// +/// +/// The operations are not identical: runs bidi analysis and run +/// itemization internally, while HarfBuzz expects the caller to have segmented the text +/// and only guesses the buffer's script, direction, and language. The comparison measures +/// what a consumer pays for a shaped glyph stream through each API. +/// +[Config(typeof(Config.Short))] +public class ShapeTextBenchmark : IDisposable +{ + private string text = string.Empty; + private TextOptions textOptions = null!; + private Blob? blob; + private HBFace? face; + private HBFont? hbFont; + private HBBuffer buffer = null!; + + /// + /// Gets or sets the text scenario used by the benchmark. + /// + [Params(ShapeTextBenchmarkScenario.Latin, ShapeTextBenchmarkScenario.Arabic)] + public ShapeTextBenchmarkScenario Scenario { get; set; } + + /// + /// Initializes the input text, fonts, and HarfBuzz state for each scenario. + /// + [GlobalSetup] + public void SetUp() + { + string fontPath; + if (this.Scenario == ShapeTextBenchmarkScenario.Latin) + { + fontPath = GetFontPath("OpenSans-Regular.ttf"); + this.text = "The quick brown fox jumps over the lazy dog; fifty fluffy waffles."; + } + else + { + fontPath = GetFontPath("Dubai-Regular.ttf"); + this.text = "ุณู„ุงู… ุนู„ูŠูƒู… ูˆุฑุญู…ุฉ ุงู„ู„ู‡ ูˆุจุฑูƒุงุชู‡ ู„ุง ุฅู„ู‡ ุฅู„ุง ุงู„ู„ู‡"; + } + + Font font = new FontCollection().Add(fontPath).CreateFont(16); + this.textOptions = new TextOptions(font); + + this.blob = Blob.FromFile(fontPath); + this.face = new HBFace(this.blob, 0); + this.hbFont = new HBFont(this.face); + this.hbFont.SetFunctionsOpenType(); + this.buffer = new HBBuffer(); + } + + /// + /// Shapes the text with and sums the resulting advances. + /// + /// The advance sum, returned so the shaped stream is fully consumed. + [Benchmark] + public int ShapeSixLaborsFonts() + { + IReadOnlyList glyphs = TextShaper.Shape(this.text, this.textOptions); + + int advanceSum = 0; + for (int i = 0; i < glyphs.Count; i++) + { + advanceSum += glyphs[i].AdvanceWidth; + } + + return advanceSum; + } + + /// + /// Shapes the text with HarfBuzz, reusing one buffer as production text stacks do, + /// and sums the resulting advances. + /// + /// The advance sum, returned so the shaped stream is fully consumed. + [Benchmark(Baseline = true)] + public int ShapeHarfBuzz() + { + this.buffer.Reset(); + this.buffer.AddUtf16(this.text); + this.buffer.GuessSegmentProperties(); + this.hbFont!.Shape(this.buffer); + + ReadOnlySpan positions = this.buffer.GetGlyphPositionSpan(); + + int advanceSum = 0; + for (int i = 0; i < positions.Length; i++) + { + advanceSum += positions[i].XAdvance; + } + + return advanceSum; + } + + /// + public void Dispose() + { + this.buffer?.Dispose(); + this.hbFont?.Dispose(); + this.face?.Dispose(); + this.blob?.Dispose(); + GC.SuppressFinalize(this); + } + + /// + /// Resolves a test font path by walking up from the benchmark output directory to + /// the repository root. + /// + /// The font file name within tests/Fonts. + /// The full font path. + private static string GetFontPath(string fileName) + { + DirectoryInfo? directory = new(AppContext.BaseDirectory); + while (directory != null && !File.Exists(Path.Combine(directory.FullName, "SixLabors.Fonts.sln"))) + { + directory = directory.Parent; + } + + if (directory is null) + { + throw new IOException("Unable to locate the repository root."); + } + + return Path.Combine(directory.FullName, "tests", "Fonts", fileName); + } +} diff --git a/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks.csproj b/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks.csproj index 139adf4da..e446d31a4 100644 --- a/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks.csproj +++ b/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks.csproj @@ -32,5 +32,8 @@ + + + diff --git a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/OpenTypeLanguageTagMapTests.cs b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/OpenTypeLanguageTagMapTests.cs index 00e75e430..c5802bd57 100644 --- a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/OpenTypeLanguageTagMapTests.cs +++ b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/OpenTypeLanguageTagMapTests.cs @@ -8,70 +8,140 @@ namespace SixLabors.Fonts.Tests.Tables.AdvancedTypographic; public class OpenTypeLanguageTagMapTests { + /// + /// Rows ported from the HarfBuzz hb_ot_tags_from_language test corpus + /// (test/api/test-ot-tag.c), asserting the primary candidate tag. Rows whose inputs + /// cannot be expressed as a are omitted: private use + /// extensions (x-hbot), grandfathered tags (i-lux, zh-min-nan), the und language, + /// and HarfBuzz's locale syntax (tr@foo=bar). Rows where the runtime demands a + /// region between the language and a variant subtag carry one. + /// [Theory] - [InlineData("tr", "TRK ")] - [InlineData("tr-TR", "TRK ")] - [InlineData("de", "DEU ")] - [InlineData("de-AT", "DEU ")] - [InlineData("sr", "SRB ")] + [InlineData("alt", "ALT ")] + [InlineData("ar", "ARA ")] + [InlineData("ar-001", "ARA ")] [InlineData("az", "AZE ")] - [InlineData("hi", "HIN ")] + [InlineData("az-IR", "AZE ")] + [InlineData("en", "ENG ")] + [InlineData("en-US", "ENG ")] + [InlineData("cjm", "CJM ")] + [InlineData("eve", "EVN ")] + [InlineData("cfm", "HAL ")] + [InlineData("hy", "HYE0")] + [InlineData("hyw", "HYE ")] + [InlineData("bgr", "QIN ")] + [InlineData("cnh", "QIN ")] + [InlineData("ctd", "QIN ")] + [InlineData("zom", "QIN ")] [InlineData("fa", "FAR ")] - [InlineData("el", "ELL ")] + [InlineData("fa-IR", "FAR ")] + [InlineData("man", "MNK ")] + [InlineData("aii", "SWA ")] + [InlineData("syr", "SYR ")] + [InlineData("amw", "SYR ")] + [InlineData("cld", "SYR ")] + [InlineData("syc", "SYR ")] + [InlineData("tru", "TUA ")] + [InlineData("ghc", "IRT ")] + [InlineData("ga-Latg", "IRT ")] + [InlineData("ka-Geok", "KGE ")] + [InlineData("ro-MD", "MOL ")] + [InlineData("el-CY-polyton", "PGR ")] + [InlineData("el-GR-polyton", "PGR ")] + [InlineData("en-US-fonipa", "IPPH")] + [InlineData("zh-CN-fonipa", "IPPH")] + [InlineData("en-US-fonnapa", "APPH")] + [InlineData("chr-US-fonnapa", "APPH")] + [InlineData("aii-Syre", "SYRE")] + [InlineData("de-Syre", "SYRE")] + [InlineData("syr-Syre", "SYRE")] + [InlineData("aii-Syrj", "SYRJ")] + [InlineData("de-Syrj", "SYRJ")] + [InlineData("syr-Syrj", "SYRJ")] + [InlineData("aii-Syrn", "SYRN")] + [InlineData("de-Syrn", "SYRN")] + [InlineData("syr-Syrn", "SYRN")] + [InlineData("aao", "ARA ")] + [InlineData("gom", "KOK ")] + [InlineData("drh", "MNG ")] + [InlineData("als", "SQI ")] + [InlineData("nb", "NOR ")] [InlineData("nn", "NYN ")] - [InlineData("ro", "ROM ")] - public void ResolvesRegistryTag(string cultureName, string expected) + [InlineData("hak", "ZHS ")] + [InlineData("wuu", "ZHS ")] + [InlineData("lzh", "ZHT ")] + [InlineData("qu", "QUZ ")] + [InlineData("quy", "QUZ ")] + [InlineData("dwk", "KUI ")] + [InlineData("ggo", "GON ")] + [InlineData("kpp", "KRN ")] + [InlineData("nln", "NAH ")] + [InlineData("xwo", "TOD ")] + public void ResolvesHarfBuzzCorpusPrimaryTag(string cultureName, string expected) { Assert.True(OpenTypeLanguageTagMap.TryGetTags(new CultureInfo(cultureName), out Tag[] tags)); Assert.Equal(Tag.Parse(expected), tags[0]); } + /// + /// The Chinese rows from the HarfBuzz corpus: the tags encode script and region, an + /// explicit simplified script overrides any region, traditional defers to the Macao + /// and Hong Kong conventions, and bare Cantonese defaults to Hong Kong. + /// [Theory] [InlineData("zh", "ZHS ")] [InlineData("zh-CN", "ZHS ")] - [InlineData("zh-Hans", "ZHS ")] - [InlineData("zh-Hans-SG", "ZHS ")] - [InlineData("zh-Hant", "ZHT ")] - [InlineData("zh-TW", "ZHT ")] + [InlineData("zh-SG", "ZHS ")] + [InlineData("zh-MO", "ZHTM")] + [InlineData("zh-Hant-MO", "ZHTM")] + [InlineData("zh-Hans-MO", "ZHS ")] [InlineData("zh-HK", "ZHH ")] - [InlineData("zh-MO", "ZHH ")] [InlineData("zh-Hant-HK", "ZHH ")] + [InlineData("zh-Hans-HK", "ZHS ")] + [InlineData("zh-TW", "ZHT ")] + [InlineData("zh-Hans", "ZHS ")] + [InlineData("zh-Hant", "ZHT ")] + [InlineData("zh-Hans-TW", "ZHS ")] + [InlineData("yue", "ZHH ")] + [InlineData("yue-Hant", "ZHH ")] + [InlineData("yue-Hans", "ZHS ")] public void ResolvesChineseByScriptAndRegion(string cultureName, string expected) { - // Chinese language system tags encode script and region: simplified, traditional, - // and the Hong Kong and Macao conventions must each win for their cultures. Assert.True(OpenTypeLanguageTagMap.TryGetTags(new CultureInfo(cultureName), out Tag[] tags)); Assert.Equal(Tag.Parse(expected), tags[0]); } - [Fact] - public void ChineseHongKongFallsBackToTraditional() + [Theory] + [InlineData("zh-HK", "ZHH ", "ZHT ")] + [InlineData("zh-MO", "ZHTM", "ZHH ")] + public void ChineseRegionalConventionsFallBackToTraditional(string cultureName, string first, string second) { - Assert.True(OpenTypeLanguageTagMap.TryGetTags(new CultureInfo("zh-HK"), out Tag[] tags)); - - // A font without ZHH should still get traditional forms before simplified ones. - Assert.Equal(Tag.Parse("ZHH "), tags[0]); - Assert.Equal(Tag.Parse("ZHT "), tags[1]); + // A font without the regional conventions must still get the nearest regional + // then traditional forms before simplified ones; Macao falls back through the + // Hong Kong conventions per the HarfBuzz language-tags expectations. + Assert.True(OpenTypeLanguageTagMap.TryGetTags(new CultureInfo(cultureName), out Tag[] tags)); + Assert.Equal(Tag.Parse(first), tags[0]); + Assert.Equal(Tag.Parse(second), tags[1]); + Assert.Contains(Tag.Parse("ZHT "), tags); } [Fact] - public void ResolvesVariantSubtag() + public void VariantOutranksLanguageMapping() { - // Polytonic Greek is registered by BCP 47 variant subtag rather than ISO code and - // must outrank the plain Greek mapping. The runtime requires a region between the - // language and variant subtags and uppercases the variant in the culture name. + // The variant defines the transcription system; the plain language mapping + // remains as a candidate for fonts without the phonetic language system. Assert.True(OpenTypeLanguageTagMap.TryGetTags(new CultureInfo("el-GR-polyton"), out Tag[] tags)); Assert.Equal(Tag.Parse("PGR "), tags[0]); Assert.Contains(Tag.Parse("ELL "), tags); } [Fact] - public void ResolvesMacrolanguageSublanguage() + public void UnknownIsoCodeSynthesizesUppercaseTag() { - // Algerian Arabic has no registry row of its own; the IANA registry keys it to the - // Arabic macrolanguage. - Assert.True(OpenTypeLanguageTagMap.TryGetTags(new CultureInfo("aao"), out Tag[] tags)); - Assert.Equal(Tag.Parse("ARA "), tags[0]); + // The specification directs fonts supporting languages without registered tags + // to use the uppercase ISO 639-3 code, matching the HarfBuzz xyz -> XYZ row. + Assert.True(OpenTypeLanguageTagMap.TryGetTags(new CultureInfo("xyz"), out Tag[] tags)); + Assert.Equal(Tag.Parse("XYZ "), tags[0]); } [Fact] diff --git a/tests/SixLabors.Fonts.Tests/TestFonts.cs b/tests/SixLabors.Fonts.Tests/TestFonts.cs index 7639b7313..5a0aa674d 100644 --- a/tests/SixLabors.Fonts.Tests/TestFonts.cs +++ b/tests/SixLabors.Fonts.Tests/TestFonts.cs @@ -19,6 +19,12 @@ public static class TestFonts // retaining the BASE table on both axes plus the vertical metrics and layout features. public static string NotoSansSCBaselineSubsetFile => GetFullPath("Noto_Sans_SC/NotoSansSC-BaselineSubset.ttf"); + // Language system selection fixture from the HarfBuzz test suite (MIT licensed, + // test/shape/data/in-house/fonts/6991b13ce889466be6de3f66e891de2bc0f117ee.ttf, + // exercised by language-tags.tests): 'J' shapes to a different glyph per selected + // KOR/ZHS/ZHT/ZHH language system, and the font declares no default language system. + public static string LanguageTagsFile => GetFullPath("HarfBuzz-LanguageTags.ttf"); + public static string WendyOneFile => GetFullPath("Wendy_One/WendyOne-Regular.ttf"); // Font from: https://google-webfonts-helper.herokuapp.com/fonts/open-sans?subsets=cyrillic,cyrillic-ext,greek,greek-ext,hebrew,latin,latin-ext,vietnamese diff --git a/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs b/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs index 8f7cff732..f6c72c146 100644 --- a/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs +++ b/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs @@ -166,6 +166,7 @@ private static RichTextOptions FromTextOptions(TextOptions options, bool customD Tracking = options.Tracking, ColorFontSupport = options.ColorFontSupport, FeatureTags = new List(options.FeatureTags), + Culture = options.Culture, }; if (options.TextRuns.Count > 0) diff --git a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs index f925cb909..4180d45a2 100644 --- a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs +++ b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs @@ -308,7 +308,12 @@ public void MeasureTextWordWrappingHorizontalTopBottom(string text, float height TextOptions options = new(font) { WrappingLength = 350, - LayoutMode = LayoutMode.HorizontalTopBottom + LayoutMode = LayoutMode.HorizontalTopBottom, + + // SimSun declares no default language system: every feature, including + // the vertical alternates, lives under its Chinese language systems, so + // the culture is pinned to keep the measurements machine independent. + Culture = new CultureInfo("zh-CN") }; TextLayoutTestUtilities.TestLayout(text, options, properties: new { height, width }); @@ -334,7 +339,12 @@ public void MeasureTextWordWrappingHorizontalBottomTop(string text, float height TextOptions options = new(font) { WrappingLength = 350, - LayoutMode = LayoutMode.HorizontalBottomTop + LayoutMode = LayoutMode.HorizontalBottomTop, + + // SimSun declares no default language system: every feature, including + // the vertical alternates, lives under its Chinese language systems, so + // the culture is pinned to keep the measurements machine independent. + Culture = new CultureInfo("zh-CN") }; TextLayoutTestUtilities.TestLayout(text, options, properties: new { height, width }); @@ -357,7 +367,12 @@ public void MeasureTextWordWrappingVerticalLeftRight(string text, float height, TextOptions options = new(font) { WrappingLength = 350, - LayoutMode = LayoutMode.VerticalLeftRight + LayoutMode = LayoutMode.VerticalLeftRight, + + // SimSun declares no default language system: every feature, including + // the vertical alternates, lives under its Chinese language systems, so + // the culture is pinned to keep the measurements machine independent. + Culture = new CultureInfo("zh-CN") }; TextLayoutTestUtilities.TestLayout(text, options, properties: new { height, width }); @@ -380,7 +395,12 @@ public void MeasureTextWordWrappingVerticalRightLeft(string text, float height, TextOptions options = new(font) { WrappingLength = 350, - LayoutMode = LayoutMode.VerticalRightLeft + LayoutMode = LayoutMode.VerticalRightLeft, + + // SimSun declares no default language system: every feature, including + // the vertical alternates, lives under its Chinese language systems, so + // the culture is pinned to keep the measurements machine independent. + Culture = new CultureInfo("zh-CN") }; TextLayoutTestUtilities.TestLayout(text, options, properties: new { height, width }); @@ -403,7 +423,12 @@ public void MeasureTextWordWrappingVerticalMixedLeftRight(string text, float hei TextOptions options = new(font) { WrappingLength = 350, - LayoutMode = LayoutMode.VerticalMixedLeftRight + LayoutMode = LayoutMode.VerticalMixedLeftRight, + + // SimSun declares no default language system: every feature, including + // the vertical alternates, lives under its Chinese language systems, so + // the culture is pinned to keep the measurements machine independent. + Culture = new CultureInfo("zh-CN") }; TextLayoutTestUtilities.TestLayout(text, options, properties: new { height, width }); diff --git a/tests/SixLabors.Fonts.Tests/TextShaperTests.cs b/tests/SixLabors.Fonts.Tests/TextShaperTests.cs new file mode 100644 index 000000000..f9dc9e6bc --- /dev/null +++ b/tests/SixLabors.Fonts.Tests/TextShaperTests.cs @@ -0,0 +1,214 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Globalization; + +namespace SixLabors.Fonts.Tests; + +public class TextShaperTests +{ + [Fact] + public void Shape_Latin_ProducesSequentialGlyphStream() + { + Font font = new FontCollection().Add(TestFonts.OpenSansFile).CreateFont(72); + IReadOnlyList glyphs = TextShaper.Shape("Hxp", new TextOptions(font)); + + Assert.Equal(3, glyphs.Count); + for (int i = 0; i < glyphs.Count; i++) + { + Assert.Equal(i, glyphs[i].CodePointIndex); + Assert.Equal(1, glyphs[i].CodePointCount); + Assert.NotEqual(0, glyphs[i].GlyphId); + Assert.True(glyphs[i].AdvanceWidth > 0); + Assert.Same(font, glyphs[i].Font); + } + } + + [Fact] + public void Shape_EmptyText_ProducesNoGlyphs() + => Assert.Empty(TextShaper.Shape( + string.Empty, + new TextOptions(new FontCollection().Add(TestFonts.OpenSansFile).CreateFont(72)))); + + [Fact] + public void Shape_AdvancesMatchMeasuredAdvance() + { + // Design-unit advances scaled to pixel units must agree with the measured + // logical advance for a single unwrapped line. + Font font = new FontCollection().Add(TestFonts.OpenSansFile).CreateFont(72); + TextOptions options = new(font); + const string text = "Hxplq"; + + IReadOnlyList glyphs = TextShaper.Shape(text, options); + + float scale = font.Size / font.FontMetrics.UnitsPerEm; + float shapedAdvance = 0; + foreach (ShapedGlyph glyph in glyphs) + { + shapedAdvance += glyph.AdvanceWidth * scale; + } + + FontRectangle measured = TextMeasurer.MeasureAdvance(text, options); + Assert.Equal(measured.Width, shapedAdvance, 3F); + } + + [Fact] + public void Shape_Ligature_MergesCodePoints() + { + // Dubai applies mandatory Arabic ligatures; Lam + Alef must merge into a single + // glyph spanning both codepoints. + Font font = new FontCollection().Add(TestFonts.ArabicFontFile).CreateFont(72); + IReadOnlyList glyphs = TextShaper.Shape("ู„ุง", new TextOptions(font)); + + Assert.Single(glyphs); + Assert.Equal(0, glyphs[0].CodePointIndex); + Assert.Equal(2, glyphs[0].CodePointCount); + } + + [Fact] + public void Shape_RightToLeft_KeepsLogicalOrder() + { + // The shaper reports glyphs in logical (source) order; visual reordering is a + // layout concern. + Font font = new FontCollection().Add(TestFonts.ArabicFontFile).CreateFont(72); + IReadOnlyList glyphs = TextShaper.Shape("ุณู„ุงู…", new TextOptions(font)); + + Assert.True(glyphs.Count > 1); + for (int i = 1; i < glyphs.Count; i++) + { + Assert.True(glyphs[i].CodePointIndex > glyphs[i - 1].CodePointIndex); + } + } + + [Fact] + public void Shape_UnmappedCodePoint_ProducesMissingGlyph() + { + // With no fallback fonts configured an unmapped codepoint emits the font's + // missing glyph, matching the single-face shaping model. + Font font = new FontCollection().Add(TestFonts.OpenSansFile).CreateFont(72); + IReadOnlyList glyphs = TextShaper.Shape("โ˜ƒ", new TextOptions(font)); + + Assert.Single(glyphs); + Assert.Equal(0, glyphs[0].GlyphId); + } + + [Fact] + public void Shape_Vertical_PopulatesAdvanceHeight() + { + Font font = new FontCollection().Add(TestFonts.NotoSansSCBaselineSubsetFile).CreateFont(72); + TextOptions options = new(font) + { + LayoutMode = LayoutMode.VerticalLeftRight + }; + + IReadOnlyList glyphs = TextShaper.Shape("ๆฐธๅ›ฝ", options); + + Assert.Equal(2, glyphs.Count); + foreach (ShapedGlyph glyph in glyphs) + { + Assert.True(glyph.AdvanceHeight > 0); + } + } + + [Theory] + [InlineData("ro-RO")] + [InlineData("ro-MD")] + public void Shape_Culture_AppliesRomanianLocalizedForms(string cultureName) + { + // Open Sans carries ROM and MOL language systems whose locl feature substitutes + // the legacy cedilla forms with the correct comma accent forms: s cedilla + // (U+015F) must shape as the s comma (U+0219) glyph. Moldova resolves to MOL and + // Romania to ROM; both select the same substitution in this font. + Font font = new FontCollection().Add(TestFonts.OpenSansFile).CreateFont(72); + + ushort plain = Assert.Single(TextShaper.Shape("ลŸ", new TextOptions(font))).GlyphId; + ushort expected = Assert.Single(TextShaper.Shape("ศ™", new TextOptions(font))).GlyphId; + Assert.NotEqual(plain, expected); + + TextOptions options = new(font) + { + Culture = new CultureInfo(cultureName) + }; + + Assert.Equal(expected, Assert.Single(TextShaper.Shape("ลŸ", options)).GlyphId); + } + + [Theory] + [InlineData("sr-RS")] + [InlineData("mk-MK")] + public void Shape_Culture_AppliesSerbianCyrillicForms(string cultureName) + { + // Open Sans carries SRB and MKD language systems on the Cyrillic script whose + // locl feature substitutes the Cyrillic be (U+0431) with its Serbian form. The + // substituted glyph has no codepoint of its own, so the assertion pins that the + // culture changes the resolved glyph. + Font font = new FontCollection().Add(TestFonts.OpenSansFile).CreateFont(72); + + ushort plain = Assert.Single(TextShaper.Shape("ะฑ", new TextOptions(font))).GlyphId; + + TextOptions options = new(font) + { + Culture = new CultureInfo(cultureName) + }; + + ushort localized = Assert.Single(TextShaper.Shape("ะฑ", options)).GlyphId; + Assert.NotEqual(plain, localized); + Assert.NotEqual(0, localized); + } + + [Fact] + public void Shape_Culture_WithoutMatchingLanguageSystem_UsesDefault() + { + // Turkish has no language system in Open Sans, so shaping falls back to the + // default language system and the localized substitution must not apply. + Font font = new FontCollection().Add(TestFonts.OpenSansFile).CreateFont(72); + + ushort plain = Assert.Single(TextShaper.Shape("ลŸ", new TextOptions(font))).GlyphId; + + TextOptions options = new(font) + { + Culture = new CultureInfo("tr-TR") + }; + + Assert.Equal(plain, Assert.Single(TextShaper.Shape("ลŸ", options)).GlyphId); + } + + /// + /// Mirrors the HarfBuzz language-tags.tests shaping expectations for the + /// HarfBuzz-LanguageTags fixture font: 'J' is substituted via locl to a different + /// glyph per selected language system on the latn script. The font declares no + /// default language system, so a language the font does not carry, and the invariant + /// culture's absent language preference, must apply no substitutions rather than + /// aggregating the named language systems' lookups. A null culture is not pinned + /// here: it resolves the ambient current culture, mirroring the reference engines. + /// + /// The culture to shape with, or for the invariant culture. + /// The expected glyph id from the HarfBuzz expectations. + [Theory] + [InlineData(null, 2)] + [InlineData("fa", 2)] + [InlineData("ja", 2)] + [InlineData("zh", 4)] + [InlineData("zh-CN", 4)] + [InlineData("zh-SG", 4)] + [InlineData("zh-TW", 5)] + [InlineData("zh-Hans", 4)] + [InlineData("zh-Hant", 5)] + [InlineData("zh-Hant-HK", 6)] + [InlineData("zh-HK", 6)] + [InlineData("zh-MO", 6)] + [InlineData("zh-Hant-MO", 6)] + public void Shape_Culture_MatchesHarfBuzzLanguageTagExpectations(string? cultureName, int expectedGlyphId) + { + Font font = new FontCollection().Add(TestFonts.LanguageTagsFile).CreateFont(72); + TextOptions options = new(font) + { + Culture = cultureName is null ? CultureInfo.InvariantCulture : new CultureInfo(cultureName) + }; + + IReadOnlyList glyphs = TextShaper.Shape("J", options); + + Assert.Single(glyphs); + Assert.Equal(expectedGlyphId, glyphs[0].GlyphId); + } +} From 2df90daddef1ffc699775d386462c9cd99c687d5 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 23 Jul 2026 20:16:33 +1000 Subject: [PATCH 02/94] Optimize glyph shaping pipeline and lookup caching Refactors shaping collections by replacing `IGlyphShapingCollection` with a shared `GlyphShapingCollection` base class, centralizing digest tracking, feature mask operations, and language/tag state. Improves shaping performance by caching GSUB/GPOS feature lookup resolution (with variable-font bypass), adding digest-based lookup/glyph gates, and using range feature assignment to reduce per-glyph overhead. Also switches `GlyphShapingBounds` to an in-place mutable struct, reuses substitution data during positioning, and streamlines glyph-skip logic via packed shaping-class bitmasks. --- .../GlyphPositioningCollection.cs | 130 +++++-------- src/SixLabors.Fonts/GlyphShapingBounds.cs | 8 +- src/SixLabors.Fonts/GlyphShapingClass.cs | 26 +++ src/SixLabors.Fonts/GlyphShapingCollection.cs | 174 ++++++++++++++++++ src/SixLabors.Fonts/GlyphShapingData.cs | 25 ++- .../GlyphSubstitutionCollection.cs | 81 ++------ .../IGlyphShapingCollection.cs | 83 --------- src/SixLabors.Fonts/ShapingProbe.cs | 19 +- .../AdvancedTypographicUtils.cs | 6 +- .../AdvancedTypographic/FeatureLookupsKey.cs | 78 ++++++++ .../Tables/AdvancedTypographic/GPosTable.cs | 66 ++++++- .../Tables/AdvancedTypographic/GSubTable.cs | 85 ++++++++- .../Shapers/ArabicShaper.cs | 4 +- .../AdvancedTypographic/Shapers/BaseShaper.cs | 12 +- .../Shapers/DefaultShaper.cs | 22 +-- .../Shapers/HangulShaper.cs | 4 +- .../Shapers/HebrewShaper.cs | 2 +- .../Shapers/IndicShaper.cs | 10 +- .../Shapers/MyanmarShaper.cs | 6 +- .../Shapers/ShapingStage.cs | 10 +- .../AdvancedTypographic/Shapers/ThaiShaper.cs | 2 +- .../Shapers/UniversalShaper.cs | 16 +- .../SkippingGlyphIterator.cs | 48 +++-- 23 files changed, 587 insertions(+), 330 deletions(-) create mode 100644 src/SixLabors.Fonts/GlyphShapingCollection.cs delete mode 100644 src/SixLabors.Fonts/IGlyphShapingCollection.cs create mode 100644 src/SixLabors.Fonts/Tables/AdvancedTypographic/FeatureLookupsKey.cs diff --git a/src/SixLabors.Fonts/GlyphPositioningCollection.cs b/src/SixLabors.Fonts/GlyphPositioningCollection.cs index d843a4d60..4c6b881f2 100644 --- a/src/SixLabors.Fonts/GlyphPositioningCollection.cs +++ b/src/SixLabors.Fonts/GlyphPositioningCollection.cs @@ -13,13 +13,12 @@ namespace SixLabors.Fonts; /// /// Represents a collection of glyph metrics that are mapped to input codepoints. /// -internal sealed class GlyphPositioningCollection : IGlyphShapingCollection +internal sealed class GlyphPositioningCollection : GlyphShapingCollection { /// /// 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 = []; - private GlyphSetDigest glyphDigest; /// /// Initializes a new instance of the class. @@ -27,78 +26,20 @@ internal sealed class GlyphPositioningCollection : IGlyphShapingCollection /// The text options. /// The feature bit assignment shared by the shaping pass. public GlyphPositioningCollection(TextOptions textOptions, ShapingFeatureMap featureMap) + : base(textOptions, featureMap) { - this.TextOptions = textOptions; - this.FeatureMap = featureMap; - - // A null culture takes the ambient current culture, mirroring the reference - // shaping engine model where an unset buffer language is guessed from the - // locale. CultureInfo.InvariantCulture expresses no language preference. - CultureInfo culture = textOptions.Culture ?? CultureInfo.CurrentCulture; - this.LanguageTags = OpenTypeLanguageTagMap.TryGetTags(culture, out Tag[] tags) ? tags : []; } /// - public int Count => this.glyphs.Count; + public override int Count => this.glyphs.Count; /// - public TextOptions TextOptions { get; } - - /// - public Tag[] LanguageTags { get; } - - /// - public GlyphSetDigest GlyphDigest => this.glyphDigest; - - /// - public ShapingFeatureMap FeatureMap { get; } - - /// - public GlyphShapingData this[int index] + public override GlyphShapingData this[int index] { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => this.glyphs[index].Data; } - /// - public void SetGlyphId(int index, ushort glyphId) - { - this.glyphDigest.Add(glyphId); - this.glyphs[index].Data.GlyphId = glyphId; - } - - /// - public void AddShapingFeature(int index, TagEntry feature) - { - // Registration only ever accumulates: adding a disabled entry for an already - // enabled feature must not clear the enabled bit, matching the list model this - // replaced where a disabled duplicate left earlier enabled entries in force. - GlyphShapingData data = this.glyphs[index].Data; - ulong mask = this.FeatureMap.GetOrAddMask(feature.Tag); - data.RegisteredFeatureMask |= mask; - if (feature.Enabled) - { - data.FeatureMask |= mask; - } - } - - /// - public void EnableShapingFeature(int index, Tag feature) - { - // Intersecting with the registered mask preserves the contract that enabling a - // feature a shaper never added for this glyph is a no-op. - GlyphShapingData data = this.glyphs[index].Data; - data.FeatureMask |= data.RegisteredFeatureMask & this.FeatureMap.GetMask(feature); - } - - /// - public void DisableShapingFeature(int index, Tag feature) - { - // An unregistered tag yields a zero mask whose complement clears nothing. - GlyphShapingData data = this.glyphs[index].Data; - data.FeatureMask &= ~this.FeatureMap.GetMask(feature); - } - /// /// Gets the full positioning data at the given index. /// @@ -236,13 +177,23 @@ public bool TryUpdate(Font font, GlyphSubstitutionCollection collection) } // We only want a single dimensional advance for positioning. - GlyphShapingBounds bounds = isVertical - ? new(0, 0, 0, metrics.AdvanceHeight) - : new(0, 0, metrics.AdvanceWidth, 0); // Track the number of inserted glyphs at the offset so we can correctly increment our position. - this.glyphDigest.Add(metrics.GlyphId); - this.glyphs.Insert(i += replacementCount, new(offset, new(shape, true) { Bounds = bounds }, font, pointSize, metrics.CloneForRendering(shape.TextRun))); + // The substituted data is reused rather than copied: the + // substitution collection releases its instances at the end of + // each run, so positioning takes ownership. + shape.ClearFeatures(); + if (isVertical) + { + shape.Bounds = new(0, 0, 0, metrics.AdvanceHeight); + } + else + { + shape.Bounds = new(0, 0, metrics.AdvanceWidth, 0); + } + + this.RecordGlyphId(metrics.GlyphId); + this.glyphs.Insert(i += replacementCount, new(offset, shape, font, pointSize, metrics.CloneForRendering(shape.TextRun))); replacementCount++; } } @@ -301,17 +252,20 @@ public bool TryAdd(Font font, GlyphSubstitutionCollection collection) this.TextOptions.Dpi, data.TextRun); - GlyphShapingBounds placeholderBounds = layoutMode.IsVertical() - ? new(0, 0, 0, placeholderMetrics.AdvanceHeight) - : new(0, 0, placeholderMetrics.AdvanceWidth, 0); - - GlyphShapingData placeholderData = new(data, true) + GlyphShapingData placeholderData = data; + placeholderData.ClearFeatures(); + if (layoutMode.IsVertical()) + { + placeholderData.Bounds = new(0, 0, 0, placeholderMetrics.AdvanceHeight); + } + else { - Bounds = placeholderBounds, - IsPositioned = true - }; + placeholderData.Bounds = new(0, 0, placeholderMetrics.AdvanceWidth, 0); + } + + placeholderData.IsPositioned = true; - this.glyphDigest.Add(placeholderMetrics.GlyphId); + this.RecordGlyphId(placeholderMetrics.GlyphId); this.glyphs.Add(new(offset, placeholderData, font, font.Size, placeholderMetrics)); continue; } @@ -331,13 +285,23 @@ public bool TryAdd(Font font, GlyphSubstitutionCollection collection) hasFallBacks = true; } - // We only want a single dimensional advance for positioning. - GlyphShapingBounds bounds = isVertical - ? new(0, 0, 0, metrics.AdvanceHeight) - : new(0, 0, metrics.AdvanceWidth, 0); + // We only want a single dimensional advance for positioning; assigning a + // fresh bounds value starts dirty tracking clean for GPOS. + // The substituted data is reused rather than copied: the substitution + // collection releases its instances at the end of each run, so positioning + // takes ownership. + data.ClearFeatures(); + if (isVertical) + { + data.Bounds = new(0, 0, 0, metrics.AdvanceHeight); + } + else + { + data.Bounds = new(0, 0, metrics.AdvanceWidth, 0); + } - this.glyphDigest.Add(metrics.GlyphId); - this.glyphs.Add(new(offset, new(data, true) { Bounds = bounds }, font, font.Size, metrics.CloneForRendering(data.TextRun))); + this.RecordGlyphId(metrics.GlyphId); + this.glyphs.Add(new(offset, data, font, font.Size, metrics.CloneForRendering(data.TextRun))); } return !hasFallBacks; diff --git a/src/SixLabors.Fonts/GlyphShapingBounds.cs b/src/SixLabors.Fonts/GlyphShapingBounds.cs index 1416cdeea..d0ecc06c4 100644 --- a/src/SixLabors.Fonts/GlyphShapingBounds.cs +++ b/src/SixLabors.Fonts/GlyphShapingBounds.cs @@ -6,11 +6,13 @@ namespace SixLabors.Fonts; /// -/// Represents the shaped bounds of a glyph. -/// Uses a class over a struct for ease of use. +/// Represents the shaped bounds of a glyph. A mutable struct embedded in +/// and accessed by reference through +/// : positioning lookups accumulate deltas into +/// the fields in place, and re-seeding is plain value assignment with no allocation. /// [DebuggerDisplay("{DebuggerDisplay,nq}")] -internal class GlyphShapingBounds +internal struct GlyphShapingBounds { private int x; private int y; diff --git a/src/SixLabors.Fonts/GlyphShapingClass.cs b/src/SixLabors.Fonts/GlyphShapingClass.cs index 24fd55188..ab04c6d08 100644 --- a/src/SixLabors.Fonts/GlyphShapingClass.cs +++ b/src/SixLabors.Fonts/GlyphShapingClass.cs @@ -5,14 +5,40 @@ namespace SixLabors.Fonts; internal readonly struct GlyphShapingClass { + /// + /// The bit for base glyphs. + /// + public const ushort BaseProp = 1; + + /// + /// The bit for ligature glyphs. + /// + public const ushort LigatureProp = 2; + + /// + /// The bit for mark glyphs. + /// + public const ushort MarkProp = 4; + public GlyphShapingClass(bool isMark, bool isBase, bool isLigature, ushort markAttachmentType) { this.IsMark = isMark; this.IsBase = isBase; this.IsLigature = isLigature; this.MarkAttachmentType = markAttachmentType; + this.Props = (ushort)((isBase ? BaseProp : 0) + | (isLigature ? LigatureProp : 0) + | (isMark ? MarkProp : 0) + | (markAttachmentType << 8)); } + /// + /// Gets the class packed into a single word: the low byte carries the glyph class + /// bits and the high byte the mark attachment class, so a skip decision is bitwise + /// arithmetic instead of a branch per class. + /// + public ushort Props { get; } + public bool IsMark { get; } public bool IsBase { get; } diff --git a/src/SixLabors.Fonts/GlyphShapingCollection.cs b/src/SixLabors.Fonts/GlyphShapingCollection.cs new file mode 100644 index 000000000..c46d49cda --- /dev/null +++ b/src/SixLabors.Fonts/GlyphShapingCollection.cs @@ -0,0 +1,174 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Globalization; +using SixLabors.Fonts.Tables.AdvancedTypographic; + +namespace SixLabors.Fonts; + +/// +/// The base for the glyph shaping collections, owning the state and operations shared +/// by substitution and positioning: the pass-wide feature bit assignment, the resolved +/// language candidates, the glyph id digest, and per-glyph feature mask manipulation. +/// Derived collections own only their storage and the shape-specific mutation APIs. +/// +internal abstract class GlyphShapingCollection +{ + /// + /// The approximate membership filter over every glyph id the collection has ever + /// contained. See for the growth contract. + /// + private GlyphSetDigest glyphDigest; + + /// + /// Initializes a new instance of the class. + /// + /// The text options. + /// The feature bit assignment shared by the shaping pass. + protected GlyphShapingCollection(TextOptions textOptions, ShapingFeatureMap featureMap) + { + this.TextOptions = textOptions; + this.FeatureMap = featureMap; + + // A null culture takes the ambient current culture, mirroring the reference + // shaping engine model where an unset buffer language is guessed from the + // locale. CultureInfo.InvariantCulture expresses no language preference. + CultureInfo culture = textOptions.Culture ?? CultureInfo.CurrentCulture; + this.LanguageTags = OpenTypeLanguageTagMap.TryGetTags(culture, out Tag[] tags) ? tags : []; + } + + /// + /// Gets the collection count. + /// + public abstract int Count { get; } + + /// + /// Gets the text options used by this collection. + /// + public TextOptions TextOptions { get; } + + /// + /// Gets the candidate OpenType language system tags resolved from + /// , most specific first, or an empty array when the + /// culture expresses no language preference. Resolved once per shaping pass. + /// + public Tag[] LanguageTags { get; } + + /// + /// Gets the feature bit assignment shared by every collection of the shaping pass. + /// See for the mask model and why the instance must + /// be shared across the substitution and positioning collections. + /// + public ShapingFeatureMap FeatureMap { get; } + + /// + /// Gets the approximate membership filter over every glyph id the collection has + /// ever contained. The digest only grows: substituted-away ids remain, keeping a + /// definitive negative from sound while + /// lookups mutate the collection mid-application. Every glyph id write must funnel + /// through the collection so the digest observes it; see + /// and . + /// + public GlyphSetDigest GlyphDigest => this.glyphDigest; + + /// + /// Gets the glyph shaping data at the specified index. + /// + /// The zero-based index of the elements to get. + /// The . + public abstract GlyphShapingData this[int index] { get; } + + /// + /// Sets the glyph id at the specified index, recording the id in + /// . Callers outside the collections must use this rather + /// than writing directly, which would leave + /// the digest unaware of the new id. + /// + /// The zero-based index of the element. + /// The glyph id to set. + public void SetGlyphId(int index, ushort glyphId) + { + this.glyphDigest.Add(glyphId); + this[index].GlyphId = glyphId; + } + + /// + /// Adds the shaping feature to the collection which should be applied to the glyph at a specified index. + /// + /// + /// Registration only ever accumulates: adding a disabled entry for an already + /// enabled feature must not clear the enabled bit, matching the list model this + /// replaced where a disabled duplicate left earlier enabled entries in force. + /// + /// The zero-based index of the element. + /// The feature to apply. + public void AddShapingFeature(int index, TagEntry feature) + { + GlyphShapingData data = this[index]; + ulong mask = this.FeatureMap.GetOrAddMask(feature.Tag); + data.RegisteredFeatureMask |= mask; + if (feature.Enabled) + { + data.FeatureMask |= mask; + } + } + + /// + /// Adds the shaping feature to every glyph in the given range, resolving the + /// feature's mask bit once for the whole range. Shaper plans register each stage + /// feature across the full run, so the per-glyph work must be a single bitwise OR. + /// + /// The zero-based index of the first element. + /// The number of elements in the range. + /// The feature to apply. + public void AddShapingFeatureRange(int index, int count, TagEntry feature) + { + ulong mask = this.FeatureMap.GetOrAddMask(feature.Tag); + int end = index + count; + for (int i = index; i < end; i++) + { + GlyphShapingData data = this[i]; + data.RegisteredFeatureMask |= mask; + if (feature.Enabled) + { + data.FeatureMask |= mask; + } + } + } + + /// + /// Enables a previously added shaping feature. + /// + /// + /// Intersecting with the registered mask preserves the contract that enabling a + /// feature a shaper never added for this glyph is a no-op. + /// + /// The zero-based index of the element. + /// The feature to enable. + public void EnableShapingFeature(int index, Tag feature) + { + GlyphShapingData data = this[index]; + data.FeatureMask |= data.RegisteredFeatureMask & this.FeatureMap.GetMask(feature); + } + + /// + /// Disables a previously added shaping feature. + /// + /// + /// An unregistered tag yields a zero mask whose complement clears nothing. + /// + /// The zero-based index of the element. + /// The feature to disable. + public void DisableShapingFeature(int index, Tag feature) + { + GlyphShapingData data = this[index]; + data.FeatureMask &= ~this.FeatureMap.GetMask(feature); + } + + /// + /// Records a glyph id in . Derived collections must call + /// this from every code path that stores or overwrites a glyph id. + /// + /// The glyph id. + protected void RecordGlyphId(ushort glyphId) => this.glyphDigest.Add(glyphId); +} diff --git a/src/SixLabors.Fonts/GlyphShapingData.cs b/src/SixLabors.Fonts/GlyphShapingData.cs index adf4f76d0..f5926a675 100644 --- a/src/SixLabors.Fonts/GlyphShapingData.cs +++ b/src/SixLabors.Fonts/GlyphShapingData.cs @@ -16,6 +16,11 @@ internal class GlyphShapingData { private ushort glyphId; + /// + /// The shaping bounds. Backing storage for . + /// + private GlyphShapingBounds bounds; + /// /// Initializes a new instance of the class. /// @@ -71,7 +76,7 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) this.AppliedFeatureMask = data.AppliedFeatureMask; - this.Bounds = data.Bounds; + this.bounds = data.bounds; this.CachedShapingClass = data.CachedShapingClass; this.ShapingClassCacheKey = data.ShapingClassCacheKey; } @@ -173,9 +178,10 @@ public ushort GlyphId public ulong AppliedFeatureMask { get; set; } /// - /// Gets or sets the shaping bounds. + /// Gets a reference to the shaping bounds, so positioning lookups mutate the + /// embedded value in place and re-seeding is plain value assignment. /// - public GlyphShapingBounds Bounds { get; set; } = new(0, 0, 0, 0); + public ref GlyphShapingBounds Bounds => ref this.bounds; /// /// Gets or sets a value indicating whether this glyph is the result of a substitution. @@ -221,6 +227,19 @@ private string DebuggerDisplay => FormattableString .Invariant($" {this.GlyphId} : {this.CodePoint.ToDebuggerDisplay()} : {CodePoint.GetScriptClass(this.CodePoint)} : {this.Direction} : {this.TextRun.TextAttributes} : {this.LigatureId} : {this.LigatureComponent} : {this.IsDecomposed}"); + /// + /// Clears the registered and enabled feature masks while preserving the applied + /// mask, matching the semantics of copying with cleared features. Positioning + /// reuses the substituted glyph data and re-plans its own features, but the applied + /// record of what substitution did must survive for consumers such as vertical + /// alternate detection. + /// + public void ClearFeatures() + { + this.RegisteredFeatureMask = 0; + this.FeatureMask = 0; + } + internal string ToDebuggerDisplay() => this.DebuggerDisplay; } diff --git a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs index 9755ce04f..1126d8eb4 100644 --- a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs +++ b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs @@ -13,13 +13,12 @@ namespace SixLabors.Fonts; /// /// Represents a collection of glyph indices that are mapped to input codepoints. /// -internal sealed class GlyphSubstitutionCollection : IGlyphShapingCollection +internal sealed class GlyphSubstitutionCollection : GlyphShapingCollection { /// /// Contains a map the index of a map within the collection, non-sequential codepoint offsets, and their glyph ids. /// private readonly List glyphs = []; - private GlyphSetDigest glyphDigest; /// /// Initializes a new instance of the class. @@ -27,34 +26,15 @@ internal sealed class GlyphSubstitutionCollection : IGlyphShapingCollection /// The text options. /// The feature bit assignment shared by the shaping pass. public GlyphSubstitutionCollection(TextOptions textOptions, ShapingFeatureMap featureMap) + : base(textOptions, featureMap) { - this.TextOptions = textOptions; - this.FeatureMap = featureMap; - - // A null culture takes the ambient current culture, mirroring the reference - // shaping engine model where an unset buffer language is guessed from the - // locale. CultureInfo.InvariantCulture expresses no language preference. - CultureInfo culture = textOptions.Culture ?? CultureInfo.CurrentCulture; - this.LanguageTags = OpenTypeLanguageTagMap.TryGetTags(culture, out Tag[] tags) ? tags : []; } /// /// 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.glyphs.Count; - - /// - public TextOptions TextOptions { get; } - - /// - public Tag[] LanguageTags { get; } - - /// - public GlyphSetDigest GlyphDigest => this.glyphDigest; - - /// - public ShapingFeatureMap FeatureMap { get; } + public override int Count => this.glyphs.Count; /// /// Gets or sets the running id of any ligature glyphs contained withing this collection are a member of. @@ -62,7 +42,7 @@ public GlyphSubstitutionCollection(TextOptions textOptions, ShapingFeatureMap fe public int LigatureId { get; set; } = 1; /// - public GlyphShapingData this[int index] + public override GlyphShapingData this[int index] { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => this.glyphs[index].Data; @@ -81,38 +61,6 @@ internal GlyphShapingData GetGlyphShapingData(int index, out int offset) return pair.Data; } - /// - public void AddShapingFeature(int index, TagEntry feature) - { - // Registration only ever accumulates: adding a disabled entry for an already - // enabled feature must not clear the enabled bit, matching the list model this - // replaced where a disabled duplicate left earlier enabled entries in force. - GlyphShapingData data = this.glyphs[index].Data; - ulong mask = this.FeatureMap.GetOrAddMask(feature.Tag); - data.RegisteredFeatureMask |= mask; - if (feature.Enabled) - { - data.FeatureMask |= mask; - } - } - - /// - public void EnableShapingFeature(int index, Tag feature) - { - // Intersecting with the registered mask preserves the contract that enabling a - // feature a shaper never added for this glyph is a no-op. - GlyphShapingData data = this.glyphs[index].Data; - data.FeatureMask |= data.RegisteredFeatureMask & this.FeatureMap.GetMask(feature); - } - - /// - public void DisableShapingFeature(int index, Tag feature) - { - // An unregistered tag yields a zero mask whose complement clears nothing. - GlyphShapingData data = this.glyphs[index].Data; - data.FeatureMask &= ~this.FeatureMap.GetMask(feature); - } - /// /// Adds a clone of the glyph shaping data to the collection at the specified offset. /// @@ -120,7 +68,7 @@ public void DisableShapingFeature(int index, Tag feature) /// The zero-based index within the input codepoint collection. public void AddGlyph(GlyphShapingData data, int offset) { - this.glyphDigest.Add(data.GlyphId); + this.RecordGlyphId(data.GlyphId); this.glyphs.Add(new(offset, new(data, false))); } @@ -134,7 +82,7 @@ public void AddGlyph(GlyphShapingData data, int offset) /// The zero-based index within the input codepoint collection. public void AddGlyph(ushort glyphId, CodePoint codePoint, TextDirection direction, TextRun textRun, int offset) { - this.glyphDigest.Add(glyphId); + this.RecordGlyphId(glyphId); this.glyphs.Add(new(offset, new(textRun) { CodePoint = codePoint, @@ -254,13 +202,6 @@ public void Sort(int startIndex, int endIndex, Comparison comp } } - /// - public void SetGlyphId(int index, ushort glyphId) - { - this.glyphDigest.Add(glyphId); - this.glyphs[index].Data.GlyphId = glyphId; - } - /// /// Removes all elements from the collection. /// @@ -312,7 +253,7 @@ public bool TryGetGlyphShapingDataAtOffset(int offset, [NotNullWhen(true)] out I public void Replace(int index, ushort glyphId, Tag feature) { GlyphShapingData current = this.glyphs[index].Data; - this.glyphDigest.Add(glyphId); + this.RecordGlyphId(glyphId); current.GlyphId = glyphId; current.LigatureId = 0; current.LigatureComponent = -1; @@ -359,7 +300,7 @@ public void Replace(int index, ReadOnlySpan removalIndices, ushort glyphId, } current.CodePointCount += codePointCount; - this.glyphDigest.Add(glyphId); + this.RecordGlyphId(glyphId); current.GlyphId = glyphId; current.LigatureId = ligatureId; current.IsLigated = true; @@ -406,7 +347,7 @@ public void Replace(int index, int count, ushort glyphId, Tag feature) } current.CodePointCount += codePointCount; - this.glyphDigest.Add(glyphId); + this.RecordGlyphId(glyphId); current.GlyphId = glyphId; current.LigatureId = 0; current.LigatureComponent = -1; @@ -428,7 +369,7 @@ public void Replace(int index, ReadOnlySpan glyphIds, Tag feature) { OffsetGlyphDataPair pair = this.glyphs[index]; GlyphShapingData current = pair.Data; - this.glyphDigest.Add(glyphIds[0]); + this.RecordGlyphId(glyphIds[0]); current.GlyphId = glyphIds[0]; current.LigatureComponent = 0; current.MarkAttachment = -1; @@ -448,7 +389,7 @@ public void Replace(int index, ReadOnlySpan glyphIds, Tag feature) LigatureComponent = i + 1 }; - this.glyphDigest.Add(glyphIds[i]); + this.RecordGlyphId(glyphIds[i]); data.AppliedFeatureMask |= this.FeatureMap.GetOrAddMask(feature); diff --git a/src/SixLabors.Fonts/IGlyphShapingCollection.cs b/src/SixLabors.Fonts/IGlyphShapingCollection.cs deleted file mode 100644 index 780b8df7a..000000000 --- a/src/SixLabors.Fonts/IGlyphShapingCollection.cs +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.Fonts.Tables.AdvancedTypographic; - -namespace SixLabors.Fonts; - -/// -/// Defines the contract for glyph shaping collections. -/// -internal interface IGlyphShapingCollection -{ - /// - /// Gets the collection count. - /// - public int Count { get; } - - /// - /// Gets the text options used by this collection. - /// - public TextOptions TextOptions { get; } - - /// - /// Gets the candidate OpenType language system tags resolved from - /// , most specific first, or an empty array when the - /// culture expresses no language preference. Resolved once per shaping pass. - /// - public Tag[] LanguageTags { get; } - - /// - /// Gets the feature bit assignment shared by every collection of the shaping pass. - /// See for the mask model and why the instance must - /// be shared across the substitution and positioning collections. - /// - public ShapingFeatureMap FeatureMap { get; } - - /// - /// Gets the approximate membership filter over every glyph id the collection has - /// ever contained. The digest only grows: substituted-away ids remain, keeping a - /// definitive negative from sound while - /// lookups mutate the collection mid-application. Every glyph id write must funnel - /// through the collection so the digest observes it; see . - /// - public GlyphSetDigest GlyphDigest { get; } - - /// - /// Gets the glyph shaping data at the specified index. - /// - /// The zero-based index of the elements to get. - /// The . - public GlyphShapingData this[int index] { get; } - - /// - /// Sets the glyph id at the specified index, recording the id in - /// . Callers outside the collection must use this rather - /// than writing directly, which would leave - /// the digest unaware of the new id. - /// - /// The zero-based index of the element. - /// The glyph id to set. - public void SetGlyphId(int index, ushort glyphId); - - /// - /// Adds the shaping feature to the collection which should be applied to the glyph at a specified index. - /// - /// The zero-based index of the element. - /// The feature to apply. - public void AddShapingFeature(int index, TagEntry feature); - - /// - /// Enables a previously added shaping feature. - /// - /// The zero-based index of the element. - /// The feature to enable. - public void EnableShapingFeature(int index, Tag feature); - - /// - /// Disables a previously added shaping feature. - /// - /// The zero-based index of the element. - /// The feature to disable. - public void DisableShapingFeature(int index, Tag feature); -} diff --git a/src/SixLabors.Fonts/ShapingProbe.cs b/src/SixLabors.Fonts/ShapingProbe.cs index 6b850ae06..595b48ece 100644 --- a/src/SixLabors.Fonts/ShapingProbe.cs +++ b/src/SixLabors.Fonts/ShapingProbe.cs @@ -6,7 +6,7 @@ namespace SixLabors.Fonts; -#pragma warning disable CS1591, SA1600 +#pragma warning disable CS1591, SA1600, SA1401, CA2211, SA1201 public static class ShapingProbe { @@ -38,6 +38,23 @@ public static class ShapingProbe public static bool Enabled { get; set; } + public static long StageFeatureCalls; + public static long LookupsConsidered; + public static long LookupsSkippedByDigest; + public static long GlyphGateChecks; + public static long SubstitutionAttempts; + + public static void PrintCounters(int iterations) + => Console.WriteLine( + $"stages/op={StageFeatureCalls / (double)iterations:F1} " + + $"lookups/op={LookupsConsidered / (double)iterations:F1} " + + $"digestSkipped/op={LookupsSkippedByDigest / (double)iterations:F1} " + + $"glyphGates/op={GlyphGateChecks / (double)iterations:F1} " + + $"substAttempts/op={SubstitutionAttempts / (double)iterations:F1}"); + + public static void ResetCounters() + => StageFeatureCalls = LookupsConsidered = LookupsSkippedByDigest = GlyphGateChecks = SubstitutionAttempts = 0; + public static (long Ticks, long Bytes) Enter() => Enabled ? (Stopwatch.GetTimestamp(), GC.GetAllocatedBytesForCurrentThread()) : default; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs index 0f7373953..a0652a94f 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs @@ -369,7 +369,7 @@ public static bool CheckAllCoverages( FontMetrics fontMetrics, LookupFlags lookupFlags, ushort markFilteringSet, - IGlyphShapingCollection collection, + GlyphShapingCollection collection, int index, int count, CoverageTable[] input, @@ -589,7 +589,7 @@ private static bool Match( { int position = iterator.Index; int offset = iterator.Increment(increment); - IGlyphShapingCollection collection = iterator.Collection; + GlyphShapingCollection collection = iterator.Collection; if (offset < 0) { @@ -646,7 +646,7 @@ private static bool Match( int saved = iterator.Index; iterator.Index = startIndex; - IGlyphShapingCollection collection = iterator.Collection; + GlyphShapingCollection collection = iterator.Collection; int limit = Math.Min(endExclusive, collection.Count); for (int i = 0; i < sequence.Length && i < MaxContextLength; i++) diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/FeatureLookupsKey.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/FeatureLookupsKey.cs new file mode 100644 index 000000000..2818e1984 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/FeatureLookupsKey.cs @@ -0,0 +1,78 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.AdvancedTypographic; + +/// +/// Cache key for resolved feature lookups: the stage feature, the script, and the +/// language system candidates the resolution ladder selected against. Language tags +/// compare by sequence because each shaping pass resolves a fresh array from the +/// culture. +/// +internal readonly struct FeatureLookupsKey : IEquatable +{ + private readonly uint feature; + private readonly Unicode.ScriptClass script; + private readonly Tag[] languageTags; + + /// + /// Initializes a new instance of the struct. + /// + /// The stage feature tag. + /// The script class. + /// The candidate language system tags, most specific first. + public FeatureLookupsKey(Tag feature, Unicode.ScriptClass script, Tag[] languageTags) + { + this.feature = feature.Value; + this.script = script; + this.languageTags = languageTags; + } + + /// + public bool Equals(FeatureLookupsKey other) + { + if (this.feature != other.feature || this.script != other.script) + { + return false; + } + + Tag[] tags = this.languageTags; + Tag[] otherTags = other.languageTags; + if (ReferenceEquals(tags, otherTags)) + { + return true; + } + + if (tags.Length != otherTags.Length) + { + return false; + } + + for (int i = 0; i < tags.Length; i++) + { + if (tags[i] != otherTags[i]) + { + return false; + } + } + + return true; + } + + /// + public override bool Equals(object? obj) => obj is FeatureLookupsKey key && this.Equals(key); + + /// + public override int GetHashCode() + { + HashCode hash = default; + hash.Add(this.feature); + hash.Add(this.script); + for (int i = 0; i < this.languageTags.Length; i++) + { + hash.Add(this.languageTags[i].Value); + } + + return hash.ToHashCode(); + } +} diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs index 1b7598593..fae88def7 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; using SixLabors.Fonts.Tables.AdvancedTypographic.GPos; using SixLabors.Fonts.Tables.AdvancedTypographic.Shapers; @@ -15,6 +16,13 @@ namespace SixLabors.Fonts.Tables.AdvancedTypographic; /// internal class GPosTable : Table { + /// + /// Caches resolved feature lookups per stage feature, script, and language + /// candidates. See for the variable-font bypass + /// and the read-only contract on cached lists. + /// + private readonly ConcurrentDictionary> featureLookupsCache = new(); + /// /// The tag for the horizontal kerning feature ('kern'). /// @@ -260,7 +268,11 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas goto EndLookups; } - if ((collection[iterator.Index].FeatureMask & featureMask) == 0) + // The digest cheaply rejects glyphs no subtable of this + // lookup can affect; a maybe falls through to the exact + // coverage test inside. + GlyphShapingData glyphData = collection[iterator.Index]; + if ((glyphData.FeatureMask & featureMask) == 0 || !featureLookupTable.Digest.MightContain(glyphData.GlyphId)) { iterator.Next(); continue; @@ -321,6 +333,43 @@ private bool TryGetFeatureLookups( return false; } + // Feature variations resolve against the font's live variation coordinates, so + // caching would mix results across differently configured variable fonts. + if (this.FeatureVariations is not null) + { + value = this.ResolveFeatureLookups(fontMetrics, stageFeature, script, languageTags); + return value.Count > 0; + } + + // Resolution depends only on this table's data for a given feature, script, + // and language candidates, so results, including empty ones, are cached for + // the table's lifetime. The cached list is shared: consumers must not mutate + // it. A concurrent first-resolution race only duplicates deterministic work. + FeatureLookupsKey key = new(stageFeature, script, languageTags); + if (!this.featureLookupsCache.TryGetValue(key, out value)) + { + value = this.ResolveFeatureLookups(fontMetrics, stageFeature, script, languageTags); + this.featureLookupsCache.TryAdd(key, value); + } + + return value.Count > 0; + } + + /// + /// Resolves the feature lookups for the given stage feature, script, and language + /// through the selection ladder documented inline. + /// + /// The font metrics. + /// The feature tag for the current shaping stage. + /// The script class. + /// The candidate OpenType language system tags, most specific first. + /// The resolved lookups; empty when the feature yields none. + private List<(Tag Feature, ushort Index, LookupTable LookupTable)> ResolveFeatureLookups( + FontMetrics fontMetrics, + Tag stageFeature, + ScriptClass script, + Tag[] languageTags) + { // Resolve feature substitutions from FeatureVariations (variable fonts). FeatureTableSubstitutionRecord[]? substitutions = this.FeatureVariations ?.FindMatchingSubstitutions(fontMetrics.GetNormalizedCoordinates()); @@ -328,7 +377,8 @@ private bool TryGetFeatureLookups( // Step 1: script selection. Map the Unicode script class onto the font's // script table, falling back to the font's first script when the font does not // declare the script. - ScriptListTable scriptListTable = this.ScriptList.Default(); + // The caching entry point rejects a null script list before dispatching here. + ScriptListTable scriptListTable = this.ScriptList!.Default(); Tag[] tags = UnicodeScriptTagMap.Instance[script]; for (int i = 0; i < tags.Length; i++) { @@ -355,8 +405,7 @@ private bool TryGetFeatureLookups( { if (langSysTables[j].LangSysTag == language) { - value = this.GetFeatureLookups(stageFeature, substitutions, langSysTables[j]); - return value.Count > 0; + return this.GetFeatureLookups(stageFeature, substitutions, langSysTables[j]); } } } @@ -370,16 +419,14 @@ private bool TryGetFeatureLookups( { if (langSysRecords[i].LangSysTag == DefaultLangSysTag.Value) { - value = this.GetFeatureLookups(stageFeature, substitutions, langSysRecords[i]); - return value.Count > 0; + return this.GetFeatureLookups(stageFeature, substitutions, langSysRecords[i]); } } LangSysTable? defaultLangSysTable = scriptListTable.DefaultLangSysTable; if (defaultLangSysTable != null) { - value = this.GetFeatureLookups(stageFeature, substitutions, defaultLangSysTable); - return value.Count > 0; + return this.GetFeatureLookups(stageFeature, substitutions, defaultLangSysTable); } // Step 4: no default language system either. Nothing applies: the font scoped @@ -387,8 +434,7 @@ private bool TryGetFeatureLookups( // language system means no lookups. Features such as SimSun's vertical // alternates, which live only under its Chinese language systems, are reached by // setting TextOptions.Culture to a Chinese culture. - value = null; - return false; + return []; } /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs index 83d264ab6..18dc9de77 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 Six Labors Split License. +using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; using SixLabors.Fonts.Tables.AdvancedTypographic.GSub; using SixLabors.Fonts.Tables.AdvancedTypographic.Shapers; @@ -15,6 +16,13 @@ namespace SixLabors.Fonts.Tables.AdvancedTypographic; /// internal class GSubTable : Table { + /// + /// Caches resolved feature lookups per stage feature, script, and language + /// candidates. See for the variable-font bypass + /// and the read-only contract on cached lists. + /// + private readonly ConcurrentDictionary> featureLookupsCache = new(); + /// /// The OpenType table tag for the GSUB table. /// @@ -278,8 +286,18 @@ internal void ApplyFeature( // Skip the whole lookup when its coverage cannot intersect any glyph id // the collection has ever contained; most fonts carry many lookups for // glyphs a given text never produces. + if (ShapingProbe.Enabled) + { + ShapingProbe.LookupsConsidered++; + } + if (!featureLookup.LookupTable.Digest.MightIntersect(collection.GlyphDigest)) { + if (ShapingProbe.Enabled) + { + ShapingProbe.LookupsSkippedByDigest++; + } + continue; } @@ -296,12 +314,25 @@ internal void ApplyFeature( return; } - if ((collection[iterator.Index].FeatureMask & featureMask) == 0) + if (ShapingProbe.Enabled) + { + ShapingProbe.GlyphGateChecks++; + } + + // The digest cheaply rejects glyphs no subtable of this lookup can + // affect; a maybe falls through to the exact coverage test inside. + GlyphShapingData glyphData = collection[iterator.Index]; + if ((glyphData.FeatureMask & featureMask) == 0 || !featureLookupTable.Digest.MightContain(glyphData.GlyphId)) { iterator.Next(); continue; } + if (ShapingProbe.Enabled) + { + ShapingProbe.SubstitutionAttempts++; + } + collectionCount = collection.Count; featureLookup.LookupTable.TrySubstitution(fontMetrics, this, collection, featureLookup.Feature, iterator.Index, count - (iterator.Index - index)); iterator.Next(); @@ -340,6 +371,43 @@ internal bool TryGetFeatureLookups( return false; } + // Feature variations resolve against the font's live variation coordinates, so + // caching would mix results across differently configured variable fonts. + if (this.FeatureVariations is not null) + { + value = this.ResolveFeatureLookups(fontMetrics, stageFeature, script, languageTags); + return value.Count > 0; + } + + // Resolution depends only on this table's data for a given feature, script, + // and language candidates, so results, including empty ones, are cached for + // the table's lifetime. The cached list is shared: consumers must not mutate + // it. A concurrent first-resolution race only duplicates deterministic work. + FeatureLookupsKey key = new(stageFeature, script, languageTags); + if (!this.featureLookupsCache.TryGetValue(key, out value)) + { + value = this.ResolveFeatureLookups(fontMetrics, stageFeature, script, languageTags); + this.featureLookupsCache.TryAdd(key, value); + } + + return value.Count > 0; + } + + /// + /// Resolves the feature lookups for the given stage feature, script, and language + /// through the selection ladder documented inline. + /// + /// The font metrics. + /// The feature tag for the current shaping stage. + /// The script class. + /// The candidate OpenType language system tags, most specific first. + /// The resolved lookups; empty when the feature yields none. + private List<(Tag Feature, ushort Index, LookupTable LookupTable)> ResolveFeatureLookups( + FontMetrics fontMetrics, + Tag stageFeature, + ScriptClass script, + Tag[] languageTags) + { // Resolve feature substitutions from FeatureVariations (variable fonts). FeatureTableSubstitutionRecord[]? substitutions = this.FeatureVariations ?.FindMatchingSubstitutions(fontMetrics.GetNormalizedCoordinates()); @@ -347,7 +415,8 @@ internal bool TryGetFeatureLookups( // Step 1: script selection. Map the Unicode script class onto the font's // script table, falling back to the font's first script when the font does not // declare the script. - ScriptListTable scriptListTable = this.ScriptList.Default(); + // The caching entry point rejects a null script list before dispatching here. + ScriptListTable scriptListTable = this.ScriptList!.Default(); Tag[] tags = UnicodeScriptTagMap.Instance[script]; for (int i = 0; i < tags.Length; i++) { @@ -374,8 +443,7 @@ internal bool TryGetFeatureLookups( { if (langSysTables[j].LangSysTag == language) { - value = this.GetFeatureLookups(stageFeature, substitutions, langSysTables[j]); - return value.Count > 0; + return this.GetFeatureLookups(stageFeature, substitutions, langSysTables[j]); } } } @@ -389,16 +457,14 @@ internal bool TryGetFeatureLookups( { if (langSysRecords[i].LangSysTag == DefaultLangSysTag.Value) { - value = this.GetFeatureLookups(stageFeature, substitutions, langSysRecords[i]); - return value.Count > 0; + return this.GetFeatureLookups(stageFeature, substitutions, langSysRecords[i]); } } LangSysTable? defaultLangSysTable = scriptListTable.DefaultLangSysTable; if (defaultLangSysTable != null) { - value = this.GetFeatureLookups(stageFeature, substitutions, defaultLangSysTable); - return value.Count > 0; + return this.GetFeatureLookups(stageFeature, substitutions, defaultLangSysTable); } // Step 4: no default language system either. Nothing applies: the font scoped @@ -406,8 +472,7 @@ internal bool TryGetFeatureLookups( // language system means no lookups. Features such as SimSun's vertical // alternates, which live only under its Chinese language systems, are reached by // setting TextOptions.Culture to a Chinese culture. - value = null; - return false; + return []; } /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs index b447ba2ba..e3036a849 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs @@ -100,7 +100,7 @@ public ArabicShaper(ScriptClass script, TextOptions textOptions) } /// - protected override void PlanFeatures(IGlyphShapingCollection collection, int index, int count) + protected override void PlanFeatures(GlyphShapingCollection collection, int index, int count) { this.AddFeature(collection, index, count, CcmpTag); this.AddFeature(collection, index, count, LoclTag); @@ -127,7 +127,7 @@ protected override void PlanFeatures(IGlyphShapingCollection collection, int ind } /// - protected override void AssignFeatures(IGlyphShapingCollection collection, int index, int count) + protected override void AssignFeatures(GlyphShapingCollection collection, int index, int count) { base.AssignFeatures(collection, index, count); diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs index 9e3ca8c67..5427bef47 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs @@ -27,7 +27,7 @@ internal abstract class BaseShaper /// The glyph shaping collection. /// The zero-based index of the elements to assign. /// The number of elements to assign. - public void Plan(IGlyphShapingCollection collection, int index, int count) + public void Plan(GlyphShapingCollection collection, int index, int count) { int collectionCount = collection.Count; @@ -52,7 +52,7 @@ public void Plan(IGlyphShapingCollection collection, int index, int count) /// The glyph shaping collection. /// The zero-based index of the elements to assign. /// The number of elements to assign. - protected abstract void PlanFeatures(IGlyphShapingCollection collection, int index, int count); + protected abstract void PlanFeatures(GlyphShapingCollection collection, int index, int count); /// /// Assigns the preprocessing features to each glyph within the collection. @@ -60,7 +60,7 @@ public void Plan(IGlyphShapingCollection collection, int index, int count) /// The glyph shaping collection. /// The zero-based index of the elements to assign. /// The number of elements to assign. - protected abstract void PlanPreprocessingFeatures(IGlyphShapingCollection collection, int index, int count); + protected abstract void PlanPreprocessingFeatures(GlyphShapingCollection collection, int index, int count); /// /// Assigns the postprocessing features to each glyph within the collection. @@ -68,7 +68,7 @@ public void Plan(IGlyphShapingCollection collection, int index, int count) /// The glyph shaping collection. /// The zero-based index of the elements to assign. /// The number of elements to assign. - protected abstract void PlanPostprocessingFeatures(IGlyphShapingCollection collection, int index, int count); + protected abstract void PlanPostprocessingFeatures(GlyphShapingCollection collection, int index, int count); /// /// Assigns the shaper specific substitution features to each glyph within the collection. @@ -76,7 +76,7 @@ public void Plan(IGlyphShapingCollection collection, int index, int count) /// The glyph shaping collection. /// The zero-based index of the elements to assign. /// The number of elements to assign. - protected abstract void AssignFeatures(IGlyphShapingCollection collection, int index, int count); + protected abstract void AssignFeatures(GlyphShapingCollection collection, int index, int count); /// /// Gets the ordered collection of shaping stages for this shaper. @@ -90,7 +90,7 @@ public void Plan(IGlyphShapingCollection collection, int index, int count) /// The glyph shaping collection. /// The previous collection count, updated to the current count. /// The element count, adjusted by the size delta. - private static void RecalculateCount(IGlyphShapingCollection collection, ref int oldCount, ref int count) + private static void RecalculateCount(GlyphShapingCollection collection, ref int oldCount, ref int count) { // If the collection has changed size we need to recalculate the count. int delta = collection.Count - oldCount; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs index 6194f4b25..3a7eee0f1 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs @@ -114,12 +114,12 @@ protected DefaultShaper(ScriptClass script, MarkZeroingMode markZeroingMode, Tex } /// - protected override void PlanFeatures(IGlyphShapingCollection collection, int index, int count) + protected override void PlanFeatures(GlyphShapingCollection collection, int index, int count) { } /// - protected override void PlanPreprocessingFeatures(IGlyphShapingCollection collection, int index, int count) + protected override void PlanPreprocessingFeatures(GlyphShapingCollection collection, int index, int count) { // Add variation Features. this.AddFeature(collection, index, count, RvnrTag); @@ -146,7 +146,7 @@ protected override void PlanPreprocessingFeatures(IGlyphShapingCollection collec } /// - protected override void PlanPostprocessingFeatures(IGlyphShapingCollection collection, int index, int count) + protected override void PlanPostprocessingFeatures(GlyphShapingCollection collection, int index, int count) { // Add common features. this.AddFeature(collection, index, count, CcmpTag); @@ -199,7 +199,7 @@ protected override void PlanPostprocessingFeatures(IGlyphShapingCollection colle } /// - protected override void AssignFeatures(IGlyphShapingCollection collection, int index, int count) + protected override void AssignFeatures(GlyphShapingCollection collection, int index, int count) { // TODO: We shouldn't be relying on the feature list // User defined fractional features require special treatment. @@ -221,13 +221,13 @@ protected override void AssignFeatures(IGlyphShapingCollection collection, int i /// An optional action to invoke before the feature is applied. /// An optional action to invoke after the feature is applied. protected void AddFeature( - IGlyphShapingCollection collection, + GlyphShapingCollection collection, int index, int count, Tag feature, bool enabled = true, - Action? preAction = null, - Action? postAction = null) + Action? preAction = null, + Action? postAction = null) { if (this.kerningMode == KerningMode.None) { @@ -237,11 +237,7 @@ protected void AddFeature( } } - int end = index + count; - for (int i = index; i < end; i++) - { - collection.AddShapingFeature(i, new TagEntry(feature, enabled)); - } + collection.AddShapingFeatureRange(index, count, new TagEntry(feature, enabled)); this.shapingStages.Add(new ShapingStage(feature, preAction, postAction)); } @@ -255,7 +251,7 @@ protected void AddFeature( /// The glyph shaping collection. /// The zero-based index of the first element. /// The number of elements. - private void AssignFractionalFeatures(IGlyphShapingCollection collection, int index, int count) + private void AssignFractionalFeatures(GlyphShapingCollection collection, int index, int count) { // Enable contextual fractions. for (int i = index; i < index + count; i++) diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs index c6453db0e..783638181 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs @@ -124,7 +124,7 @@ public HangulShaper(ScriptClass script, TextOptions textOptions, FontMetrics fon => this.fontMetrics = fontMetrics; /// - protected override void PlanFeatures(IGlyphShapingCollection collection, int index, int count) + protected override void PlanFeatures(GlyphShapingCollection collection, int index, int count) { this.AddFeature(collection, index, count, LjmoTag, false); this.AddFeature(collection, index, count, VjmoTag, false); @@ -132,7 +132,7 @@ protected override void PlanFeatures(IGlyphShapingCollection collection, int ind } /// - protected override void AssignFeatures(IGlyphShapingCollection collection, int index, int count) + protected override void AssignFeatures(GlyphShapingCollection collection, int index, int count) { for (int i = index; i < count; i++) { diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HebrewShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HebrewShaper.cs index ab4fd13d3..ec71b5b54 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HebrewShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HebrewShaper.cs @@ -68,7 +68,7 @@ public HebrewShaper(ScriptClass script, TextOptions textOptions, FontMetrics fon } /// - protected override void AssignFeatures(IGlyphShapingCollection collection, int index, int count) + protected override void AssignFeatures(GlyphShapingCollection collection, int index, int count) { base.AssignFeatures(collection, index, count); diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs index 8bfa1e803..b4139cf42 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs @@ -128,7 +128,7 @@ public IndicShaper(ScriptClass script, Tag unicodeScriptTag, TextOptions textOpt } /// - protected override void PlanFeatures(IGlyphShapingCollection collection, int index, int count) + protected override void PlanFeatures(GlyphShapingCollection collection, int index, int count) { this.AddFeature(collection, index, count, LoclTag, preAction: this.SetupSyllables); this.AddFeature(collection, index, count, CcmpTag); @@ -159,7 +159,7 @@ protected override void PlanFeatures(IGlyphShapingCollection collection, int ind } /// - protected override void AssignFeatures(IGlyphShapingCollection collection, int index, int count) + protected override void AssignFeatures(GlyphShapingCollection collection, int index, int count) { if (collection is not GlyphSubstitutionCollection substitutionCollection) { @@ -209,7 +209,7 @@ protected override void AssignFeatures(IGlyphShapingCollection collection, int i /// The glyph shaping collection. /// The zero-based start index. /// The number of elements to process. - private void SetupSyllables(IGlyphShapingCollection collection, int index, int count) + private void SetupSyllables(GlyphShapingCollection collection, int index, int count) { if (collection is not GlyphSubstitutionCollection substitutionCollection) { @@ -310,7 +310,7 @@ private static int IndicShapingPosition(CodePoint codePoint) /// The glyph shaping collection. /// The zero-based start index. /// The number of elements to process. - private void InitialReorder(IGlyphShapingCollection collection, int index, int count) + private void InitialReorder(GlyphShapingCollection collection, int index, int count) { if (collection is not GlyphSubstitutionCollection substitutionCollection) { @@ -1039,7 +1039,7 @@ private static int NextSyllable(GlyphSubstitutionCollection collection, int inde /// The glyph shaping collection. /// The zero-based start index. /// The number of elements to process. - private void FinalReorder(IGlyphShapingCollection collection, int index, int count) + private void FinalReorder(GlyphShapingCollection collection, int index, int count) { if (collection is not GlyphSubstitutionCollection substitutionCollection) { diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs index 99b3cdf94..1c304b7fc 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs @@ -73,7 +73,7 @@ public MyanmarShaper(ScriptClass script, TextOptions textOptions, FontMetrics fo } /// - protected override void PlanFeatures(IGlyphShapingCollection collection, int index, int count) + protected override void PlanFeatures(GlyphShapingCollection collection, int index, int count) { this.AddFeature(collection, index, count, LoclTag, preAction: this.SetupSyllables); this.AddFeature(collection, index, count, CcmpTag); @@ -95,7 +95,7 @@ protected override void PlanFeatures(IGlyphShapingCollection collection, int ind /// The glyph shaping collection. /// The zero-based start index. /// The number of elements to process. - private void SetupSyllables(IGlyphShapingCollection collection, int index, int count) + private void SetupSyllables(GlyphShapingCollection collection, int index, int count) { if (collection is not GlyphSubstitutionCollection substitutionCollection) { @@ -181,7 +181,7 @@ private void SetupSyllables(IGlyphShapingCollection collection, int index, int c /// The glyph shaping collection. /// The zero-based start index. /// The number of elements to process. - private void InitialReorder(IGlyphShapingCollection collection, int index, int count) + private void InitialReorder(GlyphShapingCollection collection, int index, int count) { if (collection is not GlyphSubstitutionCollection substitutionCollection) { diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShapingStage.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShapingStage.cs index 0300a1acd..e535765fa 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShapingStage.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShapingStage.cs @@ -13,10 +13,10 @@ namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers; internal readonly struct ShapingStage : IEquatable { /// The optional action to invoke before the feature is applied. - private readonly Action? preAction; + private readonly Action? preAction; /// The optional action to invoke after the feature is applied. - private readonly Action? postAction; + private readonly Action? postAction; /// /// Initializes a new instance of the struct. @@ -24,7 +24,7 @@ namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers; /// The OpenType feature tag for this stage. /// An optional action to invoke before the feature is applied. /// An optional action to invoke after the feature is applied. - public ShapingStage(Tag featureTag, Action? preAction, Action? postAction) + public ShapingStage(Tag featureTag, Action? preAction, Action? postAction) { this.FeatureTag = featureTag; this.preAction = preAction; @@ -42,7 +42,7 @@ public ShapingStage(Tag featureTag, Action? p /// The glyph shaping collection. /// The zero-based index of the first element. /// The number of elements. - public void PreProcessFeature(IGlyphShapingCollection collection, int index, int count) + public void PreProcessFeature(GlyphShapingCollection collection, int index, int count) => this.preAction?.Invoke(collection, index, count); /// @@ -51,7 +51,7 @@ public void PreProcessFeature(IGlyphShapingCollection collection, int index, int /// The glyph shaping collection. /// The zero-based index of the first element. /// The number of elements. - public void PostProcessFeature(IGlyphShapingCollection collection, int index, int count) + public void PostProcessFeature(GlyphShapingCollection collection, int index, int count) => this.postAction?.Invoke(collection, index, count); /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ThaiShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ThaiShaper.cs index f561941d1..22412341d 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ThaiShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ThaiShaper.cs @@ -172,7 +172,7 @@ public ThaiShaper(ScriptClass script, TextOptions textOptions, FontMetrics fontM } /// - protected override void AssignFeatures(IGlyphShapingCollection collection, int index, int count) + protected override void AssignFeatures(GlyphShapingCollection collection, int index, int count) { base.AssignFeatures(collection, index, count); diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs index 0b46f7231..0af01fc17 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs @@ -92,7 +92,7 @@ public UniversalShaper(ScriptClass script, TextOptions textOptions, FontMetrics => this.fontMetrics = fontMetrics; /// - protected override void PlanFeatures(IGlyphShapingCollection collection, int index, int count) + protected override void PlanFeatures(GlyphShapingCollection collection, int index, int count) { // Default glyph pre-processing group this.AddFeature(collection, index, count, LoclTag, preAction: this.SetupSyllables); @@ -124,7 +124,7 @@ protected override void PlanFeatures(IGlyphShapingCollection collection, int ind } /// - protected override void AssignFeatures(IGlyphShapingCollection collection, int index, int count) + protected override void AssignFeatures(GlyphShapingCollection collection, int index, int count) => this.DecomposeSplitVowels(collection, index, count); /// @@ -133,7 +133,7 @@ protected override void AssignFeatures(IGlyphShapingCollection collection, int i /// The glyph shaping collection. /// The zero-based start index. /// The number of elements to process. - private void DecomposeSplitVowels(IGlyphShapingCollection collection, int index, int count) + private void DecomposeSplitVowels(GlyphShapingCollection collection, int index, int count) { if (collection is not GlyphSubstitutionCollection substitutionCollection) { @@ -179,7 +179,7 @@ private void DecomposeSplitVowels(IGlyphShapingCollection collection, int index, /// The glyph shaping collection. /// The zero-based start index. /// The number of elements to process. - private void SetupSyllables(IGlyphShapingCollection collection, int index, int count) + private void SetupSyllables(GlyphShapingCollection collection, int index, int count) { if (collection is not GlyphSubstitutionCollection substitutionCollection) { @@ -235,7 +235,7 @@ private void SetupSyllables(IGlyphShapingCollection collection, int index, int c /// The glyph shaping collection. /// The zero-based start index. /// The number of elements to process. - private static void ClearSubstitutionFlags(IGlyphShapingCollection collection, int index, int count) + private static void ClearSubstitutionFlags(GlyphShapingCollection collection, int index, int count) { if (collection is not GlyphSubstitutionCollection substitutionCollection) { @@ -256,7 +256,7 @@ private static void ClearSubstitutionFlags(IGlyphShapingCollection collection, i /// The glyph shaping collection. /// The zero-based start index. /// The number of elements to process. - private static void RecordRhpf(IGlyphShapingCollection collection, int index, int count) + private static void RecordRhpf(GlyphShapingCollection collection, int index, int count) { if (collection is not GlyphSubstitutionCollection substitutionCollection) { @@ -285,7 +285,7 @@ private static void RecordRhpf(IGlyphShapingCollection collection, int index, in /// The glyph shaping collection. /// The zero-based start index. /// The number of elements to process. - private static void RecordPref(IGlyphShapingCollection collection, int index, int count) + private static void RecordPref(GlyphShapingCollection collection, int index, int count) { if (collection is not GlyphSubstitutionCollection substitutionCollection) { @@ -314,7 +314,7 @@ private static void RecordPref(IGlyphShapingCollection collection, int index, in /// The glyph shaping collection. /// The zero-based start index. /// The number of elements to process. - private void Reorder(IGlyphShapingCollection collection, int index, int count) + private void Reorder(GlyphShapingCollection collection, int index, int count) { if (collection is not GlyphSubstitutionCollection substitutionCollection) { diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs index 52b3c8e9a..bb232fc62 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs @@ -10,9 +10,12 @@ namespace SixLabors.Fonts.Tables.AdvancedTypographic; internal struct SkippingGlyphIterator { private readonly FontMetrics fontMetrics; - private bool ignoreMarks; - private bool ignoreBaseGlyphs; - private bool ignoreLigatures; + + /// + /// The three ignore lookup flags collapsed into a mask over the packed glyph class + /// bits, so the common skip decision is a single bitwise test. + /// + private ushort ignoreClassMask; private ushort markAttachmentType; private bool useMarkFilteringSet; private ushort markFilteringSet; @@ -27,7 +30,7 @@ internal struct SkippingGlyphIterator /// The mark filtering set index, used when is set. public SkippingGlyphIterator( FontMetrics fontMetrics, - IGlyphShapingCollection collection, + GlyphShapingCollection collection, int index, LookupFlags lookupFlags, ushort markFilteringSet) @@ -35,9 +38,9 @@ public SkippingGlyphIterator( this.fontMetrics = fontMetrics; this.Collection = collection; this.Index = index; - this.ignoreMarks = (lookupFlags & LookupFlags.IgnoreMarks) != 0; - this.ignoreBaseGlyphs = (lookupFlags & LookupFlags.IgnoreBaseGlyphs) != 0; - this.ignoreLigatures = (lookupFlags & LookupFlags.IgnoreLigatures) != 0; + this.ignoreClassMask = (ushort)(((lookupFlags & LookupFlags.IgnoreBaseGlyphs) != 0 ? GlyphShapingClass.BaseProp : 0) + | ((lookupFlags & LookupFlags.IgnoreLigatures) != 0 ? GlyphShapingClass.LigatureProp : 0) + | ((lookupFlags & LookupFlags.IgnoreMarks) != 0 ? GlyphShapingClass.MarkProp : 0)); this.markAttachmentType = (ushort)((int)(lookupFlags & LookupFlags.MarkAttachmentTypeMask) >> 8); this.useMarkFilteringSet = (lookupFlags & LookupFlags.UseMarkFilteringSet) != 0; this.markFilteringSet = markFilteringSet; @@ -46,7 +49,7 @@ public SkippingGlyphIterator( /// /// Gets the glyph shaping collection being iterated. /// - public IGlyphShapingCollection Collection { get; } + public GlyphShapingCollection Collection { get; } /// /// Gets or sets the current index in the collection. @@ -99,9 +102,9 @@ public int Increment(int count = 1) public void Reset(int index, LookupFlags lookupFlags, ushort markFilteringSet) { this.Index = index; - this.ignoreMarks = (lookupFlags & LookupFlags.IgnoreMarks) != 0; - this.ignoreBaseGlyphs = (lookupFlags & LookupFlags.IgnoreBaseGlyphs) != 0; - this.ignoreLigatures = (lookupFlags & LookupFlags.IgnoreLigatures) != 0; + this.ignoreClassMask = (ushort)(((lookupFlags & LookupFlags.IgnoreBaseGlyphs) != 0 ? GlyphShapingClass.BaseProp : 0) + | ((lookupFlags & LookupFlags.IgnoreLigatures) != 0 ? GlyphShapingClass.LigatureProp : 0) + | ((lookupFlags & LookupFlags.IgnoreMarks) != 0 ? GlyphShapingClass.MarkProp : 0)); this.markAttachmentType = (ushort)((int)(lookupFlags & LookupFlags.MarkAttachmentTypeMask) >> 8); this.useMarkFilteringSet = (lookupFlags & LookupFlags.UseMarkFilteringSet) != 0; this.markFilteringSet = markFilteringSet; @@ -133,21 +136,30 @@ private void Move(int direction) private readonly bool ShouldIgnore(int index) { GlyphShapingData data = this.Collection[index]; - GlyphShapingClass shapingClass = AdvancedTypographicUtils.GetGlyphShapingClass(this.fontMetrics, data.GlyphId, data); + ushort props = AdvancedTypographicUtils.GetGlyphShapingClass(this.fontMetrics, data.GlyphId, data).Props; + + if ((props & this.ignoreClassMask) != 0) + { + return true; + } - if (this.useMarkFilteringSet && shapingClass.IsMark) + if ((props & GlyphShapingClass.MarkProp) != 0) { // Skip marks not in the lookup's MarkFilteringSet. // This requires GDEF MarkGlyphSetsDef support. - if (!AdvancedTypographicUtils.IsInMarkFilteringSet(this.fontMetrics, this.markFilteringSet, data.GlyphId)) + if (this.useMarkFilteringSet && !AdvancedTypographicUtils.IsInMarkFilteringSet(this.fontMetrics, this.markFilteringSet, data.GlyphId)) + { + return true; + } + + // The high byte carries the mark attachment class; a lookup restricted to + // one attachment class skips marks of any other. + if (this.markAttachmentType > 0 && (props >> 8) != this.markAttachmentType) { return true; } } - return (this.ignoreMarks && shapingClass.IsMark) || - (this.ignoreBaseGlyphs && shapingClass.IsBase) || - (this.ignoreLigatures && shapingClass.IsLigature) || - (this.markAttachmentType > 0 && shapingClass.IsMark && shapingClass.MarkAttachmentType != this.markAttachmentType); + return false; } } From 428b10e43d9c051f964b5d11b48d9b35f49c3bcd Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 23 Jul 2026 20:27:42 +1000 Subject: [PATCH 03/94] Introduce PositionedGlyphMetrics wrapper type Adds a new PositionedGlyphMetrics struct that pairs FontGlyphMetrics with its post-shaping positioned state (advance width, advance height, offset). GlyphLayoutData and TextLine now store IReadOnlyList instead of IReadOnlyList, allowing metrics instances to remain immutable and shared while layout positions are captured separately. --- src/SixLabors.Fonts/GlyphLayoutData.cs | 6 +-- src/SixLabors.Fonts/PositionedGlyphMetrics.cs | 49 +++++++++++++++++++ .../TextLayout.LineBreaking.cs | 19 ++++--- src/SixLabors.Fonts/TextLayout.cs | 30 ++++++------ src/SixLabors.Fonts/TextLine.cs | 8 +-- 5 files changed, 83 insertions(+), 29 deletions(-) create mode 100644 src/SixLabors.Fonts/PositionedGlyphMetrics.cs diff --git a/src/SixLabors.Fonts/GlyphLayoutData.cs b/src/SixLabors.Fonts/GlyphLayoutData.cs index 8d7840945..67635063f 100644 --- a/src/SixLabors.Fonts/GlyphLayoutData.cs +++ b/src/SixLabors.Fonts/GlyphLayoutData.cs @@ -39,7 +39,7 @@ internal struct GlyphLayoutData /// The UTF-16 character index in the source string. /// The marker index to use if this entry becomes a selected soft-hyphen break. public GlyphLayoutData( - IReadOnlyList metrics, + IReadOnlyList metrics, Font font, float pointSize, float scaledAdvance, @@ -79,10 +79,10 @@ public GlyphLayoutData( } /// Gets the source codepoint for this entry. - public readonly CodePoint CodePoint => this.Metrics[0].CodePoint; + public readonly CodePoint CodePoint => this.Metrics[0].Metrics.CodePoint; /// Gets the shaped glyph metrics produced for this codepoint (one codepoint may map to several glyphs). - public IReadOnlyList Metrics { get; } + public IReadOnlyList Metrics { get; } /// Gets the font used to shape and render this entry. public Font Font { get; } diff --git a/src/SixLabors.Fonts/PositionedGlyphMetrics.cs b/src/SixLabors.Fonts/PositionedGlyphMetrics.cs new file mode 100644 index 000000000..59ffced3d --- /dev/null +++ b/src/SixLabors.Fonts/PositionedGlyphMetrics.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.Fonts; + +/// +/// Pairs a glyph's metrics with its positioned state for layout: the post-positioning +/// advances and placement offset. Layout and rendering read positions from here so the +/// metrics instance itself can remain immutable and shared. +/// +internal readonly struct PositionedGlyphMetrics +{ + /// + /// Initializes a new instance of the struct. + /// + /// The glyph metrics. + /// The horizontal advance after positioning. + /// The vertical advance after positioning. + /// The placement offset after positioning. + public PositionedGlyphMetrics(FontGlyphMetrics metrics, ushort advanceWidth, ushort advanceHeight, Vector2 offset) + { + this.Metrics = metrics; + this.AdvanceWidth = advanceWidth; + this.AdvanceHeight = advanceHeight; + this.Offset = offset; + } + + /// + /// Gets the glyph metrics. + /// + public FontGlyphMetrics Metrics { get; } + + /// + /// Gets the horizontal advance in font design units after positioning. + /// + public ushort AdvanceWidth { get; } + + /// + /// Gets the vertical advance in font design units after positioning. + /// + public ushort AdvanceHeight { get; } + + /// + /// Gets the placement offset in font design units after positioning. + /// + public Vector2 Offset { get; } +} diff --git a/src/SixLabors.Fonts/TextLayout.LineBreaking.cs b/src/SixLabors.Fonts/TextLayout.LineBreaking.cs index 75b125d0f..2d130e6a5 100644 --- a/src/SixLabors.Fonts/TextLayout.LineBreaking.cs +++ b/src/SixLabors.Fonts/TextLayout.LineBreaking.cs @@ -76,7 +76,7 @@ public static LogicalTextLine ComposeLogicalLine( continue; } - List metrics = []; + List metrics = []; for (int i = 0; i < glyphData.Count; i++) { GlyphPositioningCollection.GlyphPositioningData data = glyphData[i]; @@ -93,7 +93,9 @@ public static LogicalTextLine ComposeLogicalLine( continue; } - metrics.Add(data.Metrics); + // Positions are read from the baked metrics for now; the flip to + // shared metrics sources these from the shaping bounds instead. + metrics.Add(new(data.Metrics, data.Metrics.AdvanceWidth, data.Metrics.AdvanceHeight, data.Metrics.Offset)); } if (metrics.Count == 0) @@ -105,7 +107,7 @@ public static LogicalTextLine ComposeLogicalLine( continue; } - FontGlyphMetrics glyph = metrics[0]; + FontGlyphMetrics glyph = metrics[0].Metrics; // Retrieve the current codepoint from the enumerator. // If the glyph represents a substituted codepoint and the substitution is a single codepoint substitution, @@ -154,11 +156,11 @@ VerticalOrientationType.Rotate or float glyphAdvance; if (isHorizontalLayout || shouldRotate) { - glyphAdvance = glyph.AdvanceWidth; + glyphAdvance = metrics[0].AdvanceWidth; } else { - glyphAdvance = glyph.AdvanceHeight; + glyphAdvance = metrics[0].AdvanceHeight; } decomposedAdvances[0] = glyphAdvance; @@ -196,6 +198,9 @@ VerticalOrientationType.Rotate or glyphAdvance = spaceMetrics.AdvanceHeight * options.TabWidth; glyph.SetAdvanceHeight((ushort)glyphAdvance); } + + // Keep the positioned snapshot in step with the tab-adjusted metric. + metrics[0] = new(glyph, glyph.AdvanceWidth, glyph.AdvanceHeight, glyph.Offset); } } } @@ -371,7 +376,7 @@ VerticalOrientationType.Rotate or // Add our metrics to the line. textLine.Add( - isDecomposed ? new FontGlyphMetrics[] { metric } : metrics, + isDecomposed ? [new PositionedGlyphMetrics(metric, metric.AdvanceWidth, metric.AdvanceHeight, metric.Offset)] : metrics, positionedGlyph.Font, pointSize, decomposedAdvance, @@ -637,7 +642,7 @@ VerticalOrientationType.Rotate or : markerMetric.GetBoundingBox(markerMode, Vector2.Zero, pointSize); return new GlyphLayoutData( - new FontGlyphMetrics[] { markerMetric }, + new PositionedGlyphMetrics[] { new(markerMetric, markerMetric.AdvanceWidth, markerMetric.AdvanceHeight, markerMetric.Offset) }, font, pointSize, markerAdvance, diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index 67a818cbe..4b231e8db 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -849,7 +849,7 @@ private static void LayoutLineHorizontal( if (data.IsNewLine) { - FontGlyphMetrics metric = data.Metrics[0]; + FontGlyphMetrics metric = data.Metrics[0].Metrics; // Hard breaks bypass the normal glyph loop, but still need the // current pen position plus the same baseline origin used by glyphs. @@ -881,10 +881,10 @@ private static void LayoutLineHorizontal( // Index rather than enumerate: the interface-typed metrics list would allocate a // heap enumerator per glyph on this per-glyph hot path. - IReadOnlyList metrics = data.Metrics; + IReadOnlyList metrics = data.Metrics; for (int j = 0; j < metrics.Count; j++) { - FontGlyphMetrics metric = metrics[j]; + FontGlyphMetrics metric = metrics[j].Metrics; Vector2 glyphOrigin = penLocation + new Vector2(0, textLine.ScaledMaxAscender); visitor.Visit( @@ -1085,7 +1085,7 @@ private static void LayoutLineVertical( if (data.IsNewLine) { - FontGlyphMetrics metric = data.Metrics[0]; + FontGlyphMetrics metric = data.Metrics[0].Metrics; Vector2 scale = new Vector2(data.PointSize) / metric.ScaleFactor; // Hard breaks bypass the normal glyph loop, but still need the @@ -1173,10 +1173,10 @@ private static void LayoutLineVertical( } // Index rather than enumerate to avoid a heap enumerator per grapheme. - IReadOnlyList inkMetrics = g.Metrics; + IReadOnlyList inkMetrics = g.Metrics; for (int m = 0; m < inkMetrics.Count; m++) { - FontGlyphMetrics inkMetric = inkMetrics[m]; + FontGlyphMetrics inkMetric = inkMetrics[m].Metrics; Vector2 s = new Vector2(g.PointSize) / inkMetric.ScaleFactor; float glyphMinX = inkMetric.Bounds.Min.X * s.X; @@ -1216,10 +1216,10 @@ private static void LayoutLineVertical( // they participate in a vertical flow. `AdvanceWidth` gives us the horizontal pen advance we must // apply between entries inside the transformed grapheme. // Index rather than enumerate to avoid a heap enumerator per glyph. - IReadOnlyList transformedMetrics = data.Metrics; + IReadOnlyList transformedMetrics = data.Metrics; for (int m = 0; m < transformedMetrics.Count; m++) { - FontGlyphMetrics transformedMetric = transformedMetrics[m]; + FontGlyphMetrics transformedMetric = transformedMetrics[m].Metrics; Vector2 s = new Vector2(data.PointSize) / transformedMetric.ScaleFactor; entryScaledAdvanceWidth += transformedMetric.AdvanceWidth * s.X; } @@ -1227,10 +1227,10 @@ private static void LayoutLineVertical( // Index rather than enumerate: the interface-typed metrics list would allocate a // heap enumerator per glyph on this per-glyph hot path. - IReadOnlyList metrics = data.Metrics; + IReadOnlyList metrics = data.Metrics; for (int metricIndex = 0; metricIndex < metrics.Count; metricIndex++) { - FontGlyphMetrics metric = metrics[metricIndex]; + FontGlyphMetrics metric = metrics[metricIndex].Metrics; // Align the glyph horizontally and vertically centering vertically around the baseline. Vector2 scale = new Vector2(data.PointSize) / metric.ScaleFactor; @@ -1460,7 +1460,7 @@ private static void LayoutLineVerticalMixed( if (data.IsNewLine) { - FontGlyphMetrics metric = data.Metrics[0]; + FontGlyphMetrics metric = data.Metrics[0].Metrics; Vector2 scale = new Vector2(data.PointSize) / metric.ScaleFactor; // Hard breaks bypass the normal glyph loop, but still need the @@ -1496,10 +1496,10 @@ private static void LayoutLineVerticalMixed( { // Index rather than enumerate: the interface-typed metrics list would allocate a // heap enumerator per glyph on this per-glyph hot path. - IReadOnlyList metrics = data.Metrics; + IReadOnlyList metrics = data.Metrics; for (int j = 0; j < metrics.Count; j++) { - FontGlyphMetrics metric = metrics[j]; + FontGlyphMetrics metric = metrics[j].Metrics; // The glyph will be rotated 90 degrees for vertical mixed layout. // We still advance along Y, but the glyphs are laid out sideways in X. @@ -1536,10 +1536,10 @@ private static void LayoutLineVerticalMixed( else { // Index rather than enumerate to avoid a heap enumerator per glyph. - IReadOnlyList metrics = data.Metrics; + IReadOnlyList metrics = data.Metrics; for (int j = 0; j < metrics.Count; j++) { - FontGlyphMetrics metric = metrics[j]; + FontGlyphMetrics metric = metrics[j].Metrics; // Align the glyph horizontally and vertically centering vertically around the baseline. Vector2 scale = new Vector2(data.PointSize) / metric.ScaleFactor; diff --git a/src/SixLabors.Fonts/TextLine.cs b/src/SixLabors.Fonts/TextLine.cs index b1251edfb..99680173c 100644 --- a/src/SixLabors.Fonts/TextLine.cs +++ b/src/SixLabors.Fonts/TextLine.cs @@ -161,7 +161,7 @@ public int CountGlyphLayouts() /// The line-spacing factor to apply to . /// The marker index to use if this entry becomes a selected soft-hyphen break. public void Add( - IReadOnlyList metrics, + IReadOnlyList metrics, Font font, float pointSize, float scaledAdvance, @@ -203,7 +203,7 @@ public void Add( float scaledMinY = 0; for (int i = 0; i < metrics.Count; i++) { - FontGlyphMetrics metric = metrics[i]; + FontGlyphMetrics metric = metrics[i].Metrics; if (FontGlyphMetrics.ShouldSkipGlyphRendering(metric.CodePoint)) { continue; @@ -302,7 +302,7 @@ public void AddPlaceholder( // Placeholders share the source codepoint offset at their insertion point, // but they do not consume source grapheme, codepoint, or UTF-16 indexes. this.Add( - new FontGlyphMetrics[] { placeholderGlyph }, + new PositionedGlyphMetrics[] { new(placeholderGlyph, placeholderGlyph.AdvanceWidth, placeholderGlyph.AdvanceHeight, placeholderGlyph.Offset) }, placeholder.Font, placeholder.PointSize, placeholderAdvance, @@ -413,7 +413,7 @@ public void ApplyEllipsisMarker( GlyphLayoutData anchor = this.data[^1]; GlyphLayoutData marker = TextLayout.CreateGeneratedMarker( - anchor.Metrics[0], + anchor.Metrics[0].Metrics, anchor.PointSize, anchor.BidiRun, anchor.GraphemeIndex, From 60a0db33169c22f2466e15d8fe87cad106ef078f Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 23 Jul 2026 21:47:09 +1000 Subject: [PATCH 04/94] Remove render-time glyph metrics cloning Refactors text shaping/layout/rendering to stop mutating or cloning `FontGlyphMetrics` per glyph. Positioned advances and offsets now live in shaping/positioned data (`GlyphPositioningCollection` and `PositionedGlyphMetrics`), while rendering and measurement paths explicitly pass `TextRun` and position offset through `Glyph` and `GetBoundingBox`/`RenderOutlineTo` APIs. This also updates synthetic bold/oblique logic to take an explicit nullable `TextRun` context, preserves shared metric immutability, and aligns related layout, GPOS, and tests with the new data flow. --- src/SixLabors.Fonts/Font.cs | 2 +- src/SixLabors.Fonts/FontGlyphMetrics.cs | 151 +++++++++--------- src/SixLabors.Fonts/Glyph.cs | 11 +- .../GlyphPositioningCollection.cs | 76 ++++----- src/SixLabors.Fonts/GlyphShapingBounds.cs | 38 ++--- .../PlaceholderGlyphMetrics.cs | 13 +- src/SixLabors.Fonts/PositionedGlyphMetrics.cs | 10 +- .../Rendering/PaintedGlyphMetrics.cs | 58 +------ src/SixLabors.Fonts/Rendering/TextRenderer.cs | 8 +- .../Tables/AdvancedTypographic/GPosTable.cs | 8 +- .../Tables/Cff/CffGlyphMetrics.cs | 74 +-------- .../Tables/TrueType/TrueTypeGlyphMetrics.cs | 105 +++--------- .../TextLayout.LineBreaking.cs | 30 ++-- src/SixLabors.Fonts/TextLayout.cs | 22 +-- src/SixLabors.Fonts/TextLine.cs | 8 +- src/SixLabors.Fonts/TextMeasurer.cs | 4 +- src/SixLabors.Fonts/TextShaper.cs | 6 +- .../FontSynthesisTests.cs | 46 ++++-- .../SixLabors.Fonts.Tests/FontWeightTests.cs | 13 +- tests/SixLabors.Fonts.Tests/GlyphTests.cs | 2 +- 20 files changed, 250 insertions(+), 435 deletions(-) diff --git a/src/SixLabors.Fonts/Font.cs b/src/SixLabors.Fonts/Font.cs index 1c197f499..67952cc49 100644 --- a/src/SixLabors.Fonts/Font.cs +++ b/src/SixLabors.Fonts/Font.cs @@ -373,7 +373,7 @@ public bool TryGetGlyph( TextRun textRun = new() { Start = 0, End = 1, Font = this, TextAttributes = textAttributes, TextDecorations = textDecorations }; FontGlyphMetrics metrics = fontMetrics.GetGlyphMetrics(codePoint, glyphId, textAttributes, textDecorations, layoutMode, support); - glyph = new(metrics.CloneForRendering(textRun), this.Size); + glyph = new(metrics, this.Size, textRun, Vector2.Zero); return true; } diff --git a/src/SixLabors.Fonts/FontGlyphMetrics.cs b/src/SixLabors.Fonts/FontGlyphMetrics.cs index 8cb94b96f..52b0e33d8 100644 --- a/src/SixLabors.Fonts/FontGlyphMetrics.cs +++ b/src/SixLabors.Fonts/FontGlyphMetrics.cs @@ -44,6 +44,24 @@ public abstract class FontGlyphMetrics /// private const float SyntheticBoldEmScale = 1F / 31F; + /// + /// Initializes a new instance of the class, deriving the + /// scale factor and offset from the text attributes: subscript and superscript glyphs + /// receive the font's configured scaling and baseline displacement. All glyphs loaded + /// from font data use this form. + /// + /// The font metrics this glyph belongs to. + /// The glyph identifier. + /// The Unicode code point for this glyph. + /// The glyph bounds in font design units. + /// The advance width in font design units. + /// The advance height in font design units. + /// The left side bearing in font design units. + /// The top side bearing in font design units. + /// The units per em for the font. + /// The text attributes applied to the glyph. + /// The text decorations applied to the glyph. + /// The glyph type. internal FontGlyphMetrics( StreamFontMetrics font, ushort glyphId, @@ -98,6 +116,26 @@ internal FontGlyphMetrics( this.Offset = offset; } + /// + /// Initializes a new instance of the class taking the + /// scale factor and offset verbatim. Used by synthetic metrics such as placeholders, + /// whose dimensions are authored against the plain font scale factor and must not + /// receive attribute-derived scaling. + /// + /// The font metrics this glyph belongs to. + /// The glyph identifier. + /// The Unicode code point for this glyph. + /// The glyph bounds in font design units. + /// The advance width in font design units. + /// The advance height in font design units. + /// The left side bearing in font design units. + /// The top side bearing in font design units. + /// The units per em for the font. + /// The offset in font design units. + /// The scale factor. Dividing a scaled pixels-per-em value by it yields device pixels per font unit. + /// The text attributes applied to the glyph. + /// The text decorations applied to the glyph. + /// The glyph type. internal FontGlyphMetrics( StreamFontMetrics font, ushort glyphId, @@ -110,14 +148,14 @@ internal FontGlyphMetrics( ushort unitsPerEM, Vector2 offset, Vector2 scaleFactor, - TextRun textRun, + TextAttributes textAttributes, + TextDecorations textDecorations, GlyphType glyphType) { - // This is used during cloning. Ensure anything that could be changed is copied. this.FontMetrics = font; this.GlyphId = glyphId; this.CodePoint = codePoint; - this.Bounds = new Bounds(bounds.Min, bounds.Max); + this.Bounds = bounds; this.Width = bounds.Max.X - bounds.Min.X; this.Height = bounds.Max.Y - bounds.Min.Y; this.UnitsPerEm = unitsPerEM; @@ -127,12 +165,11 @@ internal FontGlyphMetrics( this.RightSideBearing = (short)(this.AdvanceWidth - this.LeftSideBearing - this.Width); this.TopSideBearing = topSideBearing; this.BottomSideBearing = (short)(this.AdvanceHeight - this.TopSideBearing - this.Height); - this.TextAttributes = textRun.TextAttributes; - this.TextDecorations = textRun.TextDecorations; + this.TextAttributes = textAttributes; + this.TextDecorations = textDecorations; this.GlyphType = glyphType; this.ScaleFactor = scaleFactor; this.Offset = offset; - this.TextRun = textRun; } /// @@ -148,12 +185,12 @@ internal FontGlyphMetrics( /// /// Gets the advance width for horizontal layout, expressed in font units. /// - public ushort AdvanceWidth { get; private set; } + public ushort AdvanceWidth { get; } /// /// Gets the advance height for vertical layout, expressed in font units. /// - public ushort AdvanceHeight { get; private set; } + public ushort AdvanceHeight { get; } /// /// Gets the left side bearing for horizontal layout, expressed in font units. @@ -211,14 +248,9 @@ internal FontGlyphMetrics( public Vector2 ScaleFactor { get; } /// - /// Gets or sets the offset in font design units. + /// Gets the offset in font design units. /// - internal Vector2 Offset { get; set; } - - /// - /// Gets the text run that the glyph belongs to. - /// - internal TextRun TextRun { get; } = null!; + internal Vector2 Offset { get; } /// /// Gets the text attributes applied to the glyph. @@ -230,64 +262,20 @@ internal FontGlyphMetrics( /// public TextDecorations TextDecorations { get; } - /// - /// Performs a shallow positioning clone: the returned instance carries its own - /// mutable scalar state (advances, offset, text run) so glyph positioning can write - /// to it, while immutable loaded data (font metrics, outline or charstring data) is - /// shared with the cached original. Implementations must not copy outline data: the - /// raw outline is immutable after load and every consumer that transforms it clones - /// it first. - /// - /// The current text run this glyph belongs to. - /// The new . - internal abstract FontGlyphMetrics CloneForRendering(TextRun textRun); - - /// - /// Apply an offset to the glyph. - /// - /// The x-offset. - /// The y-offset. - internal void ApplyOffset(short x, short y) - => this.Offset = Vector2.Transform(this.Offset, Matrix3x2.CreateTranslation(x, y)); - - /// - /// Applies an advance to the glyph. - /// - /// The x-advance. - /// The y-advance. - internal void ApplyAdvance(short x, short y) - { - this.AdvanceWidth = (ushort)(this.AdvanceWidth + x); - - // AdvanceHeight values grow downward but font-space grows upward, hence negation - this.AdvanceHeight = (ushort)(this.AdvanceHeight - y); - } - - /// - /// Sets a new advance width. - /// - /// The x-advance. - internal void SetAdvanceWidth(ushort x) => this.AdvanceWidth = x; - - /// - /// Sets a new advance height. - /// - /// The y-advance. - internal void SetAdvanceHeight(ushort y) => this.AdvanceHeight = y; - /// /// Gets the horizontal shear factor used to synthesize an oblique (faux italic) slant for /// this glyph. A non-zero value is returned only when the associated text run requests an /// italic style that the resolved font face does not itself provide, mirroring the CSS /// font-synthesis: style behavior used by web browsers. /// + /// The text run providing the styling information for this glyph. /// /// The shear factor to apply in the glyph's Y-up coordinate space, or 0 when no /// synthesis is required. /// - internal float GetObliqueSkew() + internal float GetObliqueSkew(TextRun? textRun) { - Font? font = this.TextRun?.ResolvedFont; + Font? font = textRun?.ResolvedFont; if (font is null) { return 0F; @@ -315,8 +303,9 @@ internal static Matrix3x2 CreateObliqueMatrix(float skew) /// Gets a value indicating whether a heavier weight must be synthesized for this glyph. /// Variable fonts apply their registered weight axis instead. /// + /// The text run providing the styling information for this glyph. /// true if bold synthesis is required; otherwise false. - internal bool ShouldSynthesizeBold() + internal bool ShouldSynthesizeBold(TextRun? textRun) { // Chromium deliberately leaves color emoji at their authored weight. Applying outline // dilation to a painted glyph would also distort each independently colored layer. @@ -325,7 +314,6 @@ internal bool ShouldSynthesizeBold() return false; } - TextRun? textRun = this.TextRun; Font? font = textRun?.ResolvedFont; if (font is null || (textRun!.UsesVariableWeight && ReferenceEquals(font.FontMetrics, this.FontMetrics))) { @@ -357,9 +345,10 @@ internal bool ShouldSynthesizeBold() /// halves this value internally before moving points along their lateral bisectors. /// /// The scaled point size, mapped to pixels by the caller. + /// The text run providing the styling information for this glyph. /// The emboldening strength in pixels. - internal float GetSyntheticBoldStrength(float scaledPointSize) - => this.ShouldSynthesizeBold() ? SyntheticBoldEmScale * this.UnitsPerEm * (scaledPointSize / this.ScaleFactor.X) : 0F; + internal float GetSyntheticBoldStrength(float scaledPointSize, TextRun? textRun) + => this.ShouldSynthesizeBold(textRun) ? SyntheticBoldEmScale * this.UnitsPerEm * (scaledPointSize / this.ScaleFactor.X) : 0F; /// /// Calculates the glyph bounding box in device-space (Y-down) coordinates, @@ -375,10 +364,12 @@ internal float GetSyntheticBoldStrength(float scaledPointSize) /// The glyph layout mode (horizontal, vertical, or vertical rotated). /// The render-space origin in pixels. /// The scaled point size, mapped to pixels by the caller. + /// The text run providing the styling information for this glyph. + /// The positioned placement offset in font design units, composed with the glyph's base offset. /// /// A representing the glyph bounds in device space. /// - internal FontRectangle GetBoundingBox(GlyphLayoutMode mode, Vector2 origin, float scaledPointSize) + internal FontRectangle GetBoundingBox(GlyphLayoutMode mode, Vector2 origin, float scaledPointSize, TextRun? textRun, Vector2 positionOffset) { Vector2 scale = new(scaledPointSize / this.ScaleFactor.X, scaledPointSize / this.ScaleFactor.Y); Bounds b = this.GetDesignBounds(); @@ -398,19 +389,20 @@ internal FontRectangle GetBoundingBox(GlyphLayoutMode mode, Vector2 origin, floa } } - // 2) Apply synthetic weight and the shared outline transform. - Vector2 offsetUp = this.Offset; + // 2) Apply synthetic weight and the shared outline transform. The positioned + // offset composes with any base metric offset in the same Y-up space. + Vector2 offsetUp = this.Offset + positionOffset; // FreeType halves the supplied strength before moving points. Inflating both sides by that // half-strength preserves the nominal increase in width and height for measurement while // the renderer applies the direction-sensitive point dilation. - if (this.ShouldSynthesizeBold()) + if (this.ShouldSynthesizeBold(textRun)) { float inflate = SyntheticBoldEmScale * this.UnitsPerEm * .5F; b = new Bounds(b.Min - new Vector2(inflate), b.Max + new Vector2(inflate)); } - Matrix3x2 outlineTransform = this.GetOutlineTransform(mode); + Matrix3x2 outlineTransform = this.GetOutlineTransform(mode, textRun); b = Bounds.Transform(in b, outlineTransform); offsetUp = Vector2.Transform(offsetUp, outlineTransform); @@ -461,6 +453,7 @@ internal FontRectangle GetBoundingBox(GlyphLayoutMode mode, Vector2 origin, floa /// /// The glyph layout mode to render using. /// The text run providing the styling information for this glyph. + /// The positioned placement offset in font design units, composed with the glyph's base offset. /// The point size used to render this glyph. /// The pixel density used to render this glyph. /// The hinting mode used to render this glyph. @@ -475,6 +468,7 @@ internal virtual void RenderTo( Vector2 layoutAdvance, GlyphLayoutMode mode, TextRun textRun, + Vector2 positionOffset, float pointSize, float dpi, HintingMode hintingMode, @@ -495,7 +489,7 @@ internal virtual void RenderTo( float scaledPPEM = this.GetScaledSize(pointSize, dpi); Matrix3x2 rotation = GetRotationMatrix(mode); - FontRectangle box = this.GetBoundingBox(mode, glyphOrigin, scaledPPEM); + FontRectangle box = this.GetBoundingBox(mode, glyphOrigin, scaledPPEM, textRun, positionOffset); GlyphRendererParameters parameters = new(this, textRun, pointSize, dpi, mode, graphemeIndex); if (!renderer.BeginGlyph(in box, in parameters)) @@ -559,7 +553,7 @@ internal virtual void RenderTo( { if (!whitespace) { - this.RenderOutlineTo(outlineTarget, glyphOrigin, mode, scaledPPEM, hintingMode); + this.RenderOutlineTo(outlineTarget, glyphOrigin, mode, textRun, positionOffset, scaledPPEM, hintingMode); } renderer.EndGlyph(); @@ -578,17 +572,21 @@ internal virtual void RenderTo( /// /// Renders only the glyph outline geometry to the specified renderer, without glyph or /// decoration bookkeeping, using the same transforms as - /// . + /// . /// /// The surface renderer. /// The origin used to render the glyph outline, in device pixels. /// The glyph layout mode to render using. + /// The text run providing the styling information for this glyph. + /// The positioned placement offset in font design units, composed with the glyph's base offset. /// The scaled pixels-per-em value used to scale the outline. /// The hinting mode used to render the glyph. internal virtual void RenderOutlineTo( IGlyphRenderer renderer, Vector2 glyphOrigin, GlyphLayoutMode mode, + TextRun? textRun, + Vector2 positionOffset, float scaledPPEM, HintingMode hintingMode) { @@ -934,13 +932,14 @@ internal static Matrix3x2 GetRotationMatrix(GlyphLayoutMode mode) /// shear with the rotation required by the layout mode. /// /// The glyph layout mode. + /// The text run providing the styling information for this glyph. /// The outline transform. - internal Matrix3x2 GetOutlineTransform(GlyphLayoutMode mode) + internal Matrix3x2 GetOutlineTransform(GlyphLayoutMode mode, TextRun? textRun) { // All metrics keep outlines in the glyph's Y-up coordinate system through this transform // and convert to device-space Y-down coordinates afterwards. Decorations use // GetRotationMatrix directly because browser underlines are not themselves italicized. - Matrix3x2 transform = CreateObliqueMatrix(this.GetObliqueSkew()); + Matrix3x2 transform = CreateObliqueMatrix(this.GetObliqueSkew(textRun)); transform *= GetRotationMatrix(mode); return transform; } diff --git a/src/SixLabors.Fonts/Glyph.cs b/src/SixLabors.Fonts/Glyph.cs index 88a4f8b25..6208ddf9a 100644 --- a/src/SixLabors.Fonts/Glyph.cs +++ b/src/SixLabors.Fonts/Glyph.cs @@ -12,11 +12,15 @@ namespace SixLabors.Fonts; public readonly struct Glyph { private readonly float pointSize; + private readonly TextRun textRun; + private readonly Vector2 positionOffset; - internal Glyph(FontGlyphMetrics glyphMetrics, float pointSize) + internal Glyph(FontGlyphMetrics glyphMetrics, float pointSize, TextRun textRun, Vector2 positionOffset) { this.GlyphMetrics = glyphMetrics; this.pointSize = pointSize; + this.textRun = textRun; + this.positionOffset = positionOffset; } /// @@ -32,7 +36,7 @@ internal Glyph(FontGlyphMetrics glyphMetrics, float pointSize) /// The DPI to measure the glyph at. /// The rendered glyph bounds. public FontRectangle BoundingBox(GlyphLayoutMode mode, Vector2 glyphOrigin, float dpi) - => this.GlyphMetrics.GetBoundingBox(mode, glyphOrigin, this.pointSize * dpi); + => this.GlyphMetrics.GetBoundingBox(mode, glyphOrigin, this.pointSize * dpi, this.textRun, this.positionOffset); /// /// Renders the glyph to the render surface. @@ -57,7 +61,7 @@ internal void RenderTo( GlyphLayoutMode mode, TextOptions options) { - TextRun textRun = this.GlyphMetrics.TextRun; + TextRun textRun = this.textRun; float pointSize = textRun.Font?.Size ?? options.Font.Size; this.GlyphMetrics.RenderTo( @@ -68,6 +72,7 @@ internal void RenderTo( layoutAdvance, mode, textRun, + this.positionOffset, pointSize, options.Dpi, options.HintingMode, diff --git a/src/SixLabors.Fonts/GlyphPositioningCollection.cs b/src/SixLabors.Fonts/GlyphPositioningCollection.cs index 4c6b881f2..9ecd4ceb8 100644 --- a/src/SixLabors.Fonts/GlyphPositioningCollection.cs +++ b/src/SixLabors.Fonts/GlyphPositioningCollection.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Numerics; using System.Runtime.CompilerServices; using SixLabors.Fonts.Tables.AdvancedTypographic; using SixLabors.Fonts.Unicode; @@ -150,8 +151,6 @@ public bool TryUpdate(Font font, GlyphSubstitutionCollection collection) ushort id = shape.GlyphId; CodePoint codePoint = shape.CodePoint; - // 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. TextAttributes textAttributes = shape.TextRun.TextAttributes; TextDecorations textDecorations = shape.TextRun.TextDecorations; @@ -193,7 +192,7 @@ public bool TryUpdate(Font font, GlyphSubstitutionCollection collection) } this.RecordGlyphId(metrics.GlyphId); - this.glyphs.Insert(i += replacementCount, new(offset, shape, font, pointSize, metrics.CloneForRendering(shape.TextRun))); + this.glyphs.Insert(i += replacementCount, new(offset, shape, font, pointSize, metrics)); replacementCount++; } } @@ -270,8 +269,6 @@ public bool TryAdd(Font font, GlyphSubstitutionCollection collection) continue; } - // 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. TextAttributes textAttributes = data.TextRun.TextAttributes; TextDecorations textDecorations = data.TextRun.TextDecorations; @@ -301,52 +298,24 @@ public bool TryAdd(Font font, GlyphSubstitutionCollection collection) } this.RecordGlyphId(metrics.GlyphId); - this.glyphs.Add(new(offset, data, font, font.Size, metrics.CloneForRendering(data.TextRun))); + this.glyphs.Add(new(offset, data, font, font.Size, metrics)); } return !hasFallBacks; } /// - /// Updates the position of the glyph at the specified index. + /// Marks the glyph at the specified index as positioned. Positions accumulate in the + /// glyph's shaping bounds and are read from there by consumers, so the shared metrics + /// instance is never mutated. /// - /// The font metrics. /// The zero-based index of the element. - public void UpdatePosition(FontMetrics fontMetrics, int index) - { - GlyphShapingData data = this[index]; - bool isDirtyXY = data.Bounds.IsDirtyXY; - bool isDirtyWH = data.Bounds.IsDirtyWH; - if (!isDirtyXY && !isDirtyWH) - { - // No change required but the glyph has been processed. - data.IsPositioned = true; - return; - } - - ushort glyphId = data.GlyphId; - FontGlyphMetrics m = this.glyphs[index].Metrics; - - if (m.GlyphId == glyphId && fontMetrics == m.FontMetrics) - { - if (isDirtyXY) - { - m.ApplyOffset((short)data.Bounds.X, (short)data.Bounds.Y); - data.IsPositioned = true; - } - - if (isDirtyWH) - { - m.SetAdvanceWidth((ushort)data.Bounds.Width); - m.SetAdvanceHeight((ushort)data.Bounds.Height); - data.IsPositioned = true; - } - } - } + public void UpdatePosition(int index) => this[index].IsPositioned = true; /// - /// Updates the advanced metrics of the glyphs at the given index and id, - /// adding dx and dy to the current advance. + /// Adds dx and dy to the positioned advance of the glyph at the given index and id. + /// Advances accumulate in the glyph's shaping bounds so the shared metrics instance + /// is never mutated. /// /// The font face with metrics. /// The zero-based index of the element. @@ -364,7 +333,12 @@ public void Advance(FontMetrics fontMetrics, int index, ushort glyphId, short dx bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(m.CodePoint, layoutMode) || (glyph.Data.AppliedFeatureMask & this.GetVerticalFeatureMask()) != 0; - m.ApplyAdvance(dx, isVertical ? dy : (short)0); + // Advance heights grow downward but font-space grows upward, hence the negation. + glyph.Data.Bounds.Width += dx; + if (isVertical) + { + glyph.Data.Bounds.Height -= dy; + } } } @@ -418,6 +392,24 @@ public GlyphPositioningData(int offset, GlyphShapingData data, Font font, float public FontGlyphMetrics Metrics { get; set; } + /// + /// Gets the positioned horizontal advance in font design units: the shaping bounds + /// value once positioning has written one, otherwise the metrics advance. + /// + public ushort AdvanceWidth => this.Data.Bounds.IsDirtyWH ? (ushort)this.Data.Bounds.Width : this.Metrics.AdvanceWidth; + + /// + /// Gets the positioned vertical advance in font design units: the shaping bounds + /// value once positioning has written one, otherwise the metrics advance. + /// + public ushort AdvanceHeight => this.Data.Bounds.IsDirtyWH ? (ushort)this.Data.Bounds.Height : this.Metrics.AdvanceHeight; + + /// + /// Gets the placement offset written by positioning, in font design units. Geometry + /// consumers compose it with the metrics offset. + /// + public Vector2 PositionOffset => new(this.Data.Bounds.X, this.Data.Bounds.Y); + private string DebuggerDisplay => FormattableString.Invariant($"Offset: {this.Offset}, Data: {this.Data.ToDebuggerDisplay()}"); } } diff --git a/src/SixLabors.Fonts/GlyphShapingBounds.cs b/src/SixLabors.Fonts/GlyphShapingBounds.cs index d0ecc06c4..9e4497840 100644 --- a/src/SixLabors.Fonts/GlyphShapingBounds.cs +++ b/src/SixLabors.Fonts/GlyphShapingBounds.cs @@ -14,42 +14,21 @@ namespace SixLabors.Fonts; [DebuggerDisplay("{DebuggerDisplay,nq}")] internal struct GlyphShapingBounds { - private int x; - private int y; private int width; private int height; public GlyphShapingBounds(int x, int y, int width, int height) { - this.x = x; - this.y = y; + this.X = x; + this.Y = y; this.width = width; this.height = height; - this.IsDirtyXY = false; this.IsDirtyWH = false; } - public int X - { - get => this.x; + public int X { get; set; } - set - { - this.x = value; - this.IsDirtyXY = true; - } - } - - public int Y - { - get => this.y; - - set - { - this.y = value; - this.IsDirtyXY = true; - } - } + public int Y { get; set; } public int Width { @@ -73,10 +52,13 @@ public int Height } } - public bool IsDirtyXY { get; private set; } - + /// + /// Gets a value indicating whether positioning has written either advance dimension. + /// Consumers use this to choose between the positioned advances and the metrics + /// advances. + /// public bool IsDirtyWH { get; private set; } private string DebuggerDisplay - => FormattableString.Invariant($"{this.X} : {this.Y} : {this.Width} : {this.Height} : {this.IsDirtyXY} : {this.IsDirtyWH}"); + => FormattableString.Invariant($"{this.X} : {this.Y} : {this.Width} : {this.Height} : {this.IsDirtyWH}"); } diff --git a/src/SixLabors.Fonts/PlaceholderGlyphMetrics.cs b/src/SixLabors.Fonts/PlaceholderGlyphMetrics.cs index d2d68c166..45a2062e2 100644 --- a/src/SixLabors.Fonts/PlaceholderGlyphMetrics.cs +++ b/src/SixLabors.Fonts/PlaceholderGlyphMetrics.cs @@ -42,7 +42,8 @@ internal PlaceholderGlyphMetrics( font.UnitsPerEm, Vector2.Zero, new Vector2(font.ScaleFactor), - textRun, + textRun.TextAttributes, + textRun.TextDecorations, GlyphType.Placeholder) { this.placeholder = placeholder; @@ -50,15 +51,6 @@ internal PlaceholderGlyphMetrics( this.dpi = dpi; } - /// - internal override FontGlyphMetrics CloneForRendering(TextRun textRun) - => new PlaceholderGlyphMetrics( - this.FontMetrics, - this.placeholder, - this.pointSize, - this.dpi, - textRun); - /// internal override void RenderTo( IGlyphRenderer renderer, @@ -68,6 +60,7 @@ internal override void RenderTo( Vector2 layoutAdvance, GlyphLayoutMode mode, TextRun textRun, + Vector2 positionOffset, float pointSize, float dpi, HintingMode hintingMode, diff --git a/src/SixLabors.Fonts/PositionedGlyphMetrics.cs b/src/SixLabors.Fonts/PositionedGlyphMetrics.cs index 59ffced3d..78f9ba9f0 100644 --- a/src/SixLabors.Fonts/PositionedGlyphMetrics.cs +++ b/src/SixLabors.Fonts/PositionedGlyphMetrics.cs @@ -19,12 +19,14 @@ internal readonly struct PositionedGlyphMetrics /// The horizontal advance after positioning. /// The vertical advance after positioning. /// The placement offset after positioning. - public PositionedGlyphMetrics(FontGlyphMetrics metrics, ushort advanceWidth, ushort advanceHeight, Vector2 offset) + /// The text run the glyph belongs to. + public PositionedGlyphMetrics(FontGlyphMetrics metrics, ushort advanceWidth, ushort advanceHeight, Vector2 offset, TextRun textRun) { this.Metrics = metrics; this.AdvanceWidth = advanceWidth; this.AdvanceHeight = advanceHeight; this.Offset = offset; + this.TextRun = textRun; } /// @@ -46,4 +48,10 @@ public PositionedGlyphMetrics(FontGlyphMetrics metrics, ushort advanceWidth, ush /// Gets the placement offset in font design units after positioning. /// public Vector2 Offset { get; } + + /// + /// Gets the text run the glyph belongs to, carried here so rendering reads it from + /// the positioned state rather than from per-glyph metric clones. + /// + public TextRun TextRun { get; } } diff --git a/src/SixLabors.Fonts/Rendering/PaintedGlyphMetrics.cs b/src/SixLabors.Fonts/Rendering/PaintedGlyphMetrics.cs index 4d1d55b80..5f6a21ec1 100644 --- a/src/SixLabors.Fonts/Rendering/PaintedGlyphMetrics.cs +++ b/src/SixLabors.Fonts/Rendering/PaintedGlyphMetrics.cs @@ -58,56 +58,6 @@ internal PaintedGlyphMetrics( GlyphType.Painted) => this.source = source; - /// - /// Initializes a new instance of the class for rendering with overrides. - /// - internal PaintedGlyphMetrics( - StreamFontMetrics font, - ushort glyphId, - CodePoint codePoint, - IPaintedGlyphSource source, - Bounds bounds, - ushort advanceWidth, - ushort advanceHeight, - short leftSideBearing, - short topSideBearing, - ushort unitsPerEM, - Vector2 offset, - Vector2 scaleFactor, - TextRun textRun) - : base( - font, - glyphId, - codePoint, - bounds, - advanceWidth, - advanceHeight, - leftSideBearing, - topSideBearing, - unitsPerEM, - offset, - scaleFactor, - textRun, - GlyphType.Painted) - => this.source = source; - - /// - internal override FontGlyphMetrics CloneForRendering(TextRun textRun) - => new PaintedGlyphMetrics( - this.FontMetrics, - this.GlyphId, - this.CodePoint, - this.source, - this.Bounds, - this.AdvanceWidth, - this.AdvanceHeight, - this.LeftSideBearing, - this.TopSideBearing, - this.UnitsPerEm, - this.Offset, - this.ScaleFactor, - textRun); - /// internal override Bounds GetDesignBounds() { @@ -125,6 +75,8 @@ internal override void RenderOutlineTo( IGlyphRenderer renderer, Vector2 glyphOrigin, GlyphLayoutMode mode, + TextRun? textRun, + Vector2 positionOffset, float scaledPPEM, HintingMode hintingMode) { @@ -134,17 +86,17 @@ internal override void RenderOutlineTo( } Vector2 scale = new Vector2(scaledPPEM) / this.ScaleFactor; // uniform - Matrix3x2 outlineTransform = this.GetOutlineTransform(mode); + Matrix3x2 outlineTransform = this.GetOutlineTransform(mode, textRun); // Keep painted geometry in Y-up font space through the same scale, offset, oblique, and // rotation sequence used by TrueType and CFF, then perform the device-space Y inversion. Matrix3x2 layout = Matrix3x2.CreateScale(scale); - layout.Translation = this.Offset * scale; + layout.Translation = (this.Offset + positionOffset) * scale; layout *= outlineTransform; layout *= Matrix3x2.CreateScale(1F, -1F); layout.Translation += glyphOrigin; - FontRectangle box = this.GetBoundingBox(mode, glyphOrigin, scaledPPEM); + FontRectangle box = this.GetBoundingBox(mode, glyphOrigin, scaledPPEM, textRun, positionOffset); // Source-to-UPEM: viewBox mapping (uniform "meet"), optional y-flip, optional root transform. Matrix3x2 s2u = ComputeSourceToUpem(canvas, this.UnitsPerEm); diff --git a/src/SixLabors.Fonts/Rendering/TextRenderer.cs b/src/SixLabors.Fonts/Rendering/TextRenderer.cs index a8736f141..a217c6f3a 100644 --- a/src/SixLabors.Fonts/Rendering/TextRenderer.cs +++ b/src/SixLabors.Fonts/Rendering/TextRenderer.cs @@ -130,8 +130,8 @@ public void Render(ushort glyphId, GlyphOptions options) // divided by DPI), so the ink box computed against it compares directly with the // scaled region. Inflating by the scaled line height for the glyph's orientation // gives em-box anchored decorations the same tolerance culled text receives, and - // rejecting here also skips the per-glyph metrics clone below. - FontRectangle box = metrics.GetBoundingBox(glyphLayoutMode, origin, options.Font.Size); + // rejecting here also skips creating the per-glyph text run below. + FontRectangle box = metrics.GetBoundingBox(glyphLayoutMode, origin, options.Font.Size, null, Vector2.Zero); IMetricsHeader metricsHeader = glyphLayoutMode == GlyphLayoutMode.Vertical ? fontMetrics.VerticalMetrics : fontMetrics.HorizontalMetrics; @@ -148,9 +148,8 @@ public void Render(ushort glyphId, GlyphOptions options) } TextRun textRun = options.CreateTextRun(); - FontGlyphMetrics renderMetrics = metrics.CloneForRendering(textRun); - renderMetrics.RenderTo( + metrics.RenderTo( this.renderer, options.GraphemeIndex, origin, @@ -158,6 +157,7 @@ public void Render(ushort glyphId, GlyphOptions options) NoLayoutAdvance, glyphLayoutMode, textRun, + Vector2.Zero, options.Font.Size, options.Dpi, options.HintingMode, diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs index fae88def7..5b9e31a82 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs @@ -297,7 +297,7 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas FixCursiveAttachment(collection, index, count); FixMarkAttachment(collection, index, count); - UpdatePositions(fontMetrics, collection, index, count); + UpdatePositions(collection, index, count); if (i >= maxCount || maxOperationsReached) { @@ -659,16 +659,14 @@ private static void ZeroMarkAdvances(FontMetrics fontMetrics, GlyphPositioningCo /// /// Updates glyph positions in the collection for the specified range. /// - /// The font metrics. /// The glyph positioning collection. /// The starting index. /// The number of glyphs to process. - private static void UpdatePositions(FontMetrics fontMetrics, GlyphPositioningCollection collection, int index, int count) + private static void UpdatePositions(GlyphPositioningCollection collection, int index, int count) { for (int i = 0; i < count; i++) { - int currentIndex = i + index; - collection.UpdatePosition(fontMetrics, currentIndex); + collection.UpdatePosition(i + index); } } } diff --git a/src/SixLabors.Fonts/Tables/Cff/CffGlyphMetrics.cs b/src/SixLabors.Fonts/Tables/Cff/CffGlyphMetrics.cs index 97ac2631d..1cd2bb4c7 100644 --- a/src/SixLabors.Fonts/Tables/Cff/CffGlyphMetrics.cs +++ b/src/SixLabors.Fonts/Tables/Cff/CffGlyphMetrics.cs @@ -59,81 +59,17 @@ internal CffGlyphMetrics( glyphType) => this.glyphData = glyphData; - /// - /// Initializes a new instance of the class with offset, scale, and text run parameters. - /// - /// The font metrics. - /// The glyph identifier. - /// The Unicode code point. - /// The CFF glyph data containing the charstring program. - /// The glyph bounding box. - /// The advance width. - /// The advance height. - /// The left side bearing. - /// The top side bearing. - /// The units per em. - /// The glyph offset. - /// The scale factor. - /// The text run for rendering. - /// The glyph type. - internal CffGlyphMetrics( - StreamFontMetrics fontMetrics, - ushort glyphId, - CodePoint codePoint, - CffGlyphData glyphData, - Bounds bounds, - ushort advanceWidth, - ushort advanceHeight, - short leftSideBearing, - short topSideBearing, - ushort unitsPerEM, - Vector2 offset, - Vector2 scaleFactor, - TextRun textRun, - GlyphType glyphType) - : base( - fontMetrics, - glyphId, - codePoint, - bounds, - advanceWidth, - advanceHeight, - leftSideBearing, - topSideBearing, - unitsPerEM, - offset, - scaleFactor, - textRun, - glyphType) - => this.glyphData = glyphData; - - /// - internal override FontGlyphMetrics CloneForRendering(TextRun textRun) - => new CffGlyphMetrics( - this.FontMetrics, - this.GlyphId, - this.CodePoint, - this.glyphData, - this.Bounds, - this.AdvanceWidth, - this.AdvanceHeight, - this.LeftSideBearing, - this.TopSideBearing, - this.UnitsPerEm, - this.Offset, - this.ScaleFactor, - textRun, - this.GlyphType); - /// internal override void RenderOutlineTo( IGlyphRenderer renderer, Vector2 glyphOrigin, GlyphLayoutMode mode, + TextRun? textRun, + Vector2 positionOffset, float scaledPPEM, HintingMode hintingMode) { - Matrix3x2 transform = this.GetOutlineTransform(mode); + Matrix3x2 transform = this.GetOutlineTransform(mode, textRun); Vector2 scale = new Vector2(scaledPPEM) / this.ScaleFactor; @@ -146,8 +82,8 @@ internal override void RenderOutlineTo( scale *= new Vector2((float)(fm[0] * upm), (float)(fm[3] * upm)); } - Vector2 scaledOffset = this.Offset * scale; - float boldStrength = this.GetSyntheticBoldStrength(scaledPPEM); + Vector2 scaledOffset = (this.Offset + positionOffset) * scale; + float boldStrength = this.GetSyntheticBoldStrength(scaledPPEM, textRun); if (boldStrength > 0F) { // Flush through the supplied renderer before glyph completion so skip-ink observes diff --git a/src/SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.cs b/src/SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.cs index 55c80d50e..fdb9714e3 100644 --- a/src/SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.cs +++ b/src/SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.cs @@ -14,12 +14,13 @@ namespace SixLabors.Fonts.Tables.TrueType; /// public partial class TrueTypeGlyphMetrics : FontGlyphMetrics { - private static readonly Vector2 YInverter = new(1, -1); private readonly GlyphVector vector; /// - /// Scaled, hinted outline copies keyed by ppem. Allocated on first render: shaping - /// and measurement clone metrics without ever rendering them, so an eager cache + /// Scaled, hinted, upright outline copies keyed by ppem. Offset translation, synthetic + /// oblique, and layout rotation are applied per point at emit time so one cached copy + /// serves every run, layout mode, and positioned offset. Allocated on first render: + /// shaping and measurement clone metrics without ever rendering them, so an eager cache /// would cost a dictionary per glyph per shaping pass. /// private ConcurrentDictionary? scaledVectorCache; @@ -67,70 +68,6 @@ internal TrueTypeGlyphMetrics( glyphType) => this.vector = vector; - /// - /// Initializes a new instance of the class - /// with explicit offset, scale, and text run for rendering clones. - /// - /// The font metrics this glyph belongs to. - /// The glyph identifier. - /// The Unicode code point for this glyph. - /// The glyph outline vector. - /// The advance width in font units. - /// The advance height in font units. - /// The left side bearing in font units. - /// The top side bearing in font units. - /// The units per em for the font. - /// The rendering offset. - /// The scale factor. - /// The text run this glyph is associated with. - /// The glyph type. - internal TrueTypeGlyphMetrics( - StreamFontMetrics font, - ushort glyphId, - CodePoint codePoint, - GlyphVector vector, - ushort advanceWidth, - ushort advanceHeight, - short leftSideBearing, - short topSideBearing, - ushort unitsPerEM, - Vector2 offset, - Vector2 scaleFactor, - TextRun textRun, - GlyphType glyphType) - : base( - font, - glyphId, - codePoint, - vector.Bounds, - advanceWidth, - advanceHeight, - leftSideBearing, - topSideBearing, - unitsPerEM, - offset, - scaleFactor, - textRun, - glyphType) - => this.vector = vector; - - /// - internal override FontGlyphMetrics CloneForRendering(TextRun textRun) - => new TrueTypeGlyphMetrics( - this.FontMetrics, - this.GlyphId, - this.CodePoint, - this.vector, - this.AdvanceWidth, - this.AdvanceHeight, - this.LeftSideBearing, - this.TopSideBearing, - this.UnitsPerEm, - this.Offset, - this.ScaleFactor, - textRun, - this.GlyphType); - /// /// Gets the outline for the current glyph. /// @@ -142,36 +79,40 @@ internal override void RenderOutlineTo( IGlyphRenderer renderer, Vector2 glyphOrigin, GlyphLayoutMode mode, + TextRun? textRun, + Vector2 positionOffset, float scaledPPEM, HintingMode hintingMode) { - Matrix3x2 transform = this.GetOutlineTransform(mode); ConcurrentDictionary cache = LazyInitializer.EnsureInitialized(ref this.scaledVectorCache, static () => new()); + Vector2 scale = new Vector2(scaledPPEM) / this.ScaleFactor; GlyphVector scaledVector = cache.GetOrAdd(scaledPPEM, _ => { // Create a scaled deep copy of the vector so that we do not alter - // the globally cached instance. + // the globally cached instance. The hinter always receives the upright, + // untranslated outline. GlyphVector clone = GlyphVector.DeepClone(this.vector); - Vector2 scale = new Vector2(scaledPPEM) / this.ScaleFactor; - - Matrix3x2 matrix = Matrix3x2.CreateScale(scale); - matrix.Translation = this.Offset * scale; - GlyphVector.TransformInPlace(ref clone, matrix); + GlyphVector.TransformInPlace(ref clone, Matrix3x2.CreateScale(scale)); float pixelSize = scaledPPEM / 72F; this.FontMetrics.ApplyTrueTypeHinting(this.GetHintingMode(hintingMode), this, ref clone, scale, pixelSize); - - // The shared outline transform applies synthetic oblique before layout rotation. - // Both happen after hinting so the hinter always receives the upright outline. - GlyphVector.TransformInPlace(ref clone, transform); return clone; }); IList controlPoints = scaledVector.ControlPoints; IReadOnlyList endPoints = scaledVector.EndPoints; - float boldStrength = this.GetSyntheticBoldStrength(scaledPPEM); + // Offset translation, synthetic oblique, and layout rotation are applied per point at + // emit time so the cached outline stays shareable across runs, modes, and positioned + // offsets. Placement lands after hinting, matching FreeType's treatment of GPOS + // offsets, followed by the Y-flip into device space and the origin translation. + Matrix3x2 emit = Matrix3x2.CreateTranslation((this.Offset + positionOffset) * scale); + emit *= this.GetOutlineTransform(mode, textRun); + emit *= Matrix3x2.CreateScale(1F, -1F); + emit.Translation += glyphOrigin; + + float boldStrength = this.GetSyntheticBoldStrength(scaledPPEM, textRun); EmboldeningGlyphRenderer? emboldening = null; IGlyphRenderer target = renderer; if (boldStrength > 0F) @@ -190,8 +131,8 @@ internal override void RenderOutlineTo( endOfContour = endPoints[i]; Vector2 prev; - Vector2 curr = (YInverter * controlPoints[endOfContour].Point) + glyphOrigin; - Vector2 next = (YInverter * controlPoints[startOfContour].Point) + glyphOrigin; + Vector2 curr = Vector2.Transform(controlPoints[endOfContour].Point, emit); + Vector2 next = Vector2.Transform(controlPoints[startOfContour].Point, emit); if (controlPoints[endOfContour].OnCurve) { @@ -219,7 +160,7 @@ internal override void RenderOutlineTo( int currentIndex = startOfContour + p; int nextIndex = startOfContour + ((p + 1) % length); int prevIndex = startOfContour + ((length + p - 1) % length); - next = (YInverter * controlPoints[nextIndex].Point) + glyphOrigin; + next = Vector2.Transform(controlPoints[nextIndex].Point, emit); if (controlPoints[currentIndex].OnCurve) { diff --git a/src/SixLabors.Fonts/TextLayout.LineBreaking.cs b/src/SixLabors.Fonts/TextLayout.LineBreaking.cs index 2d130e6a5..8b123331c 100644 --- a/src/SixLabors.Fonts/TextLayout.LineBreaking.cs +++ b/src/SixLabors.Fonts/TextLayout.LineBreaking.cs @@ -93,9 +93,9 @@ public static LogicalTextLine ComposeLogicalLine( continue; } - // Positions are read from the baked metrics for now; the flip to - // shared metrics sources these from the shaping bounds instead. - metrics.Add(new(data.Metrics, data.Metrics.AdvanceWidth, data.Metrics.AdvanceHeight, data.Metrics.Offset)); + // Post-GPOS positions live in the shaping bounds; the shared + // metrics instance is never mutated by positioning. + metrics.Add(new(data.Metrics, data.AdvanceWidth, data.AdvanceHeight, data.PositionOffset, data.Data.TextRun)); } if (metrics.Count == 0) @@ -188,19 +188,18 @@ VerticalOrientationType.Rotate or shapedText.LayoutMode, options.ColorFontSupport); + // The tab advance lives only in the positioned snapshot; + // the metrics instance is shared and must not be mutated. if (isHorizontalLayout || shouldRotate) { glyphAdvance = spaceMetrics.AdvanceWidth * options.TabWidth; - glyph.SetAdvanceWidth((ushort)glyphAdvance); + metrics[0] = new(glyph, (ushort)glyphAdvance, metrics[0].AdvanceHeight, metrics[0].Offset, metrics[0].TextRun); } else { glyphAdvance = spaceMetrics.AdvanceHeight * options.TabWidth; - glyph.SetAdvanceHeight((ushort)glyphAdvance); + metrics[0] = new(glyph, metrics[0].AdvanceWidth, (ushort)glyphAdvance, metrics[0].Offset, metrics[0].TextRun); } - - // Keep the positioned snapshot in step with the tab-adjusted metric. - metrics[0] = new(glyph, glyph.AdvanceWidth, glyph.AdvanceHeight, glyph.Offset); } } } @@ -360,7 +359,7 @@ VerticalOrientationType.Rotate or // then account for its advance without rescanning or reshaping the line. hyphenationMarkerIndex = hyphenationMarkers.Count; hyphenationMarkers.Add(CreateGeneratedMarker( - glyph, + metrics[0], pointSize, shapedText.BidiRuns[shapedText.BidiMap[codePointIndex]], graphemeIndex, @@ -376,7 +375,7 @@ VerticalOrientationType.Rotate or // Add our metrics to the line. textLine.Add( - isDecomposed ? [new PositionedGlyphMetrics(metric, metric.AdvanceWidth, metric.AdvanceHeight, metric.Offset)] : metrics, + isDecomposed ? [new PositionedGlyphMetrics(metric, positionedGlyph.AdvanceWidth, positionedGlyph.AdvanceHeight, positionedGlyph.PositionOffset, positionedGlyph.Data.TextRun)] : metrics, positionedGlyph.Font, pointSize, decomposedAdvance, @@ -555,7 +554,7 @@ private static List CollectLineBreaks(ReadOnlySpan text, bool i /// /// Creates a visible generated marker that matches the layout style of the anchor entry. /// - /// The glyph metric that supplies font, run, attributes, and decorations. + /// The positioned anchor glyph that supplies font, run, attributes, and decorations. /// The point size at which the marker is rendered. /// The bidi run that the marker belongs to. /// The source grapheme index to map the marker to. @@ -569,7 +568,7 @@ private static List CollectLineBreaks(ReadOnlySpan text, bool i /// The text options used for layout. /// The generated marker entry. internal static GlyphLayoutData CreateGeneratedMarker( - FontGlyphMetrics anchorMetric, + PositionedGlyphMetrics anchor, float pointSize, BidiRun bidiRun, int graphemeIndex, @@ -582,6 +581,7 @@ internal static GlyphLayoutData CreateGeneratedMarker( Font font, TextOptions options) { + FontGlyphMetrics anchorMetric = anchor.Metrics; anchorMetric.FontMetrics.TryGetGlyphId(markerCodePoint, out ushort markerGlyphId); FontGlyphMetrics markerMetric = anchorMetric.FontMetrics.GetGlyphMetrics( @@ -592,8 +592,6 @@ internal static GlyphLayoutData CreateGeneratedMarker( layoutMode, options.ColorFontSupport); - markerMetric = markerMetric.CloneForRendering(anchorMetric.TextRun); - bool isHorizontalLayout = layoutMode.IsHorizontal(); bool isVerticalLayout = layoutMode.IsVertical(); bool isVerticalMixedLayout = layoutMode.IsVerticalMixed(); @@ -639,10 +637,10 @@ VerticalOrientationType.Rotate or FontRectangle markerBox = FontGlyphMetrics.ShouldSkipGlyphRendering(markerMetric.CodePoint) ? FontRectangle.Empty - : markerMetric.GetBoundingBox(markerMode, Vector2.Zero, pointSize); + : markerMetric.GetBoundingBox(markerMode, Vector2.Zero, pointSize, anchor.TextRun, Vector2.Zero); return new GlyphLayoutData( - new PositionedGlyphMetrics[] { new(markerMetric, markerMetric.AdvanceWidth, markerMetric.AdvanceHeight, markerMetric.Offset) }, + [new(markerMetric, markerMetric.AdvanceWidth, markerMetric.AdvanceHeight, Vector2.Zero, anchor.TextRun)], font, pointSize, markerAdvance, diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index 4b231e8db..fb2ccb19c 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -857,7 +857,7 @@ private static void LayoutLineHorizontal( visitor.Visit( new GlyphLayout( - new Glyph(metric, data.PointSize), + new Glyph(metric, data.PointSize, data.Metrics[0].TextRun, data.Metrics[0].Offset), data.Font, boundsLocation, hardBreakGlyphOrigin, @@ -889,7 +889,7 @@ private static void LayoutLineHorizontal( visitor.Visit( new GlyphLayout( - new Glyph(metric, data.PointSize), + new Glyph(metric, data.PointSize, metrics[j].TextRun, metrics[j].Offset), data.Font, boundsLocation, glyphOrigin, @@ -1095,7 +1095,7 @@ private static void LayoutLineVertical( visitor.Visit( new GlyphLayout( - new Glyph(metric, data.PointSize), + new Glyph(metric, data.PointSize, data.Metrics[0].TextRun, data.Metrics[0].Offset), data.Font, boundsLocation, hardBreakGlyphOrigin, @@ -1221,7 +1221,7 @@ private static void LayoutLineVertical( { FontGlyphMetrics transformedMetric = transformedMetrics[m].Metrics; Vector2 s = new Vector2(data.PointSize) / transformedMetric.ScaleFactor; - entryScaledAdvanceWidth += transformedMetric.AdvanceWidth * s.X; + entryScaledAdvanceWidth += transformedMetrics[m].AdvanceWidth * s.X; } } @@ -1241,7 +1241,7 @@ private static void LayoutLineVertical( // Vertical origin fallback places the vertical origin at half the // horizontal advance. The decoration origin has already centered this // entry's line box in the column, so center the glyph advance inside it. - glyphAlignX = (scaledLineHeight - (metric.AdvanceWidth * scale.X)) * .5F; + glyphAlignX = (scaledLineHeight - (metrics[metricIndex].AdvanceWidth * scale.X)) * .5F; } // Move the glyph origin without changing the advance or decoration origin. @@ -1256,12 +1256,12 @@ private static void LayoutLineVertical( // For transformed glyphs after the first in the grapheme we advance // horizontally using the horizontal advance not the line height. // This gives us the correct total advance across the grapheme. - advanceW = scale.X * metric.AdvanceWidth; + advanceW = scale.X * metrics[metricIndex].AdvanceWidth; } visitor.Visit( new GlyphLayout( - new Glyph(metric, data.PointSize), + new Glyph(metric, data.PointSize, metrics[metricIndex].TextRun, metrics[metricIndex].Offset), data.Font, boundsLocation, glyphOrigin, @@ -1470,7 +1470,7 @@ private static void LayoutLineVerticalMixed( visitor.Visit( new GlyphLayout( - new Glyph(metric, data.PointSize), + new Glyph(metric, data.PointSize, data.Metrics[0].TextRun, data.Metrics[0].Offset), data.Font, boundsLocation, hardBreakGlyphOrigin, @@ -1519,7 +1519,7 @@ private static void LayoutLineVerticalMixed( visitor.Visit( new GlyphLayout( - new Glyph(metric, data.PointSize), + new Glyph(metric, data.PointSize, metrics[j].TextRun, metrics[j].Offset), data.Font, boundsLocation, glyphOrigin, @@ -1547,14 +1547,14 @@ private static void LayoutLineVerticalMixed( // Vertical origin fallback places the vertical origin at half the // horizontal advance. The decoration origin has already centered this // entry's line box in the column, so center the glyph advance inside it. - float glyphAlignX = (scaledLineHeight - (metric.AdvanceWidth * scale.X)) * .5F; + float glyphAlignX = (scaledLineHeight - (metrics[j].AdvanceWidth * scale.X)) * .5F; Vector2 glyphOffset = new(glyphAlignX, (metric.Bounds.Max.Y + metric.TopSideBearing) * scale.Y); Vector2 decorationOrigin = penLocation + new Vector2((unscaledLineHeight - scaledLineHeight) * .5F, 0); Vector2 glyphOrigin = decorationOrigin + glyphOffset; visitor.Visit( new GlyphLayout( - new Glyph(metric, data.PointSize), + new Glyph(metric, data.PointSize, metrics[j].TextRun, metrics[j].Offset), data.Font, boundsLocation, glyphOrigin, diff --git a/src/SixLabors.Fonts/TextLine.cs b/src/SixLabors.Fonts/TextLine.cs index 99680173c..3072d95e9 100644 --- a/src/SixLabors.Fonts/TextLine.cs +++ b/src/SixLabors.Fonts/TextLine.cs @@ -209,7 +209,7 @@ public void Add( continue; } - FontRectangle bbox = metric.GetBoundingBox(layoutMode, Vector2.Zero, pointSize); + FontRectangle bbox = metric.GetBoundingBox(layoutMode, Vector2.Zero, pointSize, metrics[i].TextRun, metrics[i].Offset); scaledMinY = MathF.Min(scaledMinY, bbox.Y); } @@ -278,7 +278,7 @@ public void AddPlaceholder( ? GlyphLayoutMode.Horizontal : GlyphLayoutMode.Vertical; - FontRectangle placeholderBox = placeholderGlyph.GetBoundingBox(placeholderMode, Vector2.Zero, placeholder.PointSize); + FontRectangle placeholderBox = placeholderGlyph.GetBoundingBox(placeholderMode, Vector2.Zero, placeholder.PointSize, placeholder.Data.TextRun, Vector2.Zero); IMetricsHeader metricsHeader = isPlaceholderHorizontal ? placeholderGlyph.FontMetrics.HorizontalMetrics @@ -302,7 +302,7 @@ public void AddPlaceholder( // Placeholders share the source codepoint offset at their insertion point, // but they do not consume source grapheme, codepoint, or UTF-16 indexes. this.Add( - new PositionedGlyphMetrics[] { new(placeholderGlyph, placeholderGlyph.AdvanceWidth, placeholderGlyph.AdvanceHeight, placeholderGlyph.Offset) }, + [new(placeholderGlyph, placeholderGlyph.AdvanceWidth, placeholderGlyph.AdvanceHeight, Vector2.Zero, placeholder.Data.TextRun)], placeholder.Font, placeholder.PointSize, placeholderAdvance, @@ -413,7 +413,7 @@ public void ApplyEllipsisMarker( GlyphLayoutData anchor = this.data[^1]; GlyphLayoutData marker = TextLayout.CreateGeneratedMarker( - anchor.Metrics[0].Metrics, + anchor.Metrics[0], anchor.PointSize, anchor.BidiRun, anchor.GraphemeIndex, diff --git a/src/SixLabors.Fonts/TextMeasurer.cs b/src/SixLabors.Fonts/TextMeasurer.cs index ec0fb8bc5..0d04d9da4 100644 --- a/src/SixLabors.Fonts/TextMeasurer.cs +++ b/src/SixLabors.Fonts/TextMeasurer.cs @@ -651,7 +651,9 @@ private static FontRectangle GetGlyphBounds(FontGlyphMetrics metrics, GlyphOptio return metrics.GetBoundingBox( layoutMode, GetAnchoredOrigin(options, layoutMode), - metrics.GetScaledSize(options.Font.Size, options.Dpi)); + metrics.GetScaledSize(options.Font.Size, options.Dpi), + null, + Vector2.Zero); } /// diff --git a/src/SixLabors.Fonts/TextShaper.cs b/src/SixLabors.Fonts/TextShaper.cs index d327e4c8c..7cac4d077 100644 --- a/src/SixLabors.Fonts/TextShaper.cs +++ b/src/SixLabors.Fonts/TextShaper.cs @@ -80,9 +80,9 @@ public static IReadOnlyList Shape(ReadOnlySpan text, TextOpti data.Data.CodePoint, data.Offset, data.Data.CodePointCount, - metrics.AdvanceWidth, - metrics.AdvanceHeight, - metrics.Offset)); + data.AdvanceWidth, + data.AdvanceHeight, + metrics.Offset + data.PositionOffset)); } ShapingProbe.Exit(ShapingProbe.Projection, probe); diff --git a/tests/SixLabors.Fonts.Tests/FontSynthesisTests.cs b/tests/SixLabors.Fonts.Tests/FontSynthesisTests.cs index 5902af967..f7d2204f2 100644 --- a/tests/SixLabors.Fonts.Tests/FontSynthesisTests.cs +++ b/tests/SixLabors.Fonts.Tests/FontSynthesisTests.cs @@ -153,10 +153,10 @@ public void SyntheticItalic_DoesNotChangeAdvance_ButWidensBounds() } [Fact] - public void GetObliqueSkew_ReturnsZero_WhenGlyphHasNoTextRun() + public void GetObliqueSkew_ReturnsZero_WhenTextRunIsNull() { - // The metrics returned directly from the font (i.e. not cloned for a specific run) - // carry no text run, so synthesis cannot be determined and must be disabled. + // Synthesis is driven by the text run; without one it cannot be determined + // and must be disabled. Font italic = new(RegularOnlyFamily(TestFonts.OpenSansFile), 24, FontStyle.Italic); Assert.True(italic.FontMetrics.TryGetGlyphMetrics( @@ -167,7 +167,7 @@ public void GetObliqueSkew_ReturnsZero_WhenGlyphHasNoTextRun() ColorFontSupport.None, out FontGlyphMetrics metrics)); - Assert.Equal(0F, metrics.GetObliqueSkew()); + Assert.Equal(0F, metrics.GetObliqueSkew(null)); } private static void AssertObliqueSkew(string file) @@ -181,8 +181,11 @@ private static void AssertObliqueSkew(string file) Assert.True(regular.TryGetGlyphs(codePoint, out Glyph? regularGlyph)); Assert.True(italic.TryGetGlyphs(codePoint, out Glyph? italicGlyph)); - Assert.Equal(0F, regularGlyph.Value.GlyphMetrics.GetObliqueSkew()); - Assert.Equal(ExpectedSkew, italicGlyph.Value.GlyphMetrics.GetObliqueSkew(), SkewComparer); + TextRun regularRun = new() { Start = 0, End = 1, Font = regular }; + TextRun italicRun = new() { Start = 0, End = 1, Font = italic }; + + Assert.Equal(0F, regularGlyph.Value.GlyphMetrics.GetObliqueSkew(regularRun)); + Assert.Equal(ExpectedSkew, italicGlyph.Value.GlyphMetrics.GetObliqueSkew(italicRun), SkewComparer); } private static void AssertSyntheticItalicShear(string file, string text, ColorFontSupport colorFontSupport) @@ -257,7 +260,8 @@ public void SyntheticBoldStrength_MatchesBrowserWeight() // GetSyntheticBoldStrength receives point size multiplied by DPI. At the default 72 DPI, // the scale factors cancel to leave the browser-matched emboldening fraction times point size. - float strength = glyph.Value.GlyphMetrics.GetSyntheticBoldStrength(pointSize * 72F); + TextRun textRun = new() { Start = 0, End = 1, Font = bold }; + float strength = glyph.Value.GlyphMetrics.GetSyntheticBoldStrength(pointSize * 72F, textRun); Assert.Equal(pointSize * ExpectedBoldEmScale, strength, SkewComparer); } @@ -330,8 +334,10 @@ public void SyntheticBoldItalic_CombinesBothSyntheses() // Both syntheses are driven independently, so requesting bold italic on a regular-only // family must enable both the shear and the outline dilation. - Assert.Equal(ExpectedSkew, glyph.Value.GlyphMetrics.GetObliqueSkew(), SkewComparer); - Assert.True(glyph.Value.GlyphMetrics.ShouldSynthesizeBold()); + TextRun textRun = new() { Start = 0, End = 1, Font = boldItalic }; + FontGlyphMetrics metrics = glyph.Value.GlyphMetrics; + Assert.Equal(ExpectedSkew, metrics.GetObliqueSkew(textRun), SkewComparer); + Assert.True(metrics.ShouldSynthesizeBold(textRun)); } [Fact] @@ -343,8 +349,11 @@ public void SyntheticBold_DoesNotEmboldenPaintedEmoji() CodePoint codePoint = new(0x1F600); Assert.True(bold.TryGetGlyphs(codePoint, ColorFontSupport.ColrV0, out Glyph? glyph)); - Assert.Equal(GlyphType.Painted, glyph.Value.GlyphMetrics.GlyphType); - Assert.False(glyph.Value.GlyphMetrics.ShouldSynthesizeBold()); + + TextRun textRun = new() { Start = 0, End = 1, Font = bold }; + FontGlyphMetrics metrics = glyph.Value.GlyphMetrics; + Assert.Equal(GlyphType.Painted, metrics.GlyphType); + Assert.False(metrics.ShouldSynthesizeBold(textRun)); TextOptions regularOptions = new(regular) { @@ -379,10 +388,10 @@ public void VisualTest_SyntheticBoldPaintedEmoji() } [Fact] - public void ShouldSynthesizeBold_ReturnsFalse_WhenGlyphHasNoTextRun() + public void ShouldSynthesizeBold_ReturnsFalse_WhenTextRunIsNull() { - // The metrics returned directly from the font (i.e. not cloned for a specific run) - // carry no text run, so synthesis cannot be determined and must be disabled. + // Synthesis is driven by the text run; without one it cannot be determined + // and must be disabled. Font bold = new(RegularOnlyFamily(TestFonts.OpenSansFile), 24, FontStyle.Bold); Assert.True(bold.FontMetrics.TryGetGlyphMetrics( @@ -393,7 +402,7 @@ public void ShouldSynthesizeBold_ReturnsFalse_WhenGlyphHasNoTextRun() ColorFontSupport.None, out FontGlyphMetrics metrics)); - Assert.False(metrics.ShouldSynthesizeBold()); + Assert.False(metrics.ShouldSynthesizeBold(null)); } private static void AssertShouldSynthesizeBold(string file) @@ -407,8 +416,11 @@ private static void AssertShouldSynthesizeBold(string file) Assert.True(regular.TryGetGlyphs(codePoint, out Glyph? regularGlyph)); Assert.True(bold.TryGetGlyphs(codePoint, out Glyph? boldGlyph)); - Assert.False(regularGlyph.Value.GlyphMetrics.ShouldSynthesizeBold()); - Assert.True(boldGlyph.Value.GlyphMetrics.ShouldSynthesizeBold()); + TextRun regularRun = new() { Start = 0, End = 1, Font = regular }; + TextRun boldRun = new() { Start = 0, End = 1, Font = bold }; + + Assert.False(regularGlyph.Value.GlyphMetrics.ShouldSynthesizeBold(regularRun)); + Assert.True(boldGlyph.Value.GlyphMetrics.ShouldSynthesizeBold(boldRun)); } private static void AssertSyntheticBoldGrows(string file, string text) diff --git a/tests/SixLabors.Fonts.Tests/FontWeightTests.cs b/tests/SixLabors.Fonts.Tests/FontWeightTests.cs index 37dd48cc3..7bccf72fd 100644 --- a/tests/SixLabors.Fonts.Tests/FontWeightTests.cs +++ b/tests/SixLabors.Fonts.Tests/FontWeightTests.cs @@ -108,8 +108,7 @@ public void StaticWeight_UsesBrowserBoldThreshold(FontWeight weight, float expec ColorFontSupport.None, out FontGlyphMetrics metrics)); - FontGlyphMetrics renderMetrics = metrics.CloneForRendering(textRun); - float strength = renderMetrics.GetSyntheticBoldStrength(pointSize * 72F); + float strength = metrics.GetSyntheticBoldStrength(pointSize * 72F, textRun); Assert.Equal((pointSize / 31F) * expectedFactor, strength, new ApproximateFloatComparer(.0001F)); } @@ -150,10 +149,8 @@ public void PaintedEmoji_DoesNotSynthesizeWeight() ColorFontSupport.ColrV0, out FontGlyphMetrics metrics)); - FontGlyphMetrics renderMetrics = metrics.CloneForRendering(textRun); - - Assert.Equal(GlyphType.Painted, renderMetrics.GlyphType); - Assert.False(renderMetrics.ShouldSynthesizeBold()); + Assert.Equal(GlyphType.Painted, metrics.GlyphType); + Assert.False(metrics.ShouldSynthesizeBold(textRun)); } [Theory] @@ -183,7 +180,7 @@ public void SystemWeight_UsesInstalledFaceWhenAvailable(FontWeight requestedWeig ColorFontSupport.None, out FontGlyphMetrics metrics)); - Assert.False(metrics.CloneForRendering(textRun).ShouldSynthesizeBold()); + Assert.False(metrics.ShouldSynthesizeBold(textRun)); } [Theory] @@ -221,7 +218,7 @@ public void SystemWeight_UsesBrowserFaceMatching(FontWeight requestedWeight, Fon ColorFontSupport.None, out FontGlyphMetrics metrics)); - Assert.False(metrics.CloneForRendering(textRun).ShouldSynthesizeBold()); + Assert.False(metrics.ShouldSynthesizeBold(textRun)); } [Theory] diff --git a/tests/SixLabors.Fonts.Tests/GlyphTests.cs b/tests/SixLabors.Fonts.Tests/GlyphTests.cs index ee873be91..a2639ea05 100644 --- a/tests/SixLabors.Fonts.Tests/GlyphTests.cs +++ b/tests/SixLabors.Fonts.Tests/GlyphTests.cs @@ -45,7 +45,7 @@ public void RenderToPointAndSingleDPI() textRun.TextDecorations, GlyphType.Standard); - Glyph glyph = new(glyphMetrics.CloneForRendering(textRun), font.Size); + Glyph glyph = new(glyphMetrics, font.Size, textRun, Vector2.Zero); Vector2 locationInFontSpace = new Vector2(99, 99) / 72; // glyph ends up 10px over due to offset in fake glyph glyph.RenderTo(this.renderer, 0, locationInFontSpace, Vector2.Zero, new Vector2(-1F), GlyphLayoutMode.Horizontal, new TextOptions(font)); From b1bea16005f9e4d92fd0ea26451db725ad222d7c Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 00:15:38 +1000 Subject: [PATCH 05/94] Checkpoint inherited optimizations: subtable digests, closure-free matching, iterator fast path Inherited from the interrupted session, gated as one checkpoint on the full test suite (4683 passed, 0 failed). Contains: - Per-subtable coverage digests in GSUB/GPOS so application skips virtual probes whose gating coverage cannot contain the current glyph. - Static-lambda Match overloads threading state, removing closure/delegate allocations per ligature and contextual rule attempt. - Chained sequence rules match input/lookahead before backtrack (HarfBuzz order). - SkippingGlyphIterator skipsNothing fast path and inline shaping-class cache. - ShapingFeatureMap single-entry memo for GetMask. - Bidi left-to-right fast path skipping the UAX#9 pass for uniform LTR text. - Extended Enabled-gated ShapingProbe counters (diagnostic only). Benchmarks follow in the next report entry; the complex-script SyllableType work and local dev-env sln/csproj edits from the same session were reverted (no covering benchmark scenario / machine-local paths). --- src/SixLabors.Fonts/ShapingProbe.cs | 55 ++++++++- .../AdvancedTypographicUtils.cs | 114 +++++++++++++++--- .../GPos/LookupListTable.cs | 27 +++++ .../Tables/AdvancedTypographic/GPosTable.cs | 5 + .../GSub/LookupListTable.cs | 27 +++++ .../Tables/AdvancedTypographic/GSubTable.cs | 5 + .../AdvancedTypographic/ShapingFeatureMap.cs | 35 +++++- .../SkippingGlyphIterator.cs | 30 ++++- src/SixLabors.Fonts/TextLayout.cs | 20 ++- src/SixLabors.Fonts/Unicode/BidiData.cs | 36 ++++++ 10 files changed, 331 insertions(+), 23 deletions(-) diff --git a/src/SixLabors.Fonts/ShapingProbe.cs b/src/SixLabors.Fonts/ShapingProbe.cs index 595b48ece..24f1c5956 100644 --- a/src/SixLabors.Fonts/ShapingProbe.cs +++ b/src/SixLabors.Fonts/ShapingProbe.cs @@ -3,6 +3,7 @@ // TEMPORARY diagnostic probe for shaping performance attribution. Delete before commit. using System.Diagnostics; +using SixLabors.Fonts.Tables.AdvancedTypographic; namespace SixLabors.Fonts; @@ -38,6 +39,19 @@ public static class ShapingProbe public static bool Enabled { get; set; } + public static readonly Dictionary FeatureTicks = []; + public static readonly Dictionary FeatureApplies = []; + + public static long IteratorSteps; + + public static long ContextIterators; + + public static long ClassifyCalls; + + public static long ClassifyMisses; + + public static long SubTableProbes; + public static long StageFeatureCalls; public static long LookupsConsidered; public static long LookupsSkippedByDigest; @@ -46,14 +60,51 @@ public static class ShapingProbe public static void PrintCounters(int iterations) => Console.WriteLine( - $"stages/op={StageFeatureCalls / (double)iterations:F1} " + + $"iterSteps/op={IteratorSteps / (double)iterations:F1} ctxIters/op={ContextIterators / (double)iterations:F1} classify/op={ClassifyCalls / (double)iterations:F1} classifyMiss/op={ClassifyMisses / (double)iterations:F1} subProbes/op={SubTableProbes / (double)iterations:F1} " + $"lookups/op={LookupsConsidered / (double)iterations:F1} " + $"digestSkipped/op={LookupsSkippedByDigest / (double)iterations:F1} " + $"glyphGates/op={GlyphGateChecks / (double)iterations:F1} " + $"substAttempts/op={SubstitutionAttempts / (double)iterations:F1}"); public static void ResetCounters() - => StageFeatureCalls = LookupsConsidered = LookupsSkippedByDigest = GlyphGateChecks = SubstitutionAttempts = 0; + => StageFeatureCalls = LookupsConsidered = LookupsSkippedByDigest = GlyphGateChecks = SubstitutionAttempts = + IteratorSteps = ContextIterators = ClassifyCalls = ClassifyMisses = SubTableProbes = 0; + + public static long FeatureStepsStart; + + public static long Timestamp() + { + FeatureStepsStart = IteratorSteps; + return Enabled ? Stopwatch.GetTimestamp() : 0; + } + + public static readonly Dictionary FeatureSteps = []; + + public static void ExitFeature(string table, Tag feature, long start, long applies) + { + if (!Enabled) + { + return; + } + + string key = $"{table}:{feature}"; + FeatureTicks[key] = FeatureTicks.GetValueOrDefault(key) + Stopwatch.GetTimestamp() - start; + FeatureApplies[key] = FeatureApplies.GetValueOrDefault(key) + applies; + FeatureSteps[key] = FeatureSteps.GetValueOrDefault(key) + IteratorSteps - FeatureStepsStart; + } + + public static void PrintFeatures(int iterations) + { + foreach (KeyValuePair pair in FeatureTicks.OrderByDescending(x => x.Value)) + { + double us = pair.Value * 1_000_000.0 / Stopwatch.Frequency / iterations; + Console.WriteLine($" {pair.Key,-16} {us,8:F2} us/op applies/op={FeatureApplies.GetValueOrDefault(pair.Key) / (double)iterations:F1} steps/op={FeatureSteps.GetValueOrDefault(pair.Key) / (double)iterations:F1}"); + } + + FeatureTicks.Clear(); + FeatureApplies.Clear(); + FeatureSteps.Clear(); + } public static (long Ticks, long Bytes) Enter() => Enabled ? (Stopwatch.GetTimestamp(), GC.GetAllocatedBytesForCurrentThread()) : default; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs index a0652a94f..9afb64c7c 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs @@ -117,6 +117,11 @@ public static bool ApplyLookupList( int count) { SkippingGlyphIterator iterator = new(fontMetrics, collection, index, lookupFlags, markFilteringSet); + if (ShapingProbe.Enabled) + { + ShapingProbe.ContextIterators++; + } + int currentCount = collection.Count; foreach (SequenceLookupRecord lookupRecord in records) @@ -164,6 +169,11 @@ public static bool ApplyLookupList( int count) { SkippingGlyphIterator iterator = new(fontMetrics, collection, index, lookupFlags, markFilteringSet); + if (ShapingProbe.Enabled) + { + ShapingProbe.ContextIterators++; + } + foreach (SequenceLookupRecord lookupRecord in records) { ushort sequenceIndex = lookupRecord.SequenceIndex; @@ -190,13 +200,16 @@ public static bool MatchInputSequence(SkippingGlyphIterator iterator, Tag featur { ulong featureMask = iterator.Collection.FeatureMap.GetMask(feature); + // The mask travels as match state so the lambda stays static: a capturing + // lambda here would allocate a closure and delegate on every ligature attempt. return Match( increment, sequence, iterator, - (component, data) => + featureMask, + static (component, data, mask) => { - if ((data.FeatureMask & featureMask) == 0) + if ((data.FeatureMask & mask) == 0) { return false; } @@ -234,11 +247,16 @@ public static bool MatchClassSequence( int increment, ushort[] sequence, ClassDefinitionTable classDefinitionTable) + + // The class table travels as match state so the lambda stays static: a + // capturing lambda here would allocate a closure and delegate on every + // contextual rule attempt. => Match( increment, sequence, iterator, - (component, data) => component == classDefinitionTable.ClassIndexOf(data.GlyphId), + classDefinitionTable, + static (component, data, table) => component == table.ClassIndexOf(data.GlyphId), default); /// @@ -294,12 +312,6 @@ public static bool MatchBacktrackCoverageSequence( /// if all sequences matched; otherwise, . public static bool ApplyChainedSequenceRule(SkippingGlyphIterator iterator, ChainedSequenceRuleTable rule) { - if (rule.BacktrackSequence.Length > 0 - && !MatchSequence(iterator, -rule.BacktrackSequence.Length, rule.BacktrackSequence)) - { - return false; - } - if (rule.InputSequence.Length > 0 && !MatchSequence(iterator, 1, rule.InputSequence)) { @@ -312,6 +324,12 @@ public static bool ApplyChainedSequenceRule(SkippingGlyphIterator iterator, Chai return false; } + if (rule.BacktrackSequence.Length > 0 + && !MatchSequence(iterator, -rule.BacktrackSequence.Length, rule.BacktrackSequence)) + { + return false; + } + return true; } @@ -331,12 +349,6 @@ public static bool ApplyChainedClassSequenceRule( ClassDefinitionTable backtrackClassDefinitionTable, ClassDefinitionTable lookaheadClassDefinitionTable) { - if (rule.BacktrackSequence.Length > 0 - && !MatchClassSequence(iterator, -rule.BacktrackSequence.Length, rule.BacktrackSequence, backtrackClassDefinitionTable)) - { - return false; - } - if (rule.InputSequence.Length > 0 && !MatchClassSequence(iterator, 1, rule.InputSequence, inputClassDefinitionTable)) { @@ -349,6 +361,12 @@ public static bool ApplyChainedClassSequenceRule( return false; } + if (rule.BacktrackSequence.Length > 0 + && !MatchClassSequence(iterator, -rule.BacktrackSequence.Length, rule.BacktrackSequence, backtrackClassDefinitionTable)) + { + return false; + } + return true; } @@ -379,6 +397,10 @@ public static bool CheckAllCoverages( int endExclusive = index + count; SkippingGlyphIterator iterator = new(fontMetrics, collection, index, lookupFlags, markFilteringSet); + if (ShapingProbe.Enabled) + { + ShapingProbe.ContextIterators++; + } // Compute backtrack start using skippy prev(), not index-1. int backtrackStart = index; @@ -527,11 +549,21 @@ public static GlyphShapingClass GetGlyphShapingClass(FontMetrics fontMetrics, us { // Cache the shaping class on the GlyphShapingData to avoid repeated GDEF lookups. // The cache key stores the glyph id; -1 means "not cached". + if (ShapingProbe.Enabled) + { + ShapingProbe.ClassifyCalls++; + } + if (shapingData.ShapingClassCacheKey == glyphId) { return shapingData.CachedShapingClass; } + if (ShapingProbe.Enabled) + { + ShapingProbe.ClassifyMisses++; + } + bool isMark; bool isBase; bool isLigature; @@ -570,6 +602,58 @@ public static GlyphShapingClass GetGlyphShapingClass(FontMetrics fontMetrics, us public static bool IsInMarkFilteringSet(FontMetrics fontMetrics, ushort markFilteringSet, ushort glyphId) => fontMetrics.IsInMarkFilteringSet(markFilteringSet, glyphId); + /// + /// Matches a sequence of elements against glyphs using an increment-based approach, + /// threading caller state through to the condition so callers can use static lambdas + /// instead of allocating closures. + /// + /// The type of sequence elements to match. + /// The type of the state passed to the condition. + /// The initial increment from the iterator's current position. + /// The array of elements to match. + /// The skipping glyph iterator. + /// The caller state passed to each condition invocation. + /// The condition function to test each element against glyph data. + /// A span to store matched glyph indices, or default if not needed. + /// if all elements in the sequence were matched; otherwise, . + private static bool Match( + int increment, + T[] sequence, + SkippingGlyphIterator iterator, + TState state, + Func condition, + Span matches) + { + int position = iterator.Index; + int offset = iterator.Increment(increment); + GlyphShapingCollection collection = iterator.Collection; + + if (offset < 0) + { + return false; + } + + int i = 0; + while (i < sequence.Length && i < MaxContextLength && offset < collection.Count) + { + if (!condition(sequence[i], collection[offset], state)) + { + break; + } + + if (matches.Length == MaxContextLength) + { + matches[i] = iterator.Index; + } + + i++; + offset = iterator.Next(); + } + + iterator.Index = position; + return i == sequence.Length; + } + /// /// Matches a sequence of elements against glyphs using an increment-based approach. /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupListTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupListTable.cs index a28dc7569..2074cf45f 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupListTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupListTable.cs @@ -99,6 +99,13 @@ private LookupTable( GlyphSetDigest digest = default; for (int i = 0; i < lookupSubTables.Length; i++) { + // Each subtable also carries its own digest so application can skip + // subtables whose gating coverage cannot contain the current glyph + // without paying the virtual probe. Contextual formats that expose no + // leading coverage flood their digest and therefore always pass. + GlyphSetDigest subTableDigest = default; + lookupSubTables[i].CollectDigest(ref subTableDigest); + lookupSubTables[i].Digest = subTableDigest; lookupSubTables[i].CollectDigest(ref digest); } @@ -221,8 +228,22 @@ public bool TryUpdatePosition( int index, int count) { + ushort glyphId = collection[index].GlyphId; foreach (LookupSubTable subTable in this.LookupSubTables) { + // A glyph outside the subtable's digest cannot match its coverage, so the + // probe (a virtual call, coverage search, or full context-match attempt) + // is skipped entirely. + if (!subTable.Digest.MightContain(glyphId)) + { + continue; + } + + if (ShapingProbe.Enabled) + { + ShapingProbe.SubTableProbes++; + } + // A lookup is finished for a glyph after the client locates the target // glyph or glyph context and performs a positioning action, if specified. if (subTable.TryUpdatePosition(fontMetrics, table, collection, feature, index, count)) @@ -261,6 +282,12 @@ protected LookupSubTable(LookupFlags lookupFlags, ushort markFilteringSet) /// public ushort MarkFilteringSet { get; } + /// + /// Gets or sets the approximate membership filter for the glyphs this subtable can + /// affect. Assigned once by the owning during construction. + /// + public GlyphSetDigest Digest { get; internal set; } + /// /// Adds the coverage that gates this subtable's applicability to the digest. /// The default adds every glyph so the lookup is always attempted, the correct diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs index 5b9e31a82..b7473b2f9 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs @@ -259,6 +259,8 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas ulong featureMask = collection.FeatureMap.GetMask(feature); LookupTable featureLookupTable = featureLookup.LookupTable; iterator.Reset(index, featureLookupTable.LookupFlags, featureLookupTable.MarkFilteringSet); + long featureStart = ShapingProbe.Timestamp(); + long featureApplies = 0; while (iterator.Index < index + count) { @@ -279,10 +281,13 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas } bool success = featureLookup.LookupTable.TryUpdatePosition(fontMetrics, this, collection, featureLookup.Feature, iterator.Index, count - (iterator.Index - index)); + featureApplies++; kerned |= success && (feature == KernTag || feature == VKernTag); updated |= success; iterator.Next(); } + + ShapingProbe.ExitFeature("GPOS", feature, featureStart, featureApplies); } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs index 5d7cdf146..910705b71 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs @@ -99,6 +99,13 @@ private LookupTable( GlyphSetDigest digest = default; for (int i = 0; i < lookupSubTables.Length; i++) { + // Each subtable also carries its own digest so application can skip + // subtables whose gating coverage cannot contain the current glyph + // without paying the virtual probe. Contextual formats that expose no + // leading coverage flood their digest and therefore always pass. + GlyphSetDigest subTableDigest = default; + lookupSubTables[i].CollectDigest(ref subTableDigest); + lookupSubTables[i].Digest = subTableDigest; lookupSubTables[i].CollectDigest(ref digest); } @@ -198,8 +205,22 @@ public bool TrySubstitution( int index, int count) { + ushort glyphId = collection[index].GlyphId; foreach (LookupSubTable subTable in this.LookupSubTables) { + // A glyph outside the subtable's digest cannot match its coverage, so the + // probe (a virtual call, coverage search, or full context-match attempt) + // is skipped entirely. + if (!subTable.Digest.MightContain(glyphId)) + { + continue; + } + + if (ShapingProbe.Enabled) + { + ShapingProbe.SubTableProbes++; + } + if (subTable.TrySubstitution(fontMetrics, table, collection, feature, index, count)) { // A lookup is finished for a glyph after the client locates the target @@ -268,6 +289,12 @@ protected LookupSubTable(LookupFlags lookupFlags, ushort markFilteringSet) /// public ushort MarkFilteringSet { get; } + /// + /// Gets or sets the approximate membership filter for the glyphs this subtable can + /// affect. Assigned once by the owning during construction. + /// + public GlyphSetDigest Digest { get; internal set; } + /// /// Adds the coverage that gates this subtable's applicability to the digest. /// The default adds every glyph so the lookup is always attempted, the correct diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs index 18dc9de77..96fa20c34 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs @@ -306,6 +306,8 @@ internal void ApplyFeature( ulong featureMask = collection.FeatureMap.GetMask(feature); LookupTable featureLookupTable = featureLookup.LookupTable; iterator.Reset(index, featureLookupTable.LookupFlags, featureLookupTable.MarkFilteringSet); + long featureStart = ShapingProbe.Timestamp(); + long featureApplies = 0; while (iterator.Index < index + count) { @@ -335,6 +337,7 @@ internal void ApplyFeature( collectionCount = collection.Count; featureLookup.LookupTable.TrySubstitution(fontMetrics, this, collection, featureLookup.Feature, iterator.Index, count - (iterator.Index - index)); + featureApplies++; iterator.Next(); // Account for substitutions changing the length of the collection. @@ -342,6 +345,8 @@ internal void ApplyFeature( count += delta; i += delta; } + + ShapingProbe.ExitFeature("GSUB", feature, featureStart, featureApplies); } } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapingFeatureMap.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapingFeatureMap.cs index d7033d302..7c069ac50 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapingFeatureMap.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapingFeatureMap.cs @@ -50,6 +50,19 @@ internal sealed class ShapingFeatureMap /// private readonly List tags = new(16); + /// + /// The most recently resolved tag value. Queries within a pass strongly repeat the + /// same feature (every per-glyph apply during one feature's application resolves that + /// feature's tag), so a single-entry memo answers almost every query without a list + /// search. Zero means the memo is empty; the zero tag is never a valid feature. + /// + private uint lastTagValue; + + /// + /// The mask paired with . + /// + private ulong lastMask; + /// /// Gets the mask bit for the given feature tag, or zero when the tag has not been /// registered. A zero result is safe at every consumption site: testing it enables @@ -59,8 +72,23 @@ internal sealed class ShapingFeatureMap /// The single-bit mask, or zero. public ulong GetMask(Tag tag) { + if (tag.Value == this.lastTagValue) + { + return this.lastMask; + } + int index = this.tags.IndexOf(tag.Value); - return index < 0 ? 0 : 1UL << index; + ulong mask = index < 0 ? 0 : 1UL << index; + + // A zero mask is never memoized: the tag may be registered later in the pass + // and the memo must not serve a stale zero after that registration. + if (mask != 0) + { + this.lastTagValue = tag.Value; + this.lastMask = mask; + } + + return mask; } /// @@ -87,6 +115,9 @@ public ulong GetOrAddMask(Tag tag) } this.tags.Add(tag.Value); - return 1UL << (this.tags.Count - 1); + mask = 1UL << (this.tags.Count - 1); + this.lastTagValue = tag.Value; + this.lastMask = mask; + return mask; } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs index bb232fc62..974cb7403 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs @@ -20,6 +20,13 @@ internal struct SkippingGlyphIterator private bool useMarkFilteringSet; private ushort markFilteringSet; + /// + /// True when the current lookup flags cannot ignore any glyph, so stepping never + /// needs to fetch or classify glyphs. Most lookups carry no ignore flags, which + /// makes plain index arithmetic the common path. + /// + private bool skipsNothing; + /// /// Initializes a new instance of the struct. /// @@ -44,6 +51,7 @@ public SkippingGlyphIterator( this.markAttachmentType = (ushort)((int)(lookupFlags & LookupFlags.MarkAttachmentTypeMask) >> 8); this.useMarkFilteringSet = (lookupFlags & LookupFlags.UseMarkFilteringSet) != 0; this.markFilteringSet = markFilteringSet; + this.skipsNothing = this.ignoreClassMask == 0 && this.markAttachmentType == 0 && !this.useMarkFilteringSet; } /// @@ -62,6 +70,11 @@ public SkippingGlyphIterator( /// The new index after advancing. public int Next() { + if (ShapingProbe.Enabled) + { + ShapingProbe.IteratorSteps++; + } + this.Move(1); return this.Index; } @@ -108,6 +121,7 @@ public void Reset(int index, LookupFlags lookupFlags, ushort markFilteringSet) this.markAttachmentType = (ushort)((int)(lookupFlags & LookupFlags.MarkAttachmentTypeMask) >> 8); this.useMarkFilteringSet = (lookupFlags & LookupFlags.UseMarkFilteringSet) != 0; this.markFilteringSet = markFilteringSet; + this.skipsNothing = this.ignoreClassMask == 0 && this.markAttachmentType == 0 && !this.useMarkFilteringSet; } /// @@ -117,6 +131,15 @@ public void Reset(int index, LookupFlags lookupFlags, ushort markFilteringSet) private void Move(int direction) { this.Index += direction; + + // When the flags cannot ignore anything, ShouldIgnore is provably false for + // every glyph: the class mask test is against zero and the mark branches are + // disabled. Skip the per-glyph fetch and classification entirely. + if (this.skipsNothing) + { + return; + } + while (this.Index >= 0 && this.Index < this.Collection.Count) { if (!this.ShouldIgnore(this.Index)) @@ -136,7 +159,12 @@ private void Move(int direction) private readonly bool ShouldIgnore(int index) { GlyphShapingData data = this.Collection[index]; - ushort props = AdvancedTypographicUtils.GetGlyphShapingClass(this.fontMetrics, data.GlyphId, data).Props; + + // The shaping class is cached on the glyph keyed by glyph id; test the cache + // inline so the common hit path avoids the classification call entirely. + ushort props = data.ShapingClassCacheKey == data.GlyphId + ? data.CachedShapingClass.Props + : AdvancedTypographicUtils.GetGlyphShapingClass(this.fontMetrics, data.GlyphId, data).Props; if ((props & this.ignoreClassMask) != 0) { diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index fb2ccb19c..5674c693b 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -171,10 +171,24 @@ or BidiCharacterType.FirstStrongIsolate } } - bidi.Process(bidiData); + // Purely left-to-right text resolves to a single even-level run without running + // the bidirectional algorithm: with no right-to-left or directional codepoints and + // a left-to-right (or auto) paragraph direction, every resolved level is zero. + // This is the overwhelmingly common case for Latin text and skips the full UAX#9 + // pass. An overridden or right-to-left paragraph always resolves levels. + BidiRun[] bidiRuns; + if (options.TextDirection != TextDirection.RightToLeft + && options.TextBidiMode != TextBidiMode.Override + && bidiData.IsUniformLeftToRight) + { + bidiRuns = [new BidiRun(BidiCharacterType.LeftToRight, 0, 0, bidiData.Types.Length)]; + } + else + { + bidi.Process(bidiData); + bidiRuns = [.. BidiRun.CoalesceLevels(bidi.ResolvedLevels)]; + } - // Get the list of directional runs - BidiRun[] bidiRuns = [.. BidiRun.CoalesceLevels(bidi.ResolvedLevels)]; Dictionary bidiMap = []; ShapingProbe.Exit(ShapingProbe.Bidi, probe); diff --git a/src/SixLabors.Fonts/Unicode/BidiData.cs b/src/SixLabors.Fonts/Unicode/BidiData.cs index 6133a6889..6d5c7612b 100644 --- a/src/SixLabors.Fonts/Unicode/BidiData.cs +++ b/src/SixLabors.Fonts/Unicode/BidiData.cs @@ -35,6 +35,42 @@ internal class BidiData /// public ArraySlice Types { get; private set; } + /// + /// Gets a value indicating whether every resolved level is provably zero in a + /// left-to-right paragraph, allowing the bidirectional algorithm to be skipped. + /// True only when the text contains no embedding or isolate initiators and no + /// codepoint whose type can raise an embedding level: strong right-to-left letters + /// and Arabic numbers force non-zero levels, and boundary neutrals are excluded + /// conservatively because their resolved levels depend on removed-character + /// handling. Everything else (L, EN, separators, whitespace, neutrals, NSM) keeps + /// level zero. + /// + public bool IsUniformLeftToRight + { + get + { + if (this.HasEmbeddings || this.HasIsolates) + { + return false; + } + + ArraySlice types = this.Types; + for (int i = 0; i < types.Length; i++) + { + BidiCharacterType type = types[i]; + if (type is BidiCharacterType.RightToLeft + or BidiCharacterType.ArabicLetter + or BidiCharacterType.ArabicNumber + or BidiCharacterType.BoundaryNeutral) + { + return false; + } + } + + return true; + } + } + /// /// Gets the paired bracket type for each code point /// From 892499440373984835d9c642415e25237319969e Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 00:33:21 +1000 Subject: [PATCH 06/94] Flatten shaping stage pipeline: List-based stages, no per-call hashing or boxed enumeration DefaultShaper accumulated stages in a HashSet rebuilt per section per table per call, with one hash insert per glyph for the per-glyph directional features, and GSUB/GPOS walked it through IEnumerable (interface dispatch + boxed enumerator). Stages are now an insertion-ordered List with linear tag dedup (stage counts are <= ~16, uint compares beat hashing) returned as the concrete List type. Stage application order is now deterministic registration order rather than HashSet implementation order. Tests: 4683 passed, 0 failed. Differential vs HarfBuzzSharp: exact glyph id and advance match on Latin and Arabic scenarios before and after. ShapeTextBenchmark vs previous commit (two runs; allocation figures exact): | Scenario | Mean before | Mean after | Ratio vs HB | Alloc before | Alloc after | |---|---:|---:|---:|---:|---:| | Latin | 30.020 us | 26.816 / 22.606 us | 9.18 -> 8.15 / 6.45 | 28,968 B | 27,352 B (-5.6%) | | Arabic | 31.021 us | 25.816 / 24.408 us | 5.08 -> 5.20 / 5.01 | 24,840 B | 22,040 B (-11.3%) | --- .../Tables/AdvancedTypographic/GPosTable.cs | 2 +- .../Tables/AdvancedTypographic/GSubTable.cs | 2 +- .../AdvancedTypographic/Shapers/BaseShaper.cs | 6 +++-- .../Shapers/DefaultShaper.cs | 23 +++++++++++++++---- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs index b7473b2f9..6031bc003 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs @@ -228,7 +228,7 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas // Plan positioning features for each glyph. shaper.Plan(collection, index, count); - IEnumerable shapingStages = shaper.GetShapingStages(); + List shapingStages = shaper.GetShapingStages(); SkippingGlyphIterator iterator = new(fontMetrics, collection, index, default, 0); foreach (ShapingStage stage in shapingStages) { diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs index 96fa20c34..c34908635 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs @@ -205,7 +205,7 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas i += delta; count += delta; - IEnumerable stages = shaper.GetShapingStages(); + List stages = shaper.GetShapingStages(); SkippingGlyphIterator iterator = new(fontMetrics, collection, index, default, 0); foreach (ShapingStage stage in stages) { diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs index 5427bef47..e7d6db387 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs @@ -79,10 +79,12 @@ public void Plan(GlyphShapingCollection collection, int index, int count) protected abstract void AssignFeatures(GlyphShapingCollection collection, int index, int count); /// - /// Gets the ordered collection of shaping stages for this shaper. + /// Gets the ordered collection of shaping stages for this shaper. The concrete + /// list type lets the per-section stage walk enumerate without interface + /// dispatch or a boxed enumerator. /// /// The shaping stages. - public abstract IEnumerable GetShapingStages(); + public abstract List GetShapingStages(); /// /// Recalculates the count when the collection size changes during shaping. diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs index 3a7eee0f1..83ecf0170 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs @@ -80,8 +80,12 @@ internal class DefaultShaper : BaseShaper /// The solidus (slash) code point (U+002F). private static readonly CodePoint Slash = new(0x002F); - /// The set of shaping stages accumulated during feature planning. - private readonly HashSet shapingStages = []; + /// + /// The shaping stages accumulated during feature planning, in registration order. + /// Stage counts are small (โ‰ค ~16), so duplicate suppression scans the list by tag; + /// this keeps application order deterministic and avoids per-call hashing. + /// + private readonly List shapingStages = new(16); /// The kerning mode from the text options. private readonly KerningMode kerningMode; @@ -239,11 +243,22 @@ protected void AddFeature( collection.AddShapingFeatureRange(index, count, new TagEntry(feature, enabled)); - this.shapingStages.Add(new ShapingStage(feature, preAction, postAction)); + // First registration wins, matching the previous set semantics: a duplicate + // tag keeps the originally supplied pre and post actions. + List stages = this.shapingStages; + for (int i = 0; i < stages.Count; i++) + { + if (stages[i].FeatureTag == feature) + { + return; + } + } + + stages.Add(new ShapingStage(feature, preAction, postAction)); } /// - public override IEnumerable GetShapingStages() => this.shapingStages; + public override List GetShapingStages() => this.shapingStages; /// /// Assigns fractional feature tags (numerator, denominator, fraction) to glyphs forming fraction sequences. From 56b3bae26464f32fbf3405f5e3d2199ba7a05020 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 00:38:37 +1000 Subject: [PATCH 07/94] Cut fixed-pipeline shaping allocations: struct offset pairs and array bidi map Two allocation sources with fixed structure removed from every shape call: - GlyphSubstitutionCollection's OffsetGlyphDataPair becomes a readonly struct, eliminating one wrapper object per glyph; the data-shuffling sites (MoveGlyph, Sort) rewrite whole pairs keeping offsets in place. - The codepoint-to-bidi-run map becomes an int[] indexed by codepoint position (-1 = unvisited) instead of a per-codepoint Dictionary insert; consumers already indexed it directly. Tests: 4683 passed, 0 failed. Differential vs HarfBuzzSharp: exact glyph id and advance match on Latin and Arabic scenarios. Benchmark table for this state is carried as the before-column of the immediately following pooled-memory commit. --- .../GlyphSubstitutionCollection.cs | 19 +++++++++++-------- src/SixLabors.Fonts/ShapedText.cs | 12 ++++++++---- src/SixLabors.Fonts/TextLayout.cs | 5 +++-- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs index 1126d8eb4..d72e8c4ac 100644 --- a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs +++ b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs @@ -126,7 +126,7 @@ public void MoveGlyph(int fromIndex, int toIndex) // Move item to the right for (int i = fromIndex; i > toIndex; i--) { - this.glyphs[i].Data = this.glyphs[i - 1].Data; + this.glyphs[i] = new(this.glyphs[i].Offset, this.glyphs[i - 1].Data); } } else @@ -134,11 +134,11 @@ public void MoveGlyph(int fromIndex, int toIndex) // Move item to the left for (int i = fromIndex; i < toIndex; i++) { - this.glyphs[i].Data = this.glyphs[i + 1].Data; + this.glyphs[i] = new(this.glyphs[i].Offset, this.glyphs[i + 1].Data); } } - this.glyphs[toIndex].Data = data; + this.glyphs[toIndex] = new(this.glyphs[toIndex].Offset, data); } /// @@ -195,8 +195,11 @@ public void Sort(int startIndex, int endIndex, Comparison comp int j = i; while (j > startIndex && comparer(glyphs[j - 1].Data, glyphs[j].Data) > 0) { - // Swap Data references between adjacent slots. - (glyphs[j].Data, glyphs[j - 1].Data) = (glyphs[j - 1].Data, glyphs[j].Data); + // Swap Data references between adjacent slots; offsets stay in place. + OffsetGlyphDataPair upper = glyphs[j]; + OffsetGlyphDataPair lower = glyphs[j - 1]; + glyphs[j] = new(upper.Offset, lower.Data); + glyphs[j - 1] = new(lower.Offset, upper.Data); j--; } } @@ -412,7 +415,7 @@ public void Insert(int index, GlyphShapingData data) } [DebuggerDisplay("{DebuggerDisplay,nq}")] - private class OffsetGlyphDataPair + private readonly struct OffsetGlyphDataPair { public OffsetGlyphDataPair(int offset, GlyphShapingData data) { @@ -420,9 +423,9 @@ public OffsetGlyphDataPair(int offset, GlyphShapingData data) this.Data = data; } - public int Offset { get; set; } + public int Offset { get; } - public GlyphShapingData Data { get; set; } + public GlyphShapingData Data { get; } private string DebuggerDisplay => FormattableString.Invariant($"Offset: {this.Offset}, Data: {this.Data.ToDebuggerDisplay()}"); } diff --git a/src/SixLabors.Fonts/ShapedText.cs b/src/SixLabors.Fonts/ShapedText.cs index b91307d03..3eb8ac028 100644 --- a/src/SixLabors.Fonts/ShapedText.cs +++ b/src/SixLabors.Fonts/ShapedText.cs @@ -15,12 +15,15 @@ internal readonly struct ShapedText /// /// The positioned glyph shaping collection. /// The resolved bidi runs covering the shaped text. - /// The code point to bidi-run mapping built during shaping. + /// + /// The code point index to bidi-run index mapping built during shaping. Entries for + /// code points no shaping pass visited hold -1. + /// /// The layout mode used while shaping. public ShapedText( GlyphPositioningCollection positionings, BidiRun[] bidiRuns, - Dictionary bidiMap, + int[] bidiMap, LayoutMode layoutMode) { this.Positionings = positionings; @@ -40,9 +43,10 @@ public ShapedText( public BidiRun[] BidiRuns { get; } /// - /// Gets the code point to bidi-run mapping built during shaping. + /// Gets the code point index to bidi-run index mapping built during shaping, + /// indexed by code point position. Unvisited positions hold -1. /// - public Dictionary BidiMap { get; } + public int[] BidiMap { get; } /// /// Gets the layout mode used while shaping. diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index 5674c693b..c4019a3f7 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -189,7 +189,8 @@ or BidiCharacterType.FirstStrongIsolate bidiRuns = [.. BidiRun.CoalesceLevels(bidi.ResolvedLevels)]; } - Dictionary bidiMap = []; + int[] bidiMap = new int[bidiData.Types.Length]; + Array.Fill(bidiMap, -1); ShapingProbe.Exit(ShapingProbe.Bidi, probe); probe = ShapingProbe.Enter(); @@ -1627,7 +1628,7 @@ private static bool DoFontRun( bool isFallbackRun, Font font, BidiRun[] bidiRuns, - Dictionary bidiMap, + int[] bidiMap, GlyphSubstitutionCollection substitutions, GlyphPositioningCollection positionings) { From da42797b7cc2fef44f23a2d2124d7a0dedd75dc9 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 08:33:07 +1000 Subject: [PATCH 08/94] Port the HarfBuzz buffer memory model: pooled shaping pipeline state TextShaper.Shape now rents ShapingScratch from the repository ObjectPool, the hb_buffer_t memory model: the substitution and positioning collections, their shared ShapingFeatureMap, and the GlyphShapingData instances themselves are reused across calls. The positioning collection - sole owner of a pass's final instances after the ownership transfer - returns them to a pool at reset; the substitution collection rents them back, reset in place, as glyphs are added. Scratch storage stays at its high-water mark. Reuse is safe because the public result is materialized by value before the scratch is returned; layout and measure paths are untouched and keep fresh collections pending the shaping pipeline consolidation. Tests: 4683 passed, 0 failed. Pooled-reuse oracle (interleaved Latin/Arabic x3 rounds) reproduces round-0 output exactly; HarfBuzzSharp differential remains an exact glyph id and advance match. ShapeTextBenchmark, 12 iterations, vs previous commit (measured on the thread-static variant; the ObjectPool refit's separate run showed byte-identical allocations, with means noise-dominated - HarfBuzz's own times co-moved ~+50% - and pool overhead of two interlocked ops per call not resolvable above that noise): | Scenario | Mean | Ratio vs HB | Allocated | Gen1 | |---|---:|---:|---:|---| | Latin | 17.226 -> 16.103 us (-6.5%) | 7.59 -> 7.22 | 24,144 -> 9,208 B (-61.9%) | 0 | | Arabic | 18.148 -> 16.897 us (-6.9%) | 4.78 -> 3.83 | 18,400 -> 9,184 B (-50.1%) | 0 | --- .../GlyphPositioningCollection.cs | 21 +++++ src/SixLabors.Fonts/GlyphShapingCollection.cs | 20 ++++- src/SixLabors.Fonts/GlyphShapingData.cs | 34 +++++++++ .../GlyphSubstitutionCollection.cs | 71 +++++++++++++---- src/SixLabors.Fonts/ShapingScratch.cs | 66 ++++++++++++++++ .../AdvancedTypographic/ShapingFeatureMap.cs | 11 +++ src/SixLabors.Fonts/TextLayout.cs | 34 +++++++-- src/SixLabors.Fonts/TextShaper.cs | 76 +++++++++++++------ 8 files changed, 288 insertions(+), 45 deletions(-) create mode 100644 src/SixLabors.Fonts/ShapingScratch.cs diff --git a/src/SixLabors.Fonts/GlyphPositioningCollection.cs b/src/SixLabors.Fonts/GlyphPositioningCollection.cs index 9ecd4ceb8..0a3db1b4b 100644 --- a/src/SixLabors.Fonts/GlyphPositioningCollection.cs +++ b/src/SixLabors.Fonts/GlyphPositioningCollection.cs @@ -48,6 +48,27 @@ public override GlyphShapingData this[int index] /// The positioning data. public GlyphPositioningData GetPositioningData(int index) => this.glyphs[index]; + /// + /// Resets the collection for reuse by a new shaping pass, returning its glyph data + /// instances to the pool. This collection owns the pass's final instances (the + /// substitution collection transfers ownership during metrics population), so this + /// is the single point instances are pooled, keeping each instance pooled at most + /// once. + /// + /// The text options for the new pass. + /// The pool receiving the retired instances. + internal void ResetForReuse(TextOptions textOptions, List pool) + { + List glyphs = this.glyphs; + for (int i = 0; i < glyphs.Count; i++) + { + pool.Add(glyphs[i].Data); + } + + glyphs.Clear(); + this.ResetCore(textOptions); + } + /// /// Gets the glyph metrics at the given codepoint offset. /// diff --git a/src/SixLabors.Fonts/GlyphShapingCollection.cs b/src/SixLabors.Fonts/GlyphShapingCollection.cs index c46d49cda..b74721bfa 100644 --- a/src/SixLabors.Fonts/GlyphShapingCollection.cs +++ b/src/SixLabors.Fonts/GlyphShapingCollection.cs @@ -45,14 +45,14 @@ protected GlyphShapingCollection(TextOptions textOptions, ShapingFeatureMap feat /// /// Gets the text options used by this collection. /// - public TextOptions TextOptions { get; } + public TextOptions TextOptions { get; private set; } /// /// Gets the candidate OpenType language system tags resolved from /// , most specific first, or an empty array when the /// culture expresses no language preference. Resolved once per shaping pass. /// - public Tag[] LanguageTags { get; } + public Tag[] LanguageTags { get; private set; } /// /// Gets the feature bit assignment shared by every collection of the shaping pass. @@ -92,6 +92,22 @@ public void SetGlyphId(int index, ushort glyphId) this[index].GlyphId = glyphId; } + /// + /// Resets the pass-wide state shared by the collections for reuse by a new shaping + /// pass: adopts the new options, re-resolves the language candidates, and empties + /// the glyph digest. Derived collections call this from their reuse reset after + /// clearing their own storage. + /// + /// The text options for the new pass. + private protected void ResetCore(TextOptions textOptions) + { + this.TextOptions = textOptions; + this.glyphDigest = default; + + CultureInfo culture = textOptions.Culture ?? CultureInfo.CurrentCulture; + this.LanguageTags = OpenTypeLanguageTagMap.TryGetTags(culture, out Tag[] tags) ? tags : []; + } + /// /// Adds the shaping feature to the collection which should be applied to the glyph at a specified index. /// diff --git a/src/SixLabors.Fonts/GlyphShapingData.cs b/src/SixLabors.Fonts/GlyphShapingData.cs index f5926a675..4e27bab89 100644 --- a/src/SixLabors.Fonts/GlyphShapingData.cs +++ b/src/SixLabors.Fonts/GlyphShapingData.cs @@ -227,6 +227,40 @@ private string DebuggerDisplay => FormattableString .Invariant($" {this.GlyphId} : {this.CodePoint.ToDebuggerDisplay()} : {CodePoint.GetScriptClass(this.CodePoint)} : {this.Direction} : {this.TextRun.TextAttributes} : {this.LigatureId} : {this.LigatureComponent} : {this.IsDecomposed}"); + /// + /// Resets the instance to its freshly constructed state for reuse by a pooled + /// shaping pass. Every mutable member must be reset here; a missed member leaks + /// state between shaping calls. + /// + /// The text run. + public void Reset(TextRun textRun) + { + this.glyphId = 0; + this.bounds = default; + this.CachedShapingClass = default; + this.ShapingClassCacheKey = -1; + this.CodePoint = default; + this.CodePointCount = 1; + this.Direction = default; + this.TextRun = textRun; + this.LigatureId = 0; + this.IsLigated = false; + this.LigatureComponent = -1; + this.MarkAttachment = -1; + this.CursiveAttachment = -1; + this.RegisteredFeatureMask = 0; + this.FeatureMask = 0; + this.AppliedFeatureMask = 0; + this.IsSubstituted = false; + this.IsDecomposed = false; + this.IsPlaceholder = false; + this.BidiRun = default; + this.IsPositioned = false; + this.IsKerned = false; + this.UniversalShapingEngineInfo = null; + this.IndicShapingEngineInfo = null; + } + /// /// Clears the registered and enabled feature masks while preserving the applied /// mask, matching the semantics of copying with cleared features. Positioning diff --git a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs index d72e8c4ac..6688603d5 100644 --- a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs +++ b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs @@ -41,6 +41,14 @@ public GlyphSubstitutionCollection(TextOptions textOptions, ShapingFeatureMap fe /// public int LigatureId { get; set; } = 1; + /// + /// Gets or sets the pool of retired instances a + /// reusable shaping pass rents from. Null outside buffer-managed passes. The + /// positioning collection returns its instances here at reuse-reset time; this + /// collection only ever rents. + /// + internal List? ReusePool { get; set; } + /// public override GlyphShapingData this[int index] { @@ -61,6 +69,39 @@ internal GlyphShapingData GetGlyphShapingData(int index, out int offset) return pair.Data; } + /// + /// Resets the collection for reuse by a new shaping pass. Storage references are + /// dropped without pooling: any instance still held here was either transferred to + /// the positioning collection, which pools it, or is dead. + /// + /// The text options for the new pass. + internal void ResetForReuse(TextOptions textOptions) + { + this.glyphs.Clear(); + this.LigatureId = 1; + this.ResetCore(textOptions); + } + + /// + /// Rents a reset instance from the reuse pool, or allocates one when the pool is + /// absent or empty. + /// + /// The text run. + /// The . + private GlyphShapingData RentData(TextRun textRun) + { + List? pool = this.ReusePool; + if (pool is { Count: > 0 }) + { + GlyphShapingData data = pool[^1]; + pool.RemoveAt(pool.Count - 1); + data.Reset(textRun); + return data; + } + + return new(textRun); + } + /// /// Adds a clone of the glyph shaping data to the collection at the specified offset. /// @@ -83,12 +124,12 @@ public void AddGlyph(GlyphShapingData data, int offset) public void AddGlyph(ushort glyphId, CodePoint codePoint, TextDirection direction, TextRun textRun, int offset) { this.RecordGlyphId(glyphId); - this.glyphs.Add(new(offset, new(textRun) - { - CodePoint = codePoint, - Direction = direction, - GlyphId = glyphId, - })); + GlyphShapingData data = this.RentData(textRun); + data.CodePoint = codePoint; + data.Direction = direction; + data.GlyphId = glyphId; + + this.glyphs.Add(new(offset, data)); } /// @@ -99,14 +140,16 @@ public void AddGlyph(ushort glyphId, CodePoint codePoint, TextDirection directio /// The text run this placeholder belongs to. /// The zero-based index within the input codepoint collection. public void AddPlaceholder(CodePoint codePoint, BidiRun bidiRun, TextRun textRun, int offset) - => this.glyphs.Add(new(offset, new(textRun) - { - CodePoint = codePoint, - Direction = (TextDirection)bidiRun.Direction, - GlyphId = 0, - IsPlaceholder = true, - BidiRun = bidiRun, - })); + { + GlyphShapingData data = this.RentData(textRun); + data.CodePoint = codePoint; + data.Direction = (TextDirection)bidiRun.Direction; + data.GlyphId = 0; + data.IsPlaceholder = true; + data.BidiRun = bidiRun; + + this.glyphs.Add(new(offset, data)); + } /// /// Moves the specified glyph to the specified position. diff --git a/src/SixLabors.Fonts/ShapingScratch.cs b/src/SixLabors.Fonts/ShapingScratch.cs new file mode 100644 index 000000000..f0baaf1ee --- /dev/null +++ b/src/SixLabors.Fonts/ShapingScratch.cs @@ -0,0 +1,66 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Tables.AdvancedTypographic; + +namespace SixLabors.Fonts; + +/// +/// Reusable shaping pipeline state: the substitution and positioning collections, their +/// shared feature map, and the pool of retired glyph data instances. This is the port of +/// the reference engine's reusable buffer memory model (hb_buffer_t): storage grows to +/// the workload's high-water mark and is reused across calls, so steady-state shaping +/// performs no per-call allocation for pipeline state. +/// +/// +/// A scratch is exclusively owned by one shaping call at a time, enforced by +/// ownership in . Reuse is safe +/// because every public shaping result is materialized by value before the scratch is +/// returned; nothing the pipeline pools can escape a call. +/// +internal sealed class ShapingScratch +{ + /// The pass-wide feature bit assignment, reset per call. + private ShapingFeatureMap? featureMap; + + /// The reusable substitution collection. + private GlyphSubstitutionCollection? substitutions; + + /// The reusable positioning collection. + private GlyphPositioningCollection? positionings; + + /// + /// Retired instances awaiting reuse. Filled from the + /// positioning collection at reset time, drained by the substitution collection as + /// glyphs are added. + /// + private readonly List dataPool = []; + + /// + /// Gets the reusable shaping collections, reset for a new pass over the given + /// options. The positioning collection's retired glyph data instances are returned + /// to the pool before the substitution collection begins renting. + /// + /// The text options for the pass. + /// The reusable collections, sharing one feature map. + internal (GlyphSubstitutionCollection Substitutions, GlyphPositioningCollection Positionings) Prepare(TextOptions options) + { + if (this.featureMap is null) + { + this.featureMap = new(); + this.substitutions = new(options, this.featureMap) + { + ReusePool = this.dataPool, + }; + this.positionings = new(options, this.featureMap); + } + else + { + this.featureMap.Reset(); + this.positionings!.ResetForReuse(options, this.dataPool); + this.substitutions!.ResetForReuse(options); + } + + return (this.substitutions!, this.positionings!); + } +} diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapingFeatureMap.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapingFeatureMap.cs index 7c069ac50..a1254b9d0 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapingFeatureMap.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapingFeatureMap.cs @@ -63,6 +63,17 @@ internal sealed class ShapingFeatureMap /// private ulong lastMask; + /// + /// Resets the map for reuse by a new shaping pass, emptying the tag registry and + /// the single-entry memo. + /// + internal void Reset() + { + this.tags.Clear(); + this.lastTagValue = 0; + this.lastMask = 0; + } + /// /// Gets the mask bit for the given feature tag, or zero when the tag has not been /// registered. A zero result is safe at every consumption site: testing it enables diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index c4019a3f7..b43febbbf 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -121,13 +121,6 @@ public static IReadOnlyList BuildTextRuns(ReadOnlySpan text, Text /// The wrapping-independent shaping state. public static ShapedText ShapeText(ReadOnlySpan text, TextOptions options) { - // Gather the font and fallbacks. - Font[] fallbackFonts = (options.FallbackFontFamilies?.Count > 0) - ? [.. options.FallbackFontFamilies.Select(x => new Font(x, options.Font.Size, options.Font.RequestedStyle))] - : []; - - LayoutMode layoutMode = options.LayoutMode; - // One feature bit assignment for the whole pass: applied feature bits written // while substituting are read after the glyph data is copied into the // positioning collection, so both collections must agree on bit meaning. @@ -135,6 +128,33 @@ public static ShapedText ShapeText(ReadOnlySpan text, TextOptions options) GlyphSubstitutionCollection substitutions = new(options, featureMap); GlyphPositioningCollection positionings = new(options, featureMap); + return ShapeText(text, options, substitutions, positionings); + } + + /// + /// Shapes using caller-supplied shaping collections, + /// allowing a reusable buffer to supply pre-reset collections whose storage + /// survives across calls. Both collections must share one + /// and already reflect . + /// + /// The text to process. + /// The text options used while shaping. + /// The substitution collection to shape into. + /// The positioning collection to shape into. + /// The wrapping-independent shaping state. + public static ShapedText ShapeText( + ReadOnlySpan text, + TextOptions options, + GlyphSubstitutionCollection substitutions, + GlyphPositioningCollection positionings) + { + // Gather the font and fallbacks. + Font[] fallbackFonts = (options.FallbackFontFamilies?.Count > 0) + ? [.. options.FallbackFontFamilies.Select(x => new Font(x, options.Font.Size, options.Font.RequestedStyle))] + : []; + + LayoutMode layoutMode = options.LayoutMode; + var probe = ShapingProbe.Enter(); // Analyse the text for bidi directional runs. diff --git a/src/SixLabors.Fonts/TextShaper.cs b/src/SixLabors.Fonts/TextShaper.cs index 7cac4d077..019972f6d 100644 --- a/src/SixLabors.Fonts/TextShaper.cs +++ b/src/SixLabors.Fonts/TextShaper.cs @@ -27,6 +27,15 @@ namespace SixLabors.Fonts; /// public static class TextShaper { + /// + /// The pool of reusable pipeline state, the reference engine's buffer memory model. + /// A scratch is exclusively owned between and + /// , and the result list is materialized by value + /// before the scratch is returned, so nothing pooled escapes a call. Retained + /// scratch storage stays at its high-water mark. + /// + private static readonly ObjectPool ScratchPool = new(new ShapingScratchPooledObjectPolicy()); + /// public static IReadOnlyList Shape(string text, TextOptions options) { @@ -59,33 +68,56 @@ public static IReadOnlyList Shape(ReadOnlySpan text, TextOpti return []; } - GlyphPositioningCollection positionings = TextLayout.ShapeText(text, options).Positionings; - - var probe = ShapingProbe.Enter(); - List glyphs = new(positionings.Count); - for (int i = 0; i < positionings.Count; i++) + ShapingScratch scratch = ScratchPool.Get(); + try { - GlyphPositioningCollection.GlyphPositioningData data = positionings.GetPositioningData(i); - if (data.Data.IsPlaceholder) + (GlyphSubstitutionCollection substitutions, GlyphPositioningCollection positionings) = scratch.Prepare(options); + TextLayout.ShapeText(text, options, substitutions, positionings); + + var probe = ShapingProbe.Enter(); + List glyphs = new(positionings.Count); + for (int i = 0; i < positionings.Count; i++) { - // Placeholder runs reserve layout space for inline objects; they carry - // no glyph. - continue; + GlyphPositioningCollection.GlyphPositioningData data = positionings.GetPositioningData(i); + if (data.Data.IsPlaceholder) + { + // Placeholder runs reserve layout space for inline objects; they carry + // no glyph. + continue; + } + + FontGlyphMetrics metrics = data.Metrics; + glyphs.Add(new ShapedGlyph( + data.Font, + metrics.GlyphId, + data.Data.CodePoint, + data.Offset, + data.Data.CodePointCount, + data.AdvanceWidth, + data.AdvanceHeight, + metrics.Offset + data.PositionOffset)); } - FontGlyphMetrics metrics = data.Metrics; - glyphs.Add(new ShapedGlyph( - data.Font, - metrics.GlyphId, - data.Data.CodePoint, - data.Offset, - data.Data.CodePointCount, - data.AdvanceWidth, - data.AdvanceHeight, - metrics.Offset + data.PositionOffset)); + ShapingProbe.Exit(ShapingProbe.Projection, probe); + return glyphs; } + finally + { + ScratchPool.Return(scratch); + } + } + + /// + /// The pooling policy for instances: scratch state is + /// reset on acquisition by , so returned + /// instances are always accepted. + /// + private sealed class ShapingScratchPooledObjectPolicy : IPooledObjectPolicy + { + /// + public ShapingScratch Create() => new(); - ShapingProbe.Exit(ShapingProbe.Projection, probe); - return glyphs; + /// + public bool Return(ShapingScratch obj) => true; } } From 14b67d809bacc898fd7b0de5b2ceff993f740464 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 08:47:43 +1000 Subject: [PATCH 09/94] Move the shaping pipeline into TextShaper TextShaper now owns the whole shaping pipeline: font-run itemization (BuildTextRuns), bidi analysis, glyph population and font fallback (DoFontRun), bidi mirroring, and GSUB/GPOS orchestration (ShapeText), relocated verbatim from TextLayout into the TextShaper.Pipeline partial. TextLayout retains only composition, line breaking, and the layout walks. Moved members are internal on the public class, so the public API surface is unchanged. Mechanical code motion: tests 4683 passed, 0 failed; pooled-reuse and HarfBuzzSharp oracles exact; benchmark allocations byte-identical (9,208 / 9,184 B) with means inside the run's noise band. --- src/SixLabors.Fonts/Rendering/TextRenderer.cs | 2 +- src/SixLabors.Fonts/TextBlock.cs | 2 +- src/SixLabors.Fonts/TextLayout.cs | 496 ----------------- src/SixLabors.Fonts/TextShaper.Pipeline.cs | 512 ++++++++++++++++++ src/SixLabors.Fonts/TextShaper.cs | 4 +- .../SixLabors.Fonts.Tests/FontWeightTests.cs | 6 +- .../Issues/Issues_412.cs | 2 +- .../SixLabors.Fonts.Tests/TextLayoutTests.cs | 8 +- 8 files changed, 524 insertions(+), 508 deletions(-) create mode 100644 src/SixLabors.Fonts/TextShaper.Pipeline.cs diff --git a/src/SixLabors.Fonts/Rendering/TextRenderer.cs b/src/SixLabors.Fonts/Rendering/TextRenderer.cs index a217c6f3a..e9695846a 100644 --- a/src/SixLabors.Fonts/Rendering/TextRenderer.cs +++ b/src/SixLabors.Fonts/Rendering/TextRenderer.cs @@ -83,7 +83,7 @@ public void Render(ReadOnlySpan text, TextOptions options) return; } - ShapedText shaped = TextLayout.ShapeText(text, options); + ShapedText shaped = TextShaper.ShapeText(text, options); LogicalTextLine logicalLine = TextLayout.ComposeLogicalLine(shaped, text, options); this.RenderText(logicalLine, options); } diff --git a/src/SixLabors.Fonts/TextBlock.cs b/src/SixLabors.Fonts/TextBlock.cs index 15a48f3b0..b1c5a2a26 100644 --- a/src/SixLabors.Fonts/TextBlock.cs +++ b/src/SixLabors.Fonts/TextBlock.cs @@ -58,7 +58,7 @@ public TextBlock(ReadOnlySpan text, TextOptions options) return; } - ShapedText shaped = TextLayout.ShapeText(text, options); + ShapedText shaped = TextShaper.ShapeText(text, options); this.LogicalLine = TextLayout.ComposeLogicalLine(shaped, text, options); } diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index b43febbbf..d1110d13e 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -22,314 +22,6 @@ internal static partial class TextLayout /// private static readonly Tag IdeographicBaselineTag = Tag.Parse("ideo"); - /// - /// Resolves the ordered sequence of instances that cover . - /// - /// - /// If is or empty, a single run covering the entire - /// grapheme range of using is returned. Otherwise the - /// supplied runs are ordered, gaps are filled with default-font runs, and overlapping ranges are trimmed. - /// - /// The text to partition into runs. - /// The text options supplying the default font and optional user-defined runs. - /// The resolved runs that together cover the entire grapheme range of . - public static IReadOnlyList BuildTextRuns(ReadOnlySpan text, TextOptions options) - { - int start = 0; - int end = text.GetGraphemeCount(); - if (end == 0) - { - return []; - } - - if (options.TextRuns is null || options.TextRuns.Count == 0) - { - TextRun textRun = new() - { - Start = 0, - End = text.GetGraphemeCount(), - Font = options.Font - }; - - textRun.ResolveFontWeight(options.FontWeight); - return [textRun]; - } - - List textRuns = []; - foreach (TextRun textRun in options.TextRuns.OrderBy(x => x.Start)) - { - // Fill gaps within runs. - if (textRun.Start > start) - { - textRuns.Add(new() - { - Start = start, - End = textRun.Start, - Font = options.Font - }); - } - - // Add the current run, ensuring the font is not null. - textRun.Font ??= options.Font; - - if (textRun.Placeholder.HasValue && textRun.End != textRun.Start) - { - throw new ArgumentException("Placeholder text runs must be zero-length insertion runs.", nameof(options)); - } - - // Ensure that the previous run does not overlap the current. - if (textRuns.Count > 0) - { - int prevIndex = textRuns.Count - 1; - TextRun previous = textRuns[prevIndex]; - previous.End = Math.Min(previous.End, textRun.Start); - } - - textRuns.Add(textRun); - start = textRun.End; - } - - // Add a final run if required. - if (start < end) - { - textRuns.Add(new() - { - Start = start, - End = end, - Font = options.Font - }); - } - - foreach (TextRun textRun in textRuns) - { - textRun.ResolveFontWeight(options.FontWeight); - } - - return textRuns; - } - - /// - /// Shapes into shaping state that is independent of the wrapping length. - /// - /// - /// Performs the font-run build, bidi analysis, GSUB/GPOS shaping (including fallback font - /// resolution for unmapped codepoints). The result contains the positioned glyph collection - /// and bidi state used by logical line composition. - /// - /// The text to process. - /// The text options used while shaping. - /// The wrapping-independent shaping state. - public static ShapedText ShapeText(ReadOnlySpan text, TextOptions options) - { - // One feature bit assignment for the whole pass: applied feature bits written - // while substituting are read after the glyph data is copied into the - // positioning collection, so both collections must agree on bit meaning. - ShapingFeatureMap featureMap = new(); - GlyphSubstitutionCollection substitutions = new(options, featureMap); - GlyphPositioningCollection positionings = new(options, featureMap); - - return ShapeText(text, options, substitutions, positionings); - } - - /// - /// Shapes using caller-supplied shaping collections, - /// allowing a reusable buffer to supply pre-reset collections whose storage - /// survives across calls. Both collections must share one - /// and already reflect . - /// - /// The text to process. - /// The text options used while shaping. - /// The substitution collection to shape into. - /// The positioning collection to shape into. - /// The wrapping-independent shaping state. - public static ShapedText ShapeText( - ReadOnlySpan text, - TextOptions options, - GlyphSubstitutionCollection substitutions, - GlyphPositioningCollection positionings) - { - // Gather the font and fallbacks. - Font[] fallbackFonts = (options.FallbackFontFamilies?.Count > 0) - ? [.. options.FallbackFontFamilies.Select(x => new Font(x, options.Font.Size, options.Font.RequestedStyle))] - : []; - - LayoutMode layoutMode = options.LayoutMode; - - var probe = ShapingProbe.Enter(); - - // Analyse the text for bidi directional runs. - BidiAlgorithm bidi = BidiAlgorithm.Instance.Value!; - BidiData bidiData = new(); - bidiData.Init(text, (sbyte)options.TextDirection); - - if (options.TextBidiMode == TextBidiMode.Override) - { - BidiCharacterType overrideType = options.TextDirection == TextDirection.Auto - ? (bidi.ResolveEmbeddingLevel(bidiData.Types) == 1 ? BidiCharacterType.RightToLeft : BidiCharacterType.LeftToRight) - : (options.TextDirection == TextDirection.RightToLeft ? BidiCharacterType.RightToLeft : BidiCharacterType.LeftToRight); - - for (int i = 0; i < bidiData.Types.Length; i++) - { - // Bidi override is a higher-level protocol override: real text behaves as the requested - // strong direction, while separators and explicit bidi controls keep their structural role. - bidiData.Types[i] = bidiData.Types[i] switch - { - BidiCharacterType.ParagraphSeparator - or BidiCharacterType.SegmentSeparator - or BidiCharacterType.BoundaryNeutral - or BidiCharacterType.LeftToRightEmbedding - or BidiCharacterType.RightToLeftEmbedding - or BidiCharacterType.LeftToRightOverride - or BidiCharacterType.RightToLeftOverride - or BidiCharacterType.PopDirectionalFormat - or BidiCharacterType.LeftToRightIsolate - or BidiCharacterType.RightToLeftIsolate - or BidiCharacterType.FirstStrongIsolate - or BidiCharacterType.PopDirectionalIsolate => bidiData.Types[i], - _ => overrideType, - }; - } - } - - // Purely left-to-right text resolves to a single even-level run without running - // the bidirectional algorithm: with no right-to-left or directional codepoints and - // a left-to-right (or auto) paragraph direction, every resolved level is zero. - // This is the overwhelmingly common case for Latin text and skips the full UAX#9 - // pass. An overridden or right-to-left paragraph always resolves levels. - BidiRun[] bidiRuns; - if (options.TextDirection != TextDirection.RightToLeft - && options.TextBidiMode != TextBidiMode.Override - && bidiData.IsUniformLeftToRight) - { - bidiRuns = [new BidiRun(BidiCharacterType.LeftToRight, 0, 0, bidiData.Types.Length)]; - } - else - { - bidi.Process(bidiData); - bidiRuns = [.. BidiRun.CoalesceLevels(bidi.ResolvedLevels)]; - } - - int[] bidiMap = new int[bidiData.Types.Length]; - Array.Fill(bidiMap, -1); - ShapingProbe.Exit(ShapingProbe.Bidi, probe); - - probe = ShapingProbe.Enter(); - - // Incrementally build out collection of glyphs. - IReadOnlyList textRuns = BuildTextRuns(text, options); - ShapingProbe.Exit(ShapingProbe.BuildTextRuns, probe); - - // First do multiple font runs using the individual text runs. - bool complete = true; - int textRunIndex = 0; - int codePointIndex = 0; - int bidiRunIndex = 0; - foreach (TextRun textRun in textRuns) - { - if (textRun.Placeholder.HasValue) - { - substitutions.Clear(); - - while (bidiRunIndex < bidiRuns.Length && codePointIndex == bidiRuns[bidiRunIndex].End) - { - bidiRunIndex++; - } - - // Placeholder direction comes from the bidi region at the insertion - // point. If the insertion point is after all source text, use the - // default even/LTR embedding level. - BidiRun placeholderBidiRun = bidiRunIndex < bidiRuns.Length - ? bidiRuns[bidiRunIndex] - : new(BidiCharacterType.LeftToRight, 2, codePointIndex, 0); - - // Placeholder runs are inserted into the layout stream and do not consume - // source graphemes, source codepoints, or bidi runs. - substitutions.AddPlaceholder( - CodePoint.ObjectReplacementChar, - placeholderBidiRun, - textRun, - codePointIndex); - - complete &= positionings.TryAdd(textRun.ResolvedFont, substitutions); - textRunIndex++; - continue; - } - - if (!DoFontRun( - textRun.Slice(text), - textRun.Start, - textRuns, - ref textRunIndex, - ref codePointIndex, - ref bidiRunIndex, - false, - textRun.ResolvedFont, - bidiRuns, - bidiMap, - substitutions, - positionings)) - { - complete = false; - } - } - - if (!complete) - { - // Finally try our fallback fonts. - // We do a complete run here across the whole collection. - foreach (Font font in fallbackFonts) - { - textRunIndex = 0; - codePointIndex = 0; - bidiRunIndex = 0; - if (DoFontRun( - text, - 0, - textRuns, - ref textRunIndex, - ref codePointIndex, - ref bidiRunIndex, - true, - font, - bidiRuns, - bidiMap, - substitutions, - positionings)) - { - break; - } - } - } - - // Update the positions of the glyphs in the completed collection. - // Each set of metrics is associated with single font and will only be updated - // by that font so it's safe to use a single collection. - probe = ShapingProbe.Enter(); - Font? lastFont = null; - for (int i = 0; i < textRuns.Count; i++) - { - TextRun textRun = textRuns[i]; - - Font font = textRun.ResolvedFont; - if (font == lastFont) - { - continue; - } - - font.FontMetrics.UpdatePositions(positionings); - lastFont = font; - } - - foreach (Font font in fallbackFonts) - { - font.FontMetrics.UpdatePositions(positionings); - } - - ShapingProbe.Exit(ShapingProbe.Positioning, probe); - - return new ShapedText(positionings, bidiRuns, bidiMap, layoutMode); - } - /// /// Lays out the supplied , streaming each laid-out glyph through the /// supplied in layout order using the supplied wrapping length for alignment. @@ -1614,194 +1306,6 @@ private static void LayoutLineVerticalMixed( penLocation.X += xLineAdvance; } - /// - /// Shapes a single font run โ€” maps codepoints in to glyph ids using - /// , then runs GSUB substitution and GPOS positioning. Codepoints that - /// the font cannot map are recorded for a later fallback pass. - /// - /// The run-relative text slice to shape. - /// The starting grapheme index (absolute within the original input). - /// The ordered list of resolved text runs. - /// The index of the current text run; advanced as the enumerator crosses run boundaries. - /// The running codepoint index (absolute within the original input). - /// The running bidi run index. - /// - /// if this call is the fallback-font pass (in which case unmapped codepoints - /// may still emit .notdef glyphs). - /// - /// The font to shape with. - /// The resolved bidi runs covering the whole input. - /// A codepoint โ†’ bidi-run mapping accumulated across shaping passes. - /// The GSUB substitution collection to write into. - /// The GPOS positioning collection to write into. - /// - /// if every codepoint mapped successfully; if any - /// codepoint remains unmapped (so a fallback-font pass is needed). - /// - private static bool DoFontRun( - ReadOnlySpan text, - int start, - IReadOnlyList textRuns, - ref int textRunIndex, - ref int codePointIndex, - ref int bidiRunIndex, - bool isFallbackRun, - Font font, - BidiRun[] bidiRuns, - int[] bidiMap, - GlyphSubstitutionCollection substitutions, - GlyphPositioningCollection positionings) - { - // For each run we start with a fresh substitution collection to avoid - // overwriting the glyph ids. - substitutions.Clear(); - - var probe = ShapingProbe.Enter(); - - // Enumerate through each grapheme in the text. - int graphemeIndex = start; - SpanGraphemeEnumerator graphemeEnumerator = new(text); - while (graphemeEnumerator.MoveNext()) - { - ReadOnlySpan grapheme = graphemeEnumerator.Current.Span; - int graphemeMax = grapheme.Length - 1; - int graphemeCodePointIndex = 0; - int charIndex = 0; - - while (textRunIndex < textRuns.Count - 1 && graphemeIndex == textRuns[textRunIndex].End) - { - textRunIndex++; - } - - // Now enumerate through each codepoint in the grapheme. - bool skipNextCodePoint = false; - SpanCodePointEnumerator codePointEnumerator = new(grapheme); - while (codePointEnumerator.MoveNext()) - { - if (codePointIndex == bidiRuns[bidiRunIndex].End) - { - bidiRunIndex++; - } - - if (skipNextCodePoint) - { - codePointIndex++; - graphemeCodePointIndex++; - continue; - } - - bidiMap[codePointIndex] = bidiRunIndex; - - int charsConsumed = 0; - CodePoint current = codePointEnumerator.Current; - charIndex += current.Utf16SequenceLength; - CodePoint? next = graphemeCodePointIndex < graphemeMax - ? CodePoint.DecodeFromUtf16At(grapheme, charIndex, out charsConsumed) - : null; - - charIndex += charsConsumed; - - // Get the glyph id for the codepoint and add to the collection. - bool hasGlyph = font.FontMetrics.TryGetGlyphId(current, next, out ushort glyphId, out skipNextCodePoint); - - // Unsupported default-ignorable code points such as FE0F should not block - // GSUB sequences like emoji ZWJ ligatures. Preserve joiners explicitly. - if (!hasGlyph && - UnicodeUtility.IsDefaultIgnorableCodePoint((uint)current.Value) && - !UnicodeUtility.ShouldRenderWhiteSpaceOnly(current) && - !CodePoint.IsZeroWidthJoiner(current) && - !CodePoint.IsZeroWidthNonJoiner(current)) - { - codePointIndex++; - graphemeCodePointIndex++; - continue; - } - - substitutions.AddGlyph(glyphId, current, (TextDirection)bidiRuns[bidiRunIndex].Direction, textRuns[textRunIndex], codePointIndex); - - codePointIndex++; - graphemeCodePointIndex++; - } - - graphemeIndex++; - } - - ShapingProbe.Exit(ShapingProbe.Populate, probe); - - // Apply the simple and complex substitutions. - // TODO: Investigate HarfBuzz normalizer. - probe = ShapingProbe.Enter(); - SubstituteBidiMirrors(font.FontMetrics, substitutions); - ShapingProbe.Exit(ShapingProbe.Mirrors, probe); - - probe = ShapingProbe.Enter(); - font.FontMetrics.ApplySubstitution(substitutions); - ShapingProbe.Exit(ShapingProbe.Substitution, probe); - - probe = ShapingProbe.Enter(); - bool result = !isFallbackRun - ? positionings.TryAdd(font, substitutions) - : positionings.TryUpdate(font, substitutions); - ShapingProbe.Exit(ShapingProbe.MetricsAdd, probe); - return result; - } - - /// - /// Substitutes mirrored bracket glyphs (for example ( โ†” )) inside right-to-left - /// bidi runs, per Unicode Bidirectional Algorithm rule L4. Relies on the font's rtlm - /// feature when available and falls back to the Unicode mirror table otherwise. - /// - /// The font metrics used to look up mirrored glyph ids. - /// The substitution collection whose glyphs will be rewritten in place. - private static void SubstituteBidiMirrors(FontMetrics fontMetrics, GlyphSubstitutionCollection collection) - { - for (int i = 0; i < collection.Count; i++) - { - GlyphShapingData data = collection[i]; - - if (data.Direction != TextDirection.RightToLeft) - { - continue; - } - - if (!CodePoint.TryGetBidiMirror(data.CodePoint, out CodePoint mirror)) - { - continue; - } - - if (fontMetrics.TryGetGlyphId(mirror, out ushort glyphId)) - { - collection.Replace(i, glyphId, KnownFeatureTags.RightToLeftMirroredForms); - } - } - - // TODO: This only replaces certain glyphs. We should investigate the specification further. - // https://www.unicode.org/reports/tr50/#vertical_alternates - if (collection.TextOptions.LayoutMode.IsHorizontal()) - { - return; - } - - for (int i = 0; i < collection.Count; i++) - { - GlyphShapingData data = collection[i]; - if (CodePoint.GetVerticalOrientationType(data.CodePoint) is VerticalOrientationType.Upright or VerticalOrientationType.TransformUpright) - { - continue; - } - - if (!CodePoint.TryGetVerticalMirror(data.CodePoint, out CodePoint mirror)) - { - continue; - } - - if (fontMetrics.TryGetGlyphId(mirror, out ushort glyphId)) - { - collection.Replace(i, glyphId, KnownFeatureTags.VerticalAlternates); - } - } - } - /// /// Calculates the X offset to apply to a single line of horizontal text so that it is positioned /// within the wrapping block according to the requested horizontal and text alignment. diff --git a/src/SixLabors.Fonts/TextShaper.Pipeline.cs b/src/SixLabors.Fonts/TextShaper.Pipeline.cs new file mode 100644 index 000000000..df2fc1b35 --- /dev/null +++ b/src/SixLabors.Fonts/TextShaper.Pipeline.cs @@ -0,0 +1,512 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Tables.AdvancedTypographic; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts; + +/// +/// The shaping pipeline: font-run itemization, bidi analysis, glyph population, GSUB +/// substitution, and GPOS positioning. and the public shaping +/// API both consume this single pipeline; layout composes and positions lines from its +/// output but performs no shaping of its own. +/// +public static partial class TextShaper +{ + /// + /// Resolves the ordered sequence of instances that cover . + /// + /// + /// If is or empty, a single run covering the entire + /// grapheme range of using is returned. Otherwise the + /// supplied runs are ordered, gaps are filled with default-font runs, and overlapping ranges are trimmed. + /// + /// The text to partition into runs. + /// The text options supplying the default font and optional user-defined runs. + /// The resolved runs that together cover the entire grapheme range of . + internal static IReadOnlyList BuildTextRuns(ReadOnlySpan text, TextOptions options) + { + int start = 0; + int end = text.GetGraphemeCount(); + if (end == 0) + { + return []; + } + + if (options.TextRuns is null || options.TextRuns.Count == 0) + { + TextRun textRun = new() + { + Start = 0, + End = text.GetGraphemeCount(), + Font = options.Font + }; + + textRun.ResolveFontWeight(options.FontWeight); + return [textRun]; + } + + List textRuns = []; + foreach (TextRun textRun in options.TextRuns.OrderBy(x => x.Start)) + { + // Fill gaps within runs. + if (textRun.Start > start) + { + textRuns.Add(new() + { + Start = start, + End = textRun.Start, + Font = options.Font + }); + } + + // Add the current run, ensuring the font is not null. + textRun.Font ??= options.Font; + + if (textRun.Placeholder.HasValue && textRun.End != textRun.Start) + { + throw new ArgumentException("Placeholder text runs must be zero-length insertion runs.", nameof(options)); + } + + // Ensure that the previous run does not overlap the current. + if (textRuns.Count > 0) + { + int prevIndex = textRuns.Count - 1; + TextRun previous = textRuns[prevIndex]; + previous.End = Math.Min(previous.End, textRun.Start); + } + + textRuns.Add(textRun); + start = textRun.End; + } + + // Add a final run if required. + if (start < end) + { + textRuns.Add(new() + { + Start = start, + End = end, + Font = options.Font + }); + } + + foreach (TextRun textRun in textRuns) + { + textRun.ResolveFontWeight(options.FontWeight); + } + + return textRuns; + } + + /// + /// Shapes into shaping state that is independent of the wrapping length. + /// + /// + /// Performs the font-run build, bidi analysis, GSUB/GPOS shaping (including fallback font + /// resolution for unmapped codepoints). The result contains the positioned glyph collection + /// and bidi state used by logical line composition. + /// + /// The text to process. + /// The text options used while shaping. + /// The wrapping-independent shaping state. + internal static ShapedText ShapeText(ReadOnlySpan text, TextOptions options) + { + // One feature bit assignment for the whole pass: applied feature bits written + // while substituting are read after the glyph data is copied into the + // positioning collection, so both collections must agree on bit meaning. + ShapingFeatureMap featureMap = new(); + GlyphSubstitutionCollection substitutions = new(options, featureMap); + GlyphPositioningCollection positionings = new(options, featureMap); + + return ShapeText(text, options, substitutions, positionings); + } + + /// + /// Shapes using caller-supplied shaping collections, + /// allowing a reusable buffer to supply pre-reset collections whose storage + /// survives across calls. Both collections must share one + /// and already reflect . + /// + /// The text to process. + /// The text options used while shaping. + /// The substitution collection to shape into. + /// The positioning collection to shape into. + /// The wrapping-independent shaping state. + internal static ShapedText ShapeText( + ReadOnlySpan text, + TextOptions options, + GlyphSubstitutionCollection substitutions, + GlyphPositioningCollection positionings) + { + // Gather the font and fallbacks. + Font[] fallbackFonts = (options.FallbackFontFamilies?.Count > 0) + ? [.. options.FallbackFontFamilies.Select(x => new Font(x, options.Font.Size, options.Font.RequestedStyle))] + : []; + + LayoutMode layoutMode = options.LayoutMode; + + var probe = ShapingProbe.Enter(); + + // Analyse the text for bidi directional runs. + BidiAlgorithm bidi = BidiAlgorithm.Instance.Value!; + BidiData bidiData = new(); + bidiData.Init(text, (sbyte)options.TextDirection); + + if (options.TextBidiMode == TextBidiMode.Override) + { + BidiCharacterType overrideType = options.TextDirection == TextDirection.Auto + ? (bidi.ResolveEmbeddingLevel(bidiData.Types) == 1 ? BidiCharacterType.RightToLeft : BidiCharacterType.LeftToRight) + : (options.TextDirection == TextDirection.RightToLeft ? BidiCharacterType.RightToLeft : BidiCharacterType.LeftToRight); + + for (int i = 0; i < bidiData.Types.Length; i++) + { + // Bidi override is a higher-level protocol override: real text behaves as the requested + // strong direction, while separators and explicit bidi controls keep their structural role. + bidiData.Types[i] = bidiData.Types[i] switch + { + BidiCharacterType.ParagraphSeparator + or BidiCharacterType.SegmentSeparator + or BidiCharacterType.BoundaryNeutral + or BidiCharacterType.LeftToRightEmbedding + or BidiCharacterType.RightToLeftEmbedding + or BidiCharacterType.LeftToRightOverride + or BidiCharacterType.RightToLeftOverride + or BidiCharacterType.PopDirectionalFormat + or BidiCharacterType.LeftToRightIsolate + or BidiCharacterType.RightToLeftIsolate + or BidiCharacterType.FirstStrongIsolate + or BidiCharacterType.PopDirectionalIsolate => bidiData.Types[i], + _ => overrideType, + }; + } + } + + // Purely left-to-right text resolves to a single even-level run without running + // the bidirectional algorithm: with no right-to-left or directional codepoints and + // a left-to-right (or auto) paragraph direction, every resolved level is zero. + // This is the overwhelmingly common case for Latin text and skips the full UAX#9 + // pass. An overridden or right-to-left paragraph always resolves levels. + BidiRun[] bidiRuns; + if (options.TextDirection != TextDirection.RightToLeft + && options.TextBidiMode != TextBidiMode.Override + && bidiData.IsUniformLeftToRight) + { + bidiRuns = [new BidiRun(BidiCharacterType.LeftToRight, 0, 0, bidiData.Types.Length)]; + } + else + { + bidi.Process(bidiData); + bidiRuns = [.. BidiRun.CoalesceLevels(bidi.ResolvedLevels)]; + } + + int[] bidiMap = new int[bidiData.Types.Length]; + Array.Fill(bidiMap, -1); + ShapingProbe.Exit(ShapingProbe.Bidi, probe); + + probe = ShapingProbe.Enter(); + + // Incrementally build out collection of glyphs. + IReadOnlyList textRuns = BuildTextRuns(text, options); + ShapingProbe.Exit(ShapingProbe.BuildTextRuns, probe); + + // First do multiple font runs using the individual text runs. + bool complete = true; + int textRunIndex = 0; + int codePointIndex = 0; + int bidiRunIndex = 0; + foreach (TextRun textRun in textRuns) + { + if (textRun.Placeholder.HasValue) + { + substitutions.Clear(); + + while (bidiRunIndex < bidiRuns.Length && codePointIndex == bidiRuns[bidiRunIndex].End) + { + bidiRunIndex++; + } + + // Placeholder direction comes from the bidi region at the insertion + // point. If the insertion point is after all source text, use the + // default even/LTR embedding level. + BidiRun placeholderBidiRun = bidiRunIndex < bidiRuns.Length + ? bidiRuns[bidiRunIndex] + : new(BidiCharacterType.LeftToRight, 2, codePointIndex, 0); + + // Placeholder runs are inserted into the layout stream and do not consume + // source graphemes, source codepoints, or bidi runs. + substitutions.AddPlaceholder( + CodePoint.ObjectReplacementChar, + placeholderBidiRun, + textRun, + codePointIndex); + + complete &= positionings.TryAdd(textRun.ResolvedFont, substitutions); + textRunIndex++; + continue; + } + + if (!DoFontRun( + textRun.Slice(text), + textRun.Start, + textRuns, + ref textRunIndex, + ref codePointIndex, + ref bidiRunIndex, + false, + textRun.ResolvedFont, + bidiRuns, + bidiMap, + substitutions, + positionings)) + { + complete = false; + } + } + + if (!complete) + { + // Finally try our fallback fonts. + // We do a complete run here across the whole collection. + foreach (Font font in fallbackFonts) + { + textRunIndex = 0; + codePointIndex = 0; + bidiRunIndex = 0; + if (DoFontRun( + text, + 0, + textRuns, + ref textRunIndex, + ref codePointIndex, + ref bidiRunIndex, + true, + font, + bidiRuns, + bidiMap, + substitutions, + positionings)) + { + break; + } + } + } + + // Update the positions of the glyphs in the completed collection. + // Each set of metrics is associated with single font and will only be updated + // by that font so it's safe to use a single collection. + probe = ShapingProbe.Enter(); + Font? lastFont = null; + for (int i = 0; i < textRuns.Count; i++) + { + TextRun textRun = textRuns[i]; + + Font font = textRun.ResolvedFont; + if (font == lastFont) + { + continue; + } + + font.FontMetrics.UpdatePositions(positionings); + lastFont = font; + } + + foreach (Font font in fallbackFonts) + { + font.FontMetrics.UpdatePositions(positionings); + } + + ShapingProbe.Exit(ShapingProbe.Positioning, probe); + + return new ShapedText(positionings, bidiRuns, bidiMap, layoutMode); + } + + /// + /// Shapes a single font run โ€” maps codepoints in to glyph ids using + /// , then runs GSUB substitution and GPOS positioning. Codepoints that + /// the font cannot map are recorded for a later fallback pass. + /// + /// The run-relative text slice to shape. + /// The starting grapheme index (absolute within the original input). + /// The ordered list of resolved text runs. + /// The index of the current text run; advanced as the enumerator crosses run boundaries. + /// The running codepoint index (absolute within the original input). + /// The running bidi run index. + /// + /// if this call is the fallback-font pass (in which case unmapped codepoints + /// may still emit .notdef glyphs). + /// + /// The font to shape with. + /// The resolved bidi runs covering the whole input. + /// A codepoint โ†’ bidi-run mapping accumulated across shaping passes. + /// The GSUB substitution collection to write into. + /// The GPOS positioning collection to write into. + /// + /// if every codepoint mapped successfully; if any + /// codepoint remains unmapped (so a fallback-font pass is needed). + /// + private static bool DoFontRun( + ReadOnlySpan text, + int start, + IReadOnlyList textRuns, + ref int textRunIndex, + ref int codePointIndex, + ref int bidiRunIndex, + bool isFallbackRun, + Font font, + BidiRun[] bidiRuns, + int[] bidiMap, + GlyphSubstitutionCollection substitutions, + GlyphPositioningCollection positionings) + { + // For each run we start with a fresh substitution collection to avoid + // overwriting the glyph ids. + substitutions.Clear(); + + var probe = ShapingProbe.Enter(); + + // Enumerate through each grapheme in the text. + int graphemeIndex = start; + SpanGraphemeEnumerator graphemeEnumerator = new(text); + while (graphemeEnumerator.MoveNext()) + { + ReadOnlySpan grapheme = graphemeEnumerator.Current.Span; + int graphemeMax = grapheme.Length - 1; + int graphemeCodePointIndex = 0; + int charIndex = 0; + + while (textRunIndex < textRuns.Count - 1 && graphemeIndex == textRuns[textRunIndex].End) + { + textRunIndex++; + } + + // Now enumerate through each codepoint in the grapheme. + bool skipNextCodePoint = false; + SpanCodePointEnumerator codePointEnumerator = new(grapheme); + while (codePointEnumerator.MoveNext()) + { + if (codePointIndex == bidiRuns[bidiRunIndex].End) + { + bidiRunIndex++; + } + + if (skipNextCodePoint) + { + codePointIndex++; + graphemeCodePointIndex++; + continue; + } + + bidiMap[codePointIndex] = bidiRunIndex; + + int charsConsumed = 0; + CodePoint current = codePointEnumerator.Current; + charIndex += current.Utf16SequenceLength; + CodePoint? next = graphemeCodePointIndex < graphemeMax + ? CodePoint.DecodeFromUtf16At(grapheme, charIndex, out charsConsumed) + : null; + + charIndex += charsConsumed; + + // Get the glyph id for the codepoint and add to the collection. + bool hasGlyph = font.FontMetrics.TryGetGlyphId(current, next, out ushort glyphId, out skipNextCodePoint); + + // Unsupported default-ignorable code points such as FE0F should not block + // GSUB sequences like emoji ZWJ ligatures. Preserve joiners explicitly. + if (!hasGlyph && + UnicodeUtility.IsDefaultIgnorableCodePoint((uint)current.Value) && + !UnicodeUtility.ShouldRenderWhiteSpaceOnly(current) && + !CodePoint.IsZeroWidthJoiner(current) && + !CodePoint.IsZeroWidthNonJoiner(current)) + { + codePointIndex++; + graphemeCodePointIndex++; + continue; + } + + substitutions.AddGlyph(glyphId, current, (TextDirection)bidiRuns[bidiRunIndex].Direction, textRuns[textRunIndex], codePointIndex); + + codePointIndex++; + graphemeCodePointIndex++; + } + + graphemeIndex++; + } + + ShapingProbe.Exit(ShapingProbe.Populate, probe); + + // Apply the simple and complex substitutions. + // TODO: Investigate HarfBuzz normalizer. + probe = ShapingProbe.Enter(); + SubstituteBidiMirrors(font.FontMetrics, substitutions); + ShapingProbe.Exit(ShapingProbe.Mirrors, probe); + + probe = ShapingProbe.Enter(); + font.FontMetrics.ApplySubstitution(substitutions); + ShapingProbe.Exit(ShapingProbe.Substitution, probe); + + probe = ShapingProbe.Enter(); + bool result = !isFallbackRun + ? positionings.TryAdd(font, substitutions) + : positionings.TryUpdate(font, substitutions); + ShapingProbe.Exit(ShapingProbe.MetricsAdd, probe); + return result; + } + + /// + /// Substitutes mirrored bracket glyphs (for example ( โ†” )) inside right-to-left + /// bidi runs, per Unicode Bidirectional Algorithm rule L4. Relies on the font's rtlm + /// feature when available and falls back to the Unicode mirror table otherwise. + /// + /// The font metrics used to look up mirrored glyph ids. + /// The substitution collection whose glyphs will be rewritten in place. + private static void SubstituteBidiMirrors(FontMetrics fontMetrics, GlyphSubstitutionCollection collection) + { + for (int i = 0; i < collection.Count; i++) + { + GlyphShapingData data = collection[i]; + + if (data.Direction != TextDirection.RightToLeft) + { + continue; + } + + if (!CodePoint.TryGetBidiMirror(data.CodePoint, out CodePoint mirror)) + { + continue; + } + + if (fontMetrics.TryGetGlyphId(mirror, out ushort glyphId)) + { + collection.Replace(i, glyphId, KnownFeatureTags.RightToLeftMirroredForms); + } + } + + // TODO: This only replaces certain glyphs. We should investigate the specification further. + // https://www.unicode.org/reports/tr50/#vertical_alternates + if (collection.TextOptions.LayoutMode.IsHorizontal()) + { + return; + } + + for (int i = 0; i < collection.Count; i++) + { + GlyphShapingData data = collection[i]; + if (CodePoint.GetVerticalOrientationType(data.CodePoint) is VerticalOrientationType.Upright or VerticalOrientationType.TransformUpright) + { + continue; + } + + if (!CodePoint.TryGetVerticalMirror(data.CodePoint, out CodePoint mirror)) + { + continue; + } + + if (fontMetrics.TryGetGlyphId(mirror, out ushort glyphId)) + { + collection.Replace(i, glyphId, KnownFeatureTags.VerticalAlternates); + } + } + } +} diff --git a/src/SixLabors.Fonts/TextShaper.cs b/src/SixLabors.Fonts/TextShaper.cs index 019972f6d..1749f617a 100644 --- a/src/SixLabors.Fonts/TextShaper.cs +++ b/src/SixLabors.Fonts/TextShaper.cs @@ -25,7 +25,7 @@ namespace SixLabors.Fonts; /// falls inside the slice. /// /// -public static class TextShaper +public static partial class TextShaper { /// /// The pool of reusable pipeline state, the reference engine's buffer memory model. @@ -72,7 +72,7 @@ public static IReadOnlyList Shape(ReadOnlySpan text, TextOpti try { (GlyphSubstitutionCollection substitutions, GlyphPositioningCollection positionings) = scratch.Prepare(options); - TextLayout.ShapeText(text, options, substitutions, positionings); + ShapeText(text, options, substitutions, positionings); var probe = ShapingProbe.Enter(); List glyphs = new(positionings.Count); diff --git a/tests/SixLabors.Fonts.Tests/FontWeightTests.cs b/tests/SixLabors.Fonts.Tests/FontWeightTests.cs index 7bccf72fd..31fcfda77 100644 --- a/tests/SixLabors.Fonts.Tests/FontWeightTests.cs +++ b/tests/SixLabors.Fonts.Tests/FontWeightTests.cs @@ -54,7 +54,7 @@ public void TextRunWeight_OverridesTextOptionsWeight() TextRuns = [textRun] }; - TextRun resolvedRun = Assert.Single(TextLayout.BuildTextRuns("A", options)); + TextRun resolvedRun = Assert.Single(TextShaper.BuildTextRuns("A", options)); FontVariation weight = Assert.Single( resolvedRun.ResolvedFont.Variations.ToArray(), variation => variation.Tag == KnownVariationAxes.Weight); @@ -165,7 +165,7 @@ public void SystemWeight_UsesInstalledFaceWhenAvailable(FontWeight requestedWeig Font font = family.CreateFont(18); TextOptions options = new(font) { FontWeight = requestedWeight }; - TextRun textRun = Assert.Single(TextLayout.BuildTextRuns("Weight", options)); + TextRun textRun = Assert.Single(TextShaper.BuildTextRuns("Weight", options)); // DirectWrite resolves both 500 and 600 to Segoe UI Semibold. This specifically protects // the equal-distance 500 request from incorrectly selecting the lighter Regular 400 face. @@ -202,7 +202,7 @@ public void SystemWeight_UsesBrowserFaceMatching(FontWeight requestedWeight, Fon Font font = family.CreateFont(18); TextOptions options = new(font) { FontWeight = requestedWeight }; - TextRun textRun = Assert.Single(TextLayout.BuildTextRuns("Weight", options)); + TextRun textRun = Assert.Single(TextShaper.BuildTextRuns("Weight", options)); // CSS Fonts Level 4 section 5 maps missing weights to the nearest face in a specified // search direction. For the installed Arial family, 100-500 select Regular, 600-700 diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_412.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_412.cs index 482f23d9d..4d5277034 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_412.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_412.cs @@ -18,7 +18,7 @@ public void ShouldCreateCorrectTextRunCount() } }; - IReadOnlyList runs = TextLayout.BuildTextRuns("abcde", options); + IReadOnlyList runs = TextShaper.BuildTextRuns("abcde", options); Assert.Equal(2, runs.Count); diff --git a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs index 4180d45a2..942176810 100644 --- a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs +++ b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs @@ -1006,7 +1006,7 @@ public void TextPlaceholder_SharesInsertionCodePointOffset() ] }; - ShapedText shapedText = TextLayout.ShapeText(text.AsSpan(), options); + ShapedText shapedText = TextShaper.ShapeText(text.AsSpan(), options); LogicalTextLine logicalLine = TextLayout.ComposeLogicalLine(shapedText, text.AsSpan(), options); GlyphLayoutData placeholder = default; @@ -2107,7 +2107,7 @@ public void BuildTextRuns_EmptyReturnsDefaultRun() Font font = CreateFont(text); TextOptions options = new(font); - IReadOnlyList runs = TextLayout.BuildTextRuns(text.AsSpan(), options); + IReadOnlyList runs = TextShaper.BuildTextRuns(text.AsSpan(), options); Assert.Single(runs); Assert.Equal(font, runs[0].Font); @@ -2131,7 +2131,7 @@ public void BuildTextRuns_ReturnsCreatesInterimRuns() } }; - IReadOnlyList runs = TextLayout.BuildTextRuns(text.AsSpan(), options); + IReadOnlyList runs = TextShaper.BuildTextRuns(text.AsSpan(), options); Assert.Equal(7, runs.Count); @@ -2185,7 +2185,7 @@ public void BuildTextRuns_PreventsOverlappingRun() } }; - IReadOnlyList runs = TextLayout.BuildTextRuns(text.AsSpan(), options); + IReadOnlyList runs = TextShaper.BuildTextRuns(text.AsSpan(), options); Assert.Equal(2, runs.Count); Assert.Equal(font, runs[0].Font); From 394ce1ee1ad78fe380a1b7bbea96f0ba003cc630 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 09:11:23 +1000 Subject: [PATCH 10/94] Seal pooling inside ShapeText and make the shaped result inert TextShaper.ShapeText is now the single pooling site: it rents the pipeline scratch, shapes, copies the result out by value, and returns the scratch before any caller sees the result. ShapedText becomes a run table holding run-constant state once (font, point size, text run, placeholder bidi run) plus parallel per-glyph info and position arrays of pure numbers - no metrics, collection, or pooled references survive shaping. Line composition resolves metrics instances from the owning font's cache by the same arguments shaping used; placeholder metrics are recreated from the run entry via PlaceholderGlyphMetrics.Create. TextBlock, TextRenderer, and Shape all call the static shaper API with no pooling knowledge, and the layout path shapes through the pooled pipeline for the first time. Tests: 4683 passed, 0 failed. Pooled-reuse and HarfBuzzSharp oracles exact. ShapeTextBenchmark, 12 iterations, vs previous commit (noisy run: HarfBuzz's own means moved ~+60%; ratios flat within error): | Scenario | Ratio vs HB | Allocated | |---|---:|---:| | Latin | 7.22 -> 7.15 | 9,208 -> 11,984 B | | Arabic | 3.83-4.80 band | 9,184 -> 10,952 B | The allocation increase is the inert result arrays (~24 B info + 24 B pos per glyph plus the run table) that replace escaped references on the Shape path; the layout path sheds its per-offset List allocations in exchange. The arrays are the flat storage the pipeline itself adopts next, at which point the copy-out collapses. --- .../GlyphPositioningCollection.cs | 79 +----------- .../PlaceholderGlyphMetrics.cs | 17 +++ src/SixLabors.Fonts/ShapedGlyphInfo.cs | 121 ++++++++++++++++++ src/SixLabors.Fonts/ShapedGlyphPosition.cs | 48 +++++++ src/SixLabors.Fonts/ShapedText.cs | 98 +++++++++++++- src/SixLabors.Fonts/ShapedTextRun.cs | 50 ++++++++ src/SixLabors.Fonts/ShapingScratch.cs | 3 +- .../TextLayout.LineBreaking.cs | 73 +++++++---- src/SixLabors.Fonts/TextLine.cs | 27 ++-- src/SixLabors.Fonts/TextShaper.Pipeline.cs | 120 +++++++++++++++-- src/SixLabors.Fonts/TextShaper.cs | 79 ++++-------- 11 files changed, 522 insertions(+), 193 deletions(-) create mode 100644 src/SixLabors.Fonts/ShapedGlyphInfo.cs create mode 100644 src/SixLabors.Fonts/ShapedGlyphPosition.cs create mode 100644 src/SixLabors.Fonts/ShapedTextRun.cs diff --git a/src/SixLabors.Fonts/GlyphPositioningCollection.cs b/src/SixLabors.Fonts/GlyphPositioningCollection.cs index 0a3db1b4b..b86d861c7 100644 --- a/src/SixLabors.Fonts/GlyphPositioningCollection.cs +++ b/src/SixLabors.Fonts/GlyphPositioningCollection.cs @@ -69,72 +69,6 @@ internal void ResetForReuse(TextOptions textOptions, List pool this.ResetCore(textOptions); } - /// - /// Gets the glyph metrics at the given codepoint offset. - /// - /// The zero-based index within the input codepoint collection. - /// - /// The index within the glyph list to start searching from. Updated to the position of the match - /// so that subsequent calls with increasing offsets avoid rescanning from the beginning. - /// - /// The font size in PT units of the font containing this glyph. - /// Whether the glyph is the result of a substitution. - /// Whether the glyph is the result of a vertical substitution. - /// 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, - ref int startIndex, - out float pointSize, - out bool isSubstituted, - out bool isVerticalSubstitution, - out bool isDecomposed, - [NotNullWhen(true)] out IReadOnlyList? data) - { - List match = []; - pointSize = 0; - isSubstituted = false; - isVerticalSubstitution = false; - isDecomposed = false; - - ulong verticalMask = this.GetVerticalFeatureMask(); - - for (int i = startIndex; i < this.glyphs.Count; i++) - { - if (this.glyphs[i].Offset == offset) - { - if (match.Count == 0) - { - startIndex = i; - } - - GlyphPositioningData glyph = this.glyphs[i]; - if (!glyph.Data.IsPlaceholder) - { - isSubstituted = glyph.Data.IsSubstituted; - isDecomposed = glyph.Data.IsDecomposed; - isVerticalSubstitution |= (glyph.Data.AppliedFeatureMask & verticalMask) != 0; - pointSize = glyph.PointSize; - } - - match.Add(glyph); - } - else if (match.Count > 0) - { - // Offsets, though non-sequential, are sorted, so we can stop searching. - break; - } - } - - data = match; - return match.Count > 0; - } - /// /// Updates the collection of glyph ids to the metrics collection to overwrite any glyphs that have been previously /// identified as fallbacks. @@ -261,16 +195,7 @@ public bool TryAdd(Font font, GlyphSubstitutionCollection collection) { // Placeholders are synthetic glyphs: they need layout metrics but must not // go through font glyph lookup, fallback resolution, or GPOS positioning. - StreamFontMetrics streamFontMetrics = fontMetrics is FileFontMetrics fileFontMetrics - ? fileFontMetrics.StreamFontMetrics - : (StreamFontMetrics)fontMetrics; - - FontGlyphMetrics placeholderMetrics = new PlaceholderGlyphMetrics( - streamFontMetrics, - data.TextRun.Placeholder.GetValueOrDefault(), - font.Size, - this.TextOptions.Dpi, - data.TextRun); + FontGlyphMetrics placeholderMetrics = PlaceholderGlyphMetrics.Create(font, data.TextRun, this.TextOptions.Dpi); GlyphShapingData placeholderData = data; placeholderData.ClearFeatures(); @@ -386,7 +311,7 @@ public bool ShouldProcess(FontMetrics fontMetrics, int index) /// substitution and read here after the copy into this collection. /// /// The combined mask, or zero when no vertical feature was registered. - private ulong GetVerticalFeatureMask() + internal ulong GetVerticalFeatureMask() => this.FeatureMap.GetMask(KnownFeatureTags.VerticalAlternates) | this.FeatureMap.GetMask(KnownFeatureTags.VerticalAlternatesAndRotation) | this.FeatureMap.GetMask(KnownFeatureTags.VerticalAlternatesForRotation); diff --git a/src/SixLabors.Fonts/PlaceholderGlyphMetrics.cs b/src/SixLabors.Fonts/PlaceholderGlyphMetrics.cs index 45a2062e2..113a74346 100644 --- a/src/SixLabors.Fonts/PlaceholderGlyphMetrics.cs +++ b/src/SixLabors.Fonts/PlaceholderGlyphMetrics.cs @@ -51,6 +51,23 @@ internal PlaceholderGlyphMetrics( this.dpi = dpi; } + /// + /// Creates placeholder metrics for the given font and placeholder text run. + /// + /// The font whose metrics anchor the placeholder's line metrics. + /// The placeholder text run. + /// The resolution used to convert placeholder pixels into layout units. + /// The placeholder metrics. + internal static PlaceholderGlyphMetrics Create(Font font, TextRun textRun, float dpi) + { + FontMetrics fontMetrics = font.FontMetrics; + StreamFontMetrics streamFontMetrics = fontMetrics is FileFontMetrics fileFontMetrics + ? fileFontMetrics.StreamFontMetrics + : (StreamFontMetrics)fontMetrics; + + return new(streamFontMetrics, textRun.Placeholder.GetValueOrDefault(), font.Size, dpi, textRun); + } + /// internal override void RenderTo( IGlyphRenderer renderer, diff --git a/src/SixLabors.Fonts/ShapedGlyphInfo.cs b/src/SixLabors.Fonts/ShapedGlyphInfo.cs new file mode 100644 index 000000000..0c041503c --- /dev/null +++ b/src/SixLabors.Fonts/ShapedGlyphInfo.cs @@ -0,0 +1,121 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts; + +/// +/// Flags describing how shaping produced a glyph. +/// +[Flags] +internal enum ShapedGlyphFlags : byte +{ + /// + /// No flags. + /// + None = 0, + + /// + /// The entry is an inline placeholder. + /// + Placeholder = 1, + + /// + /// The glyph is the result of a substitution. + /// + Substituted = 2, + + /// + /// The glyph is the result of a decomposition substitution. + /// + Decomposed = 4, + + /// + /// A vertical alternate feature changed the glyph. + /// + VerticalSubstituted = 8, +} + +/// +/// The identity half of a shaped glyph: which glyph, which source codepoints it +/// covers, and which run it belongs to. Positioning values live in the parallel +/// position array. +/// +internal readonly struct ShapedGlyphInfo +{ + /// + /// Initializes a new instance of the struct. + /// + /// The zero-based index within the input codepoint collection. + /// The leading codepoint the glyph represents. + /// The codepoint count represented by the glyph. + /// The glyph id. + /// The index of the owning entry in the shaped run table. + /// The shaping flags. + public ShapedGlyphInfo( + int codePointIndex, + CodePoint codePoint, + int codePointCount, + ushort glyphId, + ushort runIndex, + ShapedGlyphFlags flags) + { + this.CodePointIndex = codePointIndex; + this.CodePoint = codePoint; + this.CodePointCount = codePointCount; + this.GlyphId = glyphId; + this.RunIndex = runIndex; + this.Flags = flags; + } + + /// + /// Gets the zero-based index within the input codepoint collection. + /// + public int CodePointIndex { get; } + + /// + /// Gets the leading codepoint the glyph represents. + /// + public CodePoint CodePoint { get; } + + /// + /// Gets the codepoint count represented by the glyph. + /// + public int CodePointCount { get; } + + /// + /// Gets the glyph id. + /// + public ushort GlyphId { get; } + + /// + /// Gets the index of the owning entry in the shaped run table. + /// + public ushort RunIndex { get; } + + /// + /// Gets the shaping flags. + /// + public ShapedGlyphFlags Flags { get; } + + /// + /// Gets a value indicating whether the entry is an inline placeholder. + /// + public bool IsPlaceholder => (this.Flags & ShapedGlyphFlags.Placeholder) != 0; + + /// + /// Gets a value indicating whether the glyph is the result of a substitution. + /// + public bool IsSubstituted => (this.Flags & ShapedGlyphFlags.Substituted) != 0; + + /// + /// Gets a value indicating whether the glyph is the result of a decomposition substitution. + /// + public bool IsDecomposed => (this.Flags & ShapedGlyphFlags.Decomposed) != 0; + + /// + /// Gets a value indicating whether a vertical alternate feature changed the glyph. + /// + public bool IsVerticalSubstituted => (this.Flags & ShapedGlyphFlags.VerticalSubstituted) != 0; +} diff --git a/src/SixLabors.Fonts/ShapedGlyphPosition.cs b/src/SixLabors.Fonts/ShapedGlyphPosition.cs new file mode 100644 index 000000000..b74465d66 --- /dev/null +++ b/src/SixLabors.Fonts/ShapedGlyphPosition.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.Fonts; + +/// +/// The geometry half of a shaped glyph: pure numbers in font design units with no +/// object references. Identity lives in the parallel info array. +/// +internal readonly struct ShapedGlyphPosition +{ + /// + /// Initializes a new instance of the struct. + /// + /// The positioned horizontal advance. + /// The positioned vertical advance. + /// The placement offset written by positioning. + /// The glyph-origin bearing offset copied from the glyph metrics. + public ShapedGlyphPosition(ushort advanceWidth, ushort advanceHeight, Vector2 offset, Vector2 bearing) + { + this.AdvanceWidth = advanceWidth; + this.AdvanceHeight = advanceHeight; + this.Offset = offset; + this.Bearing = bearing; + } + + /// + /// Gets the positioned horizontal advance in font design units. + /// + public ushort AdvanceWidth { get; } + + /// + /// Gets the positioned vertical advance in font design units. + /// + public ushort AdvanceHeight { get; } + + /// + /// Gets the placement offset written by positioning, in font design units. + /// + public Vector2 Offset { get; } + + /// + /// Gets the glyph-origin bearing offset in font design units, copied out of the glyph metrics so projection needs no metrics lookup. + /// + public Vector2 Bearing { get; } +} diff --git a/src/SixLabors.Fonts/ShapedText.cs b/src/SixLabors.Fonts/ShapedText.cs index 3eb8ac028..6d3a44ca9 100644 --- a/src/SixLabors.Fonts/ShapedText.cs +++ b/src/SixLabors.Fonts/ShapedText.cs @@ -6,14 +6,20 @@ namespace SixLabors.Fonts; /// -/// Contains the width-independent result of shaping text before logical line composition. +/// Contains the width-independent result of shaping text before logical line +/// composition: a run table holding run-constant state once, and parallel per-glyph +/// info and position arrays holding identities and pure numbers. The result is +/// inert - it references no pipeline state - and consumers needing glyph metrics +/// query the owning font by glyph id. /// internal readonly struct ShapedText { /// /// Initializes a new instance of the struct. /// - /// The positioned glyph shaping collection. + /// The shaped run table. + /// The per-glyph identity records, parallel to . + /// The per-glyph geometry records, parallel to . /// The resolved bidi runs covering the shaped text. /// /// The code point index to bidi-run index mapping built during shaping. Entries for @@ -21,21 +27,36 @@ internal readonly struct ShapedText /// /// The layout mode used while shaping. public ShapedText( - GlyphPositioningCollection positionings, + ShapedTextRun[] runs, + ShapedGlyphInfo[] infos, + ShapedGlyphPosition[] positions, BidiRun[] bidiRuns, int[] bidiMap, LayoutMode layoutMode) { - this.Positionings = positionings; + this.Runs = runs; + this.Infos = infos; + this.Positions = positions; this.BidiRuns = bidiRuns; this.BidiMap = bidiMap; this.LayoutMode = layoutMode; } /// - /// Gets the positioned glyph shaping collection. + /// Gets the shaped run table: run-constant state referenced per glyph by + /// . /// - public GlyphPositioningCollection Positionings { get; } + public ShapedTextRun[] Runs { get; } + + /// + /// Gets the per-glyph identity records in logical order. + /// + public ShapedGlyphInfo[] Infos { get; } + + /// + /// Gets the per-glyph geometry records, parallel to . + /// + public ShapedGlyphPosition[] Positions { get; } /// /// Gets the resolved bidi runs covering the shaped text. @@ -52,4 +73,69 @@ public ShapedText( /// Gets the layout mode used while shaping. /// public LayoutMode LayoutMode { get; } + + /// + /// Gets the contiguous range of shaped glyphs at the given codepoint offset, along + /// with the aggregate shaping state of the range's non-placeholder entries. + /// + /// The zero-based index within the input codepoint collection. + /// + /// The glyph index to start searching from. Updated to the position of the first + /// match so that subsequent calls with increasing offsets avoid rescanning. + /// + /// The index of the first matching glyph. + /// The number of matching glyphs. + /// The font size in PT units of the font containing the glyphs. + /// Whether the range is the result of a substitution. + /// Whether a vertical alternate feature changed any glyph in the range. + /// Whether the range is the result of a decomposition substitution. + /// when at least one glyph matches the offset. + public bool TryGetGlyphsAtOffset( + int offset, + ref int searchIndex, + out int start, + out int count, + out float pointSize, + out bool isSubstituted, + out bool isVerticalSubstitution, + out bool isDecomposed) + { + ShapedGlyphInfo[] infos = this.Infos; + start = 0; + count = 0; + pointSize = 0; + isSubstituted = false; + isVerticalSubstitution = false; + isDecomposed = false; + + for (int i = searchIndex; i < infos.Length; i++) + { + if (infos[i].CodePointIndex == offset) + { + if (count == 0) + { + start = i; + searchIndex = i; + } + + ref readonly ShapedGlyphInfo info = ref infos[i]; + if (!info.IsPlaceholder) + { + isSubstituted = info.IsSubstituted; + isDecomposed = info.IsDecomposed; + isVerticalSubstitution |= info.IsVerticalSubstituted; + pointSize = this.Runs[info.RunIndex].PointSize; + } + + count++; + } + else if (count > 0) + { + // Codepoint indices, though non-sequential, are sorted, so we can stop searching. + break; + } + } + + return count > 0; + } } diff --git a/src/SixLabors.Fonts/ShapedTextRun.cs b/src/SixLabors.Fonts/ShapedTextRun.cs new file mode 100644 index 000000000..2a113496c --- /dev/null +++ b/src/SixLabors.Fonts/ShapedTextRun.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts; + +/// +/// Run-constant shaping state shared by a consecutive range of shaped glyphs: the +/// resolved font, its point size, the source text run, and, for placeholder entries, +/// the bidi run at the insertion point. Run-level state lives once here rather than +/// being repeated per glyph. +/// +internal readonly struct ShapedTextRun +{ + /// + /// Initializes a new instance of the struct. + /// + /// The font that resolved the run's glyphs. + /// The font size in PT units. + /// The source text run. + /// The bidi run assigned to a placeholder insertion point. + public ShapedTextRun(Font font, float pointSize, TextRun textRun, BidiRun bidiRun) + { + this.Font = font; + this.PointSize = pointSize; + this.TextRun = textRun; + this.BidiRun = bidiRun; + } + + /// + /// Gets the font that resolved the run's glyphs. + /// + public Font Font { get; } + + /// + /// Gets the font size in PT units. + /// + public float PointSize { get; } + + /// + /// Gets the source text run. + /// + public TextRun TextRun { get; } + + /// + /// Gets the bidi run assigned to a placeholder insertion point. + /// + public BidiRun BidiRun { get; } +} diff --git a/src/SixLabors.Fonts/ShapingScratch.cs b/src/SixLabors.Fonts/ShapingScratch.cs index f0baaf1ee..c6e29d8d0 100644 --- a/src/SixLabors.Fonts/ShapingScratch.cs +++ b/src/SixLabors.Fonts/ShapingScratch.cs @@ -7,8 +7,7 @@ namespace SixLabors.Fonts; /// /// Reusable shaping pipeline state: the substitution and positioning collections, their -/// shared feature map, and the pool of retired glyph data instances. This is the port of -/// the reference engine's reusable buffer memory model (hb_buffer_t): storage grows to +/// shared feature map, and the pool of retired glyph data instances. Storage grows to /// the workload's high-water mark and is reused across calls, so steady-state shaping /// performs no per-call allocation for pipeline state. /// diff --git a/src/SixLabors.Fonts/TextLayout.LineBreaking.cs b/src/SixLabors.Fonts/TextLayout.LineBreaking.cs index 8b123331c..a8a5761a5 100644 --- a/src/SixLabors.Fonts/TextLayout.LineBreaking.cs +++ b/src/SixLabors.Fonts/TextLayout.LineBreaking.cs @@ -61,14 +61,15 @@ public static LogicalTextLine ComposeLogicalLine( SpanCodePointEnumerator codePointEnumerator = new(grapheme); while (codePointEnumerator.MoveNext()) { - if (!shapedText.Positionings.TryGetGlyphMetricsAtOffset( + if (!shapedText.TryGetGlyphsAtOffset( codePointIndex, ref glyphSearchIndex, + out int glyphStart, + out int glyphCount, out float pointSize, out bool isSubstituted, out bool isVerticalSubstitution, - out bool isDecomposed, - out IReadOnlyList? glyphData)) + out bool isDecomposed)) { // Codepoint was skipped during original enumeration. codePointIndex++; @@ -77,13 +78,17 @@ public static LogicalTextLine ComposeLogicalLine( } List metrics = []; - for (int i = 0; i < glyphData.Count; i++) + List metricFonts = []; + for (int i = 0; i < glyphCount; i++) { - GlyphPositioningCollection.GlyphPositioningData data = glyphData[i]; - if (data.Data.IsPlaceholder) + ref readonly ShapedGlyphInfo info = ref shapedText.Infos[glyphStart + i]; + ShapedTextRun run = shapedText.Runs[info.RunIndex]; + if (info.IsPlaceholder) { textLine.AddPlaceholder( - data, + PlaceholderGlyphMetrics.Create(run.Font, run.TextRun, options.Dpi), + in run, + info.CodePointIndex, graphemeIndex, stringIndex, isHorizontalLayout, @@ -93,9 +98,20 @@ public static LogicalTextLine ComposeLogicalLine( continue; } - // Post-GPOS positions live in the shaping bounds; the shared - // metrics instance is never mutated by positioning. - metrics.Add(new(data.Metrics, data.AdvanceWidth, data.AdvanceHeight, data.PositionOffset, data.Data.TextRun)); + // The shaped result carries numbers only; composition resolves + // the metrics instance from the owning font's cache by the same + // arguments shaping used, so the same instance is returned. + ref readonly ShapedGlyphPosition position = ref shapedText.Positions[glyphStart + i]; + FontGlyphMetrics glyphMetrics = run.Font.FontMetrics.GetGlyphMetrics( + info.CodePoint, + info.GlyphId, + run.TextRun.TextAttributes, + run.TextRun.TextDecorations, + shapedText.LayoutMode, + options.ColorFontSupport); + + metrics.Add(new(glyphMetrics, position.AdvanceWidth, position.AdvanceHeight, position.Offset, run.TextRun)); + metricFonts.Add(run.Font); } if (metrics.Count == 0) @@ -282,14 +298,11 @@ VerticalOrientationType.Rotate or float decomposedAdvance = decomposedAdvances[i]; - // Work out the scaled metrics for the glyph. - while (glyphData[glyphDataIndex].Data.IsPlaceholder) - { - glyphDataIndex++; - } - - GlyphPositioningCollection.GlyphPositioningData positionedGlyph = glyphData[glyphDataIndex]; - FontGlyphMetrics metric = positionedGlyph.Metrics; + // Work out the scaled metrics for the glyph. The metrics list + // excludes placeholders, so the loop index addresses it directly. + PositionedGlyphMetrics positioned = metrics[glyphDataIndex]; + FontGlyphMetrics metric = positioned.Metrics; + Font positionedFont = metricFonts[glyphDataIndex]; // Adjust the advance for the last decomposed glyph to add tracking if applicable. // Tracking should only be added once per grapheme, so only on the last codepoint of the grapheme. @@ -369,14 +382,14 @@ VerticalOrientationType.Rotate or stringIndex, hyphenationMarkerCodePoint.Value, shapedText.LayoutMode, - positionedGlyph.Font, + positionedFont, options)); } // Add our metrics to the line. textLine.Add( - isDecomposed ? [new PositionedGlyphMetrics(metric, positionedGlyph.AdvanceWidth, positionedGlyph.AdvanceHeight, positionedGlyph.PositionOffset, positionedGlyph.Data.TextRun)] : metrics, - positionedGlyph.Font, + isDecomposed ? [positioned] : metrics, + positionedFont, pointSize, decomposedAdvance, lineHeight, @@ -416,22 +429,26 @@ VerticalOrientationType.Rotate or // Placeholders do not consume source text. A placeholder inserted at // the final source position has no following codepoint to visit in // the main loop, so we add those trailing placeholder entries here. - if (shapedText.Positionings.TryGetGlyphMetricsAtOffset( + if (shapedText.TryGetGlyphsAtOffset( codePointIndex, ref glyphSearchIndex, + out int endStart, + out int endCount, out _, out _, out _, - out _, - out IReadOnlyList? endGlyphData)) + out _)) { - for (int i = 0; i < endGlyphData.Count; i++) + for (int i = 0; i < endCount; i++) { - GlyphPositioningCollection.GlyphPositioningData data = endGlyphData[i]; - if (data.Data.IsPlaceholder) + ref readonly ShapedGlyphInfo info = ref shapedText.Infos[endStart + i]; + if (info.IsPlaceholder) { + ShapedTextRun run = shapedText.Runs[info.RunIndex]; textLine.AddPlaceholder( - data, + PlaceholderGlyphMetrics.Create(run.Font, run.TextRun, options.Dpi), + in run, + info.CodePointIndex, graphemeIndex, stringIndex, isHorizontalLayout, diff --git a/src/SixLabors.Fonts/TextLine.cs b/src/SixLabors.Fonts/TextLine.cs index 3072d95e9..c0e29b791 100644 --- a/src/SixLabors.Fonts/TextLine.cs +++ b/src/SixLabors.Fonts/TextLine.cs @@ -248,29 +248,32 @@ public void Add( /// /// Adds an inline placeholder entry at an existing source codepoint position without consuming source text. /// - /// The positioned placeholder glyph data. + /// The placeholder glyph metrics. + /// The shaped run carrying the placeholder's font, point size, text run, and bidi run. + /// The source codepoint index at the placeholder insertion point. /// The source grapheme index at the placeholder insertion point. /// The source UTF-16 index at the placeholder insertion point. /// when the current layout advances horizontally. /// when the current layout is vertical mixed. /// The line-spacing factor to apply to placeholder line height. public void AddPlaceholder( - GlyphPositioningCollection.GlyphPositioningData placeholder, + FontGlyphMetrics placeholderGlyph, + in ShapedTextRun run, + int offset, int graphemeIndex, int stringIndex, bool isHorizontalLayout, bool isVerticalMixedLayout, float lineSpacing) { - FontGlyphMetrics placeholderGlyph = placeholder.Metrics; bool isPlaceholderHorizontal = isHorizontalLayout || isVerticalMixedLayout; float placeholderAdvance = isPlaceholderHorizontal ? placeholderGlyph.AdvanceWidth : placeholderGlyph.AdvanceHeight; Vector2 placeholderScale = new( - placeholder.PointSize / placeholderGlyph.ScaleFactor.X, - placeholder.PointSize / placeholderGlyph.ScaleFactor.Y); + run.PointSize / placeholderGlyph.ScaleFactor.X, + run.PointSize / placeholderGlyph.ScaleFactor.Y); placeholderAdvance *= isPlaceholderHorizontal ? placeholderScale.X : placeholderScale.Y; @@ -278,7 +281,7 @@ public void AddPlaceholder( ? GlyphLayoutMode.Horizontal : GlyphLayoutMode.Vertical; - FontRectangle placeholderBox = placeholderGlyph.GetBoundingBox(placeholderMode, Vector2.Zero, placeholder.PointSize, placeholder.Data.TextRun, Vector2.Zero); + FontRectangle placeholderBox = placeholderGlyph.GetBoundingBox(placeholderMode, Vector2.Zero, run.PointSize, run.TextRun, Vector2.Zero); IMetricsHeader metricsHeader = isPlaceholderHorizontal ? placeholderGlyph.FontMetrics.HorizontalMetrics @@ -288,7 +291,7 @@ public void AddPlaceholder( // normal ascender/descender band. Keep the run font line-box model as // the baseline contribution, then expand only the side the placeholder // actually overhangs so following lines reserve enough space. - float placeholderScaleY = placeholder.PointSize / placeholderGlyph.ScaleFactor.Y; + float placeholderScaleY = run.PointSize / placeholderGlyph.ScaleFactor.Y; float placeholderLineHeight = placeholderGlyph.UnitsPerEm * placeholderScaleY; float placeholderDelta = ((metricsHeader.LineHeight * placeholderScaleY) - placeholderLineHeight) * .5F; float placeholderAscender = (metricsHeader.Ascender * placeholderScaleY) - placeholderDelta; @@ -302,18 +305,18 @@ public void AddPlaceholder( // Placeholders share the source codepoint offset at their insertion point, // but they do not consume source grapheme, codepoint, or UTF-16 indexes. this.Add( - [new(placeholderGlyph, placeholderGlyph.AdvanceWidth, placeholderGlyph.AdvanceHeight, Vector2.Zero, placeholder.Data.TextRun)], - placeholder.Font, - placeholder.PointSize, + [new(placeholderGlyph, placeholderGlyph.AdvanceWidth, placeholderGlyph.AdvanceHeight, Vector2.Zero, run.TextRun)], + run.Font, + run.PointSize, placeholderAdvance, placeholderLineHeight, placeholderAscender, placeholderDescender, placeholderDelta, - placeholder.Data.BidiRun, + run.BidiRun, graphemeIndex, true, - placeholder.Offset, + offset, 0, false, false, diff --git a/src/SixLabors.Fonts/TextShaper.Pipeline.cs b/src/SixLabors.Fonts/TextShaper.Pipeline.cs index df2fc1b35..355c5d575 100644 --- a/src/SixLabors.Fonts/TextShaper.Pipeline.cs +++ b/src/SixLabors.Fonts/TextShaper.Pipeline.cs @@ -14,6 +14,14 @@ namespace SixLabors.Fonts; /// public static partial class TextShaper { + /// + /// The pool of reusable pipeline state. A scratch is exclusively owned between + /// and , and the + /// shaped result is copied out by value before the scratch is returned, so nothing + /// pooled escapes a call. Retained scratch storage stays at its high-water mark. + /// + private static readonly ObjectPool ScratchPool = new(new ShapingScratchPooledObjectPolicy()); + /// /// Resolves the ordered sequence of instances that cover . /// @@ -113,28 +121,33 @@ internal static IReadOnlyList BuildTextRuns(ReadOnlySpan text, Te /// The wrapping-independent shaping state. internal static ShapedText ShapeText(ReadOnlySpan text, TextOptions options) { - // One feature bit assignment for the whole pass: applied feature bits written - // while substituting are read after the glyph data is copied into the - // positioning collection, so both collections must agree on bit meaning. - ShapingFeatureMap featureMap = new(); - GlyphSubstitutionCollection substitutions = new(options, featureMap); - GlyphPositioningCollection positionings = new(options, featureMap); - - return ShapeText(text, options, substitutions, positionings); + // The single pooling site for the shaping pipeline: rent the reusable pipeline + // state, shape, copy the result out by value, and return the state before the + // caller sees the result. Every consumer of shaping goes through here and + // shares the pooled machinery without knowing it exists. + ShapingScratch scratch = ScratchPool.Get(); + try + { + (GlyphSubstitutionCollection substitutions, GlyphPositioningCollection positionings) = scratch.Prepare(options); + return ShapeText(text, options, substitutions, positionings); + } + finally + { + ScratchPool.Return(scratch); + } } /// - /// Shapes using caller-supplied shaping collections, - /// allowing a reusable buffer to supply pre-reset collections whose storage - /// survives across calls. Both collections must share one - /// and already reflect . + /// Shapes using caller-supplied shaping collections. Both + /// collections must share one and already reflect + /// . /// /// The text to process. /// The text options used while shaping. /// The substitution collection to shape into. /// The positioning collection to shape into. /// The wrapping-independent shaping state. - internal static ShapedText ShapeText( + private static ShapedText ShapeText( ReadOnlySpan text, TextOptions options, GlyphSubstitutionCollection substitutions, @@ -319,7 +332,72 @@ or BidiCharacterType.FirstStrongIsolate ShapingProbe.Exit(ShapingProbe.Positioning, probe); - return new ShapedText(positionings, bidiRuns, bidiMap, layoutMode); + // Copy the shaped result out of the pooled collections: run-constant state + // deduplicates into a run table and per-glyph state splits into parallel + // identity and geometry arrays of pure values, so the scratch can go back to + // the pool before consumption and no metrics reference survives shaping. + ulong verticalMask = positionings.GetVerticalFeatureMask(); + int count = positionings.Count; + ShapedGlyphInfo[] infos = new ShapedGlyphInfo[count]; + ShapedGlyphPosition[] positions = new ShapedGlyphPosition[count]; + List runs = []; + + Font? runFont = null; + TextRun? runTextRun = null; + BidiRun runBidiRun = default; + for (int i = 0; i < count; i++) + { + GlyphPositioningCollection.GlyphPositioningData data = positionings.GetPositioningData(i); + GlyphShapingData shaping = data.Data; + + // Placeholders carry a bidi run of their own, so they always cut a run. + if (data.Font != runFont + || shaping.TextRun != runTextRun + || (shaping.IsPlaceholder && !shaping.BidiRun.Equals(runBidiRun))) + { + runFont = data.Font; + runTextRun = shaping.TextRun; + runBidiRun = shaping.BidiRun; + runs.Add(new(data.Font, data.PointSize, shaping.TextRun, shaping.BidiRun)); + } + + ShapedGlyphFlags flags = ShapedGlyphFlags.None; + if (shaping.IsPlaceholder) + { + flags |= ShapedGlyphFlags.Placeholder; + } + + if (shaping.IsSubstituted) + { + flags |= ShapedGlyphFlags.Substituted; + } + + if (shaping.IsDecomposed) + { + flags |= ShapedGlyphFlags.Decomposed; + } + + if ((shaping.AppliedFeatureMask & verticalMask) != 0) + { + flags |= ShapedGlyphFlags.VerticalSubstituted; + } + + infos[i] = new( + data.Offset, + shaping.CodePoint, + shaping.CodePointCount, + data.Metrics.GlyphId, + (ushort)(runs.Count - 1), + flags); + + positions[i] = new( + data.AdvanceWidth, + data.AdvanceHeight, + data.PositionOffset, + data.Metrics.Offset); + } + + return new ShapedText([.. runs], infos, positions, bidiRuns, bidiMap, layoutMode); } /// @@ -509,4 +587,18 @@ private static void SubstituteBidiMirrors(FontMetrics fontMetrics, GlyphSubstitu } } } + + /// + /// The pooling policy for instances: scratch state is + /// reset on acquisition by , so returned + /// instances are always accepted. + /// + private sealed class ShapingScratchPooledObjectPolicy : IPooledObjectPolicy + { + /// + public ShapingScratch Create() => new(); + + /// + public bool Return(ShapingScratch obj) => true; + } } diff --git a/src/SixLabors.Fonts/TextShaper.cs b/src/SixLabors.Fonts/TextShaper.cs index 1749f617a..e7f5d94bf 100644 --- a/src/SixLabors.Fonts/TextShaper.cs +++ b/src/SixLabors.Fonts/TextShaper.cs @@ -27,15 +27,6 @@ namespace SixLabors.Fonts; /// public static partial class TextShaper { - /// - /// The pool of reusable pipeline state, the reference engine's buffer memory model. - /// A scratch is exclusively owned between and - /// , and the result list is materialized by value - /// before the scratch is returned, so nothing pooled escapes a call. Retained - /// scratch storage stays at its high-water mark. - /// - private static readonly ObjectPool ScratchPool = new(new ShapingScratchPooledObjectPolicy()); - /// public static IReadOnlyList Shape(string text, TextOptions options) { @@ -68,56 +59,36 @@ public static IReadOnlyList Shape(ReadOnlySpan text, TextOpti return []; } - ShapingScratch scratch = ScratchPool.Get(); - try - { - (GlyphSubstitutionCollection substitutions, GlyphPositioningCollection positionings) = scratch.Prepare(options); - ShapeText(text, options, substitutions, positionings); + ShapedText shaped = ShapeText(text, options); - var probe = ShapingProbe.Enter(); - List glyphs = new(positionings.Count); - for (int i = 0; i < positionings.Count; i++) + var probe = ShapingProbe.Enter(); + ShapedGlyphInfo[] infos = shaped.Infos; + ShapedGlyphPosition[] positions = shaped.Positions; + ShapedTextRun[] runs = shaped.Runs; + List glyphs = new(infos.Length); + for (int i = 0; i < infos.Length; i++) + { + ref readonly ShapedGlyphInfo info = ref infos[i]; + if (info.IsPlaceholder) { - GlyphPositioningCollection.GlyphPositioningData data = positionings.GetPositioningData(i); - if (data.Data.IsPlaceholder) - { - // Placeholder runs reserve layout space for inline objects; they carry - // no glyph. - continue; - } - - FontGlyphMetrics metrics = data.Metrics; - glyphs.Add(new ShapedGlyph( - data.Font, - metrics.GlyphId, - data.Data.CodePoint, - data.Offset, - data.Data.CodePointCount, - data.AdvanceWidth, - data.AdvanceHeight, - metrics.Offset + data.PositionOffset)); + // Placeholder runs reserve layout space for inline objects; they carry + // no glyph. + continue; } - ShapingProbe.Exit(ShapingProbe.Projection, probe); - return glyphs; + ref readonly ShapedGlyphPosition position = ref positions[i]; + glyphs.Add(new ShapedGlyph( + runs[info.RunIndex].Font, + info.GlyphId, + info.CodePoint, + info.CodePointIndex, + info.CodePointCount, + position.AdvanceWidth, + position.AdvanceHeight, + position.Bearing + position.Offset)); } - finally - { - ScratchPool.Return(scratch); - } - } - - /// - /// The pooling policy for instances: scratch state is - /// reset on acquisition by , so returned - /// instances are always accepted. - /// - private sealed class ShapingScratchPooledObjectPolicy : IPooledObjectPolicy - { - /// - public ShapingScratch Create() => new(); - /// - public bool Return(ShapingScratch obj) => true; + ShapingProbe.Exit(ShapingProbe.Projection, probe); + return glyphs; } } From d90c731cda55759b53bfd10bde64d106184be2a0 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 10:11:20 +1000 Subject: [PATCH 11/94] Replace the shaping collections with a single flat-array ShapingBuffer The hb_buffer model applied end to end. One ShapingBuffer type replaces GlyphShapingCollection, GlyphSubstitutionCollection, and GlyphPositioningCollection: glyph state is a struct record (GlyphShapingData, codepoint index folded in) living in one growable array, with a parallel metrics stream seeded after substitution and truncate-only resets that keep storage at the pass high-water mark. The scratch owns two buffer instances - a per-font-run substitution workspace and the accumulated positioning result - distinguished by an explicit role that also replaces the shapers' phase dispatch, which previously pattern-matched on collection type. Element access is by interior reference: buffer indexers return ref, call sites mutate through the indexer expression or explicit ref locals, and helper methods take the record by ref so the shaping-class cache writes land and no ~120-byte copies occur per call. The conversion surfaced one real defect class - reassigning a ref local from the buffer stores a whole record through the reference where the old class code merely rebound a variable - found by bisecting a corrupted Arabic mark test down to FixMarkAttachment, and fixed there and in the fraction scanners. Two source-convention tests now fail the build on value-copy element bindings and on buffer type-test dispatch so both silent-corruption classes are compile-gated. Tests: 5565 passed, 0 failed (includes the new convention scans). Pooled-reuse and HarfBuzzSharp differential oracles exact, including the lam+shadda mark anchoring case used to isolate the rebind defect. ShapeTextBenchmark, 12 iterations, low-noise runs, vs this morning's clean baseline and the previous commit's allocations: | Scenario | Mean | Ratio vs HB | Allocated | |---|---:|---:|---:| | Latin | 17.226 -> 15.483 us (-10.1%) | 7.59 -> 6.96 | 24,144 -> 8,296 B (-65.6%; prev commit 11,984) | | Arabic | 18.148 -> 15.651 us (-13.8%) | 4.78 -> 4.15 | 18,400 -> 7,856 B (-57.3%; prev commit 10,952) | --- src/SixLabors.Fonts/FileFontMetrics.cs | 12 +- src/SixLabors.Fonts/FontMetrics.cs | 12 +- .../GlyphPositioningCollection.cs | 361 ------- src/SixLabors.Fonts/GlyphShapingCollection.cs | 190 ---- src/SixLabors.Fonts/GlyphShapingData.cs | 70 +- .../GlyphSubstitutionCollection.cs | 475 --------- src/SixLabors.Fonts/MemoryFontMetrics.cs | 10 +- src/SixLabors.Fonts/ShapingBuffer.cs | 949 ++++++++++++++++++ src/SixLabors.Fonts/ShapingBufferRole.cs | 22 + src/SixLabors.Fonts/ShapingScratch.cs | 48 +- src/SixLabors.Fonts/StreamFontMetrics.cs | 16 +- .../AdvancedTypographicUtils.cs | 112 +-- .../AdvancedTypographic/GPos/AnchorTable.cs | 20 +- .../GPos/LookupListTable.cs | 16 +- .../GPos/LookupType1SubTable.cs | 12 +- .../GPos/LookupType2SubTable.cs | 20 +- .../GPos/LookupType3SubTable.cs | 32 +- .../GPos/LookupType4SubTable.cs | 12 +- .../GPos/LookupType5SubTable.cs | 16 +- .../GPos/LookupType6SubTable.cs | 14 +- .../GPos/LookupType7SubTable.cs | 24 +- .../GPos/LookupType8SubTable.cs | 24 +- .../GPos/NotImplementedSubTable.cs | 2 +- .../Tables/AdvancedTypographic/GPosTable.cs | 101 +- .../GSub/LookupListTable.cs | 20 +- .../GSub/LookupType1SubTable.cs | 12 +- .../GSub/LookupType2SubTable.cs | 6 +- .../GSub/LookupType3SubTable.cs | 6 +- .../GSub/LookupType4SubTable.cs | 24 +- .../GSub/LookupType5SubTable.cs | 24 +- .../GSub/LookupType6SubTable.cs | 24 +- .../GSub/LookupType8SubTable.cs | 10 +- .../GSub/NotImplementedSubTable.cs | 2 +- .../Tables/AdvancedTypographic/GSubTable.cs | 78 +- .../Shapers/ArabicShaper.cs | 50 +- .../AdvancedTypographic/Shapers/BaseShaper.cs | 60 +- .../Shapers/DefaultShaper.cs | 90 +- .../Shapers/HangulShaper.cs | 116 +-- .../Shapers/HebrewShaper.cs | 38 +- .../Shapers/IndicShaper.cs | 388 +++---- .../Shapers/MyanmarShaper.cs | 112 +-- .../Shapers/ShapingStage.cs | 18 +- .../AdvancedTypographic/Shapers/ThaiShaper.cs | 42 +- .../Shapers/UniversalShaper.cs | 156 +-- .../SkippingGlyphIterator.cs | 20 +- .../Tables/General/Kern/KerningTable.cs | 16 +- src/SixLabors.Fonts/TextLayout.cs | 1 - src/SixLabors.Fonts/TextShaper.Pipeline.cs | 73 +- .../ShapingBufferConventionTests.cs | 93 ++ 49 files changed, 2025 insertions(+), 2024 deletions(-) delete mode 100644 src/SixLabors.Fonts/GlyphPositioningCollection.cs delete mode 100644 src/SixLabors.Fonts/GlyphShapingCollection.cs delete mode 100644 src/SixLabors.Fonts/GlyphSubstitutionCollection.cs create mode 100644 src/SixLabors.Fonts/ShapingBuffer.cs create mode 100644 src/SixLabors.Fonts/ShapingBufferRole.cs create mode 100644 tests/SixLabors.Fonts.Tests/ShapingBufferConventionTests.cs diff --git a/src/SixLabors.Fonts/FileFontMetrics.cs b/src/SixLabors.Fonts/FileFontMetrics.cs index b0f9a1993..941df1202 100644 --- a/src/SixLabors.Fonts/FileFontMetrics.cs +++ b/src/SixLabors.Fonts/FileFontMetrics.cs @@ -207,16 +207,16 @@ internal override bool TryGetBaselineCoordinate(Tag baselineTag, bool isVertical => this.fontMetrics.Value.TryGetBaselineCoordinate(baselineTag, isVerticalLayout, out coordinate); /// - internal override void ApplySubstitution(GlyphSubstitutionCollection collection) - => this.fontMetrics.Value.ApplySubstitution(collection); + internal override void ApplySubstitution(ShapingBuffer buffer) + => this.fontMetrics.Value.ApplySubstitution(buffer); /// internal override bool TryGetKerningOffset(ushort currentId, ushort nextId, out Vector2 vector) => this.fontMetrics.Value.TryGetKerningOffset(currentId, nextId, out vector); /// - internal override void UpdatePositions(GlyphPositioningCollection collection) - => this.fontMetrics.Value.UpdatePositions(collection); + internal override void UpdatePositions(ShapingBuffer buffer) + => this.fontMetrics.Value.UpdatePositions(buffer); /// internal override float GetGDefVariationDelta(uint packedVariationIndex) @@ -227,9 +227,9 @@ internal override ReadOnlySpan GetNormalizedCoordinates() => this.fontMetrics.Value.GetNormalizedCoordinates(); /// - /// Reads a font collection from the specified filesystem path. + /// Reads a font buffer from the specified filesystem path. /// - /// The filesystem path to the font collection. + /// The filesystem path to the font buffer. /// A read-only memory region containing the font metrics. public static ReadOnlyMemory LoadFontCollection(string path) { diff --git a/src/SixLabors.Fonts/FontMetrics.cs b/src/SixLabors.Fonts/FontMetrics.cs index 999d400c5..0384222b4 100644 --- a/src/SixLabors.Fonts/FontMetrics.cs +++ b/src/SixLabors.Fonts/FontMetrics.cs @@ -316,10 +316,10 @@ internal abstract FontGlyphMetrics GetGlyphMetrics( internal abstract bool TryGetBaselineCoordinate(Tag baselineTag, bool isVerticalLayout, out short coordinate); /// - /// Applies any available substitutions to the collection of glyphs. + /// Applies any available substitutions to the buffer of glyphs. /// - /// The glyph substitution collection. - internal abstract void ApplySubstitution(GlyphSubstitutionCollection collection); + /// The glyph substitution buffer. + internal abstract void ApplySubstitution(ShapingBuffer buffer); /// /// Gets the amount, in font units, the glyph should be offset if it is followed by @@ -338,10 +338,10 @@ internal abstract FontGlyphMetrics GetGlyphMetrics( internal abstract bool TryGetKerningOffset(ushort currentId, ushort nextId, out Vector2 vector); /// - /// Applies any available positioning updates to the collection of glyphs. + /// Applies any available positioning updates to the buffer of glyphs. /// - /// The glyph positioning collection. - internal abstract void UpdatePositions(GlyphPositioningCollection collection); + /// The glyph positioning buffer. + internal abstract void UpdatePositions(ShapingBuffer buffer); /// /// Computes a GPOS/GSUB variation delta for the given packed VariationIndex. diff --git a/src/SixLabors.Fonts/GlyphPositioningCollection.cs b/src/SixLabors.Fonts/GlyphPositioningCollection.cs deleted file mode 100644 index b86d861c7..000000000 --- a/src/SixLabors.Fonts/GlyphPositioningCollection.cs +++ /dev/null @@ -1,361 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Numerics; -using System.Runtime.CompilerServices; -using SixLabors.Fonts.Tables.AdvancedTypographic; -using SixLabors.Fonts.Unicode; - -namespace SixLabors.Fonts; - -/// -/// Represents a collection of glyph metrics that are mapped to input codepoints. -/// -internal sealed class GlyphPositioningCollection : GlyphShapingCollection -{ - /// - /// 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 = []; - - /// - /// Initializes a new instance of the class. - /// - /// The text options. - /// The feature bit assignment shared by the shaping pass. - public GlyphPositioningCollection(TextOptions textOptions, ShapingFeatureMap featureMap) - : base(textOptions, featureMap) - { - } - - /// - public override int Count => this.glyphs.Count; - - /// - public override GlyphShapingData this[int index] - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => this.glyphs[index].Data; - } - - /// - /// Gets the full positioning data at the given index. - /// - /// The zero-based index of the element. - /// The positioning data. - public GlyphPositioningData GetPositioningData(int index) => this.glyphs[index]; - - /// - /// Resets the collection for reuse by a new shaping pass, returning its glyph data - /// instances to the pool. This collection owns the pass's final instances (the - /// substitution collection transfers ownership during metrics population), so this - /// is the single point instances are pooled, keeping each instance pooled at most - /// once. - /// - /// The text options for the new pass. - /// The pool receiving the retired instances. - internal void ResetForReuse(TextOptions textOptions, List pool) - { - List glyphs = this.glyphs; - for (int i = 0; i < glyphs.Count; i++) - { - pool.Add(glyphs[i].Data); - } - - glyphs.Clear(); - this.ResetCore(textOptions); - } - - /// - /// Updates the collection of glyph ids to the metrics collection to overwrite any glyphs that have been previously - /// identified as fallbacks. - /// - /// The font face with metrics. - /// The glyph substitution collection. - /// if the metrics collection does not contain any fallbacks; otherwise . - public bool TryUpdate(Font font, GlyphSubstitutionCollection collection) - { - FontMetrics fontMetrics = font.FontMetrics; - LayoutMode layoutMode = this.TextOptions.LayoutMode; - ColorFontSupport colorFontSupport = this.TextOptions.ColorFontSupport; - bool hasFallBacks = false; - List orphans = []; - - ulong verticalMask = this.GetVerticalFeatureMask(); - - for (int i = 0; i < this.glyphs.Count; i++) - { - GlyphPositioningData current = this.glyphs[i]; - if (current.Metrics.GlyphType != GlyphType.Fallback) - { - // We've already got the correct glyph. - continue; - } - - int offset = current.Offset; - float pointSize = current.PointSize; - if (collection.TryGetGlyphShapingDataAtOffset(offset, out IReadOnlyList? data)) - { - int replacementCount = 0; - for (int j = 0; j < data.Count; j++) - { - GlyphShapingData shape = data[j]; - ushort id = shape.GlyphId; - CodePoint codePoint = shape.CodePoint; - - TextAttributes textAttributes = shape.TextRun.TextAttributes; - TextDecorations textDecorations = shape.TextRun.TextDecorations; - - bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(codePoint, layoutMode) - || (shape.AppliedFeatureMask & verticalMask) != 0; - - FontGlyphMetrics metrics = fontMetrics.GetGlyphMetrics(codePoint, id, textAttributes, textDecorations, layoutMode, colorFontSupport); - { - // If the glyphs are fallbacks we don't want them as - // we've already captured them on the first run. - if (metrics.GlyphType == GlyphType.Fallback && !CodePoint.IsControl(codePoint)) - { - hasFallBacks = true; - } - } - - if (metrics.GlyphType != GlyphType.Fallback) - { - if (replacementCount == 0) - { - // There should only be a single fallback glyph at this position from the previous collection. - this.glyphs.RemoveAt(i); - } - - // We only want a single dimensional advance for positioning. - - // Track the number of inserted glyphs at the offset so we can correctly increment our position. - // The substituted data is reused rather than copied: the - // substitution collection releases its instances at the end of - // each run, so positioning takes ownership. - shape.ClearFeatures(); - if (isVertical) - { - shape.Bounds = new(0, 0, 0, metrics.AdvanceHeight); - } - else - { - shape.Bounds = new(0, 0, metrics.AdvanceWidth, 0); - } - - this.RecordGlyphId(metrics.GlyphId); - this.glyphs.Insert(i += replacementCount, new(offset, shape, font, pointSize, metrics)); - replacementCount++; - } - } - } - else - { - // 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); - } - } - - // Remove any orphans. - for (int i = orphans.Count - 1; i >= 0; i--) - { - this.glyphs.RemoveAt(orphans[i]); - } - - return !hasFallBacks; - } - - /// - /// Adds the collection of glyph ids to the metrics collection. - /// identified as fallbacks. - /// - /// The font face with metrics. - /// The glyph substitution collection. - /// if the metrics collection does not contain any fallbacks; otherwise . - public bool TryAdd(Font font, GlyphSubstitutionCollection collection) - { - bool hasFallBacks = false; - FontMetrics fontMetrics = font.FontMetrics; - LayoutMode layoutMode = this.TextOptions.LayoutMode; - ColorFontSupport colorFontSupport = this.TextOptions.ColorFontSupport; - - ulong verticalMask = this.GetVerticalFeatureMask(); - - for (int i = 0; i < collection.Count; i++) - { - GlyphShapingData data = collection.GetGlyphShapingData(i, out int offset); - CodePoint codePoint = data.CodePoint; - ushort id = data.GlyphId; - - if (data.IsPlaceholder) - { - // Placeholders are synthetic glyphs: they need layout metrics but must not - // go through font glyph lookup, fallback resolution, or GPOS positioning. - FontGlyphMetrics placeholderMetrics = PlaceholderGlyphMetrics.Create(font, data.TextRun, this.TextOptions.Dpi); - - GlyphShapingData placeholderData = data; - placeholderData.ClearFeatures(); - if (layoutMode.IsVertical()) - { - placeholderData.Bounds = new(0, 0, 0, placeholderMetrics.AdvanceHeight); - } - else - { - placeholderData.Bounds = new(0, 0, placeholderMetrics.AdvanceWidth, 0); - } - - placeholderData.IsPositioned = true; - - this.RecordGlyphId(placeholderMetrics.GlyphId); - this.glyphs.Add(new(offset, placeholderData, font, font.Size, placeholderMetrics)); - continue; - } - - TextAttributes textAttributes = data.TextRun.TextAttributes; - TextDecorations textDecorations = data.TextRun.TextDecorations; - - bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(codePoint, layoutMode) - || (data.AppliedFeatureMask & verticalMask) != 0; - - FontGlyphMetrics metrics = fontMetrics.GetGlyphMetrics(codePoint, id, textAttributes, textDecorations, layoutMode, colorFontSupport); - - if (metrics.GlyphType == GlyphType.Fallback && !CodePoint.IsControl(codePoint)) - { - hasFallBacks = true; - } - - // We only want a single dimensional advance for positioning; assigning a - // fresh bounds value starts dirty tracking clean for GPOS. - // The substituted data is reused rather than copied: the substitution - // collection releases its instances at the end of each run, so positioning - // takes ownership. - data.ClearFeatures(); - if (isVertical) - { - data.Bounds = new(0, 0, 0, metrics.AdvanceHeight); - } - else - { - data.Bounds = new(0, 0, metrics.AdvanceWidth, 0); - } - - this.RecordGlyphId(metrics.GlyphId); - this.glyphs.Add(new(offset, data, font, font.Size, metrics)); - } - - return !hasFallBacks; - } - - /// - /// Marks the glyph at the specified index as positioned. Positions accumulate in the - /// glyph's shaping bounds and are read from there by consumers, so the shared metrics - /// instance is never mutated. - /// - /// The zero-based index of the element. - public void UpdatePosition(int index) => this[index].IsPositioned = true; - - /// - /// Adds dx and dy to the positioned advance of the glyph at the given index and id. - /// Advances accumulate in the glyph's shaping bounds so the shared metrics instance - /// is never mutated. - /// - /// The font face with metrics. - /// The zero-based index of the element. - /// The id of the glyph to offset. - /// The delta x-advance. - /// The delta y-advance. - public void Advance(FontMetrics fontMetrics, int index, ushort glyphId, short dx, short dy) - { - LayoutMode layoutMode = this.TextOptions.LayoutMode; - GlyphPositioningData glyph = this.glyphs[index]; - FontGlyphMetrics m = glyph.Metrics; - - if (m.GlyphId == glyphId && fontMetrics == m.FontMetrics) - { - bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(m.CodePoint, layoutMode) - || (glyph.Data.AppliedFeatureMask & this.GetVerticalFeatureMask()) != 0; - - // Advance heights grow downward but font-space grows upward, hence the negation. - glyph.Data.Bounds.Width += dx; - if (isVertical) - { - glyph.Data.Bounds.Height -= dy; - } - } - } - - /// - /// 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 position. - /// if the element should be processed; otherwise, . - public bool ShouldProcess(FontMetrics fontMetrics, int index) - { - GlyphPositioningData data = this.glyphs[index]; - if (data.Data.IsPositioned) - { - return false; - } - - return data.Metrics.FontMetrics == fontMetrics; - } - - /// - /// Gets the combined mask of the three vertical alternate features. Computed from - /// the shared feature map so it stays valid for applied bits written during - /// substitution and read here after the copy into this collection. - /// - /// The combined mask, or zero when no vertical feature was registered. - internal ulong GetVerticalFeatureMask() - => this.FeatureMap.GetMask(KnownFeatureTags.VerticalAlternates) - | this.FeatureMap.GetMask(KnownFeatureTags.VerticalAlternatesAndRotation) - | this.FeatureMap.GetMask(KnownFeatureTags.VerticalAlternatesForRotation); - - [DebuggerDisplay("{DebuggerDisplay,nq}")] - public class GlyphPositioningData - { - public GlyphPositioningData(int offset, GlyphShapingData data, Font font, float pointSize, FontGlyphMetrics metrics) - { - this.Offset = offset; - this.Data = data; - this.Font = font; - this.PointSize = pointSize; - this.Metrics = metrics; - } - - public int Offset { get; set; } - - public GlyphShapingData Data { get; set; } - - public Font Font { get; set; } - - public float PointSize { get; set; } - - public FontGlyphMetrics Metrics { get; set; } - - /// - /// Gets the positioned horizontal advance in font design units: the shaping bounds - /// value once positioning has written one, otherwise the metrics advance. - /// - public ushort AdvanceWidth => this.Data.Bounds.IsDirtyWH ? (ushort)this.Data.Bounds.Width : this.Metrics.AdvanceWidth; - - /// - /// Gets the positioned vertical advance in font design units: the shaping bounds - /// value once positioning has written one, otherwise the metrics advance. - /// - public ushort AdvanceHeight => this.Data.Bounds.IsDirtyWH ? (ushort)this.Data.Bounds.Height : this.Metrics.AdvanceHeight; - - /// - /// Gets the placement offset written by positioning, in font design units. Geometry - /// consumers compose it with the metrics offset. - /// - public Vector2 PositionOffset => new(this.Data.Bounds.X, this.Data.Bounds.Y); - - private string DebuggerDisplay => FormattableString.Invariant($"Offset: {this.Offset}, Data: {this.Data.ToDebuggerDisplay()}"); - } -} diff --git a/src/SixLabors.Fonts/GlyphShapingCollection.cs b/src/SixLabors.Fonts/GlyphShapingCollection.cs deleted file mode 100644 index b74721bfa..000000000 --- a/src/SixLabors.Fonts/GlyphShapingCollection.cs +++ /dev/null @@ -1,190 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Globalization; -using SixLabors.Fonts.Tables.AdvancedTypographic; - -namespace SixLabors.Fonts; - -/// -/// The base for the glyph shaping collections, owning the state and operations shared -/// by substitution and positioning: the pass-wide feature bit assignment, the resolved -/// language candidates, the glyph id digest, and per-glyph feature mask manipulation. -/// Derived collections own only their storage and the shape-specific mutation APIs. -/// -internal abstract class GlyphShapingCollection -{ - /// - /// The approximate membership filter over every glyph id the collection has ever - /// contained. See for the growth contract. - /// - private GlyphSetDigest glyphDigest; - - /// - /// Initializes a new instance of the class. - /// - /// The text options. - /// The feature bit assignment shared by the shaping pass. - protected GlyphShapingCollection(TextOptions textOptions, ShapingFeatureMap featureMap) - { - this.TextOptions = textOptions; - this.FeatureMap = featureMap; - - // A null culture takes the ambient current culture, mirroring the reference - // shaping engine model where an unset buffer language is guessed from the - // locale. CultureInfo.InvariantCulture expresses no language preference. - CultureInfo culture = textOptions.Culture ?? CultureInfo.CurrentCulture; - this.LanguageTags = OpenTypeLanguageTagMap.TryGetTags(culture, out Tag[] tags) ? tags : []; - } - - /// - /// Gets the collection count. - /// - public abstract int Count { get; } - - /// - /// Gets the text options used by this collection. - /// - public TextOptions TextOptions { get; private set; } - - /// - /// Gets the candidate OpenType language system tags resolved from - /// , most specific first, or an empty array when the - /// culture expresses no language preference. Resolved once per shaping pass. - /// - public Tag[] LanguageTags { get; private set; } - - /// - /// Gets the feature bit assignment shared by every collection of the shaping pass. - /// See for the mask model and why the instance must - /// be shared across the substitution and positioning collections. - /// - public ShapingFeatureMap FeatureMap { get; } - - /// - /// Gets the approximate membership filter over every glyph id the collection has - /// ever contained. The digest only grows: substituted-away ids remain, keeping a - /// definitive negative from sound while - /// lookups mutate the collection mid-application. Every glyph id write must funnel - /// through the collection so the digest observes it; see - /// and . - /// - public GlyphSetDigest GlyphDigest => this.glyphDigest; - - /// - /// Gets the glyph shaping data at the specified index. - /// - /// The zero-based index of the elements to get. - /// The . - public abstract GlyphShapingData this[int index] { get; } - - /// - /// Sets the glyph id at the specified index, recording the id in - /// . Callers outside the collections must use this rather - /// than writing directly, which would leave - /// the digest unaware of the new id. - /// - /// The zero-based index of the element. - /// The glyph id to set. - public void SetGlyphId(int index, ushort glyphId) - { - this.glyphDigest.Add(glyphId); - this[index].GlyphId = glyphId; - } - - /// - /// Resets the pass-wide state shared by the collections for reuse by a new shaping - /// pass: adopts the new options, re-resolves the language candidates, and empties - /// the glyph digest. Derived collections call this from their reuse reset after - /// clearing their own storage. - /// - /// The text options for the new pass. - private protected void ResetCore(TextOptions textOptions) - { - this.TextOptions = textOptions; - this.glyphDigest = default; - - CultureInfo culture = textOptions.Culture ?? CultureInfo.CurrentCulture; - this.LanguageTags = OpenTypeLanguageTagMap.TryGetTags(culture, out Tag[] tags) ? tags : []; - } - - /// - /// Adds the shaping feature to the collection which should be applied to the glyph at a specified index. - /// - /// - /// Registration only ever accumulates: adding a disabled entry for an already - /// enabled feature must not clear the enabled bit, matching the list model this - /// replaced where a disabled duplicate left earlier enabled entries in force. - /// - /// The zero-based index of the element. - /// The feature to apply. - public void AddShapingFeature(int index, TagEntry feature) - { - GlyphShapingData data = this[index]; - ulong mask = this.FeatureMap.GetOrAddMask(feature.Tag); - data.RegisteredFeatureMask |= mask; - if (feature.Enabled) - { - data.FeatureMask |= mask; - } - } - - /// - /// Adds the shaping feature to every glyph in the given range, resolving the - /// feature's mask bit once for the whole range. Shaper plans register each stage - /// feature across the full run, so the per-glyph work must be a single bitwise OR. - /// - /// The zero-based index of the first element. - /// The number of elements in the range. - /// The feature to apply. - public void AddShapingFeatureRange(int index, int count, TagEntry feature) - { - ulong mask = this.FeatureMap.GetOrAddMask(feature.Tag); - int end = index + count; - for (int i = index; i < end; i++) - { - GlyphShapingData data = this[i]; - data.RegisteredFeatureMask |= mask; - if (feature.Enabled) - { - data.FeatureMask |= mask; - } - } - } - - /// - /// Enables a previously added shaping feature. - /// - /// - /// Intersecting with the registered mask preserves the contract that enabling a - /// feature a shaper never added for this glyph is a no-op. - /// - /// The zero-based index of the element. - /// The feature to enable. - public void EnableShapingFeature(int index, Tag feature) - { - GlyphShapingData data = this[index]; - data.FeatureMask |= data.RegisteredFeatureMask & this.FeatureMap.GetMask(feature); - } - - /// - /// Disables a previously added shaping feature. - /// - /// - /// An unregistered tag yields a zero mask whose complement clears nothing. - /// - /// The zero-based index of the element. - /// The feature to disable. - public void DisableShapingFeature(int index, Tag feature) - { - GlyphShapingData data = this[index]; - data.FeatureMask &= ~this.FeatureMap.GetMask(feature); - } - - /// - /// Records a glyph id in . Derived collections must call - /// this from every code path that stores or overwrites a glyph id. - /// - /// The glyph id. - protected void RecordGlyphId(ushort glyphId) => this.glyphDigest.Add(glyphId); -} diff --git a/src/SixLabors.Fonts/GlyphShapingData.cs b/src/SixLabors.Fonts/GlyphShapingData.cs index 4e27bab89..d497bfaed 100644 --- a/src/SixLabors.Fonts/GlyphShapingData.cs +++ b/src/SixLabors.Fonts/GlyphShapingData.cs @@ -9,32 +9,38 @@ namespace SixLabors.Fonts; /// -/// Contains supplementary data that allows the shaping of glyphs. +/// Contains supplementary data that allows the shaping of glyphs. Stored by value in +/// the shaping buffer's flat storage; call sites mutate through the buffer indexer so +/// writes land in place. /// [DebuggerDisplay("{DebuggerDisplay,nq}")] -internal class GlyphShapingData +internal struct GlyphShapingData { - private ushort glyphId; - +#pragma warning disable SA1401 // Field exposed so positioning mutates the embedded bounds in place. /// - /// The shaping bounds. Backing storage for . + /// The shaping bounds. A field rather than a property so positioning lookups + /// mutate the embedded value in place and re-seeding is plain value assignment. /// - private GlyphShapingBounds bounds; + public GlyphShapingBounds Bounds; +#pragma warning restore SA1401 + + private ushort glyphId; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the struct. /// /// The text run. public GlyphShapingData(TextRun textRun) => this.TextRun = textRun; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the struct. /// /// The data to copy properties from. /// Whether to clear features. public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) { this.GlyphId = data.GlyphId; + this.CodePointIndex = data.CodePointIndex; this.CodePoint = data.CodePoint; this.CodePointCount = data.CodePointCount; this.Direction = data.Direction; @@ -76,7 +82,7 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) this.AppliedFeatureMask = data.AppliedFeatureMask; - this.bounds = data.bounds; + this.Bounds = data.Bounds; this.CachedShapingClass = data.CachedShapingClass; this.ShapingClassCacheKey = data.ShapingClassCacheKey; } @@ -108,6 +114,12 @@ public ushort GlyphId /// internal int ShapingClassCacheKey { get; set; } = -1; + /// + /// Gets or sets the zero-based index within the input codepoint collection of the + /// leading codepoint this glyph represents. + /// + public int CodePointIndex { get; set; } + /// /// Gets or sets the leading codepoint. /// @@ -177,12 +189,6 @@ public ushort GlyphId /// public ulong AppliedFeatureMask { get; set; } - /// - /// Gets a reference to the shaping bounds, so positioning lookups mutate the - /// embedded value in place and re-seeding is plain value assignment. - /// - public ref GlyphShapingBounds Bounds => ref this.bounds; - /// /// Gets or sets a value indicating whether this glyph is the result of a substitution. /// @@ -227,40 +233,6 @@ private string DebuggerDisplay => FormattableString .Invariant($" {this.GlyphId} : {this.CodePoint.ToDebuggerDisplay()} : {CodePoint.GetScriptClass(this.CodePoint)} : {this.Direction} : {this.TextRun.TextAttributes} : {this.LigatureId} : {this.LigatureComponent} : {this.IsDecomposed}"); - /// - /// Resets the instance to its freshly constructed state for reuse by a pooled - /// shaping pass. Every mutable member must be reset here; a missed member leaks - /// state between shaping calls. - /// - /// The text run. - public void Reset(TextRun textRun) - { - this.glyphId = 0; - this.bounds = default; - this.CachedShapingClass = default; - this.ShapingClassCacheKey = -1; - this.CodePoint = default; - this.CodePointCount = 1; - this.Direction = default; - this.TextRun = textRun; - this.LigatureId = 0; - this.IsLigated = false; - this.LigatureComponent = -1; - this.MarkAttachment = -1; - this.CursiveAttachment = -1; - this.RegisteredFeatureMask = 0; - this.FeatureMask = 0; - this.AppliedFeatureMask = 0; - this.IsSubstituted = false; - this.IsDecomposed = false; - this.IsPlaceholder = false; - this.BidiRun = default; - this.IsPositioned = false; - this.IsKerned = false; - this.UniversalShapingEngineInfo = null; - this.IndicShapingEngineInfo = null; - } - /// /// Clears the registered and enabled feature masks while preserving the applied /// mask, matching the semantics of copying with cleared features. Positioning diff --git a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs deleted file mode 100644 index 6688603d5..000000000 --- a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs +++ /dev/null @@ -1,475 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Runtime.CompilerServices; -using SixLabors.Fonts.Tables.AdvancedTypographic; -using SixLabors.Fonts.Unicode; - -namespace SixLabors.Fonts; - -/// -/// Represents a collection of glyph indices that are mapped to input codepoints. -/// -internal sealed class GlyphSubstitutionCollection : GlyphShapingCollection -{ - /// - /// Contains a map the index of a map within the collection, non-sequential codepoint offsets, and their glyph ids. - /// - private readonly List glyphs = []; - - /// - /// Initializes a new instance of the class. - /// - /// The text options. - /// The feature bit assignment shared by the shaping pass. - public GlyphSubstitutionCollection(TextOptions textOptions, ShapingFeatureMap featureMap) - : base(textOptions, featureMap) - { - } - - /// - /// 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 override int Count => this.glyphs.Count; - - /// - /// Gets or sets the running id of any ligature glyphs contained withing this collection are a member of. - /// - public int LigatureId { get; set; } = 1; - - /// - /// Gets or sets the pool of retired instances a - /// reusable shaping pass rents from. Null outside buffer-managed passes. The - /// positioning collection returns its instances here at reuse-reset time; this - /// collection only ever rents. - /// - internal List? ReusePool { get; set; } - - /// - public override GlyphShapingData this[int index] - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => this.glyphs[index].Data; - } - - /// - /// Gets the shaping data at the specified position. - /// - /// The zero-based index of the elements to get. - /// The zero-based index within the input codepoint collection. - /// The . - internal GlyphShapingData GetGlyphShapingData(int index, out int offset) - { - OffsetGlyphDataPair pair = this.glyphs[index]; - offset = pair.Offset; - return pair.Data; - } - - /// - /// Resets the collection for reuse by a new shaping pass. Storage references are - /// dropped without pooling: any instance still held here was either transferred to - /// the positioning collection, which pools it, or is dead. - /// - /// The text options for the new pass. - internal void ResetForReuse(TextOptions textOptions) - { - this.glyphs.Clear(); - this.LigatureId = 1; - this.ResetCore(textOptions); - } - - /// - /// Rents a reset instance from the reuse pool, or allocates one when the pool is - /// absent or empty. - /// - /// The text run. - /// The . - private GlyphShapingData RentData(TextRun textRun) - { - List? pool = this.ReusePool; - if (pool is { Count: > 0 }) - { - GlyphShapingData data = pool[^1]; - pool.RemoveAt(pool.Count - 1); - data.Reset(textRun); - return data; - } - - return new(textRun); - } - - /// - /// Adds a clone of the glyph shaping data to the collection at the specified offset. - /// - /// The data. - /// The zero-based index within the input codepoint collection. - public void AddGlyph(GlyphShapingData data, int offset) - { - this.RecordGlyphId(data.GlyphId); - this.glyphs.Add(new(offset, new(data, false))); - } - - /// - /// Adds the glyph id and the codepoint it represents to the collection. - /// - /// The id of the glyph to add. - /// The codepoint the glyph represents. - /// The resolved text direction for the codepoint. - /// 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.RecordGlyphId(glyphId); - GlyphShapingData data = this.RentData(textRun); - data.CodePoint = codePoint; - data.Direction = direction; - data.GlyphId = glyphId; - - this.glyphs.Add(new(offset, data)); - } - - /// - /// Adds an atomic inline placeholder to the collection. - /// - /// The object replacement codepoint used for Unicode processing. - /// The resolved bidi run for the placeholder. - /// The text run this placeholder belongs to. - /// The zero-based index within the input codepoint collection. - public void AddPlaceholder(CodePoint codePoint, BidiRun bidiRun, TextRun textRun, int offset) - { - GlyphShapingData data = this.RentData(textRun); - data.CodePoint = codePoint; - data.Direction = (TextDirection)bidiRun.Direction; - data.GlyphId = 0; - data.IsPlaceholder = true; - data.BidiRun = bidiRun; - - this.glyphs.Add(new(offset, data)); - } - - /// - /// 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) - { - if (fromIndex == toIndex) - { - return; - } - - GlyphShapingData data = this[fromIndex]; - if (fromIndex > toIndex) - { - // Move item to the right - for (int i = fromIndex; i > toIndex; i--) - { - this.glyphs[i] = new(this.glyphs[i].Offset, this.glyphs[i - 1].Data); - } - } - else - { - // Move item to the left - for (int i = fromIndex; i < toIndex; i++) - { - this.glyphs[i] = new(this.glyphs[i].Offset, this.glyphs[i + 1].Data); - } - } - - this.glyphs[toIndex] = new(this.glyphs[toIndex].Offset, data); - } - - /// - /// Reverses the order of elements in the specified range of the collection. - /// - /// - /// The range is interpreted as half-open, from (inclusive) - /// to (exclusive). - /// - /// Both indices are clamped to the valid range [0, ]. - /// If the resulting range contains fewer than two elements, the method performs no action. - /// The method does not throw if either index is equal to ; in such - /// cases the range is considered valid but may be empty. - /// - /// - /// The zero-based index at which to start reversing (inclusive). This value should be - /// greater than or equal to 0. Values greater than are treated as - /// . - /// - /// - /// The zero-based index at which to stop reversing (exclusive). This value should be - /// greater than or equal to . Values greater than - /// are treated as . - /// - public void ReverseRange(int startIndex, int endIndex) - { - int s = Math.Min(startIndex, this.Count); - int e = Math.Min(endIndex, this.Count); - - if (e < s + 2) - { - return; - } - - this.glyphs.Reverse(s, e - s); - } - - /// - /// Performs a stable sort of the glyphs by the comparison delegate starting at the specified index. - /// Only the references are reordered; offsets remain in place. - /// - /// The start index. - /// The end index. - /// The comparison delegate. - public void Sort(int startIndex, int endIndex, Comparison comparer) - { - // Stable insertion sort using adjacent swaps of Data references. - // The sorted ranges are typically small (syllable clusters of 2-10 glyphs), - // so insertion sort is optimal and avoids allocations. Adjacent swaps - // replace the previous MoveGlyph approach which shifted all intermediate elements. - List glyphs = this.glyphs; - for (int i = startIndex + 1; i < endIndex; i++) - { - int j = i; - while (j > startIndex && comparer(glyphs[j - 1].Data, glyphs[j].Data) > 0) - { - // Swap Data references between adjacent slots; offsets stay in place. - OffsetGlyphDataPair upper = glyphs[j]; - OffsetGlyphDataPair lower = glyphs[j - 1]; - glyphs[j] = new(upper.Offset, lower.Data); - glyphs[j - 1] = new(lower.Offset, upper.Data); - j--; - } - } - } - - /// - /// Removes all elements from the collection. - /// - public void Clear() - { - this.glyphs.Clear(); - this.LigatureId = 1; - } - - /// - /// Gets the specified glyph ids matching the given codepoint offset. - /// - /// The zero-based index within the input codepoint collection. - /// - /// When this method returns, contains the shaping data associated with the specified offset, - /// if the value is found; otherwise, the default value for the type of the data parameter. - /// This parameter is passed uninitialized. - /// - /// - /// if the contains glyph ids - /// for the specified offset; otherwise, . - /// - public bool TryGetGlyphShapingDataAtOffset(int offset, [NotNullWhen(true)] out IReadOnlyList? data) - { - List match = []; - for (int i = 0; i < this.glyphs.Count; i++) - { - 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 = match; - return match.Count > 0; - } - - /// - /// Performs a 1:1 replacement of a glyph id at the given position. - /// - /// The zero-based index of the element to replace. - /// The replacement glyph id. - /// The feature to apply to the glyph at the specified index. - public void Replace(int index, ushort glyphId, Tag feature) - { - GlyphShapingData current = this.glyphs[index].Data; - this.RecordGlyphId(glyphId); - current.GlyphId = glyphId; - current.LigatureId = 0; - current.LigatureComponent = -1; - current.MarkAttachment = -1; - current.CursiveAttachment = -1; - current.IsSubstituted = true; - current.AppliedFeatureMask |= this.FeatureMap.GetOrAddMask(feature); - } - - /// - /// 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. - /// - /// The zero-based index of the element to replace. - /// The indices at which to remove elements. - /// The replacement glyph id. - /// The ligature id. - /// The feature to apply to the glyph at the specified index. - public void Replace(int index, ReadOnlySpan removalIndices, ushort glyphId, int ligatureId, Tag feature) - { - // Remove the glyphs at each index. - int codePointCount = 0; - CodePoint codePoint = default; - for (int i = removalIndices.Length - 1; i >= 0; i--) - { - int match = removalIndices[i]; - codePointCount += this.glyphs[match].Data.CodePointCount; - CodePoint currentCodePoint = this.glyphs[match].Data.CodePoint; - if (!UnicodeUtility.IsDefaultIgnorableCodePoint((uint)codePoint.Value) || UnicodeUtility.ShouldRenderWhiteSpaceOnly(codePoint)) - { - if (!CodePoint.IsZeroWidthJoiner(currentCodePoint) && !CodePoint.IsZeroWidthNonJoiner(currentCodePoint)) - { - codePoint = currentCodePoint; - } - } - - this.glyphs.RemoveAt(match); - } - - // Assign our new id at the index. - GlyphShapingData current = this.glyphs[index].Data; - if (codePoint != default) - { - current.CodePoint = codePoint; - } - - current.CodePointCount += codePointCount; - this.RecordGlyphId(glyphId); - current.GlyphId = glyphId; - current.LigatureId = ligatureId; - current.IsLigated = true; - current.LigatureComponent = -1; - current.MarkAttachment = -1; - current.CursiveAttachment = -1; - current.IsSubstituted = true; - current.AppliedFeatureMask |= this.FeatureMap.GetOrAddMask(feature); - } - - /// - /// 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. - /// The feature to apply to the glyph at the specified index. - public void Replace(int index, int count, ushort glyphId, Tag feature) - { - // Remove the glyphs at each index. - int codePointCount = 0; - CodePoint codePoint = default; - for (int i = count; i > 0; i--) - { - int match = index + i; - codePointCount += this.glyphs[match].Data.CodePointCount; - CodePoint currentCodePoint = this.glyphs[match].Data.CodePoint; - if (!UnicodeUtility.IsDefaultIgnorableCodePoint((uint)codePoint.Value) || UnicodeUtility.ShouldRenderWhiteSpaceOnly(codePoint)) - { - if (!CodePoint.IsZeroWidthJoiner(currentCodePoint) && !CodePoint.IsZeroWidthNonJoiner(currentCodePoint)) - { - codePoint = currentCodePoint; - } - } - - this.glyphs.RemoveAt(match); - } - - // Assign our new id at the index. - GlyphShapingData current = this.glyphs[index].Data; - if (codePoint != default) - { - current.CodePoint = codePoint; - } - - current.CodePointCount += codePointCount; - this.RecordGlyphId(glyphId); - current.GlyphId = glyphId; - current.LigatureId = 0; - current.LigatureComponent = -1; - current.MarkAttachment = -1; - current.CursiveAttachment = -1; - current.IsSubstituted = true; - current.AppliedFeatureMask |= this.FeatureMap.GetOrAddMask(feature); - } - - /// - /// 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. - /// The feature to apply to the glyph at the specified index. - public void Replace(int index, ReadOnlySpan glyphIds, Tag feature) - { - if (glyphIds.Length > 0) - { - OffsetGlyphDataPair pair = this.glyphs[index]; - GlyphShapingData current = pair.Data; - this.RecordGlyphId(glyphIds[0]); - current.GlyphId = glyphIds[0]; - current.LigatureComponent = 0; - current.MarkAttachment = -1; - current.CursiveAttachment = -1; - current.IsSubstituted = true; - current.IsDecomposed = true; - - // Add additional glyphs from the rest of the sequence. - if (glyphIds.Length > 1) - { - glyphIds = glyphIds[1..]; - for (int i = 0; i < glyphIds.Length; i++) - { - GlyphShapingData data = new(current, false) - { - GlyphId = glyphIds[i], - LigatureComponent = i + 1 - }; - - this.RecordGlyphId(glyphIds[i]); - - data.AppliedFeatureMask |= this.FeatureMap.GetOrAddMask(feature); - - this.glyphs.Insert(++index, new(pair.Offset, data)); - } - } - } - 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); - } - } - - public void Insert(int index, GlyphShapingData data) - { - OffsetGlyphDataPair pair = this.glyphs[index]; - this.glyphs.Insert(index, new(pair.Offset, data)); - } - - [DebuggerDisplay("{DebuggerDisplay,nq}")] - private readonly struct OffsetGlyphDataPair - { - public OffsetGlyphDataPair(int offset, GlyphShapingData data) - { - this.Offset = offset; - this.Data = data; - } - - public int Offset { get; } - - public GlyphShapingData Data { get; } - - private string DebuggerDisplay => FormattableString.Invariant($"Offset: {this.Offset}, Data: {this.Data.ToDebuggerDisplay()}"); - } -} diff --git a/src/SixLabors.Fonts/MemoryFontMetrics.cs b/src/SixLabors.Fonts/MemoryFontMetrics.cs index dc83badc3..055b447bb 100644 --- a/src/SixLabors.Fonts/MemoryFontMetrics.cs +++ b/src/SixLabors.Fonts/MemoryFontMetrics.cs @@ -201,16 +201,16 @@ internal override bool TryGetBaselineCoordinate(Tag baselineTag, bool isVertical => this.fontMetrics.Value.TryGetBaselineCoordinate(baselineTag, isVerticalLayout, out coordinate); /// - internal override void ApplySubstitution(GlyphSubstitutionCollection collection) - => this.fontMetrics.Value.ApplySubstitution(collection); + internal override void ApplySubstitution(ShapingBuffer buffer) + => this.fontMetrics.Value.ApplySubstitution(buffer); /// internal override bool TryGetKerningOffset(ushort currentId, ushort nextId, out Vector2 vector) => this.fontMetrics.Value.TryGetKerningOffset(currentId, nextId, out vector); /// - internal override void UpdatePositions(GlyphPositioningCollection collection) - => this.fontMetrics.Value.UpdatePositions(collection); + internal override void UpdatePositions(ShapingBuffer buffer) + => this.fontMetrics.Value.UpdatePositions(buffer); /// internal override float GetGDefVariationDelta(uint packedVariationIndex) @@ -221,7 +221,7 @@ internal override ReadOnlySpan GetNormalizedCoordinates() => this.fontMetrics.Value.GetNormalizedCoordinates(); /// - /// Reads a collection of instances from the specified stream. + /// Reads a buffer of instances from the specified stream. /// /// The source stream. /// A read-only memory region containing the font metrics. diff --git a/src/SixLabors.Fonts/ShapingBuffer.cs b/src/SixLabors.Fonts/ShapingBuffer.cs new file mode 100644 index 000000000..8e5e36c0c --- /dev/null +++ b/src/SixLabors.Fonts/ShapingBuffer.cs @@ -0,0 +1,949 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.Fonts.Tables.AdvancedTypographic; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts; + +/// +/// The shaping pipeline's glyph buffer. Glyph state lives in one flat array of +/// records with a parallel metrics stream seeded after +/// substitution, both mutated in place through interior references. Storage doubles on +/// demand, is truncated rather than released on reset, and so stays at the workload's +/// high-water mark across pooled shaping passes. +/// +/// +/// Mutation contract: write through the indexer expression +/// (buffer[i].GlyphId = x), which addresses storage directly. Binding an element +/// to a local without copies it and silently discards writes; a +/// convention test rejects such bindings. Interior references are invalidated by any +/// operation that inserts or removes glyphs. +/// +internal sealed class ShapingBuffer +{ + /// + /// The flat glyph storage. Only the first records are live; + /// records beyond the count are stale leftovers awaiting overwrite. + /// + private GlyphShapingData[] data = new GlyphShapingData[64]; + + /// + /// The metrics stream, parallel to . Entries are seeded by + /// and ; substitution-phase buffers + /// never populate it. + /// + private GlyphMetricsEntry[] metrics = new GlyphMetricsEntry[64]; + + /// + /// The live record count. + /// + private int count; + + /// + /// The approximate membership filter over every glyph id the buffer has ever + /// contained. See for the growth contract. + /// + private GlyphSetDigest glyphDigest; + + /// + /// Initializes a new instance of the class. + /// + /// The text options. + /// The feature bit assignment shared by the shaping pass. + /// The shaping phase this buffer serves. + public ShapingBuffer(TextOptions textOptions, ShapingFeatureMap featureMap, ShapingBufferRole role) + { + this.TextOptions = textOptions; + this.FeatureMap = featureMap; + this.Role = role; + this.LanguageTags = ResolveLanguageTags(textOptions); + } + + /// + /// Gets the shaping phase this buffer serves. Shapers gate phase-specific work, + /// such as syllable analysis and reordering, on the substitution role. + /// + public ShapingBufferRole Role { get; } + + /// + /// Gets the number of live glyph records. Substitution can leave this greater or + /// smaller than the input codepoint count. + /// + public int Count => this.count; + + /// + /// Gets the text options used by this buffer. + /// + public TextOptions TextOptions { get; private set; } + + /// + /// Gets the candidate OpenType language system tags resolved from + /// , most specific first, or an empty array when + /// the culture expresses no language preference. Resolved once per shaping pass. + /// + public Tag[] LanguageTags { get; private set; } + + /// + /// Gets the feature bit assignment shared by every buffer of the shaping pass. + /// See for the mask model and why the instance must + /// be shared across the substitution and positioning phases. + /// + public ShapingFeatureMap FeatureMap { get; } + + /// + /// Gets the approximate membership filter over every glyph id the buffer has ever + /// contained. The digest only grows: substituted-away ids remain, keeping a + /// definitive negative from sound while + /// lookups mutate the buffer mid-application. + /// + public GlyphSetDigest GlyphDigest => this.glyphDigest; + + /// + /// Gets or sets the running id of any ligature glyphs contained within this buffer. + /// + public int LigatureId { get; set; } = 1; + + /// + /// Gets an interior reference to the glyph shaping data at the specified index. + /// The reference writes through to the buffer's storage and is invalidated by any + /// operation that inserts or removes glyphs. + /// + /// The zero-based index of the record to get. + /// The . + public ref GlyphShapingData this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref this.data[index]; + } + + /// + /// Gets an interior reference to the metrics entry at the specified index. Valid + /// only after the buffer has been seeded by . + /// + /// The zero-based index of the entry to get. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ref GlyphMetricsEntry MetricsAt(int index) => ref this.metrics[index]; + + /// + /// Resets the buffer for reuse by a new shaping pass: adopts the new options, + /// re-resolves the language candidates, empties the digest, and truncates the glyph + /// count. Records are stored by value, so no per-record cleanup is required and + /// storage is retained at its high-water mark. + /// + /// The text options for the new pass. + public void Reset(TextOptions textOptions) + { + this.count = 0; + this.LigatureId = 1; + this.glyphDigest = default; + this.TextOptions = textOptions; + this.LanguageTags = ResolveLanguageTags(textOptions); + } + + /// + /// Removes all glyph records while keeping the pass-wide state, so a fresh font run + /// can populate the buffer without re-resolving options or language tags. + /// + public void Clear() + { + this.count = 0; + this.LigatureId = 1; + } + + /// + /// Sets the glyph id at the specified index, recording the id in + /// . Callers outside the buffer must use this rather than + /// writing directly, which would leave the + /// digest unaware of the new id. + /// + /// The zero-based index of the record. + /// The glyph id to set. + public void SetGlyphId(int index, ushort glyphId) + { + this.glyphDigest.Add(glyphId); + this.data[index].GlyphId = glyphId; + } + + /// + /// Adds the shaping feature to the record at the given index. + /// + /// + /// Registration only ever accumulates: adding a disabled entry for an already + /// enabled feature must not clear the enabled bit. + /// + /// The zero-based index of the record. + /// The feature to apply. + public void AddShapingFeature(int index, TagEntry feature) + { + ulong mask = this.FeatureMap.GetOrAddMask(feature.Tag); + ref GlyphShapingData item = ref this.data[index]; + item.RegisteredFeatureMask |= mask; + if (feature.Enabled) + { + item.FeatureMask |= mask; + } + } + + /// + /// Adds the shaping feature to every record in the given range, resolving the + /// feature's mask bit once for the whole range. Shaper plans register each stage + /// feature across the full run, so the per-glyph work must be a single bitwise OR. + /// + /// The zero-based index of the first record. + /// The number of records in the range. + /// The feature to apply. + public void AddShapingFeatureRange(int index, int count, TagEntry feature) + { + ulong mask = this.FeatureMap.GetOrAddMask(feature.Tag); + int end = index + count; + for (int i = index; i < end; i++) + { + ref GlyphShapingData item = ref this.data[i]; + item.RegisteredFeatureMask |= mask; + if (feature.Enabled) + { + item.FeatureMask |= mask; + } + } + } + + /// + /// Enables a previously added shaping feature. + /// + /// + /// Intersecting with the registered mask preserves the contract that enabling a + /// feature a shaper never added for this record is a no-op. + /// + /// The zero-based index of the record. + /// The feature to enable. + public void EnableShapingFeature(int index, Tag feature) + { + ref GlyphShapingData item = ref this.data[index]; + item.FeatureMask |= item.RegisteredFeatureMask & this.FeatureMap.GetMask(feature); + } + + /// + /// Disables a previously added shaping feature. + /// + /// + /// An unregistered tag yields a zero mask whose complement clears nothing. + /// + /// The zero-based index of the record. + /// The feature to disable. + public void DisableShapingFeature(int index, Tag feature) + { + ref GlyphShapingData item = ref this.data[index]; + item.FeatureMask &= ~this.FeatureMap.GetMask(feature); + } + + /// + /// Adds a copy of the glyph shaping data at the specified codepoint offset. + /// + /// The data to copy. + /// The zero-based index within the input codepoint buffer. + public void AddGlyph(GlyphShapingData data, int offset) + { + this.glyphDigest.Add(data.GlyphId); + ref GlyphShapingData slot = ref this.Append(); + slot = new(data, false); + slot.CodePointIndex = offset; + } + + /// + /// Adds the glyph id and the codepoint it represents. + /// + /// The id of the glyph to add. + /// The codepoint the glyph represents. + /// The resolved text direction for the codepoint. + /// The text run this glyph belongs to. + /// The zero-based index within the input codepoint buffer. + public void AddGlyph(ushort glyphId, CodePoint codePoint, TextDirection direction, TextRun textRun, int offset) + { + this.glyphDigest.Add(glyphId); + ref GlyphShapingData slot = ref this.Append(); + slot = new(textRun) + { + CodePointIndex = offset, + CodePoint = codePoint, + Direction = direction, + GlyphId = glyphId, + }; + } + + /// + /// Adds an atomic inline placeholder. + /// + /// The object replacement codepoint used for Unicode processing. + /// The resolved bidi run for the placeholder. + /// The text run this placeholder belongs to. + /// The zero-based index within the input codepoint buffer. + public void AddPlaceholder(CodePoint codePoint, BidiRun bidiRun, TextRun textRun, int offset) + { + ref GlyphShapingData slot = ref this.Append(); + slot = new(textRun) + { + CodePointIndex = offset, + CodePoint = codePoint, + Direction = (TextDirection)bidiRun.Direction, + GlyphId = 0, + IsPlaceholder = true, + BidiRun = bidiRun, + }; + } + + /// + /// Moves the specified glyph to the specified position. Codepoint offsets stay + /// bound to their slots: only the shaping state travels. + /// + /// The index to move from. + /// The index to move to. + public void MoveGlyph(int fromIndex, int toIndex) + { + if (fromIndex == toIndex) + { + return; + } + + GlyphShapingData[] items = this.data; + GlyphShapingData moved = items[fromIndex]; + int targetOffset = items[toIndex].CodePointIndex; + + if (fromIndex > toIndex) + { + // Move item to the right + for (int i = fromIndex; i > toIndex; i--) + { + int keep = items[i].CodePointIndex; + items[i] = items[i - 1]; + items[i].CodePointIndex = keep; + } + } + else + { + // Move item to the left + for (int i = fromIndex; i < toIndex; i++) + { + int keep = items[i].CodePointIndex; + items[i] = items[i + 1]; + items[i].CodePointIndex = keep; + } + } + + items[toIndex] = moved; + items[toIndex].CodePointIndex = targetOffset; + } + + /// + /// Reverses the order of glyph records in the specified range. + /// + /// + /// The range is interpreted as half-open, from + /// (inclusive) to (exclusive). Both indices are clamped + /// to the valid range [0, ]. If the resulting range contains + /// fewer than two records, the method performs no action. + /// + /// The zero-based index at which to start reversing (inclusive). + /// The zero-based index at which to stop reversing (exclusive). + public void ReverseRange(int startIndex, int endIndex) + { + int s = Math.Min(startIndex, this.count); + int e = Math.Min(endIndex, this.count); + + if (e < s + 2) + { + return; + } + + Array.Reverse(this.data, s, e - s); + } + + /// + /// Performs a stable sort of the glyph records by the comparison delegate. + /// Codepoint offsets stay bound to their slots: only the shaping state is + /// reordered. + /// + /// The start index. + /// The end index. + /// The comparison delegate. + public void Sort(int startIndex, int endIndex, Comparison comparer) + { + // Stable insertion sort using adjacent swaps. The sorted ranges are typically + // small (syllable clusters of 2-10 glyphs), so insertion sort is optimal and + // avoids allocations. + GlyphShapingData[] items = this.data; + for (int i = startIndex + 1; i < endIndex; i++) + { + int j = i; + while (j > startIndex && comparer(items[j - 1], items[j]) > 0) + { + // Swap the records, then swap the offsets back so they keep their slots. + (items[j], items[j - 1]) = (items[j - 1], items[j]); + (items[j].CodePointIndex, items[j - 1].CodePointIndex) = (items[j - 1].CodePointIndex, items[j].CodePointIndex); + j--; + } + } + } + + /// + /// Gets the glyph records matching the given codepoint offset as copies. + /// + /// The zero-based index within the input codepoint buffer. + /// + /// When this method returns, contains copies of the records associated with the + /// specified offset, if any were found. + /// + /// + /// if the buffer contains records for the specified offset; + /// otherwise, . + /// + public bool TryGetGlyphShapingDataAtOffset(int offset, [NotNullWhen(true)] out IReadOnlyList? data) + { + List match = []; + for (int i = 0; i < this.count; i++) + { + if (this.data[i].CodePointIndex == offset) + { + match.Add(this.data[i]); + } + else if (match.Count > 0) + { + // Offsets, though non-sequential, are sorted, so we can stop searching. + break; + } + } + + data = match; + return match.Count > 0; + } + + /// + /// Performs a 1:1 replacement of a glyph id at the given position. + /// + /// The zero-based index of the record to replace. + /// The replacement glyph id. + /// The feature to apply to the record at the specified index. + public void Replace(int index, ushort glyphId, Tag feature) + { + this.glyphDigest.Add(glyphId); + ref GlyphShapingData current = ref this.data[index]; + current.GlyphId = glyphId; + current.LigatureId = 0; + current.LigatureComponent = -1; + current.MarkAttachment = -1; + current.CursiveAttachment = -1; + current.IsSubstituted = true; + current.AppliedFeatureMask |= this.FeatureMap.GetOrAddMask(feature); + } + + /// + /// Performs a 1:1 replacement of a glyph id at the given position while removing a + /// series of records at the given positions within the sequence. + /// + /// The zero-based index of the record to replace. + /// The indices at which to remove records. + /// The replacement glyph id. + /// The ligature id. + /// The feature to apply to the record at the specified index. + public void Replace(int index, ReadOnlySpan removalIndices, ushort glyphId, int ligatureId, Tag feature) + { + // Remove the glyphs at each index. + int codePointCount = 0; + CodePoint codePoint = default; + for (int i = removalIndices.Length - 1; i >= 0; i--) + { + int match = removalIndices[i]; + codePointCount += this.data[match].CodePointCount; + CodePoint currentCodePoint = this.data[match].CodePoint; + if (!UnicodeUtility.IsDefaultIgnorableCodePoint((uint)codePoint.Value) || UnicodeUtility.ShouldRenderWhiteSpaceOnly(codePoint)) + { + if (!CodePoint.IsZeroWidthJoiner(currentCodePoint) && !CodePoint.IsZeroWidthNonJoiner(currentCodePoint)) + { + codePoint = currentCodePoint; + } + } + + this.RemoveAt(match); + } + + // Assign our new id at the index. The reference is taken after every removal + // so it addresses the record's final slot. + this.glyphDigest.Add(glyphId); + ref GlyphShapingData current = ref this.data[index]; + if (codePoint != default) + { + current.CodePoint = codePoint; + } + + current.CodePointCount += codePointCount; + current.GlyphId = glyphId; + current.LigatureId = ligatureId; + current.IsLigated = true; + current.LigatureComponent = -1; + current.MarkAttachment = -1; + current.CursiveAttachment = -1; + current.IsSubstituted = true; + current.AppliedFeatureMask |= this.FeatureMap.GetOrAddMask(feature); + } + + /// + /// Performs a 1:1 replacement of a glyph id at the given position while removing a + /// series of following records. + /// + /// The zero-based index of the record to replace. + /// The number of following records to remove. + /// The replacement glyph id. + /// The feature to apply to the record at the specified index. + public void Replace(int index, int count, ushort glyphId, Tag feature) + { + // Remove the glyphs at each index. + int codePointCount = 0; + CodePoint codePoint = default; + for (int i = count; i > 0; i--) + { + int match = index + i; + codePointCount += this.data[match].CodePointCount; + CodePoint currentCodePoint = this.data[match].CodePoint; + if (!UnicodeUtility.IsDefaultIgnorableCodePoint((uint)codePoint.Value) || UnicodeUtility.ShouldRenderWhiteSpaceOnly(codePoint)) + { + if (!CodePoint.IsZeroWidthJoiner(currentCodePoint) && !CodePoint.IsZeroWidthNonJoiner(currentCodePoint)) + { + codePoint = currentCodePoint; + } + } + + this.RemoveAt(match); + } + + // Assign our new id at the index. The reference is taken after every removal + // so it addresses the record's final slot. + this.glyphDigest.Add(glyphId); + ref GlyphShapingData current = ref this.data[index]; + if (codePoint != default) + { + current.CodePoint = codePoint; + } + + current.CodePointCount += codePointCount; + current.GlyphId = glyphId; + current.LigatureId = 0; + current.LigatureComponent = -1; + current.MarkAttachment = -1; + current.CursiveAttachment = -1; + current.IsSubstituted = true; + current.AppliedFeatureMask |= this.FeatureMap.GetOrAddMask(feature); + } + + /// + /// Replaces a single glyph id with a buffer of glyph ids. + /// + /// The zero-based index of the record to replace. + /// The buffer of replacement glyph ids. + /// The feature to apply to the record at the specified index. + public void Replace(int index, ReadOnlySpan glyphIds, Tag feature) + { + if (glyphIds.Length > 0) + { + this.glyphDigest.Add(glyphIds[0]); + this.data[index].GlyphId = glyphIds[0]; + this.data[index].LigatureComponent = 0; + this.data[index].MarkAttachment = -1; + this.data[index].CursiveAttachment = -1; + this.data[index].IsSubstituted = true; + this.data[index].IsDecomposed = true; + + // Add additional glyphs from the rest of the sequence. Insertion can grow + // and shift the storage, so the mutated record is captured by value as the + // template for the additions rather than held by reference. + if (glyphIds.Length > 1) + { + GlyphShapingData template = this.data[index]; + ulong mask = this.FeatureMap.GetOrAddMask(feature); + for (int i = 1; i < glyphIds.Length; i++) + { + GlyphShapingData inserted = new(template, false) + { + GlyphId = glyphIds[i], + LigatureComponent = i, + }; + + inserted.AppliedFeatureMask |= mask; + this.glyphDigest.Add(glyphIds[i]); + this.InsertAt(++index, inserted); + } + } + } + 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.RemoveAt(index); + } + } + + /// + /// Inserts the shaping data at the given index, adopting the slot's codepoint offset. + /// + /// The zero-based index at which to insert. + /// The shaping data to insert. + public void Insert(int index, GlyphShapingData data) + { + data.CodePointIndex = this.data[index].CodePointIndex; + this.InsertAt(index, data); + } + + /// + /// Seeds this buffer from a substituted workspace buffer: fetches each glyph's + /// metrics from , seeds the record's shaping bounds with the + /// single-axis advance so positioning starts from clean dirty-tracking, and appends + /// record and metrics entry. Placeholders receive synthetic metrics and skip glyph + /// lookup entirely. + /// + /// The font used to resolve metrics. + /// The substituted workspace buffer. + /// + /// when every mapped codepoint resolved a real glyph; + /// when fallback glyphs remain for a later font pass. + /// + public bool TryAdd(Font font, ShapingBuffer workspace) + { + bool hasFallBacks = false; + FontMetrics fontMetrics = font.FontMetrics; + LayoutMode layoutMode = this.TextOptions.LayoutMode; + ColorFontSupport colorFontSupport = this.TextOptions.ColorFontSupport; + + ulong verticalMask = this.GetVerticalFeatureMask(); + + for (int i = 0; i < workspace.count; i++) + { + ref GlyphShapingData source = ref workspace.data[i]; + CodePoint codePoint = source.CodePoint; + ushort id = source.GlyphId; + + if (source.IsPlaceholder) + { + // Placeholders are synthetic glyphs: they need layout metrics but must not + // go through font glyph lookup, fallback resolution, or GPOS positioning. + FontGlyphMetrics placeholderMetrics = PlaceholderGlyphMetrics.Create(font, source.TextRun, this.TextOptions.Dpi); + + this.glyphDigest.Add(placeholderMetrics.GlyphId); + ref GlyphShapingData placeholderSlot = ref this.Append(); + placeholderSlot = source; + placeholderSlot.ClearFeatures(); + placeholderSlot.Bounds = layoutMode.IsVertical() + ? new(0, 0, 0, placeholderMetrics.AdvanceHeight) + : new(0, 0, placeholderMetrics.AdvanceWidth, 0); + placeholderSlot.IsPositioned = true; + + this.metrics[this.count - 1] = new(font, font.Size, placeholderMetrics); + continue; + } + + TextAttributes textAttributes = source.TextRun.TextAttributes; + TextDecorations textDecorations = source.TextRun.TextDecorations; + + bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(codePoint, layoutMode) + || (source.AppliedFeatureMask & verticalMask) != 0; + + FontGlyphMetrics glyphMetrics = fontMetrics.GetGlyphMetrics(codePoint, id, textAttributes, textDecorations, layoutMode, colorFontSupport); + + if (glyphMetrics.GlyphType == GlyphType.Fallback && !CodePoint.IsControl(codePoint)) + { + hasFallBacks = true; + } + + // We only want a single dimensional advance for positioning; assigning a + // fresh bounds value starts dirty tracking clean for GPOS. + this.glyphDigest.Add(glyphMetrics.GlyphId); + ref GlyphShapingData slot = ref this.Append(); + slot = source; + slot.ClearFeatures(); + slot.Bounds = isVertical + ? new(0, 0, 0, glyphMetrics.AdvanceHeight) + : new(0, 0, glyphMetrics.AdvanceWidth, 0); + + this.metrics[this.count - 1] = new(font, font.Size, glyphMetrics); + } + + return !hasFallBacks; + } + + /// + /// Replaces fallback glyphs in this buffer with glyphs shaped by a fallback font: + /// records whose metrics resolved to real glyphs in + /// supersede the fallback records at the same codepoint offset. + /// + /// The fallback font used to resolve metrics. + /// The substituted workspace buffer for the fallback font. + /// + /// when no fallback glyphs remain; + /// when further font passes are required. + /// + public bool TryUpdate(Font font, ShapingBuffer workspace) + { + FontMetrics fontMetrics = font.FontMetrics; + LayoutMode layoutMode = this.TextOptions.LayoutMode; + ColorFontSupport colorFontSupport = this.TextOptions.ColorFontSupport; + bool hasFallBacks = false; + + ulong verticalMask = this.GetVerticalFeatureMask(); + + for (int i = 0; i < this.count; i++) + { + if (this.metrics[i].Metrics.GlyphType != GlyphType.Fallback) + { + // We've already got the correct glyph. + continue; + } + + int offset = this.data[i].CodePointIndex; + float pointSize = this.metrics[i].PointSize; + if (workspace.TryGetGlyphShapingDataAtOffset(offset, out IReadOnlyList? replacements)) + { + int replacementCount = 0; + for (int j = 0; j < replacements.Count; j++) + { + GlyphShapingData shape = replacements[j]; + ushort id = shape.GlyphId; + CodePoint codePoint = shape.CodePoint; + + TextAttributes textAttributes = shape.TextRun.TextAttributes; + TextDecorations textDecorations = shape.TextRun.TextDecorations; + + bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(codePoint, layoutMode) + || (shape.AppliedFeatureMask & verticalMask) != 0; + + FontGlyphMetrics glyphMetrics = fontMetrics.GetGlyphMetrics(codePoint, id, textAttributes, textDecorations, layoutMode, colorFontSupport); + + // If the glyphs are fallbacks we don't want them as + // we've already captured them on the first run. + if (glyphMetrics.GlyphType == GlyphType.Fallback && !CodePoint.IsControl(codePoint)) + { + hasFallBacks = true; + continue; + } + + if (replacementCount == 0) + { + // There should only be a single fallback glyph at this position + // from the previous buffer. + this.RemoveAt(i); + } + + // Track the number of inserted glyphs at the offset so we can + // correctly increment our position. + shape.CodePointIndex = offset; + shape.ClearFeatures(); + shape.Bounds = isVertical + ? new(0, 0, 0, glyphMetrics.AdvanceHeight) + : new(0, 0, glyphMetrics.AdvanceWidth, 0); + + this.glyphDigest.Add(glyphMetrics.GlyphId); + this.InsertAt(i + replacementCount, shape, new(font, pointSize, glyphMetrics)); + replacementCount++; + } + + if (replacementCount > 0) + { + i += replacementCount - 1; + } + } + } + + return !hasFallBacks; + } + + /// + /// Marks the glyph at the specified index as positioned. Positions accumulate in + /// the record's shaping bounds and are read from there by consumers, so the shared + /// metrics instance is never mutated. + /// + /// The zero-based index of the record. + public void UpdatePosition(int index) => this.data[index].IsPositioned = true; + + /// + /// Adds dx and dy to the positioned advance of the glyph at the given index and id. + /// Advances accumulate in the record's shaping bounds so the shared metrics + /// instance is never mutated. + /// + /// The font face with metrics. + /// The zero-based index of the record. + /// The id of the glyph to offset. + /// The delta x-advance. + /// The delta y-advance. + public void Advance(FontMetrics fontMetrics, int index, ushort glyphId, short dx, short dy) + { + FontGlyphMetrics m = this.metrics[index].Metrics; + if (m.GlyphId != glyphId || fontMetrics != m.FontMetrics) + { + return; + } + + bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(m.CodePoint, this.TextOptions.LayoutMode) + || (this.data[index].AppliedFeatureMask & this.GetVerticalFeatureMask()) != 0; + + // Advance heights grow downward but font-space grows upward, hence the negation. + this.data[index].Bounds.Width += dx; + if (isVertical) + { + this.data[index].Bounds.Height -= dy; + } + } + + /// + /// Returns a value indicating whether the record at the given index should be + /// processed by the given font's positioning pass. + /// + /// The font face with metrics. + /// The zero-based index of the record. + /// if the record should be processed. + public bool ShouldProcess(FontMetrics fontMetrics, int index) + => !this.data[index].IsPositioned && this.metrics[index].Metrics.FontMetrics == fontMetrics; + + /// + /// Gets the combined mask of the three vertical alternate features. Computed from + /// the shared feature map so it stays valid for applied bits written during + /// substitution and read after the seed into the positioning phase. + /// + /// The combined mask, or zero when no vertical feature was registered. + internal ulong GetVerticalFeatureMask() + => this.FeatureMap.GetMask(KnownFeatureTags.VerticalAlternates) + | this.FeatureMap.GetMask(KnownFeatureTags.VerticalAlternatesForRotation) + | this.FeatureMap.GetMask(KnownFeatureTags.VerticalKerning); + + /// + /// Resolves the candidate OpenType language system tags for the options' culture. + /// A null culture takes the ambient current culture; the invariant culture + /// expresses no language preference. + /// + /// The text options. + /// The candidate tags, most specific first. + private static Tag[] ResolveLanguageTags(TextOptions textOptions) + { + CultureInfo culture = textOptions.Culture ?? CultureInfo.CurrentCulture; + return OpenTypeLanguageTagMap.TryGetTags(culture, out Tag[] tags) ? tags : []; + } + + /// + /// Appends one record and returns an interior reference to it. The slot may hold a + /// stale record from an earlier pass; callers overwrite it entirely. The metrics + /// slot is grown in lockstep but left untouched. + /// + /// The appended record. + private ref GlyphShapingData Append() + { + if (this.count == this.data.Length) + { + Array.Resize(ref this.data, this.data.Length * 2); + Array.Resize(ref this.metrics, this.metrics.Length * 2); + } + + return ref this.data[this.count++]; + } + + /// + /// Inserts one record at the given index, shifting later records right. + /// + /// The zero-based index at which to insert. + /// The record to insert. + private void InsertAt(int index, GlyphShapingData item) + { + this.InsertAt(index, item, default); + } + + /// + /// Inserts one record and its metrics entry at the given index, shifting later + /// entries in both streams right. + /// + /// The zero-based index at which to insert. + /// The record to insert. + /// The metrics entry to insert. + private void InsertAt(int index, GlyphShapingData item, GlyphMetricsEntry metricsEntry) + { + if (this.count == this.data.Length) + { + Array.Resize(ref this.data, this.data.Length * 2); + Array.Resize(ref this.metrics, this.metrics.Length * 2); + } + + Array.Copy(this.data, index, this.data, index + 1, this.count - index); + Array.Copy(this.metrics, index, this.metrics, index + 1, this.count - index); + this.data[index] = item; + this.metrics[index] = metricsEntry; + this.count++; + } + + /// + /// Removes the record and metrics entry at the given index, shifting later entries + /// left. Stale entries beyond the count are overwritten by later appends. + /// + /// The zero-based index to remove at. + private void RemoveAt(int index) + { + Array.Copy(this.data, index + 1, this.data, index, this.count - index - 1); + Array.Copy(this.metrics, index + 1, this.metrics, index, this.count - index - 1); + this.count--; + } + +#pragma warning disable SA1401 // Fields exposed so callers can take interior references into buffer storage. + /// + /// One glyph's metrics-phase state: the resolving font, its point size, and the + /// resolved metrics instance. Stored in a stream parallel to the glyph records. + /// + public struct GlyphMetricsEntry + { + /// + /// The font that resolved the glyph. + /// + public Font Font; + + /// + /// The font size in PT units of the font containing this glyph. + /// + public float PointSize; + + /// + /// The font glyph metrics. + /// + public FontGlyphMetrics Metrics; + + /// + /// Initializes a new instance of the struct. + /// + /// The font that resolved the glyph. + /// The font size in PT units. + /// The font glyph metrics. + public GlyphMetricsEntry(Font font, float pointSize, FontGlyphMetrics metrics) + { + this.Font = font; + this.PointSize = pointSize; + this.Metrics = metrics; + } + + /// + /// Gets the positioned horizontal advance in font design units for the paired + /// record: the shaping bounds value once positioning has written one, otherwise + /// the metrics advance. + /// + /// The paired glyph record. + /// The advance. + public readonly ushort GetAdvanceWidth(in GlyphShapingData data) + => data.Bounds.IsDirtyWH ? (ushort)data.Bounds.Width : this.Metrics.AdvanceWidth; + + /// + /// Gets the positioned vertical advance in font design units for the paired + /// record: the shaping bounds value once positioning has written one, otherwise + /// the metrics advance. + /// + /// The paired glyph record. + /// The advance. + public readonly ushort GetAdvanceHeight(in GlyphShapingData data) + => data.Bounds.IsDirtyWH ? (ushort)data.Bounds.Height : this.Metrics.AdvanceHeight; + } +#pragma warning restore SA1401 +} diff --git a/src/SixLabors.Fonts/ShapingBufferRole.cs b/src/SixLabors.Fonts/ShapingBufferRole.cs new file mode 100644 index 000000000..657d4b134 --- /dev/null +++ b/src/SixLabors.Fonts/ShapingBufferRole.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts; + +/// +/// Identifies which phase of the shaping pass a serves. +/// Shapers use the role to run phase-specific work: syllable analysis and reordering +/// belong to substitution and must not run again during positioning. +/// +internal enum ShapingBufferRole +{ + /// + /// The per-font-run workspace buffer glyphs are substituted in. + /// + Substitution, + + /// + /// The accumulated result buffer glyphs are seeded and positioned in. + /// + Positioning, +} diff --git a/src/SixLabors.Fonts/ShapingScratch.cs b/src/SixLabors.Fonts/ShapingScratch.cs index c6e29d8d0..17cbc22ee 100644 --- a/src/SixLabors.Fonts/ShapingScratch.cs +++ b/src/SixLabors.Fonts/ShapingScratch.cs @@ -6,10 +6,10 @@ namespace SixLabors.Fonts; /// -/// Reusable shaping pipeline state: the substitution and positioning collections, their -/// shared feature map, and the pool of retired glyph data instances. Storage grows to -/// the workload's high-water mark and is reused across calls, so steady-state shaping -/// performs no per-call allocation for pipeline state. +/// Reusable shaping pipeline state: the per-font-run workspace buffer, the accumulated +/// result buffer, and their shared feature map. Buffer storage grows to the workload's +/// high-water mark and is reused across calls, so steady-state shaping performs no +/// per-call allocation for pipeline state. /// /// /// A scratch is exclusively owned by one shaping call at a time, enforced by @@ -19,47 +19,41 @@ namespace SixLabors.Fonts; /// internal sealed class ShapingScratch { - /// The pass-wide feature bit assignment, reset per call. + /// + /// The pass-wide feature bit assignment, reset per call. + /// private ShapingFeatureMap? featureMap; - /// The reusable substitution collection. - private GlyphSubstitutionCollection? substitutions; - - /// The reusable positioning collection. - private GlyphPositioningCollection? positionings; + /// + /// The per-font-run workspace buffer glyphs are substituted in. + /// + private ShapingBuffer? workspace; /// - /// Retired instances awaiting reuse. Filled from the - /// positioning collection at reset time, drained by the substitution collection as - /// glyphs are added. + /// The accumulated result buffer glyphs are seeded and positioned in. /// - private readonly List dataPool = []; + private ShapingBuffer? result; /// - /// Gets the reusable shaping collections, reset for a new pass over the given - /// options. The positioning collection's retired glyph data instances are returned - /// to the pool before the substitution collection begins renting. + /// Gets the reusable shaping buffers, reset for a new pass over the given options. /// /// The text options for the pass. - /// The reusable collections, sharing one feature map. - internal (GlyphSubstitutionCollection Substitutions, GlyphPositioningCollection Positionings) Prepare(TextOptions options) + /// The reusable buffers, sharing one feature map. + internal (ShapingBuffer Workspace, ShapingBuffer Result) Prepare(TextOptions options) { if (this.featureMap is null) { this.featureMap = new(); - this.substitutions = new(options, this.featureMap) - { - ReusePool = this.dataPool, - }; - this.positionings = new(options, this.featureMap); + this.workspace = new(options, this.featureMap, ShapingBufferRole.Substitution); + this.result = new(options, this.featureMap, ShapingBufferRole.Positioning); } else { this.featureMap.Reset(); - this.positionings!.ResetForReuse(options, this.dataPool); - this.substitutions!.ResetForReuse(options); + this.workspace!.Reset(options); + this.result!.Reset(options); } - return (this.substitutions!, this.positionings!); + return (this.workspace!, this.result!); } } diff --git a/src/SixLabors.Fonts/StreamFontMetrics.cs b/src/SixLabors.Fonts/StreamFontMetrics.cs index bcd197f41..5cad64de1 100644 --- a/src/SixLabors.Fonts/StreamFontMetrics.cs +++ b/src/SixLabors.Fonts/StreamFontMetrics.cs @@ -451,11 +451,11 @@ internal override bool TryGetBaselineCoordinate(Tag baselineTag, bool isVertical } /// - internal override void ApplySubstitution(GlyphSubstitutionCollection collection) + internal override void ApplySubstitution(ShapingBuffer buffer) { if (this.TryGetGSubTable(out GSubTable? gSubTable)) { - gSubTable.ApplySubstitution(this, collection); + gSubTable.ApplySubstitution(this, buffer); } } @@ -477,7 +477,7 @@ internal override bool TryGetKerningOffset(ushort currentId, ushort nextId, out } /// - internal override void UpdatePositions(GlyphPositioningCollection collection) + internal override void UpdatePositions(ShapingBuffer buffer) { bool isTTF = this.outlineType == OutlineType.TrueType; GPosTable? gpos = isTTF @@ -485,9 +485,9 @@ internal override void UpdatePositions(GlyphPositioningCollection collection) : this.compactFontTables!.GPos; bool kerned = false; - KerningMode kerningMode = collection.TextOptions.KerningMode; + KerningMode kerningMode = buffer.TextOptions.KerningMode; - gpos?.TryUpdatePositions(this, collection, out kerned); + gpos?.TryUpdatePositions(this, buffer, out kerned); // TODO: I don't think we should disable kerning here. if (!kerned && kerningMode != KerningMode.None) @@ -499,15 +499,15 @@ internal override void UpdatePositions(GlyphPositioningCollection collection) if (kern?.Count > 0) { // Set max constraints to prevent OutOfMemoryException or infinite loops from attacks. - int maxCount = AdvancedTypographicUtils.GetMaxAllowableShapingCollectionCount(collection.Count); - for (int index = 0; index < collection.Count - 1; index++) + int maxCount = AdvancedTypographicUtils.GetMaxAllowableShapingCollectionCount(buffer.Count); + for (int index = 0; index < buffer.Count - 1; index++) { if (index >= maxCount) { break; } - kern.UpdatePositions(this, collection, index, index + 1); + kern.UpdatePositions(this, buffer, index, index + 1); } } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs index 9afb64c7c..c8f885b5d 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs @@ -19,17 +19,17 @@ internal static class AdvancedTypographicUtils public const int MaxContextLength = 64; /// - /// The maximum length factor multiplied by collection count to compute max allowable collection size. + /// The maximum length factor multiplied by buffer count to compute max allowable buffer size. /// private const int MaxLengthFactor = 64; /// - /// The minimum value for the max allowable collection size. + /// The minimum value for the max allowable buffer size. /// private const int MaxLengthMinimum = 16384; /// - /// The maximum operations factor multiplied by collection count to compute max allowable operations. + /// The maximum operations factor multiplied by buffer count to compute max allowable operations. /// private const int MaxOperationsFactor = 1024; @@ -77,9 +77,9 @@ public static bool IsVerticalGlyph(CodePoint codePoint, LayoutMode layoutMode) } /// - /// Gets the maximum allowable shaping collection count for the given input length. + /// Gets the maximum allowable shaping buffer count for the given input length. /// - /// The input collection length. + /// The input buffer length. /// The maximum allowable count. public static int GetMaxAllowableShapingCollectionCount(int length) => (int)Math.Min(Math.Max((long)length * MaxLengthFactor, MaxLengthMinimum), MaxShapingCharsLength); @@ -87,7 +87,7 @@ public static int GetMaxAllowableShapingCollectionCount(int length) /// /// Gets the maximum allowable shaping operations count for the given input length. /// - /// The input collection length. + /// The input buffer length. /// The maximum allowable operations count. public static int GetMaxAllowableShapingOperationsCount(int length) => (int)Math.Min(Math.Max((long)length * MaxOperationsFactor, MaxOperationsMinimum), MaxShapingCharsLength); @@ -101,8 +101,8 @@ public static int GetMaxAllowableShapingOperationsCount(int length) /// The lookup flags for glyph filtering. /// The mark filtering set index. /// The sequence lookup records specifying which lookups to apply at which positions. - /// The glyph substitution collection. - /// The starting index in the collection. + /// The glyph substitution buffer. + /// The starting index in the buffer. /// The number of glyphs in the input sequence. /// if the lookups were applied. public static bool ApplyLookupList( @@ -112,17 +112,17 @@ public static bool ApplyLookupList( LookupFlags lookupFlags, ushort markFilteringSet, SequenceLookupRecord[] records, - GlyphSubstitutionCollection collection, + ShapingBuffer buffer, int index, int count) { - SkippingGlyphIterator iterator = new(fontMetrics, collection, index, lookupFlags, markFilteringSet); + SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, lookupFlags, markFilteringSet); if (ShapingProbe.Enabled) { ShapingProbe.ContextIterators++; } - int currentCount = collection.Count; + int currentCount = buffer.Count; foreach (SequenceLookupRecord lookupRecord in records) { @@ -131,13 +131,13 @@ public static bool ApplyLookupList( iterator.Index = index; iterator.Increment(sequenceIndex); GSub.LookupTable lookup = table.LookupList.LookupTables[lookupIndex]; - _ = lookup.TrySubstitution(fontMetrics, table, collection, feature, iterator.Index, count - (iterator.Index - index)); + _ = lookup.TrySubstitution(fontMetrics, table, buffer, feature, iterator.Index, count - (iterator.Index - index)); - // Account for substitutions changing the length of the collection. - if (collection.Count != currentCount) + // Account for substitutions changing the length of the buffer. + if (buffer.Count != currentCount) { - count -= currentCount - collection.Count; - currentCount = collection.Count; + count -= currentCount - buffer.Count; + currentCount = buffer.Count; } } @@ -153,8 +153,8 @@ public static bool ApplyLookupList( /// The lookup flags for glyph filtering. /// The mark filtering set index. /// The sequence lookup records specifying which lookups to apply at which positions. - /// The glyph positioning collection. - /// The starting index in the collection. + /// The glyph positioning buffer. + /// The starting index in the buffer. /// The number of glyphs in the input sequence. /// if the lookups were applied. public static bool ApplyLookupList( @@ -164,11 +164,11 @@ public static bool ApplyLookupList( LookupFlags lookupFlags, ushort markFilteringSet, SequenceLookupRecord[] records, - GlyphPositioningCollection collection, + ShapingBuffer buffer, int index, int count) { - SkippingGlyphIterator iterator = new(fontMetrics, collection, index, lookupFlags, markFilteringSet); + SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, lookupFlags, markFilteringSet); if (ShapingProbe.Enabled) { ShapingProbe.ContextIterators++; @@ -181,7 +181,7 @@ public static bool ApplyLookupList( iterator.Index = index; iterator.Increment(sequenceIndex); LookupTable lookup = table.LookupList.LookupTables[lookupIndex]; - _ = lookup.TryUpdatePosition(fontMetrics, table, collection, feature, iterator.Index, count - (iterator.Index - index)); + _ = lookup.TryUpdatePosition(fontMetrics, table, buffer, feature, iterator.Index, count - (iterator.Index - index)); } return true; @@ -264,8 +264,8 @@ public static bool MatchClassSequence( /// /// The skipping glyph iterator. /// The array of coverage tables to match against. - /// The starting index in the collection. - /// The exclusive end index in the collection. + /// The starting index in the buffer. + /// The exclusive end index in the buffer. /// if all coverage tables matched; otherwise, . public static bool MatchCoverageSequence( SkippingGlyphIterator iterator, @@ -287,8 +287,8 @@ public static bool MatchCoverageSequence( /// /// The skipping glyph iterator. /// The array of backtrack coverage tables to match against. - /// The starting index in the collection (the first backtrack position). - /// The exclusive end index in the collection. + /// The starting index in the buffer (the first backtrack position). + /// The exclusive end index in the buffer. /// if all backtrack coverage tables matched; otherwise, . public static bool MatchBacktrackCoverageSequence( SkippingGlyphIterator iterator, @@ -376,7 +376,7 @@ public static bool ApplyChainedClassSequenceRule( /// The font metrics. /// The lookup flags for glyph filtering. /// The mark filtering set index. - /// The glyph shaping collection. + /// The glyph shaping buffer. /// The starting index of the input sequence. /// The number of glyphs available from the starting index. /// The array of input coverage tables. @@ -387,7 +387,7 @@ public static bool CheckAllCoverages( FontMetrics fontMetrics, LookupFlags lookupFlags, ushort markFilteringSet, - GlyphShapingCollection collection, + ShapingBuffer buffer, int index, int count, CoverageTable[] input, @@ -396,7 +396,7 @@ public static bool CheckAllCoverages( { int endExclusive = index + count; - SkippingGlyphIterator iterator = new(fontMetrics, collection, index, lookupFlags, markFilteringSet); + SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, lookupFlags, markFilteringSet); if (ShapingProbe.Enabled) { ShapingProbe.ContextIterators++; @@ -445,15 +445,15 @@ public static bool CheckAllCoverages( /// Applies anchor-based positioning for mark-to-base, mark-to-ligature, or mark-to-mark attachment. /// /// The font metrics. - /// The glyph positioning collection. - /// The index of the mark glyph in the collection. + /// The glyph positioning buffer. + /// The index of the mark glyph in the buffer. /// The anchor table for the base glyph, or if no anchor is defined. /// The mark record containing the mark anchor table and class. - /// The index of the base glyph in the collection. + /// The index of the base glyph in the buffer. /// The feature tag being applied. public static void ApplyAnchor( FontMetrics fontMetrics, - GlyphPositioningCollection collection, + ShapingBuffer buffer, int index, AnchorTable? baseAnchor, MarkRecord markRecord, @@ -468,34 +468,34 @@ public static void ApplyAnchor( return; } - GlyphShapingData baseData = collection[baseGlyphIndex]; - AnchorXY baseXY = baseAnchor.GetAnchor(fontMetrics, baseData, collection); + ref GlyphShapingData baseData = ref buffer[baseGlyphIndex]; + AnchorXY baseXY = baseAnchor.GetAnchor(fontMetrics, ref baseData, buffer); - GlyphShapingData markData = collection[index]; - AnchorXY markXY = markRecord.MarkAnchorTable.GetAnchor(fontMetrics, markData, collection); + ref GlyphShapingData markData = ref buffer[index]; + AnchorXY markXY = markRecord.MarkAnchorTable.GetAnchor(fontMetrics, ref markData, buffer); markData.Bounds.X = baseXY.XCoordinate - markXY.XCoordinate; markData.Bounds.Y = baseXY.YCoordinate - markXY.YCoordinate; markData.MarkAttachment = baseGlyphIndex; - markData.AppliedFeatureMask |= collection.FeatureMap.GetOrAddMask(feature); + markData.AppliedFeatureMask |= buffer.FeatureMap.GetOrAddMask(feature); } /// - /// Applies a value record's positioning adjustments to a glyph in the collection. + /// Applies a value record's positioning adjustments to a glyph in the buffer. /// /// The font metrics. - /// The glyph positioning collection. - /// The index of the glyph in the collection. + /// The glyph positioning buffer. + /// The index of the glyph in the buffer. /// The value record containing positioning adjustments. /// The feature tag being applied. public static void ApplyPosition( FontMetrics fontMetrics, - GlyphPositioningCollection collection, + ShapingBuffer buffer, int index, ValueRecord record, Tag feature) { - GlyphShapingData current = collection[index]; + ref GlyphShapingData current = ref buffer[index]; current.Bounds.Width += record.XAdvance; current.Bounds.Height += record.YAdvance; current.Bounds.X += record.XPlacement; @@ -510,7 +510,7 @@ public static void ApplyPosition( current.Bounds.Height += (short)MathF.Round(fontMetrics.GetGDefVariationDelta(record.YAdvanceVariation)); } - current.AppliedFeatureMask |= collection.FeatureMap.GetOrAddMask(feature); + current.AppliedFeatureMask |= buffer.FeatureMap.GetOrAddMask(feature); } /// @@ -520,7 +520,7 @@ public static void ApplyPosition( /// The glyph identifier. /// The glyph shaping data. /// if the glyph is a mark; otherwise, . - public static bool IsMarkGlyph(FontMetrics fontMetrics, ushort glyphId, GlyphShapingData shapingData) + public static bool IsMarkGlyph(FontMetrics fontMetrics, ushort glyphId, ref GlyphShapingData shapingData) { if (!fontMetrics.TryGetGlyphClass(glyphId, out GlyphClassDef? glyphClass) && !CodePoint.IsMark(shapingData.CodePoint)) @@ -545,7 +545,7 @@ public static bool IsMarkGlyph(FontMetrics fontMetrics, ushort glyphId, GlyphSha /// The glyph identifier. /// The glyph shaping data, used for caching and Unicode fallback. /// The . - public static GlyphShapingClass GetGlyphShapingClass(FontMetrics fontMetrics, ushort glyphId, GlyphShapingData shapingData) + public static GlyphShapingClass GetGlyphShapingClass(FontMetrics fontMetrics, ushort glyphId, ref GlyphShapingData shapingData) { // Cache the shaping class on the GlyphShapingData to avoid repeated GDEF lookups. // The cache key stores the glyph id; -1 means "not cached". @@ -626,7 +626,7 @@ private static bool Match( { int position = iterator.Index; int offset = iterator.Increment(increment); - GlyphShapingCollection collection = iterator.Collection; + ShapingBuffer buffer = iterator.Collection; if (offset < 0) { @@ -634,9 +634,9 @@ private static bool Match( } int i = 0; - while (i < sequence.Length && i < MaxContextLength && offset < collection.Count) + while (i < sequence.Length && i < MaxContextLength && offset < buffer.Count) { - if (!condition(sequence[i], collection[offset], state)) + if (!condition(sequence[i], buffer[offset], state)) { break; } @@ -673,7 +673,7 @@ private static bool Match( { int position = iterator.Index; int offset = iterator.Increment(increment); - GlyphShapingCollection collection = iterator.Collection; + ShapingBuffer buffer = iterator.Collection; if (offset < 0) { @@ -681,9 +681,9 @@ private static bool Match( } int i = 0; - while (i < sequence.Length && i < MaxContextLength && offset < collection.Count) + while (i < sequence.Length && i < MaxContextLength && offset < buffer.Count) { - if (!condition(sequence[i], collection[offset])) + if (!condition(sequence[i], buffer[offset])) { break; } @@ -706,10 +706,10 @@ private static bool Match( /// /// The type of sequence elements to match. /// The skipping glyph iterator. - /// The starting index in the collection. + /// The starting index in the buffer. /// The array of elements to match. /// The direction to iterate (forward or backward). - /// The exclusive end index in the collection. + /// The exclusive end index in the buffer. /// The condition function to test each element against glyph data. /// A span to store matched glyph indices, or default if not needed. /// if all elements in the sequence were matched; otherwise, . @@ -730,8 +730,8 @@ private static bool Match( int saved = iterator.Index; iterator.Index = startIndex; - GlyphShapingCollection collection = iterator.Collection; - int limit = Math.Min(endExclusive, collection.Count); + ShapingBuffer buffer = iterator.Collection; + int limit = Math.Min(endExclusive, buffer.Count); for (int i = 0; i < sequence.Length && i < MaxContextLength; i++) { @@ -741,7 +741,7 @@ private static bool Match( return false; } - GlyphShapingData data = collection[iterator.Index]; + ref GlyphShapingData data = ref buffer[iterator.Index]; if (!condition(sequence[i], data)) { iterator.Index = saved; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs index d10b49f45..84c91487f 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs @@ -42,9 +42,9 @@ protected AnchorTable(short xCoordinate, short yCoordinate) /// /// The font metrics. /// The glyph shaping data. - /// The glyph positioning collection. + /// The glyph positioning buffer. /// The resolved anchor coordinates. - public abstract AnchorXY GetAnchor(FontMetrics fontMetrics, GlyphShapingData data, GlyphPositioningCollection collection); + public abstract AnchorXY GetAnchor(FontMetrics fontMetrics, ref GlyphShapingData data, ShapingBuffer buffer); /// /// Loads the anchor table. @@ -106,7 +106,7 @@ public static AnchorFormat1 Load(BigEndianBinaryReader reader) } /// - public override AnchorXY GetAnchor(FontMetrics fontMetrics, GlyphShapingData data, GlyphPositioningCollection collection) + public override AnchorXY GetAnchor(FontMetrics fontMetrics, ref GlyphShapingData data, ShapingBuffer buffer) => new(this.XCoordinate, this.YCoordinate); } @@ -152,14 +152,14 @@ public static AnchorFormat2 Load(BigEndianBinaryReader reader) } /// - public override AnchorXY GetAnchor(FontMetrics fontMetrics, GlyphShapingData data, GlyphPositioningCollection collection) + public override AnchorXY GetAnchor(FontMetrics fontMetrics, ref GlyphShapingData data, ShapingBuffer buffer) { - if (collection.TextOptions.HintingMode != HintingMode.None) + if (buffer.TextOptions.HintingMode != HintingMode.None) { TextAttributes textAttributes = data.TextRun.TextAttributes; TextDecorations textDecorations = data.TextRun.TextDecorations; - LayoutMode layoutMode = collection.TextOptions.LayoutMode; - ColorFontSupport colorFontSupport = collection.TextOptions.ColorFontSupport; + LayoutMode layoutMode = buffer.TextOptions.LayoutMode; + ColorFontSupport colorFontSupport = buffer.TextOptions.ColorFontSupport; if (fontMetrics.TryGetGlyphMetrics(data.CodePoint, textAttributes, textDecorations, layoutMode, colorFontSupport, out FontGlyphMetrics? metrics)) { if (metrics is TrueTypeGlyphMetrics ttmetric) @@ -247,7 +247,7 @@ public static AnchorFormat3 LoadFormat3(BigEndianBinaryReader reader, long ancho } /// - public override AnchorXY GetAnchor(FontMetrics fontMetrics, GlyphShapingData data, GlyphPositioningCollection collection) + public override AnchorXY GetAnchor(FontMetrics fontMetrics, ref GlyphShapingData data, ShapingBuffer buffer) { short x = this.XCoordinate; short y = this.YCoordinate; @@ -320,8 +320,8 @@ private EmptyAnchorTable() /// public override AnchorXY GetAnchor( FontMetrics fontMetrics, - GlyphShapingData data, - GlyphPositioningCollection collection) + ref GlyphShapingData data, + ShapingBuffer buffer) => new(0, 0); } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupListTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupListTable.cs index 2074cf45f..c94748f39 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupListTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupListTable.cs @@ -211,11 +211,11 @@ private static LookupSubTable LoadLookupSubTable(ushort lookupType, LookupFlags }; /// - /// Attempts to update the position of glyphs in the collection at the specified index. + /// Attempts to update the position of glyphs in the buffer at the specified index. /// /// The font metrics. /// The GPOS table. - /// The glyph positioning collection. + /// The glyph positioning buffer. /// The feature tag. /// The zero-based index of the glyph to position. /// The number of glyphs remaining in the sequence. @@ -223,12 +223,12 @@ private static LookupSubTable LoadLookupSubTable(ushort lookupType, LookupFlags public bool TryUpdatePosition( FontMetrics fontMetrics, GPosTable table, - GlyphPositioningCollection collection, + ShapingBuffer buffer, Tag feature, int index, int count) { - ushort glyphId = collection[index].GlyphId; + ushort glyphId = buffer[index].GlyphId; foreach (LookupSubTable subTable in this.LookupSubTables) { // A glyph outside the subtable's digest cannot match its coverage, so the @@ -246,7 +246,7 @@ public bool TryUpdatePosition( // A lookup is finished for a glyph after the client locates the target // glyph or glyph context and performs a positioning action, if specified. - if (subTable.TryUpdatePosition(fontMetrics, table, collection, feature, index, count)) + if (subTable.TryUpdatePosition(fontMetrics, table, buffer, feature, index, count)) { return true; } @@ -297,11 +297,11 @@ protected LookupSubTable(LookupFlags lookupFlags, ushort markFilteringSet) public virtual void CollectDigest(ref GlyphSetDigest digest) => digest.AddAll(); /// - /// Attempts to update the position of glyphs in the collection at the specified index. + /// Attempts to update the position of glyphs in the buffer at the specified index. /// /// The font metrics. /// The GPOS table. - /// The glyph positioning collection. + /// The glyph positioning buffer. /// The feature tag. /// The zero-based index of the glyph to position. /// The number of glyphs remaining in the sequence. @@ -309,7 +309,7 @@ protected LookupSubTable(LookupFlags lookupFlags, ushort markFilteringSet) public abstract bool TryUpdatePosition( FontMetrics fontMetrics, GPosTable table, - GlyphPositioningCollection collection, + ShapingBuffer buffer, Tag feature, int index, int count); diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType1SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType1SubTable.cs index 4f24f9fb2..1f51db55c 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType1SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType1SubTable.cs @@ -97,12 +97,12 @@ public static LookupType1Format1SubTable Load(BigEndianBinaryReader reader, long public override bool TryUpdatePosition( FontMetrics fontMetrics, GPosTable table, - GlyphPositioningCollection collection, + ShapingBuffer buffer, Tag feature, int index, int count) { - ushort glyphId = collection[index].GlyphId; + ushort glyphId = buffer[index].GlyphId; if (glyphId == 0) { return false; @@ -112,7 +112,7 @@ public override bool TryUpdatePosition( if (coverage > -1) { ValueRecord record = this.valueRecord; - AdvancedTypographicUtils.ApplyPosition(fontMetrics, collection, index, record, feature); + AdvancedTypographicUtils.ApplyPosition(fontMetrics, buffer, index, record, feature); return true; } @@ -191,12 +191,12 @@ public static LookupType1Format2SubTable Load(BigEndianBinaryReader reader, long public override bool TryUpdatePosition( FontMetrics fontMetrics, GPosTable table, - GlyphPositioningCollection collection, + ShapingBuffer buffer, Tag feature, int index, int count) { - ushort glyphId = collection[index].GlyphId; + ushort glyphId = buffer[index].GlyphId; if (glyphId == 0) { return false; @@ -206,7 +206,7 @@ public override bool TryUpdatePosition( if (coverage > -1 && coverage < this.valueRecords.Length) { ValueRecord record = this.valueRecords[coverage]; - AdvancedTypographicUtils.ApplyPosition(fontMetrics, collection, index, record, feature); + AdvancedTypographicUtils.ApplyPosition(fontMetrics, buffer, index, record, feature); return true; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType2SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType2SubTable.cs index 3369781b1..fff497789 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType2SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType2SubTable.cs @@ -120,7 +120,7 @@ public static LookupType2Format1SubTable Load(BigEndianBinaryReader reader, long public override bool TryUpdatePosition( FontMetrics fontMetrics, GPosTable table, - GlyphPositioningCollection collection, + ShapingBuffer buffer, Tag feature, int index, int count) @@ -130,7 +130,7 @@ public override bool TryUpdatePosition( return false; } - ushort glyphId = collection[index].GlyphId; + ushort glyphId = buffer[index].GlyphId; if (glyphId == 0) { return false; @@ -140,7 +140,7 @@ public override bool TryUpdatePosition( if (coverage > -1 && coverage < this.pairSets.Length) { PairSetTable pairSet = this.pairSets[coverage]; - ushort glyphId2 = collection[index + 1].GlyphId; + ushort glyphId2 = buffer[index + 1].GlyphId; if (glyphId2 == 0) { return false; @@ -149,10 +149,10 @@ public override bool TryUpdatePosition( if (pairSet.TryGetPairValueRecord(glyphId2, out PairValueRecord pairValueRecord)) { ValueRecord record1 = pairValueRecord.ValueRecord1; - AdvancedTypographicUtils.ApplyPosition(fontMetrics, collection, index, record1, feature); + AdvancedTypographicUtils.ApplyPosition(fontMetrics, buffer, index, record1, feature); ValueRecord record2 = pairValueRecord.ValueRecord2; - AdvancedTypographicUtils.ApplyPosition(fontMetrics, collection, index + 1, record2, feature); + AdvancedTypographicUtils.ApplyPosition(fontMetrics, buffer, index + 1, record2, feature); return true; } @@ -346,7 +346,7 @@ public static LookupType2Format2SubTable Load(BigEndianBinaryReader reader, long public override bool TryUpdatePosition( FontMetrics fontMetrics, GPosTable table, - GlyphPositioningCollection collection, + ShapingBuffer buffer, Tag feature, int index, int count) @@ -356,7 +356,7 @@ public override bool TryUpdatePosition( return false; } - ushort glyphId = collection[index].GlyphId; + ushort glyphId = buffer[index].GlyphId; if (glyphId == 0) { return false; @@ -366,7 +366,7 @@ public override bool TryUpdatePosition( if (coverage > -1) { int classDef1 = this.classDefinitionTable1.ClassIndexOf(glyphId); - ushort glyphId2 = collection[index + 1].GlyphId; + ushort glyphId2 = buffer[index + 1].GlyphId; if (glyphId2 == 0) { return false; @@ -378,10 +378,10 @@ public override bool TryUpdatePosition( Class2Record class2Record = class1Record.Class2Records[classDef2]; ValueRecord record1 = class2Record.ValueRecord1; - AdvancedTypographicUtils.ApplyPosition(fontMetrics, collection, index, record1, feature); + AdvancedTypographicUtils.ApplyPosition(fontMetrics, buffer, index, record1, feature); ValueRecord record2 = class2Record.ValueRecord2; - AdvancedTypographicUtils.ApplyPosition(fontMetrics, collection, index + 1, record2, feature); + AdvancedTypographicUtils.ApplyPosition(fontMetrics, buffer, index + 1, record2, feature); return true; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType3SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType3SubTable.cs index ac6b481c9..6754b8303 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType3SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType3SubTable.cs @@ -108,7 +108,7 @@ public static LookupType3Format1SubTable Load(BigEndianBinaryReader reader, long public override bool TryUpdatePosition( FontMetrics fontMetrics, GPosTable table, - GlyphPositioningCollection collection, + ShapingBuffer buffer, Tag feature, int index, int count) @@ -120,14 +120,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].GlyphId; + ushort glyphId = buffer[index].GlyphId; if (glyphId == 0) { return false; } int nextIndex = index + 1; - ushort nextGlyphId = collection[nextIndex].GlyphId; + ushort nextGlyphId = buffer[nextIndex].GlyphId; if (nextGlyphId == 0) { return false; @@ -159,13 +159,13 @@ public override bool TryUpdatePosition( return false; } - GlyphShapingData current = collection[index]; - GlyphShapingData next = collection[nextIndex]; + ref GlyphShapingData current = ref buffer[index]; + ref GlyphShapingData next = ref buffer[nextIndex]; - AnchorXY exitXY = exit.GetAnchor(fontMetrics, current, collection); - AnchorXY entryXY = entry.GetAnchor(fontMetrics, next, collection); + AnchorXY exitXY = exit.GetAnchor(fontMetrics, ref current, buffer); + AnchorXY entryXY = entry.GetAnchor(fontMetrics, ref next, buffer); - bool isVerticalLayout = AdvancedTypographicUtils.IsVerticalGlyph(current.CodePoint, collection.TextOptions.LayoutMode); + bool isVerticalLayout = AdvancedTypographicUtils.IsVerticalGlyph(current.CodePoint, buffer.TextOptions.LayoutMode); if (!isVerticalLayout) { // Horizontal @@ -213,9 +213,9 @@ public override bool TryUpdatePosition( // previous connection now attaches to new parent.Watch out for case // where new parent is on the path from old chain... bool horizontal = !isVerticalLayout; - ReverseCursiveMinorOffset(collection, index, child, horizontal, parent); + ReverseCursiveMinorOffset(buffer, index, child, horizontal, parent); - GlyphShapingData c = collection[child]; + ref GlyphShapingData c = ref buffer[child]; c.CursiveAttachment = parent - child; if (horizontal) { @@ -228,7 +228,7 @@ public override bool TryUpdatePosition( // If parent was attached to child, separate them. // https://github.com/harfbuzz/harfbuzz/issues/2469 - GlyphShapingData p = collection[parent]; + ref GlyphShapingData p = ref buffer[parent]; if (p.CursiveAttachment == -c.CursiveAttachment) { p.CursiveAttachment = 0; @@ -252,19 +252,19 @@ public override bool TryUpdatePosition( /// Recursively reverses the cursive minor offset chain so that the entire tree /// of a previous connection attaches to the new parent. /// - /// The glyph positioning collection. + /// The glyph positioning buffer. /// The original glyph position that initiated the chain reversal. /// The current index in the chain being reversed. /// Whether the layout is horizontal. /// The new parent index to stop at. private static void ReverseCursiveMinorOffset( - GlyphPositioningCollection collection, + ShapingBuffer buffer, int position, int i, bool horizontal, int parent) { - GlyphShapingData c = collection[i]; + ref GlyphShapingData c = ref buffer[i]; int chain = c.CursiveAttachment; if (chain <= 0) { @@ -281,9 +281,9 @@ private static void ReverseCursiveMinorOffset( return; } - ReverseCursiveMinorOffset(collection, position, j, horizontal, parent); + ReverseCursiveMinorOffset(buffer, position, j, horizontal, parent); - GlyphShapingData p = collection[j]; + ref GlyphShapingData p = ref buffer[j]; if (horizontal) { p.Bounds.Y = -c.Bounds.Y; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType4SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType4SubTable.cs index 8aa71b6d5..b0e851686 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType4SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType4SubTable.cs @@ -116,14 +116,14 @@ public static LookupType4Format1SubTable Load(BigEndianBinaryReader reader, long public override bool TryUpdatePosition( FontMetrics fontMetrics, GPosTable table, - GlyphPositioningCollection collection, + ShapingBuffer buffer, Tag feature, int index, int count) { // 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].GlyphId; + ushort glyphId = buffer[index].GlyphId; if (glyphId == 0) { return false; @@ -139,8 +139,8 @@ public override bool TryUpdatePosition( int baseGlyphIndex = index; while (--baseGlyphIndex >= 0) { - GlyphShapingData data = collection[baseGlyphIndex]; - if (!AdvancedTypographicUtils.IsMarkGlyph(fontMetrics, data.GlyphId, data) && data.LigatureComponent <= 0) + ref GlyphShapingData data = ref buffer[baseGlyphIndex]; + if (!AdvancedTypographicUtils.IsMarkGlyph(fontMetrics, data.GlyphId, ref data) && data.LigatureComponent <= 0) { break; } @@ -151,7 +151,7 @@ public override bool TryUpdatePosition( return false; } - ushort baseGlyphId = collection[baseGlyphIndex].GlyphId; + ushort baseGlyphId = buffer[baseGlyphIndex].GlyphId; int baseIndex = this.baseCoverage.CoverageIndexOf(baseGlyphId); if (baseIndex < 0 || baseIndex >= this.baseArrayTable.BaseRecords.Length) { @@ -160,7 +160,7 @@ public override bool TryUpdatePosition( MarkRecord markRecord = this.markArrayTable.MarkRecords[markIndex]; AnchorTable baseAnchor = this.baseArrayTable.BaseRecords[baseIndex].BaseAnchorTables[markRecord.MarkClass]; - AdvancedTypographicUtils.ApplyAnchor(fontMetrics, collection, index, baseAnchor, markRecord, baseGlyphIndex, feature); + AdvancedTypographicUtils.ApplyAnchor(fontMetrics, buffer, index, baseAnchor, markRecord, baseGlyphIndex, feature); return true; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType5SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType5SubTable.cs index bc9402360..3bbb1191a 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType5SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType5SubTable.cs @@ -119,14 +119,14 @@ public static LookupType5Format1SubTable Load(BigEndianBinaryReader reader, long public override bool TryUpdatePosition( FontMetrics fontMetrics, GPosTable table, - GlyphPositioningCollection collection, + ShapingBuffer buffer, Tag feature, int index, int count) { // 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].GlyphId; + ushort glyphId = buffer[index].GlyphId; if (glyphId == 0) { return false; @@ -142,8 +142,8 @@ public override bool TryUpdatePosition( int baseGlyphIndex = index; while (--baseGlyphIndex >= 0) { - GlyphShapingData data = collection[baseGlyphIndex]; - if (!AdvancedTypographicUtils.IsMarkGlyph(fontMetrics, data.GlyphId, data)) + ref GlyphShapingData data = ref buffer[baseGlyphIndex]; + if (!AdvancedTypographicUtils.IsMarkGlyph(fontMetrics, data.GlyphId, ref data)) { break; } @@ -154,7 +154,7 @@ public override bool TryUpdatePosition( return false; } - ushort baseGlyphId = collection[baseGlyphIndex].GlyphId; + ushort baseGlyphId = buffer[baseGlyphIndex].GlyphId; int ligatureIndex = this.ligatureCoverage.CoverageIndexOf(baseGlyphId); if (ligatureIndex < 0 || ligatureIndex >= this.ligatureArrayTable.LigatureAttachTables.Length) { @@ -166,15 +166,15 @@ public override bool TryUpdatePosition( // If yes, we can directly use the component index. If not, we attach the mark // glyph to the last component of the ligature. LigatureAttachTable ligatureAttach = this.ligatureArrayTable.LigatureAttachTables[ligatureIndex]; - GlyphShapingData markGlyph = collection[index]; - GlyphShapingData ligGlyph = collection[baseGlyphIndex]; + ref GlyphShapingData markGlyph = ref buffer[index]; + ref GlyphShapingData ligGlyph = ref buffer[baseGlyphIndex]; int compIndex = ligGlyph.LigatureId > 0 && ligGlyph.LigatureId == markGlyph.LigatureId && markGlyph.LigatureComponent > 0 ? Math.Min(markGlyph.LigatureComponent, ligGlyph.CodePointCount) - 1 : ligGlyph.CodePointCount - 1; MarkRecord markRecord = this.markArrayTable.MarkRecords[markIndex]; AnchorTable baseAnchor = ligatureAttach.ComponentRecords[compIndex].LigatureAnchorTables[markRecord.MarkClass]; - AdvancedTypographicUtils.ApplyAnchor(fontMetrics, collection, index, baseAnchor, markRecord, baseGlyphIndex, feature); + AdvancedTypographicUtils.ApplyAnchor(fontMetrics, buffer, index, baseAnchor, markRecord, baseGlyphIndex, feature); return true; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType6SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType6SubTable.cs index fc3bae2b3..a8c873caf 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType6SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType6SubTable.cs @@ -118,14 +118,14 @@ public static LookupType6Format1SubTable Load(BigEndianBinaryReader reader, long public override bool TryUpdatePosition( FontMetrics fontMetrics, GPosTable table, - GlyphPositioningCollection collection, + ShapingBuffer buffer, Tag feature, int index, int count) { // 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].GlyphId; + ushort glyphId = buffer[index].GlyphId; if (glyphId == 0) { return false; @@ -142,7 +142,7 @@ public override bool TryUpdatePosition( // It clears ignore flags when searching, but keeps mark attachment / filtering behavior. LookupFlags searchFlags = this.LookupFlags & ~(LookupFlags.IgnoreMarks | LookupFlags.IgnoreBaseGlyphs | LookupFlags.IgnoreLigatures); - SkippingGlyphIterator it = new(fontMetrics, collection, index, searchFlags, this.MarkFilteringSet); + SkippingGlyphIterator it = new(fontMetrics, buffer, index, searchFlags, this.MarkFilteringSet); int j = it.Prev(); if (j < 0) @@ -150,13 +150,13 @@ public override bool TryUpdatePosition( return false; } - GlyphShapingData prevGlyph = collection[j]; - if (!AdvancedTypographicUtils.IsMarkGlyph(fontMetrics, prevGlyph.GlyphId, prevGlyph)) + ref GlyphShapingData prevGlyph = ref buffer[j]; + if (!AdvancedTypographicUtils.IsMarkGlyph(fontMetrics, prevGlyph.GlyphId, ref prevGlyph)) { return false; } - GlyphShapingData curGlyph = collection[index]; + ref GlyphShapingData curGlyph = ref buffer[index]; bool good; int id1 = curGlyph.LigatureId; @@ -196,7 +196,7 @@ public override bool TryUpdatePosition( MarkRecord markRecord = this.mark1ArrayTable.MarkRecords[mark1Index]; AnchorTable? baseAnchor = this.mark2ArrayTable.Mark2Records[mark2Index].MarkAnchorTable[markRecord.MarkClass]; - AdvancedTypographicUtils.ApplyAnchor(fontMetrics, collection, index, baseAnchor, markRecord, j, feature); + AdvancedTypographicUtils.ApplyAnchor(fontMetrics, buffer, index, baseAnchor, markRecord, j, feature); return true; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType7SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType7SubTable.cs index 4c9bf5435..db11ced15 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType7SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType7SubTable.cs @@ -81,12 +81,12 @@ public static LookupType7Format1SubTable Load(BigEndianBinaryReader reader, long public override bool TryUpdatePosition( FontMetrics fontMetrics, GPosTable table, - GlyphPositioningCollection collection, + ShapingBuffer buffer, Tag feature, int index, int count) { - ushort glyphId = collection[index].GlyphId; + ushort glyphId = buffer[index].GlyphId; if (glyphId == 0) { return false; @@ -101,7 +101,7 @@ public override bool TryUpdatePosition( // TODO: Check this. // https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#example-7-contextual-substitution-format-1 SequenceRuleSetTable ruleSetTable = this.seqRuleSetTables[offset]; - SkippingGlyphIterator iterator = new(fontMetrics, collection, index, this.LookupFlags, this.MarkFilteringSet); + SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, this.LookupFlags, this.MarkFilteringSet); foreach (SequenceRuleTable ruleTable in ruleSetTable.SequenceRuleTables) { int remaining = count - 1; @@ -124,7 +124,7 @@ public override bool TryUpdatePosition( this.LookupFlags, this.MarkFilteringSet, ruleTable.SequenceLookupRecords, - collection, + buffer, index, count); } @@ -190,12 +190,12 @@ public static LookupType7Format2SubTable Load(BigEndianBinaryReader reader, long public override bool TryUpdatePosition( FontMetrics fontMetrics, GPosTable table, - GlyphPositioningCollection collection, + ShapingBuffer buffer, Tag feature, int index, int count) { - ushort glyphId = collection[index].GlyphId; + ushort glyphId = buffer[index].GlyphId; if (glyphId == 0) { return false; @@ -213,7 +213,7 @@ public override bool TryUpdatePosition( } ClassSequenceRuleSetTable ruleSetTable = this.sequenceRuleSetTables[offset]; - SkippingGlyphIterator iterator = new(fontMetrics, collection, index, this.LookupFlags, this.MarkFilteringSet); + SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, this.LookupFlags, this.MarkFilteringSet); foreach (ClassSequenceRuleTable ruleTable in ruleSetTable.SequenceRuleTables) { int remaining = count - 1; @@ -236,7 +236,7 @@ public override bool TryUpdatePosition( this.LookupFlags, this.MarkFilteringSet, ruleTable.SequenceLookupRecords, - collection, + buffer, index, count); } @@ -305,18 +305,18 @@ public override void CollectDigest(ref GlyphSetDigest digest) public override bool TryUpdatePosition( FontMetrics fontMetrics, GPosTable table, - GlyphPositioningCollection collection, + ShapingBuffer buffer, Tag feature, int index, int count) { - ushort glyphId = collection[index].GlyphId; + ushort glyphId = buffer[index].GlyphId; if (glyphId == 0) { return false; } - SkippingGlyphIterator iterator = new(fontMetrics, collection, index, this.LookupFlags, this.MarkFilteringSet); + SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, this.LookupFlags, this.MarkFilteringSet); if (!AdvancedTypographicUtils.MatchCoverageSequence(iterator, this.coverageTables, index, index + count)) { return false; @@ -329,7 +329,7 @@ public override bool TryUpdatePosition( this.LookupFlags, this.MarkFilteringSet, this.sequenceLookupRecords, - collection, + buffer, index, count); } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType8SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType8SubTable.cs index 00d76178e..ae01b5985 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType8SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType8SubTable.cs @@ -83,14 +83,14 @@ public static LookupType8Format1SubTable Load(BigEndianBinaryReader reader, long public override bool TryUpdatePosition( FontMetrics fontMetrics, GPosTable table, - GlyphPositioningCollection collection, + ShapingBuffer buffer, Tag feature, int index, int count) { // 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].GlyphId; + ushort glyphId = buffer[index].GlyphId; if (glyphId == 0) { return false; @@ -116,7 +116,7 @@ public override bool TryUpdatePosition( // Apply ruleset for the given glyph id. ChainedSequenceRuleTable[] rules = seqRuleSet.SequenceRuleTables; - SkippingGlyphIterator iterator = new(fontMetrics, collection, index, this.LookupFlags, this.MarkFilteringSet); + SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, this.LookupFlags, this.MarkFilteringSet); for (int lookupIndex = 0; lookupIndex < rules.Length; lookupIndex++) { ChainedSequenceRuleTable rule = rules[lookupIndex]; @@ -131,7 +131,7 @@ public override bool TryUpdatePosition( SequenceLookupRecord sequenceLookupRecord = rule.SequenceLookupRecords[j]; LookupTable lookup = table.LookupList.LookupTables[sequenceLookupRecord.LookupListIndex]; ushort sequenceIndex = sequenceLookupRecord.SequenceIndex; - if (lookup.TryUpdatePosition(fontMetrics, table, collection, feature, index + sequenceIndex, 1)) + if (lookup.TryUpdatePosition(fontMetrics, table, buffer, feature, index + sequenceIndex, 1)) { hasChanged = true; } @@ -217,14 +217,14 @@ public static LookupType8Format2SubTable Load(BigEndianBinaryReader reader, long public override bool TryUpdatePosition( FontMetrics fontMetrics, GPosTable table, - GlyphPositioningCollection collection, + ShapingBuffer buffer, Tag feature, int index, int count) { // 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].GlyphId; + ushort glyphId = buffer[index].GlyphId; if (glyphId == 0) { return false; @@ -249,7 +249,7 @@ public override bool TryUpdatePosition( } // Apply ruleset for the given glyph class id. - SkippingGlyphIterator iterator = new(fontMetrics, collection, index, this.LookupFlags, this.MarkFilteringSet); + SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, this.LookupFlags, this.MarkFilteringSet); for (int lookupIndex = 0; lookupIndex < rules.Length; lookupIndex++) { ChainedClassSequenceRuleTable rule = rules[lookupIndex]; @@ -266,7 +266,7 @@ public override bool TryUpdatePosition( SequenceLookupRecord sequenceLookupRecord = rule.SequenceLookupRecords[j]; LookupTable lookup = table.LookupList.LookupTables[sequenceLookupRecord.LookupListIndex]; ushort sequenceIndex = sequenceLookupRecord.SequenceIndex; - if (lookup.TryUpdatePosition(fontMetrics, table, collection, feature, index + sequenceIndex, 1)) + if (lookup.TryUpdatePosition(fontMetrics, table, buffer, feature, index + sequenceIndex, 1)) { hasChanged = true; } @@ -357,18 +357,18 @@ public override void CollectDigest(ref GlyphSetDigest digest) public override bool TryUpdatePosition( FontMetrics fontMetrics, GPosTable table, - GlyphPositioningCollection collection, + ShapingBuffer buffer, Tag feature, int index, int count) { - ushort glyphId = collection[index].GlyphId; + ushort glyphId = buffer[index].GlyphId; if (glyphId == 0) { return false; } - if (!AdvancedTypographicUtils.CheckAllCoverages(fontMetrics, this.LookupFlags, this.MarkFilteringSet, collection, index, count, this.inputCoverageTables, this.backtrackCoverageTables, this.lookaheadCoverageTables)) + if (!AdvancedTypographicUtils.CheckAllCoverages(fontMetrics, this.LookupFlags, this.MarkFilteringSet, buffer, index, count, this.inputCoverageTables, this.backtrackCoverageTables, this.lookaheadCoverageTables)) { return false; } @@ -381,7 +381,7 @@ public override bool TryUpdatePosition( ushort lookupIndex = lookupRecord.LookupListIndex; LookupTable lookup = table.LookupList.LookupTables[lookupIndex]; - if (lookup.TryUpdatePosition(fontMetrics, table, collection, feature, index + sequenceIndex, count - sequenceIndex)) + if (lookup.TryUpdatePosition(fontMetrics, table, buffer, feature, index + sequenceIndex, count - sequenceIndex)) { hasChanged = true; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/NotImplementedSubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/NotImplementedSubTable.cs index c3656b607..70a8edf1c 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/NotImplementedSubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/NotImplementedSubTable.cs @@ -21,7 +21,7 @@ public NotImplementedSubTable() public override bool TryUpdatePosition( FontMetrics fontMetrics, GPosTable table, - GlyphPositioningCollection collection, + ShapingBuffer buffer, Tag feature, int index, int count) diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs index 6031bc003..f9cd76ca7 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs @@ -157,41 +157,41 @@ internal static GPosTable Load(BigEndianBinaryReader reader) } /// - /// Tries to update the positions of glyphs in the collection using GPOS lookup rules. + /// Tries to update the positions of glyphs in the buffer using GPOS lookup rules. /// /// The font metrics. - /// The glyph positioning collection. + /// The glyph positioning buffer. /// When this method returns, indicates whether kerning was applied. /// if any positioning was updated; otherwise, . - public bool TryUpdatePositions(FontMetrics fontMetrics, GlyphPositioningCollection collection, out bool kerned) + public bool TryUpdatePositions(FontMetrics fontMetrics, ShapingBuffer buffer, out bool kerned) { // Set max constraints to prevent OutOfMemoryException or infinite loops from attacks. - int maxCount = AdvancedTypographicUtils.GetMaxAllowableShapingCollectionCount(collection.Count); - int maxOperationsCount = AdvancedTypographicUtils.GetMaxAllowableShapingOperationsCount(collection.Count); + int maxCount = AdvancedTypographicUtils.GetMaxAllowableShapingCollectionCount(buffer.Count); + int maxOperationsCount = AdvancedTypographicUtils.GetMaxAllowableShapingOperationsCount(buffer.Count); int currentOperations = 0; bool maxOperationsReached = false; kerned = false; bool updated = false; - for (int i = 0; i < collection.Count; i++) + for (int i = 0; i < buffer.Count; i++) { - if (!collection.ShouldProcess(fontMetrics, i)) + if (!buffer.ShouldProcess(fontMetrics, i)) { continue; } - ScriptClass current = this.GetScriptClass(CodePoint.GetScriptClass(collection[i].CodePoint)); + ScriptClass current = this.GetScriptClass(CodePoint.GetScriptClass(buffer[i].CodePoint)); int index = i; int count = 1; - while (i < collection.Count - 1) + while (i < buffer.Count - 1) { // We want to assign the same feature lookups to individual sections of the text rather // than the text as a whole to ensure that different language shapers do not interfere // with each other when the text contains multiple languages. int ni = i + 1; - GlyphShapingData nextData = collection[ni]; - if (!collection.ShouldProcess(fontMetrics, ni)) + ref GlyphShapingData nextData = ref buffer[ni]; + if (!buffer.ShouldProcess(fontMetrics, ni)) { break; } @@ -219,24 +219,24 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas } Tag unicodeScriptTag = this.GetUnicodeScriptTag(current); - BaseShaper shaper = ShaperFactory.Create(current, unicodeScriptTag, fontMetrics, collection.TextOptions); + BaseShaper shaper = ShaperFactory.Create(current, unicodeScriptTag, fontMetrics, buffer.TextOptions); if (shaper.MarkZeroingMode == MarkZeroingMode.PreGPos) { - ZeroMarkAdvances(fontMetrics, collection, index, count); + ZeroMarkAdvances(fontMetrics, buffer, index, count); } // Plan positioning features for each glyph. - shaper.Plan(collection, index, count); + shaper.Plan(buffer, index, count); List shapingStages = shaper.GetShapingStages(); - SkippingGlyphIterator iterator = new(fontMetrics, collection, index, default, 0); + SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, default, 0); foreach (ShapingStage stage in shapingStages) { - stage.PreProcessFeature(collection, index, count); + stage.PreProcessFeature(buffer, index, count); Tag featureTag = stage.FeatureTag; var lookupProbe = ShapingProbe.Enter(); - bool found = this.TryGetFeatureLookups(fontMetrics, in featureTag, current, collection.LanguageTags, out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? lookups); + bool found = this.TryGetFeatureLookups(fontMetrics, in featureTag, current, buffer.LanguageTags, out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? lookups); ShapingProbe.Exit(ShapingProbe.LookupResolve, lookupProbe); if (found && lookups is not null) { @@ -246,9 +246,9 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas Tag feature = featureLookup.Feature; // Skip the whole lookup when its coverage cannot intersect any - // glyph id the collection has ever contained; most fonts carry + // glyph id the buffer has ever contained; most fonts carry // many lookups for glyphs a given text never produces. - if (!featureLookup.LookupTable.Digest.MightIntersect(collection.GlyphDigest)) + if (!featureLookup.LookupTable.Digest.MightIntersect(buffer.GlyphDigest)) { continue; } @@ -256,7 +256,7 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas // Resolve the feature's mask bit once per lookup; the per-glyph // gate below is then a single bitwise AND against the glyph's // enabled mask. - ulong featureMask = collection.FeatureMap.GetMask(feature); + ulong featureMask = buffer.FeatureMap.GetMask(feature); LookupTable featureLookupTable = featureLookup.LookupTable; iterator.Reset(index, featureLookupTable.LookupFlags, featureLookupTable.MarkFilteringSet); long featureStart = ShapingProbe.Timestamp(); @@ -273,14 +273,14 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas // The digest cheaply rejects glyphs no subtable of this // lookup can affect; a maybe falls through to the exact // coverage test inside. - GlyphShapingData glyphData = collection[iterator.Index]; + ref GlyphShapingData glyphData = ref buffer[iterator.Index]; if ((glyphData.FeatureMask & featureMask) == 0 || !featureLookupTable.Digest.MightContain(glyphData.GlyphId)) { iterator.Next(); continue; } - bool success = featureLookup.LookupTable.TryUpdatePosition(fontMetrics, this, collection, featureLookup.Feature, iterator.Index, count - (iterator.Index - index)); + bool success = featureLookup.LookupTable.TryUpdatePosition(fontMetrics, this, buffer, featureLookup.Feature, iterator.Index, count - (iterator.Index - index)); featureApplies++; kerned |= success && (feature == KernTag || feature == VKernTag); updated |= success; @@ -291,18 +291,18 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas } } - stage.PostProcessFeature(collection, index, count); + stage.PostProcessFeature(buffer, index, count); } EndLookups: if (shaper.MarkZeroingMode == MarkZeroingMode.PostGpos) { - ZeroMarkAdvances(fontMetrics, collection, index, count); + ZeroMarkAdvances(fontMetrics, buffer, index, count); } - FixCursiveAttachment(collection, index, count); - FixMarkAttachment(collection, index, count); - UpdatePositions(collection, index, count); + FixCursiveAttachment(buffer, index, count); + FixMarkAttachment(buffer, index, count); + UpdatePositions(buffer, index, count); if (i >= maxCount || maxOperationsReached) { @@ -568,16 +568,16 @@ private ScriptClass GetScriptClass(ScriptClass current) /// /// Fixes cursive attachment positioning by propagating Y (or X for vertical) offsets. /// - /// The glyph positioning collection. + /// The glyph positioning buffer. /// The starting index. /// The number of glyphs to process. - private static void FixCursiveAttachment(GlyphPositioningCollection collection, int index, int count) + private static void FixCursiveAttachment(ShapingBuffer buffer, int index, int count) { - LayoutMode layoutMode = collection.TextOptions.LayoutMode; + LayoutMode layoutMode = buffer.TextOptions.LayoutMode; for (int i = 0; i < count; i++) { int currentIndex = i + index; - GlyphShapingData data = collection[currentIndex]; + ref GlyphShapingData data = ref buffer[currentIndex]; if (data.CursiveAttachment != -1) { int j = data.CursiveAttachment + currentIndex; @@ -586,7 +586,7 @@ private static void FixCursiveAttachment(GlyphPositioningCollection collection, return; } - GlyphShapingData cursiveData = collection[j]; + ref GlyphShapingData cursiveData = ref buffer[j]; if (!AdvancedTypographicUtils.IsVerticalGlyph(data.CodePoint, layoutMode)) { data.Bounds.Y += cursiveData.Bounds.Y; @@ -602,38 +602,35 @@ private static void FixCursiveAttachment(GlyphPositioningCollection collection, /// /// Fixes mark attachment positioning by propagating offsets from base glyphs. /// - /// The glyph positioning collection. + /// The glyph positioning buffer. /// The starting index. /// The number of glyphs to process. - private static void FixMarkAttachment(GlyphPositioningCollection collection, int index, int count) + private static void FixMarkAttachment(ShapingBuffer buffer, int index, int count) { for (int i = 0; i < count; i++) { int currentIndex = i + index; - GlyphShapingData data = collection[currentIndex]; + ref GlyphShapingData data = ref buffer[currentIndex]; if (data.MarkAttachment != -1) { int j = data.MarkAttachment; - GlyphShapingData markData = collection[j]; - data.Bounds.X += markData.Bounds.X; - data.Bounds.Y += markData.Bounds.Y; + data.Bounds.X += buffer[j].Bounds.X; + data.Bounds.Y += buffer[j].Bounds.Y; if (data.Direction == TextDirection.LeftToRight) { for (int k = j; k < currentIndex; k++) { - markData = collection[k]; - data.Bounds.X -= markData.Bounds.Width; - data.Bounds.Y -= markData.Bounds.Height; + data.Bounds.X -= buffer[k].Bounds.Width; + data.Bounds.Y -= buffer[k].Bounds.Height; } } else { for (int k = j + 1; k < currentIndex + 1; k++) { - markData = collection[k]; - data.Bounds.X += markData.Bounds.Width; - data.Bounds.Y += markData.Bounds.Height; + data.Bounds.X += buffer[k].Bounds.Width; + data.Bounds.Y += buffer[k].Bounds.Height; } } } @@ -644,16 +641,16 @@ private static void FixMarkAttachment(GlyphPositioningCollection collection, int /// Zeros the advance widths and heights for mark glyphs within the specified range. /// /// The font metrics. - /// The glyph positioning collection. + /// The glyph positioning buffer. /// The starting index. /// The number of glyphs to process. - private static void ZeroMarkAdvances(FontMetrics fontMetrics, GlyphPositioningCollection collection, int index, int count) + private static void ZeroMarkAdvances(FontMetrics fontMetrics, ShapingBuffer buffer, int index, int count) { for (int i = 0; i < count; i++) { int currentIndex = i + index; - GlyphShapingData data = collection[currentIndex]; - if (AdvancedTypographicUtils.IsMarkGlyph(fontMetrics, data.GlyphId, data)) + ref GlyphShapingData data = ref buffer[currentIndex]; + if (AdvancedTypographicUtils.IsMarkGlyph(fontMetrics, data.GlyphId, ref data)) { data.Bounds.Width = 0; data.Bounds.Height = 0; @@ -662,16 +659,16 @@ private static void ZeroMarkAdvances(FontMetrics fontMetrics, GlyphPositioningCo } /// - /// Updates glyph positions in the collection for the specified range. + /// Updates glyph positions in the buffer for the specified range. /// - /// The glyph positioning collection. + /// The glyph positioning buffer. /// The starting index. /// The number of glyphs to process. - private static void UpdatePositions(GlyphPositioningCollection collection, int index, int count) + private static void UpdatePositions(ShapingBuffer buffer, int index, int count) { for (int i = 0; i < count; i++) { - collection.UpdatePosition(i + index); + buffer.UpdatePosition(i + index); } } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs index 910705b71..691ff2c7f 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs @@ -188,24 +188,24 @@ public static LookupTable Load(BigEndianBinaryReader reader, long offset) } /// - /// Attempts to perform a glyph substitution at the specified index in the collection. + /// Attempts to perform a glyph substitution at the specified index in the buffer. /// /// The font metrics. /// The GSUB table. - /// The glyph substitution collection. + /// The glyph substitution buffer. /// The feature tag to apply. - /// The index in the collection at which to attempt substitution. + /// The index in the buffer at which to attempt substitution. /// The number of glyphs in the input sequence to consider. /// if a substitution was performed; otherwise, . public bool TrySubstitution( FontMetrics fontMetrics, GSubTable table, - GlyphSubstitutionCollection collection, + ShapingBuffer buffer, Tag feature, int index, int count) { - ushort glyphId = collection[index].GlyphId; + ushort glyphId = buffer[index].GlyphId; foreach (LookupSubTable subTable in this.LookupSubTables) { // A glyph outside the subtable's digest cannot match its coverage, so the @@ -221,7 +221,7 @@ public bool TrySubstitution( ShapingProbe.SubTableProbes++; } - if (subTable.TrySubstitution(fontMetrics, table, collection, feature, index, count)) + if (subTable.TrySubstitution(fontMetrics, table, buffer, feature, index, count)) { // A lookup is finished for a glyph after the client locates the target // glyph or glyph context and performs a substitution, if specified. @@ -304,19 +304,19 @@ protected LookupSubTable(LookupFlags lookupFlags, ushort markFilteringSet) public virtual void CollectDigest(ref GlyphSetDigest digest) => digest.AddAll(); /// - /// Attempts to perform a glyph substitution at the specified index in the collection. + /// Attempts to perform a glyph substitution at the specified index in the buffer. /// /// The font metrics. /// The GSUB table. - /// The glyph substitution collection. + /// The glyph substitution buffer. /// The feature tag to apply. - /// The index in the collection at which to attempt substitution. + /// The index in the buffer at which to attempt substitution. /// The number of glyphs in the input sequence to consider. /// if a substitution was performed; otherwise, . public abstract bool TrySubstitution( FontMetrics fontMetrics, GSubTable table, - GlyphSubstitutionCollection collection, + ShapingBuffer buffer, Tag feature, int index, int count); diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs index 6a8b9735a..eb00dd957 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs @@ -100,12 +100,12 @@ public static LookupType1Format1SubTable Load(BigEndianBinaryReader reader, long public override bool TrySubstitution( FontMetrics fontMetrics, GSubTable table, - GlyphSubstitutionCollection collection, + ShapingBuffer buffer, Tag feature, int index, int count) { - ushort glyphId = collection[index].GlyphId; + ushort glyphId = buffer[index].GlyphId; if (glyphId == 0) { return false; @@ -113,7 +113,7 @@ public override bool TrySubstitution( if (this.coverageTable.CoverageIndexOf(glyphId) > -1) { - collection.Replace(index, (ushort)(glyphId + this.deltaGlyphId), feature); + buffer.Replace(index, (ushort)(glyphId + this.deltaGlyphId), feature); return true; } @@ -190,12 +190,12 @@ public static LookupType1Format2SubTable Load(BigEndianBinaryReader reader, long public override bool TrySubstitution( FontMetrics fontMetrics, GSubTable table, - GlyphSubstitutionCollection collection, + ShapingBuffer buffer, Tag feature, int index, int count) { - ushort glyphId = collection[index].GlyphId; + ushort glyphId = buffer[index].GlyphId; if (glyphId == 0) { return false; @@ -205,7 +205,7 @@ public override bool TrySubstitution( if (offset > -1 && offset < this.substituteGlyphs.Length) { - collection.Replace(index, this.substituteGlyphs[offset], feature); + buffer.Replace(index, this.substituteGlyphs[offset], feature); return true; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs index 737bd3426..6a8a86f13 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs @@ -122,12 +122,12 @@ public static LookupType2Format1SubTable Load(BigEndianBinaryReader reader, long public override bool TrySubstitution( FontMetrics fontMetrics, GSubTable table, - GlyphSubstitutionCollection collection, + ShapingBuffer buffer, Tag feature, int index, int count) { - ushort glyphId = collection[index].GlyphId; + ushort glyphId = buffer[index].GlyphId; if (glyphId == 0) { return false; @@ -137,7 +137,7 @@ public override bool TrySubstitution( if (offset > -1 && offset < this.sequenceTables.Length) { - collection.Replace(index, this.sequenceTables[offset].SubstituteGlyphs, feature); + buffer.Replace(index, this.sequenceTables[offset].SubstituteGlyphs, feature); return true; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs index 78cf9649f..408da8b5c 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs @@ -121,12 +121,12 @@ public static LookupType3Format1SubTable Load(BigEndianBinaryReader reader, long public override bool TrySubstitution( FontMetrics fontMetrics, GSubTable table, - GlyphSubstitutionCollection collection, + ShapingBuffer buffer, Tag feature, int index, int count) { - ushort glyphId = collection[index].GlyphId; + ushort glyphId = buffer[index].GlyphId; if (glyphId == 0) { return false; @@ -139,7 +139,7 @@ public override bool TrySubstitution( // TODO: We're just choosing the first alternative here. // It looks like the choice is arbitrary and should be determined by // the client. - collection.Replace(index, this.alternateSetTables[offset].AlternateGlyphs[0], feature); + buffer.Replace(index, this.alternateSetTables[offset].AlternateGlyphs[0], feature); return true; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs index cd052f175..01946fbb3 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs @@ -152,12 +152,12 @@ public static LookupType4Format1SubTable Load(BigEndianBinaryReader reader, long public override bool TrySubstitution( FontMetrics fontMetrics, GSubTable table, - GlyphSubstitutionCollection collection, + ShapingBuffer buffer, Tag feature, int index, int count) { - ushort glyphId = collection[index].GlyphId; + ushort glyphId = buffer[index].GlyphId; if (glyphId == 0) { return false; @@ -170,7 +170,7 @@ public override bool TrySubstitution( } LigatureSetTable ligatureSetTable = this.ligatureSetTables[offset]; - SkippingGlyphIterator iterator = new(fontMetrics, collection, index, this.LookupFlags, this.MarkFilteringSet); + SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, this.LookupFlags, this.MarkFilteringSet); Span matchBuffer = stackalloc int[AdvancedTypographicUtils.MaxContextLength]; for (int i = 0; i < ligatureSetTable.Ligatures.Length; i++) { @@ -211,16 +211,16 @@ public override bool TrySubstitution( // the new ligature with a component value of 2. // // This in fact happened to a font... See https://bugzilla.gnome.org/show_bug.cgi?id=437633 - GlyphShapingData data = collection[index]; - GlyphShapingClass shapingClass = AdvancedTypographicUtils.GetGlyphShapingClass(fontMetrics, glyphId, data); + ref GlyphShapingData data = ref buffer[index]; + GlyphShapingClass shapingClass = AdvancedTypographicUtils.GetGlyphShapingClass(fontMetrics, glyphId, ref data); bool isBaseLigature = shapingClass.IsBase; bool isMarkLigature = shapingClass.IsMark; Span matches = matchBuffer[..Math.Min(ligatureTable.ComponentGlyphs.Length, matchBuffer.Length)]; for (int j = 0; j < matches.Length && isMarkLigature; j++) { - GlyphShapingData match = collection[matches[j]]; - if (!AdvancedTypographicUtils.IsMarkGlyph(fontMetrics, match.GlyphId, match)) + ref GlyphShapingData match = ref buffer[matches[j]]; + if (!AdvancedTypographicUtils.IsMarkGlyph(fontMetrics, match.GlyphId, ref match)) { isBaseLigature = false; isMarkLigature = false; @@ -230,7 +230,7 @@ public override bool TrySubstitution( bool isLigature = !isBaseLigature && !isMarkLigature; - int ligatureId = isLigature ? 0 : collection.LigatureId++; + int ligatureId = isLigature ? 0 : buffer.LigatureId++; int lastLigatureId = data.LigatureId; int lastComponentCount = data.CodePointCount; int currentComponentCount = lastComponentCount; @@ -249,7 +249,7 @@ public override bool TrySubstitution( { while (idx < matchIndex) { - GlyphShapingData current = collection[idx]; + ref GlyphShapingData current = ref buffer[idx]; int currentLC = current.LigatureComponent == -1 ? 1 : current.LigatureComponent; int ligatureComponent = currentComponentCount - lastComponentCount + Math.Min(currentLC, lastComponentCount); current.LigatureId = ligatureId; @@ -259,7 +259,7 @@ public override bool TrySubstitution( } } - GlyphShapingData last = collection[idx]; + ref GlyphShapingData last = ref buffer[idx]; lastLigatureId = last.LigatureId; lastComponentCount = last.CodePointCount; currentComponentCount += lastComponentCount; @@ -273,7 +273,7 @@ public override bool TrySubstitution( int followingCount = count - (idx - index); for (int j = idx; j < followingCount; j++) { - GlyphShapingData current = collection[j]; + ref GlyphShapingData current = ref buffer[j]; if (current.LigatureId == lastLigatureId) { int currentLC = current.LigatureComponent == -1 ? 1 : current.LigatureComponent; @@ -289,7 +289,7 @@ public override bool TrySubstitution( } // Delete the matched glyphs, and replace the current glyph with the ligature glyph - collection.Replace(index, matches, ligatureTable.GlyphId, ligatureId, feature); + buffer.Replace(index, matches, ligatureTable.GlyphId, ligatureId, feature); return true; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs index ad2bb34e2..f5b88ec00 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs @@ -86,12 +86,12 @@ public static LookupType5Format1SubTable Load(BigEndianBinaryReader reader, long public override bool TrySubstitution( FontMetrics fontMetrics, GSubTable table, - GlyphSubstitutionCollection collection, + ShapingBuffer buffer, Tag feature, int index, int count) { - ushort glyphId = collection[index].GlyphId; + ushort glyphId = buffer[index].GlyphId; if (glyphId == 0) { return false; @@ -106,7 +106,7 @@ public override bool TrySubstitution( // TODO: Check this. // https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#example-7-contextual-substitution-format-1 SequenceRuleSetTable ruleSetTable = this.seqRuleSetTables[offset]; - SkippingGlyphIterator iterator = new(fontMetrics, collection, index, this.LookupFlags, this.MarkFilteringSet); + SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, this.LookupFlags, this.MarkFilteringSet); foreach (SequenceRuleTable ruleTable in ruleSetTable.SequenceRuleTables) { int remaining = count - 1; @@ -129,7 +129,7 @@ public override bool TrySubstitution( this.LookupFlags, this.MarkFilteringSet, ruleTable.SequenceLookupRecords, - collection, + buffer, index, count); } @@ -203,12 +203,12 @@ public static LookupType5Format2SubTable Load(BigEndianBinaryReader reader, long public override bool TrySubstitution( FontMetrics fontMetrics, GSubTable table, - GlyphSubstitutionCollection collection, + ShapingBuffer buffer, Tag feature, int index, int count) { - ushort glyphId = collection[index].GlyphId; + ushort glyphId = buffer[index].GlyphId; if (glyphId == 0) { return false; @@ -233,7 +233,7 @@ public override bool TrySubstitution( return false; } - SkippingGlyphIterator iterator = new(fontMetrics, collection, index, this.LookupFlags, this.MarkFilteringSet); + SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, this.LookupFlags, this.MarkFilteringSet); foreach (ClassSequenceRuleTable ruleTable in ruleSetTable.SequenceRuleTables) { int remaining = count - 1; @@ -256,7 +256,7 @@ public override bool TrySubstitution( this.LookupFlags, this.MarkFilteringSet, ruleTable.SequenceLookupRecords, - collection, + buffer, index, count); } @@ -333,19 +333,19 @@ public override void CollectDigest(ref GlyphSetDigest digest) public override bool TrySubstitution( FontMetrics fontMetrics, GSubTable table, - GlyphSubstitutionCollection collection, + ShapingBuffer buffer, Tag feature, int index, int count) { - ushort glyphId = collection[index].GlyphId; + ushort glyphId = buffer[index].GlyphId; if (glyphId == 0) { return false; } // https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#53-context-substitution-format-3-coverage-based-glyph-contexts - SkippingGlyphIterator iterator = new(fontMetrics, collection, index, this.LookupFlags, this.MarkFilteringSet); + SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, this.LookupFlags, this.MarkFilteringSet); if (!AdvancedTypographicUtils.MatchCoverageSequence(iterator, this.coverageTables, index, index + count)) { return false; @@ -359,7 +359,7 @@ public override bool TrySubstitution( this.LookupFlags, this.MarkFilteringSet, this.sequenceLookupRecords, - collection, + buffer, index, count); } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs index 80f843183..e5bae3b95 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs @@ -89,14 +89,14 @@ public static LookupType6Format1SubTable Load(BigEndianBinaryReader reader, long public override bool TrySubstitution( FontMetrics fontMetrics, GSubTable table, - GlyphSubstitutionCollection collection, + ShapingBuffer buffer, Tag feature, int index, int count) { // 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].GlyphId; + ushort glyphId = buffer[index].GlyphId; if (glyphId == 0) { return false; @@ -115,7 +115,7 @@ public override bool TrySubstitution( } // Apply ruleset for the given glyph id. - SkippingGlyphIterator iterator = new(fontMetrics, collection, index, this.LookupFlags, this.MarkFilteringSet); + SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, this.LookupFlags, this.MarkFilteringSet); ChainedSequenceRuleSetTable seqRuleSet = this.seqRuleSetTables[offset]; ChainedSequenceRuleTable[] rules = seqRuleSet.SequenceRuleTables; for (int i = 0; i < rules.Length; i++) @@ -133,7 +133,7 @@ public override bool TrySubstitution( this.LookupFlags, this.MarkFilteringSet, ruleTable.SequenceLookupRecords, - collection, + buffer, index, count); } @@ -236,14 +236,14 @@ public static LookupType6Format2SubTable Load(BigEndianBinaryReader reader, long public override bool TrySubstitution( FontMetrics fontMetrics, GSubTable table, - GlyphSubstitutionCollection collection, + ShapingBuffer buffer, Tag feature, int index, int count) { // 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].GlyphId; + ushort glyphId = buffer[index].GlyphId; if (glyphId == 0) { return false; @@ -265,7 +265,7 @@ public override bool TrySubstitution( } // Apply ruleset for the given glyph class id. - SkippingGlyphIterator iterator = new(fontMetrics, collection, index, this.LookupFlags, this.MarkFilteringSet); + SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, this.LookupFlags, this.MarkFilteringSet); for (int lookupIndex = 0; lookupIndex < rules.Length; lookupIndex++) { ChainedClassSequenceRuleTable ruleTable = rules[lookupIndex]; @@ -282,7 +282,7 @@ public override bool TrySubstitution( this.LookupFlags, this.MarkFilteringSet, ruleTable.SequenceLookupRecords, - collection, + buffer, index, count); } @@ -386,12 +386,12 @@ public override void CollectDigest(ref GlyphSetDigest digest) public override bool TrySubstitution( FontMetrics fontMetrics, GSubTable table, - GlyphSubstitutionCollection collection, + ShapingBuffer buffer, Tag feature, int index, int count) { - ushort glyphId = collection[index].GlyphId; + ushort glyphId = buffer[index].GlyphId; if (glyphId == 0) { return false; @@ -401,7 +401,7 @@ public override bool TrySubstitution( fontMetrics, this.LookupFlags, this.MarkFilteringSet, - collection, + buffer, index, count, this.inputCoverageTables, @@ -419,7 +419,7 @@ public override bool TrySubstitution( this.LookupFlags, this.MarkFilteringSet, this.sequenceLookupRecords, - collection, + buffer, index, count); } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType8SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType8SubTable.cs index beca5b323..534eb1f02 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType8SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType8SubTable.cs @@ -155,13 +155,13 @@ public static LookupType8Format1SubTable Load(BigEndianBinaryReader reader, long public override bool TrySubstitution( FontMetrics fontMetrics, GSubTable table, - GlyphSubstitutionCollection collection, + ShapingBuffer buffer, Tag feature, int index, 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].GlyphId; + ushort glyphId = buffer[index].GlyphId; if (glyphId == 0) { return false; @@ -175,7 +175,7 @@ public override bool TrySubstitution( for (int i = 0; i < this.backtrackCoverageTables.Length; ++i) { - ushort id = collection[index - 1 - i].GlyphId; + ushort id = buffer[index - 1 - i].GlyphId; if (id == 0 || this.backtrackCoverageTables[i].CoverageIndexOf(id) < 0) { return false; @@ -184,7 +184,7 @@ public override bool TrySubstitution( for (int i = 0; i < this.lookaheadCoverageTables.Length; ++i) { - ushort id = collection[index + i].GlyphId; + ushort id = buffer[index + i].GlyphId; if (id == 0 || this.lookaheadCoverageTables[i].CoverageIndexOf(id) < 0) { return false; @@ -195,7 +195,7 @@ public override bool TrySubstitution( bool hasChanged = false; for (int i = 0; i < this.substituteGlyphIds.Length; i++) { - collection.Replace(index + i, this.substituteGlyphIds[i], feature); + buffer.Replace(index + i, this.substituteGlyphIds[i], feature); hasChanged = true; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/NotImplementedSubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/NotImplementedSubTable.cs index 0265292fd..d12071514 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/NotImplementedSubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/NotImplementedSubTable.cs @@ -21,7 +21,7 @@ public NotImplementedSubTable() public override bool TrySubstitution( FontMetrics fontMetrics, GSubTable table, - GlyphSubstitutionCollection collection, + ShapingBuffer buffer, Tag feature, int index, int count) diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs index c34908635..4bab9783b 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs @@ -147,31 +147,31 @@ internal static GSubTable Load(BigEndianBinaryReader reader) } /// - /// Applies glyph substitution to the collection using GSUB lookup rules. + /// Applies glyph substitution to the buffer using GSUB lookup rules. /// /// The font metrics. - /// The glyph substitution collection. - public void ApplySubstitution(FontMetrics fontMetrics, GlyphSubstitutionCollection collection) + /// The glyph substitution buffer. + public void ApplySubstitution(FontMetrics fontMetrics, ShapingBuffer buffer) { // Set max constraints to prevent OutOfMemoryException or infinite loops from attacks. - int maxCount = AdvancedTypographicUtils.GetMaxAllowableShapingCollectionCount(collection.Count); - int maxOperationsCount = AdvancedTypographicUtils.GetMaxAllowableShapingOperationsCount(collection.Count); + int maxCount = AdvancedTypographicUtils.GetMaxAllowableShapingCollectionCount(buffer.Count); + int maxOperationsCount = AdvancedTypographicUtils.GetMaxAllowableShapingOperationsCount(buffer.Count); int currentOperations = 0; - for (int i = 0; i < collection.Count; i++) + for (int i = 0; i < buffer.Count; i++) { // Choose a shaper based on the script. // This determines which features to apply to which glyphs. - ScriptClass current = this.GetScriptClass(CodePoint.GetScriptClass(collection[i].CodePoint)); + ScriptClass current = this.GetScriptClass(CodePoint.GetScriptClass(buffer[i].CodePoint)); int index = i; int count = 1; - while (i < collection.Count - 1) + while (i < buffer.Count - 1) { // We want to assign the same feature lookups to individual sections of the text rather // than the text as a whole to ensure that different language shapers do not interfere // with each other when the text contains multiple languages. - ScriptClass next = this.GetScriptClass(CodePoint.GetScriptClass(collection[i + 1].CodePoint)); + ScriptClass next = this.GetScriptClass(CodePoint.GetScriptClass(buffer[i + 1].CodePoint)); if (next != current && current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClass.Inherited && next is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClass.Inherited) @@ -194,26 +194,26 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas } Tag unicodeScriptTag = this.GetUnicodeScriptTag(current); - BaseShaper shaper = ShaperFactory.Create(current, unicodeScriptTag, fontMetrics, collection.TextOptions); + BaseShaper shaper = ShaperFactory.Create(current, unicodeScriptTag, fontMetrics, buffer.TextOptions); // Plan substitution features for each glyph. // Shapers can adjust the count during initialization and feature processing so we must capture // the current count to allow resetting indexes and processing counts. - int collectionCount = collection.Count; - shaper.Plan(collection, index, count); - int delta = collection.Count - collectionCount; + int collectionCount = buffer.Count; + shaper.Plan(buffer, index, count); + int delta = buffer.Count - collectionCount; i += delta; count += delta; List stages = shaper.GetShapingStages(); - SkippingGlyphIterator iterator = new(fontMetrics, collection, index, default, 0); + SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, default, 0); foreach (ShapingStage stage in stages) { - collectionCount = collection.Count; - stage.PreProcessFeature(collection, index, count); + collectionCount = buffer.Count; + stage.PreProcessFeature(buffer, index, count); - // Account for substitutions changing the length of the collection. - delta = collection.Count - collectionCount; + // Account for substitutions changing the length of the buffer. + delta = buffer.Count - collectionCount; count += delta; i += delta; @@ -221,7 +221,7 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas this.ApplyFeature( fontMetrics, - collection, + buffer, ref iterator, in featureTag, current, @@ -233,11 +233,11 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas maxOperationsCount, ref currentOperations); - collectionCount = collection.Count; - stage.PostProcessFeature(collection, index, count); + collectionCount = buffer.Count; + stage.PostProcessFeature(buffer, index, count); - // Account for substitutions changing the length of the collection. - delta = collection.Count - collectionCount; + // Account for substitutions changing the length of the buffer. + delta = buffer.Count - collectionCount; count += delta; i += delta; } @@ -245,23 +245,23 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas } /// - /// Applies a specific feature's lookups to the glyph substitution collection. + /// Applies a specific feature's lookups to the glyph substitution buffer. /// /// The font metrics. - /// The glyph substitution collection. + /// The glyph substitution buffer. /// The skipping glyph iterator. /// The feature tag to apply. /// The current script class. - /// The starting index in the collection. + /// The starting index in the buffer. /// The number of glyphs to process (updated by substitutions). /// The outer loop index (updated by substitutions). - /// The tracked collection count (updated by substitutions). - /// The maximum allowable collection count. + /// The tracked buffer count (updated by substitutions). + /// The maximum allowable buffer count. /// The maximum allowable operations count. /// The current operations counter. internal void ApplyFeature( FontMetrics fontMetrics, - GlyphSubstitutionCollection collection, + ShapingBuffer buffer, ref SkippingGlyphIterator iterator, in Tag featureTag, ScriptClass current, @@ -274,7 +274,7 @@ internal void ApplyFeature( ref int currentOperations) { var lookupProbe = ShapingProbe.Enter(); - bool found = this.TryGetFeatureLookups(fontMetrics, in featureTag, current, collection.LanguageTags, out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? lookups); + bool found = this.TryGetFeatureLookups(fontMetrics, in featureTag, current, buffer.LanguageTags, out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? lookups); ShapingProbe.Exit(ShapingProbe.LookupResolve, lookupProbe); if (found && lookups is not null) { @@ -284,14 +284,14 @@ internal void ApplyFeature( Tag feature = featureLookup.Feature; // Skip the whole lookup when its coverage cannot intersect any glyph id - // the collection has ever contained; most fonts carry many lookups for + // the buffer has ever contained; most fonts carry many lookups for // glyphs a given text never produces. if (ShapingProbe.Enabled) { ShapingProbe.LookupsConsidered++; } - if (!featureLookup.LookupTable.Digest.MightIntersect(collection.GlyphDigest)) + if (!featureLookup.LookupTable.Digest.MightIntersect(buffer.GlyphDigest)) { if (ShapingProbe.Enabled) { @@ -303,7 +303,7 @@ internal void ApplyFeature( // Resolve the feature's mask bit once per lookup; the per-glyph gate // below is then a single bitwise AND against the glyph's enabled mask. - ulong featureMask = collection.FeatureMap.GetMask(feature); + ulong featureMask = buffer.FeatureMap.GetMask(feature); LookupTable featureLookupTable = featureLookup.LookupTable; iterator.Reset(index, featureLookupTable.LookupFlags, featureLookupTable.MarkFilteringSet); long featureStart = ShapingProbe.Timestamp(); @@ -311,7 +311,7 @@ internal void ApplyFeature( while (iterator.Index < index + count) { - if (collection.Count >= maxCount || currentOperations++ >= maxOperationsCount) + if (buffer.Count >= maxCount || currentOperations++ >= maxOperationsCount) { return; } @@ -323,7 +323,7 @@ internal void ApplyFeature( // The digest cheaply rejects glyphs no subtable of this lookup can // affect; a maybe falls through to the exact coverage test inside. - GlyphShapingData glyphData = collection[iterator.Index]; + ref GlyphShapingData glyphData = ref buffer[iterator.Index]; if ((glyphData.FeatureMask & featureMask) == 0 || !featureLookupTable.Digest.MightContain(glyphData.GlyphId)) { iterator.Next(); @@ -335,13 +335,13 @@ internal void ApplyFeature( ShapingProbe.SubstitutionAttempts++; } - collectionCount = collection.Count; - featureLookup.LookupTable.TrySubstitution(fontMetrics, this, collection, featureLookup.Feature, iterator.Index, count - (iterator.Index - index)); + collectionCount = buffer.Count; + featureLookup.LookupTable.TrySubstitution(fontMetrics, this, buffer, featureLookup.Feature, iterator.Index, count - (iterator.Index - index)); featureApplies++; iterator.Next(); - // Account for substitutions changing the length of the collection. - int delta = collection.Count - collectionCount; + // Account for substitutions changing the length of the buffer. + int delta = buffer.Count - collectionCount; count += delta; i += delta; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs index e3036a849..db1ba36a0 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs @@ -100,36 +100,36 @@ public ArabicShaper(ScriptClass script, TextOptions textOptions) } /// - protected override void PlanFeatures(GlyphShapingCollection collection, int index, int count) + protected override void PlanFeatures(ShapingBuffer buffer, int index, int count) { - this.AddFeature(collection, index, count, CcmpTag); - this.AddFeature(collection, index, count, LoclTag); + this.AddFeature(buffer, index, count, CcmpTag); + this.AddFeature(buffer, index, count, LoclTag); - this.AddFeature(collection, index, count, IsolTag, false); - this.AddFeature(collection, index, count, FinaTag, false); - this.AddFeature(collection, index, count, Fin2Tag, false); - this.AddFeature(collection, index, count, Fin3Tag, false); - this.AddFeature(collection, index, count, MediTag, false); - this.AddFeature(collection, index, count, Med2Tag, false); - this.AddFeature(collection, index, count, InitTag, false); + this.AddFeature(buffer, index, count, IsolTag, false); + this.AddFeature(buffer, index, count, FinaTag, false); + this.AddFeature(buffer, index, count, Fin2Tag, false); + this.AddFeature(buffer, index, count, Fin3Tag, false); + this.AddFeature(buffer, index, count, MediTag, false); + this.AddFeature(buffer, index, count, Med2Tag, false); + this.AddFeature(buffer, index, count, InitTag, false); // HarfBuzz plans these as Arabic-script features, independently of the // generic horizontal feature list. Horizontal runs already get them from // DefaultShaper; forced vertical Arabic needs them here as well. - if (collection.TextOptions.LayoutMode.IsVertical()) + if (buffer.TextOptions.LayoutMode.IsVertical()) { - this.AddFeature(collection, index, count, CaltTag); - this.AddFeature(collection, index, count, LigaTag); - this.AddFeature(collection, index, count, CligTag); + this.AddFeature(buffer, index, count, CaltTag); + this.AddFeature(buffer, index, count, LigaTag); + this.AddFeature(buffer, index, count, CligTag); } - this.AddFeature(collection, index, count, MsetTag); + this.AddFeature(buffer, index, count, MsetTag); } /// - protected override void AssignFeatures(GlyphShapingCollection collection, int index, int count) + protected override void AssignFeatures(ShapingBuffer buffer, int index, int count) { - base.AssignFeatures(collection, index, count); + base.AssignFeatures(buffer, index, count); int prev = -1; int state = 0; @@ -138,7 +138,7 @@ protected override void AssignFeatures(GlyphShapingCollection collection, int in // Apply the state machine to map glyphs to features. for (int i = 0; i < count; i++) { - GlyphShapingData data = collection[i + index]; + ref GlyphShapingData data = ref buffer[i + index]; ArabicJoiningClass joiningClass = CodePoint.GetArabicJoiningClass(data.CodePoint); ArabicJoiningType joiningType = joiningClass.JoiningType; if (joiningType == ArabicJoiningType.Transparent) @@ -168,25 +168,25 @@ protected override void AssignFeatures(GlyphShapingCollection collection, int in switch (actions[i]) { case Fina: - collection.EnableShapingFeature(i + index, FinaTag); + buffer.EnableShapingFeature(i + index, FinaTag); break; case Fin2: - collection.EnableShapingFeature(i + index, Fin2Tag); + buffer.EnableShapingFeature(i + index, Fin2Tag); break; case Fin3: - collection.EnableShapingFeature(i + index, Fin3Tag); + buffer.EnableShapingFeature(i + index, Fin3Tag); break; case Isol: - collection.EnableShapingFeature(i + index, IsolTag); + buffer.EnableShapingFeature(i + index, IsolTag); break; case Init: - collection.EnableShapingFeature(i + index, InitTag); + buffer.EnableShapingFeature(i + index, InitTag); break; case Medi: - collection.EnableShapingFeature(i + index, MediTag); + buffer.EnableShapingFeature(i + index, MediTag); break; case Med2: - collection.EnableShapingFeature(i + index, Med2Tag); + buffer.EnableShapingFeature(i + index, Med2Tag); break; } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs index e7d6db387..1a4c610cd 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs @@ -22,64 +22,64 @@ internal abstract class BaseShaper public MarkZeroingMode MarkZeroingMode { get; protected set; } /// - /// Assigns the features to each glyph within the collection. + /// Assigns the features to each glyph within the buffer. /// - /// The glyph shaping collection. + /// The glyph shaping buffer. /// The zero-based index of the elements to assign. /// The number of elements to assign. - public void Plan(GlyphShapingCollection collection, int index, int count) + public void Plan(ShapingBuffer buffer, int index, int count) { - int collectionCount = collection.Count; + int collectionCount = buffer.Count; - this.PlanPreprocessingFeatures(collection, index, count); + this.PlanPreprocessingFeatures(buffer, index, count); - RecalculateCount(collection, ref collectionCount, ref count); + RecalculateCount(buffer, ref collectionCount, ref count); - this.PlanFeatures(collection, index, count); + this.PlanFeatures(buffer, index, count); - RecalculateCount(collection, ref collectionCount, ref count); + RecalculateCount(buffer, ref collectionCount, ref count); - this.PlanPostprocessingFeatures(collection, index, count); + this.PlanPostprocessingFeatures(buffer, index, count); - RecalculateCount(collection, ref collectionCount, ref count); + RecalculateCount(buffer, ref collectionCount, ref count); - this.AssignFeatures(collection, index, count); + this.AssignFeatures(buffer, index, count); } /// - /// Assigns the features to each glyph within the collection. + /// Assigns the features to each glyph within the buffer. /// - /// The glyph shaping collection. + /// The glyph shaping buffer. /// The zero-based index of the elements to assign. /// The number of elements to assign. - protected abstract void PlanFeatures(GlyphShapingCollection collection, int index, int count); + protected abstract void PlanFeatures(ShapingBuffer buffer, int index, int count); /// - /// Assigns the preprocessing features to each glyph within the collection. + /// Assigns the preprocessing features to each glyph within the buffer. /// - /// The glyph shaping collection. + /// The glyph shaping buffer. /// The zero-based index of the elements to assign. /// The number of elements to assign. - protected abstract void PlanPreprocessingFeatures(GlyphShapingCollection collection, int index, int count); + protected abstract void PlanPreprocessingFeatures(ShapingBuffer buffer, int index, int count); /// - /// Assigns the postprocessing features to each glyph within the collection. + /// Assigns the postprocessing features to each glyph within the buffer. /// - /// The glyph shaping collection. + /// The glyph shaping buffer. /// The zero-based index of the elements to assign. /// The number of elements to assign. - protected abstract void PlanPostprocessingFeatures(GlyphShapingCollection collection, int index, int count); + protected abstract void PlanPostprocessingFeatures(ShapingBuffer buffer, int index, int count); /// - /// Assigns the shaper specific substitution features to each glyph within the collection. + /// Assigns the shaper specific substitution features to each glyph within the buffer. /// - /// The glyph shaping collection. + /// The glyph shaping buffer. /// The zero-based index of the elements to assign. /// The number of elements to assign. - protected abstract void AssignFeatures(GlyphShapingCollection collection, int index, int count); + protected abstract void AssignFeatures(ShapingBuffer buffer, int index, int count); /// - /// Gets the ordered collection of shaping stages for this shaper. The concrete + /// Gets the ordered buffer of shaping stages for this shaper. The concrete /// list type lets the per-section stage walk enumerate without interface /// dispatch or a boxed enumerator. /// @@ -87,15 +87,15 @@ public void Plan(GlyphShapingCollection collection, int index, int count) public abstract List GetShapingStages(); /// - /// Recalculates the count when the collection size changes during shaping. + /// Recalculates the count when the buffer size changes during shaping. /// - /// The glyph shaping collection. - /// The previous collection count, updated to the current count. + /// The glyph shaping buffer. + /// The previous buffer count, updated to the current count. /// The element count, adjusted by the size delta. - private static void RecalculateCount(GlyphShapingCollection collection, ref int oldCount, ref int count) + private static void RecalculateCount(ShapingBuffer buffer, ref int oldCount, ref int count) { - // If the collection has changed size we need to recalculate the count. - int delta = collection.Count - oldCount; + // If the buffer has changed size we need to recalculate the count. + int delta = buffer.Count - oldCount; count += delta; oldCount += delta; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs index 83ecf0170..901b453da 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs @@ -118,30 +118,30 @@ protected DefaultShaper(ScriptClass script, MarkZeroingMode markZeroingMode, Tex } /// - protected override void PlanFeatures(GlyphShapingCollection collection, int index, int count) + protected override void PlanFeatures(ShapingBuffer buffer, int index, int count) { } /// - protected override void PlanPreprocessingFeatures(GlyphShapingCollection collection, int index, int count) + protected override void PlanPreprocessingFeatures(ShapingBuffer buffer, int index, int count) { // Add variation Features. - this.AddFeature(collection, index, count, RvnrTag); + this.AddFeature(buffer, index, count, RvnrTag); // Add directional features. for (int i = index; i < index + count; i++) { - GlyphShapingData shapingData = collection[i]; + ref GlyphShapingData shapingData = ref buffer[i]; if (shapingData.Direction == TextDirection.LeftToRight) { - this.AddFeature(collection, i, 1, LtraTag); - this.AddFeature(collection, i, 1, LtrmTag); + this.AddFeature(buffer, i, 1, LtraTag); + this.AddFeature(buffer, i, 1, LtrmTag); } else { - this.AddFeature(collection, i, 1, RtlaTag); - this.AddFeature(collection, i, 1, RtlmTag); + this.AddFeature(buffer, i, 1, RtlaTag); + this.AddFeature(buffer, i, 1, RtlmTag); } } @@ -150,20 +150,20 @@ protected override void PlanPreprocessingFeatures(GlyphShapingCollection collect } /// - protected override void PlanPostprocessingFeatures(GlyphShapingCollection collection, int index, int count) + protected override void PlanPostprocessingFeatures(ShapingBuffer buffer, int index, int count) { // Add common features. - this.AddFeature(collection, index, count, CcmpTag); - this.AddFeature(collection, index, count, LoclTag); - this.AddFeature(collection, index, count, RligTag); - this.AddFeature(collection, index, count, MarkTag); - this.AddFeature(collection, index, count, MkmkTag); + this.AddFeature(buffer, index, count, CcmpTag); + this.AddFeature(buffer, index, count, LoclTag); + this.AddFeature(buffer, index, count, RligTag); + this.AddFeature(buffer, index, count, MarkTag); + this.AddFeature(buffer, index, count, MkmkTag); - LayoutMode layoutMode = collection.TextOptions.LayoutMode; + LayoutMode layoutMode = buffer.TextOptions.LayoutMode; bool isVerticalLayout = false; for (int i = index; i < index + count; i++) { - GlyphShapingData shapingData = collection[i]; + ref GlyphShapingData shapingData = ref buffer[i]; isVerticalLayout |= AdvancedTypographicUtils.IsVerticalGlyph(shapingData.CodePoint, layoutMode); } @@ -171,12 +171,12 @@ protected override void PlanPostprocessingFeatures(GlyphShapingCollection collec if (!isVerticalLayout) { // Add horizontal features. - this.AddFeature(collection, index, count, CaltTag); - this.AddFeature(collection, index, count, CligTag); - this.AddFeature(collection, index, count, LigaTag); - this.AddFeature(collection, index, count, RcltTag); - this.AddFeature(collection, index, count, CursTag); - this.AddFeature(collection, index, count, KernTag); + this.AddFeature(buffer, index, count, CaltTag); + this.AddFeature(buffer, index, count, CligTag); + this.AddFeature(buffer, index, count, LigaTag); + this.AddFeature(buffer, index, count, RcltTag); + this.AddFeature(buffer, index, count, CursTag); + this.AddFeature(buffer, index, count, KernTag); } else { @@ -188,7 +188,7 @@ protected override void PlanPostprocessingFeatures(GlyphShapingCollection collec // matter which script/langsys it is listed (or not) under. // See various bugs referenced from: // https://github.com/harfbuzz/harfbuzz/issues/63 - this.AddFeature(collection, index, count, VertTag); + this.AddFeature(buffer, index, count, VertTag); } // Add user defined features. @@ -197,27 +197,27 @@ protected override void PlanPostprocessingFeatures(GlyphShapingCollection collec // We've already dealt with fractional features. if (feature != FracTag && feature != NumrTag && feature != DnomTag) { - this.AddFeature(collection, index, count, feature); + this.AddFeature(buffer, index, count, feature); } } } /// - protected override void AssignFeatures(GlyphShapingCollection collection, int index, int count) + protected override void AssignFeatures(ShapingBuffer buffer, int index, int count) { // TODO: We shouldn't be relying on the feature list // User defined fractional features require special treatment. // https://docs.microsoft.com/en-us/typography/opentype/spec/features_fj#tag-frac if (this.HasFractions()) { - this.AssignFractionalFeatures(collection, index, count); + this.AssignFractionalFeatures(buffer, index, count); } } /// - /// Adds a shaping feature to the specified range of glyphs in the collection and registers the corresponding shaping stage. + /// Adds a shaping feature to the specified range of glyphs in the buffer and registers the corresponding shaping stage. /// - /// The glyph shaping collection. + /// The glyph shaping buffer. /// The zero-based index of the first element. /// The number of elements. /// The feature tag to add. @@ -225,13 +225,13 @@ protected override void AssignFeatures(GlyphShapingCollection collection, int in /// An optional action to invoke before the feature is applied. /// An optional action to invoke after the feature is applied. protected void AddFeature( - GlyphShapingCollection collection, + ShapingBuffer buffer, int index, int count, Tag feature, bool enabled = true, - Action? preAction = null, - Action? postAction = null) + Action? preAction = null, + Action? postAction = null) { if (this.kerningMode == KerningMode.None) { @@ -241,7 +241,7 @@ protected void AddFeature( } } - collection.AddShapingFeatureRange(index, count, new TagEntry(feature, enabled)); + buffer.AddShapingFeatureRange(index, count, new TagEntry(feature, enabled)); // First registration wins, matching the previous set semantics: a duplicate // tag keeps the originally supplied pre and post actions. @@ -263,15 +263,15 @@ protected void AddFeature( /// /// Assigns fractional feature tags (numerator, denominator, fraction) to glyphs forming fraction sequences. /// - /// The glyph shaping collection. + /// The glyph shaping buffer. /// The zero-based index of the first element. /// The number of elements. - private void AssignFractionalFeatures(GlyphShapingCollection collection, int index, int count) + private void AssignFractionalFeatures(ShapingBuffer buffer, int index, int count) { // Enable contextual fractions. for (int i = index; i < index + count; i++) { - GlyphShapingData shapingData = collection[i]; + ref GlyphShapingData shapingData = ref buffer[i]; if (shapingData.CodePoint == FractionSlash || shapingData.CodePoint == Slash) { int start = i; @@ -280,29 +280,29 @@ private void AssignFractionalFeatures(GlyphShapingCollection collection, int ind // Apply numerator. if (start > 0) { - shapingData = collection[start - 1]; - while (start > 0 && CodePoint.IsDigit(shapingData.CodePoint)) + CodePoint numeratorCodePoint = buffer[start - 1].CodePoint; + while (start > 0 && CodePoint.IsDigit(numeratorCodePoint)) { - this.AddFeature(collection, start - 1, 1, NumrTag); - this.AddFeature(collection, start - 1, 1, FracTag); + this.AddFeature(buffer, start - 1, 1, NumrTag); + this.AddFeature(buffer, start - 1, 1, FracTag); start--; } } // Apply denominator. - if (end < collection.Count) + if (end < buffer.Count) { - shapingData = collection[end]; - while (end < collection.Count && CodePoint.IsDigit(shapingData.CodePoint)) + CodePoint denominatorCodePoint = buffer[end].CodePoint; + while (end < buffer.Count && CodePoint.IsDigit(denominatorCodePoint)) { - this.AddFeature(collection, end, 1, DnomTag); - this.AddFeature(collection, end, 1, FracTag); + this.AddFeature(buffer, end, 1, DnomTag); + this.AddFeature(buffer, end, 1, FracTag); end++; } } // Apply fraction slash. - this.AddFeature(collection, i, 1, FracTag); + this.AddFeature(buffer, i, 1, FracTag); i = end - 1; } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs index 783638181..06fc1923d 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs @@ -124,26 +124,26 @@ public HangulShaper(ScriptClass script, TextOptions textOptions, FontMetrics fon => this.fontMetrics = fontMetrics; /// - protected override void PlanFeatures(GlyphShapingCollection collection, int index, int count) + protected override void PlanFeatures(ShapingBuffer buffer, 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); + this.AddFeature(buffer, index, count, LjmoTag, false); + this.AddFeature(buffer, index, count, VjmoTag, false); + this.AddFeature(buffer, index, count, TjmoTag, false); } /// - protected override void AssignFeatures(GlyphShapingCollection collection, int index, int count) + protected override void AssignFeatures(ShapingBuffer buffer, int index, int 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); + buffer.DisableShapingFeature(i, CaltTag); } // Apply the state machine to map glyphs to features. - if (collection is GlyphSubstitutionCollection substitutionCollection) + if (buffer.Role == ShapingBufferRole.Substitution) { // Allocate a small buffer for composition operations. Span compositionBuffer = stackalloc ushort[3]; @@ -152,12 +152,12 @@ protected override void AssignFeatures(GlyphShapingCollection collection, int in int state = 0; for (int i = 0; i < count; i++) { - if (i + index >= substitutionCollection.Count) + if (i + index >= buffer.Count) { break; } - GlyphShapingData data = substitutionCollection[i + index]; + ref GlyphShapingData data = ref buffer[i + index]; CodePoint codePoint = data.CodePoint; int type = GetSyllableType(codePoint); byte[] actionsWithState = StateTable[state, type]; @@ -172,7 +172,7 @@ protected override void AssignFeatures(GlyphShapingCollection collection, int in // Decompose the composed syllable if it is not supported by the font. if (data.GlyphId == 0) { - i = this.DecomposeGlyph(substitutionCollection, data, i, compositionBuffer); + i = this.DecomposeGlyph(buffer, ref data, i, compositionBuffer); } break; @@ -180,19 +180,19 @@ protected override void AssignFeatures(GlyphShapingCollection collection, int in case Compose: // Found a decomposed syllable. Try to compose if supported by the font. - i = this.ComposeGlyph(substitutionCollection, i, type, compositionBuffer); + i = this.ComposeGlyph(buffer, i, type, compositionBuffer); break; case ToneMark: // Got a valid syllable, followed by a tone mark. Move the tone mark to the beginning of the syllable. - this.ReOrderToneMark(substitutionCollection, data, i); + this.ReOrderToneMark(buffer, ref data, i); break; case Invalid: // Tone mark has no valid syllable to attach to, so insert a dotted circle. - i = this.InsertDottedCircle(substitutionCollection, data, i, compositionBuffer); + i = this.InsertDottedCircle(buffer, ref data, i, compositionBuffer); break; } } @@ -204,32 +204,32 @@ protected override void AssignFeatures(GlyphShapingCollection collection, int in // Glyph substitution has handled [de]composition. for (int i = 0; i < count; i++) { - if (i + index >= collection.Count) + if (i + index >= buffer.Count) { break; } - GlyphShapingData data = collection[i + index]; + ref GlyphShapingData data = ref buffer[i + index]; CodePoint codePoint = data.CodePoint; switch (GetSyllableType(codePoint)) { case L: - collection.EnableShapingFeature(i, LjmoTag); + buffer.EnableShapingFeature(i, LjmoTag); break; case V: - collection.EnableShapingFeature(i, VjmoTag); + buffer.EnableShapingFeature(i, VjmoTag); break; case T: - collection.EnableShapingFeature(i, TjmoTag); + buffer.EnableShapingFeature(i, TjmoTag); break; case LV: - collection.EnableShapingFeature(i, LjmoTag); - collection.EnableShapingFeature(i, VjmoTag); + buffer.EnableShapingFeature(i, LjmoTag); + buffer.EnableShapingFeature(i, VjmoTag); break; case LVT: - collection.EnableShapingFeature(i, LjmoTag); - collection.EnableShapingFeature(i, VjmoTag); - collection.EnableShapingFeature(i, TjmoTag); + buffer.EnableShapingFeature(i, LjmoTag); + buffer.EnableShapingFeature(i, VjmoTag); + buffer.EnableShapingFeature(i, TjmoTag); break; } } @@ -277,12 +277,12 @@ private static int GetSyllableLength(CodePoint codePoint) /// /// Decomposes a precomposed Hangul syllable into its constituent Jamo glyphs. /// - /// The glyph substitution collection. + /// The glyph substitution buffer. /// The shaping data for the composed syllable. /// The index of the glyph to decompose. /// A buffer for temporary glyph ID storage. /// The updated index after decomposition. - private int DecomposeGlyph(GlyphSubstitutionCollection collection, GlyphShapingData data, int index, Span compositinoBuffer) + private int DecomposeGlyph(ShapingBuffer buffer, ref GlyphShapingData data, int index, Span compositinoBuffer) { // Decompose the syllable into a sequence of glyphs. int s = data.CodePoint.Value - HangulBase; @@ -309,9 +309,9 @@ private int DecomposeGlyph(GlyphSubstitutionCollection collection, GlyphShapingD ii[1] = vjmo; ii[0] = ljmo; - collection.Replace(index, ii, KnownFeatureTags.GlyphCompositionDecomposition); - collection.EnableShapingFeature(index, LjmoTag); - collection.EnableShapingFeature(index + 1, VjmoTag); + buffer.Replace(index, ii, KnownFeatureTags.GlyphCompositionDecomposition); + buffer.EnableShapingFeature(index, LjmoTag); + buffer.EnableShapingFeature(index + 1, VjmoTag); return index + 1; } @@ -320,29 +320,29 @@ private int DecomposeGlyph(GlyphSubstitutionCollection collection, GlyphShapingD iii[1] = vjmo; iii[0] = ljmo; - collection.Replace(index, iii, KnownFeatureTags.GlyphCompositionDecomposition); - collection.EnableShapingFeature(index, LjmoTag); - collection.EnableShapingFeature(index + 1, VjmoTag); - collection.EnableShapingFeature(index + 2, TjmoTag); + buffer.Replace(index, iii, KnownFeatureTags.GlyphCompositionDecomposition); + buffer.EnableShapingFeature(index, LjmoTag); + buffer.EnableShapingFeature(index + 1, VjmoTag); + buffer.EnableShapingFeature(index + 2, TjmoTag); return index + 2; } /// /// Attempts to compose decomposed Jamo into a precomposed Hangul syllable. /// - /// The glyph substitution collection. - /// The current index in the collection. + /// The glyph substitution buffer. + /// The current index in the buffer. /// The syllable type of the current glyph. /// A buffer for glyph IDs during composition. /// The updated index after composition. - private int ComposeGlyph(GlyphSubstitutionCollection collection, int index, int type, Span compositionBuffer) + private int ComposeGlyph(ShapingBuffer buffer, int index, int type, Span compositionBuffer) { if (index == 0) { return index; } - GlyphShapingData prev = collection[index - 1]; + ref GlyphShapingData prev = ref buffer[index - 1]; CodePoint prevCodePoint = prev.CodePoint; int prevType = GetSyllableType(prevCodePoint); @@ -372,8 +372,8 @@ private int ComposeGlyph(GlyphSubstitutionCollection collection, int index, int tjmo = index; } - CodePoint l = collection[ljmo].CodePoint; - CodePoint v = collection[vjmo].CodePoint; + CodePoint l = buffer[ljmo].CodePoint; + CodePoint v = buffer[vjmo].CodePoint; // Make sure L and V are combining characters if (IsCombiningL(l) && IsCombiningV(v)) @@ -382,7 +382,7 @@ private int ComposeGlyph(GlyphSubstitutionCollection collection, int index, int } } - CodePoint t = tjmo >= 0 ? collection[tjmo].CodePoint : new CodePoint(TBase); + CodePoint t = tjmo >= 0 ? buffer[tjmo].CodePoint : new CodePoint(TBase); if ((lv != default) && (t.Value == TBase || IsCombiningT(t))) { CodePoint s = new(lv.Value + (t.Value - TBase)); @@ -393,8 +393,8 @@ private int ComposeGlyph(GlyphSubstitutionCollection collection, int index, int { int del = prevType == V ? 3 : 2; int idx = index - del + 1; - collection.Replace(idx, del - 1, id, KnownFeatureTags.GlyphCompositionDecomposition); - collection[idx].CodePoint = s; + buffer.Replace(idx, del - 1, id, KnownFeatureTags.GlyphCompositionDecomposition); + buffer[idx].CodePoint = s; return idx; } } @@ -402,17 +402,17 @@ private int ComposeGlyph(GlyphSubstitutionCollection collection, int index, int // Didn't compose (either a non-combining component or unsupported by font). if (ljmo >= 0) { - collection.EnableShapingFeature(ljmo, LjmoTag); + buffer.EnableShapingFeature(ljmo, LjmoTag); } if (vjmo >= 0) { - collection.EnableShapingFeature(vjmo, VjmoTag); + buffer.EnableShapingFeature(vjmo, VjmoTag); } if (tjmo >= 0) { - collection.EnableShapingFeature(tjmo, TjmoTag); + buffer.EnableShapingFeature(tjmo, TjmoTag); } if (prevType == LV) @@ -420,7 +420,7 @@ private int ComposeGlyph(GlyphSubstitutionCollection collection, int index, int // 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. - this.DecomposeGlyph(collection, collection[index - 1], index - 1, compositionBuffer); + this.DecomposeGlyph(buffer, ref buffer[index - 1], index - 1, compositionBuffer); return index + 1; } @@ -430,10 +430,10 @@ private int ComposeGlyph(GlyphSubstitutionCollection collection, int index, int /// /// Reorders a tone mark to the beginning of the preceding syllable. /// - /// The glyph substitution collection. + /// The glyph substitution buffer. /// The shaping data of the tone mark glyph. - /// The index of the tone mark in the collection. - private void ReOrderToneMark(GlyphSubstitutionCollection collection, GlyphShapingData data, int index) + /// The index of the tone mark in the buffer. + private void ReOrderToneMark(ShapingBuffer buffer, ref GlyphShapingData data, int index) { if (index == 0) { @@ -445,28 +445,28 @@ private void ReOrderToneMark(GlyphSubstitutionCollection collection, GlyphShapin FontMetrics fontMetrics = this.fontMetrics; TextAttributes textAttributes = data.TextRun.TextAttributes; TextDecorations textDecorations = data.TextRun.TextDecorations; - LayoutMode layoutMode = collection.TextOptions.LayoutMode; - ColorFontSupport colorFontSupport = collection.TextOptions.ColorFontSupport; + LayoutMode layoutMode = buffer.TextOptions.LayoutMode; + ColorFontSupport colorFontSupport = buffer.TextOptions.ColorFontSupport; if (fontMetrics.TryGetGlyphMetrics(data.CodePoint, textAttributes, textDecorations, layoutMode, colorFontSupport, out FontGlyphMetrics? metrics) && metrics.AdvanceWidth == 0) { return; } - GlyphShapingData prev = collection[index - 1]; + ref GlyphShapingData prev = ref buffer[index - 1]; int len = GetSyllableLength(prev.CodePoint); - collection.MoveGlyph(index, index - len); + buffer.MoveGlyph(index, index - len); } /// /// Inserts a dotted circle glyph as a placeholder for an invalid tone mark that has no syllable to attach to. /// - /// The glyph substitution collection. + /// The glyph substitution buffer. /// The shaping data of the invalid tone mark glyph. - /// The index of the tone mark in the collection. + /// The index of the tone mark in the buffer. /// A buffer for glyph IDs during insertion. /// The updated index after insertion. - private int InsertDottedCircle(GlyphSubstitutionCollection collection, GlyphShapingData data, int index, Span compositionBuffer) + private int InsertDottedCircle(ShapingBuffer buffer, ref GlyphShapingData data, int index, Span compositionBuffer) { bool after = false; FontMetrics fontMetrics = this.fontMetrics; @@ -475,8 +475,8 @@ private int InsertDottedCircle(GlyphSubstitutionCollection collection, GlyphShap { TextAttributes textAttributes = data.TextRun.TextAttributes; TextDecorations textDecorations = data.TextRun.TextDecorations; - LayoutMode layoutMode = collection.TextOptions.LayoutMode; - ColorFontSupport colorFontSupport = collection.TextOptions.ColorFontSupport; + LayoutMode layoutMode = buffer.TextOptions.LayoutMode; + ColorFontSupport colorFontSupport = buffer.TextOptions.ColorFontSupport; if (fontMetrics.TryGetGlyphMetrics(data.CodePoint, textAttributes, textDecorations, layoutMode, colorFontSupport, out FontGlyphMetrics? metrics) && metrics.AdvanceWidth != 0) { @@ -496,7 +496,7 @@ private int InsertDottedCircle(GlyphSubstitutionCollection collection, GlyphShap glyphs[0] = id; } - collection.Replace(index, glyphs, KnownFeatureTags.GlyphCompositionDecomposition); + buffer.Replace(index, glyphs, KnownFeatureTags.GlyphCompositionDecomposition); return index + 1; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HebrewShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HebrewShaper.cs index ec71b5b54..91a63fd55 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HebrewShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HebrewShaper.cs @@ -68,11 +68,11 @@ public HebrewShaper(ScriptClass script, TextOptions textOptions, FontMetrics fon } /// - protected override void AssignFeatures(GlyphShapingCollection collection, int index, int count) + protected override void AssignFeatures(ShapingBuffer buffer, int index, int count) { - base.AssignFeatures(collection, index, count); + base.AssignFeatures(buffer, index, count); - if (collection is not GlyphSubstitutionCollection substitutionCollection) + if (buffer.Role != ShapingBufferRole.Substitution) { return; } @@ -80,13 +80,13 @@ protected override void AssignFeatures(GlyphShapingCollection collection, int in // Step 1: Reorder Hebrew marks. // Swap SHEVA/HIRIQ with following METEG when preceded by PATAH/QAMATS. // https://bugzilla.mozilla.org/show_bug.cgi?id=728866 - ReorderMarks(substitutionCollection, index, count); + ReorderMarks(buffer, index, count); // Step 2: Compose Hebrew presentation forms for legacy fonts. // Only applied when the font lacks GSUB features (proxy for lacking GPOS mark). if (!this.hasGsub) { - ComposeHebrewForms(substitutionCollection, this.fontMetrics, index, count); + ComposeHebrewForms(buffer, this.fontMetrics, index, count); } } @@ -98,17 +98,17 @@ protected override void AssignFeatures(GlyphShapingCollection collection, int in /// and the meteg stress mark. /// /// - /// The glyph substitution collection. + /// The glyph substitution buffer. /// The zero-based start index. /// The number of elements to process. - private static void ReorderMarks(GlyphSubstitutionCollection collection, int index, int count) + private static void ReorderMarks(ShapingBuffer buffer, int index, int count) { int end = index + count; for (int i = index + 2; i < end; i++) { - int c0 = collection[i - 2].CodePoint.Value; - int c1 = collection[i - 1].CodePoint.Value; - int c2 = collection[i].CodePoint.Value; + int c0 = buffer[i - 2].CodePoint.Value; + int c1 = buffer[i - 1].CodePoint.Value; + int c2 = buffer[i].CodePoint.Value; // c0: PATAH (U+05B7) or QAMATS (U+05B8) // c1: SHEVA (U+05B0) or HIRIQ (U+05B4) @@ -116,12 +116,12 @@ private static void ReorderMarks(GlyphSubstitutionCollection collection, int ind if (IsPatahOrQamats(c0) && IsShevaOrHiriq(c1) && IsMetegOrBelow(c2)) { // Swap positions i-1 and i. - GlyphShapingData data1 = collection[i - 1]; - GlyphShapingData data2 = collection[i]; + ref GlyphShapingData data1 = ref buffer[i - 1]; + ref GlyphShapingData data2 = ref buffer[i]; // Swap codepoints and glyph IDs. - (collection[i - 1].CodePoint, collection[i].CodePoint) = (data2.CodePoint, data1.CodePoint); - (collection[i - 1].GlyphId, collection[i].GlyphId) = (data2.GlyphId, data1.GlyphId); + (buffer[i - 1].CodePoint, buffer[i].CodePoint) = (data2.CodePoint, data1.CodePoint); + (buffer[i - 1].GlyphId, buffer[i].GlyphId) = (data2.GlyphId, data1.GlyphId); break; } } @@ -131,23 +131,23 @@ private static void ReorderMarks(GlyphSubstitutionCollection collection, int ind /// Composes Hebrew base + mark sequences into precomposed presentation forms. /// This is a fallback for legacy fonts that lack GPOS mark-to-base positioning. /// - /// The glyph substitution collection. + /// The glyph substitution buffer. /// The font metrics for glyph lookups. /// The zero-based start index. /// The number of elements to process. - private static void ComposeHebrewForms(GlyphSubstitutionCollection collection, FontMetrics fontMetrics, int index, int count) + private static void ComposeHebrewForms(ShapingBuffer buffer, FontMetrics fontMetrics, int index, int count) { int end = index + count; for (int i = index + 1; i < end; i++) { - int a = collection[i - 1].CodePoint.Value; - int b = collection[i].CodePoint.Value; + int a = buffer[i - 1].CodePoint.Value; + int b = buffer[i].CodePoint.Value; int composed = TryCompose(a, b); if (composed != 0 && fontMetrics.TryGetGlyphId(new CodePoint(composed), out ushort composedGlyphId)) { // Replace the two glyphs with the composed form. - collection.Replace(i - 1, 2, composedGlyphId, KnownFeatureTags.GlyphCompositionDecomposition); + buffer.Replace(i - 1, 2, composedGlyphId, KnownFeatureTags.GlyphCompositionDecomposition); end--; i--; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs index b4139cf42..35947cf4a 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs @@ -128,40 +128,40 @@ public IndicShaper(ScriptClass script, Tag unicodeScriptTag, TextOptions textOpt } /// - protected override void PlanFeatures(GlyphShapingCollection collection, int index, int count) + protected override void PlanFeatures(ShapingBuffer buffer, int index, int count) { - this.AddFeature(collection, index, count, LoclTag, preAction: this.SetupSyllables); - this.AddFeature(collection, index, count, CcmpTag); - - this.AddFeature(collection, index, count, NuktTag, preAction: this.InitialReorder); - this.AddFeature(collection, index, count, AkhnTag); - - this.AddFeature(collection, index, count, RphfTag, false); - this.AddFeature(collection, index, count, RkrfTag); - this.AddFeature(collection, index, count, PrefTag, false); - this.AddFeature(collection, index, count, BlwfTag, false); - this.AddFeature(collection, index, count, AbvfTag, false); - this.AddFeature(collection, index, count, HalfTag, false); - this.AddFeature(collection, index, count, PstfTag, false); - this.AddFeature(collection, index, count, VatuTag); - this.AddFeature(collection, index, count, CjctTag); - this.AddFeature(collection, index, count, CfarTag, false, postAction: this.FinalReorder); - - this.AddFeature(collection, index, count, InitTag, false); - this.AddFeature(collection, index, count, PresTag); - this.AddFeature(collection, index, count, AbvsTag); - this.AddFeature(collection, index, count, BlwsTag); - this.AddFeature(collection, index, count, PstsTag); - this.AddFeature(collection, index, count, HalnTag); - this.AddFeature(collection, index, count, DistTag); - this.AddFeature(collection, index, count, AbvmTag); - this.AddFeature(collection, index, count, BlwmTag); + this.AddFeature(buffer, index, count, LoclTag, preAction: this.SetupSyllables); + this.AddFeature(buffer, index, count, CcmpTag); + + this.AddFeature(buffer, index, count, NuktTag, preAction: this.InitialReorder); + this.AddFeature(buffer, index, count, AkhnTag); + + this.AddFeature(buffer, index, count, RphfTag, false); + this.AddFeature(buffer, index, count, RkrfTag); + this.AddFeature(buffer, index, count, PrefTag, false); + this.AddFeature(buffer, index, count, BlwfTag, false); + this.AddFeature(buffer, index, count, AbvfTag, false); + this.AddFeature(buffer, index, count, HalfTag, false); + this.AddFeature(buffer, index, count, PstfTag, false); + this.AddFeature(buffer, index, count, VatuTag); + this.AddFeature(buffer, index, count, CjctTag); + this.AddFeature(buffer, index, count, CfarTag, false, postAction: this.FinalReorder); + + this.AddFeature(buffer, index, count, InitTag, false); + this.AddFeature(buffer, index, count, PresTag); + this.AddFeature(buffer, index, count, AbvsTag); + this.AddFeature(buffer, index, count, BlwsTag); + this.AddFeature(buffer, index, count, PstsTag); + this.AddFeature(buffer, index, count, HalnTag); + this.AddFeature(buffer, index, count, DistTag); + this.AddFeature(buffer, index, count, AbvmTag); + this.AddFeature(buffer, index, count, BlwmTag); } /// - protected override void AssignFeatures(GlyphShapingCollection collection, int index, int count) + protected override void AssignFeatures(ShapingBuffer buffer, int index, int count) { - if (collection is not GlyphSubstitutionCollection substitutionCollection) + if (buffer.Role != ShapingBufferRole.Substitution) { return; } @@ -169,16 +169,16 @@ protected override void AssignFeatures(GlyphShapingCollection collection, int in FontMetrics fontMetrics = this.fontMetrics; // Decompose split matras - Span buffer = stackalloc ushort[16]; + Span decompositionIds = stackalloc ushort[16]; int end = index + count; for (int i = end - 1; i >= index; i--) { - GlyphShapingData data = substitutionCollection[i]; + ref GlyphShapingData data = ref buffer[i]; if ((Decompositions.TryGetValue(data.CodePoint.Value, out int[]? decompositions) || UniversalShapingData.Decompositions.TryGetValue(data.CodePoint.Value, out decompositions)) && decompositions != null) { - Span ids = buffer[..decompositions.Length]; + Span ids = decompositionIds[..decompositions.Length]; bool shouldDecompose = true; for (int j = 0; j < decompositions.Length; j++) { @@ -193,10 +193,10 @@ protected override void AssignFeatures(GlyphShapingCollection collection, int in if (shouldDecompose) { - substitutionCollection.Replace(i, ids, KnownFeatureTags.GlyphCompositionDecomposition); + buffer.Replace(i, ids, KnownFeatureTags.GlyphCompositionDecomposition); for (int j = 0; j < decompositions.Length; j++) { - substitutionCollection[i + j].CodePoint = new(decompositions[j]); + buffer[i + j].CodePoint = new(decompositions[j]); } } } @@ -206,12 +206,12 @@ protected override void AssignFeatures(GlyphShapingCollection collection, int in /// /// Identifies Indic syllables using the state machine and assigns shaping info to each glyph. /// - /// The glyph shaping collection. + /// The glyph shaping buffer. /// The zero-based start index. /// The number of elements to process. - private void SetupSyllables(GlyphShapingCollection collection, int index, int count) + private void SetupSyllables(ShapingBuffer buffer, int index, int count) { - if (collection is not GlyphSubstitutionCollection substitutionCollection) + if (buffer.Role != ShapingBufferRole.Substitution) { return; } @@ -233,7 +233,7 @@ private void SetupSyllables(GlyphShapingCollection collection, int index, int co // CategoryToSymbolId[IndicShapingCategory(codePoint)] performs this mapping, ensuring that // every codepoint is presented to the DFA using the correct compact // symbol index. - CodePoint codePoint = substitutionCollection[i].CodePoint; + CodePoint codePoint = buffer[i].CodePoint; values[i - index] = CategoryToSymbolId[IndicShapingCategory(codePoint)]; } @@ -246,7 +246,7 @@ private void SetupSyllables(GlyphShapingCollection collection, int index, int co ++syllable; for (int i = last; i < match.StartIndex; i++) { - GlyphShapingData data = substitutionCollection[i + index]; + ref GlyphShapingData data = ref buffer[i + index]; data.IndicShapingEngineInfo = new(Categories.X, Positions.End, "non_indic_cluster", syllable); } } @@ -256,7 +256,7 @@ private void SetupSyllables(GlyphShapingCollection collection, int index, int co // Create shaper info. for (int i = match.StartIndex; i <= match.EndIndex; i++) { - GlyphShapingData data = substitutionCollection[i + index]; + ref GlyphShapingData data = ref buffer[i + index]; CodePoint codePoint = data.CodePoint; string syllableType = match.Tags[0]; @@ -281,7 +281,7 @@ private void SetupSyllables(GlyphShapingCollection collection, int index, int co ++syllable; for (int i = last; i < count; i++) { - GlyphShapingData data = substitutionCollection[i + index]; + ref GlyphShapingData data = ref buffer[i + index]; data.IndicShapingEngineInfo = new(Categories.X, Positions.End, "non_indic_cluster", syllable); } } @@ -307,22 +307,22 @@ private static int IndicShapingPosition(CodePoint codePoint) /// Performs the initial reordering pass for Indic syllables, including base consonant /// identification, reph handling, matra reordering, and feature assignment. /// - /// The glyph shaping collection. + /// The glyph shaping buffer. /// The zero-based start index. /// The number of elements to process. - private void InitialReorder(GlyphShapingCollection collection, int index, int count) + private void InitialReorder(ShapingBuffer buffer, int index, int count) { - if (collection is not GlyphSubstitutionCollection substitutionCollection) + if (buffer.Role != ShapingBufferRole.Substitution) { return; } - // Create a reusable temporary substitution collection and buffer to allow checking whether - // certain combinations will be substituted. The probe collection MUST share the + // Create a reusable temporary substitution buffer and probe span to allow checking whether + // certain combinations will be substituted. The probe buffer MUST share the // pass's feature map: the probed glyphs are copies of real glyphs whose feature // masks were assigned by it, and a private map would resolve the probed feature // to a different bit, turning the enable into a no-op. - GlyphSubstitutionCollection tempCollection = new(this.textOptions, substitutionCollection.FeatureMap); + ShapingBuffer tempCollection = new(this.textOptions, buffer.FeatureMap, ShapingBufferRole.Substitution); Span tempBuffer = new GlyphShapingData[3]; ShapingConfiguration indicConfiguration = this.indicConfiguration; @@ -333,7 +333,7 @@ private void InitialReorder(GlyphShapingCollection collection, int index, int co { for (int i = 0; i < count; i++) { - GlyphShapingData data = substitutionCollection[i + index]; + ref GlyphShapingData data = ref buffer[i + index]; IndicShapingEngineInfo? info = data.IndicShapingEngineInfo; if (info?.Position == Positions.Base_C) @@ -355,7 +355,7 @@ private void InitialReorder(GlyphShapingCollection collection, int index, int co int max = index + count; int start = index; - int end = NextSyllable(substitutionCollection, index, max); + int end = NextSyllable(buffer, index, max); if (this.hasBrokenClusters) { @@ -364,7 +364,7 @@ private void InitialReorder(GlyphShapingCollection collection, int index, int co Span glyphs = stackalloc ushort[2]; while (start < max) { - GlyphShapingData data = substitutionCollection[start]; + ref GlyphShapingData data = ref buffer[start]; IndicShapingEngineInfo? dataInfo = data.IndicShapingEngineInfo; string? type = dataInfo?.SyllableType; @@ -374,22 +374,22 @@ private void InitialReorder(GlyphShapingCollection collection, int index, int co int i = start; for (i = start; i < end; i++) { - if (substitutionCollection[i].IndicShapingEngineInfo?.Category != Categories.Repha) + if (buffer[i].IndicShapingEngineInfo?.Category != Categories.Repha) { break; } } - GlyphShapingData current = substitutionCollection[i]; + ref GlyphShapingData current = ref buffer[i]; IndicShapingEngineInfo currentInfo = current.IndicShapingEngineInfo!; glyphs[0] = circleId; glyphs[1] = current.GlyphId; - substitutionCollection.Replace(i, glyphs, KnownFeatureTags.GlyphCompositionDecomposition); + buffer.Replace(i, glyphs, KnownFeatureTags.GlyphCompositionDecomposition); // The dotted circle is now at position i (inherits original shaping info). // Update it to be a dotted circle base. - GlyphShapingData dotted = substitutionCollection[i]; + ref GlyphShapingData dotted = ref buffer[i]; dotted.IndicShapingEngineInfo!.Category = Categories.Dotted_Circle; dotted.IndicShapingEngineInfo.Position = Positions.End; @@ -400,18 +400,18 @@ private void InitialReorder(GlyphShapingCollection collection, int index, int co } start = end; - end = NextSyllable(substitutionCollection, start, max); + end = NextSyllable(buffer, start, max); } start = index; - end = NextSyllable(substitutionCollection, index, max); + end = NextSyllable(buffer, index, max); } } _ = fontMetrics.TryGetGSubTable(out GSubTable? gSubTable); while (start < max) { - GlyphShapingData data = substitutionCollection[start]; + ref GlyphShapingData data = ref buffer[start]; IndicShapingEngineInfo? dataInfo = data.IndicShapingEngineInfo; string? type = dataInfo?.SyllableType; @@ -437,20 +437,20 @@ private void InitialReorder(GlyphShapingCollection collection, int index, int co // base consonants. if (start + 3 <= end && indicConfiguration.RephPosition != Positions.Ra_To_Become_Reph && - gSubTable?.TryGetFeatureLookups(fontMetrics, in RphfTag, this.ScriptClass, substitutionCollection.LanguageTags, out _) == true && - ((indicConfiguration.RephMode == RephMode.Implicit && !IsJoiner(substitutionCollection[start + 2])) || - (indicConfiguration.RephMode == RephMode.Explicit && substitutionCollection[start + 2].IndicShapingEngineInfo?.Category == Categories.ZWJ))) + gSubTable?.TryGetFeatureLookups(fontMetrics, in RphfTag, this.ScriptClass, buffer.LanguageTags, out _) == true && + ((indicConfiguration.RephMode == RephMode.Implicit && !IsJoiner(ref buffer[start + 2])) || + (indicConfiguration.RephMode == RephMode.Explicit && buffer[start + 2].IndicShapingEngineInfo?.Category == Categories.ZWJ))) { // See if it matches the 'rphf' feature. - tempBuffer[2] = substitutionCollection[start + 2]; - tempBuffer[1] = substitutionCollection[start + 1]; - tempBuffer[0] = substitutionCollection[start]; + tempBuffer[2] = buffer[start + 2]; + tempBuffer[1] = buffer[start + 1]; + tempBuffer[0] = buffer[start]; if ((indicConfiguration.RephMode == RephMode.Explicit && this.WouldSubstitute(tempCollection, in RphfTag, tempBuffer)) || this.WouldSubstitute(tempCollection, in RphfTag, tempBuffer[..2])) { limit += 2; - while (limit < end && IsJoiner(substitutionCollection[limit])) + while (limit < end && IsJoiner(ref buffer[limit])) { limit++; } @@ -460,10 +460,10 @@ private void InitialReorder(GlyphShapingCollection collection, int index, int co } } else if (indicConfiguration.RephMode == RephMode.Log_Repha && - substitutionCollection[start].IndicShapingEngineInfo?.Category == Categories.Repha) + buffer[start].IndicShapingEngineInfo?.Category == Categories.Repha) { limit++; - while (limit < end && IsJoiner(substitutionCollection[limit])) + while (limit < end && IsJoiner(ref buffer[limit])) { limit++; } @@ -482,10 +482,10 @@ private void InitialReorder(GlyphShapingCollection collection, int index, int co do { - IndicShapingEngineInfo? prevInfo = substitutionCollection[--i].IndicShapingEngineInfo; + IndicShapingEngineInfo? prevInfo = buffer[--i].IndicShapingEngineInfo; // Until a consonant is found - if (IsConsonant(substitutionCollection[i])) + if (IsConsonant(ref buffer[i])) { // that does not have a below-base or post-base form // (post-base forms have to follow below-base forms), @@ -513,7 +513,7 @@ private void InitialReorder(GlyphShapingCollection collection, int index, int co basePosition = i; } else if (start < i && prevInfo?.Category == Categories.ZWJ && - substitutionCollection[i - 1].IndicShapingEngineInfo?.Category == Categories.H) + buffer[i - 1].IndicShapingEngineInfo?.Category == Categories.H) { // A ZWJ after a Halant stops the base search, and requests an explicit // half form. @@ -535,8 +535,8 @@ private void InitialReorder(GlyphShapingCollection collection, int index, int co for (int i = basePosition + 1; i < end; i++) { - GlyphShapingData c = substitutionCollection[i]; - if (IsConsonant(c) && c.IndicShapingEngineInfo != null) + ref GlyphShapingData c = ref buffer[i]; + if (IsConsonant(ref c) && c.IndicShapingEngineInfo != null) { c.IndicShapingEngineInfo.Position = Positions.Below_C; } @@ -587,7 +587,7 @@ private void InitialReorder(GlyphShapingCollection collection, int index, int co // Reorder characters for (int i = start; i < basePosition; i++) { - IndicShapingEngineInfo? info = substitutionCollection[i].IndicShapingEngineInfo; + IndicShapingEngineInfo? info = buffer[i].IndicShapingEngineInfo; if (info != null) { info.Position = (Positions)Math.Min((int)Positions.Pre_C, (int)info.Position); @@ -596,7 +596,7 @@ private void InitialReorder(GlyphShapingCollection collection, int index, int co if (basePosition < end) { - IndicShapingEngineInfo? info = substitutionCollection[basePosition].IndicShapingEngineInfo; + IndicShapingEngineInfo? info = buffer[basePosition].IndicShapingEngineInfo; if (info != null) { info.Position = Positions.Base_C; @@ -607,12 +607,12 @@ private void InitialReorder(GlyphShapingCollection collection, int index, int co // like in Khmer. for (int i = basePosition + 1; i < end; i++) { - if (substitutionCollection[i].IndicShapingEngineInfo?.Category == Categories.M) + if (buffer[i].IndicShapingEngineInfo?.Category == Categories.M) { for (int j = i + 1; j < end; j++) { - GlyphShapingData c = substitutionCollection[j]; - if (IsConsonant(c) && c.IndicShapingEngineInfo != null) + ref GlyphShapingData c = ref buffer[j]; + if (IsConsonant(ref c) && c.IndicShapingEngineInfo != null) { c.IndicShapingEngineInfo.Position = Positions.Final_C; break; @@ -626,7 +626,7 @@ private void InitialReorder(GlyphShapingCollection collection, int index, int co // Handle beginning Ra if (hasReph) { - GlyphShapingData c = substitutionCollection[start]; + ref GlyphShapingData c = ref buffer[start]; if (c.IndicShapingEngineInfo != null) { c.IndicShapingEngineInfo.Position = Positions.Ra_To_Become_Reph; @@ -654,22 +654,22 @@ private void InitialReorder(GlyphShapingCollection collection, int index, int co bool disallowDoubleHalants = this.ScriptClass != ScriptClass.Malayalam; for (int i = basePosition + 1; i < end; i++) { - if (substitutionCollection[i].IndicShapingEngineInfo?.Category == Categories.H) + if (buffer[i].IndicShapingEngineInfo?.Category == Categories.H) { int j; for (j = end - 1; j > i; j--) { - GlyphShapingData c = substitutionCollection[j]; - if (IsConsonant(c) || (disallowDoubleHalants && c.IndicShapingEngineInfo?.Category == Categories.H)) + ref GlyphShapingData c = ref buffer[j]; + if (IsConsonant(ref c) || (disallowDoubleHalants && c.IndicShapingEngineInfo?.Category == Categories.H)) { break; } } - if (j > i && substitutionCollection[j].IndicShapingEngineInfo?.Category != Categories.H) + if (j > i && buffer[j].IndicShapingEngineInfo?.Category != Categories.H) { // Move Halant to after last consonant. - substitutionCollection.MoveGlyph(i, j); + buffer.MoveGlyph(i, j); } break; @@ -681,7 +681,7 @@ private void InitialReorder(GlyphShapingCollection collection, int index, int co Positions lastPosition = Positions.Start; for (int i = start; i < end; i++) { - IndicShapingEngineInfo? info = substitutionCollection[i].IndicShapingEngineInfo; + IndicShapingEngineInfo? info = buffer[i].IndicShapingEngineInfo; if (info != null) { if ((FlagUnsafe(info.Category) & (JoinerFlags | Flag(Categories.N) | Flag(Categories.RS) | Flag(Categories.CM) | (HalantOrCoengFlags & FlagUnsafe(info.Category)))) != 0) @@ -698,7 +698,7 @@ private void InitialReorder(GlyphShapingCollection collection, int index, int co // TEST: U+0D9A,U+0DDA for (int j = i; j > start; j--) { - Positions? pos = substitutionCollection[j - 1].IndicShapingEngineInfo?.Position; + Positions? pos = buffer[j - 1].IndicShapingEngineInfo?.Position; if (pos is not null and not Positions.Pre_M) { info.Position = pos.Value; @@ -713,9 +713,9 @@ private void InitialReorder(GlyphShapingCollection collection, int index, int co // so they move together during reordering. if (info.Category == Categories.MPst && i > start - && substitutionCollection[i - 1].IndicShapingEngineInfo?.Category == Categories.SM) + && buffer[i - 1].IndicShapingEngineInfo?.Category == Categories.SM) { - substitutionCollection[i - 1].IndicShapingEngineInfo!.Position = info.Position; + buffer[i - 1].IndicShapingEngineInfo!.Position = info.Position; } lastPosition = info.Position; @@ -728,15 +728,15 @@ private void InitialReorder(GlyphShapingCollection collection, int index, int co int last = basePosition; for (int i = basePosition + 1; i < end; i++) { - GlyphShapingData current = substitutionCollection[i]; + ref GlyphShapingData current = ref buffer[i]; IndicShapingEngineInfo? info = current.IndicShapingEngineInfo; if (info != null) { - if (IsConsonant(current)) + if (IsConsonant(ref current)) { for (int j = last + 1; j < i; j++) { - IndicShapingEngineInfo? jInfo = substitutionCollection[j].IndicShapingEngineInfo; + IndicShapingEngineInfo? jInfo = buffer[j].IndicShapingEngineInfo; if (jInfo?.Position < Positions.SMVD) { jInfo.Position = info.Position; @@ -752,7 +752,7 @@ private void InitialReorder(GlyphShapingCollection collection, int index, int co } } - substitutionCollection.Sort(start, end, (a, b) => + buffer.Sort(start, end, (a, b) => { int pa = a.IndicShapingEngineInfo?.Position != null ? (int)a.IndicShapingEngineInfo.Position : 0; int pb = b.IndicShapingEngineInfo?.Position != null ? (int)b.IndicShapingEngineInfo.Position : 0; @@ -762,7 +762,7 @@ private void InitialReorder(GlyphShapingCollection collection, int index, int co // Find base again for (int i = start; i < end; i++) { - if (substitutionCollection[i].IndicShapingEngineInfo?.Position == Positions.Base_C) + if (buffer[i].IndicShapingEngineInfo?.Position == Positions.Base_C) { basePosition = i; break; @@ -774,32 +774,32 @@ private void InitialReorder(GlyphShapingCollection collection, int index, int co // Reph. for (int i = start; i < end; i++) { - IndicShapingEngineInfo? info = substitutionCollection[i].IndicShapingEngineInfo; + IndicShapingEngineInfo? info = buffer[i].IndicShapingEngineInfo; if (info?.Position != Positions.Ra_To_Become_Reph) { break; } - substitutionCollection.EnableShapingFeature(i, RphfTag); + buffer.EnableShapingFeature(i, RphfTag); } // Pre-base bool blwf = !this.isOldSpec && indicConfiguration.BlwfMode == BlwfMode.Pre_And_Post; for (int i = start; i < basePosition; i++) { - substitutionCollection.EnableShapingFeature(i, HalfTag); + buffer.EnableShapingFeature(i, HalfTag); if (blwf) { - substitutionCollection.EnableShapingFeature(i, BlwfTag); + buffer.EnableShapingFeature(i, BlwfTag); } } // Post-base for (int i = basePosition + 1; i < end; i++) { - substitutionCollection.EnableShapingFeature(i, AbvfTag); - substitutionCollection.EnableShapingFeature(i, PstfTag); - substitutionCollection.EnableShapingFeature(i, BlwfTag); + buffer.EnableShapingFeature(i, AbvfTag); + buffer.EnableShapingFeature(i, PstfTag); + buffer.EnableShapingFeature(i, BlwfTag); } if (this.isOldSpec && this.ScriptClass == ScriptClass.Devanagari) @@ -823,30 +823,30 @@ private void InitialReorder(GlyphShapingCollection collection, int index, int co // Test case: U+0924,U+094D,U+0930,U+094d,U+200D,U+0915 for (int i = start; i + 1 < basePosition; i++) { - if (substitutionCollection[i].IndicShapingEngineInfo?.Category == Categories.Ra && - substitutionCollection[i + 1].IndicShapingEngineInfo?.Category == Categories.H && - (i + 1 == basePosition || substitutionCollection[i + 2].IndicShapingEngineInfo?.Category == Categories.ZWJ)) + if (buffer[i].IndicShapingEngineInfo?.Category == Categories.Ra && + buffer[i + 1].IndicShapingEngineInfo?.Category == Categories.H && + (i + 1 == basePosition || buffer[i + 2].IndicShapingEngineInfo?.Category == Categories.ZWJ)) { - substitutionCollection.EnableShapingFeature(i, BlwfTag); - substitutionCollection.EnableShapingFeature(i + 1, BlwfTag); + buffer.EnableShapingFeature(i, BlwfTag); + buffer.EnableShapingFeature(i + 1, BlwfTag); } } } const int prefLen = 2; if (basePosition + prefLen < end && - gSubTable?.TryGetFeatureLookups(fontMetrics, in PrefTag, this.ScriptClass, substitutionCollection.LanguageTags, out _) == true) + gSubTable?.TryGetFeatureLookups(fontMetrics, in PrefTag, this.ScriptClass, buffer.LanguageTags, out _) == true) { // Find a Halant,Ra sequence and mark it for pre-base reordering processing. for (int i = basePosition + 1; i + prefLen - 1 < end; i++) { - tempBuffer[1] = substitutionCollection[i + 1]; - tempBuffer[0] = substitutionCollection[i]; + tempBuffer[1] = buffer[i + 1]; + tempBuffer[0] = buffer[i]; if (this.WouldSubstitute(tempCollection, in PrefTag, tempBuffer[..2])) { for (int j = 0; j < prefLen; j++) { - substitutionCollection.EnableShapingFeature(i++, PrefTag); + buffer.EnableShapingFeature(i++, PrefTag); } // Mark the subsequent stuff with 'cfar'. Used in Khmer. @@ -854,11 +854,11 @@ private void InitialReorder(GlyphShapingCollection collection, int index, int co // This allows distinguishing the following cases with MS Khmer fonts: // U+1784,U+17D2,U+179A,U+17D2,U+1782 // U+1784,U+17D2,U+1782,U+17D2,U+179A - if (gSubTable.TryGetFeatureLookups(fontMetrics, in CfarTag, this.ScriptClass, substitutionCollection.LanguageTags, out _)) + if (gSubTable.TryGetFeatureLookups(fontMetrics, in CfarTag, this.ScriptClass, buffer.LanguageTags, out _)) { while (i < end) { - substitutionCollection.EnableShapingFeature(i, CfarTag); + buffer.EnableShapingFeature(i, CfarTag); i++; } } @@ -871,8 +871,8 @@ private void InitialReorder(GlyphShapingCollection collection, int index, int co // Apply ZWJ/ZWNJ effects for (int i = start + 1; i < end; i++) { - GlyphShapingData current = substitutionCollection[i]; - if (IsJoiner(current)) + ref GlyphShapingData current = ref buffer[i]; + if (IsJoiner(ref current)) { bool nonJoiner = current.IndicShapingEngineInfo?.Category == Categories.ZWNJ; int j = i; @@ -888,16 +888,16 @@ private void InitialReorder(GlyphShapingCollection collection, int index, int co // A ZWNJ disables HALF. if (nonJoiner) { - substitutionCollection.DisableShapingFeature(j, HalfTag); + buffer.DisableShapingFeature(j, HalfTag); } } - while (j > start && !IsConsonant(substitutionCollection[j])); + while (j > start && !IsConsonant(ref buffer[j])); } } Increment: start = end; - end = NextSyllable(substitutionCollection, start, max); + end = NextSyllable(buffer, start, max); } } @@ -905,25 +905,25 @@ private void InitialReorder(GlyphShapingCollection collection, int index, int co /// Determines the positional class of a consonant by testing whether it would be /// substituted by below-base, post-base, or pre-base features. /// - /// A temporary substitution collection for testing. + /// A temporary substitution buffer for testing. /// The consonant and virama glyph data to test. /// The consonant's positional class. - private Positions ConsonantPosition(GlyphSubstitutionCollection collection, ReadOnlySpan data) + private Positions ConsonantPosition(ShapingBuffer buffer, ReadOnlySpan data) { - if (this.WouldSubstitute(collection, in BlwfTag, data[..2]) || - this.WouldSubstitute(collection, in BlwfTag, data.Slice(1, 2))) + if (this.WouldSubstitute(buffer, in BlwfTag, data[..2]) || + this.WouldSubstitute(buffer, in BlwfTag, data.Slice(1, 2))) { return Positions.Below_C; } - if (this.WouldSubstitute(collection, in PstfTag, data[..2]) || - this.WouldSubstitute(collection, in PstfTag, data.Slice(1, 2))) + if (this.WouldSubstitute(buffer, in PstfTag, data[..2]) || + this.WouldSubstitute(buffer, in PstfTag, data.Slice(1, 2))) { return Positions.Post_C; } - if (this.WouldSubstitute(collection, in PrefTag, data[..2]) || - this.WouldSubstitute(collection, in PrefTag, data.Slice(1, 2))) + if (this.WouldSubstitute(buffer, in PrefTag, data[..2]) || + this.WouldSubstitute(buffer, in PrefTag, data.Slice(1, 2))) { return Positions.Post_C; } @@ -934,37 +934,37 @@ private Positions ConsonantPosition(GlyphSubstitutionCollection collection, Read /// /// Tests whether applying a specific feature to the given glyphs would produce a substitution. /// - /// A temporary substitution collection for testing. + /// A temporary substitution buffer for testing. /// The feature tag to test. - /// The glyph data to test. + /// The glyph data to test. /// if a substitution would occur. - private bool WouldSubstitute(GlyphSubstitutionCollection collection, in Tag featureTag, ReadOnlySpan buffer) + private bool WouldSubstitute(ShapingBuffer buffer, in Tag featureTag, ReadOnlySpan glyphs) { - collection.Clear(); - for (int i = 0; i < buffer.Length; i++) + buffer.Clear(); + for (int i = 0; i < glyphs.Length; i++) { - collection.AddGlyph(buffer[i], i); - collection.EnableShapingFeature(i, featureTag); + buffer.AddGlyph(glyphs[i], i); + buffer.EnableShapingFeature(i, featureTag); } FontMetrics fontMetrics = this.fontMetrics; if (fontMetrics.TryGetGSubTable(out GSubTable? gSubTable)) { const int index = 0; - SkippingGlyphIterator iterator = new(fontMetrics, collection, index, default, 0); - int initialCount = collection.Count; + SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, default, 0); + int initialCount = buffer.Count; int collectionCount = initialCount; int count = initialCount - index; int i = index; // Set max constraints to prevent OutOfMemoryException or infinite loops from attacks. - int maxCount = AdvancedTypographicUtils.GetMaxAllowableShapingCollectionCount(collection.Count); - int maxOperationsCount = AdvancedTypographicUtils.GetMaxAllowableShapingOperationsCount(collection.Count); + int maxCount = AdvancedTypographicUtils.GetMaxAllowableShapingCollectionCount(buffer.Count); + int maxOperationsCount = AdvancedTypographicUtils.GetMaxAllowableShapingOperationsCount(buffer.Count); int currentOperations = 0; gSubTable.ApplyFeature( fontMetrics, - collection, + buffer, ref iterator, in featureTag, this.ScriptClass, @@ -976,7 +976,7 @@ private bool WouldSubstitute(GlyphSubstitutionCollection collection, in Tag feat maxOperationsCount, ref currentOperations); - return collection.Count != initialCount; + return buffer.Count != initialCount; } return false; @@ -987,7 +987,7 @@ private bool WouldSubstitute(GlyphSubstitutionCollection collection, in Tag feat /// /// The glyph shaping data. /// if the glyph is a consonant. - private static bool IsConsonant(GlyphShapingData data) + private static bool IsConsonant(ref GlyphShapingData data) => data.IndicShapingEngineInfo != null && (FlagUnsafe(data.IndicShapingEngineInfo.Category) & ConsonantFlags) != 0; /// @@ -995,7 +995,7 @@ private static bool IsConsonant(GlyphShapingData data) /// /// The glyph shaping data. /// if the glyph is a joiner. - private static bool IsJoiner(GlyphShapingData data) + private static bool IsJoiner(ref GlyphShapingData data) => data.IndicShapingEngineInfo != null && (FlagUnsafe(data.IndicShapingEngineInfo.Category) & JoinerFlags) != 0; /// @@ -1003,27 +1003,27 @@ private static bool IsJoiner(GlyphShapingData data) /// /// The glyph shaping data. /// if the glyph is a halant or coeng. - private static bool IsHalantOrCoeng(GlyphShapingData data) + private static bool IsHalantOrCoeng(ref GlyphShapingData data) => data.IndicShapingEngineInfo != null && (FlagUnsafe(data.IndicShapingEngineInfo.Category) & HalantOrCoengFlags) != 0; /// - /// Finds the start index of the next syllable in the collection. + /// Finds the start index of the next syllable in the buffer. /// - /// The glyph substitution collection. + /// The glyph substitution buffer. /// The current index. /// The maximum index bound. /// The start index of the next syllable. - private static int NextSyllable(GlyphSubstitutionCollection collection, int index, int count) + private static int NextSyllable(ShapingBuffer buffer, int index, int count) { if (index >= count) { return index; } - int? syllable = collection[index].IndicShapingEngineInfo?.Syllable; + int? syllable = buffer[index].IndicShapingEngineInfo?.Syllable; while (++index < count) { - if (collection[index].IndicShapingEngineInfo?.Syllable != syllable) + if (buffer[index].IndicShapingEngineInfo?.Syllable != syllable) { break; } @@ -1036,19 +1036,19 @@ private static int NextSyllable(GlyphSubstitutionCollection collection, int inde /// Performs the final reordering pass for Indic syllables, repositioning reph, /// pre-base consonants, and pre-base matras after basic shaping. /// - /// The glyph shaping collection. + /// The glyph shaping buffer. /// The zero-based start index. /// The number of elements to process. - private void FinalReorder(GlyphShapingCollection collection, int index, int count) + private void FinalReorder(ShapingBuffer buffer, int index, int count) { - if (collection is not GlyphSubstitutionCollection substitutionCollection) + if (buffer.Role != ShapingBufferRole.Substitution) { return; } int max = index + count; int start = index; - int end = NextSyllable(substitutionCollection, index, max); + int end = NextSyllable(buffer, index, max); FontMetrics fontMetrics = this.fontMetrics; _ = fontMetrics.TryGetGSubTable(out GSubTable? gSubTable); while (start < max) @@ -1059,32 +1059,32 @@ private void FinalReorder(GlyphShapingCollection collection, int index, int coun // applied (see below), the shaping engine performs some final glyph // reordering before applying all the remaining font features to the entire // cluster. - bool tryPref = gSubTable?.TryGetFeatureLookups(fontMetrics, in PrefTag, this.ScriptClass, substitutionCollection.LanguageTags, out _) == true; + bool tryPref = gSubTable?.TryGetFeatureLookups(fontMetrics, in PrefTag, this.ScriptClass, buffer.LanguageTags, out _) == true; // Find base consonant again. int basePosition = start; for (; basePosition < end; basePosition++) { - if (substitutionCollection[basePosition].IndicShapingEngineInfo?.Position >= Positions.Base_C) + if (buffer[basePosition].IndicShapingEngineInfo?.Position >= Positions.Base_C) { if (tryPref && basePosition + 1 < end) { for (int i = basePosition + 1; i < end; i++) { - GlyphShapingData current = substitutionCollection[i]; - if ((current.FeatureMask & substitutionCollection.FeatureMap.GetMask(PrefTag)) != 0) + ref GlyphShapingData current = ref buffer[i]; + if ((current.FeatureMask & buffer.FeatureMap.GetMask(PrefTag)) != 0) { if (!current.IsSubstituted && current.IsLigated && !current.IsDecomposed) { // Ok, this was a 'pref' candidate but didn't form any. // Base is around here... basePosition = i; - while (basePosition < end && IsHalantOrCoeng(substitutionCollection[basePosition])) + while (basePosition < end && IsHalantOrCoeng(ref buffer[basePosition])) { basePosition++; } - IndicShapingEngineInfo? info = substitutionCollection[basePosition].IndicShapingEngineInfo; + IndicShapingEngineInfo? info = buffer[basePosition].IndicShapingEngineInfo; if (info != null) { info.Position = Positions.Base_C; @@ -1102,29 +1102,29 @@ private void FinalReorder(GlyphShapingCollection collection, int index, int coun { for (int i = basePosition + 1; i < end; i++) { - while (i < end && IsJoiner(substitutionCollection[i])) + while (i < end && IsJoiner(ref buffer[i])) { i++; } - if (i == end || !IsHalantOrCoeng(substitutionCollection[i])) + if (i == end || !IsHalantOrCoeng(ref buffer[i])) { break; } i++; // Skip halant. - while (i < end && IsJoiner(substitutionCollection[i])) + while (i < end && IsJoiner(ref buffer[i])) { i++; } if (i < end) { - GlyphShapingData current = substitutionCollection[i]; - if (IsConsonant(current) && current.IndicShapingEngineInfo?.Position == Positions.Below_C) + ref GlyphShapingData current = ref buffer[i]; + if (IsConsonant(ref current) && current.IndicShapingEngineInfo?.Position == Positions.Below_C) { basePosition = i; - IndicShapingEngineInfo? info = substitutionCollection[basePosition].IndicShapingEngineInfo; + IndicShapingEngineInfo? info = buffer[basePosition].IndicShapingEngineInfo; if (info != null) { info.Position = Positions.Base_C; @@ -1134,7 +1134,7 @@ private void FinalReorder(GlyphShapingCollection collection, int index, int coun } } - if (start < basePosition && substitutionCollection[basePosition].IndicShapingEngineInfo?.Position > Positions.Base_C) + if (start < basePosition && buffer[basePosition].IndicShapingEngineInfo?.Position > Positions.Base_C) { basePosition--; } @@ -1143,14 +1143,14 @@ private void FinalReorder(GlyphShapingCollection collection, int index, int coun } } - if (basePosition == end && start < basePosition && substitutionCollection[basePosition - 1].IndicShapingEngineInfo?.Category == Categories.ZWJ) + if (basePosition == end && start < basePosition && buffer[basePosition - 1].IndicShapingEngineInfo?.Category == Categories.ZWJ) { basePosition--; } if (basePosition < end) { - while (start < basePosition && (FlagUnsafe(substitutionCollection[basePosition].IndicShapingEngineInfo?.Category) & (Flag(Categories.N) | HalantOrCoengFlags)) != 0) + while (start < basePosition && (FlagUnsafe(buffer[basePosition].IndicShapingEngineInfo?.Category) & (Flag(Categories.N) | HalantOrCoengFlags)) != 0) { basePosition--; } @@ -1176,7 +1176,7 @@ private void FinalReorder(GlyphShapingCollection collection, int index, int coun // We want to position matra after them. if (this.ScriptClass is not ScriptClass.Malayalam and not ScriptClass.Tamil) { - while (newPos > start && (FlagUnsafe(substitutionCollection[newPos].IndicShapingEngineInfo?.Category) & (Flag(Categories.M) | HalantOrCoengFlags)) == 0) + while (newPos > start && (FlagUnsafe(buffer[newPos].IndicShapingEngineInfo?.Category) & (Flag(Categories.M) | HalantOrCoengFlags)) == 0) { newPos--; } @@ -1184,11 +1184,11 @@ private void FinalReorder(GlyphShapingCollection collection, int index, int coun // If we found no Halant we are done. // Otherwise only proceed if the Halant does // not belong to the Matra itself! - GlyphShapingData current = substitutionCollection[newPos]; - if (IsHalantOrCoeng(current) && current.IndicShapingEngineInfo?.Position != Positions.Pre_M) + ref GlyphShapingData current = ref buffer[newPos]; + if (IsHalantOrCoeng(ref current) && current.IndicShapingEngineInfo?.Position != Positions.Pre_M) { // If ZWJ or ZWNJ follow this halant, position is moved after it. - if (newPos + 1 < end && IsJoiner(substitutionCollection[newPos + 1])) + if (newPos + 1 < end && IsJoiner(ref buffer[newPos + 1])) { newPos++; } @@ -1199,12 +1199,12 @@ private void FinalReorder(GlyphShapingCollection collection, int index, int coun } } - if (start < newPos && substitutionCollection[newPos].IndicShapingEngineInfo?.Position != Positions.Pre_M) + if (start < newPos && buffer[newPos].IndicShapingEngineInfo?.Position != Positions.Pre_M) { // Now go see if there's actually any matras... for (int i = newPos; i > start; i--) { - if (substitutionCollection[i - 1].IndicShapingEngineInfo?.Position == Positions.Pre_M) + if (buffer[i - 1].IndicShapingEngineInfo?.Position == Positions.Pre_M) { int oldPos = i - 1; if (oldPos < basePosition && basePosition <= newPos) @@ -1213,7 +1213,7 @@ private void FinalReorder(GlyphShapingCollection collection, int index, int coun basePosition--; } - substitutionCollection.MoveGlyph(oldPos, newPos); + buffer.MoveGlyph(oldPos, newPos); newPos--; } } @@ -1236,7 +1236,7 @@ private void FinalReorder(GlyphShapingCollection collection, int index, int coun // - If repha is encoded separately and in the logical position, we should only // move it if it did NOT ligate. If it ligated, it's probably the font trying // to make it work without the reordering. - GlyphShapingData original = substitutionCollection[start]; + ref GlyphShapingData original = ref buffer[start]; if (start + 1 < end && original.IndicShapingEngineInfo?.Position == Positions.Ra_To_Become_Reph && (original.IndicShapingEngineInfo?.Category == Categories.Repha != (original.IsLigated && !original.IsDecomposed))) @@ -1260,15 +1260,15 @@ private void FinalReorder(GlyphShapingCollection collection, int index, int coun // fixed in shaping engine, there was no case where reph position // will be found on this step. newRephPos = start + 1; - while (newRephPos < basePosition && !IsHalantOrCoeng(substitutionCollection[newRephPos])) + while (newRephPos < basePosition && !IsHalantOrCoeng(ref buffer[newRephPos])) { newRephPos++; } - if (newRephPos < basePosition && IsHalantOrCoeng(substitutionCollection[newRephPos])) + if (newRephPos < basePosition && IsHalantOrCoeng(ref buffer[newRephPos])) { // ->If ZWJ or ZWNJ are following this halant, position is moved after it. - if (newRephPos + 1 < basePosition && IsJoiner(substitutionCollection[newRephPos + 1])) + if (newRephPos + 1 < basePosition && IsJoiner(ref buffer[newRephPos + 1])) { newRephPos++; } @@ -1282,7 +1282,7 @@ private void FinalReorder(GlyphShapingCollection collection, int index, int coun if (!found && rephPos == Positions.After_Main) { newRephPos = basePosition; - while (newRephPos + 1 < end && substitutionCollection[newRephPos + 1].IndicShapingEngineInfo?.Position <= Positions.After_Main) + while (newRephPos + 1 < end && buffer[newRephPos + 1].IndicShapingEngineInfo?.Position <= Positions.After_Main) { newRephPos++; } @@ -1299,7 +1299,7 @@ private void FinalReorder(GlyphShapingCollection collection, int index, int coun if (!found && rephPos == Positions.After_Sub) { newRephPos = basePosition; - while (newRephPos + 1 < end && (substitutionCollection[newRephPos + 1].IndicShapingEngineInfo?.Position & (Positions.Post_C | Positions.After_Post | Positions.SMVD)) == 0) + while (newRephPos + 1 < end && (buffer[newRephPos + 1].IndicShapingEngineInfo?.Position & (Positions.Post_C | Positions.After_Post | Positions.SMVD)) == 0) { newRephPos++; } @@ -1318,15 +1318,15 @@ private void FinalReorder(GlyphShapingCollection collection, int index, int coun { // Copied from step 2. newRephPos = start + 1; - while (newRephPos < basePosition && !IsHalantOrCoeng(substitutionCollection[newRephPos])) + while (newRephPos < basePosition && !IsHalantOrCoeng(ref buffer[newRephPos])) { newRephPos++; } - if (newRephPos < basePosition && IsHalantOrCoeng(substitutionCollection[newRephPos])) + if (newRephPos < basePosition && IsHalantOrCoeng(ref buffer[newRephPos])) { // ->If ZWJ or ZWNJ are following this halant, position is moved after it. - if (newRephPos + 1 < basePosition && IsJoiner(substitutionCollection[newRephPos + 1])) + if (newRephPos + 1 < basePosition && IsJoiner(ref buffer[newRephPos + 1])) { newRephPos++; } @@ -1339,7 +1339,7 @@ private void FinalReorder(GlyphShapingCollection collection, int index, int coun if (!found) { newRephPos = end - 1; - while (newRephPos > start && substitutionCollection[newRephPos].IndicShapingEngineInfo?.Position == Positions.SMVD) + while (newRephPos > start && buffer[newRephPos].IndicShapingEngineInfo?.Position == Positions.SMVD) { newRephPos--; } @@ -1349,11 +1349,11 @@ private void FinalReorder(GlyphShapingCollection collection, int index, int coun // However, if it's a plain Consonant,Halant we shouldn't do that. // Uniscribe doesn't do this. // TEST: U+0930,U+094D,U+0915,U+094B,U+094D - if (IsHalantOrCoeng(substitutionCollection[newRephPos])) + if (IsHalantOrCoeng(ref buffer[newRephPos])) { for (int i = basePosition + 1; i < newRephPos; i++) { - if ((FlagUnsafe(substitutionCollection[i].IndicShapingEngineInfo?.Category) & Flag(Categories.M)) != 0) + if ((FlagUnsafe(buffer[i].IndicShapingEngineInfo?.Category) & Flag(Categories.M)) != 0) { newRephPos--; } @@ -1363,7 +1363,7 @@ private void FinalReorder(GlyphShapingCollection collection, int index, int coun if (newRephPos != start) { - substitutionCollection.MoveGlyph(start, newRephPos); + buffer.MoveGlyph(start, newRephPos); } if (start < basePosition && basePosition <= newRephPos) @@ -1380,8 +1380,8 @@ private void FinalReorder(GlyphShapingCollection collection, int index, int coun { for (int i = basePosition + 1; i < end; i++) { - GlyphShapingData current = substitutionCollection[i]; - if ((current.FeatureMask & substitutionCollection.FeatureMap.GetMask(PrefTag)) != 0) + ref GlyphShapingData current = ref buffer[i]; + if ((current.FeatureMask & buffer.FeatureMap.GetMask(PrefTag)) != 0) { // 1. Only reorder a glyph produced by substitution during application // of the feature. (Note that a font may shape a Ra consonant with @@ -1405,7 +1405,7 @@ private void FinalReorder(GlyphShapingCollection collection, int index, int coun // We want to position matra after them. if (this.ScriptClass is not ScriptClass.Malayalam and not ScriptClass.Tamil) { - while (newPos > start && (FlagUnsafe(substitutionCollection[newPos - 1].IndicShapingEngineInfo?.Category) & (Flag(Categories.M) | HalantOrCoengFlags)) == 0) + while (newPos > start && (FlagUnsafe(buffer[newPos - 1].IndicShapingEngineInfo?.Category) & (Flag(Categories.M) | HalantOrCoengFlags)) == 0) { newPos--; } @@ -1413,12 +1413,12 @@ private void FinalReorder(GlyphShapingCollection collection, int index, int coun // TODO: Remove once we have Kmher shaper. // In Khmer coeng model, a H,Ra can go *after* matras. If it goes after a // split matra, it should be reordered to *before* the left part of such matra. - if (newPos > start && substitutionCollection[newPos - 1].IndicShapingEngineInfo?.Category == Categories.M) + if (newPos > start && buffer[newPos - 1].IndicShapingEngineInfo?.Category == Categories.M) { int oldPos = i; for (int j = basePosition + 1; j < oldPos; j++) { - if (substitutionCollection[j].IndicShapingEngineInfo?.Category == Categories.M) + if (buffer[j].IndicShapingEngineInfo?.Category == Categories.M) { newPos--; break; @@ -1427,16 +1427,16 @@ private void FinalReorder(GlyphShapingCollection collection, int index, int coun } } - if (newPos > start && IsHalantOrCoeng(substitutionCollection[newPos - 1])) + if (newPos > start && IsHalantOrCoeng(ref buffer[newPos - 1])) { // -> If ZWJ or ZWNJ follow this halant, position is moved after it. - if (newPos < end && IsJoiner(substitutionCollection[newPos])) + if (newPos < end && IsJoiner(ref buffer[newPos])) { newPos++; } } - substitutionCollection.MoveGlyph(i, newPos); + buffer.MoveGlyph(i, newPos); if (newPos <= basePosition && basePosition < i) { @@ -1450,14 +1450,14 @@ private void FinalReorder(GlyphShapingCollection collection, int index, int coun } // Apply 'init' to the Left Matra if it's a word start. - if (substitutionCollection[start].IndicShapingEngineInfo?.Position == Positions.Pre_M && - (start == 0 || CodePoint.GetGeneralCategory(substitutionCollection[start - 1].CodePoint) is not UnicodeCategory.NonSpacingMark and not UnicodeCategory.Format)) + if (buffer[start].IndicShapingEngineInfo?.Position == Positions.Pre_M && + (start == 0 || CodePoint.GetGeneralCategory(buffer[start - 1].CodePoint) is not UnicodeCategory.NonSpacingMark and not UnicodeCategory.Format)) { - substitutionCollection.EnableShapingFeature(start, InitTag); + buffer.EnableShapingFeature(start, InitTag); } start = end; - end = NextSyllable(substitutionCollection, start, max); + end = NextSyllable(buffer, start, max); } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs index 1c304b7fc..60680b3fa 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs @@ -73,31 +73,31 @@ public MyanmarShaper(ScriptClass script, TextOptions textOptions, FontMetrics fo } /// - protected override void PlanFeatures(GlyphShapingCollection collection, int index, int count) + protected override void PlanFeatures(ShapingBuffer buffer, int index, int count) { - this.AddFeature(collection, index, count, LoclTag, preAction: this.SetupSyllables); - this.AddFeature(collection, index, count, CcmpTag); - - this.AddFeature(collection, index, count, RphfTag, preAction: this.InitialReorder); - this.AddFeature(collection, index, count, PrefTag); - this.AddFeature(collection, index, count, BlwfTag); - this.AddFeature(collection, index, count, PstfTag); - - this.AddFeature(collection, index, count, PresTag); - this.AddFeature(collection, index, count, AbvsTag); - this.AddFeature(collection, index, count, BlwsTag); - this.AddFeature(collection, index, count, PstsTag); + this.AddFeature(buffer, index, count, LoclTag, preAction: this.SetupSyllables); + this.AddFeature(buffer, index, count, CcmpTag); + + this.AddFeature(buffer, index, count, RphfTag, preAction: this.InitialReorder); + this.AddFeature(buffer, index, count, PrefTag); + this.AddFeature(buffer, index, count, BlwfTag); + this.AddFeature(buffer, index, count, PstfTag); + + this.AddFeature(buffer, index, count, PresTag); + this.AddFeature(buffer, index, count, AbvsTag); + this.AddFeature(buffer, index, count, BlwsTag); + this.AddFeature(buffer, index, count, PstsTag); } /// /// Identifies Myanmar syllables using the state machine and assigns shaping info to each glyph. /// - /// The glyph shaping collection. + /// The glyph shaping buffer. /// The zero-based start index. /// The number of elements to process. - private void SetupSyllables(GlyphShapingCollection collection, int index, int count) + private void SetupSyllables(ShapingBuffer buffer, int index, int count) { - if (collection is not GlyphSubstitutionCollection substitutionCollection) + if (buffer.Role != ShapingBufferRole.Substitution) { return; } @@ -119,7 +119,7 @@ private void SetupSyllables(GlyphShapingCollection collection, int index, int co // CategoryToSymbolId[(int)my] performs this mapping, ensuring that // every codepoint is presented to the DFA using the correct compact // symbol index. - CodePoint codePoint = substitutionCollection[i].CodePoint; + CodePoint codePoint = buffer[i].CodePoint; MyanmarCategories my = (MyanmarCategories)IndicShapingCategory(codePoint); values[i - index] = CategoryToSymbolId[(int)my]; } @@ -133,7 +133,7 @@ private void SetupSyllables(GlyphShapingCollection collection, int index, int co ++syllable; for (int i = last; i < match.StartIndex; i++) { - GlyphShapingData data = substitutionCollection[i + index]; + ref GlyphShapingData data = ref buffer[i + index]; data.IndicShapingEngineInfo = new(Categories.X, Positions.End, "non_indic_cluster", syllable); } } @@ -143,7 +143,7 @@ private void SetupSyllables(GlyphShapingCollection collection, int index, int co // Create shaper info. for (int i = match.StartIndex; i <= match.EndIndex; i++) { - GlyphShapingData data = substitutionCollection[i + index]; + ref GlyphShapingData data = ref buffer[i + index]; CodePoint codePoint = data.CodePoint; string syllableType = match.Tags[0]; @@ -168,7 +168,7 @@ private void SetupSyllables(GlyphShapingCollection collection, int index, int co ++syllable; for (int i = last; i < count; i++) { - GlyphShapingData data = substitutionCollection[i + index]; + ref GlyphShapingData data = ref buffer[i + index]; data.IndicShapingEngineInfo = new(Categories.X, Positions.End, "non_indic_cluster", syllable); } } @@ -178,12 +178,12 @@ private void SetupSyllables(GlyphShapingCollection collection, int index, int co /// Performs the initial reordering pass for Myanmar consonant syllables, including /// dotted circle insertion for broken clusters. /// - /// The glyph shaping collection. + /// The glyph shaping buffer. /// The zero-based start index. /// The number of elements to process. - private void InitialReorder(GlyphShapingCollection collection, int index, int count) + private void InitialReorder(ShapingBuffer buffer, int index, int count) { - if (collection is not GlyphSubstitutionCollection substitutionCollection) + if (buffer.Role != ShapingBufferRole.Substitution) { return; } @@ -191,7 +191,7 @@ private void InitialReorder(GlyphShapingCollection collection, int index, int co FontMetrics fontMetrics = this.fontMetrics; int max = index + count; int start = index; - int end = NextSyllable(substitutionCollection, index, max); + int end = NextSyllable(buffer, index, max); if (this.hasBrokenClusters) { @@ -200,7 +200,7 @@ private void InitialReorder(GlyphShapingCollection collection, int index, int co Span glyphs = stackalloc ushort[2]; while (start < max) { - GlyphShapingData data = substitutionCollection[start]; + ref GlyphShapingData data = ref buffer[start]; IndicShapingEngineInfo? dataInfo = data.IndicShapingEngineInfo; string? type = dataInfo?.SyllableType; @@ -210,20 +210,20 @@ private void InitialReorder(GlyphShapingCollection collection, int index, int co int i = start; for (i = start; i < end; i++) { - if (substitutionCollection[i].IndicShapingEngineInfo?.Category != Categories.Repha) + if (buffer[i].IndicShapingEngineInfo?.Category != Categories.Repha) { break; } } - GlyphShapingData current = substitutionCollection[i]; + ref GlyphShapingData current = ref buffer[i]; glyphs[0] = current.GlyphId; glyphs[1] = circleId; - substitutionCollection.Replace(i, glyphs, KnownFeatureTags.GlyphCompositionDecomposition); + buffer.Replace(i, glyphs, KnownFeatureTags.GlyphCompositionDecomposition); // Update shaping info for newly inserted data. - GlyphShapingData dotted = substitutionCollection[i + 1]; + ref GlyphShapingData dotted = ref buffer[i + 1]; dotted.IndicShapingEngineInfo!.Category = Categories.Dotted_Circle; end++; @@ -231,17 +231,17 @@ private void InitialReorder(GlyphShapingCollection collection, int index, int co } start = end; - end = NextSyllable(substitutionCollection, start, max); + end = NextSyllable(buffer, start, max); } start = index; - end = NextSyllable(substitutionCollection, index, max); + end = NextSyllable(buffer, index, max); } } while (start < max) { - GlyphShapingData data = substitutionCollection[start]; + ref GlyphShapingData data = ref buffer[start]; IndicShapingEngineInfo? dataInfo = data.IndicShapingEngineInfo; string? type = dataInfo?.SyllableType; @@ -250,33 +250,33 @@ private void InitialReorder(GlyphShapingCollection collection, int index, int co // We already inserted dotted-circles, so just call the consonant_syllable. case "broken_cluster": case "consonant_syllable": - ReorderConsonantSyllable(substitutionCollection, start, end); + ReorderConsonantSyllable(buffer, start, end); break; default: break; } start = end; - end = NextSyllable(substitutionCollection, start, max); + end = NextSyllable(buffer, start, max); } } /// /// Reorders glyphs within a single Myanmar consonant syllable according to the Myanmar shaping spec. /// - /// The glyph substitution collection. + /// The glyph substitution buffer. /// The start index of the syllable. /// The exclusive end index of the syllable. - private static void ReorderConsonantSyllable(GlyphSubstitutionCollection substitutionCollection, int start, int end) + private static void ReorderConsonantSyllable(ShapingBuffer buffer, int start, int end) { int basePosition = end; bool hasReph = false; { int limit = start; if (start + 3 <= end && - substitutionCollection[start].IndicShapingEngineInfo?.MyanmarCategory == MyanmarCategories.Ra && - substitutionCollection[start + 1].IndicShapingEngineInfo?.MyanmarCategory == MyanmarCategories.As && - substitutionCollection[start + 2].IndicShapingEngineInfo?.MyanmarCategory == MyanmarCategories.H) + buffer[start].IndicShapingEngineInfo?.MyanmarCategory == MyanmarCategories.Ra && + buffer[start + 1].IndicShapingEngineInfo?.MyanmarCategory == MyanmarCategories.As && + buffer[start + 2].IndicShapingEngineInfo?.MyanmarCategory == MyanmarCategories.H) { limit += 3; basePosition = start; @@ -291,7 +291,7 @@ private static void ReorderConsonantSyllable(GlyphSubstitutionCollection substit for (int i = limit; i < end; i++) { - if (IsConsonant(substitutionCollection[i])) + if (IsConsonant(ref buffer[i])) { basePosition = i; break; @@ -305,17 +305,17 @@ private static void ReorderConsonantSyllable(GlyphSubstitutionCollection substit int i = start; for (; i < start + (hasReph ? 3 : 0); i++) { - substitutionCollection[i].IndicShapingEngineInfo!.Position = Positions.After_Main; + buffer[i].IndicShapingEngineInfo!.Position = Positions.After_Main; } for (; i < basePosition; i++) { - substitutionCollection[i].IndicShapingEngineInfo!.Position = Positions.Pre_C; + buffer[i].IndicShapingEngineInfo!.Position = Positions.Pre_C; } if (i < end) { - substitutionCollection[i].IndicShapingEngineInfo!.Position = Positions.Base_C; + buffer[i].IndicShapingEngineInfo!.Position = Positions.Base_C; i++; } @@ -324,7 +324,7 @@ private static void ReorderConsonantSyllable(GlyphSubstitutionCollection substit // The following loop may be ugly, but it implements all of Myanmar reordering! for (; i < end; i++) { - GlyphShapingData data = substitutionCollection[i]; + ref GlyphShapingData data = ref buffer[i]; IndicShapingEngineInfo info = data.IndicShapingEngineInfo!; // Pre-base reordering @@ -343,7 +343,7 @@ private static void ReorderConsonantSyllable(GlyphSubstitutionCollection substit if (info.MyanmarCategory == MyanmarCategories.VS) { - info.Position = substitutionCollection[i - 1].IndicShapingEngineInfo!.Position; + info.Position = buffer[i - 1].IndicShapingEngineInfo!.Position; continue; } @@ -377,7 +377,7 @@ private static void ReorderConsonantSyllable(GlyphSubstitutionCollection substit } } - substitutionCollection.Sort(start, end, (a, b) => + buffer.Sort(start, end, (a, b) => { int pa = a.IndicShapingEngineInfo?.Position != null ? (int)a.IndicShapingEngineInfo.Position : 0; int pb = b.IndicShapingEngineInfo?.Position != null ? (int)b.IndicShapingEngineInfo.Position : 0; @@ -390,7 +390,7 @@ private static void ReorderConsonantSyllable(GlyphSubstitutionCollection substit for (int i = start; i < end; i++) { - if (substitutionCollection[i].IndicShapingEngineInfo?.Position == Positions.Pre_M) + if (buffer[i].IndicShapingEngineInfo?.Position == Positions.Pre_M) { if (firstLeftMatra == end) { @@ -405,15 +405,15 @@ private static void ReorderConsonantSyllable(GlyphSubstitutionCollection substit if (firstLeftMatra < lastLeftMatra) { // No need to merge clusters, done already? - substitutionCollection.ReverseRange(firstLeftMatra, lastLeftMatra + 1); + buffer.ReverseRange(firstLeftMatra, lastLeftMatra + 1); // Reverse back VS, etc. int i = firstLeftMatra; for (int j = i; j <= lastLeftMatra; j++) { - if (substitutionCollection[j].IndicShapingEngineInfo?.MyanmarCategory == MyanmarCategories.VPre) + if (buffer[j].IndicShapingEngineInfo?.MyanmarCategory == MyanmarCategories.VPre) { - substitutionCollection.ReverseRange(i, j + 1); + buffer.ReverseRange(i, j + 1); i = j + 1; } } @@ -425,27 +425,27 @@ private static void ReorderConsonantSyllable(GlyphSubstitutionCollection substit /// /// The glyph shaping data. /// if the glyph is a consonant. - private static bool IsConsonant(GlyphShapingData data) + private static bool IsConsonant(ref GlyphShapingData data) => data.IndicShapingEngineInfo != null && (FlagUnsafe(data.IndicShapingEngineInfo.MyanmarCategory) & MyanmarConsonantFlags) != 0; /// - /// Finds the start index of the next syllable in the collection. + /// Finds the start index of the next syllable in the buffer. /// - /// The glyph substitution collection. + /// The glyph substitution buffer. /// The current index. /// The maximum index bound. /// The start index of the next syllable. - private static int NextSyllable(GlyphSubstitutionCollection collection, int index, int count) + private static int NextSyllable(ShapingBuffer buffer, int index, int count) { if (index >= count) { return index; } - int? syllable = collection[index].IndicShapingEngineInfo?.Syllable; + int? syllable = buffer[index].IndicShapingEngineInfo?.Syllable; while (++index < count) { - if (collection[index].IndicShapingEngineInfo?.Syllable != syllable) + if (buffer[index].IndicShapingEngineInfo?.Syllable != syllable) { break; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShapingStage.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShapingStage.cs index e535765fa..433beb194 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShapingStage.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShapingStage.cs @@ -13,10 +13,10 @@ namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers; internal readonly struct ShapingStage : IEquatable { /// The optional action to invoke before the feature is applied. - private readonly Action? preAction; + private readonly Action? preAction; /// The optional action to invoke after the feature is applied. - private readonly Action? postAction; + private readonly Action? postAction; /// /// Initializes a new instance of the struct. @@ -24,7 +24,7 @@ namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers; /// The OpenType feature tag for this stage. /// An optional action to invoke before the feature is applied. /// An optional action to invoke after the feature is applied. - public ShapingStage(Tag featureTag, Action? preAction, Action? postAction) + public ShapingStage(Tag featureTag, Action? preAction, Action? postAction) { this.FeatureTag = featureTag; this.preAction = preAction; @@ -39,20 +39,20 @@ public ShapingStage(Tag featureTag, Action? pr /// /// Invokes the pre-processing action for this shaping stage, if one was provided. /// - /// The glyph shaping collection. + /// The glyph shaping buffer. /// The zero-based index of the first element. /// The number of elements. - public void PreProcessFeature(GlyphShapingCollection collection, int index, int count) - => this.preAction?.Invoke(collection, index, count); + public void PreProcessFeature(ShapingBuffer buffer, int index, int count) + => this.preAction?.Invoke(buffer, index, count); /// /// Invokes the post-processing action for this shaping stage, if one was provided. /// - /// The glyph shaping collection. + /// The glyph shaping buffer. /// The zero-based index of the first element. /// The number of elements. - public void PostProcessFeature(GlyphShapingCollection collection, int index, int count) - => this.postAction?.Invoke(collection, index, count); + public void PostProcessFeature(ShapingBuffer buffer, int index, int count) + => this.postAction?.Invoke(buffer, index, count); /// public override bool Equals(object? obj) diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ThaiShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ThaiShaper.cs index 22412341d..666543155 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ThaiShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ThaiShaper.cs @@ -172,24 +172,24 @@ public ThaiShaper(ScriptClass script, TextOptions textOptions, FontMetrics fontM } /// - protected override void AssignFeatures(GlyphShapingCollection collection, int index, int count) + protected override void AssignFeatures(ShapingBuffer buffer, int index, int count) { - base.AssignFeatures(collection, index, count); + base.AssignFeatures(buffer, index, count); - if (collection is not GlyphSubstitutionCollection substitutionCollection) + if (buffer.Role != ShapingBufferRole.Substitution) { return; } // Step 1: Always decompose SARA AM -> NIKHAHIT + SARA AA and reorder. // This is needed even when the font has Thai/Lao GSUB tables. - count = PreprocessSaraAm(substitutionCollection, this.fontMetrics, index, count); + count = PreprocessSaraAm(buffer, this.fontMetrics, index, count); // Step 2: PUA-based fallback mark positioning. // Only applied for Thai (not Lao) when the font lacks Thai GSUB features. if (this.ScriptClass == ScriptClass.Thai && !this.hasGsub) { - DoThaiPuaShaping(substitutionCollection, this.fontMetrics, index, count); + DoThaiPuaShaping(buffer, this.fontMetrics, index, count); } } @@ -201,12 +201,12 @@ protected override void AssignFeatures(GlyphShapingCollection collection, int in /// /// /// - /// The glyph substitution collection. + /// The glyph substitution buffer. /// The font metrics for glyph lookups. /// The zero-based start index. /// The number of elements to process. /// The updated count after decomposition. - private static int PreprocessSaraAm(GlyphSubstitutionCollection collection, FontMetrics fontMetrics, int index, int count) + private static int PreprocessSaraAm(ShapingBuffer buffer, FontMetrics fontMetrics, int index, int count) { // Characters of significance: // @@ -222,7 +222,7 @@ private static int PreprocessSaraAm(GlyphSubstitutionCollection collection, Font int end = index + count; for (int i = index; i < end; i++) { - GlyphShapingData data = collection[i]; + ref GlyphShapingData data = ref buffer[i]; int codepoint = data.CodePoint.Value; if (!IsSaraAm(codepoint)) @@ -241,21 +241,21 @@ private static int PreprocessSaraAm(GlyphSubstitutionCollection collection, Font // Decompose SARA AM into [NIKHAHIT, SARA AA]. // Replace puts NIKHAHIT at index i, SARA AA at index i+1. - collection.Replace(i, [nikhahitId, saraAAId], KnownFeatureTags.GlyphCompositionDecomposition); - collection[i].CodePoint = new CodePoint(nikhahitCodepoint); - collection[i + 1].CodePoint = new CodePoint(saraAACodepoint); + buffer.Replace(i, [nikhahitId, saraAAId], KnownFeatureTags.GlyphCompositionDecomposition); + buffer[i].CodePoint = new CodePoint(nikhahitCodepoint); + buffer[i + 1].CodePoint = new CodePoint(saraAACodepoint); end++; // Move NIKHAHIT backward over any above-base marks. int target = i; - while (target > index && IsAboveBaseMark(collection[target - 1].CodePoint.Value)) + while (target > index && IsAboveBaseMark(buffer[target - 1].CodePoint.Value)) { target--; } if (target < i) { - collection.MoveGlyph(i, target); + buffer.MoveGlyph(i, target); } // Skip past SARA AA. @@ -269,11 +269,11 @@ private static int PreprocessSaraAm(GlyphSubstitutionCollection collection, Font /// Applies PUA-based fallback mark positioning using state machines. /// Only used for Thai fonts that lack GSUB features. /// - /// The glyph substitution collection. + /// The glyph substitution buffer. /// The font metrics for glyph lookups. /// The zero-based start index. /// The number of elements to process. - private static void DoThaiPuaShaping(GlyphSubstitutionCollection collection, FontMetrics fontMetrics, int index, int count) + private static void DoThaiPuaShaping(ShapingBuffer buffer, FontMetrics fontMetrics, int index, int count) { int aboveState = AboveStartState[(int)ConsonantType.NotConsonant]; int belowState = BelowStartState[(int)ConsonantType.NotConsonant]; @@ -282,7 +282,7 @@ private static void DoThaiPuaShaping(GlyphSubstitutionCollection collection, Fon int end = index + count; for (int i = index; i < end; i++) { - int codepoint = collection[i].CodePoint.Value; + int codepoint = buffer[i].CodePoint.Value; MarkType mt = GetMarkType(codepoint); if (mt == MarkType.NotMark) @@ -304,12 +304,12 @@ private static void DoThaiPuaShaping(GlyphSubstitutionCollection collection, Fon if (action == PuaAction.RD) { - int baseCp = collection[baseIndex].CodePoint.Value; + int baseCp = buffer[baseIndex].CodePoint.Value; int puaCp = ThaiPuaShape(baseCp, action, fontMetrics); if (puaCp != baseCp && fontMetrics.TryGetGlyphId(new CodePoint(puaCp), out ushort puaId)) { - collection[baseIndex].CodePoint = new CodePoint(puaCp); - collection.SetGlyphId(baseIndex, puaId); + buffer[baseIndex].CodePoint = new CodePoint(puaCp); + buffer.SetGlyphId(baseIndex, puaId); } } else if (action != PuaAction.NOP) @@ -317,8 +317,8 @@ private static void DoThaiPuaShaping(GlyphSubstitutionCollection collection, Fon int puaCp = ThaiPuaShape(codepoint, action, fontMetrics); if (puaCp != codepoint && fontMetrics.TryGetGlyphId(new CodePoint(puaCp), out ushort puaId)) { - collection[i].CodePoint = new CodePoint(puaCp); - collection.SetGlyphId(i, puaId); + buffer[i].CodePoint = new CodePoint(puaCp); + buffer.SetGlyphId(i, puaId); } } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs index 0af01fc17..76889e147 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs @@ -92,63 +92,63 @@ public UniversalShaper(ScriptClass script, TextOptions textOptions, FontMetrics => this.fontMetrics = fontMetrics; /// - protected override void PlanFeatures(GlyphShapingCollection collection, int index, int count) + protected override void PlanFeatures(ShapingBuffer buffer, int index, int count) { // Default glyph pre-processing group - this.AddFeature(collection, index, count, LoclTag, preAction: this.SetupSyllables); - this.AddFeature(collection, index, count, CcmpTag); - this.AddFeature(collection, index, count, NuktTag); - this.AddFeature(collection, index, count, AkhnTag); + this.AddFeature(buffer, index, count, LoclTag, preAction: this.SetupSyllables); + this.AddFeature(buffer, index, count, CcmpTag); + this.AddFeature(buffer, index, count, NuktTag); + this.AddFeature(buffer, index, count, AkhnTag); // Reordering group - this.AddFeature(collection, index, count, RphfTag, true, ClearSubstitutionFlags, RecordRhpf); - this.AddFeature(collection, index, count, PrefTag, true, ClearSubstitutionFlags, RecordPref); + this.AddFeature(buffer, index, count, RphfTag, true, ClearSubstitutionFlags, RecordRhpf); + this.AddFeature(buffer, index, count, PrefTag, true, ClearSubstitutionFlags, RecordPref); // Orthographic unit shaping group - this.AddFeature(collection, index, count, RkrfTag); - this.AddFeature(collection, index, count, AbvfTag); - this.AddFeature(collection, index, count, BlwfTag); - this.AddFeature(collection, index, count, HalfTag); - this.AddFeature(collection, index, count, PstfTag); - this.AddFeature(collection, index, count, VatuTag); - this.AddFeature(collection, index, count, CjctTag, postAction: this.Reorder); + this.AddFeature(buffer, index, count, RkrfTag); + this.AddFeature(buffer, index, count, AbvfTag); + this.AddFeature(buffer, index, count, BlwfTag); + this.AddFeature(buffer, index, count, HalfTag); + this.AddFeature(buffer, index, count, PstfTag); + this.AddFeature(buffer, index, count, VatuTag); + this.AddFeature(buffer, index, count, CjctTag, postAction: this.Reorder); // Standard topographic presentation and positional feature application - this.AddFeature(collection, index, count, AbvsTag); - this.AddFeature(collection, index, count, BlwsTag); - this.AddFeature(collection, index, count, PresTag); - this.AddFeature(collection, index, count, PstsTag); - this.AddFeature(collection, index, count, DistTag); - this.AddFeature(collection, index, count, AbvmTag); - this.AddFeature(collection, index, count, BlwmTag); + this.AddFeature(buffer, index, count, AbvsTag); + this.AddFeature(buffer, index, count, BlwsTag); + this.AddFeature(buffer, index, count, PresTag); + this.AddFeature(buffer, index, count, PstsTag); + this.AddFeature(buffer, index, count, DistTag); + this.AddFeature(buffer, index, count, AbvmTag); + this.AddFeature(buffer, index, count, BlwmTag); } /// - protected override void AssignFeatures(GlyphShapingCollection collection, int index, int count) - => this.DecomposeSplitVowels(collection, index, count); + protected override void AssignFeatures(ShapingBuffer buffer, int index, int count) + => this.DecomposeSplitVowels(buffer, index, count); /// /// Decomposes split vowels into their constituent parts if supported by the font. /// - /// The glyph shaping collection. + /// The glyph shaping buffer. /// The zero-based start index. /// The number of elements to process. - private void DecomposeSplitVowels(GlyphShapingCollection collection, int index, int count) + private void DecomposeSplitVowels(ShapingBuffer buffer, int index, int count) { - if (collection is not GlyphSubstitutionCollection substitutionCollection) + if (buffer.Role != ShapingBufferRole.Substitution) { return; } FontMetrics fontMetrics = this.fontMetrics; - Span buffer = stackalloc ushort[16]; + Span decompositionIds = stackalloc ushort[16]; int end = index + count; for (int i = end - 1; i >= index; i--) { - GlyphShapingData data = substitutionCollection[i]; + ref GlyphShapingData data = ref buffer[i]; if (UniversalShapingData.Decompositions.TryGetValue(data.CodePoint.Value, out int[]? decompositions) && decompositions != null) { - Span ids = buffer[..decompositions.Length]; + Span ids = decompositionIds[..decompositions.Length]; bool shouldDecompose = true; for (int j = 0; j < decompositions.Length; j++) { @@ -163,10 +163,10 @@ private void DecomposeSplitVowels(GlyphShapingCollection collection, int index, if (shouldDecompose) { - substitutionCollection.Replace(i, ids, KnownFeatureTags.GlyphCompositionDecomposition); + buffer.Replace(i, ids, KnownFeatureTags.GlyphCompositionDecomposition); for (int j = 0; j < decompositions.Length; j++) { - substitutionCollection[i + j].CodePoint = new(decompositions[j]); + buffer[i + j].CodePoint = new(decompositions[j]); } } } @@ -176,12 +176,12 @@ private void DecomposeSplitVowels(GlyphShapingCollection collection, int index, /// /// Identifies syllables using the Universal Shaping Engine state machine and assigns shaping info to each glyph. /// - /// The glyph shaping collection. + /// The glyph shaping buffer. /// The zero-based start index. /// The number of elements to process. - private void SetupSyllables(GlyphShapingCollection collection, int index, int count) + private void SetupSyllables(ShapingBuffer buffer, int index, int count) { - if (collection is not GlyphSubstitutionCollection substitutionCollection) + if (buffer.Role != ShapingBufferRole.Substitution) { return; } @@ -191,7 +191,7 @@ private void SetupSyllables(GlyphShapingCollection collection, int index, int co Span values = count <= 64 ? stackalloc int[count] : new int[count]; for (int i = index; i < index + count; i++) { - CodePoint codePoint = substitutionCollection[i].CodePoint; + CodePoint codePoint = buffer[i].CodePoint; values[i - index] = UnicodeData.GetUniversalShapingSymbolCount((uint)codePoint.Value); } @@ -203,7 +203,7 @@ private void SetupSyllables(GlyphShapingCollection collection, int index, int co // Create shaper info for (int i = match.StartIndex; i <= match.EndIndex; i++) { - GlyphShapingData data = substitutionCollection[i + index]; + ref GlyphShapingData data = ref buffer[i + index]; CodePoint codePoint = data.CodePoint; string category = UniversalShapingData.Categories[UnicodeData.GetUniversalShapingSymbolCount((uint)codePoint.Value)]; @@ -218,13 +218,13 @@ private void SetupSyllables(GlyphShapingCollection collection, int index, int co } // Assign rphf feature - int limit = substitutionCollection[match.StartIndex + index].UniversalShapingEngineInfo!.Category == "R" + int limit = buffer[match.StartIndex + index].UniversalShapingEngineInfo!.Category == "R" ? 1 : Math.Min(3, match.EndIndex - match.StartIndex); for (int i = match.StartIndex; i < match.StartIndex + limit; i++) { - substitutionCollection.AddShapingFeature(i + index, new TagEntry(RcltTag, true)); + buffer.AddShapingFeature(i + index, new TagEntry(RcltTag, true)); } } } @@ -232,12 +232,12 @@ private void SetupSyllables(GlyphShapingCollection collection, int index, int co /// /// Clears substitution flags on all glyphs in the range, preparing for the next substitution pass. /// - /// The glyph shaping collection. + /// The glyph shaping buffer. /// The zero-based start index. /// The number of elements to process. - private static void ClearSubstitutionFlags(GlyphShapingCollection collection, int index, int count) + private static void ClearSubstitutionFlags(ShapingBuffer buffer, int index, int count) { - if (collection is not GlyphSubstitutionCollection substitutionCollection) + if (buffer.Role != ShapingBufferRole.Substitution) { return; } @@ -245,7 +245,7 @@ private static void ClearSubstitutionFlags(GlyphShapingCollection collection, in int end = index + count; for (int i = index; i < end; i++) { - GlyphShapingData data = substitutionCollection[i]; + ref GlyphShapingData data = ref buffer[i]; data.IsSubstituted = false; } } @@ -253,21 +253,21 @@ private static void ClearSubstitutionFlags(GlyphShapingCollection collection, in /// /// Records glyphs substituted by the 'rphf' feature by marking their category as repha ("R"). /// - /// The glyph shaping collection. + /// The glyph shaping buffer. /// The zero-based start index. /// The number of elements to process. - private static void RecordRhpf(GlyphShapingCollection collection, int index, int count) + private static void RecordRhpf(ShapingBuffer buffer, int index, int count) { - if (collection is not GlyphSubstitutionCollection substitutionCollection) + if (buffer.Role != ShapingBufferRole.Substitution) { return; } int end = index + count; - ulong rphfMask = substitutionCollection.FeatureMap.GetMask(RphfTag); + ulong rphfMask = buffer.FeatureMap.GetMask(RphfTag); for (int i = index; i < end; i++) { - GlyphShapingData data = substitutionCollection[i]; + ref GlyphShapingData data = ref buffer[i]; if (data.IsSubstituted && (data.RegisteredFeatureMask & rphfMask) != 0) { // Mark a substituted repha. @@ -282,12 +282,12 @@ private static void RecordRhpf(GlyphShapingCollection collection, int index, int /// /// Records glyphs substituted by the 'pref' feature by marking their category as pre-base vowel ("VPre"). /// - /// The glyph shaping collection. + /// The glyph shaping buffer. /// The zero-based start index. /// The number of elements to process. - private static void RecordPref(GlyphShapingCollection collection, int index, int count) + private static void RecordPref(ShapingBuffer buffer, int index, int count) { - if (collection is not GlyphSubstitutionCollection substitutionCollection) + if (buffer.Role != ShapingBufferRole.Substitution) { return; } @@ -295,7 +295,7 @@ private static void RecordPref(GlyphShapingCollection collection, int index, int int end = index + count; for (int i = index; i < end; i++) { - GlyphShapingData data = substitutionCollection[i]; + ref GlyphShapingData data = ref buffer[i]; if (data.IsSubstituted) { // Mark a substituted pref as VPre, as they behave the same way. @@ -311,12 +311,12 @@ private static void RecordPref(GlyphShapingCollection collection, int index, int /// Reorders glyphs within syllables, handling repha movement, pre-base vowel movement, /// and dotted circle insertion for broken clusters. /// - /// The glyph shaping collection. + /// The glyph shaping buffer. /// The zero-based start index. /// The number of elements to process. - private void Reorder(GlyphShapingCollection collection, int index, int count) + private void Reorder(ShapingBuffer buffer, int index, int count) { - if (collection is not GlyphSubstitutionCollection substitutionCollection) + if (buffer.Role != ShapingBufferRole.Substitution) { return; } @@ -324,7 +324,7 @@ private void Reorder(GlyphShapingCollection collection, int index, int count) FontMetrics fontMetrics = this.fontMetrics; int max = index + count; int start = index; - int end = NextSyllable(substitutionCollection, index, max); + int end = NextSyllable(buffer, index, max); if (this.hasBrokenClusters) { @@ -333,7 +333,7 @@ private void Reorder(GlyphShapingCollection collection, int index, int count) Span glyphs = stackalloc ushort[2]; while (start < max) { - GlyphShapingData data = substitutionCollection[start]; + ref GlyphShapingData data = ref buffer[start]; UniversalShapingEngineInfo? info = data.UniversalShapingEngineInfo; string? type = info?.SyllableType; @@ -343,21 +343,21 @@ private void Reorder(GlyphShapingCollection collection, int index, int count) int i = start; for (i = start; i < end; i++) { - if (substitutionCollection[i].UniversalShapingEngineInfo?.Category != "R") + if (buffer[i].UniversalShapingEngineInfo?.Category != "R") { break; } } - GlyphShapingData current = substitutionCollection[i]; + ref GlyphShapingData current = ref buffer[i]; UniversalShapingEngineInfo currentInfo = current.UniversalShapingEngineInfo!; glyphs[0] = current.GlyphId; glyphs[1] = circleId; - substitutionCollection.Replace(i, glyphs, KnownFeatureTags.GlyphCompositionDecomposition); + buffer.Replace(i, glyphs, KnownFeatureTags.GlyphCompositionDecomposition); // Update shaping info for newly inserted data. - GlyphShapingData dotted = substitutionCollection[i + 1]; + ref GlyphShapingData dotted = ref buffer[i + 1]; dotted.UniversalShapingEngineInfo!.Category = "B"; dotted.UniversalShapingEngineInfo.SyllableType = currentInfo.SyllableType; dotted.UniversalShapingEngineInfo.Syllable = currentInfo.Syllable; @@ -367,17 +367,17 @@ private void Reorder(GlyphShapingCollection collection, int index, int count) } start = end; - end = NextSyllable(substitutionCollection, start, max); + end = NextSyllable(buffer, start, max); } start = index; - end = NextSyllable(substitutionCollection, index, max); + end = NextSyllable(buffer, index, max); } } while (start < max) { - GlyphShapingData data = substitutionCollection[start]; + ref GlyphShapingData data = ref buffer[start]; UniversalShapingEngineInfo? info = data.UniversalShapingEngineInfo; string? type = info?.SyllableType; @@ -394,18 +394,18 @@ private void Reorder(GlyphShapingCollection collection, int index, int count) // Got a repha. Reorder it to after first base, before first halant. for (int i = start + 1; i < end; i++) { - GlyphShapingData current = substitutionCollection[i]; + ref GlyphShapingData current = ref buffer[i]; info = current.UniversalShapingEngineInfo; - if (IsBase(info) || IsHalant(current)) + if (IsBase(info) || IsHalant(ref current)) { // If we hit a halant, move before it; otherwise it's a base: move to it's // place, and shift things in between backward. - if (IsHalant(current)) + if (IsHalant(ref current)) { i--; } - substitutionCollection.MoveGlyph(start, i); + buffer.MoveGlyph(start, i); break; } } @@ -414,14 +414,14 @@ private void Reorder(GlyphShapingCollection collection, int index, int count) // Move things back for (int i = start, j = start; i < end; i++) { - GlyphShapingData current = substitutionCollection[i]; + ref GlyphShapingData current = ref buffer[i]; info = current.UniversalShapingEngineInfo; - if (IsBase(info) || IsHalant(current)) + if (IsBase(info) || IsHalant(ref current)) { // If we hit a halant, move after it; otherwise move to the beginning, and // shift things in between forward. - if (IsHalant(current)) + if (IsHalant(ref current)) { j = i + 1; } @@ -434,34 +434,34 @@ private void Reorder(GlyphShapingCollection collection, int index, int count) && current.LigatureComponent <= 0 // Only move the first component of a MultipleSubst && j < i) { - substitutionCollection.MoveGlyph(i, j); + buffer.MoveGlyph(i, j); } } Increment: start = end; - end = NextSyllable(substitutionCollection, start, max); + end = NextSyllable(buffer, start, max); } } /// - /// Finds the start index of the next syllable in the collection. + /// Finds the start index of the next syllable in the buffer. /// - /// The glyph substitution collection. + /// The glyph substitution buffer. /// The current index. /// The maximum index bound. /// The start index of the next syllable. - private static int NextSyllable(GlyphSubstitutionCollection collection, int index, int count) + private static int NextSyllable(ShapingBuffer buffer, int index, int count) { if (index >= count) { return index; } - int? syllable = collection[index].UniversalShapingEngineInfo?.Syllable; + int? syllable = buffer[index].UniversalShapingEngineInfo?.Syllable; while (++index < count) { - if (collection[index].UniversalShapingEngineInfo?.Syllable != syllable) + if (buffer[index].UniversalShapingEngineInfo?.Syllable != syllable) { break; } @@ -475,7 +475,7 @@ private static int NextSyllable(GlyphSubstitutionCollection collection, int inde /// /// The glyph shaping data. /// if the glyph is a halant or equivalent. - private static bool IsHalant(GlyphShapingData data) + private static bool IsHalant(ref GlyphShapingData data) => (data.UniversalShapingEngineInfo?.Category is "H" or "HVM" or "IS") && !data.IsLigated; /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs index 974cb7403..b07df54f4 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs @@ -4,7 +4,7 @@ namespace SixLabors.Fonts.Tables.AdvancedTypographic; /// -/// An iterator over a glyph shaping collection that respects OpenType lookup flags, +/// An iterator over a glyph shaping buffer that respects OpenType lookup flags, /// skipping glyphs that should be ignored (marks, base glyphs, ligatures) based on the flags. /// internal struct SkippingGlyphIterator @@ -31,19 +31,19 @@ internal struct SkippingGlyphIterator /// Initializes a new instance of the struct. /// /// The font metrics for glyph class lookups. - /// The glyph shaping collection to iterate over. - /// The starting index in the collection. + /// The glyph shaping buffer to iterate over. + /// The starting index in the buffer. /// The lookup flags that control which glyphs to skip. /// The mark filtering set index, used when is set. public SkippingGlyphIterator( FontMetrics fontMetrics, - GlyphShapingCollection collection, + ShapingBuffer buffer, int index, LookupFlags lookupFlags, ushort markFilteringSet) { this.fontMetrics = fontMetrics; - this.Collection = collection; + this.Collection = buffer; this.Index = index; this.ignoreClassMask = (ushort)(((lookupFlags & LookupFlags.IgnoreBaseGlyphs) != 0 ? GlyphShapingClass.BaseProp : 0) | ((lookupFlags & LookupFlags.IgnoreLigatures) != 0 ? GlyphShapingClass.LigatureProp : 0) @@ -55,12 +55,12 @@ public SkippingGlyphIterator( } /// - /// Gets the glyph shaping collection being iterated. + /// Gets the glyph shaping buffer being iterated. /// - public GlyphShapingCollection Collection { get; } + public ShapingBuffer Collection { get; } /// - /// Gets or sets the current index in the collection. + /// Gets or sets the current index in the buffer. /// public int Index { get; set; } @@ -158,13 +158,13 @@ private void Move(int direction) /// if the glyph should be skipped; otherwise, . private readonly bool ShouldIgnore(int index) { - GlyphShapingData data = this.Collection[index]; + ref GlyphShapingData data = ref this.Collection[index]; // The shaping class is cached on the glyph keyed by glyph id; test the cache // inline so the common hit path avoids the classification call entirely. ushort props = data.ShapingClassCacheKey == data.GlyphId ? data.CachedShapingClass.Props - : AdvancedTypographicUtils.GetGlyphShapingClass(this.fontMetrics, data.GlyphId, data).Props; + : AdvancedTypographicUtils.GetGlyphShapingClass(this.fontMetrics, data.GlyphId, ref data).Props; if ((props & this.ignoreClassMask) != 0) { diff --git a/src/SixLabors.Fonts/Tables/General/Kern/KerningTable.cs b/src/SixLabors.Fonts/Tables/General/Kern/KerningTable.cs index ba2211ba5..7744b135c 100644 --- a/src/SixLabors.Fonts/Tables/General/Kern/KerningTable.cs +++ b/src/SixLabors.Fonts/Tables/General/Kern/KerningTable.cs @@ -89,17 +89,17 @@ public static KerningTable Load(BigEndianBinaryReader reader) /// Updates glyph positions by applying kerning adjustments for the specified glyph pair. /// /// The font metrics used for position calculations. - /// The glyph positioning collection to update. - /// The index of the left glyph in the collection. - /// The index of the right glyph in the collection. - public void UpdatePositions(FontMetrics fontMetrics, GlyphPositioningCollection collection, int left, int right) + /// The glyph positioning buffer to update. + /// The index of the left glyph in the buffer. + /// The index of the right glyph in the buffer. + public void UpdatePositions(FontMetrics fontMetrics, ShapingBuffer buffer, int left, int right) { - if (this.Count == 0 || collection.Count == 0) + if (this.Count == 0 || buffer.Count == 0) { return; } - GlyphShapingData current = collection[left]; + ref GlyphShapingData current = ref buffer[left]; if (current.IsKerned) { // Already kerned via previous processing. @@ -107,11 +107,11 @@ public void UpdatePositions(FontMetrics fontMetrics, GlyphPositioningCollection } ushort currentId = current.GlyphId; - ushort nextId = collection[right].GlyphId; + ushort nextId = buffer[right].GlyphId; if (this.TryGetKerningOffset(currentId, nextId, out Vector2 result)) { - collection.Advance(fontMetrics, left, currentId, (short)result.X, (short)result.Y); + buffer.Advance(fontMetrics, left, currentId, (short)result.X, (short)result.Y); current.IsKerned = true; } } diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index d1110d13e..2980d0afb 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -3,7 +3,6 @@ using System.Numerics; using SixLabors.Fonts.Tables.AdvancedTypographic; -using SixLabors.Fonts.Unicode; namespace SixLabors.Fonts; diff --git a/src/SixLabors.Fonts/TextShaper.Pipeline.cs b/src/SixLabors.Fonts/TextShaper.Pipeline.cs index 355c5d575..77be4bb74 100644 --- a/src/SixLabors.Fonts/TextShaper.Pipeline.cs +++ b/src/SixLabors.Fonts/TextShaper.Pipeline.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; using SixLabors.Fonts.Tables.AdvancedTypographic; using SixLabors.Fonts.Unicode; @@ -113,7 +114,7 @@ internal static IReadOnlyList BuildTextRuns(ReadOnlySpan text, Te /// /// /// Performs the font-run build, bidi analysis, GSUB/GPOS shaping (including fallback font - /// resolution for unmapped codepoints). The result contains the positioned glyph collection + /// resolution for unmapped codepoints). The result contains the positioned glyph buffer /// and bidi state used by logical line composition. /// /// The text to process. @@ -128,7 +129,7 @@ internal static ShapedText ShapeText(ReadOnlySpan text, TextOptions option ShapingScratch scratch = ScratchPool.Get(); try { - (GlyphSubstitutionCollection substitutions, GlyphPositioningCollection positionings) = scratch.Prepare(options); + (ShapingBuffer substitutions, ShapingBuffer positionings) = scratch.Prepare(options); return ShapeText(text, options, substitutions, positionings); } finally @@ -144,14 +145,14 @@ internal static ShapedText ShapeText(ReadOnlySpan text, TextOptions option /// /// The text to process. /// The text options used while shaping. - /// The substitution collection to shape into. - /// The positioning collection to shape into. + /// The substitution buffer to shape into. + /// The positioning buffer to shape into. /// The wrapping-independent shaping state. private static ShapedText ShapeText( ReadOnlySpan text, TextOptions options, - GlyphSubstitutionCollection substitutions, - GlyphPositioningCollection positionings) + ShapingBuffer substitutions, + ShapingBuffer positionings) { // Gather the font and fallbacks. Font[] fallbackFonts = (options.FallbackFontFamilies?.Count > 0) @@ -220,7 +221,7 @@ or BidiCharacterType.FirstStrongIsolate probe = ShapingProbe.Enter(); - // Incrementally build out collection of glyphs. + // Incrementally build out buffer of glyphs. IReadOnlyList textRuns = BuildTextRuns(text, options); ShapingProbe.Exit(ShapingProbe.BuildTextRuns, probe); @@ -281,7 +282,7 @@ or BidiCharacterType.FirstStrongIsolate if (!complete) { // Finally try our fallback fonts. - // We do a complete run here across the whole collection. + // We do a complete run here across the whole buffer. foreach (Font font in fallbackFonts) { textRunIndex = 0; @@ -306,9 +307,9 @@ or BidiCharacterType.FirstStrongIsolate } } - // Update the positions of the glyphs in the completed collection. + // Update the positions of the glyphs in the completed buffer. // Each set of metrics is associated with single font and will only be updated - // by that font so it's safe to use a single collection. + // by that font so it's safe to use a single buffer. probe = ShapingProbe.Enter(); Font? lastFont = null; for (int i = 0; i < textRuns.Count; i++) @@ -347,18 +348,18 @@ or BidiCharacterType.FirstStrongIsolate BidiRun runBidiRun = default; for (int i = 0; i < count; i++) { - GlyphPositioningCollection.GlyphPositioningData data = positionings.GetPositioningData(i); - GlyphShapingData shaping = data.Data; + ref GlyphShapingData shaping = ref positionings[i]; + ref ShapingBuffer.GlyphMetricsEntry entry = ref positionings.MetricsAt(i); // Placeholders carry a bidi run of their own, so they always cut a run. - if (data.Font != runFont + if (entry.Font != runFont || shaping.TextRun != runTextRun || (shaping.IsPlaceholder && !shaping.BidiRun.Equals(runBidiRun))) { - runFont = data.Font; + runFont = entry.Font; runTextRun = shaping.TextRun; runBidiRun = shaping.BidiRun; - runs.Add(new(data.Font, data.PointSize, shaping.TextRun, shaping.BidiRun)); + runs.Add(new(entry.Font, entry.PointSize, shaping.TextRun, shaping.BidiRun)); } ShapedGlyphFlags flags = ShapedGlyphFlags.None; @@ -383,18 +384,18 @@ or BidiCharacterType.FirstStrongIsolate } infos[i] = new( - data.Offset, + shaping.CodePointIndex, shaping.CodePoint, shaping.CodePointCount, - data.Metrics.GlyphId, + entry.Metrics.GlyphId, (ushort)(runs.Count - 1), flags); positions[i] = new( - data.AdvanceWidth, - data.AdvanceHeight, - data.PositionOffset, - data.Metrics.Offset); + entry.GetAdvanceWidth(in shaping), + entry.GetAdvanceHeight(in shaping), + new Vector2(shaping.Bounds.X, shaping.Bounds.Y), + entry.Metrics.Offset); } return new ShapedText([.. runs], infos, positions, bidiRuns, bidiMap, layoutMode); @@ -418,8 +419,8 @@ or BidiCharacterType.FirstStrongIsolate /// The font to shape with. /// The resolved bidi runs covering the whole input. /// A codepoint โ†’ bidi-run mapping accumulated across shaping passes. - /// The GSUB substitution collection to write into. - /// The GPOS positioning collection to write into. + /// The GSUB substitution buffer to write into. + /// The GPOS positioning buffer to write into. /// /// if every codepoint mapped successfully; if any /// codepoint remains unmapped (so a fallback-font pass is needed). @@ -435,10 +436,10 @@ private static bool DoFontRun( Font font, BidiRun[] bidiRuns, int[] bidiMap, - GlyphSubstitutionCollection substitutions, - GlyphPositioningCollection positionings) + ShapingBuffer substitutions, + ShapingBuffer positionings) { - // For each run we start with a fresh substitution collection to avoid + // For each run we start with a fresh substitution buffer to avoid // overwriting the glyph ids. substitutions.Clear(); @@ -487,7 +488,7 @@ private static bool DoFontRun( charIndex += charsConsumed; - // Get the glyph id for the codepoint and add to the collection. + // Get the glyph id for the codepoint and add to the buffer. bool hasGlyph = font.FontMetrics.TryGetGlyphId(current, next, out ushort glyphId, out skipNextCodePoint); // Unsupported default-ignorable code points such as FE0F should not block @@ -538,12 +539,12 @@ private static bool DoFontRun( /// feature when available and falls back to the Unicode mirror table otherwise. /// /// The font metrics used to look up mirrored glyph ids. - /// The substitution collection whose glyphs will be rewritten in place. - private static void SubstituteBidiMirrors(FontMetrics fontMetrics, GlyphSubstitutionCollection collection) + /// The substitution buffer whose glyphs will be rewritten in place. + private static void SubstituteBidiMirrors(FontMetrics fontMetrics, ShapingBuffer buffer) { - for (int i = 0; i < collection.Count; i++) + for (int i = 0; i < buffer.Count; i++) { - GlyphShapingData data = collection[i]; + ref GlyphShapingData data = ref buffer[i]; if (data.Direction != TextDirection.RightToLeft) { @@ -557,20 +558,20 @@ private static void SubstituteBidiMirrors(FontMetrics fontMetrics, GlyphSubstitu if (fontMetrics.TryGetGlyphId(mirror, out ushort glyphId)) { - collection.Replace(i, glyphId, KnownFeatureTags.RightToLeftMirroredForms); + buffer.Replace(i, glyphId, KnownFeatureTags.RightToLeftMirroredForms); } } // TODO: This only replaces certain glyphs. We should investigate the specification further. // https://www.unicode.org/reports/tr50/#vertical_alternates - if (collection.TextOptions.LayoutMode.IsHorizontal()) + if (buffer.TextOptions.LayoutMode.IsHorizontal()) { return; } - for (int i = 0; i < collection.Count; i++) + for (int i = 0; i < buffer.Count; i++) { - GlyphShapingData data = collection[i]; + ref GlyphShapingData data = ref buffer[i]; if (CodePoint.GetVerticalOrientationType(data.CodePoint) is VerticalOrientationType.Upright or VerticalOrientationType.TransformUpright) { continue; @@ -583,7 +584,7 @@ private static void SubstituteBidiMirrors(FontMetrics fontMetrics, GlyphSubstitu if (fontMetrics.TryGetGlyphId(mirror, out ushort glyphId)) { - collection.Replace(i, glyphId, KnownFeatureTags.VerticalAlternates); + buffer.Replace(i, glyphId, KnownFeatureTags.VerticalAlternates); } } } diff --git a/tests/SixLabors.Fonts.Tests/ShapingBufferConventionTests.cs b/tests/SixLabors.Fonts.Tests/ShapingBufferConventionTests.cs new file mode 100644 index 000000000..593a465c3 --- /dev/null +++ b/tests/SixLabors.Fonts.Tests/ShapingBufferConventionTests.cs @@ -0,0 +1,93 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Text.RegularExpressions; + +namespace SixLabors.Fonts.Tests; + +/// +/// Source conventions for element access. Glyph records are +/// large structs stored in flat buffer storage: binding an element to a local without +/// silently copies it and discards subsequent writes, and +/// assigning a buffer element to an existing local stores through +/// the reference instead of rebinding it. Both mistakes compile cleanly and corrupt +/// shaping output at runtime, so these tests fail the build instead. +/// +public class ShapingBufferConventionTests +{ + /// + /// Matches value-copy bindings of buffer elements, for example + /// GlyphShapingData x = buffer[i];. Legal forms are + /// ref GlyphShapingData x = ref buffer[i]; or mutation through the indexer + /// expression itself. + /// + private static readonly Regex CopyBinding = new( + @"(? + /// Matches phase dispatch by buffer type test, which became meaningless when the + /// substitution and positioning collections merged; the buffer's role property is + /// the correct dispatch. + /// + private static readonly Regex TypeTestDispatch = new( + @"is\s+(not\s+)?ShapingBuffer\b", + RegexOptions.Compiled); + + public static TheoryData SourceFiles() + { + TheoryData data = []; + foreach (string path in Directory.EnumerateFiles(GetSourceRoot(), "*.cs", SearchOption.AllDirectories)) + { + // The buffer's own storage management copies records deliberately. + if (Path.GetFileName(path) == "ShapingBuffer.cs") + { + continue; + } + + data.Add(path); + } + + return data; + } + + [Theory] + [MemberData(nameof(SourceFiles))] + public void BufferElementBindingsTakeRef(string path) + { + foreach (string line in File.ReadLines(path)) + { + Assert.False( + CopyBinding.IsMatch(line), + $"Value-copy binding of a buffer element in {path}: '{line.Trim()}'. Bind with 'ref ... = ref ...[...]' or mutate through the indexer expression."); + } + } + + [Theory] + [MemberData(nameof(SourceFiles))] + public void NoPhaseDispatchByBufferType(string path) + { + foreach (string line in File.ReadLines(path)) + { + Assert.False( + TypeTestDispatch.IsMatch(line), + $"Phase dispatch by buffer type test in {path}: '{line.Trim()}'. Use ShapingBuffer.Role instead."); + } + } + + private static string GetSourceRoot() + { + DirectoryInfo? directory = new(AppContext.BaseDirectory); + while (directory != null && !File.Exists(Path.Combine(directory.FullName, "SixLabors.Fonts.sln"))) + { + directory = directory.Parent; + } + + if (directory is null) + { + throw new IOException("Unable to locate the repository root."); + } + + return Path.Combine(directory.FullName, "src", "SixLabors.Fonts"); + } +} From 3aa1613d2c40228bbfcb82c395de2762b868ee80 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 10:54:07 +1000 Subject: [PATCH 12/94] Add Devanagari benchmark scenario and HarfBuzz differential tests ShapeTextBenchmark gains a Devanagari scenario (Noto Sans Devanagari, conjunct- and matra-heavy text) so complex-script shaping changes are measured, not assumed. HarfBuzzDifferentialTests shapes Latin, Arabic, and Devanagari cases through both engines and requires exact glyph id and advance equality; it is the standing correctness gate for shaping performance work. All six cases pass at this commit. --- .gitignore | 1 + .../ShapeTextBenchmark.cs | 17 ++++- .../HarfBuzzDifferentialTests.cs | 66 +++++++++++++++++++ .../SixLabors.Fonts.Tests.csproj | 1 + 4 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 tests/SixLabors.Fonts.Tests/HarfBuzzDifferentialTests.cs diff --git a/.gitignore b/.gitignore index 68bc86872..c15bc2af5 100644 --- a/.gitignore +++ b/.gitignore @@ -262,3 +262,4 @@ SixLabors.Shapes.Coverage.xml /coverage.*.xml *.lcov +BenchmarkDotNet.Artifacts/ diff --git a/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/ShapeTextBenchmark.cs b/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/ShapeTextBenchmark.cs index fffd9dc92..cebaf59f2 100644 --- a/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/ShapeTextBenchmark.cs +++ b/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/ShapeTextBenchmark.cs @@ -22,7 +22,13 @@ public enum ShapeTextBenchmarkScenario /// /// Arabic text exercising joining forms and mandatory ligatures, shaped with Dubai. /// - Arabic + Arabic, + + /// + /// Devanagari text exercising conjuncts, matras, and reordering, shaped with + /// Noto Sans Devanagari. + /// + Devanagari } /// @@ -49,7 +55,7 @@ public class ShapeTextBenchmark : IDisposable /// /// Gets or sets the text scenario used by the benchmark. /// - [Params(ShapeTextBenchmarkScenario.Latin, ShapeTextBenchmarkScenario.Arabic)] + [Params(ShapeTextBenchmarkScenario.Latin, ShapeTextBenchmarkScenario.Arabic, ShapeTextBenchmarkScenario.Devanagari)] public ShapeTextBenchmarkScenario Scenario { get; set; } /// @@ -64,11 +70,16 @@ public void SetUp() fontPath = GetFontPath("OpenSans-Regular.ttf"); this.text = "The quick brown fox jumps over the lazy dog; fifty fluffy waffles."; } - else + else if (this.Scenario == ShapeTextBenchmarkScenario.Arabic) { fontPath = GetFontPath("Dubai-Regular.ttf"); this.text = "ุณู„ุงู… ุนู„ูŠูƒู… ูˆุฑุญู…ุฉ ุงู„ู„ู‡ ูˆุจุฑูƒุงุชู‡ ู„ุง ุฅู„ู‡ ุฅู„ุง ุงู„ู„ู‡"; } + else + { + fontPath = GetFontPath("NotoSansDevanagari-Regular.ttf"); + this.text = "เค•เฅเคทเคคเฅเคฐเคฟเคฏ เคฆเฅเคตเคพเคฐเคพ เคชเฅเคฐเค•เฅƒเคคเคฟ เค•เฅ€ เคฐเค•เฅเคทเคพ เค•เคฐเฅเคคเคตเฅเคฏ เคนเฅˆ"; + } Font font = new FontCollection().Add(fontPath).CreateFont(16); this.textOptions = new TextOptions(font); diff --git a/tests/SixLabors.Fonts.Tests/HarfBuzzDifferentialTests.cs b/tests/SixLabors.Fonts.Tests/HarfBuzzDifferentialTests.cs new file mode 100644 index 000000000..a16939851 --- /dev/null +++ b/tests/SixLabors.Fonts.Tests/HarfBuzzDifferentialTests.cs @@ -0,0 +1,66 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using HarfBuzzSharp; +using HBBuffer = HarfBuzzSharp.Buffer; +using HBFace = HarfBuzzSharp.Face; +using HBFont = HarfBuzzSharp.Font; + +namespace SixLabors.Fonts.Tests; + +/// +/// Differential shaping checks against HarfBuzz. Both engines shape identical font +/// bytes; glyph ids and advances must match exactly. These tests are the correctness +/// gate for shaping performance work: any change to the shaping pipeline must keep +/// them green, and new benchmark scenarios must add a matching case here. +/// +public class HarfBuzzDifferentialTests +{ + public static TheoryData ShapingCases() + => new() + { + // Latin: ligature opportunities and kern-sensitive pairs. + { TestFonts.OpenSansFile, "The quick brown fox jumps over the lazy dog; fifty fluffy waffles.", false }, + { TestFonts.OpenSansFile, "AVATAR To Ya. WAV Tc office flag 1/2 fi ffl", false }, + + // Arabic: joining forms, mandatory ligatures, mark anchoring. + { TestFonts.ArabicFontFile, "ุณู„ุงู… ุนู„ูŠูƒู… ูˆุฑุญู…ุฉ ุงู„ู„ู‡ ูˆุจุฑูƒุงุชู‡ ู„ุง ุฅู„ู‡ ุฅู„ุง ุงู„ู„ู‡", true }, + { TestFonts.ArabicFontFile, "ู„ุขู„ุฆ", true }, + + // Devanagari: conjuncts, matras, and reordering. + { TestFonts.NotoSansDevanagariRegular, "เค•เฅเคทเคคเฅเคฐเคฟเคฏ เคฆเฅเคตเคพเคฐเคพ เคชเฅเคฐเค•เฅƒเคคเคฟ เค•เฅ€ เคฐเค•เฅเคทเคพ เค•เคฐเฅเคคเคตเฅเคฏ เคนเฅˆ", false }, + { TestFonts.NotoSansDevanagariRegular, "เคถเฅเคฐเคฆเฅเคงเคพเค‚เคœเคฒเคฟ", false }, + }; + + [Theory] + [MemberData(nameof(ShapingCases))] + public void ShapesIdenticallyToHarfBuzz(string fontFile, string text, bool rightToLeft) + { + // SixLabors side, font design units, logical order. + Font font = new FontCollection().Add(fontFile).CreateFont(16); + IReadOnlyList glyphs = TextShaper.Shape(text, new TextOptions(font)); + + // HarfBuzz side. Output for right-to-left runs is in visual order, so the + // comparison walks it reversed to recover logical order. + using Blob blob = Blob.FromFile(fontFile); + using HBFace face = new(blob, 0); + using HBFont hbFont = new(face); + hbFont.SetFunctionsOpenType(); + using HBBuffer buffer = new(); + buffer.AddUtf16(text); + buffer.GuessSegmentProperties(); + hbFont.Shape(buffer); + + GlyphInfo[] infos = buffer.GetGlyphInfoSpan().ToArray(); + GlyphPosition[] positions = buffer.GetGlyphPositionSpan().ToArray(); + + Assert.Equal(infos.Length, glyphs.Count); + + for (int i = 0; i < glyphs.Count; i++) + { + int hbIndex = rightToLeft ? infos.Length - 1 - i : i; + Assert.Equal(infos[hbIndex].Codepoint, glyphs[i].GlyphId); + Assert.Equal(positions[hbIndex].XAdvance, glyphs[i].AdvanceWidth); + } + } +} diff --git a/tests/SixLabors.Fonts.Tests/SixLabors.Fonts.Tests.csproj b/tests/SixLabors.Fonts.Tests/SixLabors.Fonts.Tests.csproj index afef4585d..b2bfb838b 100644 --- a/tests/SixLabors.Fonts.Tests/SixLabors.Fonts.Tests.csproj +++ b/tests/SixLabors.Fonts.Tests/SixLabors.Fonts.Tests.csproj @@ -49,6 +49,7 @@ + From 37c8f4797705070561b69e180b062cbf04f969f2 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 11:19:47 +1000 Subject: [PATCH 13/94] Store syllable classification in per-glyph SyllableInfo structs Replace the per-glyph IndicShapingEngineInfo/UniversalShapingEngineInfo heap objects and string syllable types with a SyllableInfo struct embedded in GlyphShapingData, matching how HarfBuzz keeps indic_category(), indic_position(), and syllable() in hb_glyph_info_t vars. Classification now allocates nothing and every syllable-type comparison is an integer compare. Gate: 5,577/0 tests including exact HarfBuzz differential on Devanagari. | Scenario | Metric | Before | After | |------------|-----------|-----------|-----------| | Devanagari | Allocated | 313,024 B | 299,840 B | | Devanagari | Ratio | 9.63x | 7.43x | | Devanagari | Mean | 112.13 us | 91.93 us | | Latin | Allocated | 8,296 B | 8,296 B | | Arabic | Allocated | 7,856 B | 7,856 B | Means were measured under daytime machine load; the HarfBuzz control moved too, so the exact allocation counts and the ratio are the comparable metrics. Latin and Arabic allocations are byte-identical, confirming the shared shaping path is untouched. --- src/SixLabors.Fonts/GlyphShapingData.cs | 80 +------ .../Shapers/IndicShaper.cs | 212 +++++++++--------- .../Shapers/MyanmarShaper.cs | 100 ++++----- .../Shapers/UniversalShaper.cs | 120 ++++++---- .../AdvancedTypographic/SyllableInfo.cs | 51 +++++ .../AdvancedTypographic/SyllableType.cs | 79 +++++++ .../AdvancedTypographic/SyllableTypeMap.cs | 37 +++ 7 files changed, 400 insertions(+), 279 deletions(-) create mode 100644 src/SixLabors.Fonts/Tables/AdvancedTypographic/SyllableInfo.cs create mode 100644 src/SixLabors.Fonts/Tables/AdvancedTypographic/SyllableType.cs create mode 100644 src/SixLabors.Fonts/Tables/AdvancedTypographic/SyllableTypeMap.cs diff --git a/src/SixLabors.Fonts/GlyphShapingData.cs b/src/SixLabors.Fonts/GlyphShapingData.cs index d497bfaed..4a03a6fb1 100644 --- a/src/SixLabors.Fonts/GlyphShapingData.cs +++ b/src/SixLabors.Fonts/GlyphShapingData.cs @@ -16,12 +16,19 @@ namespace SixLabors.Fonts; [DebuggerDisplay("{DebuggerDisplay,nq}")] internal struct GlyphShapingData { -#pragma warning disable SA1401 // Field exposed so positioning mutates the embedded bounds in place. +#pragma warning disable SA1401 // Fields exposed so shaping mutates embedded values in place. /// /// The shaping bounds. A field rather than a property so positioning lookups /// mutate the embedded value in place and re-seeding is plain value assignment. /// public GlyphShapingBounds Bounds; + + /// + /// The syllable classification assigned by the complex-script shapers, stored by + /// value so classification never allocates. A of + /// means no classification has been assigned. + /// + public SyllableInfo Syllable; #pragma warning restore SA1401 private ushort glyphId; @@ -57,22 +64,7 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) this.IsPositioned = data.IsPositioned; this.IsKerned = data.IsKerned; - if (data.UniversalShapingEngineInfo != null) - { - this.UniversalShapingEngineInfo = new( - data.UniversalShapingEngineInfo.Category, - data.UniversalShapingEngineInfo.SyllableType, - data.UniversalShapingEngineInfo.Syllable); - } - - if (data.IndicShapingEngineInfo != null) - { - this.IndicShapingEngineInfo = new( - data.IndicShapingEngineInfo.Category, - data.IndicShapingEngineInfo.Position, - data.IndicShapingEngineInfo.SyllableType, - data.IndicShapingEngineInfo.Syllable); - } + this.Syllable = data.Syllable; if (!clearFeatures) { @@ -219,16 +211,6 @@ public ushort GlyphId /// public bool IsKerned { get; set; } - /// - /// Gets or sets the universal shaping information. - /// - public UniversalShapingEngineInfo? UniversalShapingEngineInfo { get; set; } - - /// - /// Gets or sets the Indic shaping information. - /// - public IndicShapingEngineInfo? IndicShapingEngineInfo { 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.IsDecomposed}"); @@ -248,47 +230,3 @@ public void ClearFeatures() internal string ToDebuggerDisplay() => this.DebuggerDisplay; } - -/// -/// Represents information required for universal shaping. -/// -internal class UniversalShapingEngineInfo -{ - public UniversalShapingEngineInfo(string category, string syllableType, int syllable) - { - this.Category = category; - this.SyllableType = syllableType; - this.Syllable = syllable; - } - - public string Category { get; set; } - - public string SyllableType { get; set; } - - public int Syllable { get; set; } -} - -internal class IndicShapingEngineInfo -{ - public IndicShapingEngineInfo( - Categories category, - Positions position, - string syllableType, - int syllable) - { - this.Category = category; - this.Position = position; - this.SyllableType = syllableType; - this.Syllable = syllable; - } - - public Categories Category { get; set; } - - public MyanmarCategories MyanmarCategory => (MyanmarCategories)this.Category; - - public Positions Position { get; set; } - - public string SyllableType { get; set; } - - public int Syllable { get; set; } -} diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs index 35947cf4a..05b007643 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs @@ -247,7 +247,10 @@ private void SetupSyllables(ShapingBuffer buffer, int index, int count) for (int i = last; i < match.StartIndex; i++) { ref GlyphShapingData data = ref buffer[i + index]; - data.IndicShapingEngineInfo = new(Categories.X, Positions.End, "non_indic_cluster", syllable); + data.Syllable.IndicCategory = Categories.X; + data.Syllable.IndicPosition = Positions.End; + data.Syllable.Type = SyllableType.NonIndicCluster; + data.Syllable.Number = syllable; } } @@ -259,18 +262,17 @@ private void SetupSyllables(ShapingBuffer buffer, int index, int count) ref GlyphShapingData data = ref buffer[i + index]; CodePoint codePoint = data.CodePoint; - string syllableType = match.Tags[0]; + SyllableType syllableType = SyllableTypeMap.FromTag(match.Tags[0]); - if (syllableType == "broken_cluster") + if (syllableType == SyllableType.BrokenCluster) { this.hasBrokenClusters = true; } - data.IndicShapingEngineInfo = new( - (Categories)IndicShapingCategory(codePoint), - (Positions)IndicShapingPosition(codePoint), - syllableType, - syllable); + data.Syllable.IndicCategory = (Categories)IndicShapingCategory(codePoint); + data.Syllable.IndicPosition = (Positions)IndicShapingPosition(codePoint); + data.Syllable.Type = syllableType; + data.Syllable.Number = syllable; } last = match.EndIndex + 1; @@ -282,7 +284,10 @@ private void SetupSyllables(ShapingBuffer buffer, int index, int count) for (int i = last; i < count; i++) { ref GlyphShapingData data = ref buffer[i + index]; - data.IndicShapingEngineInfo = new(Categories.X, Positions.End, "non_indic_cluster", syllable); + data.Syllable.IndicCategory = Categories.X; + data.Syllable.IndicPosition = Positions.End; + data.Syllable.Type = SyllableType.NonIndicCluster; + data.Syllable.Number = syllable; } } } @@ -334,9 +339,8 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) for (int i = 0; i < count; i++) { ref GlyphShapingData data = ref buffer[i + index]; - IndicShapingEngineInfo? info = data.IndicShapingEngineInfo; - if (info?.Position == Positions.Base_C) + if (data.Syllable.IndicPosition == Positions.Base_C) { GlyphShapingData virama = new(data, false) { @@ -348,7 +352,7 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) tempBuffer[1] = data; tempBuffer[0] = virama; - info.Position = this.ConsonantPosition(tempCollection, tempBuffer); + data.Syllable.IndicPosition = this.ConsonantPosition(tempCollection, tempBuffer); } } } @@ -364,24 +368,19 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) Span glyphs = stackalloc ushort[2]; while (start < max) { - ref GlyphShapingData data = ref buffer[start]; - IndicShapingEngineInfo? dataInfo = data.IndicShapingEngineInfo; - string? type = dataInfo?.SyllableType; - - if (type == "broken_cluster") + if (buffer[start].Syllable.Type == SyllableType.BrokenCluster) { // Insert after possible Repha. int i = start; for (i = start; i < end; i++) { - if (buffer[i].IndicShapingEngineInfo?.Category != Categories.Repha) + if (buffer[i].Syllable.IndicCategory != Categories.Repha) { break; } } ref GlyphShapingData current = ref buffer[i]; - IndicShapingEngineInfo currentInfo = current.IndicShapingEngineInfo!; glyphs[0] = circleId; glyphs[1] = current.GlyphId; @@ -390,8 +389,8 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) // The dotted circle is now at position i (inherits original shaping info). // Update it to be a dotted circle base. ref GlyphShapingData dotted = ref buffer[i]; - dotted.IndicShapingEngineInfo!.Category = Categories.Dotted_Circle; - dotted.IndicShapingEngineInfo.Position = Positions.End; + dotted.Syllable.IndicCategory = Categories.Dotted_Circle; + dotted.Syllable.IndicPosition = Positions.End; // The original mark glyph is now at position i + 1 (copy of original info). // Its shaping info is already correct from the copy. @@ -411,11 +410,7 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) _ = fontMetrics.TryGetGSubTable(out GSubTable? gSubTable); while (start < max) { - ref GlyphShapingData data = ref buffer[start]; - IndicShapingEngineInfo? dataInfo = data.IndicShapingEngineInfo; - string? type = dataInfo?.SyllableType; - - if (type is "symbol_cluster" or "non_indic_cluster") + if (buffer[start].Syllable.Type is SyllableType.SymbolCluster or SyllableType.NonIndicCluster) { goto Increment; } @@ -439,7 +434,7 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) indicConfiguration.RephPosition != Positions.Ra_To_Become_Reph && gSubTable?.TryGetFeatureLookups(fontMetrics, in RphfTag, this.ScriptClass, buffer.LanguageTags, out _) == true && ((indicConfiguration.RephMode == RephMode.Implicit && !IsJoiner(ref buffer[start + 2])) || - (indicConfiguration.RephMode == RephMode.Explicit && buffer[start + 2].IndicShapingEngineInfo?.Category == Categories.ZWJ))) + (indicConfiguration.RephMode == RephMode.Explicit && buffer[start + 2].Syllable.IndicCategory == Categories.ZWJ))) { // See if it matches the 'rphf' feature. tempBuffer[2] = buffer[start + 2]; @@ -460,7 +455,7 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) } } else if (indicConfiguration.RephMode == RephMode.Log_Repha && - buffer[start].IndicShapingEngineInfo?.Category == Categories.Repha) + buffer[start].Syllable.IndicCategory == Categories.Repha) { limit++; while (limit < end && IsJoiner(ref buffer[limit])) @@ -482,14 +477,14 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) do { - IndicShapingEngineInfo? prevInfo = buffer[--i].IndicShapingEngineInfo; + ref GlyphShapingData prev = ref buffer[--i]; // Until a consonant is found - if (IsConsonant(ref buffer[i])) + if (IsConsonant(ref prev)) { // that does not have a below-base or post-base form // (post-base forms have to follow below-base forms), - if (prevInfo?.Position != Positions.Below_C && (prevInfo?.Position != Positions.Post_C || seenBelow)) + if (prev.Syllable.IndicPosition != Positions.Below_C && (prev.Syllable.IndicPosition != Positions.Post_C || seenBelow)) { basePosition = i; break; @@ -505,15 +500,15 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) // or arrive at the first consonant. The consonant stopped at will // be the base. - if (prevInfo?.Position == Positions.Below_C) + if (prev.Syllable.Type != SyllableType.None && prev.Syllable.IndicPosition == Positions.Below_C) { seenBelow = true; } basePosition = i; } - else if (start < i && prevInfo?.Category == Categories.ZWJ && - buffer[i - 1].IndicShapingEngineInfo?.Category == Categories.H) + else if (start < i && prev.Syllable.IndicCategory == Categories.ZWJ && prev.Syllable.Type != SyllableType.None && + buffer[i - 1].Syllable.IndicCategory == Categories.H) { // A ZWJ after a Halant stops the base search, and requests an explicit // half form. @@ -536,9 +531,9 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) for (int i = basePosition + 1; i < end; i++) { ref GlyphShapingData c = ref buffer[i]; - if (IsConsonant(ref c) && c.IndicShapingEngineInfo != null) + if (IsConsonant(ref c)) { - c.IndicShapingEngineInfo.Position = Positions.Below_C; + c.Syllable.IndicPosition = Positions.Below_C; } } @@ -587,19 +582,19 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) // Reorder characters for (int i = start; i < basePosition; i++) { - IndicShapingEngineInfo? info = buffer[i].IndicShapingEngineInfo; - if (info != null) + ref GlyphShapingData item = ref buffer[i]; + if (item.Syllable.Type != SyllableType.None) { - info.Position = (Positions)Math.Min((int)Positions.Pre_C, (int)info.Position); + item.Syllable.IndicPosition = (Positions)Math.Min((int)Positions.Pre_C, (int)item.Syllable.IndicPosition); } } if (basePosition < end) { - IndicShapingEngineInfo? info = buffer[basePosition].IndicShapingEngineInfo; - if (info != null) + ref GlyphShapingData item = ref buffer[basePosition]; + if (item.Syllable.Type != SyllableType.None) { - info.Position = Positions.Base_C; + item.Syllable.IndicPosition = Positions.Base_C; } } @@ -607,14 +602,14 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) // like in Khmer. for (int i = basePosition + 1; i < end; i++) { - if (buffer[i].IndicShapingEngineInfo?.Category == Categories.M) + if (buffer[i].Syllable.IndicCategory == Categories.M) { for (int j = i + 1; j < end; j++) { ref GlyphShapingData c = ref buffer[j]; - if (IsConsonant(ref c) && c.IndicShapingEngineInfo != null) + if (IsConsonant(ref c)) { - c.IndicShapingEngineInfo.Position = Positions.Final_C; + c.Syllable.IndicPosition = Positions.Final_C; break; } } @@ -627,9 +622,9 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) if (hasReph) { ref GlyphShapingData c = ref buffer[start]; - if (c.IndicShapingEngineInfo != null) + if (c.Syllable.Type != SyllableType.None) { - c.IndicShapingEngineInfo.Position = Positions.Ra_To_Become_Reph; + c.Syllable.IndicPosition = Positions.Ra_To_Become_Reph; } } @@ -654,19 +649,19 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) bool disallowDoubleHalants = this.ScriptClass != ScriptClass.Malayalam; for (int i = basePosition + 1; i < end; i++) { - if (buffer[i].IndicShapingEngineInfo?.Category == Categories.H) + if (buffer[i].Syllable.IndicCategory == Categories.H) { int j; for (j = end - 1; j > i; j--) { ref GlyphShapingData c = ref buffer[j]; - if (IsConsonant(ref c) || (disallowDoubleHalants && c.IndicShapingEngineInfo?.Category == Categories.H)) + if (IsConsonant(ref c) || (disallowDoubleHalants && c.Syllable.IndicCategory == Categories.H)) { break; } } - if (j > i && buffer[j].IndicShapingEngineInfo?.Category != Categories.H) + if (j > i && buffer[j].Syllable.IndicCategory != Categories.H) { // Move Halant to after last consonant. buffer.MoveGlyph(i, j); @@ -681,13 +676,14 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) Positions lastPosition = Positions.Start; for (int i = start; i < end; i++) { - IndicShapingEngineInfo? info = buffer[i].IndicShapingEngineInfo; - if (info != null) + ref GlyphShapingData item = ref buffer[i]; + if (item.Syllable.Type != SyllableType.None) { - if ((FlagUnsafe(info.Category) & (JoinerFlags | Flag(Categories.N) | Flag(Categories.RS) | Flag(Categories.CM) | (HalantOrCoengFlags & FlagUnsafe(info.Category)))) != 0) + Categories category = item.Syllable.IndicCategory; + if ((FlagUnsafe(category) & (JoinerFlags | Flag(Categories.N) | Flag(Categories.RS) | Flag(Categories.CM) | (HalantOrCoengFlags & FlagUnsafe(category)))) != 0) { - info.Position = lastPosition; - if (info.Category == Categories.H && info.Position == Positions.Pre_M) + item.Syllable.IndicPosition = lastPosition; + if (category == Categories.H && item.Syllable.IndicPosition == Positions.Pre_M) { // Uniscribe doesn't move the Halant with Left Matra. // TEST: U+092B,U+093F,U+094DE @@ -698,27 +694,29 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) // TEST: U+0D9A,U+0DDA for (int j = i; j > start; j--) { - Positions? pos = buffer[j - 1].IndicShapingEngineInfo?.Position; - if (pos is not null and not Positions.Pre_M) + // An unassigned record reads as position zero, matching + // the previous null semantics: keep scanning. + Positions pos = buffer[j - 1].Syllable.IndicPosition; + if (pos is not 0 and not Positions.Pre_M) { - info.Position = pos.Value; + item.Syllable.IndicPosition = pos; break; } } } } - else if (info.Position != Positions.SMVD) + else if (item.Syllable.IndicPosition != Positions.SMVD) { // If an MPst follows an SM, update the SM's position to match // so they move together during reordering. - if (info.Category == Categories.MPst + if (category == Categories.MPst && i > start - && buffer[i - 1].IndicShapingEngineInfo?.Category == Categories.SM) + && buffer[i - 1].Syllable.IndicCategory == Categories.SM) { - buffer[i - 1].IndicShapingEngineInfo!.Position = info.Position; + buffer[i - 1].Syllable.IndicPosition = item.Syllable.IndicPosition; } - lastPosition = info.Position; + lastPosition = item.Syllable.IndicPosition; } } } @@ -729,23 +727,22 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) for (int i = basePosition + 1; i < end; i++) { ref GlyphShapingData current = ref buffer[i]; - IndicShapingEngineInfo? info = current.IndicShapingEngineInfo; - if (info != null) + if (current.Syllable.Type != SyllableType.None) { if (IsConsonant(ref current)) { for (int j = last + 1; j < i; j++) { - IndicShapingEngineInfo? jInfo = buffer[j].IndicShapingEngineInfo; - if (jInfo?.Position < Positions.SMVD) + ref GlyphShapingData between = ref buffer[j]; + if (between.Syllable.Type != SyllableType.None && between.Syllable.IndicPosition < Positions.SMVD) { - jInfo.Position = info.Position; + between.Syllable.IndicPosition = current.Syllable.IndicPosition; } } last = i; } - else if ((FlagUnsafe(info.Category) & (Flag(Categories.M) | Flag(Categories.MPst))) != 0) + else if ((FlagUnsafe(current.Syllable.IndicCategory) & (Flag(Categories.M) | Flag(Categories.MPst))) != 0) { last = i; } @@ -754,15 +751,15 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) buffer.Sort(start, end, (a, b) => { - int pa = a.IndicShapingEngineInfo?.Position != null ? (int)a.IndicShapingEngineInfo.Position : 0; - int pb = b.IndicShapingEngineInfo?.Position != null ? (int)b.IndicShapingEngineInfo.Position : 0; + int pa = (int)a.Syllable.IndicPosition; + int pb = (int)b.Syllable.IndicPosition; return pa - pb; }); // Find base again for (int i = start; i < end; i++) { - if (buffer[i].IndicShapingEngineInfo?.Position == Positions.Base_C) + if (buffer[i].Syllable.IndicPosition == Positions.Base_C) { basePosition = i; break; @@ -774,8 +771,7 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) // Reph. for (int i = start; i < end; i++) { - IndicShapingEngineInfo? info = buffer[i].IndicShapingEngineInfo; - if (info?.Position != Positions.Ra_To_Become_Reph) + if (buffer[i].Syllable.IndicPosition != Positions.Ra_To_Become_Reph) { break; } @@ -823,9 +819,9 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) // Test case: U+0924,U+094D,U+0930,U+094d,U+200D,U+0915 for (int i = start; i + 1 < basePosition; i++) { - if (buffer[i].IndicShapingEngineInfo?.Category == Categories.Ra && - buffer[i + 1].IndicShapingEngineInfo?.Category == Categories.H && - (i + 1 == basePosition || buffer[i + 2].IndicShapingEngineInfo?.Category == Categories.ZWJ)) + if (buffer[i].Syllable.IndicCategory == Categories.Ra && + buffer[i + 1].Syllable.IndicCategory == Categories.H && + (i + 1 == basePosition || buffer[i + 2].Syllable.IndicCategory == Categories.ZWJ)) { buffer.EnableShapingFeature(i, BlwfTag); buffer.EnableShapingFeature(i + 1, BlwfTag); @@ -874,7 +870,7 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) ref GlyphShapingData current = ref buffer[i]; if (IsJoiner(ref current)) { - bool nonJoiner = current.IndicShapingEngineInfo?.Category == Categories.ZWNJ; + bool nonJoiner = current.Syllable.IndicCategory == Categories.ZWNJ; int j = i; do @@ -988,7 +984,7 @@ private bool WouldSubstitute(ShapingBuffer buffer, in Tag featureTag, ReadOnlySp /// The glyph shaping data. /// if the glyph is a consonant. private static bool IsConsonant(ref GlyphShapingData data) - => data.IndicShapingEngineInfo != null && (FlagUnsafe(data.IndicShapingEngineInfo.Category) & ConsonantFlags) != 0; + => (FlagUnsafe(data.Syllable.IndicCategory) & ConsonantFlags) != 0; /// /// Determines whether the glyph data represents a joiner (ZWJ or ZWNJ). @@ -996,7 +992,7 @@ private static bool IsConsonant(ref GlyphShapingData data) /// The glyph shaping data. /// if the glyph is a joiner. private static bool IsJoiner(ref GlyphShapingData data) - => data.IndicShapingEngineInfo != null && (FlagUnsafe(data.IndicShapingEngineInfo.Category) & JoinerFlags) != 0; + => (FlagUnsafe(data.Syllable.IndicCategory) & JoinerFlags) != 0; /// /// Determines whether the glyph data represents a halant or coeng character. @@ -1004,7 +1000,7 @@ private static bool IsJoiner(ref GlyphShapingData data) /// The glyph shaping data. /// if the glyph is a halant or coeng. private static bool IsHalantOrCoeng(ref GlyphShapingData data) - => data.IndicShapingEngineInfo != null && (FlagUnsafe(data.IndicShapingEngineInfo.Category) & HalantOrCoengFlags) != 0; + => (FlagUnsafe(data.Syllable.IndicCategory) & HalantOrCoengFlags) != 0; /// /// Finds the start index of the next syllable in the buffer. @@ -1020,10 +1016,10 @@ private static int NextSyllable(ShapingBuffer buffer, int index, int count) return index; } - int? syllable = buffer[index].IndicShapingEngineInfo?.Syllable; + int? syllable = buffer[index].Syllable.Number; while (++index < count) { - if (buffer[index].IndicShapingEngineInfo?.Syllable != syllable) + if (buffer[index].Syllable.Number != syllable) { break; } @@ -1065,7 +1061,7 @@ private void FinalReorder(ShapingBuffer buffer, int index, int count) int basePosition = start; for (; basePosition < end; basePosition++) { - if (buffer[basePosition].IndicShapingEngineInfo?.Position >= Positions.Base_C) + if (buffer[basePosition].Syllable.IndicPosition >= Positions.Base_C) { if (tryPref && basePosition + 1 < end) { @@ -1084,10 +1080,10 @@ private void FinalReorder(ShapingBuffer buffer, int index, int count) basePosition++; } - IndicShapingEngineInfo? info = buffer[basePosition].IndicShapingEngineInfo; - if (info != null) + ref GlyphShapingData newBase = ref buffer[basePosition]; + if (newBase.Syllable.Type != SyllableType.None) { - info.Position = Positions.Base_C; + newBase.Syllable.IndicPosition = Positions.Base_C; tryPref = false; } } @@ -1121,20 +1117,20 @@ private void FinalReorder(ShapingBuffer buffer, int index, int count) if (i < end) { ref GlyphShapingData current = ref buffer[i]; - if (IsConsonant(ref current) && current.IndicShapingEngineInfo?.Position == Positions.Below_C) + if (IsConsonant(ref current) && current.Syllable.IndicPosition == Positions.Below_C) { basePosition = i; - IndicShapingEngineInfo? info = buffer[basePosition].IndicShapingEngineInfo; - if (info != null) + ref GlyphShapingData newBase = ref buffer[basePosition]; + if (newBase.Syllable.Type != SyllableType.None) { - info.Position = Positions.Base_C; + newBase.Syllable.IndicPosition = Positions.Base_C; } } } } } - if (start < basePosition && buffer[basePosition].IndicShapingEngineInfo?.Position > Positions.Base_C) + if (start < basePosition && buffer[basePosition].Syllable.IndicPosition > Positions.Base_C) { basePosition--; } @@ -1143,14 +1139,14 @@ private void FinalReorder(ShapingBuffer buffer, int index, int count) } } - if (basePosition == end && start < basePosition && buffer[basePosition - 1].IndicShapingEngineInfo?.Category == Categories.ZWJ) + if (basePosition == end && start < basePosition && buffer[basePosition - 1].Syllable.IndicCategory == Categories.ZWJ) { basePosition--; } if (basePosition < end) { - while (start < basePosition && (FlagUnsafe(buffer[basePosition].IndicShapingEngineInfo?.Category) & (Flag(Categories.N) | HalantOrCoengFlags)) != 0) + while (start < basePosition && (FlagUnsafe(buffer[basePosition].Syllable.IndicCategory) & (Flag(Categories.N) | HalantOrCoengFlags)) != 0) { basePosition--; } @@ -1176,7 +1172,7 @@ private void FinalReorder(ShapingBuffer buffer, int index, int count) // We want to position matra after them. if (this.ScriptClass is not ScriptClass.Malayalam and not ScriptClass.Tamil) { - while (newPos > start && (FlagUnsafe(buffer[newPos].IndicShapingEngineInfo?.Category) & (Flag(Categories.M) | HalantOrCoengFlags)) == 0) + while (newPos > start && (FlagUnsafe(buffer[newPos].Syllable.IndicCategory) & (Flag(Categories.M) | HalantOrCoengFlags)) == 0) { newPos--; } @@ -1185,7 +1181,7 @@ private void FinalReorder(ShapingBuffer buffer, int index, int count) // Otherwise only proceed if the Halant does // not belong to the Matra itself! ref GlyphShapingData current = ref buffer[newPos]; - if (IsHalantOrCoeng(ref current) && current.IndicShapingEngineInfo?.Position != Positions.Pre_M) + if (IsHalantOrCoeng(ref current) && current.Syllable.IndicPosition != Positions.Pre_M) { // If ZWJ or ZWNJ follow this halant, position is moved after it. if (newPos + 1 < end && IsJoiner(ref buffer[newPos + 1])) @@ -1199,12 +1195,12 @@ private void FinalReorder(ShapingBuffer buffer, int index, int count) } } - if (start < newPos && buffer[newPos].IndicShapingEngineInfo?.Position != Positions.Pre_M) + if (start < newPos && buffer[newPos].Syllable.IndicPosition != Positions.Pre_M) { // Now go see if there's actually any matras... for (int i = newPos; i > start; i--) { - if (buffer[i - 1].IndicShapingEngineInfo?.Position == Positions.Pre_M) + if (buffer[i - 1].Syllable.IndicPosition == Positions.Pre_M) { int oldPos = i - 1; if (oldPos < basePosition && basePosition <= newPos) @@ -1238,8 +1234,8 @@ private void FinalReorder(ShapingBuffer buffer, int index, int count) // to make it work without the reordering. ref GlyphShapingData original = ref buffer[start]; if (start + 1 < end && - original.IndicShapingEngineInfo?.Position == Positions.Ra_To_Become_Reph && - (original.IndicShapingEngineInfo?.Category == Categories.Repha != (original.IsLigated && !original.IsDecomposed))) + original.Syllable.IndicPosition == Positions.Ra_To_Become_Reph && + (original.Syllable.IndicCategory == Categories.Repha != (original.IsLigated && !original.IsDecomposed))) { int newRephPos = start; Positions rephPos = this.indicConfiguration.RephPosition; @@ -1282,7 +1278,7 @@ private void FinalReorder(ShapingBuffer buffer, int index, int count) if (!found && rephPos == Positions.After_Main) { newRephPos = basePosition; - while (newRephPos + 1 < end && buffer[newRephPos + 1].IndicShapingEngineInfo?.Position <= Positions.After_Main) + while (newRephPos + 1 < end && buffer[newRephPos + 1].Syllable.IndicPosition <= Positions.After_Main) { newRephPos++; } @@ -1299,7 +1295,7 @@ private void FinalReorder(ShapingBuffer buffer, int index, int count) if (!found && rephPos == Positions.After_Sub) { newRephPos = basePosition; - while (newRephPos + 1 < end && (buffer[newRephPos + 1].IndicShapingEngineInfo?.Position & (Positions.Post_C | Positions.After_Post | Positions.SMVD)) == 0) + while (newRephPos + 1 < end && (buffer[newRephPos + 1].Syllable.IndicPosition & (Positions.Post_C | Positions.After_Post | Positions.SMVD)) == 0) { newRephPos++; } @@ -1339,7 +1335,7 @@ private void FinalReorder(ShapingBuffer buffer, int index, int count) if (!found) { newRephPos = end - 1; - while (newRephPos > start && buffer[newRephPos].IndicShapingEngineInfo?.Position == Positions.SMVD) + while (newRephPos > start && buffer[newRephPos].Syllable.IndicPosition == Positions.SMVD) { newRephPos--; } @@ -1353,7 +1349,7 @@ private void FinalReorder(ShapingBuffer buffer, int index, int count) { for (int i = basePosition + 1; i < newRephPos; i++) { - if ((FlagUnsafe(buffer[i].IndicShapingEngineInfo?.Category) & Flag(Categories.M)) != 0) + if ((FlagUnsafe(buffer[i].Syllable.IndicCategory) & Flag(Categories.M)) != 0) { newRephPos--; } @@ -1405,7 +1401,7 @@ private void FinalReorder(ShapingBuffer buffer, int index, int count) // We want to position matra after them. if (this.ScriptClass is not ScriptClass.Malayalam and not ScriptClass.Tamil) { - while (newPos > start && (FlagUnsafe(buffer[newPos - 1].IndicShapingEngineInfo?.Category) & (Flag(Categories.M) | HalantOrCoengFlags)) == 0) + while (newPos > start && (FlagUnsafe(buffer[newPos - 1].Syllable.IndicCategory) & (Flag(Categories.M) | HalantOrCoengFlags)) == 0) { newPos--; } @@ -1413,12 +1409,12 @@ private void FinalReorder(ShapingBuffer buffer, int index, int count) // TODO: Remove once we have Kmher shaper. // In Khmer coeng model, a H,Ra can go *after* matras. If it goes after a // split matra, it should be reordered to *before* the left part of such matra. - if (newPos > start && buffer[newPos - 1].IndicShapingEngineInfo?.Category == Categories.M) + if (newPos > start && buffer[newPos - 1].Syllable.IndicCategory == Categories.M) { int oldPos = i; for (int j = basePosition + 1; j < oldPos; j++) { - if (buffer[j].IndicShapingEngineInfo?.Category == Categories.M) + if (buffer[j].Syllable.IndicCategory == Categories.M) { newPos--; break; @@ -1450,7 +1446,7 @@ private void FinalReorder(ShapingBuffer buffer, int index, int count) } // Apply 'init' to the Left Matra if it's a word start. - if (buffer[start].IndicShapingEngineInfo?.Position == Positions.Pre_M && + if (buffer[start].Syllable.IndicPosition == Positions.Pre_M && (start == 0 || CodePoint.GetGeneralCategory(buffer[start - 1].CodePoint) is not UnicodeCategory.NonSpacingMark and not UnicodeCategory.Format)) { buffer.EnableShapingFeature(start, InitTag); diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs index 60680b3fa..bd85f872d 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs @@ -134,7 +134,10 @@ private void SetupSyllables(ShapingBuffer buffer, int index, int count) for (int i = last; i < match.StartIndex; i++) { ref GlyphShapingData data = ref buffer[i + index]; - data.IndicShapingEngineInfo = new(Categories.X, Positions.End, "non_indic_cluster", syllable); + data.Syllable.IndicCategory = Categories.X; + data.Syllable.IndicPosition = Positions.End; + data.Syllable.Type = SyllableType.NonIndicCluster; + data.Syllable.Number = syllable; } } @@ -146,18 +149,17 @@ private void SetupSyllables(ShapingBuffer buffer, int index, int count) ref GlyphShapingData data = ref buffer[i + index]; CodePoint codePoint = data.CodePoint; - string syllableType = match.Tags[0]; + SyllableType syllableType = SyllableTypeMap.FromTag(match.Tags[0]); - if (syllableType == "broken_cluster") + if (syllableType == SyllableType.BrokenCluster) { this.hasBrokenClusters = true; } - data.IndicShapingEngineInfo = new( - (Categories)IndicShapingCategory(codePoint), - (Positions)IndicShapingPosition(codePoint), - syllableType, - syllable); + data.Syllable.IndicCategory = (Categories)IndicShapingCategory(codePoint); + data.Syllable.IndicPosition = (Positions)IndicShapingPosition(codePoint); + data.Syllable.Type = syllableType; + data.Syllable.Number = syllable; } last = match.EndIndex + 1; @@ -169,7 +171,10 @@ private void SetupSyllables(ShapingBuffer buffer, int index, int count) for (int i = last; i < count; i++) { ref GlyphShapingData data = ref buffer[i + index]; - data.IndicShapingEngineInfo = new(Categories.X, Positions.End, "non_indic_cluster", syllable); + data.Syllable.IndicCategory = Categories.X; + data.Syllable.IndicPosition = Positions.End; + data.Syllable.Type = SyllableType.NonIndicCluster; + data.Syllable.Number = syllable; } } } @@ -200,17 +205,13 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) Span glyphs = stackalloc ushort[2]; while (start < max) { - ref GlyphShapingData data = ref buffer[start]; - IndicShapingEngineInfo? dataInfo = data.IndicShapingEngineInfo; - string? type = dataInfo?.SyllableType; - - if (type == "broken_cluster") + if (buffer[start].Syllable.Type == SyllableType.BrokenCluster) { // Insert after possible Repha. int i = start; for (i = start; i < end; i++) { - if (buffer[i].IndicShapingEngineInfo?.Category != Categories.Repha) + if (buffer[i].Syllable.IndicCategory != Categories.Repha) { break; } @@ -224,7 +225,7 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) // Update shaping info for newly inserted data. ref GlyphShapingData dotted = ref buffer[i + 1]; - dotted.IndicShapingEngineInfo!.Category = Categories.Dotted_Circle; + dotted.Syllable.IndicCategory = Categories.Dotted_Circle; end++; max++; @@ -241,15 +242,11 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) while (start < max) { - ref GlyphShapingData data = ref buffer[start]; - IndicShapingEngineInfo? dataInfo = data.IndicShapingEngineInfo; - string? type = dataInfo?.SyllableType; - - switch (type) + switch (buffer[start].Syllable.Type) { // We already inserted dotted-circles, so just call the consonant_syllable. - case "broken_cluster": - case "consonant_syllable": + case SyllableType.BrokenCluster: + case SyllableType.ConsonantSyllable: ReorderConsonantSyllable(buffer, start, end); break; default: @@ -274,9 +271,9 @@ private static void ReorderConsonantSyllable(ShapingBuffer buffer, int start, in { int limit = start; if (start + 3 <= end && - buffer[start].IndicShapingEngineInfo?.MyanmarCategory == MyanmarCategories.Ra && - buffer[start + 1].IndicShapingEngineInfo?.MyanmarCategory == MyanmarCategories.As && - buffer[start + 2].IndicShapingEngineInfo?.MyanmarCategory == MyanmarCategories.H) + buffer[start].Syllable.MyanmarCategory == MyanmarCategories.Ra && + buffer[start + 1].Syllable.MyanmarCategory == MyanmarCategories.As && + buffer[start + 2].Syllable.MyanmarCategory == MyanmarCategories.H) { limit += 3; basePosition = start; @@ -305,17 +302,17 @@ private static void ReorderConsonantSyllable(ShapingBuffer buffer, int start, in int i = start; for (; i < start + (hasReph ? 3 : 0); i++) { - buffer[i].IndicShapingEngineInfo!.Position = Positions.After_Main; + buffer[i].Syllable.IndicPosition = Positions.After_Main; } for (; i < basePosition; i++) { - buffer[i].IndicShapingEngineInfo!.Position = Positions.Pre_C; + buffer[i].Syllable.IndicPosition = Positions.Pre_C; } if (i < end) { - buffer[i].IndicShapingEngineInfo!.Position = Positions.Base_C; + buffer[i].Syllable.IndicPosition = Positions.Base_C; i++; } @@ -325,62 +322,61 @@ private static void ReorderConsonantSyllable(ShapingBuffer buffer, int start, in for (; i < end; i++) { ref GlyphShapingData data = ref buffer[i]; - IndicShapingEngineInfo info = data.IndicShapingEngineInfo!; // Pre-base reordering - if (info.MyanmarCategory == MyanmarCategories.MR) + if (data.Syllable.MyanmarCategory == MyanmarCategories.MR) { - info.Position = Positions.Pre_C; + data.Syllable.IndicPosition = Positions.Pre_C; continue; } // Left matra - if (info.MyanmarCategory == MyanmarCategories.VPre) + if (data.Syllable.MyanmarCategory == MyanmarCategories.VPre) { - info.Position = Positions.Pre_M; + data.Syllable.IndicPosition = Positions.Pre_M; continue; } - if (info.MyanmarCategory == MyanmarCategories.VS) + if (data.Syllable.MyanmarCategory == MyanmarCategories.VS) { - info.Position = buffer[i - 1].IndicShapingEngineInfo!.Position; + data.Syllable.IndicPosition = buffer[i - 1].Syllable.IndicPosition; continue; } - if (pos == Positions.After_Main && info.MyanmarCategory == MyanmarCategories.VBlw) + if (pos == Positions.After_Main && data.Syllable.MyanmarCategory == MyanmarCategories.VBlw) { pos = Positions.Below_C; - info.Position = pos; + data.Syllable.IndicPosition = pos; continue; } - if (pos == Positions.Below_C && info.MyanmarCategory == MyanmarCategories.A) + if (pos == Positions.Below_C && data.Syllable.MyanmarCategory == MyanmarCategories.A) { - info.Position = Positions.Before_Sub; + data.Syllable.IndicPosition = Positions.Before_Sub; continue; } - if (pos == Positions.Below_C && info.MyanmarCategory == MyanmarCategories.VBlw) + if (pos == Positions.Below_C && data.Syllable.MyanmarCategory == MyanmarCategories.VBlw) { - info.Position = pos; + data.Syllable.IndicPosition = pos; continue; } - if (pos == Positions.Below_C && info.MyanmarCategory != MyanmarCategories.A) + if (pos == Positions.Below_C && data.Syllable.MyanmarCategory != MyanmarCategories.A) { pos = Positions.After_Sub; - info.Position = pos; + data.Syllable.IndicPosition = pos; continue; } - info.Position = pos; + data.Syllable.IndicPosition = pos; } } buffer.Sort(start, end, (a, b) => { - int pa = a.IndicShapingEngineInfo?.Position != null ? (int)a.IndicShapingEngineInfo.Position : 0; - int pb = b.IndicShapingEngineInfo?.Position != null ? (int)b.IndicShapingEngineInfo.Position : 0; + int pa = (int)a.Syllable.IndicPosition; + int pb = (int)b.Syllable.IndicPosition; return pa - pb; }); @@ -390,7 +386,7 @@ private static void ReorderConsonantSyllable(ShapingBuffer buffer, int start, in for (int i = start; i < end; i++) { - if (buffer[i].IndicShapingEngineInfo?.Position == Positions.Pre_M) + if (buffer[i].Syllable.IndicPosition == Positions.Pre_M) { if (firstLeftMatra == end) { @@ -411,7 +407,7 @@ private static void ReorderConsonantSyllable(ShapingBuffer buffer, int start, in int i = firstLeftMatra; for (int j = i; j <= lastLeftMatra; j++) { - if (buffer[j].IndicShapingEngineInfo?.MyanmarCategory == MyanmarCategories.VPre) + if (buffer[j].Syllable.MyanmarCategory == MyanmarCategories.VPre) { buffer.ReverseRange(i, j + 1); i = j + 1; @@ -426,7 +422,7 @@ private static void ReorderConsonantSyllable(ShapingBuffer buffer, int start, in /// The glyph shaping data. /// if the glyph is a consonant. private static bool IsConsonant(ref GlyphShapingData data) - => data.IndicShapingEngineInfo != null && (FlagUnsafe(data.IndicShapingEngineInfo.MyanmarCategory) & MyanmarConsonantFlags) != 0; + => data.Syllable.Type != SyllableType.None && (FlagUnsafe(data.Syllable.MyanmarCategory) & MyanmarConsonantFlags) != 0; /// /// Finds the start index of the next syllable in the buffer. @@ -442,10 +438,10 @@ private static int NextSyllable(ShapingBuffer buffer, int index, int count) return index; } - int? syllable = buffer[index].IndicShapingEngineInfo?.Syllable; + int syllable = buffer[index].Syllable.Number; while (++index < count) { - if (buffer[index].IndicShapingEngineInfo?.Syllable != syllable) + if (buffer[index].Syllable.Number != syllable) { break; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs index 76889e147..527e6bc1b 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs @@ -14,6 +14,34 @@ namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers; /// internal sealed class UniversalShaper : DefaultShaper { + /// + /// The generated category name table, captured once: the generated property + /// allocates a fresh array on every access. + /// + private static readonly string[] CategoryNames = UniversalShapingData.Categories; + + /// + /// Symbol indices for the categories compared during reordering, resolved once from + /// the generated table so per-glyph category tests are integer comparisons. The + /// symbol index is the value the state machine consumes, so glyphs never + /// materialize category name strings. + /// + private static readonly int CategoryB = Array.IndexOf(CategoryNames, "B"); + + private static readonly int CategoryGB = Array.IndexOf(CategoryNames, "GB"); + + private static readonly int CategoryH = Array.IndexOf(CategoryNames, "H"); + + private static readonly int CategoryHVM = Array.IndexOf(CategoryNames, "HVM"); + + private static readonly int CategoryIS = Array.IndexOf(CategoryNames, "IS"); + + private static readonly int CategoryR = Array.IndexOf(CategoryNames, "R"); + + private static readonly int CategoryVPre = Array.IndexOf(CategoryNames, "VPre"); + + private static readonly int CategoryVMPre = Array.IndexOf(CategoryNames, "VMPre"); + /// The state machine for Universal Shaping Engine syllable identification. private static readonly StateMachine StateMachine = new(UniversalShapingData.StateTable, UniversalShapingData.AcceptingStates, UniversalShapingData.Tags); @@ -200,25 +228,24 @@ private void SetupSyllables(ShapingBuffer buffer, int index, int count) { ++syllable; - // Create shaper info + // Create shaper info. The symbol index is stored directly: it is the value + // the state machine consumes and the key into the generated name table. + SyllableType syllableType = SyllableTypeMap.FromTag(match.Tags[0]); + if (syllableType == SyllableType.BrokenCluster) + { + this.hasBrokenClusters = true; + } + for (int i = match.StartIndex; i <= match.EndIndex; i++) { ref GlyphShapingData data = ref buffer[i + index]; - CodePoint codePoint = data.CodePoint; - string category = UniversalShapingData.Categories[UnicodeData.GetUniversalShapingSymbolCount((uint)codePoint.Value)]; - - string syllableType = match.Tags[0]; - - if (syllableType == "broken_cluster") - { - this.hasBrokenClusters = true; - } - - data.UniversalShapingEngineInfo = new(category, syllableType, syllable); + data.Syllable.UseCategory = UnicodeData.GetUniversalShapingSymbolCount((uint)data.CodePoint.Value); + data.Syllable.Type = syllableType; + data.Syllable.Number = syllable; } // Assign rphf feature - int limit = buffer[match.StartIndex + index].UniversalShapingEngineInfo!.Category == "R" + int limit = buffer[match.StartIndex + index].Syllable.UseCategory == CategoryR ? 1 : Math.Min(3, match.EndIndex - match.StartIndex); @@ -271,9 +298,9 @@ private static void RecordRhpf(ShapingBuffer buffer, int index, int count) if (data.IsSubstituted && (data.RegisteredFeatureMask & rphfMask) != 0) { // Mark a substituted repha. - if (data.UniversalShapingEngineInfo != null) + if (data.Syllable.Type != SyllableType.None) { - data.UniversalShapingEngineInfo.Category = "R"; + data.Syllable.UseCategory = CategoryR; } } } @@ -299,9 +326,9 @@ private static void RecordPref(ShapingBuffer buffer, int index, int count) if (data.IsSubstituted) { // Mark a substituted pref as VPre, as they behave the same way. - if (data.UniversalShapingEngineInfo != null) + if (data.Syllable.Type != SyllableType.None) { - data.UniversalShapingEngineInfo.Category = "VPre"; + data.Syllable.UseCategory = CategoryVPre; } } } @@ -333,34 +360,31 @@ private void Reorder(ShapingBuffer buffer, int index, int count) Span glyphs = stackalloc ushort[2]; while (start < max) { - ref GlyphShapingData data = ref buffer[start]; - UniversalShapingEngineInfo? info = data.UniversalShapingEngineInfo; - string? type = info?.SyllableType; - - if (type == "broken_cluster") + if (buffer[start].Syllable.Type == SyllableType.BrokenCluster) { // Insert after possible Repha. int i = start; for (i = start; i < end; i++) { - if (buffer[i].UniversalShapingEngineInfo?.Category != "R") + ref GlyphShapingData candidate = ref buffer[i]; + if (candidate.Syllable.Type == SyllableType.None || candidate.Syllable.UseCategory != CategoryR) { break; } } - ref GlyphShapingData current = ref buffer[i]; - UniversalShapingEngineInfo currentInfo = current.UniversalShapingEngineInfo!; - glyphs[0] = current.GlyphId; - glyphs[1] = circleId; + { + ref GlyphShapingData current = ref buffer[i]; + glyphs[0] = current.GlyphId; + glyphs[1] = circleId; + } buffer.Replace(i, glyphs, KnownFeatureTags.GlyphCompositionDecomposition); - // Update shaping info for newly inserted data. - ref GlyphShapingData dotted = ref buffer[i + 1]; - dotted.UniversalShapingEngineInfo!.Category = "B"; - dotted.UniversalShapingEngineInfo.SyllableType = currentInfo.SyllableType; - dotted.UniversalShapingEngineInfo.Syllable = currentInfo.Syllable; + // Update shaping info for newly inserted data. The insertion + // copied the source record, so type and syllable number are + // already correct; only the category changes. + buffer[i + 1].Syllable.UseCategory = CategoryB; end++; max++; @@ -378,25 +402,22 @@ private void Reorder(ShapingBuffer buffer, int index, int count) while (start < max) { ref GlyphShapingData data = ref buffer[start]; - UniversalShapingEngineInfo? info = data.UniversalShapingEngineInfo; - string? type = info?.SyllableType; // Only a few syllable types need reordering. - if (type is not "virama_terminated_cluster" and not "standard_cluster" and not "broken_cluster") + if (data.Syllable.Type is not SyllableType.ViramaTerminatedCluster and not SyllableType.StandardCluster and not SyllableType.BrokenCluster) { // TODO: Check this. Harfbuzz seems to test more categories and returns. goto Increment; } // Move things forward - if (info?.Category == "R" && end - start > 1) + if (data.Syllable.UseCategory == CategoryR && end - start > 1) { // Got a repha. Reorder it to after first base, before first halant. for (int i = start + 1; i < end; i++) { ref GlyphShapingData current = ref buffer[i]; - info = current.UniversalShapingEngineInfo; - if (IsBase(info) || IsHalant(ref current)) + if (IsBase(ref current) || IsHalant(ref current)) { // If we hit a halant, move before it; otherwise it's a base: move to it's // place, and shift things in between backward. @@ -415,9 +436,8 @@ private void Reorder(ShapingBuffer buffer, int index, int count) for (int i = start, j = start; i < end; i++) { ref GlyphShapingData current = ref buffer[i]; - info = current.UniversalShapingEngineInfo; - if (IsBase(info) || IsHalant(ref current)) + if (IsBase(ref current) || IsHalant(ref current)) { // If we hit a halant, move after it; otherwise move to the beginning, and // shift things in between forward. @@ -430,7 +450,8 @@ private void Reorder(ShapingBuffer buffer, int index, int count) j = i; } } - else if ((info?.Category == "VPre" || info?.Category == "VMPre") + else if (current.Syllable.Type != SyllableType.None + && (current.Syllable.UseCategory == CategoryVPre || current.Syllable.UseCategory == CategoryVMPre) && current.LigatureComponent <= 0 // Only move the first component of a MultipleSubst && j < i) { @@ -458,10 +479,10 @@ private static int NextSyllable(ShapingBuffer buffer, int index, int count) return index; } - int? syllable = buffer[index].UniversalShapingEngineInfo?.Syllable; + int syllable = buffer[index].Syllable.Number; while (++index < count) { - if (buffer[index].UniversalShapingEngineInfo?.Syllable != syllable) + if (buffer[index].Syllable.Number != syllable) { break; } @@ -476,13 +497,16 @@ private static int NextSyllable(ShapingBuffer buffer, int index, int count) /// The glyph shaping data. /// if the glyph is a halant or equivalent. private static bool IsHalant(ref GlyphShapingData data) - => (data.UniversalShapingEngineInfo?.Category is "H" or "HVM" or "IS") && !data.IsLigated; + => data.Syllable.Type != SyllableType.None + && (data.Syllable.UseCategory == CategoryH || data.Syllable.UseCategory == CategoryHVM || data.Syllable.UseCategory == CategoryIS) + && !data.IsLigated; /// - /// Determines whether the shaping info represents a base consonant or generic base. + /// Determines whether the glyph is a base consonant or generic base. /// - /// The universal shaping engine info. + /// The glyph shaping data. /// if the glyph is a base. - private static bool IsBase(UniversalShapingEngineInfo? info) - => info?.Category is "B" or "GB"; + private static bool IsBase(ref GlyphShapingData data) + => data.Syllable.Type != SyllableType.None + && (data.Syllable.UseCategory == CategoryB || data.Syllable.UseCategory == CategoryGB); } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/SyllableInfo.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/SyllableInfo.cs new file mode 100644 index 000000000..84063c3fc --- /dev/null +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/SyllableInfo.cs @@ -0,0 +1,51 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using static SixLabors.Fonts.Unicode.Resources.IndicShapingData; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic; + +/// +/// Per-glyph syllable classification assigned by the Indic, Myanmar, and Universal +/// Shaping Engine shapers, stored by value on the glyph record so classification +/// allocates nothing and every comparison is an integer compare. +/// of means no classification has +/// been assigned; consumers treat such glyphs as outside every syllable. +/// +#pragma warning disable SA1401 // Fields exposed for in-place mutation through the glyph record. +internal struct SyllableInfo +{ + /// + /// The running syllable number within the shaping pass. + /// + public int Number; + + /// + /// The syllable cluster type produced by the state machine, or + /// when unassigned. + /// + public SyllableType Type; + + /// + /// The Indic or Myanmar shaping category. + /// + public Categories IndicCategory; + + /// + /// The Indic or Myanmar positional class. + /// + public Positions IndicPosition; + + /// + /// The Universal Shaping Engine category as the symbol index the state machine + /// consumes, which is also the index into the generated category name table. + /// + public int UseCategory; + + /// + /// Gets the Myanmar view of : the Myanmar shaper shares + /// the Indic category storage and reads it through its own enum. + /// + public readonly MyanmarCategories MyanmarCategory => (MyanmarCategories)this.IndicCategory; +} +#pragma warning restore SA1401 diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/SyllableType.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/SyllableType.cs new file mode 100644 index 000000000..456e1182d --- /dev/null +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/SyllableType.cs @@ -0,0 +1,79 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.AdvancedTypographic; + +/// +/// The syllable cluster types produced by the Indic, Myanmar, and Universal Shaping +/// Engine state machines. Stored per glyph as a value so syllable classification is +/// integer comparison; the values mirror the top-level grammar rule names. +/// +internal enum SyllableType +{ + /// + /// No syllable information has been assigned. + /// + None = 0, + + /// + /// A grammar rule name with no dedicated member. Comparisons against known types + /// are false, matching the behavior of an unrecognized rule name string. + /// + Other, + + /// + /// The broken_cluster rule: a cluster missing its base, repaired with a dotted circle. + /// + BrokenCluster, + + /// + /// The consonant_syllable rule. + /// + ConsonantSyllable, + + /// + /// The independent_cluster rule. + /// + IndependentCluster, + + /// + /// The synthetic type for codepoints outside every syllable match. Assigned by the + /// shapers, not by a grammar rule. + /// + NonIndicCluster, + + /// + /// The number_joiner_terminated_cluster rule. + /// + NumberJoinerTerminatedCluster, + + /// + /// The numeral_cluster rule. + /// + NumeralCluster, + + /// + /// The standalone_cluster rule. + /// + StandaloneCluster, + + /// + /// The standard_cluster rule. + /// + StandardCluster, + + /// + /// The symbol_cluster rule. + /// + SymbolCluster, + + /// + /// The virama_terminated_cluster rule. + /// + ViramaTerminatedCluster, + + /// + /// The vowel_syllable rule. + /// + VowelSyllable, +} diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/SyllableTypeMap.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/SyllableTypeMap.cs new file mode 100644 index 000000000..aa67b53c4 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/SyllableTypeMap.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.AdvancedTypographic; + +/// +/// Maps state machine grammar rule names to values. Invoked +/// once per matched syllable at the state machine boundary, so per-glyph storage and +/// comparison never touch the rule name strings. +/// +internal static class SyllableTypeMap +{ + /// + /// Maps a grammar rule name to its . + /// + /// The rule name reported by the state machine match. + /// + /// The corresponding type, or for a rule name with + /// no dedicated member. Unknown names must not throw: an unrecognized rule name + /// string previously compared false against every known literal, and the mapping + /// preserves that behavior. + /// + public static SyllableType FromTag(string tag) => tag switch + { + "broken_cluster" => SyllableType.BrokenCluster, + "consonant_syllable" => SyllableType.ConsonantSyllable, + "independent_cluster" => SyllableType.IndependentCluster, + "number_joiner_terminated_cluster" => SyllableType.NumberJoinerTerminatedCluster, + "numeral_cluster" => SyllableType.NumeralCluster, + "standalone_cluster" => SyllableType.StandaloneCluster, + "standard_cluster" => SyllableType.StandardCluster, + "symbol_cluster" => SyllableType.SymbolCluster, + "virama_terminated_cluster" => SyllableType.ViramaTerminatedCluster, + "vowel_syllable" => SyllableType.VowelSyllable, + _ => SyllableType.Other, + }; +} From a088e978552ef897a3bb4e7cd3f994152b1d304a Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 11:34:13 +1000 Subject: [PATCH 14/94] Query feature lookups directly for Indic substitution probes Replace the Indic shaper's would-substitute probe, which ran the full GSUB feature-application machinery on a temporary three-glyph buffer, with a direct would-apply query on the feature's lookups: every GSUB lookup subtable now answers whether a glyph id sequence would trigger it by matching its coverage, components, or context rules without substituting. Queries are digest-gated per lookup and per subtable, take raw glyph ids, disallow outside context for new-spec scripts other than Malayalam, and include the vattu variants feature when classifying below-base consonants. The per-call probe buffer and glyph-data scratch array are gone. Gate: 5,577/0 tests including exact HarfBuzz differential on Devanagari. | Scenario | Metric | Before | After | |------------|-----------|-----------|-----------| | Devanagari | Mean | 91.93 us | 56.23 us | | Devanagari | Ratio | 7.43x | 6.25x | | Devanagari | Allocated | 299,840 B | 288,232 B | | Latin | Allocated | 8,296 B | 8,296 B | | Arabic | Allocated | 7,856 B | 7,856 B | Devanagari RatioSD 0.10 this run; Latin mean 15.55 us matches its low-noise baseline and shared-path allocations are byte-identical. --- .../GSub/LookupListTable.cs | 42 ++++++ .../GSub/LookupType1SubTable.cs | 8 ++ .../GSub/LookupType2SubTable.cs | 4 + .../GSub/LookupType3SubTable.cs | 4 + .../GSub/LookupType4SubTable.cs | 36 +++++ .../GSub/LookupType5SubTable.cs | 96 +++++++++++++ .../GSub/LookupType6SubTable.cs | 111 +++++++++++++++ .../GSub/LookupType8SubTable.cs | 4 + .../GSub/NotImplementedSubTable.cs | 4 + .../Shapers/IndicShaper.cs | 133 ++++++++---------- 10 files changed, 364 insertions(+), 78 deletions(-) diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs index 691ff2c7f..4bb5c7a04 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs @@ -232,6 +232,37 @@ public bool TrySubstitution( return false; } + /// + /// Tests whether this lookup would trigger a substitution on the given glyph + /// sequence without applying it. Shapers use this to probe a feature's effect + /// while classifying glyphs, before any real substitution runs. + /// + /// The glyph id sequence to query. + /// Whether matching against context outside the sequence is disallowed. + /// if a substitution would be triggered. + public bool WouldApply(ReadOnlySpan glyphs, bool zeroContext) + { + if (glyphs.Length == 0 || !this.Digest.MightContain(glyphs[0])) + { + return false; + } + + foreach (LookupSubTable subTable in this.LookupSubTables) + { + if (!subTable.Digest.MightContain(glyphs[0])) + { + continue; + } + + if (subTable.WouldApply(glyphs, zeroContext)) + { + return true; + } + } + + return false; + } + /// /// Loads a lookup subtable based on the lookup type. /// @@ -320,4 +351,15 @@ public abstract bool TrySubstitution( Tag feature, int index, int count); + + /// + /// Tests whether this subtable would trigger a substitution on the given glyph + /// sequence without applying it. The first glyph anchors the match and the + /// sequence length must account for every matched glyph; context outside the + /// sequence is consulted only when permits it. + /// + /// The glyph id sequence to query. + /// Whether matching against context outside the sequence is disallowed. + /// if a substitution would be triggered. + public abstract bool WouldApply(ReadOnlySpan glyphs, bool zeroContext); } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs index eb00dd957..6077e27be 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs @@ -119,6 +119,10 @@ public override bool TrySubstitution( return false; } + + /// + public override bool WouldApply(ReadOnlySpan glyphs, bool zeroContext) + => glyphs.Length == 1 && this.coverageTable.CoverageIndexOf(glyphs[0]) > -1; } /// @@ -211,4 +215,8 @@ public override bool TrySubstitution( return false; } + + /// + public override bool WouldApply(ReadOnlySpan glyphs, bool zeroContext) + => glyphs.Length == 1 && this.coverageTable.CoverageIndexOf(glyphs[0]) > -1; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs index 6a8a86f13..c57c8d7ef 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs @@ -144,6 +144,10 @@ public override bool TrySubstitution( return false; } + /// + public override bool WouldApply(ReadOnlySpan glyphs, bool zeroContext) + => glyphs.Length == 1 && this.coverageTable.CoverageIndexOf(glyphs[0]) > -1; + /// /// Represents a sequence table containing an ordered list of substitute glyph IDs /// that replace a single input glyph. diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs index 408da8b5c..fdd17a4df 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs @@ -146,6 +146,10 @@ public override bool TrySubstitution( return false; } + /// + public override bool WouldApply(ReadOnlySpan glyphs, bool zeroContext) + => glyphs.Length == 1 && this.coverageTable.CoverageIndexOf(glyphs[0]) > -1; + /// /// Represents an alternate set table containing an array of alternate glyph IDs /// for a single input glyph. diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs index 01946fbb3..c1fa1d986 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs @@ -296,6 +296,42 @@ public override bool TrySubstitution( return false; } + /// + public override bool WouldApply(ReadOnlySpan glyphs, bool zeroContext) + { + int offset = this.coverageTable.CoverageIndexOf(glyphs[0]); + if (offset < 0 || offset >= this.ligatureSetTables.Length) + { + return false; + } + + foreach (LigatureTable ligature in this.ligatureSetTables[offset].Ligatures) + { + ushort[] components = ligature.ComponentGlyphs; + if (components.Length + 1 != glyphs.Length) + { + continue; + } + + bool matched = true; + for (int i = 0; i < components.Length; i++) + { + if (glyphs[i + 1] != components[i]) + { + matched = false; + break; + } + } + + if (matched) + { + return true; + } + } + + return false; + } + /// /// Represents a ligature set table containing an array of ligature tables /// for a single first-component glyph, ordered by preference. diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs index f5b88ec00..32058d6de 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs @@ -136,6 +136,42 @@ public override bool TrySubstitution( return false; } + + /// + public override bool WouldApply(ReadOnlySpan glyphs, bool zeroContext) + { + int offset = this.coverageTable.CoverageIndexOf(glyphs[0]); + if (offset < 0 || offset >= this.seqRuleSetTables.Length) + { + return false; + } + + foreach (SequenceRuleTable rule in this.seqRuleSetTables[offset].SequenceRuleTables) + { + ushort[] input = rule.InputSequence; + if (input.Length + 1 != glyphs.Length) + { + continue; + } + + bool matched = true; + for (int i = 0; i < input.Length; i++) + { + if (glyphs[i + 1] != input[i]) + { + matched = false; + break; + } + } + + if (matched) + { + return true; + } + } + + return false; + } } /// @@ -263,6 +299,46 @@ public override bool TrySubstitution( return false; } + + /// + public override bool WouldApply(ReadOnlySpan glyphs, bool zeroContext) + { + int classId = this.classDefinitionTable.ClassIndexOf(glyphs[0]); + ClassSequenceRuleTable[]? rules = classId >= 0 && classId < this.sequenceRuleSetTables.Length + ? this.sequenceRuleSetTables[classId]?.SequenceRuleTables + : null; + + if (rules is null) + { + return false; + } + + foreach (ClassSequenceRuleTable rule in rules) + { + ushort[] input = rule.InputSequence; + if (input.Length + 1 != glyphs.Length) + { + continue; + } + + bool matched = true; + for (int i = 0; i < input.Length; i++) + { + if (this.classDefinitionTable.ClassIndexOf(glyphs[i + 1]) != input[i]) + { + matched = false; + break; + } + } + + if (matched) + { + return true; + } + } + + return false; + } } /// @@ -363,4 +439,24 @@ public override bool TrySubstitution( index, count); } + + /// + public override bool WouldApply(ReadOnlySpan glyphs, bool zeroContext) + { + CoverageTable[] coverages = this.coverageTables; + if (coverages.Length != glyphs.Length) + { + return false; + } + + for (int i = 1; i < glyphs.Length; i++) + { + if (coverages[i].CoverageIndexOf(glyphs[i]) < 0) + { + return false; + } + } + + return true; + } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs index e5bae3b95..419cb2dd1 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs @@ -140,6 +140,47 @@ public override bool TrySubstitution( return false; } + + /// + public override bool WouldApply(ReadOnlySpan glyphs, bool zeroContext) + { + int offset = this.coverageTable.CoverageIndexOf(glyphs[0]); + if (offset < 0 || offset >= this.seqRuleSetTables.Length) + { + return false; + } + + foreach (ChainedSequenceRuleTable rule in this.seqRuleSetTables[offset].SequenceRuleTables) + { + if (zeroContext && (rule.BacktrackSequence.Length != 0 || rule.LookaheadSequence.Length != 0)) + { + continue; + } + + ushort[] input = rule.InputSequence; + if (input.Length + 1 != glyphs.Length) + { + continue; + } + + bool matched = true; + for (int i = 0; i < input.Length; i++) + { + if (glyphs[i + 1] != input[i]) + { + matched = false; + break; + } + } + + if (matched) + { + return true; + } + } + + return false; + } } /// @@ -289,6 +330,51 @@ public override bool TrySubstitution( return false; } + + /// + public override bool WouldApply(ReadOnlySpan glyphs, bool zeroContext) + { + int classId = this.inputClassDefinitionTable.ClassIndexOf(glyphs[0]); + ChainedClassSequenceRuleTable[]? rules = classId >= 0 && classId < this.sequenceRuleSetTables.Length + ? this.sequenceRuleSetTables[classId]?.SubRules + : null; + + if (rules is null) + { + return false; + } + + foreach (ChainedClassSequenceRuleTable rule in rules) + { + if (zeroContext && (rule.BacktrackSequence.Length != 0 || rule.LookaheadSequence.Length != 0)) + { + continue; + } + + ushort[] input = rule.InputSequence; + if (input.Length + 1 != glyphs.Length) + { + continue; + } + + bool matched = true; + for (int i = 0; i < input.Length; i++) + { + if (this.inputClassDefinitionTable.ClassIndexOf(glyphs[i + 1]) != input[i]) + { + matched = false; + break; + } + } + + if (matched) + { + return true; + } + } + + return false; + } } /// @@ -423,4 +509,29 @@ public override bool TrySubstitution( index, count); } + + /// + public override bool WouldApply(ReadOnlySpan glyphs, bool zeroContext) + { + if (zeroContext && (this.backtrackCoverageTables.Length != 0 || this.lookaheadCoverageTables.Length != 0)) + { + return false; + } + + CoverageTable[] coverages = this.inputCoverageTables; + if (coverages.Length != glyphs.Length) + { + return false; + } + + for (int i = 1; i < glyphs.Length; i++) + { + if (coverages[i].CoverageIndexOf(glyphs[i]) < 0) + { + return false; + } + } + + return true; + } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType8SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType8SubTable.cs index 534eb1f02..2257ba838 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType8SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType8SubTable.cs @@ -201,4 +201,8 @@ public override bool TrySubstitution( return hasChanged; } + + /// + public override bool WouldApply(ReadOnlySpan glyphs, bool zeroContext) + => glyphs.Length == 1 && this.coverageTable.CoverageIndexOf(glyphs[0]) > -1; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/NotImplementedSubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/NotImplementedSubTable.cs index d12071514..76743a765 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/NotImplementedSubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/NotImplementedSubTable.cs @@ -26,4 +26,8 @@ public override bool TrySubstitution( int index, int count) => false; + + /// + public override bool WouldApply(ReadOnlySpan glyphs, bool zeroContext) + => false; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs index 05b007643..601e416cf 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Globalization; +using SixLabors.Fonts.Tables.AdvancedTypographic.GSub; using SixLabors.Fonts.Unicode; using SixLabors.Fonts.Unicode.Resources; using UnicodeTrieGenerator.StateAutomation; @@ -87,9 +88,6 @@ internal sealed class IndicShaper : DefaultShaper /// Dotted circle code point (U+25CC) used as a placeholder base. private const int DottedCircle = 0x25cc; - /// The text options. - private readonly TextOptions textOptions; - /// The font metrics used for glyph lookups. private readonly FontMetrics fontMetrics; @@ -99,6 +97,12 @@ internal sealed class IndicShaper : DefaultShaper /// Whether this font uses old-spec Indic script tags. private readonly bool isOldSpec; + /// + /// Whether feature probes disallow matching context outside the probed glyph + /// sequence. New-spec scripts other than Malayalam match with zero context. + /// + private readonly bool zeroContext; + /// Whether any broken clusters were detected during syllable setup. private bool hasBrokenClusters; @@ -112,7 +116,6 @@ internal sealed class IndicShaper : DefaultShaper public IndicShaper(ScriptClass script, Tag unicodeScriptTag, TextOptions textOptions, FontMetrics fontMetrics) : base(script, MarkZeroingMode.None, textOptions) { - this.textOptions = textOptions; this.fontMetrics = fontMetrics; if (IndicConfigurations.TryGetValue(script, out ShapingConfiguration value)) @@ -125,6 +128,7 @@ public IndicShaper(ScriptClass script, Tag unicodeScriptTag, TextOptions textOpt } this.isOldSpec = this.indicConfiguration.HasOldSpec && !unicodeScriptTag.ToString().EndsWith("2", StringComparison.OrdinalIgnoreCase); + this.zeroContext = !this.isOldSpec && script != ScriptClass.Malayalam; } /// @@ -322,13 +326,10 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) return; } - // Create a reusable temporary substitution buffer and probe span to allow checking whether - // certain combinations will be substituted. The probe buffer MUST share the - // pass's feature map: the probed glyphs are copies of real glyphs whose feature - // masks were assigned by it, and a private map would resolve the probed feature - // to a different bit, turning the enable into a no-op. - ShapingBuffer tempCollection = new(this.textOptions, buffer.FeatureMap, ShapingBufferRole.Substitution); - Span tempBuffer = new GlyphShapingData[3]; + // Reusable glyph id span for feature probes. Hoisted out of the syllable loop + // because a stack allocation inside the loop body would grow the stack once + // per syllable for the lifetime of the call. + Span probeGlyphs = stackalloc ushort[3]; ShapingConfiguration indicConfiguration = this.indicConfiguration; FontMetrics fontMetrics = this.fontMetrics; @@ -342,17 +343,7 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) if (data.Syllable.IndicPosition == Positions.Base_C) { - GlyphShapingData virama = new(data, false) - { - GlyphId = viramaId, - CodePoint = viramaPoint - }; - - tempBuffer[2] = virama; - tempBuffer[1] = data; - tempBuffer[0] = virama; - - data.Syllable.IndicPosition = this.ConsonantPosition(tempCollection, tempBuffer); + data.Syllable.IndicPosition = this.ConsonantPosition(buffer, viramaId, data.GlyphId); } } } @@ -437,12 +428,12 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) (indicConfiguration.RephMode == RephMode.Explicit && buffer[start + 2].Syllable.IndicCategory == Categories.ZWJ))) { // See if it matches the 'rphf' feature. - tempBuffer[2] = buffer[start + 2]; - tempBuffer[1] = buffer[start + 1]; - tempBuffer[0] = buffer[start]; + probeGlyphs[0] = buffer[start].GlyphId; + probeGlyphs[1] = buffer[start + 1].GlyphId; + probeGlyphs[2] = buffer[start + 2].GlyphId; - if ((indicConfiguration.RephMode == RephMode.Explicit && this.WouldSubstitute(tempCollection, in RphfTag, tempBuffer)) || - this.WouldSubstitute(tempCollection, in RphfTag, tempBuffer[..2])) + if ((indicConfiguration.RephMode == RephMode.Explicit && this.WouldSubstitute(buffer, in RphfTag, probeGlyphs)) || + this.WouldSubstitute(buffer, in RphfTag, probeGlyphs[..2])) { limit += 2; while (limit < end && IsJoiner(ref buffer[limit])) @@ -836,9 +827,9 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) // Find a Halant,Ra sequence and mark it for pre-base reordering processing. for (int i = basePosition + 1; i + prefLen - 1 < end; i++) { - tempBuffer[1] = buffer[i + 1]; - tempBuffer[0] = buffer[i]; - if (this.WouldSubstitute(tempCollection, in PrefTag, tempBuffer[..2])) + probeGlyphs[0] = buffer[i].GlyphId; + probeGlyphs[1] = buffer[i + 1].GlyphId; + if (this.WouldSubstitute(buffer, in PrefTag, probeGlyphs[..2])) { for (int j = 0; j < prefLen; j++) { @@ -898,28 +889,37 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) } /// - /// Determines the positional class of a consonant by testing whether it would be - /// substituted by below-base, post-base, or pre-base features. + /// Determines the positional class of a consonant by testing whether the + /// virama-consonant and consonant-virama pairs would be substituted by the + /// below-base, vattu, post-base, or pre-base forming features. /// - /// A temporary substitution buffer for testing. - /// The consonant and virama glyph data to test. + /// The glyph shaping buffer providing the language tags. + /// The virama glyph id. + /// The consonant glyph id. /// The consonant's positional class. - private Positions ConsonantPosition(ShapingBuffer buffer, ReadOnlySpan data) + private Positions ConsonantPosition(ShapingBuffer buffer, ushort virama, ushort consonant) { - if (this.WouldSubstitute(buffer, in BlwfTag, data[..2]) || - this.WouldSubstitute(buffer, in BlwfTag, data.Slice(1, 2))) + Span glyphs = stackalloc ushort[3]; + glyphs[0] = virama; + glyphs[1] = consonant; + glyphs[2] = virama; + + if (this.WouldSubstitute(buffer, in BlwfTag, glyphs[..2]) || + this.WouldSubstitute(buffer, in BlwfTag, glyphs.Slice(1, 2)) || + this.WouldSubstitute(buffer, in VatuTag, glyphs[..2]) || + this.WouldSubstitute(buffer, in VatuTag, glyphs.Slice(1, 2))) { return Positions.Below_C; } - if (this.WouldSubstitute(buffer, in PstfTag, data[..2]) || - this.WouldSubstitute(buffer, in PstfTag, data.Slice(1, 2))) + if (this.WouldSubstitute(buffer, in PstfTag, glyphs[..2]) || + this.WouldSubstitute(buffer, in PstfTag, glyphs.Slice(1, 2))) { return Positions.Post_C; } - if (this.WouldSubstitute(buffer, in PrefTag, data[..2]) || - this.WouldSubstitute(buffer, in PrefTag, data.Slice(1, 2))) + if (this.WouldSubstitute(buffer, in PrefTag, glyphs[..2]) || + this.WouldSubstitute(buffer, in PrefTag, glyphs.Slice(1, 2))) { return Positions.Post_C; } @@ -928,51 +928,28 @@ private Positions ConsonantPosition(ShapingBuffer buffer, ReadOnlySpan - /// Tests whether applying a specific feature to the given glyphs would produce a substitution. + /// Tests whether applying a specific feature to the given glyph sequence would + /// produce a substitution, querying the feature's lookups directly without + /// running any substitution. /// - /// A temporary substitution buffer for testing. + /// The glyph shaping buffer providing the language tags. /// The feature tag to test. - /// The glyph data to test. + /// The glyph id sequence to test. /// if a substitution would occur. - private bool WouldSubstitute(ShapingBuffer buffer, in Tag featureTag, ReadOnlySpan glyphs) + private bool WouldSubstitute(ShapingBuffer buffer, in Tag featureTag, ReadOnlySpan glyphs) { - buffer.Clear(); - for (int i = 0; i < glyphs.Length; i++) + if (!this.fontMetrics.TryGetGSubTable(out GSubTable? gSubTable) || + !gSubTable.TryGetFeatureLookups(this.fontMetrics, in featureTag, this.ScriptClass, buffer.LanguageTags, out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? lookups)) { - buffer.AddGlyph(glyphs[i], i); - buffer.EnableShapingFeature(i, featureTag); + return false; } - FontMetrics fontMetrics = this.fontMetrics; - if (fontMetrics.TryGetGSubTable(out GSubTable? gSubTable)) + foreach ((Tag _, ushort _, LookupTable lookupTable) in lookups) { - const int index = 0; - SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, default, 0); - int initialCount = buffer.Count; - int collectionCount = initialCount; - int count = initialCount - index; - int i = index; - - // Set max constraints to prevent OutOfMemoryException or infinite loops from attacks. - int maxCount = AdvancedTypographicUtils.GetMaxAllowableShapingCollectionCount(buffer.Count); - int maxOperationsCount = AdvancedTypographicUtils.GetMaxAllowableShapingOperationsCount(buffer.Count); - int currentOperations = 0; - - gSubTable.ApplyFeature( - fontMetrics, - buffer, - ref iterator, - in featureTag, - this.ScriptClass, - index, - ref count, - ref i, - ref collectionCount, - maxCount, - maxOperationsCount, - ref currentOperations); - - return buffer.Count != initialCount; + if (lookupTable.WouldApply(glyphs, this.zeroContext)) + { + return true; + } } return false; From 86474cd6aa55667c355c46e36b6006f873cfb662 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 11:44:42 +1000 Subject: [PATCH 15/94] Cache the generated universal shaping decompositions dictionary The generated UniversalShapingData.Decompositions property was expression-bodied, so every access rebuilt the full dictionary and all of its component arrays. The Indic and universal shapers consult it once per glyph when checking for split matra decompositions, which made dictionary construction the single largest cost in complex-script shaping. The property now caches its value; the generator template is fixed to emit the cached form. Gate: 5,577/0 tests including exact HarfBuzz differential on Devanagari. | Scenario | Metric | Before | After | |------------|-----------|-----------|----------| | Devanagari | Mean | 56.23 us | 21.19 us | | Devanagari | Ratio | 6.25x | 2.34x | | Devanagari | Allocated | 288,232 B | 11,656 B | | Latin | Allocated | 8,296 B | 8,296 B | | Arabic | Allocated | 7,856 B | 7,856 B | Devanagari RatioSD 0.04; Latin 15.50 us and Arabic 16.25 us match their low-noise baselines with byte-identical allocations. --- .../Unicode/Resources/UniversalShapingData.Generated.cs | 2 +- src/UnicodeTrieGenerator/Generator.UniversalShapingEngine.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SixLabors.Fonts/Unicode/Resources/UniversalShapingData.Generated.cs b/src/SixLabors.Fonts/Unicode/Resources/UniversalShapingData.Generated.cs index fce249dc3..dce51ab03 100644 --- a/src/SixLabors.Fonts/Unicode/Resources/UniversalShapingData.Generated.cs +++ b/src/SixLabors.Fonts/Unicode/Resources/UniversalShapingData.Generated.cs @@ -53,7 +53,7 @@ internal static class UniversalShapingData "HN" }; - public static Dictionary Decompositions => new() + public static Dictionary Decompositions { get; } = new() { { 0x9CB, new int[] { 0x9C7,0x9BE } }, { 0x9CC, new int[] { 0x9C7,0x9D7 } }, diff --git a/src/UnicodeTrieGenerator/Generator.UniversalShapingEngine.cs b/src/UnicodeTrieGenerator/Generator.UniversalShapingEngine.cs index 96e597595..d89528464 100644 --- a/src/UnicodeTrieGenerator/Generator.UniversalShapingEngine.cs +++ b/src/UnicodeTrieGenerator/Generator.UniversalShapingEngine.cs @@ -730,7 +730,7 @@ private static void GenerateDataClass( // Write the decompositions if (decompositions != null) { - writer.WriteLine(" public static Dictionary Decompositions => new()"); + writer.WriteLine(" public static Dictionary Decompositions { get; } = new()"); writer.WriteLine(" {"); counter = 0; From 1bf8db8a7e7dd71da26184e3ad7ac512a72262fc Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 11:44:45 +1000 Subject: [PATCH 16/94] Add plan and stage sub-phase counters to the shaping probe Attribute substitution-phase time and allocation to shaper creation, planning (preprocessing, features, postprocessing, assignment), stage pre/post actions, and stage application. These counters located the decomposition dictionary rebuild; all are inert unless the probe is enabled. --- src/SixLabors.Fonts/ShapingProbe.cs | 16 ++++++++++++++++ .../Tables/AdvancedTypographic/GSubTable.cs | 10 ++++++++++ .../AdvancedTypographic/Shapers/BaseShaper.cs | 8 ++++++++ 3 files changed, 34 insertions(+) diff --git a/src/SixLabors.Fonts/ShapingProbe.cs b/src/SixLabors.Fonts/ShapingProbe.cs index 24f1c5956..fbf5e4e0f 100644 --- a/src/SixLabors.Fonts/ShapingProbe.cs +++ b/src/SixLabors.Fonts/ShapingProbe.cs @@ -20,6 +20,14 @@ public static class ShapingProbe public const int Positioning = 6; public const int Projection = 7; public const int LookupResolve = 8; + public const int SubShaperCreate = 9; + public const int SubShaperPlan = 10; + public const int SubStagePrePost = 11; + public const int SubStageApply = 12; + public const int PlanPre = 13; + public const int PlanMain = 14; + public const int PlanPost = 15; + public const int PlanAssign = 16; private static readonly string[] Names = [ @@ -32,6 +40,14 @@ public static class ShapingProbe "GPOS positioning", "Projection (ShapedGlyph)", " of which lookup resolve", + " sub: shaper create", + " sub: shaper plan", + " sub: stage pre/post", + " sub: stage apply", + " plan: preprocessing", + " plan: features", + " plan: postprocessing", + " plan: assign", ]; private static readonly long[] Ticks = new long[Names.Length]; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs index 4bab9783b..b6d95942b 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs @@ -194,13 +194,17 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas } Tag unicodeScriptTag = this.GetUnicodeScriptTag(current); + var createProbe = ShapingProbe.Enter(); BaseShaper shaper = ShaperFactory.Create(current, unicodeScriptTag, fontMetrics, buffer.TextOptions); + ShapingProbe.Exit(ShapingProbe.SubShaperCreate, createProbe); // Plan substitution features for each glyph. // Shapers can adjust the count during initialization and feature processing so we must capture // the current count to allow resetting indexes and processing counts. int collectionCount = buffer.Count; + var planProbe = ShapingProbe.Enter(); shaper.Plan(buffer, index, count); + ShapingProbe.Exit(ShapingProbe.SubShaperPlan, planProbe); int delta = buffer.Count - collectionCount; i += delta; count += delta; @@ -210,7 +214,9 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas foreach (ShapingStage stage in stages) { collectionCount = buffer.Count; + var preProbe = ShapingProbe.Enter(); stage.PreProcessFeature(buffer, index, count); + ShapingProbe.Exit(ShapingProbe.SubStagePrePost, preProbe); // Account for substitutions changing the length of the buffer. delta = buffer.Count - collectionCount; @@ -219,6 +225,7 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas Tag featureTag = stage.FeatureTag; + var applyProbe = ShapingProbe.Enter(); this.ApplyFeature( fontMetrics, buffer, @@ -232,9 +239,12 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas maxCount, maxOperationsCount, ref currentOperations); + ShapingProbe.Exit(ShapingProbe.SubStageApply, applyProbe); collectionCount = buffer.Count; + var postProbe = ShapingProbe.Enter(); stage.PostProcessFeature(buffer, index, count); + ShapingProbe.Exit(ShapingProbe.SubStagePrePost, postProbe); // Account for substitutions changing the length of the buffer. delta = buffer.Count - collectionCount; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs index 1a4c610cd..2132900b7 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs @@ -31,19 +31,27 @@ public void Plan(ShapingBuffer buffer, int index, int count) { int collectionCount = buffer.Count; + var preProbe = ShapingProbe.Enter(); this.PlanPreprocessingFeatures(buffer, index, count); + ShapingProbe.Exit(ShapingProbe.PlanPre, preProbe); RecalculateCount(buffer, ref collectionCount, ref count); + var mainProbe = ShapingProbe.Enter(); this.PlanFeatures(buffer, index, count); + ShapingProbe.Exit(ShapingProbe.PlanMain, mainProbe); RecalculateCount(buffer, ref collectionCount, ref count); + var postProbe = ShapingProbe.Enter(); this.PlanPostprocessingFeatures(buffer, index, count); + ShapingProbe.Exit(ShapingProbe.PlanPost, postProbe); RecalculateCount(buffer, ref collectionCount, ref count); + var assignProbe = ShapingProbe.Enter(); this.AssignFeatures(buffer, index, count); + ShapingProbe.Exit(ShapingProbe.PlanAssign, assignProbe); } /// From 6d6a96a77d0071009943ce8eb8909fdad2e46926 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 11:55:37 +1000 Subject: [PATCH 17/94] Count graphemes once per run build with a boundary-only walk BuildTextRuns counted graphemes twice for the common no-runs path; it now reuses the first count. GetGraphemeCount also drove the full cluster enumerator, which computes terminal width, emoji, and flag metadata for every scalar that a count never reads. The enumerator gains an internal count-only mode that walks the same UAX 29 boundary rules and skips all cluster metadata, so counting stays a single implementation of the boundary algorithm. A count-only walk is 11.2 ns per char across scripts, down from up to 26. Gate: 5,577/0 tests. | Scenario | Mean before | Mean after | Ratio | |------------|-------------|------------|---------------| | Latin | 15.50 us | 13.84 us | 6.78x > 6.14x | | Arabic | 16.25 us | 14.49 us | 4.15x > 3.82x | | Devanagari | 21.19 us | 20.41 us | 2.34x > 2.25x | Allocations byte-identical in all scenarios; RatioSD at or below 0.14. Includes a probe phase around the counting call. --- src/SixLabors.Fonts/ShapingProbe.cs | 2 + src/SixLabors.Fonts/TextShaper.Pipeline.cs | 4 +- .../Unicode/MemoryExtensions.cs | 4 +- .../Unicode/SpanGraphemeEnumerator.cs | 49 +++++++++++++++---- 4 files changed, 46 insertions(+), 13 deletions(-) diff --git a/src/SixLabors.Fonts/ShapingProbe.cs b/src/SixLabors.Fonts/ShapingProbe.cs index fbf5e4e0f..8070088b5 100644 --- a/src/SixLabors.Fonts/ShapingProbe.cs +++ b/src/SixLabors.Fonts/ShapingProbe.cs @@ -28,6 +28,7 @@ public static class ShapingProbe public const int PlanMain = 14; public const int PlanPost = 15; public const int PlanAssign = 16; + public const int GraphemeCount = 17; private static readonly string[] Names = [ @@ -48,6 +49,7 @@ public static class ShapingProbe " plan: features", " plan: postprocessing", " plan: assign", + " runs: grapheme count", ]; private static readonly long[] Ticks = new long[Names.Length]; diff --git a/src/SixLabors.Fonts/TextShaper.Pipeline.cs b/src/SixLabors.Fonts/TextShaper.Pipeline.cs index 77be4bb74..421a30cdc 100644 --- a/src/SixLabors.Fonts/TextShaper.Pipeline.cs +++ b/src/SixLabors.Fonts/TextShaper.Pipeline.cs @@ -37,7 +37,9 @@ public static partial class TextShaper internal static IReadOnlyList BuildTextRuns(ReadOnlySpan text, TextOptions options) { int start = 0; + var graphemeProbe = ShapingProbe.Enter(); int end = text.GetGraphemeCount(); + ShapingProbe.Exit(ShapingProbe.GraphemeCount, graphemeProbe); if (end == 0) { return []; @@ -48,7 +50,7 @@ internal static IReadOnlyList BuildTextRuns(ReadOnlySpan text, Te TextRun textRun = new() { Start = 0, - End = text.GetGraphemeCount(), + End = end, Font = options.Font }; diff --git a/src/SixLabors.Fonts/Unicode/MemoryExtensions.cs b/src/SixLabors.Fonts/Unicode/MemoryExtensions.cs index b1554a24d..613925ce6 100644 --- a/src/SixLabors.Fonts/Unicode/MemoryExtensions.cs +++ b/src/SixLabors.Fonts/Unicode/MemoryExtensions.cs @@ -224,7 +224,7 @@ public static int GetTerminalCellWidth(this Span span, TerminalWidthOption public static int GetGraphemeCount(this ReadOnlySpan span) { int count = 0; - SpanGraphemeEnumerator enumerator = new(span); + SpanGraphemeEnumerator enumerator = new(span, countOnly: true); while (enumerator.MoveNext()) { count++; @@ -241,7 +241,7 @@ public static int GetGraphemeCount(this ReadOnlySpan span) public static int GetGraphemeCount(this Span span) { int count = 0; - SpanGraphemeEnumerator enumerator = new(span); + SpanGraphemeEnumerator enumerator = new(span, countOnly: true); while (enumerator.MoveNext()) { count++; diff --git a/src/SixLabors.Fonts/Unicode/SpanGraphemeEnumerator.cs b/src/SixLabors.Fonts/Unicode/SpanGraphemeEnumerator.cs index 40e9c20c7..c77bcaff7 100644 --- a/src/SixLabors.Fonts/Unicode/SpanGraphemeEnumerator.cs +++ b/src/SixLabors.Fonts/Unicode/SpanGraphemeEnumerator.cs @@ -18,6 +18,7 @@ public ref struct SpanGraphemeEnumerator { private ReadOnlySpan source; private readonly TerminalWidthOptions terminalWidthOptions; + private readonly bool countOnly; private int sourceOffset; /// @@ -38,6 +39,26 @@ public SpanGraphemeEnumerator(ReadOnlySpan source, TerminalWidthOptions te { this.source = source; this.terminalWidthOptions = terminalWidthOptions; + this.countOnly = false; + this.sourceOffset = 0; + this.Current = default; + } + + /// + /// Initializes a new instance of the struct + /// that walks cluster boundaries without producing cluster metadata. + /// + /// The buffer to read from. + /// + /// Whether enumeration only advances over boundaries. When set, + /// stays default and no width, emoji, or flag metadata is + /// computed, leaving a pure boundary walk for callers that only need a count. + /// + internal SpanGraphemeEnumerator(ReadOnlySpan source, bool countOnly) + { + this.source = source; + this.terminalWidthOptions = TerminalWidthOptions.Default; + this.countOnly = countOnly; this.sourceOffset = 0; this.Current = default; } @@ -67,6 +88,7 @@ public bool MoveNext() // outside Processor so Processor remains a simple UTF-16/code-point reader. IndicConjunctState indicConjunctState = default; TerminalWidthState terminalWidthState = new(this.terminalWidthOptions); + bool boundariesOnly = this.countOnly; int utf16Offset = this.sourceOffset; // Accept the current scalar into the cluster and advance to the next scalar. @@ -74,7 +96,11 @@ public bool MoveNext() void ConsumeCurrentAndAdvance(ref Processor p) { indicConjunctState.Consume(p.CurrentCodePoint); - terminalWidthState.Consume(p.CurrentCodePoint, p.CurrentType); + if (!boundariesOnly) + { + terminalWidthState.Consume(p.CurrentCodePoint, p.CurrentType); + } + p.MoveNext(); } @@ -253,15 +279,18 @@ or GraphemeClusterClass.CarriageReturn Return: - terminalWidthState.Complete(); - ReadOnlySpan grapheme = this.source[..processor.CharsConsumed]; - this.Current = new GraphemeCluster( - grapheme, - utf16Offset, - terminalWidthState.CodePointCount, - terminalWidthState.TerminalCellWidth, - terminalWidthState.Flags, - terminalWidthState.FirstCodePoint); + if (!boundariesOnly) + { + terminalWidthState.Complete(); + ReadOnlySpan grapheme = this.source[..processor.CharsConsumed]; + this.Current = new GraphemeCluster( + grapheme, + utf16Offset, + terminalWidthState.CodePointCount, + terminalWidthState.TerminalCellWidth, + terminalWidthState.Flags, + terminalWidthState.FirstCodePoint); + } this.source = this.source[processor.CharsConsumed..]; this.sourceOffset += processor.CharsConsumed; From a995598067874694303e8915b1b6216a5d615001 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 11:57:27 +1000 Subject: [PATCH 18/94] Pass the count-only flag positionally --- src/SixLabors.Fonts/Unicode/MemoryExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SixLabors.Fonts/Unicode/MemoryExtensions.cs b/src/SixLabors.Fonts/Unicode/MemoryExtensions.cs index 613925ce6..72321bd58 100644 --- a/src/SixLabors.Fonts/Unicode/MemoryExtensions.cs +++ b/src/SixLabors.Fonts/Unicode/MemoryExtensions.cs @@ -224,7 +224,7 @@ public static int GetTerminalCellWidth(this Span span, TerminalWidthOption public static int GetGraphemeCount(this ReadOnlySpan span) { int count = 0; - SpanGraphemeEnumerator enumerator = new(span, countOnly: true); + SpanGraphemeEnumerator enumerator = new(span, true); while (enumerator.MoveNext()) { count++; @@ -241,7 +241,7 @@ public static int GetGraphemeCount(this ReadOnlySpan span) public static int GetGraphemeCount(this Span span) { int count = 0; - SpanGraphemeEnumerator enumerator = new(span, countOnly: true); + SpanGraphemeEnumerator enumerator = new(span, true); while (enumerator.MoveNext()) { count++; From 129c0975265ef5f9d588d4398f71f1fdef594c27 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 12:02:45 +1000 Subject: [PATCH 19/94] Register direction features per direction span The shaping plan registered ltra/ltrm or rtla/rtlm with a separate single-glyph registration for every glyph, resolving the feature mask per glyph per pass. Direction features now register once per consecutive same-direction span, matching how the shape plan enables a segment direction's features once and producing identical per-glyph masks. Gate: 5,577/0 tests. | Scenario | Mean before | Mean after | Ratio | |------------|-------------|------------|---------------| | Latin | 13.84 us | 12.08 us | 6.14x > 5.24x | | Arabic | 14.49 us | 13.52 us | 3.82x > 3.44x | | Devanagari | 20.41 us | 19.90 us | 2.25x > 2.12x | Allocations byte-identical in all scenarios; RatioSD at or below 0.08. --- .../Shapers/DefaultShaper.cs | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs index 901b453da..b9744051e 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs @@ -128,21 +128,34 @@ protected override void PlanPreprocessingFeatures(ShapingBuffer buffer, int inde // Add variation Features. this.AddFeature(buffer, index, count, RvnrTag); - // Add directional features. - for (int i = index; i < index + count; i++) + // Add directional features once per direction span. The plan registers a + // direction's features across the whole span in a single range registration, + // producing the same per-glyph masks as per-glyph registration while + // resolving each feature's mask bit once. + int end = index + count; + int spanStart = index; + while (spanStart < end) { - ref GlyphShapingData shapingData = ref buffer[i]; + TextDirection direction = buffer[spanStart].Direction; + int spanEnd = spanStart + 1; + while (spanEnd < end && buffer[spanEnd].Direction == direction) + { + spanEnd++; + } - if (shapingData.Direction == TextDirection.LeftToRight) + int spanCount = spanEnd - spanStart; + if (direction == TextDirection.LeftToRight) { - this.AddFeature(buffer, i, 1, LtraTag); - this.AddFeature(buffer, i, 1, LtrmTag); + this.AddFeature(buffer, spanStart, spanCount, LtraTag); + this.AddFeature(buffer, spanStart, spanCount, LtrmTag); } else { - this.AddFeature(buffer, i, 1, RtlaTag); - this.AddFeature(buffer, i, 1, RtlmTag); + this.AddFeature(buffer, spanStart, spanCount, RtlaTag); + this.AddFeature(buffer, spanStart, spanCount, RtlmTag); } + + spanStart = spanEnd; } // TODO: Fractional feature should be assigned here but disabled. From 3a0b93975784edb002c8de9591db8eb6034575da Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 12:29:44 +1000 Subject: [PATCH 20/94] Add ticks-only probe mode Byte sampling and per-feature detail cost far more than timestamping and were inflating every enclosing phase's measured time. Both are now switchable so phase timings can be read without them. --- src/SixLabors.Fonts/ShapingProbe.cs | 33 ++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/SixLabors.Fonts/ShapingProbe.cs b/src/SixLabors.Fonts/ShapingProbe.cs index 8070088b5..b5dd7557d 100644 --- a/src/SixLabors.Fonts/ShapingProbe.cs +++ b/src/SixLabors.Fonts/ShapingProbe.cs @@ -57,6 +57,22 @@ public static class ShapingProbe public static bool Enabled { get; set; } + /// + /// Gets or sets a value indicating whether phase probes also sample allocated + /// bytes. Byte sampling costs far more than timestamping, so disable it when only + /// phase timings matter: nested probe pairs otherwise inflate every enclosing + /// phase's measured time. + /// + public static bool BytesEnabled { get; set; } = true; + + /// + /// Gets or sets a value indicating whether per-feature timing detail is recorded. + /// Feature exits build a key string and touch three dictionaries per feature per + /// pass, which distorts the enclosing substitution and positioning phase timings + /// when only phases matter. + /// + public static bool FeatureDetailEnabled { get; set; } = true; + public static readonly Dictionary FeatureTicks = []; public static readonly Dictionary FeatureApplies = []; @@ -100,7 +116,7 @@ public static long Timestamp() public static void ExitFeature(string table, Tag feature, long start, long applies) { - if (!Enabled) + if (!Enabled || !FeatureDetailEnabled) { return; } @@ -125,7 +141,14 @@ public static void PrintFeatures(int iterations) } public static (long Ticks, long Bytes) Enter() - => Enabled ? (Stopwatch.GetTimestamp(), GC.GetAllocatedBytesForCurrentThread()) : default; + { + if (!Enabled) + { + return default; + } + + return (Stopwatch.GetTimestamp(), BytesEnabled ? GC.GetAllocatedBytesForCurrentThread() : 0); + } public static void Exit(int phase, (long Ticks, long Bytes) entry) { @@ -134,7 +157,11 @@ public static void Exit(int phase, (long Ticks, long Bytes) entry) return; } - Bytes[phase] += GC.GetAllocatedBytesForCurrentThread() - entry.Bytes; + if (BytesEnabled) + { + Bytes[phase] += GC.GetAllocatedBytesForCurrentThread() - entry.Bytes; + } + Ticks[phase] += Stopwatch.GetTimestamp() - entry.Ticks; } From ec766daf812f96223e0fc59cf93e58fbbc2ce807 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 12:29:47 +1000 Subject: [PATCH 21/94] Apply stage groups in lookup-index order with combined masks Substitution and positioning applied each stage feature separately, in feature registration order, resolving and walking the buffer per feature. Stage actions are the only true synchronization points, so stages between actions now form one group: the group's lookups merge into a single list sorted by lookup index, the order the specification defines within an application pass, and a lookup registered by several features applies once with their glyph masks combined. Merging reuses a scratch list on the pooled buffer, so application still allocates nothing. Gate: 5,577/0 tests including exact HarfBuzz differential on Devanagari; application order changes are covered by both. | Scenario | Mean before | Mean after | Notes | |------------|-------------|------------|------------------------------| | Latin | 12.08 us | 11.56 us | clean run, RatioSD 0.10 | | Arabic | 13.52 us | 13.92 us | machine loaded, HB elevated | | Devanagari | 19.90 us | 20.46 us | within cross-run spread | Allocations byte-identical in all scenarios. Arabic and Devanagari rows were measured under active machine load (their HarfBuzz controls read 40-60% above idle baselines); both sit inside the spread identical code has shown across runs. --- src/SixLabors.Fonts/ShapingBuffer.cs | 14 ++ .../Tables/AdvancedTypographic/GPosTable.cs | 141 +++++++++---- .../Tables/AdvancedTypographic/GSubTable.cs | 196 +++++++++++------- .../Shapers/ShapingStage.cs | 14 ++ 4 files changed, 251 insertions(+), 114 deletions(-) diff --git a/src/SixLabors.Fonts/ShapingBuffer.cs b/src/SixLabors.Fonts/ShapingBuffer.cs index 8e5e36c0c..9300e14fc 100644 --- a/src/SixLabors.Fonts/ShapingBuffer.cs +++ b/src/SixLabors.Fonts/ShapingBuffer.cs @@ -108,6 +108,20 @@ public ShapingBuffer(TextOptions textOptions, ShapingFeatureMap featureMap, Shap /// public int LigatureId { get; set; } = 1; + /// + /// Gets the reusable scratch the substitution table uses to merge a stage group's + /// lookups into lookup-index order. Cleared by each group merge; kept on the pooled + /// buffer so application allocates nothing. + /// + internal List<(Tag Feature, ushort Index, Tables.AdvancedTypographic.GSub.LookupTable LookupTable, ulong Mask)> GSubLookupScratch { get; } = new(16); + + /// + /// Gets the reusable scratch the positioning table uses to merge a stage group's + /// lookups into lookup-index order. Cleared by each group merge; kept on the pooled + /// buffer so application allocates nothing. + /// + internal List<(Tag Feature, ushort Index, Tables.AdvancedTypographic.GPos.LookupTable LookupTable, ulong Mask)> GPosLookupScratch { get; } = new(16); + /// /// Gets an interior reference to the glyph shaping data at the specified index. /// The reference writes through to the buffer's storage and is invalidated by any diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs index f9cd76ca7..b9795c6de 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs @@ -228,70 +228,125 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas // Plan positioning features for each glyph. shaper.Plan(buffer, index, count); + + // Stages are applied in pause-delimited groups: a stage action is a + // synchronization point, and between two actions every registered + // feature's lookups apply together in lookup-list order, the order the + // specification defines for lookups within a single application pass. A + // lookup registered by several of the group's features applies once with + // their glyph masks combined. List shapingStages = shaper.GetShapingStages(); SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, default, 0); - foreach (ShapingStage stage in shapingStages) + List<(Tag Feature, ushort Index, LookupTable LookupTable, ulong Mask)> merged = buffer.GPosLookupScratch; + + int stageIndex = 0; + while (stageIndex < shapingStages.Count) { - stage.PreProcessFeature(buffer, index, count); + shapingStages[stageIndex].PreProcessFeature(buffer, index, count); - Tag featureTag = stage.FeatureTag; - var lookupProbe = ShapingProbe.Enter(); - bool found = this.TryGetFeatureLookups(fontMetrics, in featureTag, current, buffer.LanguageTags, out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? lookups); - ShapingProbe.Exit(ShapingProbe.LookupResolve, lookupProbe); - if (found && lookups is not null) + // Extend the group while its interior holds no actions: a post action + // closes the group after its stage and a pre action opens a new one. + int groupEnd = stageIndex; + while (true) { - // Apply features in order. - foreach ((Tag Feature, ushort Index, LookupTable LookupTable) featureLookup in lookups) + groupEnd++; + if (shapingStages[groupEnd - 1].HasPostAction || groupEnd >= shapingStages.Count || shapingStages[groupEnd].HasPreAction) { - Tag feature = featureLookup.Feature; - - // Skip the whole lookup when its coverage cannot intersect any - // glyph id the buffer has ever contained; most fonts carry - // many lookups for glyphs a given text never produces. - if (!featureLookup.LookupTable.Digest.MightIntersect(buffer.GlyphDigest)) - { - continue; - } + break; + } + } - // Resolve the feature's mask bit once per lookup; the per-glyph - // gate below is then a single bitwise AND against the glyph's - // enabled mask. - ulong featureMask = buffer.FeatureMap.GetMask(feature); - LookupTable featureLookupTable = featureLookup.LookupTable; - iterator.Reset(index, featureLookupTable.LookupFlags, featureLookupTable.MarkFilteringSet); - long featureStart = ShapingProbe.Timestamp(); - long featureApplies = 0; + // Merge the group's lookups into lookup-index order. Insertion keeps + // the scratch sorted; a lookup already present from another feature + // gains that feature's mask instead of a second entry. + merged.Clear(); + for (int s = stageIndex; s < groupEnd; s++) + { + Tag featureTag = shapingStages[s].FeatureTag; + var lookupProbe = ShapingProbe.Enter(); + bool found = this.TryGetFeatureLookups(fontMetrics, in featureTag, current, buffer.LanguageTags, out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? lookups); + ShapingProbe.Exit(ShapingProbe.LookupResolve, lookupProbe); + if (!found || lookups is null) + { + continue; + } - while (iterator.Index < index + count) + ulong mask = buffer.FeatureMap.GetMask(featureTag); + foreach ((Tag Feature, ushort Index, LookupTable LookupTable) featureLookup in lookups) + { + int insertAt = merged.Count; + bool alreadyMerged = false; + while (insertAt > 0) { - if (currentOperations++ >= maxOperationsCount) + (Tag Feature, ushort Index, LookupTable LookupTable, ulong Mask) prior = merged[insertAt - 1]; + if (prior.Index == featureLookup.Index) { - maxOperationsReached = true; - goto EndLookups; + merged[insertAt - 1] = (prior.Feature, prior.Index, prior.LookupTable, prior.Mask | mask); + alreadyMerged = true; + break; } - // The digest cheaply rejects glyphs no subtable of this - // lookup can affect; a maybe falls through to the exact - // coverage test inside. - ref GlyphShapingData glyphData = ref buffer[iterator.Index]; - if ((glyphData.FeatureMask & featureMask) == 0 || !featureLookupTable.Digest.MightContain(glyphData.GlyphId)) + if (prior.Index < featureLookup.Index) { - iterator.Next(); - continue; + break; } - bool success = featureLookup.LookupTable.TryUpdatePosition(fontMetrics, this, buffer, featureLookup.Feature, iterator.Index, count - (iterator.Index - index)); - featureApplies++; - kerned |= success && (feature == KernTag || feature == VKernTag); - updated |= success; + insertAt--; + } + + if (!alreadyMerged) + { + merged.Insert(insertAt, (featureLookup.Feature, featureLookup.Index, featureLookup.LookupTable, mask)); + } + } + } + + for (int m = 0; m < merged.Count; m++) + { + (Tag feature, ushort _, LookupTable featureLookupTable, ulong featureMask) = merged[m]; + + // Skip the whole lookup when its coverage cannot intersect any + // glyph id the buffer has ever contained; most fonts carry + // many lookups for glyphs a given text never produces. + if (!featureLookupTable.Digest.MightIntersect(buffer.GlyphDigest)) + { + continue; + } + + iterator.Reset(index, featureLookupTable.LookupFlags, featureLookupTable.MarkFilteringSet); + long featureStart = ShapingProbe.Timestamp(); + long featureApplies = 0; + + while (iterator.Index < index + count) + { + if (currentOperations++ >= maxOperationsCount) + { + maxOperationsReached = true; + goto EndLookups; + } + + // The digest cheaply rejects glyphs no subtable of this + // lookup can affect; a maybe falls through to the exact + // coverage test inside. + ref GlyphShapingData glyphData = ref buffer[iterator.Index]; + if ((glyphData.FeatureMask & featureMask) == 0 || !featureLookupTable.Digest.MightContain(glyphData.GlyphId)) + { iterator.Next(); + continue; } - ShapingProbe.ExitFeature("GPOS", feature, featureStart, featureApplies); + bool success = featureLookupTable.TryUpdatePosition(fontMetrics, this, buffer, feature, iterator.Index, count - (iterator.Index - index)); + featureApplies++; + kerned |= success && (feature == KernTag || feature == VKernTag); + updated |= success; + iterator.Next(); } + + ShapingProbe.ExitFeature("GPOS", feature, featureStart, featureApplies); } - stage.PostProcessFeature(buffer, index, count); + shapingStages[groupEnd - 1].PostProcessFeature(buffer, index, count); + stageIndex = groupEnd; } EndLookups: diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs index b6d95942b..118847888 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs @@ -209,13 +209,22 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas i += delta; count += delta; + // Stages are applied in pause-delimited groups: a stage action is a + // synchronization point, and between two actions every registered + // feature's lookups apply together in lookup-list order, the order the + // specification defines for lookups within a single application pass. A + // lookup registered by several of the group's features applies once with + // their glyph masks combined. List stages = shaper.GetShapingStages(); SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, default, 0); - foreach (ShapingStage stage in stages) + List<(Tag Feature, ushort Index, LookupTable LookupTable, ulong Mask)> merged = buffer.GSubLookupScratch; + + int stageIndex = 0; + while (stageIndex < stages.Count) { collectionCount = buffer.Count; var preProbe = ShapingProbe.Enter(); - stage.PreProcessFeature(buffer, index, count); + stages[stageIndex].PreProcessFeature(buffer, index, count); ShapingProbe.Exit(ShapingProbe.SubStagePrePost, preProbe); // Account for substitutions changing the length of the buffer. @@ -223,15 +232,69 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas count += delta; i += delta; - Tag featureTag = stage.FeatureTag; + // Extend the group while its interior holds no actions: a post action + // closes the group after its stage and a pre action opens a new one. + int groupEnd = stageIndex; + while (true) + { + groupEnd++; + if (stages[groupEnd - 1].HasPostAction || groupEnd >= stages.Count || stages[groupEnd].HasPreAction) + { + break; + } + } + + // Merge the group's lookups into lookup-index order. Insertion keeps + // the scratch sorted; a lookup already present from another feature + // gains that feature's mask instead of a second entry. + merged.Clear(); + for (int s = stageIndex; s < groupEnd; s++) + { + Tag featureTag = stages[s].FeatureTag; + var lookupProbe = ShapingProbe.Enter(); + bool found = this.TryGetFeatureLookups(fontMetrics, in featureTag, current, buffer.LanguageTags, out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? lookups); + ShapingProbe.Exit(ShapingProbe.LookupResolve, lookupProbe); + if (!found || lookups is null) + { + continue; + } + + ulong mask = buffer.FeatureMap.GetMask(featureTag); + foreach ((Tag Feature, ushort Index, LookupTable LookupTable) featureLookup in lookups) + { + int insertAt = merged.Count; + bool alreadyMerged = false; + while (insertAt > 0) + { + (Tag Feature, ushort Index, LookupTable LookupTable, ulong Mask) prior = merged[insertAt - 1]; + if (prior.Index == featureLookup.Index) + { + merged[insertAt - 1] = (prior.Feature, prior.Index, prior.LookupTable, prior.Mask | mask); + alreadyMerged = true; + break; + } + + if (prior.Index < featureLookup.Index) + { + break; + } + + insertAt--; + } + + if (!alreadyMerged) + { + merged.Insert(insertAt, (featureLookup.Feature, featureLookup.Index, featureLookup.LookupTable, mask)); + } + } + } var applyProbe = ShapingProbe.Enter(); - this.ApplyFeature( + this.ApplyMergedLookups( fontMetrics, buffer, ref iterator, - in featureTag, - current, + merged, index, ref count, ref i, @@ -243,25 +306,28 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas collectionCount = buffer.Count; var postProbe = ShapingProbe.Enter(); - stage.PostProcessFeature(buffer, index, count); + stages[groupEnd - 1].PostProcessFeature(buffer, index, count); ShapingProbe.Exit(ShapingProbe.SubStagePrePost, postProbe); // Account for substitutions changing the length of the buffer. delta = buffer.Count - collectionCount; count += delta; i += delta; + + stageIndex = groupEnd; } } } /// - /// Applies a specific feature's lookups to the glyph substitution buffer. + /// Applies a stage group's merged lookups to the glyph substitution buffer in + /// lookup-index order. Each entry's mask combines every group feature that + /// registered the lookup, so the per-glyph gate stays a single bitwise AND. /// /// The font metrics. /// The glyph substitution buffer. /// The skipping glyph iterator. - /// The feature tag to apply. - /// The current script class. + /// The group's lookups, sorted by lookup index. /// The starting index in the buffer. /// The number of glyphs to process (updated by substitutions). /// The outer loop index (updated by substitutions). @@ -269,12 +335,11 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas /// The maximum allowable buffer count. /// The maximum allowable operations count. /// The current operations counter. - internal void ApplyFeature( + private void ApplyMergedLookups( FontMetrics fontMetrics, ShapingBuffer buffer, ref SkippingGlyphIterator iterator, - in Tag featureTag, - ScriptClass current, + List<(Tag Feature, ushort Index, LookupTable LookupTable, ulong Mask)> merged, int index, ref int count, ref int i, @@ -283,81 +348,70 @@ internal void ApplyFeature( int maxOperationsCount, ref int currentOperations) { - var lookupProbe = ShapingProbe.Enter(); - bool found = this.TryGetFeatureLookups(fontMetrics, in featureTag, current, buffer.LanguageTags, out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? lookups); - ShapingProbe.Exit(ShapingProbe.LookupResolve, lookupProbe); - if (found && lookups is not null) + for (int m = 0; m < merged.Count; m++) { - // Apply features in order. - foreach ((Tag Feature, ushort Index, LookupTable LookupTable) featureLookup in lookups) + (Tag feature, ushort _, LookupTable featureLookupTable, ulong featureMask) = merged[m]; + + // Skip the whole lookup when its coverage cannot intersect any glyph id + // the buffer has ever contained; most fonts carry many lookups for + // glyphs a given text never produces. + if (ShapingProbe.Enabled) { - Tag feature = featureLookup.Feature; + ShapingProbe.LookupsConsidered++; + } - // Skip the whole lookup when its coverage cannot intersect any glyph id - // the buffer has ever contained; most fonts carry many lookups for - // glyphs a given text never produces. + if (!featureLookupTable.Digest.MightIntersect(buffer.GlyphDigest)) + { if (ShapingProbe.Enabled) { - ShapingProbe.LookupsConsidered++; + ShapingProbe.LookupsSkippedByDigest++; } - if (!featureLookup.LookupTable.Digest.MightIntersect(buffer.GlyphDigest)) - { - if (ShapingProbe.Enabled) - { - ShapingProbe.LookupsSkippedByDigest++; - } - - continue; - } + continue; + } - // Resolve the feature's mask bit once per lookup; the per-glyph gate - // below is then a single bitwise AND against the glyph's enabled mask. - ulong featureMask = buffer.FeatureMap.GetMask(feature); - LookupTable featureLookupTable = featureLookup.LookupTable; - iterator.Reset(index, featureLookupTable.LookupFlags, featureLookupTable.MarkFilteringSet); - long featureStart = ShapingProbe.Timestamp(); - long featureApplies = 0; + iterator.Reset(index, featureLookupTable.LookupFlags, featureLookupTable.MarkFilteringSet); + long featureStart = ShapingProbe.Timestamp(); + long featureApplies = 0; - while (iterator.Index < index + count) + while (iterator.Index < index + count) + { + if (buffer.Count >= maxCount || currentOperations++ >= maxOperationsCount) { - if (buffer.Count >= maxCount || currentOperations++ >= maxOperationsCount) - { - return; - } - - if (ShapingProbe.Enabled) - { - ShapingProbe.GlyphGateChecks++; - } - - // The digest cheaply rejects glyphs no subtable of this lookup can - // affect; a maybe falls through to the exact coverage test inside. - ref GlyphShapingData glyphData = ref buffer[iterator.Index]; - if ((glyphData.FeatureMask & featureMask) == 0 || !featureLookupTable.Digest.MightContain(glyphData.GlyphId)) - { - iterator.Next(); - continue; - } + return; + } - if (ShapingProbe.Enabled) - { - ShapingProbe.SubstitutionAttempts++; - } + if (ShapingProbe.Enabled) + { + ShapingProbe.GlyphGateChecks++; + } - collectionCount = buffer.Count; - featureLookup.LookupTable.TrySubstitution(fontMetrics, this, buffer, featureLookup.Feature, iterator.Index, count - (iterator.Index - index)); - featureApplies++; + // The digest cheaply rejects glyphs no subtable of this lookup can + // affect; a maybe falls through to the exact coverage test inside. + ref GlyphShapingData glyphData = ref buffer[iterator.Index]; + if ((glyphData.FeatureMask & featureMask) == 0 || !featureLookupTable.Digest.MightContain(glyphData.GlyphId)) + { iterator.Next(); + continue; + } - // Account for substitutions changing the length of the buffer. - int delta = buffer.Count - collectionCount; - count += delta; - i += delta; + if (ShapingProbe.Enabled) + { + ShapingProbe.SubstitutionAttempts++; } - ShapingProbe.ExitFeature("GSUB", feature, featureStart, featureApplies); + collectionCount = buffer.Count; + featureLookupTable.TrySubstitution(fontMetrics, this, buffer, feature, iterator.Index, count - (iterator.Index - index)); + featureApplies++; + iterator.Next(); + + // Account for substitutions changing the length of the buffer. + int delta = buffer.Count - collectionCount; + count += delta; + i += delta; } + + ShapingProbe.ExitFeature("GSUB", feature, featureStart, featureApplies); } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShapingStage.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShapingStage.cs index 433beb194..cf8b6497e 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShapingStage.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShapingStage.cs @@ -36,6 +36,20 @@ public ShapingStage(Tag featureTag, Action? preAction, /// public Tag FeatureTag { get; } + /// + /// Gets a value indicating whether this stage runs an action before its feature. + /// A pre action opens a new application group: every lookup registered by earlier + /// stages must have applied before the action runs. + /// + public bool HasPreAction => this.preAction is not null; + + /// + /// Gets a value indicating whether this stage runs an action after its feature. + /// A post action closes its application group: the group's lookups must all have + /// applied before the action runs. + /// + public bool HasPostAction => this.postAction is not null; + /// /// Invokes the pre-processing action for this shaping stage, if one was provided. /// From d1f8d1b8de5ba98e57a73550ee6e27a32981c879 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 13:45:07 +1000 Subject: [PATCH 22/94] Make the merge scratch lists public like the rest of the buffer surface --- src/SixLabors.Fonts/ShapingBuffer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SixLabors.Fonts/ShapingBuffer.cs b/src/SixLabors.Fonts/ShapingBuffer.cs index 9300e14fc..2fd65e0ef 100644 --- a/src/SixLabors.Fonts/ShapingBuffer.cs +++ b/src/SixLabors.Fonts/ShapingBuffer.cs @@ -113,14 +113,14 @@ public ShapingBuffer(TextOptions textOptions, ShapingFeatureMap featureMap, Shap /// lookups into lookup-index order. Cleared by each group merge; kept on the pooled /// buffer so application allocates nothing. /// - internal List<(Tag Feature, ushort Index, Tables.AdvancedTypographic.GSub.LookupTable LookupTable, ulong Mask)> GSubLookupScratch { get; } = new(16); + public List<(Tag Feature, ushort Index, Tables.AdvancedTypographic.GSub.LookupTable LookupTable, ulong Mask)> GSubLookupScratch { get; } = new(16); /// /// Gets the reusable scratch the positioning table uses to merge a stage group's /// lookups into lookup-index order. Cleared by each group merge; kept on the pooled /// buffer so application allocates nothing. /// - internal List<(Tag Feature, ushort Index, Tables.AdvancedTypographic.GPos.LookupTable LookupTable, ulong Mask)> GPosLookupScratch { get; } = new(16); + public List<(Tag Feature, ushort Index, Tables.AdvancedTypographic.GPos.LookupTable LookupTable, ulong Mask)> GPosLookupScratch { get; } = new(16); /// /// Gets an interior reference to the glyph shaping data at the specified index. From d9701d09db88c4b9f396847c3acf20fd9a1bd33d Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 13:52:25 +1000 Subject: [PATCH 23/94] Make cross-type members of internal types public Swept every file on the branch for members declared internal inside internal types and made them public, matching the codebase convention that an internal type's cross-type surface is uniformly public. Members kept internal are those on public types, where the modifier hides real API surface, and overrides bound to a public base class's internal abstract members. --- src/SixLabors.Fonts/GlyphLayoutData.cs | 2 +- src/SixLabors.Fonts/GlyphShapingData.cs | 6 +++--- src/SixLabors.Fonts/PlaceholderGlyphMetrics.cs | 4 ++-- src/SixLabors.Fonts/ShapingBuffer.cs | 2 +- src/SixLabors.Fonts/ShapingScratch.cs | 2 +- src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs | 4 ++-- src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs | 6 +++--- .../Tables/AdvancedTypographic/Shapers/DefaultShaper.cs | 2 +- .../Tables/AdvancedTypographic/ShapingFeatureMap.cs | 2 +- src/SixLabors.Fonts/Tables/Cff/CffGlyphMetrics.cs | 2 +- src/SixLabors.Fonts/Tables/General/Kern/KerningTable.cs | 2 +- src/SixLabors.Fonts/TextLayout.LineBreaking.cs | 2 +- src/SixLabors.Fonts/TextLayout.cs | 6 +++--- 13 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/SixLabors.Fonts/GlyphLayoutData.cs b/src/SixLabors.Fonts/GlyphLayoutData.cs index 67635063f..bdbd78738 100644 --- a/src/SixLabors.Fonts/GlyphLayoutData.cs +++ b/src/SixLabors.Fonts/GlyphLayoutData.cs @@ -15,7 +15,7 @@ namespace SixLabors.Fonts; [DebuggerDisplay("{DebuggerDisplay,nq}")] internal struct GlyphLayoutData { - internal const int NoHyphenationMarker = -1; + public const int NoHyphenationMarker = -1; /// /// Initializes a new instance of the struct. diff --git a/src/SixLabors.Fonts/GlyphShapingData.cs b/src/SixLabors.Fonts/GlyphShapingData.cs index 4a03a6fb1..8f299e644 100644 --- a/src/SixLabors.Fonts/GlyphShapingData.cs +++ b/src/SixLabors.Fonts/GlyphShapingData.cs @@ -98,13 +98,13 @@ public ushort GlyphId /// /// Gets or sets the cached glyph shaping class, avoiding repeated GDEF lookups. /// - internal GlyphShapingClass CachedShapingClass { get; set; } + public GlyphShapingClass CachedShapingClass { get; set; } /// /// Gets or sets the cache key for . /// A value of -1 indicates the cache is invalid. Valid entries store the glyph id. /// - internal int ShapingClassCacheKey { get; set; } = -1; + public int ShapingClassCacheKey { get; set; } = -1; /// /// Gets or sets the zero-based index within the input codepoint collection of the @@ -228,5 +228,5 @@ public void ClearFeatures() this.FeatureMask = 0; } - internal string ToDebuggerDisplay() => this.DebuggerDisplay; + public string ToDebuggerDisplay() => this.DebuggerDisplay; } diff --git a/src/SixLabors.Fonts/PlaceholderGlyphMetrics.cs b/src/SixLabors.Fonts/PlaceholderGlyphMetrics.cs index 113a74346..b7a536b50 100644 --- a/src/SixLabors.Fonts/PlaceholderGlyphMetrics.cs +++ b/src/SixLabors.Fonts/PlaceholderGlyphMetrics.cs @@ -24,7 +24,7 @@ internal sealed class PlaceholderGlyphMetrics : FontGlyphMetrics /// The point size used for layout. /// The resolution used to convert placeholder pixels into layout units. /// The text run this placeholder belongs to. - internal PlaceholderGlyphMetrics( + public PlaceholderGlyphMetrics( StreamFontMetrics font, TextPlaceholder placeholder, float pointSize, @@ -58,7 +58,7 @@ internal PlaceholderGlyphMetrics( /// The placeholder text run. /// The resolution used to convert placeholder pixels into layout units. /// The placeholder metrics. - internal static PlaceholderGlyphMetrics Create(Font font, TextRun textRun, float dpi) + public static PlaceholderGlyphMetrics Create(Font font, TextRun textRun, float dpi) { FontMetrics fontMetrics = font.FontMetrics; StreamFontMetrics streamFontMetrics = fontMetrics is FileFontMetrics fileFontMetrics diff --git a/src/SixLabors.Fonts/ShapingBuffer.cs b/src/SixLabors.Fonts/ShapingBuffer.cs index 2fd65e0ef..33d1194a0 100644 --- a/src/SixLabors.Fonts/ShapingBuffer.cs +++ b/src/SixLabors.Fonts/ShapingBuffer.cs @@ -825,7 +825,7 @@ public bool ShouldProcess(FontMetrics fontMetrics, int index) /// substitution and read after the seed into the positioning phase. /// /// The combined mask, or zero when no vertical feature was registered. - internal ulong GetVerticalFeatureMask() + public ulong GetVerticalFeatureMask() => this.FeatureMap.GetMask(KnownFeatureTags.VerticalAlternates) | this.FeatureMap.GetMask(KnownFeatureTags.VerticalAlternatesForRotation) | this.FeatureMap.GetMask(KnownFeatureTags.VerticalKerning); diff --git a/src/SixLabors.Fonts/ShapingScratch.cs b/src/SixLabors.Fonts/ShapingScratch.cs index 17cbc22ee..50536eb50 100644 --- a/src/SixLabors.Fonts/ShapingScratch.cs +++ b/src/SixLabors.Fonts/ShapingScratch.cs @@ -39,7 +39,7 @@ internal sealed class ShapingScratch /// /// The text options for the pass. /// The reusable buffers, sharing one feature map. - internal (ShapingBuffer Workspace, ShapingBuffer Result) Prepare(TextOptions options) + public (ShapingBuffer Workspace, ShapingBuffer Result) Prepare(TextOptions options) { if (this.featureMap is null) { diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs index b9795c6de..37ec90c8d 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs @@ -41,7 +41,7 @@ internal class GPosTable : Table /// /// The OpenType table tag for the GPOS table. /// - internal const string TableName = "GPOS"; + public const string TableName = "GPOS"; /// /// Initializes a new instance of the class. @@ -101,7 +101,7 @@ public GPosTable(ScriptList? scriptList, FeatureListTable featureList, LookupLis /// /// The big endian binary reader. /// The . - internal static GPosTable Load(BigEndianBinaryReader reader) + public static GPosTable Load(BigEndianBinaryReader reader) { // GPOS Header, Version 1.0 // +----------+-------------------+-----------------------------------------------------------+ diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs index 118847888..bd03712f0 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs @@ -26,7 +26,7 @@ internal class GSubTable : Table /// /// The OpenType table tag for the GSUB table. /// - internal const string TableName = "GSUB"; + public const string TableName = "GSUB"; /// /// The invalid but widely shipped language system record tag 'dflt'. @@ -91,7 +91,7 @@ public GSubTable(ScriptList? scriptList, FeatureListTable featureList, LookupLis /// /// The big endian binary reader. /// The . - internal static GSubTable Load(BigEndianBinaryReader reader) + public static GSubTable Load(BigEndianBinaryReader reader) { // GSUB Header, Version 1.0 // +----------+-------------------+-----------------------------------------------------------+ @@ -427,7 +427,7 @@ private void ApplyMergedLookups( /// /// When this method returns, contains the list of feature lookups if found. /// if lookups were found; otherwise, . - internal bool TryGetFeatureLookups( + public bool TryGetFeatureLookups( FontMetrics fontMetrics, in Tag stageFeature, ScriptClass script, diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs index b9744051e..4bdc45f09 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs @@ -98,7 +98,7 @@ internal class DefaultShaper : BaseShaper /// /// The script classification. /// The text options. - internal DefaultShaper(ScriptClass script, TextOptions textOptions) + public DefaultShaper(ScriptClass script, TextOptions textOptions) : this(script, MarkZeroingMode.PostGpos, textOptions) { } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapingFeatureMap.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapingFeatureMap.cs index a1254b9d0..882feb1d0 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapingFeatureMap.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapingFeatureMap.cs @@ -67,7 +67,7 @@ internal sealed class ShapingFeatureMap /// Resets the map for reuse by a new shaping pass, emptying the tag registry and /// the single-entry memo. /// - internal void Reset() + public void Reset() { this.tags.Clear(); this.lastTagValue = 0; diff --git a/src/SixLabors.Fonts/Tables/Cff/CffGlyphMetrics.cs b/src/SixLabors.Fonts/Tables/Cff/CffGlyphMetrics.cs index 1cd2bb4c7..0107f9bfb 100644 --- a/src/SixLabors.Fonts/Tables/Cff/CffGlyphMetrics.cs +++ b/src/SixLabors.Fonts/Tables/Cff/CffGlyphMetrics.cs @@ -30,7 +30,7 @@ internal class CffGlyphMetrics : FontGlyphMetrics /// The text attributes. /// The text decorations. /// The glyph type. - internal CffGlyphMetrics( + public CffGlyphMetrics( StreamFontMetrics fontMetrics, ushort glyphId, CodePoint codePoint, diff --git a/src/SixLabors.Fonts/Tables/General/Kern/KerningTable.cs b/src/SixLabors.Fonts/Tables/General/Kern/KerningTable.cs index 7744b135c..894b4b708 100644 --- a/src/SixLabors.Fonts/Tables/General/Kern/KerningTable.cs +++ b/src/SixLabors.Fonts/Tables/General/Kern/KerningTable.cs @@ -15,7 +15,7 @@ internal sealed class KerningTable : Table /// /// The table tag name identifying the 'kern' table. /// - internal const string TableName = "kern"; + public const string TableName = "kern"; /// /// The array of kerning subtables contained in this table. diff --git a/src/SixLabors.Fonts/TextLayout.LineBreaking.cs b/src/SixLabors.Fonts/TextLayout.LineBreaking.cs index a8a5761a5..0df35991e 100644 --- a/src/SixLabors.Fonts/TextLayout.LineBreaking.cs +++ b/src/SixLabors.Fonts/TextLayout.LineBreaking.cs @@ -584,7 +584,7 @@ private static List CollectLineBreaks(ReadOnlySpan text, bool i /// The font used to shape and render the marker. /// The text options used for layout. /// The generated marker entry. - internal static GlyphLayoutData CreateGeneratedMarker( + public static GlyphLayoutData CreateGeneratedMarker( PositionedGlyphMetrics anchor, float pointSize, BidiRun bidiRun, diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index 2980d0afb..5a4aec79b 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -255,7 +255,7 @@ private static float GetBaselineShift(float baselineOffset, float dpi, bool isVe /// Use to disable culling at this edge. /// /// The visitor that receives each positioned glyph. - internal static void LayoutText( + public static void LayoutText( TextBox textBox, TextOptions options, float wrappingLength, @@ -1320,7 +1320,7 @@ private static void LayoutLineVerticalMixed( /// Per-line alignment within the block. /// The resolved text direction for this line. /// The X offset to add to the line's pen location. - internal static float CalculateLineOffsetX( + public static float CalculateLineOffsetX( float lineAdvance, float maxScaledAdvance, HorizontalAlignment horizontalAlignment, @@ -1384,7 +1384,7 @@ internal static float CalculateLineOffsetX( /// Per-line alignment within the block. /// The resolved text direction for this line. /// The Y offset to add to the line's pen location. - internal static float CalculateLineOffsetY( + public static float CalculateLineOffsetY( float lineAdvance, float maxScaledAdvance, VerticalAlignment verticalAlignment, From 364535f6eed9dd69c69d3ca897bc75670eca4473 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 13:58:35 +1000 Subject: [PATCH 24/94] Front the metrics seeding with a direct-mapped cache on the buffer Seeding the positioning buffer resolved every glyph's metrics through the font's concurrent dictionary cache, paying a hash and probe per glyph per shape. The buffer now fronts that resolver with a 256-slot direct-mapped cache whose tag packs the same key fields, so a repeat lookup costs one load and one compare. The cache needs no synchronization because a pooled buffer is exclusively owned for the duration of a shaping pass; seeding from a different font clears it. Gate: 5,577/0 tests including exact HarfBuzz differential on Devanagari. | Scenario | Mean before | Mean after | |------------|-------------|------------| | Latin | 11.66 us | 11.35 us | | Arabic | 13.26 us | 12.93 us | | Devanagari | 19.93 us | 19.74 us | All rows clean (Fonts StdDev at or below 0.13 us); allocations byte-identical in all scenarios. --- src/SixLabors.Fonts/ShapingBuffer.cs | 71 +++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/src/SixLabors.Fonts/ShapingBuffer.cs b/src/SixLabors.Fonts/ShapingBuffer.cs index 33d1194a0..320ee078c 100644 --- a/src/SixLabors.Fonts/ShapingBuffer.cs +++ b/src/SixLabors.Fonts/ShapingBuffer.cs @@ -50,6 +50,24 @@ internal sealed class ShapingBuffer /// private GlyphSetDigest glyphDigest; + /// + /// Validation tags for the direct-mapped glyph metrics cache. A slot's tag packs + /// every key field of the font's own metrics cache above a marker bit, so a hit is + /// one load and one compare. Zero marks a slot empty. + /// + private readonly ulong[] metricsCacheTags = new ulong[256]; + + /// + /// The resolved metrics for each slot of . + /// + private readonly FontGlyphMetrics?[] metricsCacheValues = new FontGlyphMetrics?[256]; + + /// + /// The font metrics instance the cache entries belong to. Seeding from a different + /// font clears the cache before use. + /// + private FontMetrics? metricsCacheOwner; + /// /// Initializes a new instance of the class. /// @@ -664,7 +682,7 @@ public bool TryAdd(Font font, ShapingBuffer workspace) bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(codePoint, layoutMode) || (source.AppliedFeatureMask & verticalMask) != 0; - FontGlyphMetrics glyphMetrics = fontMetrics.GetGlyphMetrics(codePoint, id, textAttributes, textDecorations, layoutMode, colorFontSupport); + FontGlyphMetrics glyphMetrics = this.GetGlyphMetricsCached(fontMetrics, codePoint, id, textAttributes, textDecorations, layoutMode, colorFontSupport); if (glyphMetrics.GlyphType == GlyphType.Fallback && !CodePoint.IsControl(codePoint)) { @@ -732,7 +750,7 @@ public bool TryUpdate(Font font, ShapingBuffer workspace) bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(codePoint, layoutMode) || (shape.AppliedFeatureMask & verticalMask) != 0; - FontGlyphMetrics glyphMetrics = fontMetrics.GetGlyphMetrics(codePoint, id, textAttributes, textDecorations, layoutMode, colorFontSupport); + FontGlyphMetrics glyphMetrics = this.GetGlyphMetricsCached(fontMetrics, codePoint, id, textAttributes, textDecorations, layoutMode, colorFontSupport); // If the glyphs are fallbacks we don't want them as // we've already captured them on the first run. @@ -772,6 +790,55 @@ public bool TryUpdate(Font font, ShapingBuffer workspace) return !hasFallBacks; } + /// + /// Resolves glyph metrics through a direct-mapped cache in front of the font's own + /// resolver. The tag packs the same key fields the font's cache hashes, so a hit + /// replaces a dictionary probe with one load and one compare. No synchronization is + /// needed: a pooled buffer is exclusively owned for the duration of a shaping pass. + /// + /// The font metrics to resolve against. + /// The code point represented by the glyph. + /// The glyph id. + /// The text attributes applied to the glyph. + /// The text decorations applied to the glyph. + /// The layout mode. + /// The color font support level. + /// The resolved . + private FontGlyphMetrics GetGlyphMetricsCached( + FontMetrics fontMetrics, + CodePoint codePoint, + ushort glyphId, + TextAttributes textAttributes, + TextDecorations textDecorations, + LayoutMode layoutMode, + ColorFontSupport colorFontSupport) + { + if (!ReferenceEquals(this.metricsCacheOwner, fontMetrics)) + { + Array.Clear(this.metricsCacheTags); + this.metricsCacheOwner = fontMetrics; + } + + bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(codePoint, layoutMode); + ulong tag = (1UL << 63) + | (uint)codePoint.Value + | ((ulong)glyphId << 21) + | ((ulong)(uint)textAttributes << 37) + | ((ulong)(uint)colorFontSupport << 45) + | ((isVertical ? 1UL : 0UL) << 49); + + int slot = glyphId & 0xFF; + if (this.metricsCacheTags[slot] == tag) + { + return this.metricsCacheValues[slot]!; + } + + FontGlyphMetrics glyphMetrics = fontMetrics.GetGlyphMetrics(codePoint, glyphId, textAttributes, textDecorations, layoutMode, colorFontSupport); + this.metricsCacheTags[slot] = tag; + this.metricsCacheValues[slot] = glyphMetrics; + return glyphMetrics; + } + /// /// Marks the glyph at the specified index as positioned. Positions accumulate in /// the record's shaping bounds and are read from there by consumers, so the shared From d63f9677357dc4c1fc74d49e7de830f289ac09b2 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 14:13:47 +1000 Subject: [PATCH 25/94] Move placeholder bidi runs off the glyph record Every glyph record carried a sixteen byte bidi run that only inline placeholders ever set and only the copy-out ever read. The run now lives in a small list on the buffer keyed by codepoint offset, which is stable for placeholders because they shape in isolated single-glyph runs. This shrinks the record every walk, copy, and seed touches by sixteen bytes. Gate: 5,577/0 tests including exact HarfBuzz differential on Devanagari. | Scenario | Mean before | Mean after | Ratio | |------------|-------------|------------|---------------| | Latin | 11.35 us | 11.03 us | 4.94x > 4.74x | | Arabic | 12.93 us | 13.19 us | 3.40x > 3.33x | | Devanagari | 19.74 us | 19.69 us | ~2.1x > 2.16x | All rows clean; the Arabic mean moved with an elevated HarfBuzz control while its ratio improved. Allocations byte-identical in all scenarios. --- src/SixLabors.Fonts/GlyphShapingData.cs | 7 +--- src/SixLabors.Fonts/ShapingBuffer.cs | 44 +++++++++++++++++++++- src/SixLabors.Fonts/TextShaper.Pipeline.cs | 9 +++-- 3 files changed, 50 insertions(+), 10 deletions(-) diff --git a/src/SixLabors.Fonts/GlyphShapingData.cs b/src/SixLabors.Fonts/GlyphShapingData.cs index 8f299e644..d1dbb0b6d 100644 --- a/src/SixLabors.Fonts/GlyphShapingData.cs +++ b/src/SixLabors.Fonts/GlyphShapingData.cs @@ -60,7 +60,6 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) this.IsSubstituted = data.IsSubstituted; this.IsDecomposed = data.IsDecomposed; this.IsPlaceholder = data.IsPlaceholder; - this.BidiRun = data.BidiRun; this.IsPositioned = data.IsPositioned; this.IsKerned = data.IsKerned; @@ -193,14 +192,10 @@ public ushort GlyphId /// /// Gets or sets a value indicating whether this glyph represents an inline placeholder. + /// A placeholder's bidi run lives on the buffer, keyed by codepoint offset. /// public bool IsPlaceholder { get; set; } - /// - /// Gets or sets the bidi run assigned to an inline placeholder. - /// - public BidiRun BidiRun { get; set; } - /// /// Gets or sets a value indicating whether this glyph has been positioned. /// diff --git a/src/SixLabors.Fonts/ShapingBuffer.cs b/src/SixLabors.Fonts/ShapingBuffer.cs index 320ee078c..c3193096a 100644 --- a/src/SixLabors.Fonts/ShapingBuffer.cs +++ b/src/SixLabors.Fonts/ShapingBuffer.cs @@ -68,6 +68,13 @@ internal sealed class ShapingBuffer /// private FontMetrics? metricsCacheOwner; + /// + /// The bidi runs recorded for inline placeholders, keyed by codepoint offset. + /// Placeholder state lives here rather than on every glyph record because only + /// placeholders carry a bidi run of their own, and only the copy-out reads it. + /// + private readonly List<(int CodePointIndex, BidiRun Run)> placeholderBidiRuns = new(); + /// /// Initializes a new instance of the class. /// @@ -174,6 +181,7 @@ public void Reset(TextOptions textOptions) this.count = 0; this.LigatureId = 1; this.glyphDigest = default; + this.placeholderBidiRuns.Clear(); this.TextOptions = textOptions; this.LanguageTags = ResolveLanguageTags(textOptions); } @@ -186,6 +194,7 @@ public void Clear() { this.count = 0; this.LigatureId = 1; + this.placeholderBidiRuns.Clear(); } /// @@ -325,10 +334,42 @@ public void AddPlaceholder(CodePoint codePoint, BidiRun bidiRun, TextRun textRun Direction = (TextDirection)bidiRun.Direction, GlyphId = 0, IsPlaceholder = true, - BidiRun = bidiRun, }; + + this.placeholderBidiRuns.Add((offset, bidiRun)); } + /// + /// Gets the bidi run recorded for the placeholder at the given codepoint offset. + /// Placeholders shape in isolated single-glyph runs, so their offsets are stable + /// for the lifetime of the pass. + /// + /// The placeholder's zero-based codepoint offset. + /// The recorded , or the default when none was recorded. + public BidiRun GetPlaceholderBidiRun(int codePointIndex) + { + List<(int CodePointIndex, BidiRun Run)> runs = this.placeholderBidiRuns; + for (int i = 0; i < runs.Count; i++) + { + if (runs[i].CodePointIndex == codePointIndex) + { + return runs[i].Run; + } + } + + return default; + } + + /// + /// Copies the placeholder bidi run recorded at the given codepoint offset in + /// into this buffer, so seeding from a workspace + /// preserves placeholder bidi state without carrying it on every glyph record. + /// + /// The buffer to copy the recorded run from. + /// The placeholder's zero-based codepoint offset. + public void CopyPlaceholderBidiRun(ShapingBuffer source, int codePointIndex) + => this.placeholderBidiRuns.Add((codePointIndex, source.GetPlaceholderBidiRun(codePointIndex))); + /// /// Moves the specified glyph to the specified position. Codepoint offsets stay /// bound to their slots: only the shaping state travels. @@ -661,6 +702,7 @@ public bool TryAdd(Font font, ShapingBuffer workspace) { // Placeholders are synthetic glyphs: they need layout metrics but must not // go through font glyph lookup, fallback resolution, or GPOS positioning. + this.CopyPlaceholderBidiRun(workspace, source.CodePointIndex); FontGlyphMetrics placeholderMetrics = PlaceholderGlyphMetrics.Create(font, source.TextRun, this.TextOptions.Dpi); this.glyphDigest.Add(placeholderMetrics.GlyphId); diff --git a/src/SixLabors.Fonts/TextShaper.Pipeline.cs b/src/SixLabors.Fonts/TextShaper.Pipeline.cs index 421a30cdc..f937b3373 100644 --- a/src/SixLabors.Fonts/TextShaper.Pipeline.cs +++ b/src/SixLabors.Fonts/TextShaper.Pipeline.cs @@ -354,14 +354,17 @@ or BidiCharacterType.FirstStrongIsolate ref ShapingBuffer.GlyphMetricsEntry entry = ref positionings.MetricsAt(i); // Placeholders carry a bidi run of their own, so they always cut a run. + BidiRun shapingBidiRun = shaping.IsPlaceholder + ? positionings.GetPlaceholderBidiRun(shaping.CodePointIndex) + : default; if (entry.Font != runFont || shaping.TextRun != runTextRun - || (shaping.IsPlaceholder && !shaping.BidiRun.Equals(runBidiRun))) + || (shaping.IsPlaceholder && !shapingBidiRun.Equals(runBidiRun))) { runFont = entry.Font; runTextRun = shaping.TextRun; - runBidiRun = shaping.BidiRun; - runs.Add(new(entry.Font, entry.PointSize, shaping.TextRun, shaping.BidiRun)); + runBidiRun = shapingBidiRun; + runs.Add(new(entry.Font, entry.PointSize, shaping.TextRun, shapingBidiRun)); } ShapedGlyphFlags flags = ShapedGlyphFlags.None; From 06fa7c12f1d7919d6f712865f0805b2c890c91bc Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 14:37:02 +1000 Subject: [PATCH 26/94] Pack the syllable classification into a single word SyllableInfo carried five ints. The syllable positional classes are now ordinals whose order is the visual order, with zero reserved as the unassigned sentinel, so the classification packs into byte lanes of one word: number, type, category, position, and universal engine category. The trie already stored positions zero-based; the converters map them onto the ordinal enum directly instead of shifting them into flags. The final reorder placed the reph for after-subjoined scripts with a bitmask over three positional flags, the only flag-style use in the repository; it now tests the three members explicitly, preserving the exact prior semantics under both numberings. Gate: 5,577/0 tests including exact HarfBuzz differential on Devanagari and the Bengali reph cases. Glyph record shrinks from ~120 to ~108 bytes. | Scenario | Mean before | Mean after | |------------|-------------|------------| | Latin | 11.03 us | 11.08 us | | Arabic | 13.19 us | 13.42 us | | Devanagari | 19.69 us | 19.60 us | All rows clean and inside the cross-run spread of identical code; allocations byte-identical in all scenarios. --- .../Shapers/IndicShaper.cs | 11 ++-- .../Shapers/MyanmarShaper.cs | 8 ++- .../AdvancedTypographic/SyllableInfo.cs | 58 +++++++++++++------ .../Unicode/Resources/IndicShapingData.cs | 37 ++++++------ 4 files changed, 72 insertions(+), 42 deletions(-) diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs index 601e416cf..018f9efa0 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs @@ -305,12 +305,14 @@ private static int IndicShapingCategory(CodePoint codePoint) => UnicodeData.GetIndicShapingProperties((uint)codePoint.Value) >> 8; /// - /// Gets the Indic shaping position for a code point (lower 8 bits as a bit flag). + /// Gets the Indic shaping position for a code point. The trie stores the position + /// zero-based; adding one maps it onto the ordinal enum whose zero is the + /// unassigned sentinel. /// /// The code point. - /// The shaping position as a bit flag. + /// The shaping position ordinal. private static int IndicShapingPosition(CodePoint codePoint) - => 1 << (UnicodeData.GetIndicShapingProperties((uint)codePoint.Value) & 0xFF); + => (UnicodeData.GetIndicShapingProperties((uint)codePoint.Value) & 0xFF) + 1; /// /// Performs the initial reordering pass for Indic syllables, including base consonant @@ -1272,7 +1274,8 @@ private void FinalReorder(ShapingBuffer buffer, int index, int count) if (!found && rephPos == Positions.After_Sub) { newRephPos = basePosition; - while (newRephPos + 1 < end && (buffer[newRephPos + 1].Syllable.IndicPosition & (Positions.Post_C | Positions.After_Post | Positions.SMVD)) == 0) + while (newRephPos + 1 < end + && buffer[newRephPos + 1].Syllable.IndicPosition is not Positions.Post_C and not Positions.After_Post and not Positions.SMVD) { newRephPos++; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs index bd85f872d..a784edb6f 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs @@ -459,12 +459,14 @@ private static int IndicShapingCategory(CodePoint codePoint) => UnicodeData.GetIndicShapingProperties((uint)codePoint.Value) >> 8; /// - /// Gets the Indic shaping position for a code point (lower 8 bits as a bit flag). + /// Gets the Indic shaping position for a code point. The trie stores the position + /// zero-based; adding one maps it onto the ordinal enum whose zero is the + /// unassigned sentinel. /// /// The code point. - /// The shaping position as a bit flag. + /// The shaping position ordinal. private static int IndicShapingPosition(CodePoint codePoint) - => 1 << (UnicodeData.GetIndicShapingProperties((uint)codePoint.Value) & 0xFF); + => (UnicodeData.GetIndicShapingProperties((uint)codePoint.Value) & 0xFF) + 1; /// /// Builds a lookup table mapping Myanmar shaping category codes to compact DFA symbol indices. diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/SyllableInfo.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/SyllableInfo.cs index 84063c3fc..aceea04b3 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/SyllableInfo.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/SyllableInfo.cs @@ -7,40 +7,65 @@ namespace SixLabors.Fonts.Tables.AdvancedTypographic; /// /// Per-glyph syllable classification assigned by the Indic, Myanmar, and Universal -/// Shaping Engine shapers, stored by value on the glyph record so classification -/// allocates nothing and every comparison is an integer compare. -/// of means no classification has -/// been assigned; consumers treat such glyphs as outside every syllable. +/// Shaping Engine shapers, packed into a single word of byte lanes so classification +/// allocates nothing, every comparison is an integer compare, and the glyph record +/// stays narrow. of means no +/// classification has been assigned; consumers treat such glyphs as outside every +/// syllable. /// -#pragma warning disable SA1401 // Fields exposed for in-place mutation through the glyph record. internal struct SyllableInfo { + private ulong bits; + /// - /// The running syllable number within the shaping pass. + /// Gets or sets the running syllable number within the shaping pass. Stored as a + /// byte: consumers only compare the numbers of nearby glyphs for equality, and + /// adjacent syllables always differ by one, so wrap-around cannot alias them. /// - public int Number; + public int Number + { + readonly get => (byte)this.bits; + set => this.bits = (this.bits & ~0xFFUL) | (byte)value; + } /// - /// The syllable cluster type produced by the state machine, or + /// Gets or sets the syllable cluster type produced by the state machine, or /// when unassigned. /// - public SyllableType Type; + public SyllableType Type + { + readonly get => (SyllableType)(byte)(this.bits >> 8); + set => this.bits = (this.bits & ~(0xFFUL << 8)) | ((ulong)(byte)value << 8); + } /// - /// The Indic or Myanmar shaping category. + /// Gets or sets the Indic or Myanmar shaping category. /// - public Categories IndicCategory; + public Categories IndicCategory + { + readonly get => (Categories)(byte)(this.bits >> 16); + set => this.bits = (this.bits & ~(0xFFUL << 16)) | ((ulong)(byte)value << 16); + } /// - /// The Indic or Myanmar positional class. + /// Gets or sets the Indic or Myanmar positional class. /// - public Positions IndicPosition; + public Positions IndicPosition + { + readonly get => (Positions)(byte)(this.bits >> 24); + set => this.bits = (this.bits & ~(0xFFUL << 24)) | ((ulong)(byte)value << 24); + } /// - /// The Universal Shaping Engine category as the symbol index the state machine - /// consumes, which is also the index into the generated category name table. + /// Gets or sets the Universal Shaping Engine category as the symbol index the + /// state machine consumes, which is also the index into the generated category + /// name table. /// - public int UseCategory; + public int UseCategory + { + readonly get => (byte)(this.bits >> 32); + set => this.bits = (this.bits & ~(0xFFUL << 32)) | ((ulong)(byte)value << 32); + } /// /// Gets the Myanmar view of : the Myanmar shaper shares @@ -48,4 +73,3 @@ internal struct SyllableInfo /// public readonly MyanmarCategories MyanmarCategory => (MyanmarCategories)this.IndicCategory; } -#pragma warning restore SA1401 diff --git a/src/SixLabors.Fonts/Unicode/Resources/IndicShapingData.cs b/src/SixLabors.Fonts/Unicode/Resources/IndicShapingData.cs index 5287b8b47..353aba174 100644 --- a/src/SixLabors.Fonts/Unicode/Resources/IndicShapingData.cs +++ b/src/SixLabors.Fonts/Unicode/Resources/IndicShapingData.cs @@ -151,26 +151,27 @@ public enum MyanmarSyllableType NonMyanmar_Cluster = 1 << 2 } - // Visual positions in a syllable from left to right. - [Flags] + // Visual positions in a syllable from left to right. Ordinal values whose order + // is the visual order; zero is reserved as the unassigned sentinel so a default + // syllable record compares unequal to every real position. public enum Positions { - Start = 1 << 0, - Ra_To_Become_Reph = 1 << 1, - Pre_M = 1 << 2, - Pre_C = 1 << 3, - Base_C = 1 << 4, - After_Main = 1 << 5, - Above_C = 1 << 6, - Before_Sub = 1 << 7, - Below_C = 1 << 8, - After_Sub = 1 << 9, - Before_Post = 1 << 10, - Post_C = 1 << 11, - After_Post = 1 << 12, - Final_C = 1 << 13, - SMVD = 1 << 14, - End = 1 << 15 + Start = 1, + Ra_To_Become_Reph = 2, + Pre_M = 3, + Pre_C = 4, + Base_C = 5, + After_Main = 6, + Above_C = 7, + Before_Sub = 8, + Below_C = 9, + After_Sub = 10, + Before_Post = 11, + Post_C = 12, + After_Post = 13, + Final_C = 14, + SMVD = 15, + End = 16 } public enum BasePosition From 40bdf3e8036bf0bbcb37b9dc3b017c23da552887 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 14:44:02 +1000 Subject: [PATCH 27/94] Pack the boolean shaping state into one flags word Six bool auto-properties become single bits of a packed flags field, narrowing the glyph record; the property surface is unchanged. Gate: 5,577/0 tests including exact HarfBuzz differential on Devanagari. | Scenario | Mean before | Mean after | |------------|-------------|------------| | Latin | 11.08 us | 10.89 us | | Arabic | 13.42 us | 13.35 us | | Devanagari | 19.60 us | 19.63 us | All rows clean; allocations byte-identical in all scenarios. --- src/SixLabors.Fonts/GlyphShapingData.cs | 42 +++++++++++++++++++++---- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/src/SixLabors.Fonts/GlyphShapingData.cs b/src/SixLabors.Fonts/GlyphShapingData.cs index d1dbb0b6d..d1284e5d3 100644 --- a/src/SixLabors.Fonts/GlyphShapingData.cs +++ b/src/SixLabors.Fonts/GlyphShapingData.cs @@ -33,6 +33,12 @@ internal struct GlyphShapingData private ushort glyphId; + /// + /// Packed boolean shaping state. Single bits keep the record narrow; the bool + /// properties are the only readers and writers. + /// + private ushort flags; + /// /// Initializes a new instance of the struct. /// @@ -139,7 +145,11 @@ public ushort GlyphId /// /// Gets or sets a value indicating whether the glyph is ligated. /// - public bool IsLigated { get; set; } + public bool IsLigated + { + readonly get => (this.flags & 1) != 0; + set => this.flags = value ? (ushort)(this.flags | 1) : (ushort)(this.flags & ~1); + } /// /// Gets or sets the ligature component index of the glyph. @@ -183,28 +193,48 @@ public ushort GlyphId /// /// Gets or sets a value indicating whether this glyph is the result of a substitution. /// - public bool IsSubstituted { get; set; } + public bool IsSubstituted + { + readonly get => (this.flags & (1 << 1)) != 0; + set => this.flags = value ? (ushort)(this.flags | (1 << 1)) : (ushort)(this.flags & ~(1 << 1)); + } /// /// Gets or sets a value indicating whether this glyph is the result of a decomposition substitution /// - public bool IsDecomposed { get; set; } + public bool IsDecomposed + { + readonly get => (this.flags & (1 << 2)) != 0; + set => this.flags = value ? (ushort)(this.flags | (1 << 2)) : (ushort)(this.flags & ~(1 << 2)); + } /// /// Gets or sets a value indicating whether this glyph represents an inline placeholder. /// A placeholder's bidi run lives on the buffer, keyed by codepoint offset. /// - public bool IsPlaceholder { get; set; } + public bool IsPlaceholder + { + readonly get => (this.flags & (1 << 3)) != 0; + set => this.flags = value ? (ushort)(this.flags | (1 << 3)) : (ushort)(this.flags & ~(1 << 3)); + } /// /// Gets or sets a value indicating whether this glyph has been positioned. /// - public bool IsPositioned { get; set; } + public bool IsPositioned + { + readonly get => (this.flags & (1 << 4)) != 0; + set => this.flags = value ? (ushort)(this.flags | (1 << 4)) : (ushort)(this.flags & ~(1 << 4)); + } /// /// Gets or sets a value indicating whether this glyph has been kerned. /// - public bool IsKerned { get; set; } + public bool IsKerned + { + readonly get => (this.flags & (1 << 5)) != 0; + set => this.flags = value ? (ushort)(this.flags | (1 << 5)) : (ushort)(this.flags & ~(1 << 5)); + } private string DebuggerDisplay => FormattableString From 5f61a32880c8e8baf7418f2b235f4de0ec8f8d6a Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 14:48:21 +1000 Subject: [PATCH 28/94] Narrow the direction and shaping-class cache storage The text direction stores as a byte and the shaping-class cache key becomes the cached glyph id plus a validity flag bit, with the property surfaces unchanged. The flag encoding also corrects a latent defect: a default record previously carried cache key zero, which read as a valid cached class for glyph id zero, while field initializers only run when a constructor does. A default record now reports an invalid cache. Gate: 5,577/0 tests including exact HarfBuzz differential on Devanagari. | Scenario | Mean before | Mean after | |------------|-------------|------------| | Latin | 10.89 us | 10.96 us | | Arabic | 13.35 us | 13.38 us | | Devanagari | 19.63 us | 19.19 us | All rows clean; allocations byte-identical in all scenarios. --- src/SixLabors.Fonts/GlyphShapingData.cs | 34 +++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/SixLabors.Fonts/GlyphShapingData.cs b/src/SixLabors.Fonts/GlyphShapingData.cs index d1284e5d3..abb38f9cf 100644 --- a/src/SixLabors.Fonts/GlyphShapingData.cs +++ b/src/SixLabors.Fonts/GlyphShapingData.cs @@ -39,6 +39,17 @@ internal struct GlyphShapingData /// private ushort flags; + /// + /// The glyph id the cached shaping class was computed for; meaningful only while + /// the cache-valid flag bit is set. + /// + private ushort shapingClassCacheId; + + /// + /// The text direction, stored as a byte to keep the record narrow. + /// + private byte direction; + /// /// Initializes a new instance of the struct. /// @@ -109,7 +120,22 @@ public ushort GlyphId /// Gets or sets the cache key for . /// A value of -1 indicates the cache is invalid. Valid entries store the glyph id. /// - public int ShapingClassCacheKey { get; set; } = -1; + public int ShapingClassCacheKey + { + readonly get => (this.flags & (1 << 6)) != 0 ? this.shapingClassCacheId : -1; + set + { + if (value < 0) + { + this.flags = (ushort)(this.flags & ~(1 << 6)); + } + else + { + this.shapingClassCacheId = (ushort)value; + this.flags = (ushort)(this.flags | (1 << 6)); + } + } + } /// /// Gets or sets the zero-based index within the input codepoint collection of the @@ -130,7 +156,11 @@ public ushort GlyphId /// /// Gets or sets the text direction. /// - public TextDirection Direction { get; set; } + public TextDirection Direction + { + readonly get => (TextDirection)this.direction; + set => this.direction = (byte)value; + } /// /// Gets or sets the text run this glyph belongs to. From 26875d7d4fe6982ed076b339a4a3856aa4716474 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 15:07:31 +1000 Subject: [PATCH 29/94] Name the packed record bits and expand summary docs to multiple lines The packed flags and byte lanes introduced by the record shrink used raw bit numbers; every bit and lane now has a named constant so the property bodies read as intent. All single-line summary documentation across the branch's files is expanded to the multi-line form. Gate: 5,577/0 tests; no functional change. --- src/SixLabors.Fonts/GlyphLayoutData.cs | 84 +++++++++---- src/SixLabors.Fonts/GlyphShapingData.cs | 72 ++++++++--- .../Shapers/ArabicShaper.cs | 64 +++++++--- .../Shapers/DefaultShaper.cs | 100 ++++++++++++---- .../Shapers/HangulShaper.cs | 108 ++++++++++++----- .../Shapers/HebrewShaper.cs | 16 ++- .../Shapers/IndicShaper.cs | 112 +++++++++++++----- .../Shapers/MyanmarShaper.cs | 56 ++++++--- .../Shapers/ShapingStage.cs | 8 +- .../AdvancedTypographic/Shapers/ThaiShaper.cs | 100 ++++++++++++---- .../Shapers/UniversalShaper.cs | 88 ++++++++++---- .../AdvancedTypographic/SyllableInfo.cs | 41 +++++-- src/SixLabors.Fonts/TextLine.cs | 36 ++++-- 13 files changed, 667 insertions(+), 218 deletions(-) diff --git a/src/SixLabors.Fonts/GlyphLayoutData.cs b/src/SixLabors.Fonts/GlyphLayoutData.cs index bdbd78738..bb66ee9ce 100644 --- a/src/SixLabors.Fonts/GlyphLayoutData.cs +++ b/src/SixLabors.Fonts/GlyphLayoutData.cs @@ -78,67 +78,109 @@ public GlyphLayoutData( this.HyphenationMarkerIndex = hyphenationMarkerIndex; } - /// Gets the source codepoint for this entry. + /// + /// Gets the source codepoint for this entry. + /// public readonly CodePoint CodePoint => this.Metrics[0].Metrics.CodePoint; - /// Gets the shaped glyph metrics produced for this codepoint (one codepoint may map to several glyphs). + /// + /// Gets the shaped glyph metrics produced for this codepoint (one codepoint may map to several glyphs). + /// public IReadOnlyList Metrics { get; } - /// Gets the font used to shape and render this entry. + /// + /// Gets the font used to shape and render this entry. + /// public Font Font { get; } - /// Gets the point size at which this entry is rendered. + /// + /// Gets the point size at which this entry is rendered. + /// public float PointSize { get; } - /// Gets or sets the scaled advance of this entry (mutated by justification). + /// + /// Gets or sets the scaled advance of this entry (mutated by justification). + /// public float ScaledAdvance { get; set; } - /// Gets the scaled line height contributed by this entry, before line-spacing is applied. + /// + /// Gets the scaled line height contributed by this entry, before line-spacing is applied. + /// public float ScaledLineHeight { get; } - /// Gets the scaled typographic ascender. + /// + /// Gets the scaled typographic ascender. + /// public float ScaledAscender { get; } - /// Gets the scaled typographic descender. + /// + /// Gets the scaled typographic descender. + /// public float ScaledDescender { get; } - /// Gets the symmetric ascender/descender delta applied during line-box construction. + /// + /// Gets the symmetric ascender/descender delta applied during line-box construction. + /// public float ScaledDelta { get; } - /// Gets the smallest (most negative) scaled Y across . + /// + /// Gets the smallest (most negative) scaled Y across . + /// public float ScaledMinY { get; } - /// Gets the resolved bidi run this entry belongs to. + /// + /// Gets the resolved bidi run this entry belongs to. + /// public BidiRun BidiRun { get; } - /// Gets the text direction derived from . + /// + /// Gets the text direction derived from . + /// public readonly TextDirection TextDirection => (TextDirection)this.BidiRun.Direction; - /// Gets the zero-based grapheme index in the original text. + /// + /// Gets the zero-based grapheme index in the original text. + /// public int GraphemeIndex { get; } - /// Gets or sets a value indicating whether this is the last entry in its grapheme cluster. + /// + /// Gets or sets a value indicating whether this is the last entry in its grapheme cluster. + /// public bool IsLastInGrapheme { get; set; } - /// Gets the index of this codepoint within its grapheme cluster (0-based). + /// + /// Gets the index of this codepoint within its grapheme cluster (0-based). + /// public int GraphemeCodePointIndex { get; } - /// Gets the codepoint index in the source text. + /// + /// Gets the codepoint index in the source text. + /// public int CodePointIndex { get; } - /// Gets a value indicating whether the entry participates in a transformed vertical layout. + /// + /// Gets a value indicating whether the entry participates in a transformed vertical layout. + /// public bool IsTransformed { get; } - /// Gets a value indicating whether the entry was produced by Unicode decomposition. + /// + /// Gets a value indicating whether the entry was produced by Unicode decomposition. + /// public bool IsDecomposed { get; } - /// Gets the zero-based UTF-16 code unit index in the original text. + /// + /// Gets the zero-based UTF-16 code unit index in the original text. + /// public int StringIndex { get; } - /// Gets the marker index to use if this entry becomes a selected soft-hyphen break. + /// + /// Gets the marker index to use if this entry becomes a selected soft-hyphen break. + /// public int HyphenationMarkerIndex { get; } - /// Gets a value indicating whether the codepoint is a line-break character. + /// + /// Gets a value indicating whether the codepoint is a line-break character. + /// public readonly bool IsNewLine => CodePoint.IsNewLine(this.CodePoint); private readonly string DebuggerDisplay => FormattableString diff --git a/src/SixLabors.Fonts/GlyphShapingData.cs b/src/SixLabors.Fonts/GlyphShapingData.cs index abb38f9cf..8fd106687 100644 --- a/src/SixLabors.Fonts/GlyphShapingData.cs +++ b/src/SixLabors.Fonts/GlyphShapingData.cs @@ -31,11 +31,49 @@ internal struct GlyphShapingData public SyllableInfo Syllable; #pragma warning restore SA1401 + /// + /// The bit recording . + /// + private const ushort LigatedFlag = 1 << 0; + + /// + /// The bit recording . + /// + private const ushort SubstitutedFlag = 1 << 1; + + /// + /// The bit recording . + /// + private const ushort DecomposedFlag = 1 << 2; + + /// + /// The bit recording . + /// + private const ushort PlaceholderFlag = 1 << 3; + + /// + /// The bit recording . + /// + private const ushort PositionedFlag = 1 << 4; + + /// + /// The bit recording . + /// + private const ushort KernedFlag = 1 << 5; + + /// + /// The bit recording that + /// holds the glyph id was computed for. A default + /// record therefore reports an invalid cache. + /// + private const ushort ShapingClassCacheValidFlag = 1 << 6; + private ushort glyphId; /// - /// Packed boolean shaping state. Single bits keep the record narrow; the bool - /// properties are the only readers and writers. + /// Packed boolean shaping state addressed through the named flag constants above. + /// Single bits keep the record narrow; the properties are the only readers and + /// writers. /// private ushort flags; @@ -122,17 +160,17 @@ public ushort GlyphId /// public int ShapingClassCacheKey { - readonly get => (this.flags & (1 << 6)) != 0 ? this.shapingClassCacheId : -1; + readonly get => (this.flags & ShapingClassCacheValidFlag) != 0 ? this.shapingClassCacheId : -1; set { if (value < 0) { - this.flags = (ushort)(this.flags & ~(1 << 6)); + this.flags = (ushort)(this.flags & ~ShapingClassCacheValidFlag); } else { this.shapingClassCacheId = (ushort)value; - this.flags = (ushort)(this.flags | (1 << 6)); + this.flags = (ushort)(this.flags | ShapingClassCacheValidFlag); } } } @@ -177,8 +215,8 @@ public TextDirection Direction /// public bool IsLigated { - readonly get => (this.flags & 1) != 0; - set => this.flags = value ? (ushort)(this.flags | 1) : (ushort)(this.flags & ~1); + readonly get => (this.flags & LigatedFlag) != 0; + set => this.flags = value ? (ushort)(this.flags | LigatedFlag) : (ushort)(this.flags & ~LigatedFlag); } /// @@ -225,8 +263,8 @@ public bool IsLigated /// public bool IsSubstituted { - readonly get => (this.flags & (1 << 1)) != 0; - set => this.flags = value ? (ushort)(this.flags | (1 << 1)) : (ushort)(this.flags & ~(1 << 1)); + readonly get => (this.flags & SubstitutedFlag) != 0; + set => this.flags = value ? (ushort)(this.flags | SubstitutedFlag) : (ushort)(this.flags & ~SubstitutedFlag); } /// @@ -234,8 +272,8 @@ public bool IsSubstituted /// public bool IsDecomposed { - readonly get => (this.flags & (1 << 2)) != 0; - set => this.flags = value ? (ushort)(this.flags | (1 << 2)) : (ushort)(this.flags & ~(1 << 2)); + readonly get => (this.flags & DecomposedFlag) != 0; + set => this.flags = value ? (ushort)(this.flags | DecomposedFlag) : (ushort)(this.flags & ~DecomposedFlag); } /// @@ -244,8 +282,8 @@ public bool IsDecomposed /// public bool IsPlaceholder { - readonly get => (this.flags & (1 << 3)) != 0; - set => this.flags = value ? (ushort)(this.flags | (1 << 3)) : (ushort)(this.flags & ~(1 << 3)); + readonly get => (this.flags & PlaceholderFlag) != 0; + set => this.flags = value ? (ushort)(this.flags | PlaceholderFlag) : (ushort)(this.flags & ~PlaceholderFlag); } /// @@ -253,8 +291,8 @@ public bool IsPlaceholder /// public bool IsPositioned { - readonly get => (this.flags & (1 << 4)) != 0; - set => this.flags = value ? (ushort)(this.flags | (1 << 4)) : (ushort)(this.flags & ~(1 << 4)); + readonly get => (this.flags & PositionedFlag) != 0; + set => this.flags = value ? (ushort)(this.flags | PositionedFlag) : (ushort)(this.flags & ~PositionedFlag); } /// @@ -262,8 +300,8 @@ public bool IsPositioned /// public bool IsKerned { - readonly get => (this.flags & (1 << 5)) != 0; - set => this.flags = value ? (ushort)(this.flags | (1 << 5)) : (ushort)(this.flags & ~(1 << 5)); + readonly get => (this.flags & KernedFlag) != 0; + set => this.flags = value ? (ushort)(this.flags | KernedFlag) : (ushort)(this.flags & ~KernedFlag); } private string DebuggerDisplay diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs index db1ba36a0..0b283e6a8 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs @@ -12,52 +12,84 @@ namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers; /// internal sealed class ArabicShaper : DefaultShaper { - /// The 'mset' (mark positioning via substitution) feature tag. + /// + /// The 'mset' (mark positioning via substitution) feature tag. + /// private static readonly Tag MsetTag = Tag.Parse("mset"); - /// The 'fina' (terminal forms) feature tag. + /// + /// The 'fina' (terminal forms) feature tag. + /// private static readonly Tag FinaTag = Tag.Parse("fina"); - /// The 'fin2' (terminal forms #2) feature tag. + /// + /// The 'fin2' (terminal forms #2) feature tag. + /// private static readonly Tag Fin2Tag = Tag.Parse("fin2"); - /// The 'fin3' (terminal forms #3) feature tag. + /// + /// The 'fin3' (terminal forms #3) feature tag. + /// private static readonly Tag Fin3Tag = Tag.Parse("fin3"); - /// The 'isol' (isolated forms) feature tag. + /// + /// The 'isol' (isolated forms) feature tag. + /// private static readonly Tag IsolTag = Tag.Parse("isol"); - /// The 'init' (initial forms) feature tag. + /// + /// The 'init' (initial forms) feature tag. + /// private static readonly Tag InitTag = Tag.Parse("init"); - /// The 'medi' (medial forms) feature tag. + /// + /// The 'medi' (medial forms) feature tag. + /// private static readonly Tag MediTag = Tag.Parse("medi"); - /// The 'med2' (medial forms #2) feature tag. + /// + /// The 'med2' (medial forms #2) feature tag. + /// private static readonly Tag Med2Tag = Tag.Parse("med2"); - /// No joining action. + /// + /// No joining action. + /// private const byte None = 0; - /// Isolated form action. + /// + /// Isolated form action. + /// private const byte Isol = 1; - /// Final form action. + /// + /// Final form action. + /// private const byte Fina = 2; - /// Final form #2 action (for ALAPH). + /// + /// Final form #2 action (for ALAPH). + /// private const byte Fin2 = 3; - /// Final form #3 action (for ALAPH after DALATH RISH). + /// + /// Final form #3 action (for ALAPH after DALATH RISH). + /// private const byte Fin3 = 4; - /// Medial form action. + /// + /// Medial form action. + /// private const byte Medi = 5; - /// Medial form #2 action (for ALAPH). + /// + /// Medial form #2 action (for ALAPH). + /// private const byte Med2 = 6; - /// Initial form action. + /// + /// Initial form action. + /// private const byte Init = 7; /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs index 4bdc45f09..1e7b97dad 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs @@ -11,73 +11,119 @@ namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers; /// internal class DefaultShaper : BaseShaper { - /// The 'rvrn' (required variation alternates) feature tag. + /// + /// The 'rvrn' (required variation alternates) feature tag. + /// protected static readonly Tag RvnrTag = Tag.Parse("rvrn"); - /// The 'ltra' (left-to-right alternates) feature tag. + /// + /// The 'ltra' (left-to-right alternates) feature tag. + /// protected static readonly Tag LtraTag = Tag.Parse("ltra"); - /// The 'ltrm' (left-to-right mirrored forms) feature tag. + /// + /// The 'ltrm' (left-to-right mirrored forms) feature tag. + /// protected static readonly Tag LtrmTag = Tag.Parse("ltrm"); - /// The 'rtla' (right-to-left alternates) feature tag. + /// + /// The 'rtla' (right-to-left alternates) feature tag. + /// protected static readonly Tag RtlaTag = Tag.Parse("rtla"); - /// The 'rtlm' (right-to-left mirrored forms) feature tag. + /// + /// The 'rtlm' (right-to-left mirrored forms) feature tag. + /// protected static readonly Tag RtlmTag = Tag.Parse("rtlm"); - /// The 'frac' (fractions) feature tag. + /// + /// The 'frac' (fractions) feature tag. + /// protected static readonly Tag FracTag = Tag.Parse("frac"); - /// The 'numr' (numerators) feature tag. + /// + /// The 'numr' (numerators) feature tag. + /// protected static readonly Tag NumrTag = Tag.Parse("numr"); - /// The 'dnom' (denominators) feature tag. + /// + /// The 'dnom' (denominators) feature tag. + /// protected static readonly Tag DnomTag = Tag.Parse("dnom"); - /// The 'ccmp' (glyph composition/decomposition) feature tag. + /// + /// The 'ccmp' (glyph composition/decomposition) feature tag. + /// protected static readonly Tag CcmpTag = Tag.Parse("ccmp"); - /// The 'locl' (localized forms) feature tag. + /// + /// The 'locl' (localized forms) feature tag. + /// protected static readonly Tag LoclTag = Tag.Parse("locl"); - /// The 'rlig' (required ligatures) feature tag. + /// + /// The 'rlig' (required ligatures) feature tag. + /// protected static readonly Tag RligTag = Tag.Parse("rlig"); - /// The 'mark' (mark positioning) feature tag. + /// + /// The 'mark' (mark positioning) feature tag. + /// protected static readonly Tag MarkTag = Tag.Parse("mark"); - /// The 'mkmk' (mark-to-mark positioning) feature tag. + /// + /// The 'mkmk' (mark-to-mark positioning) feature tag. + /// protected static readonly Tag MkmkTag = Tag.Parse("mkmk"); - /// The 'calt' (contextual alternates) feature tag. + /// + /// The 'calt' (contextual alternates) feature tag. + /// protected static readonly Tag CaltTag = Tag.Parse("calt"); - /// The 'clig' (contextual ligatures) feature tag. + /// + /// The 'clig' (contextual ligatures) feature tag. + /// protected static readonly Tag CligTag = Tag.Parse("clig"); - /// The 'liga' (standard ligatures) feature tag. + /// + /// The 'liga' (standard ligatures) feature tag. + /// protected static readonly Tag LigaTag = Tag.Parse("liga"); - /// The 'rclt' (required contextual alternates) feature tag. + /// + /// The 'rclt' (required contextual alternates) feature tag. + /// protected static readonly Tag RcltTag = Tag.Parse("rclt"); - /// The 'curs' (cursive positioning) feature tag. + /// + /// The 'curs' (cursive positioning) feature tag. + /// protected static readonly Tag CursTag = Tag.Parse("curs"); - /// The 'kern' (kerning) feature tag. + /// + /// The 'kern' (kerning) feature tag. + /// protected static readonly Tag KernTag = Tag.Parse("kern"); - /// The 'vert' (vertical alternates) feature tag. + /// + /// The 'vert' (vertical alternates) feature tag. + /// protected static readonly Tag VertTag = Tag.Parse("vert"); - /// The 'vkrn' (vertical kerning) feature tag. + /// + /// The 'vkrn' (vertical kerning) feature tag. + /// protected static readonly Tag VKernTag = Tag.Parse("vkrn"); - /// The fraction slash code point (U+2044). + /// + /// The fraction slash code point (U+2044). + /// private static readonly CodePoint FractionSlash = new(0x2044); - /// The solidus (slash) code point (U+002F). + /// + /// The solidus (slash) code point (U+002F). + /// private static readonly CodePoint Slash = new(0x002F); /// @@ -87,10 +133,14 @@ internal class DefaultShaper : BaseShaper /// private readonly List shapingStages = new(16); - /// The kerning mode from the text options. + /// + /// The kerning mode from the text options. + /// private readonly KerningMode kerningMode; - /// The user-specified feature tags from the text options. + /// + /// The user-specified feature tags from the text options. + /// private readonly IReadOnlyList featureTags; /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs index 06fc1923d..cb5e793a5 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs @@ -12,82 +12,134 @@ namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers; /// internal sealed class HangulShaper : DefaultShaper { - /// The 'ljmo' (leading Jamo forms) feature tag. + /// + /// The 'ljmo' (leading Jamo forms) feature tag. + /// private static readonly Tag LjmoTag = Tag.Parse("ljmo"); - /// The 'vjmo' (vowel Jamo forms) feature tag. + /// + /// The 'vjmo' (vowel Jamo forms) feature tag. + /// private static readonly Tag VjmoTag = Tag.Parse("vjmo"); - /// The 'tjmo' (trailing Jamo forms) feature tag. + /// + /// The 'tjmo' (trailing Jamo forms) feature tag. + /// private static readonly Tag TjmoTag = Tag.Parse("tjmo"); - /// The base code point for precomposed Hangul syllables (U+AC00). + /// + /// The base code point for precomposed Hangul syllables (U+AC00). + /// private const int HangulBase = 0xac00; - /// The base code point for leading consonant Jamo (U+1100). + /// + /// The base code point for leading consonant Jamo (U+1100). + /// private const int LBase = 0x1100; // lead - /// The base code point for vowel Jamo (U+1161). + /// + /// The base code point for vowel Jamo (U+1161). + /// private const int VBase = 0x1161; // vowel - /// The base code point for trailing consonant Jamo (U+11A7). + /// + /// The base code point for trailing consonant Jamo (U+11A7). + /// private const int TBase = 0x11a7; // trail - /// The number of leading consonant Jamo. + /// + /// The number of leading consonant Jamo. + /// private const int LCount = 19; - /// The number of vowel Jamo. + /// + /// The number of vowel Jamo. + /// private const int VCount = 21; - /// The number of trailing consonant Jamo (including no-trail). + /// + /// The number of trailing consonant Jamo (including no-trail). + /// private const int TCount = 28; - /// The last leading consonant Jamo code point. + /// + /// The last leading consonant Jamo code point. + /// private const int LEnd = LBase + LCount - 1; - /// The last vowel Jamo code point. + /// + /// The last vowel Jamo code point. + /// private const int VEnd = VBase + VCount - 1; - /// The last trailing consonant Jamo code point. + /// + /// The last trailing consonant Jamo code point. + /// private const int TEnd = TBase + TCount - 1; - /// The dotted circle code point (U+25CC) used as a placeholder base. + /// + /// The dotted circle code point (U+25CC) used as a placeholder base. + /// private const int DottedCircle = 0x25cc; - /// Other character category. + /// + /// Other character category. + /// private const byte X = 0; - /// Leading consonant category. + /// + /// Leading consonant category. + /// private const byte L = 1; - /// Medial vowel category. + /// + /// Medial vowel category. + /// private const byte V = 2; - /// Trailing consonant category. + /// + /// Trailing consonant category. + /// private const byte T = 3; - /// Composed lead-vowel syllable category. + /// + /// Composed lead-vowel syllable category. + /// private const byte LV = 4; - /// Composed lead-vowel-trail syllable category. + /// + /// Composed lead-vowel-trail syllable category. + /// private const byte LVT = 5; - /// Tone mark category. + /// + /// Tone mark category. + /// private const byte M = 6; - /// No action. + /// + /// No action. + /// private const byte None = 0; - /// Decompose composed syllable action. + /// + /// Decompose composed syllable action. + /// private const byte Decompose = 1; - /// Compose Jamo sequence action. + /// + /// Compose Jamo sequence action. + /// private const byte Compose = 2; - /// Reorder tone mark action. + /// + /// Reorder tone mark action. + /// private const byte ToneMark = 4; - /// Invalid sequence (insert dotted circle) action. + /// + /// Invalid sequence (insert dotted circle) action. + /// private const byte Invalid = 5; /// @@ -110,7 +162,9 @@ internal sealed class HangulShaper : DefaultShaper { 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 } }, }; - /// The font metrics used for glyph lookups during composition/decomposition. + /// + /// The font metrics used for glyph lookups during composition/decomposition. + /// private readonly FontMetrics fontMetrics; /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HebrewShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HebrewShaper.cs index 91a63fd55..f4d04bcef 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HebrewShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HebrewShaper.cs @@ -47,10 +47,14 @@ internal class HebrewShaper : DefaultShaper 0xFB4A, // TAV ]; - /// The font metrics used for glyph lookups during composition. + /// + /// The font metrics used for glyph lookups during composition. + /// private readonly FontMetrics fontMetrics; - /// Whether the font has GSUB features for Hebrew. + /// + /// Whether the font has GSUB features for Hebrew. + /// private readonly bool hasGsub; /// @@ -284,13 +288,17 @@ private static int TryCompose(int a, int b) return 0; } - /// Returns if the codepoint is PATAH (U+05B7) or QAMATS (U+05B8). + /// + /// Returns if the codepoint is PATAH (U+05B7) or QAMATS (U+05B8). + /// /// The codepoint value to test. /// if the codepoint is PATAH or QAMATS. private static bool IsPatahOrQamats(int codepoint) => codepoint is 0x05B7 or 0x05B8; - /// Returns if the codepoint is SHEVA (U+05B0) or HIRIQ (U+05B4). + /// + /// Returns if the codepoint is SHEVA (U+05B0) or HIRIQ (U+05B4). + /// /// The codepoint value to test. /// if the codepoint is SHEVA or HIRIQ. private static bool IsShevaOrHiriq(int codepoint) diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs index 018f9efa0..3795cd3d9 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs @@ -15,86 +15,140 @@ namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers; /// internal sealed class IndicShaper : DefaultShaper { - /// The state machine for Indic syllable identification. + /// + /// The state machine for Indic syllable identification. + /// private static readonly StateMachine StateMachine = new(StateTable, AcceptingStates, Tags); - /// Maps Indic shaping category codes to compact DFA symbol indices. + /// + /// Maps Indic shaping category codes to compact DFA symbol indices. + /// private static readonly int[] CategoryToSymbolId = BuildCategoryToSymbolId(); - /// The 'rphf' (reph forms) feature tag. + /// + /// The 'rphf' (reph forms) feature tag. + /// private static readonly Tag RphfTag = Tag.Parse("rphf"); - /// The 'nukt' (nukta forms) feature tag. + /// + /// The 'nukt' (nukta forms) feature tag. + /// private static readonly Tag NuktTag = Tag.Parse("nukt"); - /// The 'akhn' (akhands) feature tag. + /// + /// The 'akhn' (akhands) feature tag. + /// private static readonly Tag AkhnTag = Tag.Parse("akhn"); - /// The 'pref' (pre-base forms) feature tag. + /// + /// The 'pref' (pre-base forms) feature tag. + /// private static readonly Tag PrefTag = Tag.Parse("pref"); - /// The 'rkrf' (rakar forms) feature tag. + /// + /// The 'rkrf' (rakar forms) feature tag. + /// private static readonly Tag RkrfTag = Tag.Parse("rkrf"); - /// The 'abvf' (above-base forms) feature tag. + /// + /// The 'abvf' (above-base forms) feature tag. + /// private static readonly Tag AbvfTag = Tag.Parse("abvf"); - /// The 'blwf' (below-base forms) feature tag. + /// + /// The 'blwf' (below-base forms) feature tag. + /// private static readonly Tag BlwfTag = Tag.Parse("blwf"); - /// The 'half' (half forms) feature tag. + /// + /// The 'half' (half forms) feature tag. + /// private static readonly Tag HalfTag = Tag.Parse("half"); - /// The 'pstf' (post-base forms) feature tag. + /// + /// The 'pstf' (post-base forms) feature tag. + /// private static readonly Tag PstfTag = Tag.Parse("pstf"); - /// The 'vatu' (vattu variants) feature tag. + /// + /// The 'vatu' (vattu variants) feature tag. + /// private static readonly Tag VatuTag = Tag.Parse("vatu"); - /// The 'cjct' (conjunct forms) feature tag. + /// + /// The 'cjct' (conjunct forms) feature tag. + /// private static readonly Tag CjctTag = Tag.Parse("cjct"); - /// The 'cfar' (conjunct form after Ra) feature tag. + /// + /// The 'cfar' (conjunct form after Ra) feature tag. + /// private static readonly Tag CfarTag = Tag.Parse("cfar"); - /// The 'init' (initial forms) feature tag. + /// + /// The 'init' (initial forms) feature tag. + /// private static readonly Tag InitTag = Tag.Parse("init"); - /// The 'abvs' (above-base substitutions) feature tag. + /// + /// The 'abvs' (above-base substitutions) feature tag. + /// private static readonly Tag AbvsTag = Tag.Parse("abvs"); - /// The 'blws' (below-base substitutions) feature tag. + /// + /// The 'blws' (below-base substitutions) feature tag. + /// private static readonly Tag BlwsTag = Tag.Parse("blws"); - /// The 'pres' (pre-base substitutions) feature tag. + /// + /// The 'pres' (pre-base substitutions) feature tag. + /// private static readonly Tag PresTag = Tag.Parse("pres"); - /// The 'psts' (post-base substitutions) feature tag. + /// + /// The 'psts' (post-base substitutions) feature tag. + /// private static readonly Tag PstsTag = Tag.Parse("psts"); - /// The 'haln' (halant forms) feature tag. + /// + /// The 'haln' (halant forms) feature tag. + /// private static readonly Tag HalnTag = Tag.Parse("haln"); - /// The 'dist' (distances) feature tag. + /// + /// The 'dist' (distances) feature tag. + /// private static readonly Tag DistTag = Tag.Parse("dist"); - /// The 'abvm' (above-base mark positioning) feature tag. + /// + /// The 'abvm' (above-base mark positioning) feature tag. + /// private static readonly Tag AbvmTag = Tag.Parse("abvm"); - /// The 'blwm' (below-base mark positioning) feature tag. + /// + /// The 'blwm' (below-base mark positioning) feature tag. + /// private static readonly Tag BlwmTag = Tag.Parse("blwm"); - /// Dotted circle code point (U+25CC) used as a placeholder base. + /// + /// Dotted circle code point (U+25CC) used as a placeholder base. + /// private const int DottedCircle = 0x25cc; - /// The font metrics used for glyph lookups. + /// + /// The font metrics used for glyph lookups. + /// private readonly FontMetrics fontMetrics; - /// The script-specific shaping configuration for this Indic script. + /// + /// The script-specific shaping configuration for this Indic script. + /// private ShapingConfiguration indicConfiguration; - /// Whether this font uses old-spec Indic script tags. + /// + /// Whether this font uses old-spec Indic script tags. + /// private readonly bool isOldSpec; /// @@ -103,7 +157,9 @@ internal sealed class IndicShaper : DefaultShaper /// private readonly bool zeroContext; - /// Whether any broken clusters were detected during syllable setup. + /// + /// Whether any broken clusters were detected during syllable setup. + /// private bool hasBrokenClusters; /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs index a784edb6f..a2482f2de 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs @@ -13,50 +13,78 @@ namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers; /// internal sealed class MyanmarShaper : DefaultShaper { - /// The state machine for Myanmar syllable identification. + /// + /// The state machine for Myanmar syllable identification. + /// private static readonly StateMachine StateMachine = new( Unicode.Resources.MyanmarShapingData.StateTable, Unicode.Resources.MyanmarShapingData.AcceptingStates, Unicode.Resources.MyanmarShapingData.Tags); - /// Maps Myanmar shaping category codes to compact DFA symbol indices. + /// + /// Maps Myanmar shaping category codes to compact DFA symbol indices. + /// private static readonly int[] CategoryToSymbolId = BuildCategoryToSymbolId(); - /// The 'rphf' (reph forms) feature tag. + /// + /// The 'rphf' (reph forms) feature tag. + /// private static readonly Tag RphfTag = Tag.Parse("rphf"); - /// The 'pref' (pre-base forms) feature tag. + /// + /// The 'pref' (pre-base forms) feature tag. + /// private static readonly Tag PrefTag = Tag.Parse("pref"); - /// The 'blwf' (below-base forms) feature tag. + /// + /// The 'blwf' (below-base forms) feature tag. + /// private static readonly Tag BlwfTag = Tag.Parse("blwf"); - /// The 'pstf' (post-base forms) feature tag. + /// + /// The 'pstf' (post-base forms) feature tag. + /// private static readonly Tag PstfTag = Tag.Parse("pstf"); - /// The 'pres' (pre-base substitutions) feature tag. + /// + /// The 'pres' (pre-base substitutions) feature tag. + /// private static readonly Tag PresTag = Tag.Parse("pres"); - /// The 'abvs' (above-base substitutions) feature tag. + /// + /// The 'abvs' (above-base substitutions) feature tag. + /// private static readonly Tag AbvsTag = Tag.Parse("abvs"); - /// The 'blws' (below-base substitutions) feature tag. + /// + /// The 'blws' (below-base substitutions) feature tag. + /// private static readonly Tag BlwsTag = Tag.Parse("blws"); - /// The 'psts' (post-base substitutions) feature tag. + /// + /// The 'psts' (post-base substitutions) feature tag. + /// private static readonly Tag PstsTag = Tag.Parse("psts"); - /// Dotted circle code point (U+25CC) used as a placeholder base. + /// + /// Dotted circle code point (U+25CC) used as a placeholder base. + /// private const int DottedCircle = 0x25cc; - /// The text options. + /// + /// The text options. + /// private readonly TextOptions textOptions; - /// The font metrics used for glyph lookups. + /// + /// The font metrics used for glyph lookups. + /// private readonly FontMetrics fontMetrics; - /// Whether any broken clusters were detected during syllable setup. + /// + /// Whether any broken clusters were detected during syllable setup. + /// private bool hasBrokenClusters; /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShapingStage.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShapingStage.cs index cf8b6497e..a8f01282b 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShapingStage.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShapingStage.cs @@ -12,10 +12,14 @@ namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers; /// internal readonly struct ShapingStage : IEquatable { - /// The optional action to invoke before the feature is applied. + /// + /// The optional action to invoke before the feature is applied. + /// private readonly Action? preAction; - /// The optional action to invoke after the feature is applied. + /// + /// The optional action to invoke after the feature is applied. + /// private readonly Action? postAction; /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ThaiShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ThaiShaper.cs index 666543155..a94854a6d 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ThaiShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ThaiShaper.cs @@ -44,7 +44,9 @@ internal class ThaiShaper : DefaultShaper { new(PuaAction.NOP, 2), new(PuaAction.SD, 2), new(PuaAction.NOP, 2) }, }; - /// Shift-Down PUA mappings for tone marks and below-vowel marks. + /// + /// Shift-Down PUA mappings for tone marks and below-vowel marks. + /// private static readonly PuaMapping[] SdMappings = [ new(0x0E48, 0xF70A, 0xF88B), // MAI EK @@ -57,7 +59,9 @@ internal class ThaiShaper : DefaultShaper new(0x0E3A, 0xF71A, 0xF89D), // PHINTHU ]; - /// Shift-Down-Left PUA mappings for tone marks. + /// + /// Shift-Down-Left PUA mappings for tone marks. + /// private static readonly PuaMapping[] SdlMappings = [ new(0x0E48, 0xF705, 0xF88C), // MAI EK @@ -67,7 +71,9 @@ internal class ThaiShaper : DefaultShaper new(0x0E4C, 0xF709, 0xF898), // THANTHAKHAT ]; - /// Shift-Left PUA mappings for tone marks and above-vowel marks. + /// + /// Shift-Left PUA mappings for tone marks and above-vowel marks. + /// private static readonly PuaMapping[] SlMappings = [ new(0x0E48, 0xF713, 0xF88A), // MAI EK @@ -84,17 +90,23 @@ internal class ThaiShaper : DefaultShaper new(0x0E4D, 0xF711, 0xF899), // NIKHAHIT ]; - /// Remove-Descender PUA mappings for consonants with removable descenders. + /// + /// Remove-Descender PUA mappings for consonants with removable descenders. + /// private static readonly PuaMapping[] RdMappings = [ new(0x0E0D, 0xF70F, 0xF89A), // YO YING new(0x0E10, 0xF700, 0xF89E), // THO THAN ]; - /// The font metrics used for glyph lookups and PUA shaping. + /// + /// The font metrics used for glyph lookups and PUA shaping. + /// private readonly FontMetrics fontMetrics; - /// Whether the font has GSUB features for Thai/Lao. + /// + /// Whether the font has GSUB features for Thai/Lao. + /// private readonly bool hasGsub; /// @@ -102,19 +114,29 @@ internal class ThaiShaper : DefaultShaper /// private enum ConsonantType { - /// Normal consonant. + /// + /// Normal consonant. + /// NC, - /// Ascending consonant (Thai: 0x0E1B, 0x0E1D, 0x0E1F). + /// + /// Ascending consonant (Thai: 0x0E1B, 0x0E1D, 0x0E1F). + /// AC, - /// Consonant with removable descender (Thai: 0x0E0D, 0x0E10). + /// + /// Consonant with removable descender (Thai: 0x0E0D, 0x0E10). + /// RC, - /// Consonant with strict descender (Thai: 0x0E0E, 0x0E0F). + /// + /// Consonant with strict descender (Thai: 0x0E0E, 0x0E0F). + /// DC, - /// Not a consonant. + /// + /// Not a consonant. + /// NotConsonant } @@ -123,16 +145,24 @@ private enum ConsonantType /// private enum MarkType { - /// Above-vowel mark. + /// + /// Above-vowel mark. + /// AV, - /// Below-vowel mark. + /// + /// Below-vowel mark. + /// BV, - /// Tone mark. + /// + /// Tone mark. + /// T, - /// Not a mark. + /// + /// Not a mark. + /// NotMark } @@ -141,19 +171,29 @@ private enum MarkType /// private enum PuaAction { - /// No operation. + /// + /// No operation. + /// NOP, - /// Shift combining-mark down. + /// + /// Shift combining-mark down. + /// SD, - /// Shift combining-mark left. + /// + /// Shift combining-mark left. + /// SL, - /// Shift combining-mark down-left. + /// + /// Shift combining-mark down-left. + /// SDL, - /// Remove descender from base consonant. + /// + /// Remove descender from base consonant. + /// RD } @@ -475,10 +515,14 @@ private static bool IsAboveBaseMark(int codepoint) /// private readonly struct StateTransition(PuaAction action, int nextState) { - /// Gets the PUA action to apply. + /// + /// Gets the PUA action to apply. + /// public PuaAction Action { get; } = action; - /// Gets the next state for the state machine. + /// + /// Gets the next state for the state machine. + /// public int NextState { get; } = nextState; } @@ -487,13 +531,19 @@ private readonly struct StateTransition(PuaAction action, int nextState) /// private readonly struct PuaMapping(ushort original, ushort winPua, ushort macPua) { - /// Gets the original Thai codepoint. + /// + /// Gets the original Thai codepoint. + /// public ushort Original { get; } = original; - /// Gets the Windows PUA replacement codepoint. + /// + /// Gets the Windows PUA replacement codepoint. + /// public ushort WinPua { get; } = winPua; - /// Gets the Mac PUA replacement codepoint. + /// + /// Gets the Mac PUA replacement codepoint. + /// public ushort MacPua { get; } = macPua; } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs index 527e6bc1b..8550f2c8e 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs @@ -42,71 +42,115 @@ internal sealed class UniversalShaper : DefaultShaper private static readonly int CategoryVMPre = Array.IndexOf(CategoryNames, "VMPre"); - /// The state machine for Universal Shaping Engine syllable identification. + /// + /// The state machine for Universal Shaping Engine syllable identification. + /// private static readonly StateMachine StateMachine = new(UniversalShapingData.StateTable, UniversalShapingData.AcceptingStates, UniversalShapingData.Tags); - /// The 'rphf' (reph forms) feature tag. + /// + /// The 'rphf' (reph forms) feature tag. + /// private static readonly Tag RphfTag = Tag.Parse("rphf"); - /// The 'nukt' (nukta forms) feature tag. + /// + /// The 'nukt' (nukta forms) feature tag. + /// private static readonly Tag NuktTag = Tag.Parse("nukt"); - /// The 'akhn' (akhands) feature tag. + /// + /// The 'akhn' (akhands) feature tag. + /// private static readonly Tag AkhnTag = Tag.Parse("akhn"); - /// The 'pref' (pre-base forms) feature tag. + /// + /// The 'pref' (pre-base forms) feature tag. + /// private static readonly Tag PrefTag = Tag.Parse("pref"); - /// The 'rkrf' (rakar forms) feature tag. + /// + /// The 'rkrf' (rakar forms) feature tag. + /// private static readonly Tag RkrfTag = Tag.Parse("rkrf"); - /// The 'abvf' (above-base forms) feature tag. + /// + /// The 'abvf' (above-base forms) feature tag. + /// private static readonly Tag AbvfTag = Tag.Parse("abvf"); - /// The 'blwf' (below-base forms) feature tag. + /// + /// The 'blwf' (below-base forms) feature tag. + /// private static readonly Tag BlwfTag = Tag.Parse("blwf"); - /// The 'half' (half forms) feature tag. + /// + /// The 'half' (half forms) feature tag. + /// private static readonly Tag HalfTag = Tag.Parse("half"); - /// The 'pstf' (post-base forms) feature tag. + /// + /// The 'pstf' (post-base forms) feature tag. + /// private static readonly Tag PstfTag = Tag.Parse("pstf"); - /// The 'vatu' (vattu variants) feature tag. + /// + /// The 'vatu' (vattu variants) feature tag. + /// private static readonly Tag VatuTag = Tag.Parse("vatu"); - /// The 'cjct' (conjunct forms) feature tag. + /// + /// The 'cjct' (conjunct forms) feature tag. + /// private static readonly Tag CjctTag = Tag.Parse("cjct"); - /// The 'abvs' (above-base substitutions) feature tag. + /// + /// The 'abvs' (above-base substitutions) feature tag. + /// private static readonly Tag AbvsTag = Tag.Parse("abvs"); - /// The 'blws' (below-base substitutions) feature tag. + /// + /// The 'blws' (below-base substitutions) feature tag. + /// private static readonly Tag BlwsTag = Tag.Parse("blws"); - /// The 'pres' (pre-base substitutions) feature tag. + /// + /// The 'pres' (pre-base substitutions) feature tag. + /// private static readonly Tag PresTag = Tag.Parse("pres"); - /// The 'psts' (post-base substitutions) feature tag. + /// + /// The 'psts' (post-base substitutions) feature tag. + /// private static readonly Tag PstsTag = Tag.Parse("psts"); - /// The 'dist' (distances) feature tag. + /// + /// The 'dist' (distances) feature tag. + /// private static readonly Tag DistTag = Tag.Parse("dist"); - /// The 'abvm' (above-base mark positioning) feature tag. + /// + /// The 'abvm' (above-base mark positioning) feature tag. + /// private static readonly Tag AbvmTag = Tag.Parse("abvm"); - /// The 'blwm' (below-base mark positioning) feature tag. + /// + /// The 'blwm' (below-base mark positioning) feature tag. + /// private static readonly Tag BlwmTag = Tag.Parse("blwm"); - /// Dotted circle code point (U+25CC) used as a placeholder base. + /// + /// Dotted circle code point (U+25CC) used as a placeholder base. + /// private const int DottedCircle = 0x25cc; - /// The font metrics used for glyph lookups. + /// + /// The font metrics used for glyph lookups. + /// private readonly FontMetrics fontMetrics; - /// Whether any broken clusters were detected during syllable setup. + /// + /// Whether any broken clusters were detected during syllable setup. + /// private bool hasBrokenClusters; /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/SyllableInfo.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/SyllableInfo.cs index aceea04b3..8e6ff4ebf 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/SyllableInfo.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/SyllableInfo.cs @@ -15,6 +15,31 @@ namespace SixLabors.Fonts.Tables.AdvancedTypographic; /// internal struct SyllableInfo { + /// + /// The bit offset of the byte lane in . + /// + private const int TypeShift = 8; + + /// + /// The bit offset of the byte lane in . + /// + private const int CategoryShift = 16; + + /// + /// The bit offset of the byte lane in . + /// + private const int PositionShift = 24; + + /// + /// The bit offset of the byte lane in . + /// + private const int UseCategoryShift = 32; + + /// + /// The packed classification: one byte lane per property, with + /// in the lowest byte and the lanes above it addressed + /// through the shift constants. + /// private ulong bits; /// @@ -34,8 +59,8 @@ public int Number /// public SyllableType Type { - readonly get => (SyllableType)(byte)(this.bits >> 8); - set => this.bits = (this.bits & ~(0xFFUL << 8)) | ((ulong)(byte)value << 8); + readonly get => (SyllableType)(byte)(this.bits >> TypeShift); + set => this.bits = (this.bits & ~(0xFFUL << TypeShift)) | ((ulong)(byte)value << TypeShift); } /// @@ -43,8 +68,8 @@ public SyllableType Type /// public Categories IndicCategory { - readonly get => (Categories)(byte)(this.bits >> 16); - set => this.bits = (this.bits & ~(0xFFUL << 16)) | ((ulong)(byte)value << 16); + readonly get => (Categories)(byte)(this.bits >> CategoryShift); + set => this.bits = (this.bits & ~(0xFFUL << CategoryShift)) | ((ulong)(byte)value << CategoryShift); } /// @@ -52,8 +77,8 @@ public Categories IndicCategory /// public Positions IndicPosition { - readonly get => (Positions)(byte)(this.bits >> 24); - set => this.bits = (this.bits & ~(0xFFUL << 24)) | ((ulong)(byte)value << 24); + readonly get => (Positions)(byte)(this.bits >> PositionShift); + set => this.bits = (this.bits & ~(0xFFUL << PositionShift)) | ((ulong)(byte)value << PositionShift); } /// @@ -63,8 +88,8 @@ public Positions IndicPosition /// public int UseCategory { - readonly get => (byte)(this.bits >> 32); - set => this.bits = (this.bits & ~(0xFFUL << 32)) | ((ulong)(byte)value << 32); + readonly get => (byte)(this.bits >> UseCategoryShift); + set => this.bits = (this.bits & ~(0xFFUL << UseCategoryShift)) | ((ulong)(byte)value << UseCategoryShift); } /// diff --git a/src/SixLabors.Fonts/TextLine.cs b/src/SixLabors.Fonts/TextLine.cs index c0e29b791..e2ea7f1d8 100644 --- a/src/SixLabors.Fonts/TextLine.cs +++ b/src/SixLabors.Fonts/TextLine.cs @@ -1026,21 +1026,31 @@ private sealed class OrderedBidiRun /// The bidi embedding level for this run. public OrderedBidiRun(int level) => this.Level = level; - /// Gets the bidi embedding level of this run. + /// + /// Gets the bidi embedding level of this run. + /// public int Level { get; } - /// Gets or sets the next run in visual order. + /// + /// Gets or sets the next run in visual order. + /// public OrderedBidiRun? Next { get; set; } - /// Appends an entry to this run. + /// + /// Appends an entry to this run. + /// /// The entry to append. public void Add(GlyphLayoutData info) => this.info.Add(info); - /// Returns a slice view over this run's entries. + /// + /// Returns a slice view over this run's entries. + /// /// A slice over the entries. public ArraySlice AsSlice() => this.info.AsSlice(); - /// Reverses the entries in this run in place (for rule L2). + /// + /// Reverses the entries in this run in place (for rule L2). + /// public void Reverse() => this.AsSlice().Span.Reverse(); } @@ -1050,16 +1060,24 @@ private sealed class OrderedBidiRun /// private sealed class BidiRange { - /// Gets or sets the shared bidi embedding level for this range. + /// + /// Gets or sets the shared bidi embedding level for this range. + /// public int Level { get; set; } - /// Gets or sets the leftmost run in the range. + /// + /// Gets or sets the leftmost run in the range. + /// public OrderedBidiRun? Left { get; set; } - /// Gets or sets the rightmost run in the range. + /// + /// Gets or sets the rightmost run in the range. + /// public OrderedBidiRun? Right { get; set; } - /// Gets or sets the previous range in the processing stack. + /// + /// Gets or sets the previous range in the processing stack. + /// public BidiRange? Previous { get; set; } /// From 9149c82478229eb57f93aa8fa512382925d73109 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 15:27:27 +1000 Subject: [PATCH 30/94] Store the text run on the glyph record as a run-table index Every glyph record carried a TextRun reference. Records now store a ushort index into the buffer's run list, assigned to both of a pass's buffers before population so indices agree when records seed across them. Consumers resolve the run through the buffer; the shaped run table keeps the real reference for downstream layout. The record is now free of object references, so the garbage collector no longer scans the pooled glyph arrays, and the record shrinks further toward the flat info layout. Placeholders record the loop position as their run index; the populate tracker can lag it between runs, which the placeholder tests caught. Gate: 5,577/0 tests including exact HarfBuzz differential on Devanagari and all placeholder cases. Allocations fell slightly in every scenario (8,296 to 8,264, 7,856 to 7,824, 11,656 to 11,624 bytes). Means were measured under sustained machine load; ratios, the load-independent metric, read at or better than baseline in both runs (Latin 4.12 vs 4.83, Arabic 3.32 vs 3.50, Devanagari 2.05 vs 2.13). A clean-machine validation follows at the next idle window. --- src/SixLabors.Fonts/GlyphShapingData.cs | 14 ++++--- src/SixLabors.Fonts/ShapingBuffer.cs | 39 +++++++++++++------ .../AdvancedTypographic/GPos/AnchorTable.cs | 5 ++- .../Shapers/HangulShaper.cs | 10 +++-- src/SixLabors.Fonts/TextShaper.Pipeline.cs | 24 +++++++----- 5 files changed, 60 insertions(+), 32 deletions(-) diff --git a/src/SixLabors.Fonts/GlyphShapingData.cs b/src/SixLabors.Fonts/GlyphShapingData.cs index 8fd106687..ea9298993 100644 --- a/src/SixLabors.Fonts/GlyphShapingData.cs +++ b/src/SixLabors.Fonts/GlyphShapingData.cs @@ -91,8 +91,8 @@ internal struct GlyphShapingData /// /// Initializes a new instance of the struct. /// - /// The text run. - public GlyphShapingData(TextRun textRun) => this.TextRun = textRun; + /// The index of the text run this glyph belongs to. + public GlyphShapingData(ushort textRunIndex) => this.TextRunIndex = textRunIndex; /// /// Initializes a new instance of the struct. @@ -106,7 +106,7 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) this.CodePoint = data.CodePoint; this.CodePointCount = data.CodePointCount; this.Direction = data.Direction; - this.TextRun = data.TextRun; + this.TextRunIndex = data.TextRunIndex; this.LigatureId = data.LigatureId; this.IsLigated = data.IsLigated; this.LigatureComponent = data.LigatureComponent; @@ -201,9 +201,11 @@ public TextDirection Direction } /// - /// Gets or sets the text run this glyph belongs to. + /// Gets or sets the index of the text run this glyph belongs to, resolved against + /// the buffer's run list. An index keeps the record free of object references, so + /// the garbage collector never scans the pooled glyph arrays. /// - public TextRun TextRun { get; set; } + public ushort TextRunIndex { get; set; } /// /// Gets or sets the id of any ligature this glyph is a member of. @@ -306,7 +308,7 @@ public bool IsKerned private string DebuggerDisplay => FormattableString - .Invariant($" {this.GlyphId} : {this.CodePoint.ToDebuggerDisplay()} : {CodePoint.GetScriptClass(this.CodePoint)} : {this.Direction} : {this.TextRun.TextAttributes} : {this.LigatureId} : {this.LigatureComponent} : {this.IsDecomposed}"); + .Invariant($" {this.GlyphId} : {this.CodePoint.ToDebuggerDisplay()} : {CodePoint.GetScriptClass(this.CodePoint)} : {this.Direction} : run {this.TextRunIndex} : {this.LigatureId} : {this.LigatureComponent} : {this.IsDecomposed}"); /// /// Clears the registered and enabled feature masks while preserving the applied diff --git a/src/SixLabors.Fonts/ShapingBuffer.cs b/src/SixLabors.Fonts/ShapingBuffer.cs index c3193096a..d0531c38b 100644 --- a/src/SixLabors.Fonts/ShapingBuffer.cs +++ b/src/SixLabors.Fonts/ShapingBuffer.cs @@ -133,6 +133,13 @@ public ShapingBuffer(TextOptions textOptions, ShapingFeatureMap featureMap, Shap /// public int LigatureId { get; set; } = 1; + /// + /// Gets the text runs covering the pass's input. Records store run indices into + /// this list; the substitution and positioning buffers of a pass must share one + /// list so the indices agree when records are seeded across buffers. + /// + public IReadOnlyList TextRuns { get; private set; } = Array.Empty(); + /// /// Gets the reusable scratch the substitution table uses to merge a stage group's /// lookups into lookup-index order. Cleared by each group merge; kept on the pooled @@ -169,6 +176,14 @@ public ref GlyphShapingData this[int index] [MethodImpl(MethodImplOptions.AggressiveInlining)] public ref GlyphMetricsEntry MetricsAt(int index) => ref this.metrics[index]; + /// + /// Assigns the text runs for the pass. Must run on both of a pass's buffers + /// before any glyph is added, so record run indices resolve identically across + /// them. + /// + /// The resolved text runs covering the input. + public void SetTextRuns(IReadOnlyList textRuns) => this.TextRuns = textRuns; + /// /// Resets the buffer for reuse by a new shaping pass: adopts the new options, /// re-resolves the language candidates, empties the digest, and truncates the glyph @@ -302,13 +317,13 @@ public void AddGlyph(GlyphShapingData data, int offset) /// The id of the glyph to add. /// The codepoint the glyph represents. /// The resolved text direction for the codepoint. - /// The text run this glyph belongs to. + /// The index of the text run this glyph belongs to. /// The zero-based index within the input codepoint buffer. - public void AddGlyph(ushort glyphId, CodePoint codePoint, TextDirection direction, TextRun textRun, int offset) + public void AddGlyph(ushort glyphId, CodePoint codePoint, TextDirection direction, ushort textRunIndex, int offset) { this.glyphDigest.Add(glyphId); ref GlyphShapingData slot = ref this.Append(); - slot = new(textRun) + slot = new(textRunIndex) { CodePointIndex = offset, CodePoint = codePoint, @@ -322,12 +337,12 @@ public void AddGlyph(ushort glyphId, CodePoint codePoint, TextDirection directio /// /// The object replacement codepoint used for Unicode processing. /// The resolved bidi run for the placeholder. - /// The text run this placeholder belongs to. + /// The index of the text run this placeholder belongs to. /// The zero-based index within the input codepoint buffer. - public void AddPlaceholder(CodePoint codePoint, BidiRun bidiRun, TextRun textRun, int offset) + public void AddPlaceholder(CodePoint codePoint, BidiRun bidiRun, ushort textRunIndex, int offset) { ref GlyphShapingData slot = ref this.Append(); - slot = new(textRun) + slot = new(textRunIndex) { CodePointIndex = offset, CodePoint = codePoint, @@ -703,7 +718,7 @@ public bool TryAdd(Font font, ShapingBuffer workspace) // Placeholders are synthetic glyphs: they need layout metrics but must not // go through font glyph lookup, fallback resolution, or GPOS positioning. this.CopyPlaceholderBidiRun(workspace, source.CodePointIndex); - FontGlyphMetrics placeholderMetrics = PlaceholderGlyphMetrics.Create(font, source.TextRun, this.TextOptions.Dpi); + FontGlyphMetrics placeholderMetrics = PlaceholderGlyphMetrics.Create(font, this.TextRuns[source.TextRunIndex], this.TextOptions.Dpi); this.glyphDigest.Add(placeholderMetrics.GlyphId); ref GlyphShapingData placeholderSlot = ref this.Append(); @@ -718,8 +733,9 @@ public bool TryAdd(Font font, ShapingBuffer workspace) continue; } - TextAttributes textAttributes = source.TextRun.TextAttributes; - TextDecorations textDecorations = source.TextRun.TextDecorations; + TextRun sourceRun = this.TextRuns[source.TextRunIndex]; + TextAttributes textAttributes = sourceRun.TextAttributes; + TextDecorations textDecorations = sourceRun.TextDecorations; bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(codePoint, layoutMode) || (source.AppliedFeatureMask & verticalMask) != 0; @@ -786,8 +802,9 @@ public bool TryUpdate(Font font, ShapingBuffer workspace) ushort id = shape.GlyphId; CodePoint codePoint = shape.CodePoint; - TextAttributes textAttributes = shape.TextRun.TextAttributes; - TextDecorations textDecorations = shape.TextRun.TextDecorations; + TextRun shapeRun = this.TextRuns[shape.TextRunIndex]; + TextAttributes textAttributes = shapeRun.TextAttributes; + TextDecorations textDecorations = shapeRun.TextDecorations; bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(codePoint, layoutMode) || (shape.AppliedFeatureMask & verticalMask) != 0; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs index 84c91487f..88d96a97e 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs @@ -156,8 +156,9 @@ public override AnchorXY GetAnchor(FontMetrics fontMetrics, ref GlyphShapingData { if (buffer.TextOptions.HintingMode != HintingMode.None) { - TextAttributes textAttributes = data.TextRun.TextAttributes; - TextDecorations textDecorations = data.TextRun.TextDecorations; + TextRun textRun = buffer.TextRuns[data.TextRunIndex]; + TextAttributes textAttributes = textRun.TextAttributes; + TextDecorations textDecorations = textRun.TextDecorations; LayoutMode layoutMode = buffer.TextOptions.LayoutMode; ColorFontSupport colorFontSupport = buffer.TextOptions.ColorFontSupport; if (fontMetrics.TryGetGlyphMetrics(data.CodePoint, textAttributes, textDecorations, layoutMode, colorFontSupport, out FontGlyphMetrics? metrics)) diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs index cb5e793a5..760bdcc45 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs @@ -497,8 +497,9 @@ private void ReOrderToneMark(ShapingBuffer buffer, ref GlyphShapingData data, in // 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 fontMetrics = this.fontMetrics; - TextAttributes textAttributes = data.TextRun.TextAttributes; - TextDecorations textDecorations = data.TextRun.TextDecorations; + TextRun textRun = buffer.TextRuns[data.TextRunIndex]; + TextAttributes textAttributes = textRun.TextAttributes; + TextDecorations textDecorations = textRun.TextDecorations; LayoutMode layoutMode = buffer.TextOptions.LayoutMode; ColorFontSupport colorFontSupport = buffer.TextOptions.ColorFontSupport; if (fontMetrics.TryGetGlyphMetrics(data.CodePoint, textAttributes, textDecorations, layoutMode, colorFontSupport, out FontGlyphMetrics? metrics) @@ -527,8 +528,9 @@ private int InsertDottedCircle(ShapingBuffer buffer, ref GlyphShapingData data, if (fontMetrics.TryGetGlyphId(new(DottedCircle), out ushort id)) { - TextAttributes textAttributes = data.TextRun.TextAttributes; - TextDecorations textDecorations = data.TextRun.TextDecorations; + TextRun textRun = buffer.TextRuns[data.TextRunIndex]; + TextAttributes textAttributes = textRun.TextAttributes; + TextDecorations textDecorations = textRun.TextDecorations; LayoutMode layoutMode = buffer.TextOptions.LayoutMode; ColorFontSupport colorFontSupport = buffer.TextOptions.ColorFontSupport; if (fontMetrics.TryGetGlyphMetrics(data.CodePoint, textAttributes, textDecorations, layoutMode, colorFontSupport, out FontGlyphMetrics? metrics) diff --git a/src/SixLabors.Fonts/TextShaper.Pipeline.cs b/src/SixLabors.Fonts/TextShaper.Pipeline.cs index f937b3373..032492c13 100644 --- a/src/SixLabors.Fonts/TextShaper.Pipeline.cs +++ b/src/SixLabors.Fonts/TextShaper.Pipeline.cs @@ -223,8 +223,11 @@ or BidiCharacterType.FirstStrongIsolate probe = ShapingProbe.Enter(); - // Incrementally build out buffer of glyphs. + // Incrementally build out buffer of glyphs. Both buffers share the run list so + // per-glyph run indices agree when records are seeded across them. IReadOnlyList textRuns = BuildTextRuns(text, options); + substitutions.SetTextRuns(textRuns); + positionings.SetTextRuns(textRuns); ShapingProbe.Exit(ShapingProbe.BuildTextRuns, probe); // First do multiple font runs using the individual text runs. @@ -232,8 +235,9 @@ or BidiCharacterType.FirstStrongIsolate int textRunIndex = 0; int codePointIndex = 0; int bidiRunIndex = 0; - foreach (TextRun textRun in textRuns) + for (int runIndex = 0; runIndex < textRuns.Count; runIndex++) { + TextRun textRun = textRuns[runIndex]; if (textRun.Placeholder.HasValue) { substitutions.Clear(); @@ -251,11 +255,13 @@ or BidiCharacterType.FirstStrongIsolate : new(BidiCharacterType.LeftToRight, 2, codePointIndex, 0); // Placeholder runs are inserted into the layout stream and do not consume - // source graphemes, source codepoints, or bidi runs. + // source graphemes, source codepoints, or bidi runs. The loop position + // is the placeholder's own run index; the populate tracker below may + // lag it between runs. substitutions.AddPlaceholder( CodePoint.ObjectReplacementChar, placeholderBidiRun, - textRun, + (ushort)runIndex, codePointIndex); complete &= positionings.TryAdd(textRun.ResolvedFont, substitutions); @@ -346,7 +352,7 @@ or BidiCharacterType.FirstStrongIsolate List runs = []; Font? runFont = null; - TextRun? runTextRun = null; + int runTextRunIndex = -1; BidiRun runBidiRun = default; for (int i = 0; i < count; i++) { @@ -358,13 +364,13 @@ or BidiCharacterType.FirstStrongIsolate ? positionings.GetPlaceholderBidiRun(shaping.CodePointIndex) : default; if (entry.Font != runFont - || shaping.TextRun != runTextRun + || shaping.TextRunIndex != runTextRunIndex || (shaping.IsPlaceholder && !shapingBidiRun.Equals(runBidiRun))) { runFont = entry.Font; - runTextRun = shaping.TextRun; + runTextRunIndex = shaping.TextRunIndex; runBidiRun = shapingBidiRun; - runs.Add(new(entry.Font, entry.PointSize, shaping.TextRun, shapingBidiRun)); + runs.Add(new(entry.Font, entry.PointSize, positionings.TextRuns[shaping.TextRunIndex], shapingBidiRun)); } ShapedGlyphFlags flags = ShapedGlyphFlags.None; @@ -509,7 +515,7 @@ private static bool DoFontRun( continue; } - substitutions.AddGlyph(glyphId, current, (TextDirection)bidiRuns[bidiRunIndex].Direction, textRuns[textRunIndex], codePointIndex); + substitutions.AddGlyph(glyphId, current, (TextDirection)bidiRuns[bidiRunIndex].Direction, (ushort)textRunIndex, codePointIndex); codePointIndex++; graphemeCodePointIndex++; From a73942658c737991384a114ece5d66a763c65686 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 15:35:01 +1000 Subject: [PATCH 31/94] Shape and seed a single run in place instead of copying buffers When the input resolves to one non-placeholder text run, the pipeline now shapes into the workspace, seeds its metrics stream in place, and flips the buffer to the positioning role, so no glyph record crosses buffers and no duplicate metrics fetch occurs. Only when fallback glyphs remain and fallback fonts exist does the general cross-buffer seed run so fallback passes can merge into the accumulator. The pooled scratch restores each buffer's home role on reuse. Gate: 5,577/0 tests including exact HarfBuzz differential on Devanagari and all placeholder cases. All rows clean (idle machine, Fonts StdDev at or below 0.25 us). | Scenario | Mean before | Mean after | Ratio | |------------|-------------|------------|---------------| | Latin | 10.96 us | 9.50 us | 4.83x > 4.05x | | Arabic | 13.38 us | 12.02 us | 3.50x > 3.13x | | Devanagari | 19.19 us | 17.88 us | 2.13x > 2.01x | Includes the record-shrink step 5 means that awaited clean validation; allocations 8,264 / 7,824 / 11,624 bytes. --- src/SixLabors.Fonts/ShapingBuffer.cs | 62 +++++++++++- src/SixLabors.Fonts/ShapingScratch.cs | 5 + src/SixLabors.Fonts/TextShaper.Pipeline.cs | 112 ++++++++++++++++++--- 3 files changed, 163 insertions(+), 16 deletions(-) diff --git a/src/SixLabors.Fonts/ShapingBuffer.cs b/src/SixLabors.Fonts/ShapingBuffer.cs index d0531c38b..21813ecf5 100644 --- a/src/SixLabors.Fonts/ShapingBuffer.cs +++ b/src/SixLabors.Fonts/ShapingBuffer.cs @@ -93,7 +93,7 @@ public ShapingBuffer(TextOptions textOptions, ShapingFeatureMap featureMap, Shap /// Gets the shaping phase this buffer serves. Shapers gate phase-specific work, /// such as syllable analysis and reordering, on the substitution role. /// - public ShapingBufferRole Role { get; } + public ShapingBufferRole Role { get; private set; } /// /// Gets the number of live glyph records. Substitution can leave this greater or @@ -184,6 +184,66 @@ public ref GlyphShapingData this[int index] /// The resolved text runs covering the input. public void SetTextRuns(IReadOnlyList textRuns) => this.TextRuns = textRuns; + /// + /// Changes the shaping phase this buffer serves. The single-run fast path shapes + /// and seeds one buffer in place, flipping it from substitution to positioning + /// instead of copying every record into a second buffer. + /// + /// The shaping phase the buffer serves next. + public void SetRole(ShapingBufferRole role) => this.Role = role; + + /// + /// Seeds this buffer's metrics stream in place after substitution: fetches each + /// glyph's metrics from , clears the record's feature + /// registration for the positioning pass, and starts the shaping bounds from the + /// single-axis advance. Behaviorally the in-place equivalent of + /// without the cross-buffer record copy; valid only when the + /// buffer holds no placeholders. + /// + /// The font used to resolve metrics. + /// + /// when every mapped codepoint resolved a real glyph; + /// when fallback glyphs remain for a later font pass. + /// + public bool SeedMetricsInPlace(Font font) + { + bool hasFallBacks = false; + FontMetrics fontMetrics = font.FontMetrics; + LayoutMode layoutMode = this.TextOptions.LayoutMode; + ColorFontSupport colorFontSupport = this.TextOptions.ColorFontSupport; + + ulong verticalMask = this.GetVerticalFeatureMask(); + + for (int i = 0; i < this.count; i++) + { + ref GlyphShapingData slot = ref this.data[i]; + CodePoint codePoint = slot.CodePoint; + + TextRun textRun = this.TextRuns[slot.TextRunIndex]; + TextAttributes textAttributes = textRun.TextAttributes; + TextDecorations textDecorations = textRun.TextDecorations; + + bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(codePoint, layoutMode) + || (slot.AppliedFeatureMask & verticalMask) != 0; + + FontGlyphMetrics glyphMetrics = this.GetGlyphMetricsCached(fontMetrics, codePoint, slot.GlyphId, textAttributes, textDecorations, layoutMode, colorFontSupport); + + if (glyphMetrics.GlyphType == GlyphType.Fallback && !CodePoint.IsControl(codePoint)) + { + hasFallBacks = true; + } + + slot.ClearFeatures(); + slot.Bounds = isVertical + ? new(0, 0, 0, glyphMetrics.AdvanceHeight) + : new(0, 0, glyphMetrics.AdvanceWidth, 0); + + this.metrics[i] = new(font, font.Size, glyphMetrics); + } + + return !hasFallBacks; + } + /// /// Resets the buffer for reuse by a new shaping pass: adopts the new options, /// re-resolves the language candidates, empties the digest, and truncates the glyph diff --git a/src/SixLabors.Fonts/ShapingScratch.cs b/src/SixLabors.Fonts/ShapingScratch.cs index 50536eb50..a5d7ae7c3 100644 --- a/src/SixLabors.Fonts/ShapingScratch.cs +++ b/src/SixLabors.Fonts/ShapingScratch.cs @@ -52,6 +52,11 @@ internal sealed class ShapingScratch this.featureMap.Reset(); this.workspace!.Reset(options); this.result!.Reset(options); + + // The single-run fast path flips the workspace to the positioning role in + // place; a pooled scratch must hand out buffers in their home roles. + this.workspace.SetRole(ShapingBufferRole.Substitution); + this.result.SetRole(ShapingBufferRole.Positioning); } return (this.workspace!, this.result!); diff --git a/src/SixLabors.Fonts/TextShaper.Pipeline.cs b/src/SixLabors.Fonts/TextShaper.Pipeline.cs index 032492c13..758da5d00 100644 --- a/src/SixLabors.Fonts/TextShaper.Pipeline.cs +++ b/src/SixLabors.Fonts/TextShaper.Pipeline.cs @@ -235,6 +235,47 @@ or BidiCharacterType.FirstStrongIsolate int textRunIndex = 0; int codePointIndex = 0; int bidiRunIndex = 0; + + // Single-run fast path: shape and seed one buffer in place and flip it to the + // positioning role, so no record is copied between buffers. When fallback + // glyphs remain and fallback fonts exist, fall through to the general + // cross-buffer seed so the fallback passes can merge into the accumulator. + ShapingBuffer shaped = positionings; + if (textRuns.Count == 1 && !textRuns[0].Placeholder.HasValue) + { + TextRun onlyRun = textRuns[0]; + PopulateAndSubstitute( + text, + onlyRun.Start, + textRuns, + ref textRunIndex, + ref codePointIndex, + ref bidiRunIndex, + onlyRun.ResolvedFont, + bidiRuns, + bidiMap, + substitutions); + + var seedProbe = ShapingProbe.Enter(); + complete = substitutions.SeedMetricsInPlace(onlyRun.ResolvedFont); + ShapingProbe.Exit(ShapingProbe.MetricsAdd, seedProbe); + + if (complete || fallbackFonts.Length == 0) + { + substitutions.SetRole(ShapingBufferRole.Positioning); + shaped = substitutions; + complete = true; + } + else + { + seedProbe = ShapingProbe.Enter(); + complete = positionings.TryAdd(onlyRun.ResolvedFont, substitutions); + ShapingProbe.Exit(ShapingProbe.MetricsAdd, seedProbe); + } + + goto FallbackPasses; + } + for (int runIndex = 0; runIndex < textRuns.Count; runIndex++) { TextRun textRun = textRuns[runIndex]; @@ -287,6 +328,7 @@ or BidiCharacterType.FirstStrongIsolate } } + FallbackPasses: if (!complete) { // Finally try our fallback fonts. @@ -330,13 +372,13 @@ or BidiCharacterType.FirstStrongIsolate continue; } - font.FontMetrics.UpdatePositions(positionings); + font.FontMetrics.UpdatePositions(shaped); lastFont = font; } foreach (Font font in fallbackFonts) { - font.FontMetrics.UpdatePositions(positionings); + font.FontMetrics.UpdatePositions(shaped); } ShapingProbe.Exit(ShapingProbe.Positioning, probe); @@ -345,8 +387,8 @@ or BidiCharacterType.FirstStrongIsolate // deduplicates into a run table and per-glyph state splits into parallel // identity and geometry arrays of pure values, so the scratch can go back to // the pool before consumption and no metrics reference survives shaping. - ulong verticalMask = positionings.GetVerticalFeatureMask(); - int count = positionings.Count; + ulong verticalMask = shaped.GetVerticalFeatureMask(); + int count = shaped.Count; ShapedGlyphInfo[] infos = new ShapedGlyphInfo[count]; ShapedGlyphPosition[] positions = new ShapedGlyphPosition[count]; List runs = []; @@ -356,12 +398,12 @@ or BidiCharacterType.FirstStrongIsolate BidiRun runBidiRun = default; for (int i = 0; i < count; i++) { - ref GlyphShapingData shaping = ref positionings[i]; - ref ShapingBuffer.GlyphMetricsEntry entry = ref positionings.MetricsAt(i); + ref GlyphShapingData shaping = ref shaped[i]; + ref ShapingBuffer.GlyphMetricsEntry entry = ref shaped.MetricsAt(i); // Placeholders carry a bidi run of their own, so they always cut a run. BidiRun shapingBidiRun = shaping.IsPlaceholder - ? positionings.GetPlaceholderBidiRun(shaping.CodePointIndex) + ? shaped.GetPlaceholderBidiRun(shaping.CodePointIndex) : default; if (entry.Font != runFont || shaping.TextRunIndex != runTextRunIndex @@ -370,7 +412,7 @@ or BidiCharacterType.FirstStrongIsolate runFont = entry.Font; runTextRunIndex = shaping.TextRunIndex; runBidiRun = shapingBidiRun; - runs.Add(new(entry.Font, entry.PointSize, positionings.TextRuns[shaping.TextRunIndex], shapingBidiRun)); + runs.Add(new(entry.Font, entry.PointSize, shaped.TextRuns[shaping.TextRunIndex], shapingBidiRun)); } ShapedGlyphFlags flags = ShapedGlyphFlags.None; @@ -449,6 +491,53 @@ private static bool DoFontRun( int[] bidiMap, ShapingBuffer substitutions, ShapingBuffer positionings) + { + PopulateAndSubstitute( + text, + start, + textRuns, + ref textRunIndex, + ref codePointIndex, + ref bidiRunIndex, + font, + bidiRuns, + bidiMap, + substitutions); + + var seedProbe = ShapingProbe.Enter(); + bool result = !isFallbackRun + ? positionings.TryAdd(font, substitutions) + : positionings.TryUpdate(font, substitutions); + ShapingProbe.Exit(ShapingProbe.MetricsAdd, seedProbe); + return result; + } + + /// + /// Populates the substitution buffer from and runs bidi + /// mirroring and GSUB substitution over it, leaving the shaped records in the + /// buffer for either in-place metrics seeding or a cross-buffer seed. + /// + /// The run-relative text slice to shape. + /// The starting grapheme index (absolute within the original input). + /// The ordered list of resolved text runs. + /// The index of the current text run; advanced as the enumerator crosses run boundaries. + /// The running codepoint index (absolute within the original input). + /// The running bidi run index. + /// The font to shape with. + /// The resolved bidi runs covering the whole input. + /// A codepoint โ†’ bidi-run mapping accumulated across shaping passes. + /// The GSUB substitution buffer to write into. + private static void PopulateAndSubstitute( + ReadOnlySpan text, + int start, + IReadOnlyList textRuns, + ref int textRunIndex, + ref int codePointIndex, + ref int bidiRunIndex, + Font font, + BidiRun[] bidiRuns, + int[] bidiMap, + ShapingBuffer substitutions) { // For each run we start with a fresh substitution buffer to avoid // overwriting the glyph ids. @@ -535,13 +624,6 @@ private static bool DoFontRun( probe = ShapingProbe.Enter(); font.FontMetrics.ApplySubstitution(substitutions); ShapingProbe.Exit(ShapingProbe.Substitution, probe); - - probe = ShapingProbe.Enter(); - bool result = !isFallbackRun - ? positionings.TryAdd(font, substitutions) - : positionings.TryUpdate(font, substitutions); - ShapingProbe.Exit(ShapingProbe.MetricsAdd, probe); - return result; } /// From dca2926cbf1179af6abd99dc8dc27ab43d4310cd Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 15:43:08 +1000 Subject: [PATCH 32/94] Drive positioning from the substitution pass's plan Substitution records each script segment's final range, script, and shaper on the buffer. When positioning runs over the same buffer, the in-place fast path, it reuses those segments through a factored segment positioner: no second shaper creation, no re-planning, and no second script segmentation walk. The in-place seed keeps feature masks for the same reason; one plan now drives both tables. Records seeded across buffers keep the self-segmenting re-planning path, whose seeds still clear feature registrations. Gate: 5,577/0 tests including exact HarfBuzz differential on Devanagari and all placeholder cases. | Scenario | Alloc before | Alloc after | Ratio | |------------|--------------|-------------|----------------| | Latin | 8,264 B | 7,776 B | 4.05x > 3.56x | | Arabic | 7,824 B | 6,480 B | 3.13x > 2.81x | | Devanagari | 11,624 B | 8,496 B | 2.01x > 1.93x | Allocations are exact; means were measured under machine load and a clean-machine validation follows at the next idle window. Ratios, the load-independent metric, improved in every scenario. --- src/SixLabors.Fonts/ShapingBuffer.cs | 14 +- .../Tables/AdvancedTypographic/GPosTable.cs | 298 +++++++++++------- .../Tables/AdvancedTypographic/GSubTable.cs | 5 + 3 files changed, 207 insertions(+), 110 deletions(-) diff --git a/src/SixLabors.Fonts/ShapingBuffer.cs b/src/SixLabors.Fonts/ShapingBuffer.cs index 21813ecf5..f8af15dab 100644 --- a/src/SixLabors.Fonts/ShapingBuffer.cs +++ b/src/SixLabors.Fonts/ShapingBuffer.cs @@ -140,6 +140,14 @@ public ShapingBuffer(TextOptions textOptions, ShapingFeatureMap featureMap, Shap /// public IReadOnlyList TextRuns { get; private set; } = Array.Empty(); + /// + /// Gets the shaping segments recorded during substitution: each script segment's + /// final range, script, and the shaper that planned it. The in-place positioning + /// pass reuses these so one plan drives both tables; the list stays empty when + /// records were seeded across buffers and positioning must segment for itself. + /// + public List<(int Index, int Count, ScriptClass Script, Tables.AdvancedTypographic.Shapers.BaseShaper Shaper)> SegmentShapers { get; } = new(); + /// /// Gets the reusable scratch the substitution table uses to merge a stage group's /// lookups into lookup-index order. Cleared by each group merge; kept on the pooled @@ -233,7 +241,9 @@ public bool SeedMetricsInPlace(Font font) hasFallBacks = true; } - slot.ClearFeatures(); + // Feature masks persist deliberately: the in-place positioning pass + // reuses the substitution pass's plan, whose registrations already cover + // the positioning features. slot.Bounds = isVertical ? new(0, 0, 0, glyphMetrics.AdvanceHeight) : new(0, 0, glyphMetrics.AdvanceWidth, 0); @@ -257,6 +267,7 @@ public void Reset(TextOptions textOptions) this.LigatureId = 1; this.glyphDigest = default; this.placeholderBidiRuns.Clear(); + this.SegmentShapers.Clear(); this.TextOptions = textOptions; this.LanguageTags = ResolveLanguageTags(textOptions); } @@ -270,6 +281,7 @@ public void Clear() this.count = 0; this.LigatureId = 1; this.placeholderBidiRuns.Clear(); + this.SegmentShapers.Clear(); } /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs index 37ec90c8d..158f5c4dc 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs @@ -173,6 +173,38 @@ public bool TryUpdatePositions(FontMetrics fontMetrics, ShapingBuffer buffer, ou kerned = false; bool updated = false; + + // Segments recorded during an in-place substitution pass carry their shaper + // and plan; reuse them so one plan drives both tables and positioning never + // re-segments, re-creates, or re-plans. An empty list means records were + // seeded across buffers and positioning must segment for itself below. + List<(int Index, int Count, ScriptClass Script, BaseShaper Shaper)> segments = buffer.SegmentShapers; + if (segments.Count > 0) + { + for (int s = 0; s < segments.Count; s++) + { + (int index, int count, ScriptClass script, BaseShaper shaper) = segments[s]; + updated |= this.PositionSegment( + fontMetrics, + buffer, + shaper, + script, + index, + count, + maxOperationsCount, + ref currentOperations, + ref kerned, + ref maxOperationsReached); + + if (maxOperationsReached) + { + break; + } + } + + return updated; + } + for (int i = 0; i < buffer.Count; i++) { if (!buffer.ShouldProcess(fontMetrics, i)) @@ -221,150 +253,198 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas Tag unicodeScriptTag = this.GetUnicodeScriptTag(current); BaseShaper shaper = ShaperFactory.Create(current, unicodeScriptTag, fontMetrics, buffer.TextOptions); - if (shaper.MarkZeroingMode == MarkZeroingMode.PreGPos) + // Plan positioning features for each glyph. Records seeded across buffers + // had their feature registrations cleared, so this pass re-plans. + shaper.Plan(buffer, index, count); + + updated |= this.PositionSegment( + fontMetrics, + buffer, + shaper, + current, + index, + count, + maxOperationsCount, + ref currentOperations, + ref kerned, + ref maxOperationsReached); + + if (i >= maxCount || maxOperationsReached) { - ZeroMarkAdvances(fontMetrics, buffer, index, count); + return updated; } + } - // Plan positioning features for each glyph. - shaper.Plan(buffer, index, count); + return updated; + } - // Stages are applied in pause-delimited groups: a stage action is a - // synchronization point, and between two actions every registered - // feature's lookups apply together in lookup-list order, the order the - // specification defines for lookups within a single application pass. A - // lookup registered by several of the group's features applies once with - // their glyph masks combined. - List shapingStages = shaper.GetShapingStages(); - SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, default, 0); - List<(Tag Feature, ushort Index, LookupTable LookupTable, ulong Mask)> merged = buffer.GPosLookupScratch; - - int stageIndex = 0; - while (stageIndex < shapingStages.Count) - { - shapingStages[stageIndex].PreProcessFeature(buffer, index, count); + /// + /// Applies the positioning stages of a planned segment: mark zeroing, the + /// pause-delimited stage groups in lookup-index order, attachment resolution, and + /// position materialization. The caller supplies a shaper whose plan already + /// covers the segment, either fresh from re-planning or reused from the + /// substitution pass. + /// + /// The font metrics. + /// The glyph positioning buffer. + /// The shaper whose plan covers the segment. + /// The script class the segment resolved to. + /// The starting index of the segment. + /// The number of glyphs in the segment. + /// The maximum allowable operations count. + /// The current operations counter. + /// Set when a kerning feature applied. + /// Set when the operations budget ran out. + /// if any positioning was updated. + private bool PositionSegment( + FontMetrics fontMetrics, + ShapingBuffer buffer, + BaseShaper shaper, + ScriptClass script, + int index, + int count, + int maxOperationsCount, + ref int currentOperations, + ref bool kerned, + ref bool maxOperationsReached) + { + bool updated = false; - // Extend the group while its interior holds no actions: a post action - // closes the group after its stage and a pre action opens a new one. - int groupEnd = stageIndex; - while (true) + if (shaper.MarkZeroingMode == MarkZeroingMode.PreGPos) + { + ZeroMarkAdvances(fontMetrics, buffer, index, count); + } + + // Stages are applied in pause-delimited groups: a stage action is a + // synchronization point, and between two actions every registered + // feature's lookups apply together in lookup-list order, the order the + // specification defines for lookups within a single application pass. A + // lookup registered by several of the group's features applies once with + // their glyph masks combined. + List shapingStages = shaper.GetShapingStages(); + SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, default, 0); + List<(Tag Feature, ushort Index, LookupTable LookupTable, ulong Mask)> merged = buffer.GPosLookupScratch; + + int stageIndex = 0; + while (stageIndex < shapingStages.Count) + { + shapingStages[stageIndex].PreProcessFeature(buffer, index, count); + + // Extend the group while its interior holds no actions: a post action + // closes the group after its stage and a pre action opens a new one. + int groupEnd = stageIndex; + while (true) + { + groupEnd++; + if (shapingStages[groupEnd - 1].HasPostAction || groupEnd >= shapingStages.Count || shapingStages[groupEnd].HasPreAction) { - groupEnd++; - if (shapingStages[groupEnd - 1].HasPostAction || groupEnd >= shapingStages.Count || shapingStages[groupEnd].HasPreAction) - { - break; - } + break; } + } - // Merge the group's lookups into lookup-index order. Insertion keeps - // the scratch sorted; a lookup already present from another feature - // gains that feature's mask instead of a second entry. - merged.Clear(); - for (int s = stageIndex; s < groupEnd; s++) + // Merge the group's lookups into lookup-index order. Insertion keeps + // the scratch sorted; a lookup already present from another feature + // gains that feature's mask instead of a second entry. + merged.Clear(); + for (int s = stageIndex; s < groupEnd; s++) + { + Tag featureTag = shapingStages[s].FeatureTag; + var lookupProbe = ShapingProbe.Enter(); + bool found = this.TryGetFeatureLookups(fontMetrics, in featureTag, script, buffer.LanguageTags, out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? lookups); + ShapingProbe.Exit(ShapingProbe.LookupResolve, lookupProbe); + if (!found || lookups is null) { - Tag featureTag = shapingStages[s].FeatureTag; - var lookupProbe = ShapingProbe.Enter(); - bool found = this.TryGetFeatureLookups(fontMetrics, in featureTag, current, buffer.LanguageTags, out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? lookups); - ShapingProbe.Exit(ShapingProbe.LookupResolve, lookupProbe); - if (!found || lookups is null) - { - continue; - } + continue; + } - ulong mask = buffer.FeatureMap.GetMask(featureTag); - foreach ((Tag Feature, ushort Index, LookupTable LookupTable) featureLookup in lookups) + ulong mask = buffer.FeatureMap.GetMask(featureTag); + foreach ((Tag Feature, ushort Index, LookupTable LookupTable) featureLookup in lookups) + { + int insertAt = merged.Count; + bool alreadyMerged = false; + while (insertAt > 0) { - int insertAt = merged.Count; - bool alreadyMerged = false; - while (insertAt > 0) + (Tag Feature, ushort Index, LookupTable LookupTable, ulong Mask) prior = merged[insertAt - 1]; + if (prior.Index == featureLookup.Index) { - (Tag Feature, ushort Index, LookupTable LookupTable, ulong Mask) prior = merged[insertAt - 1]; - if (prior.Index == featureLookup.Index) - { - merged[insertAt - 1] = (prior.Feature, prior.Index, prior.LookupTable, prior.Mask | mask); - alreadyMerged = true; - break; - } - - if (prior.Index < featureLookup.Index) - { - break; - } - - insertAt--; + merged[insertAt - 1] = (prior.Feature, prior.Index, prior.LookupTable, prior.Mask | mask); + alreadyMerged = true; + break; } - if (!alreadyMerged) + if (prior.Index < featureLookup.Index) { - merged.Insert(insertAt, (featureLookup.Feature, featureLookup.Index, featureLookup.LookupTable, mask)); + break; } + + insertAt--; + } + + if (!alreadyMerged) + { + merged.Insert(insertAt, (featureLookup.Feature, featureLookup.Index, featureLookup.LookupTable, mask)); } } + } - for (int m = 0; m < merged.Count; m++) + for (int m = 0; m < merged.Count; m++) + { + (Tag feature, ushort _, LookupTable featureLookupTable, ulong featureMask) = merged[m]; + + // Skip the whole lookup when its coverage cannot intersect any + // glyph id the buffer has ever contained; most fonts carry + // many lookups for glyphs a given text never produces. + if (!featureLookupTable.Digest.MightIntersect(buffer.GlyphDigest)) { - (Tag feature, ushort _, LookupTable featureLookupTable, ulong featureMask) = merged[m]; + continue; + } + + iterator.Reset(index, featureLookupTable.LookupFlags, featureLookupTable.MarkFilteringSet); + long featureStart = ShapingProbe.Timestamp(); + long featureApplies = 0; - // Skip the whole lookup when its coverage cannot intersect any - // glyph id the buffer has ever contained; most fonts carry - // many lookups for glyphs a given text never produces. - if (!featureLookupTable.Digest.MightIntersect(buffer.GlyphDigest)) + while (iterator.Index < index + count) + { + if (currentOperations++ >= maxOperationsCount) { - continue; + maxOperationsReached = true; + goto EndLookups; } - iterator.Reset(index, featureLookupTable.LookupFlags, featureLookupTable.MarkFilteringSet); - long featureStart = ShapingProbe.Timestamp(); - long featureApplies = 0; - - while (iterator.Index < index + count) + // The digest cheaply rejects glyphs no subtable of this + // lookup can affect; a maybe falls through to the exact + // coverage test inside. + ref GlyphShapingData glyphData = ref buffer[iterator.Index]; + if ((glyphData.FeatureMask & featureMask) == 0 || !featureLookupTable.Digest.MightContain(glyphData.GlyphId)) { - if (currentOperations++ >= maxOperationsCount) - { - maxOperationsReached = true; - goto EndLookups; - } - - // The digest cheaply rejects glyphs no subtable of this - // lookup can affect; a maybe falls through to the exact - // coverage test inside. - ref GlyphShapingData glyphData = ref buffer[iterator.Index]; - if ((glyphData.FeatureMask & featureMask) == 0 || !featureLookupTable.Digest.MightContain(glyphData.GlyphId)) - { - iterator.Next(); - continue; - } - - bool success = featureLookupTable.TryUpdatePosition(fontMetrics, this, buffer, feature, iterator.Index, count - (iterator.Index - index)); - featureApplies++; - kerned |= success && (feature == KernTag || feature == VKernTag); - updated |= success; iterator.Next(); + continue; } - ShapingProbe.ExitFeature("GPOS", feature, featureStart, featureApplies); + bool success = featureLookupTable.TryUpdatePosition(fontMetrics, this, buffer, feature, iterator.Index, count - (iterator.Index - index)); + featureApplies++; + kerned |= success && (feature == KernTag || feature == VKernTag); + updated |= success; + iterator.Next(); } - shapingStages[groupEnd - 1].PostProcessFeature(buffer, index, count); - stageIndex = groupEnd; - } - - EndLookups: - if (shaper.MarkZeroingMode == MarkZeroingMode.PostGpos) - { - ZeroMarkAdvances(fontMetrics, buffer, index, count); + ShapingProbe.ExitFeature("GPOS", feature, featureStart, featureApplies); } - FixCursiveAttachment(buffer, index, count); - FixMarkAttachment(buffer, index, count); - UpdatePositions(buffer, index, count); + shapingStages[groupEnd - 1].PostProcessFeature(buffer, index, count); + stageIndex = groupEnd; + } - if (i >= maxCount || maxOperationsReached) - { - return updated; - } + EndLookups: + if (shaper.MarkZeroingMode == MarkZeroingMode.PostGpos) + { + ZeroMarkAdvances(fontMetrics, buffer, index, count); } + FixCursiveAttachment(buffer, index, count); + FixMarkAttachment(buffer, index, count); + UpdatePositions(buffer, index, count); + return updated; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs index bd03712f0..84806009f 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs @@ -316,6 +316,11 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas stageIndex = groupEnd; } + + // Record the segment with its post-substitution range so the in-place + // positioning pass can reuse the shaper and its plan; one plan then + // drives both tables. + buffer.SegmentShapers.Add((index, count, current, shaper)); } } From ba1884807600262d1289160f4870d5ddca933bbb Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 16:22:34 +1000 Subject: [PATCH 33/94] Skip the lookahead decode for fonts without variation sequences Glyph population decoded the following codepoint for every codepoint so lookup could match Unicode variation sequences, but a font without format 14 cmap subtables can never consume it. The font metrics now report whether variation sequences exist and population skips the lookahead decode entirely when they do not. Gate: 5,577/0 tests. Clean rows across two runs: | Scenario | Mean before | Mean after | Ratio | |------------|-------------|------------|----------------| | Latin | 8.40 us | 8.23-8.35 | 3.67x > 3.54x | | Arabic | 10.13 us | 10.24 us | 2.60x > 2.65x | | Devanagari | 16.32 us | 15.89 us | 1.78x > 1.79x | Allocations byte-identical in all scenarios. --- src/SixLabors.Fonts/FileFontMetrics.cs | 3 +++ src/SixLabors.Fonts/FontMetrics.cs | 7 +++++++ src/SixLabors.Fonts/MemoryFontMetrics.cs | 3 +++ src/SixLabors.Fonts/StreamFontMetrics.cs | 13 +++++++++++++ src/SixLabors.Fonts/Tables/General/CMapTable.cs | 7 +++++++ src/SixLabors.Fonts/TextShaper.Pipeline.cs | 6 +++++- 6 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/SixLabors.Fonts/FileFontMetrics.cs b/src/SixLabors.Fonts/FileFontMetrics.cs index 941df1202..9cb071d19 100644 --- a/src/SixLabors.Fonts/FileFontMetrics.cs +++ b/src/SixLabors.Fonts/FileFontMetrics.cs @@ -79,6 +79,9 @@ private FileFontMetrics(FontDescription description, string path, long offset) /// public override VerticalMetrics VerticalMetrics => this.fontMetrics.Value.VerticalMetrics; + /// + internal override bool HasUnicodeVariationSequences => this.fontMetrics.Value.HasUnicodeVariationSequences; + /// public override short SubscriptXSize => this.fontMetrics.Value.SubscriptXSize; diff --git a/src/SixLabors.Fonts/FontMetrics.cs b/src/SixLabors.Fonts/FontMetrics.cs index 0384222b4..f937c43ed 100644 --- a/src/SixLabors.Fonts/FontMetrics.cs +++ b/src/SixLabors.Fonts/FontMetrics.cs @@ -44,6 +44,13 @@ internal FontMetrics() /// public abstract VerticalMetrics VerticalMetrics { get; } + /// + /// Gets a value indicating whether the font declares Unicode variation sequences. + /// When it does not, glyph lookup never consumes the following codepoint, so + /// shaping can skip decoding it. The conservative default reports support. + /// + internal virtual bool HasUnicodeVariationSequences => true; + /// /// Gets the recommended horizontal size in font design units for subscripts for this font. /// diff --git a/src/SixLabors.Fonts/MemoryFontMetrics.cs b/src/SixLabors.Fonts/MemoryFontMetrics.cs index 055b447bb..e664f6d8a 100644 --- a/src/SixLabors.Fonts/MemoryFontMetrics.cs +++ b/src/SixLabors.Fonts/MemoryFontMetrics.cs @@ -73,6 +73,9 @@ private MemoryFontMetrics(FontDescription description, byte[] data, long offset) /// public override VerticalMetrics VerticalMetrics => this.fontMetrics.Value.VerticalMetrics; + /// + internal override bool HasUnicodeVariationSequences => this.fontMetrics.Value.HasUnicodeVariationSequences; + /// public override short SubscriptXSize => this.fontMetrics.Value.SubscriptXSize; diff --git a/src/SixLabors.Fonts/StreamFontMetrics.cs b/src/SixLabors.Fonts/StreamFontMetrics.cs index 5cad64de1..0c10bd09e 100644 --- a/src/SixLabors.Fonts/StreamFontMetrics.cs +++ b/src/SixLabors.Fonts/StreamFontMetrics.cs @@ -181,6 +181,19 @@ private StreamFontMetrics( /// public override VerticalMetrics VerticalMetrics => this.verticalMetrics; + /// + internal override bool HasUnicodeVariationSequences + { + get + { + CMapTable cmap = this.outlineType == OutlineType.TrueType + ? this.trueTypeFontTables!.Cmap + : this.compactFontTables!.Cmap; + + return cmap.HasVariationSequences; + } + } + /// public override short SubscriptXSize => this.subscriptXSize; diff --git a/src/SixLabors.Fonts/Tables/General/CMapTable.cs b/src/SixLabors.Fonts/Tables/General/CMapTable.cs index 3218afa18..57981314f 100644 --- a/src/SixLabors.Fonts/Tables/General/CMapTable.cs +++ b/src/SixLabors.Fonts/Tables/General/CMapTable.cs @@ -38,6 +38,13 @@ public CMapTable(IEnumerable tables) this.format14SubTables = this.Tables.OfType().ToArray(); } + /// + /// Gets a value indicating whether the font declares Unicode variation sequences. + /// When it does not, glyph lookup never needs the following codepoint, so callers + /// can skip decoding it entirely. + /// + public bool HasVariationSequences => this.format14SubTables.Length > 0; + /// /// Gets the subtables ordered by preferred platform. /// diff --git a/src/SixLabors.Fonts/TextShaper.Pipeline.cs b/src/SixLabors.Fonts/TextShaper.Pipeline.cs index 758da5d00..5564d491b 100644 --- a/src/SixLabors.Fonts/TextShaper.Pipeline.cs +++ b/src/SixLabors.Fonts/TextShaper.Pipeline.cs @@ -545,6 +545,10 @@ private static void PopulateAndSubstitute( var probe = ShapingProbe.Enter(); + // A font without variation sequences never consumes the following codepoint + // during glyph lookup, so the per-codepoint lookahead decode is skipped. + bool hasVariationSequences = font.FontMetrics.HasUnicodeVariationSequences; + // Enumerate through each grapheme in the text. int graphemeIndex = start; SpanGraphemeEnumerator graphemeEnumerator = new(text); @@ -582,7 +586,7 @@ private static void PopulateAndSubstitute( int charsConsumed = 0; CodePoint current = codePointEnumerator.Current; charIndex += current.Utf16SequenceLength; - CodePoint? next = graphemeCodePointIndex < graphemeMax + CodePoint? next = hasVariationSequences && graphemeCodePointIndex < graphemeMax ? CodePoint.DecodeFromUtf16At(grapheme, charIndex, out charsConsumed) : null; From e008dabf5e11d9cc168eb0056941f5645ad327a1 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 16:27:23 +1000 Subject: [PATCH 34/94] Front glyph id lookups with a direct-mapped cache on the buffer Glyph population resolved every codepoint through the font's concurrent dictionary cache. The buffer now fronts that resolver with a 256-slot single-word cache whose entry packs the lookup key, the found and skip flags, and the glyph id, so a repeat lookup is one load and one masked compare. The pooled buffer is exclusively owned per pass, so no synchronization is needed; populating from a different font clears it. Gate: 5,577/0 tests. All rows clean. | Scenario | Mean before | Mean after | Ratio | |------------|-------------|------------|---------------| | Latin | 8.35 us | 8.03 us | 3.62x > 3.47x | | Arabic | 10.24 us | 9.71 us | 2.65x > 2.43x | | Devanagari | 15.89 us | 15.99 us | 1.79x > 1.75x | Allocations byte-identical in all scenarios. --- src/SixLabors.Fonts/ShapingBuffer.cs | 87 ++++++++++++++++++++++ src/SixLabors.Fonts/TextShaper.Pipeline.cs | 2 +- 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/src/SixLabors.Fonts/ShapingBuffer.cs b/src/SixLabors.Fonts/ShapingBuffer.cs index f8af15dab..a9d433716 100644 --- a/src/SixLabors.Fonts/ShapingBuffer.cs +++ b/src/SixLabors.Fonts/ShapingBuffer.cs @@ -68,6 +68,52 @@ internal sealed class ShapingBuffer /// private FontMetrics? metricsCacheOwner; + /// + /// The bit offset of the encoded following codepoint in a glyph id cache entry. + /// + private const int GlyphIdCacheNextShift = 21; + + /// + /// The bit offset of the resolved glyph id in a glyph id cache entry. + /// + private const int GlyphIdCacheGlyphShift = 43; + + /// + /// The glyph id cache entry bit recording that the lookup found a glyph. + /// + private const ulong GlyphIdCacheFoundFlag = 1UL << 59; + + /// + /// The glyph id cache entry bit recording that the following codepoint was + /// consumed as part of a variation sequence. + /// + private const ulong GlyphIdCacheSkipFlag = 1UL << 60; + + /// + /// The glyph id cache entry bit distinguishing a populated slot from an empty + /// one, since a zero entry could otherwise read as a valid all-zero lookup. + /// + private const ulong GlyphIdCacheMarkerFlag = 1UL << 63; + + /// + /// The glyph id cache entry bits forming the lookup key: the marker, the + /// codepoint, and the encoded following codepoint. + /// + private const ulong GlyphIdCacheTagMask = GlyphIdCacheMarkerFlag | ((1UL << GlyphIdCacheGlyphShift) - 1); + + /// + /// Direct-mapped codepoint-to-glyph cache: one word per slot packing the lookup + /// key alongside the found flag, skip flag, and glyph id, so a repeat lookup is + /// one load and one masked compare. Zero marks a slot empty. + /// + private readonly ulong[] glyphIdCacheEntries = new ulong[256]; + + /// + /// The font metrics instance the glyph id cache entries belong to. Populating + /// from a different font clears the cache before use. + /// + private FontMetrics? glyphIdCacheOwner; + /// /// The bidi runs recorded for inline placeholders, keyed by codepoint offset. /// Placeholder state lives here rather than on every glyph record because only @@ -921,6 +967,47 @@ public bool TryUpdate(Font font, ShapingBuffer workspace) return !hasFallBacks; } + /// + /// Resolves a glyph id through a direct-mapped cache in front of the font's own + /// resolver, which hashes a dictionary per lookup. A hit is one load and one + /// masked compare. No synchronization is needed: a pooled buffer is exclusively + /// owned for the duration of a shaping pass. + /// + /// The font metrics to resolve against. + /// The codepoint to look up. + /// The optional following codepoint for variation sequence matching. + /// When this method returns, contains the glyph id if found. + /// When this method returns, indicates whether the following codepoint was consumed. + /// if a glyph was found. + public bool TryGetGlyphIdCached(FontMetrics fontMetrics, CodePoint codePoint, CodePoint? nextCodePoint, out ushort glyphId, out bool skipNextCodePoint) + { + if (!ReferenceEquals(this.glyphIdCacheOwner, fontMetrics)) + { + Array.Clear(this.glyphIdCacheEntries); + this.glyphIdCacheOwner = fontMetrics; + } + + ulong tag = GlyphIdCacheMarkerFlag + | (uint)codePoint.Value + | ((ulong)(uint)((nextCodePoint?.Value + 1) ?? 0) << GlyphIdCacheNextShift); + + int slot = codePoint.Value & 0xFF; + ulong entry = this.glyphIdCacheEntries[slot]; + if ((entry & GlyphIdCacheTagMask) == tag) + { + glyphId = (ushort)(entry >> GlyphIdCacheGlyphShift); + skipNextCodePoint = (entry & GlyphIdCacheSkipFlag) != 0; + return (entry & GlyphIdCacheFoundFlag) != 0; + } + + bool found = fontMetrics.TryGetGlyphId(codePoint, nextCodePoint, out glyphId, out skipNextCodePoint); + this.glyphIdCacheEntries[slot] = tag + | ((ulong)glyphId << GlyphIdCacheGlyphShift) + | (found ? GlyphIdCacheFoundFlag : 0) + | (skipNextCodePoint ? GlyphIdCacheSkipFlag : 0); + return found; + } + /// /// Resolves glyph metrics through a direct-mapped cache in front of the font's own /// resolver. The tag packs the same key fields the font's cache hashes, so a hit diff --git a/src/SixLabors.Fonts/TextShaper.Pipeline.cs b/src/SixLabors.Fonts/TextShaper.Pipeline.cs index 5564d491b..a00748e50 100644 --- a/src/SixLabors.Fonts/TextShaper.Pipeline.cs +++ b/src/SixLabors.Fonts/TextShaper.Pipeline.cs @@ -593,7 +593,7 @@ private static void PopulateAndSubstitute( charIndex += charsConsumed; // Get the glyph id for the codepoint and add to the buffer. - bool hasGlyph = font.FontMetrics.TryGetGlyphId(current, next, out ushort glyphId, out skipNextCodePoint); + bool hasGlyph = substitutions.TryGetGlyphIdCached(font.FontMetrics, current, next, out ushort glyphId, out skipNextCodePoint); // Unsupported default-ignorable code points such as FE0F should not block // GSUB sequences like emoji ZWJ ligatures. Preserve joiners explicitly. From 18cb44fdaaf7345634b047eb075cc1e2ead2b876 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 16:50:53 +1000 Subject: [PATCH 35/94] Split shaping state into info and position streams Move Bounds, the mark and cursive attachment links, and the positioned and kerned flags out of GlyphShapingData into a GlyphShapingPosition stream stored parallel to the glyph records. Substitution walks the narrowed record; positioning reads the record and writes the parallel stream, which is seeded alongside the metrics stream once substitution has settled. | Scenario | Before | After | |------------|------------------|------------------| | Latin | 8.03 us / 3.47x | 7.50 us / 3.23x | | Arabic | 9.71 us / 2.43x | 9.46 us / 2.40x | | Devanagari | 15.99 us / 1.75x | 15.26 us / 1.71x | Allocations unchanged (7,776 / 6,480 / 8,496 B). Tests 5,579/0. --- src/SixLabors.Fonts/GlyphShapingBounds.cs | 4 +- src/SixLabors.Fonts/GlyphShapingData.cs | 49 ----------- src/SixLabors.Fonts/GlyphShapingPosition.cs | 79 +++++++++++++++++ src/SixLabors.Fonts/ShapingBuffer.cs | 84 +++++++++++-------- .../AdvancedTypographicUtils.cs | 26 +++--- .../GPos/LookupType3SubTable.cs | 43 +++++----- .../Tables/AdvancedTypographic/GPosTable.cs | 39 ++++----- .../Tables/General/Kern/KerningTable.cs | 8 +- src/SixLabors.Fonts/TextShaper.Pipeline.cs | 7 +- 9 files changed, 196 insertions(+), 143 deletions(-) create mode 100644 src/SixLabors.Fonts/GlyphShapingPosition.cs diff --git a/src/SixLabors.Fonts/GlyphShapingBounds.cs b/src/SixLabors.Fonts/GlyphShapingBounds.cs index 9e4497840..d80352330 100644 --- a/src/SixLabors.Fonts/GlyphShapingBounds.cs +++ b/src/SixLabors.Fonts/GlyphShapingBounds.cs @@ -7,8 +7,8 @@ namespace SixLabors.Fonts; /// /// Represents the shaped bounds of a glyph. A mutable struct embedded in -/// and accessed by reference through -/// : positioning lookups accumulate deltas into +/// and accessed by reference through +/// : positioning lookups accumulate deltas into /// the fields in place, and re-seeding is plain value assignment with no allocation. /// [DebuggerDisplay("{DebuggerDisplay,nq}")] diff --git a/src/SixLabors.Fonts/GlyphShapingData.cs b/src/SixLabors.Fonts/GlyphShapingData.cs index ea9298993..4fd81a37e 100644 --- a/src/SixLabors.Fonts/GlyphShapingData.cs +++ b/src/SixLabors.Fonts/GlyphShapingData.cs @@ -17,12 +17,6 @@ namespace SixLabors.Fonts; internal struct GlyphShapingData { #pragma warning disable SA1401 // Fields exposed so shaping mutates embedded values in place. - /// - /// The shaping bounds. A field rather than a property so positioning lookups - /// mutate the embedded value in place and re-seeding is plain value assignment. - /// - public GlyphShapingBounds Bounds; - /// /// The syllable classification assigned by the complex-script shapers, stored by /// value so classification never allocates. A of @@ -51,16 +45,6 @@ internal struct GlyphShapingData /// private const ushort PlaceholderFlag = 1 << 3; - /// - /// The bit recording . - /// - private const ushort PositionedFlag = 1 << 4; - - /// - /// The bit recording . - /// - private const ushort KernedFlag = 1 << 5; - /// /// The bit recording that /// holds the glyph id was computed for. A default @@ -110,13 +94,9 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) this.LigatureId = data.LigatureId; this.IsLigated = data.IsLigated; this.LigatureComponent = data.LigatureComponent; - this.MarkAttachment = data.MarkAttachment; - this.CursiveAttachment = data.CursiveAttachment; this.IsSubstituted = data.IsSubstituted; this.IsDecomposed = data.IsDecomposed; this.IsPlaceholder = data.IsPlaceholder; - this.IsPositioned = data.IsPositioned; - this.IsKerned = data.IsKerned; this.Syllable = data.Syllable; @@ -128,7 +108,6 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) this.AppliedFeatureMask = data.AppliedFeatureMask; - this.Bounds = data.Bounds; this.CachedShapingClass = data.CachedShapingClass; this.ShapingClassCacheKey = data.ShapingClassCacheKey; } @@ -226,16 +205,6 @@ public bool IsLigated /// public int LigatureComponent { get; set; } = -1; - /// - /// Gets or sets the index of any mark attachment. - /// - public int MarkAttachment { get; set; } = -1; - - /// - /// Gets or sets the index of any cursive attachment. - /// - public int CursiveAttachment { get; set; } = -1; - /// /// Gets or sets the mask of features a shaper has registered for this glyph, enabled /// or not. Bits are assigned by the shaping pass's . @@ -288,24 +257,6 @@ public bool IsPlaceholder set => this.flags = value ? (ushort)(this.flags | PlaceholderFlag) : (ushort)(this.flags & ~PlaceholderFlag); } - /// - /// Gets or sets a value indicating whether this glyph has been positioned. - /// - public bool IsPositioned - { - readonly get => (this.flags & PositionedFlag) != 0; - set => this.flags = value ? (ushort)(this.flags | PositionedFlag) : (ushort)(this.flags & ~PositionedFlag); - } - - /// - /// Gets or sets a value indicating whether this glyph has been kerned. - /// - public bool IsKerned - { - readonly get => (this.flags & KernedFlag) != 0; - set => this.flags = value ? (ushort)(this.flags | KernedFlag) : (ushort)(this.flags & ~KernedFlag); - } - private string DebuggerDisplay => FormattableString .Invariant($" {this.GlyphId} : {this.CodePoint.ToDebuggerDisplay()} : {CodePoint.GetScriptClass(this.CodePoint)} : {this.Direction} : run {this.TextRunIndex} : {this.LigatureId} : {this.LigatureComponent} : {this.IsDecomposed}"); diff --git a/src/SixLabors.Fonts/GlyphShapingPosition.cs b/src/SixLabors.Fonts/GlyphShapingPosition.cs new file mode 100644 index 000000000..f406b801f --- /dev/null +++ b/src/SixLabors.Fonts/GlyphShapingPosition.cs @@ -0,0 +1,79 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts; + +/// +/// One glyph's positioning-phase state, stored in a stream parallel to the glyph +/// records: the shaping bounds that accumulate placement and advance adjustments, +/// the attachment links, and the positioned and kerned marks. Entries are seeded +/// alongside the metrics stream after substitution; structural buffer operations +/// before seeding need not preserve alignment, matching the metrics stream. +/// +internal struct GlyphShapingPosition +{ + /// + /// The bit recording . + /// + private const byte PositionedFlag = 1 << 0; + + /// + /// The bit recording . + /// + private const byte KernedFlag = 1 << 1; + +#pragma warning disable SA1401 // Fields exposed so positioning mutates embedded values in place. + /// + /// The shaping bounds. A field rather than a property so positioning lookups + /// mutate the embedded value in place and re-seeding is plain value assignment. + /// + public GlyphShapingBounds Bounds; + + /// + /// The index of any mark attachment, or -1 when unattached. + /// + public int MarkAttachment; + + /// + /// The offset of any cursive attachment, or -1 when unattached. + /// + public int CursiveAttachment; +#pragma warning restore SA1401 + + /// + /// Packed boolean positioning state addressed through the named flag constants + /// above. + /// + private byte flags; + + /// + /// Initializes a new instance of the struct + /// in its seeded state: unattached, unpositioned, with the given bounds. + /// + /// The seeded shaping bounds. + public GlyphShapingPosition(GlyphShapingBounds bounds) + { + this.Bounds = bounds; + this.MarkAttachment = -1; + this.CursiveAttachment = -1; + this.flags = 0; + } + + /// + /// Gets or sets a value indicating whether this glyph has been positioned. + /// + public bool IsPositioned + { + readonly get => (this.flags & PositionedFlag) != 0; + set => this.flags = value ? (byte)(this.flags | PositionedFlag) : (byte)(this.flags & ~PositionedFlag); + } + + /// + /// Gets or sets a value indicating whether this glyph has been kerned. + /// + public bool IsKerned + { + readonly get => (this.flags & KernedFlag) != 0; + set => this.flags = value ? (byte)(this.flags | KernedFlag) : (byte)(this.flags & ~KernedFlag); + } +} diff --git a/src/SixLabors.Fonts/ShapingBuffer.cs b/src/SixLabors.Fonts/ShapingBuffer.cs index a9d433716..cd89f9b17 100644 --- a/src/SixLabors.Fonts/ShapingBuffer.cs +++ b/src/SixLabors.Fonts/ShapingBuffer.cs @@ -39,6 +39,14 @@ internal sealed class ShapingBuffer /// private GlyphMetricsEntry[] metrics = new GlyphMetricsEntry[64]; + /// + /// The positioning stream, parallel to : shaping bounds, + /// attachment links, and positioned and kerned marks. Seeded alongside the + /// metrics stream; keeping this state out of the glyph record keeps the record + /// narrow for the substitution walks that never touch it. + /// + private GlyphShapingPosition[] positions = new GlyphShapingPosition[64]; + /// /// The live record count. /// @@ -230,6 +238,15 @@ public ref GlyphShapingData this[int index] [MethodImpl(MethodImplOptions.AggressiveInlining)] public ref GlyphMetricsEntry MetricsAt(int index) => ref this.metrics[index]; + /// + /// Gets an interior reference to the positioning entry at the specified index. + /// Valid only after the buffer has been seeded. + /// + /// The zero-based index of the entry to get. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ref GlyphShapingPosition PositionAt(int index) => ref this.positions[index]; + /// /// Assigns the text runs for the pass. Must run on both of a pass's buffers /// before any glyph is added, so record run indices resolve identically across @@ -290,9 +307,9 @@ public bool SeedMetricsInPlace(Font font) // Feature masks persist deliberately: the in-place positioning pass // reuses the substitution pass's plan, whose registrations already cover // the positioning features. - slot.Bounds = isVertical + this.positions[i] = new(isVertical ? new(0, 0, 0, glyphMetrics.AdvanceHeight) - : new(0, 0, glyphMetrics.AdvanceWidth, 0); + : new(0, 0, glyphMetrics.AdvanceWidth, 0)); this.metrics[i] = new(font, font.Size, glyphMetrics); } @@ -641,8 +658,6 @@ public void Replace(int index, ushort glyphId, Tag feature) current.GlyphId = glyphId; current.LigatureId = 0; current.LigatureComponent = -1; - current.MarkAttachment = -1; - current.CursiveAttachment = -1; current.IsSubstituted = true; current.AppliedFeatureMask |= this.FeatureMap.GetOrAddMask(feature); } @@ -691,8 +706,6 @@ public void Replace(int index, ReadOnlySpan removalIndices, ushort glyphId, current.LigatureId = ligatureId; current.IsLigated = true; current.LigatureComponent = -1; - current.MarkAttachment = -1; - current.CursiveAttachment = -1; current.IsSubstituted = true; current.AppliedFeatureMask |= this.FeatureMap.GetOrAddMask(feature); } @@ -739,8 +752,6 @@ public void Replace(int index, int count, ushort glyphId, Tag feature) current.GlyphId = glyphId; current.LigatureId = 0; current.LigatureComponent = -1; - current.MarkAttachment = -1; - current.CursiveAttachment = -1; current.IsSubstituted = true; current.AppliedFeatureMask |= this.FeatureMap.GetOrAddMask(feature); } @@ -758,8 +769,6 @@ public void Replace(int index, ReadOnlySpan glyphIds, Tag feature) this.glyphDigest.Add(glyphIds[0]); this.data[index].GlyphId = glyphIds[0]; this.data[index].LigatureComponent = 0; - this.data[index].MarkAttachment = -1; - this.data[index].CursiveAttachment = -1; this.data[index].IsSubstituted = true; this.data[index].IsDecomposed = true; @@ -842,10 +851,12 @@ public bool TryAdd(Font font, ShapingBuffer workspace) ref GlyphShapingData placeholderSlot = ref this.Append(); placeholderSlot = source; placeholderSlot.ClearFeatures(); - placeholderSlot.Bounds = layoutMode.IsVertical() + this.positions[this.count - 1] = new(layoutMode.IsVertical() ? new(0, 0, 0, placeholderMetrics.AdvanceHeight) - : new(0, 0, placeholderMetrics.AdvanceWidth, 0); - placeholderSlot.IsPositioned = true; + : new(0, 0, placeholderMetrics.AdvanceWidth, 0)) + { + IsPositioned = true, + }; this.metrics[this.count - 1] = new(font, font.Size, placeholderMetrics); continue; @@ -871,9 +882,9 @@ public bool TryAdd(Font font, ShapingBuffer workspace) ref GlyphShapingData slot = ref this.Append(); slot = source; slot.ClearFeatures(); - slot.Bounds = isVertical + this.positions[this.count - 1] = new(isVertical ? new(0, 0, 0, glyphMetrics.AdvanceHeight) - : new(0, 0, glyphMetrics.AdvanceWidth, 0); + : new(0, 0, glyphMetrics.AdvanceWidth, 0)); this.metrics[this.count - 1] = new(font, font.Size, glyphMetrics); } @@ -948,12 +959,12 @@ public bool TryUpdate(Font font, ShapingBuffer workspace) // correctly increment our position. shape.CodePointIndex = offset; shape.ClearFeatures(); - shape.Bounds = isVertical - ? new(0, 0, 0, glyphMetrics.AdvanceHeight) - : new(0, 0, glyphMetrics.AdvanceWidth, 0); this.glyphDigest.Add(glyphMetrics.GlyphId); this.InsertAt(i + replacementCount, shape, new(font, pointSize, glyphMetrics)); + this.positions[i + replacementCount] = new(isVertical + ? new(0, 0, 0, glyphMetrics.AdvanceHeight) + : new(0, 0, glyphMetrics.AdvanceWidth, 0)); replacementCount++; } @@ -1059,16 +1070,16 @@ private FontGlyphMetrics GetGlyphMetricsCached( /// /// Marks the glyph at the specified index as positioned. Positions accumulate in - /// the record's shaping bounds and are read from there by consumers, so the shared - /// metrics instance is never mutated. + /// the position entry's shaping bounds and are read from there by consumers, so + /// the shared metrics instance is never mutated. /// /// The zero-based index of the record. - public void UpdatePosition(int index) => this.data[index].IsPositioned = true; + public void UpdatePosition(int index) => this.positions[index].IsPositioned = true; /// /// Adds dx and dy to the positioned advance of the glyph at the given index and id. - /// Advances accumulate in the record's shaping bounds so the shared metrics - /// instance is never mutated. + /// Advances accumulate in the position entry's shaping bounds so the shared + /// metrics instance is never mutated. /// /// The font face with metrics. /// The zero-based index of the record. @@ -1087,10 +1098,10 @@ public void Advance(FontMetrics fontMetrics, int index, ushort glyphId, short dx || (this.data[index].AppliedFeatureMask & this.GetVerticalFeatureMask()) != 0; // Advance heights grow downward but font-space grows upward, hence the negation. - this.data[index].Bounds.Width += dx; + this.positions[index].Bounds.Width += dx; if (isVertical) { - this.data[index].Bounds.Height -= dy; + this.positions[index].Bounds.Height -= dy; } } @@ -1102,7 +1113,7 @@ public void Advance(FontMetrics fontMetrics, int index, ushort glyphId, short dx /// The zero-based index of the record. /// if the record should be processed. public bool ShouldProcess(FontMetrics fontMetrics, int index) - => !this.data[index].IsPositioned && this.metrics[index].Metrics.FontMetrics == fontMetrics; + => !this.positions[index].IsPositioned && this.metrics[index].Metrics.FontMetrics == fontMetrics; /// /// Gets the combined mask of the three vertical alternate features. Computed from @@ -1140,6 +1151,7 @@ private ref GlyphShapingData Append() { Array.Resize(ref this.data, this.data.Length * 2); Array.Resize(ref this.metrics, this.metrics.Length * 2); + Array.Resize(ref this.positions, this.positions.Length * 2); } return ref this.data[this.count++]; @@ -1168,12 +1180,15 @@ private void InsertAt(int index, GlyphShapingData item, GlyphMetricsEntry metric { Array.Resize(ref this.data, this.data.Length * 2); Array.Resize(ref this.metrics, this.metrics.Length * 2); + Array.Resize(ref this.positions, this.positions.Length * 2); } Array.Copy(this.data, index, this.data, index + 1, this.count - index); Array.Copy(this.metrics, index, this.metrics, index + 1, this.count - index); + Array.Copy(this.positions, index, this.positions, index + 1, this.count - index); this.data[index] = item; this.metrics[index] = metricsEntry; + this.positions[index] = default; this.count++; } @@ -1186,6 +1201,7 @@ private void RemoveAt(int index) { Array.Copy(this.data, index + 1, this.data, index, this.count - index - 1); Array.Copy(this.metrics, index + 1, this.metrics, index, this.count - index - 1); + Array.Copy(this.positions, index + 1, this.positions, index, this.count - index - 1); this.count--; } @@ -1226,23 +1242,23 @@ public GlyphMetricsEntry(Font font, float pointSize, FontGlyphMetrics metrics) /// /// Gets the positioned horizontal advance in font design units for the paired - /// record: the shaping bounds value once positioning has written one, otherwise + /// entry: the shaping bounds value once positioning has written one, otherwise /// the metrics advance. /// - /// The paired glyph record. + /// The paired positioning entry. /// The advance. - public readonly ushort GetAdvanceWidth(in GlyphShapingData data) - => data.Bounds.IsDirtyWH ? (ushort)data.Bounds.Width : this.Metrics.AdvanceWidth; + public readonly ushort GetAdvanceWidth(in GlyphShapingPosition position) + => position.Bounds.IsDirtyWH ? (ushort)position.Bounds.Width : this.Metrics.AdvanceWidth; /// /// Gets the positioned vertical advance in font design units for the paired - /// record: the shaping bounds value once positioning has written one, otherwise + /// entry: the shaping bounds value once positioning has written one, otherwise /// the metrics advance. /// - /// The paired glyph record. + /// The paired positioning entry. /// The advance. - public readonly ushort GetAdvanceHeight(in GlyphShapingData data) - => data.Bounds.IsDirtyWH ? (ushort)data.Bounds.Height : this.Metrics.AdvanceHeight; + public readonly ushort GetAdvanceHeight(in GlyphShapingPosition position) + => position.Bounds.IsDirtyWH ? (ushort)position.Bounds.Height : this.Metrics.AdvanceHeight; } #pragma warning restore SA1401 } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs index c8f885b5d..4ef775840 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs @@ -474,9 +474,10 @@ public static void ApplyAnchor( ref GlyphShapingData markData = ref buffer[index]; AnchorXY markXY = markRecord.MarkAnchorTable.GetAnchor(fontMetrics, ref markData, buffer); - markData.Bounds.X = baseXY.XCoordinate - markXY.XCoordinate; - markData.Bounds.Y = baseXY.YCoordinate - markXY.YCoordinate; - markData.MarkAttachment = baseGlyphIndex; + ref GlyphShapingPosition markPosition = ref buffer.PositionAt(index); + markPosition.Bounds.X = baseXY.XCoordinate - markXY.XCoordinate; + markPosition.Bounds.Y = baseXY.YCoordinate - markXY.YCoordinate; + markPosition.MarkAttachment = baseGlyphIndex; markData.AppliedFeatureMask |= buffer.FeatureMap.GetOrAddMask(feature); } @@ -495,21 +496,22 @@ public static void ApplyPosition( ValueRecord record, Tag feature) { - ref GlyphShapingData current = ref buffer[index]; - current.Bounds.Width += record.XAdvance; - current.Bounds.Height += record.YAdvance; - current.Bounds.X += record.XPlacement; - current.Bounds.Y += record.YPlacement; + ref GlyphShapingPosition position = ref buffer.PositionAt(index); + position.Bounds.Width += record.XAdvance; + position.Bounds.Height += record.YAdvance; + position.Bounds.X += record.XPlacement; + position.Bounds.Y += record.YPlacement; // Apply variation deltas from VariationIndex tables (variable fonts). if (record.HasVariation) { - current.Bounds.X += (short)MathF.Round(fontMetrics.GetGDefVariationDelta(record.XPlacementVariation)); - current.Bounds.Y += (short)MathF.Round(fontMetrics.GetGDefVariationDelta(record.YPlacementVariation)); - current.Bounds.Width += (short)MathF.Round(fontMetrics.GetGDefVariationDelta(record.XAdvanceVariation)); - current.Bounds.Height += (short)MathF.Round(fontMetrics.GetGDefVariationDelta(record.YAdvanceVariation)); + position.Bounds.X += (short)MathF.Round(fontMetrics.GetGDefVariationDelta(record.XPlacementVariation)); + position.Bounds.Y += (short)MathF.Round(fontMetrics.GetGDefVariationDelta(record.YPlacementVariation)); + position.Bounds.Width += (short)MathF.Round(fontMetrics.GetGDefVariationDelta(record.XAdvanceVariation)); + position.Bounds.Height += (short)MathF.Round(fontMetrics.GetGDefVariationDelta(record.YAdvanceVariation)); } + ref GlyphShapingData current = ref buffer[index]; current.AppliedFeatureMask |= buffer.FeatureMap.GetOrAddMask(feature); } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType3SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType3SubTable.cs index 6754b8303..7339c939a 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType3SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType3SubTable.cs @@ -165,35 +165,38 @@ public override bool TryUpdatePosition( AnchorXY exitXY = exit.GetAnchor(fontMetrics, ref current, buffer); AnchorXY entryXY = entry.GetAnchor(fontMetrics, ref next, buffer); + ref GlyphShapingPosition currentPosition = ref buffer.PositionAt(index); + ref GlyphShapingPosition nextPosition = ref buffer.PositionAt(nextIndex); + bool isVerticalLayout = AdvancedTypographicUtils.IsVerticalGlyph(current.CodePoint, buffer.TextOptions.LayoutMode); if (!isVerticalLayout) { // Horizontal if (current.Direction == TextDirection.LeftToRight) { - current.Bounds.Width = exitXY.XCoordinate + current.Bounds.X; + currentPosition.Bounds.Width = exitXY.XCoordinate + currentPosition.Bounds.X; - int delta = entryXY.XCoordinate + next.Bounds.X; - next.Bounds.Width -= delta; - next.Bounds.X -= delta; + int delta = entryXY.XCoordinate + nextPosition.Bounds.X; + nextPosition.Bounds.Width -= delta; + nextPosition.Bounds.X -= delta; } else { - int delta = exitXY.XCoordinate + current.Bounds.X; - current.Bounds.Width -= delta; - current.Bounds.X -= delta; + int delta = exitXY.XCoordinate + currentPosition.Bounds.X; + currentPosition.Bounds.Width -= delta; + currentPosition.Bounds.X -= delta; - next.Bounds.Width = entryXY.XCoordinate + next.Bounds.X; + nextPosition.Bounds.Width = entryXY.XCoordinate + nextPosition.Bounds.X; } } else { // Vertical layout modes advance top-to-bottom; column progression is handled by layout. - current.Bounds.Height = exitXY.YCoordinate + current.Bounds.Y; + currentPosition.Bounds.Height = exitXY.YCoordinate + currentPosition.Bounds.Y; - int delta = entryXY.YCoordinate + next.Bounds.Y; - next.Bounds.Height -= delta; - next.Bounds.Y -= delta; + int delta = entryXY.YCoordinate + nextPosition.Bounds.Y; + nextPosition.Bounds.Height -= delta; + nextPosition.Bounds.Y -= delta; } int child = index; @@ -215,7 +218,7 @@ public override bool TryUpdatePosition( bool horizontal = !isVerticalLayout; ReverseCursiveMinorOffset(buffer, index, child, horizontal, parent); - ref GlyphShapingData c = ref buffer[child]; + ref GlyphShapingPosition c = ref buffer.PositionAt(child); c.CursiveAttachment = parent - child; if (horizontal) { @@ -226,15 +229,15 @@ public override bool TryUpdatePosition( c.Bounds.X = xOffset; } - // If parent was attached to child, separate them. - // https://github.com/harfbuzz/harfbuzz/issues/2469 - ref GlyphShapingData p = ref buffer[parent]; + // If parent was attached to child, separate them so the attachment + // graph stays acyclic. + ref GlyphShapingPosition p = ref buffer.PositionAt(parent); if (p.CursiveAttachment == -c.CursiveAttachment) { p.CursiveAttachment = 0; - // Bounds.X/Y carry shaping placement offsets here, matching - // HarfBuzz x_offset/y_offset. Clear only the detached parent's minor axis. + // Bounds.X/Y carry shaping placement offsets here. Clear only the + // detached parent's minor axis. if (horizontal) { p.Bounds.Y = 0; @@ -264,7 +267,7 @@ private static void ReverseCursiveMinorOffset( bool horizontal, int parent) { - ref GlyphShapingData c = ref buffer[i]; + ref GlyphShapingPosition c = ref buffer.PositionAt(i); int chain = c.CursiveAttachment; if (chain <= 0) { @@ -283,7 +286,7 @@ private static void ReverseCursiveMinorOffset( ReverseCursiveMinorOffset(buffer, position, j, horizontal, parent); - ref GlyphShapingData p = ref buffer[j]; + ref GlyphShapingPosition p = ref buffer.PositionAt(j); if (horizontal) { p.Bounds.Y = -c.Bounds.Y; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs index 158f5c4dc..924eb6f6e 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs @@ -712,23 +712,23 @@ private static void FixCursiveAttachment(ShapingBuffer buffer, int index, int co for (int i = 0; i < count; i++) { int currentIndex = i + index; - ref GlyphShapingData data = ref buffer[currentIndex]; - if (data.CursiveAttachment != -1) + ref GlyphShapingPosition position = ref buffer.PositionAt(currentIndex); + if (position.CursiveAttachment != -1) { - int j = data.CursiveAttachment + currentIndex; + int j = position.CursiveAttachment + currentIndex; if (j < index || j >= index + count) { return; } - ref GlyphShapingData cursiveData = ref buffer[j]; - if (!AdvancedTypographicUtils.IsVerticalGlyph(data.CodePoint, layoutMode)) + ref GlyphShapingPosition cursivePosition = ref buffer.PositionAt(j); + if (!AdvancedTypographicUtils.IsVerticalGlyph(buffer[currentIndex].CodePoint, layoutMode)) { - data.Bounds.Y += cursiveData.Bounds.Y; + position.Bounds.Y += cursivePosition.Bounds.Y; } else { - data.Bounds.X += cursiveData.Bounds.X; + position.Bounds.X += cursivePosition.Bounds.X; } } } @@ -745,27 +745,27 @@ private static void FixMarkAttachment(ShapingBuffer buffer, int index, int count for (int i = 0; i < count; i++) { int currentIndex = i + index; - ref GlyphShapingData data = ref buffer[currentIndex]; - if (data.MarkAttachment != -1) + ref GlyphShapingPosition position = ref buffer.PositionAt(currentIndex); + if (position.MarkAttachment != -1) { - int j = data.MarkAttachment; - data.Bounds.X += buffer[j].Bounds.X; - data.Bounds.Y += buffer[j].Bounds.Y; + int j = position.MarkAttachment; + position.Bounds.X += buffer.PositionAt(j).Bounds.X; + position.Bounds.Y += buffer.PositionAt(j).Bounds.Y; - if (data.Direction == TextDirection.LeftToRight) + if (buffer[currentIndex].Direction == TextDirection.LeftToRight) { for (int k = j; k < currentIndex; k++) { - data.Bounds.X -= buffer[k].Bounds.Width; - data.Bounds.Y -= buffer[k].Bounds.Height; + position.Bounds.X -= buffer.PositionAt(k).Bounds.Width; + position.Bounds.Y -= buffer.PositionAt(k).Bounds.Height; } } else { for (int k = j + 1; k < currentIndex + 1; k++) { - data.Bounds.X += buffer[k].Bounds.Width; - data.Bounds.Y += buffer[k].Bounds.Height; + position.Bounds.X += buffer.PositionAt(k).Bounds.Width; + position.Bounds.Y += buffer.PositionAt(k).Bounds.Height; } } } @@ -787,8 +787,9 @@ private static void ZeroMarkAdvances(FontMetrics fontMetrics, ShapingBuffer buff ref GlyphShapingData data = ref buffer[currentIndex]; if (AdvancedTypographicUtils.IsMarkGlyph(fontMetrics, data.GlyphId, ref data)) { - data.Bounds.Width = 0; - data.Bounds.Height = 0; + ref GlyphShapingPosition position = ref buffer.PositionAt(currentIndex); + position.Bounds.Width = 0; + position.Bounds.Height = 0; } } } diff --git a/src/SixLabors.Fonts/Tables/General/Kern/KerningTable.cs b/src/SixLabors.Fonts/Tables/General/Kern/KerningTable.cs index 894b4b708..50ac3deb1 100644 --- a/src/SixLabors.Fonts/Tables/General/Kern/KerningTable.cs +++ b/src/SixLabors.Fonts/Tables/General/Kern/KerningTable.cs @@ -99,20 +99,20 @@ public void UpdatePositions(FontMetrics fontMetrics, ShapingBuffer buffer, int l return; } - ref GlyphShapingData current = ref buffer[left]; - if (current.IsKerned) + ref GlyphShapingPosition currentPosition = ref buffer.PositionAt(left); + if (currentPosition.IsKerned) { // Already kerned via previous processing. return; } - ushort currentId = current.GlyphId; + ushort currentId = buffer[left].GlyphId; ushort nextId = buffer[right].GlyphId; if (this.TryGetKerningOffset(currentId, nextId, out Vector2 result)) { buffer.Advance(fontMetrics, left, currentId, (short)result.X, (short)result.Y); - current.IsKerned = true; + currentPosition.IsKerned = true; } } diff --git a/src/SixLabors.Fonts/TextShaper.Pipeline.cs b/src/SixLabors.Fonts/TextShaper.Pipeline.cs index a00748e50..4634c0f42 100644 --- a/src/SixLabors.Fonts/TextShaper.Pipeline.cs +++ b/src/SixLabors.Fonts/TextShaper.Pipeline.cs @@ -400,6 +400,7 @@ or BidiCharacterType.FirstStrongIsolate { ref GlyphShapingData shaping = ref shaped[i]; ref ShapingBuffer.GlyphMetricsEntry entry = ref shaped.MetricsAt(i); + ref GlyphShapingPosition shapingPosition = ref shaped.PositionAt(i); // Placeholders carry a bidi run of their own, so they always cut a run. BidiRun shapingBidiRun = shaping.IsPlaceholder @@ -445,9 +446,9 @@ or BidiCharacterType.FirstStrongIsolate flags); positions[i] = new( - entry.GetAdvanceWidth(in shaping), - entry.GetAdvanceHeight(in shaping), - new Vector2(shaping.Bounds.X, shaping.Bounds.Y), + entry.GetAdvanceWidth(in shapingPosition), + entry.GetAdvanceHeight(in shapingPosition), + new Vector2(shapingPosition.Bounds.X, shapingPosition.Bounds.Y), entry.Metrics.Offset); } From 9bbb31a8f7448f6215c187b2fc2f7acdaec3329c Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 17:08:27 +1000 Subject: [PATCH 36/94] Narrow the shaping record to one cache line Fold the ligature id and component into byte lanes behind their existing int properties, and reduce GlyphShapingClass to its packed Props word with the class flags derived from it. GlyphShapingData drops from 72 to 64 bytes, so substitution walks touch exactly one cache line per record. | Scenario | Before | After | |------------|------------------|------------------| | Latin | 7.50 us / 3.23x | 7.57 us / 3.34x | | Arabic | 9.46 us / 2.40x | 9.30 us / 2.40x | | Devanagari | 15.26 us / 1.71x | 15.37 us / 1.73x | All movement within run-to-run spread; kept as a record-narrowing structure match. Allocations unchanged (7,776 / 6,480 / 8,496 B). Tests 5,579/0. --- src/SixLabors.Fonts/GlyphShapingClass.cs | 37 +++++++++++++-------- src/SixLabors.Fonts/GlyphShapingData.cs | 41 +++++++++++++++++++++--- 2 files changed, 60 insertions(+), 18 deletions(-) diff --git a/src/SixLabors.Fonts/GlyphShapingClass.cs b/src/SixLabors.Fonts/GlyphShapingClass.cs index ab04c6d08..113c661c9 100644 --- a/src/SixLabors.Fonts/GlyphShapingClass.cs +++ b/src/SixLabors.Fonts/GlyphShapingClass.cs @@ -20,30 +20,39 @@ internal readonly struct GlyphShapingClass /// public const ushort MarkProp = 4; + /// + /// The shift positioning the mark attachment class in the high byte of + /// . Lookup flags address attachment classes through their own + /// high byte, so the eight-bit range covers every class a lookup can reference. + /// + public const int MarkAttachmentTypeShift = 8; + public GlyphShapingClass(bool isMark, bool isBase, bool isLigature, ushort markAttachmentType) - { - this.IsMark = isMark; - this.IsBase = isBase; - this.IsLigature = isLigature; - this.MarkAttachmentType = markAttachmentType; - this.Props = (ushort)((isBase ? BaseProp : 0) + => this.Props = (ushort)((isBase ? BaseProp : 0) | (isLigature ? LigatureProp : 0) | (isMark ? MarkProp : 0) - | (markAttachmentType << 8)); - } + | (markAttachmentType << MarkAttachmentTypeShift)); /// /// Gets the class packed into a single word: the low byte carries the glyph class /// bits and the high byte the mark attachment class, so a skip decision is bitwise - /// arithmetic instead of a branch per class. + /// arithmetic instead of a branch per class. The word is the struct's only storage; + /// the class properties are derived from it. /// public ushort Props { get; } - public bool IsMark { get; } - - public bool IsBase { get; } + /// + /// Gets a value indicating whether the glyph is classified as a mark. + /// + public bool IsMark => (this.Props & MarkProp) != 0; - public bool IsLigature { get; } + /// + /// Gets a value indicating whether the glyph is classified as a base. + /// + public bool IsBase => (this.Props & BaseProp) != 0; - public ushort MarkAttachmentType { get; } + /// + /// Gets a value indicating whether the glyph is classified as a ligature. + /// + public bool IsLigature => (this.Props & LigatureProp) != 0; } diff --git a/src/SixLabors.Fonts/GlyphShapingData.cs b/src/SixLabors.Fonts/GlyphShapingData.cs index 4fd81a37e..d77a05c29 100644 --- a/src/SixLabors.Fonts/GlyphShapingData.cs +++ b/src/SixLabors.Fonts/GlyphShapingData.cs @@ -52,8 +52,28 @@ internal struct GlyphShapingData /// private const ushort ShapingClassCacheValidFlag = 1 << 6; + /// + /// The modulus folding ligature ids into the stored byte range 1..255, keeping a + /// live id distinct from the zero not-a-ligature value. Id equality is only ever + /// compared between a mark and its neighbouring ligature, where folded ids stay + /// unique. + /// + private const int LigatureIdModulus = byte.MaxValue; + private ushort glyphId; + /// + /// The ligature id folded into byte range, or zero when the glyph is not a + /// ligature. + /// + private byte ligatureId; + + /// + /// The ligature component index stored as component + 1, so the default record + /// encodes the -1 no-component sentinel as zero. + /// + private byte ligatureComponentPlusOne; + /// /// Packed boolean shaping state addressed through the named flag constants above. /// Single bits keep the record narrow; the properties are the only readers and @@ -187,9 +207,15 @@ public TextDirection Direction public ushort TextRunIndex { get; set; } /// - /// Gets or sets the id of any ligature this glyph is a member of. + /// Gets or sets the id of any ligature this glyph is a member of. Zero means the + /// glyph is not a ligature member; assigned ids fold into the stored byte range + /// while remaining distinct from zero. /// - public int LigatureId { get; set; } + public int LigatureId + { + readonly get => this.ligatureId; + set => this.ligatureId = value == 0 ? (byte)0 : (byte)(((value - 1) % LigatureIdModulus) + 1); + } /// /// Gets or sets a value indicating whether the glyph is ligated. @@ -201,9 +227,16 @@ public bool IsLigated } /// - /// Gets or sets the ligature component index of the glyph. + /// Gets or sets the ligature component index of the glyph, or -1 when the glyph + /// is not a ligature component. Stored offset by one so the default record holds + /// the -1 sentinel; indices clamp to the storable range, far beyond any real + /// font's component count. /// - public int LigatureComponent { get; set; } = -1; + public int LigatureComponent + { + readonly get => this.ligatureComponentPlusOne - 1; + set => this.ligatureComponentPlusOne = (byte)(Math.Min(value, byte.MaxValue - 1) + 1); + } /// /// Gets or sets the mask of features a shaper has registered for this glyph, enabled From 65ebe85af5ce86fda15a48be6a86376980a457c8 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 17:16:44 +1000 Subject: [PATCH 37/94] Add buffer-local glyph class cache Third instance of the direct-mapped buffer cache, this time in front of the GDEF class definition walks: classification after a substitution changes a glyph id previously re-bisected the class and mark attachment tables per glyph (49 walks per op on the Arabic scenario, 38 on Devanagari). Table-derived classes now hit a 256-slot packed-word cache; codepoint-fallback classifications stay out because they depend on record state. | Scenario | Before | After | |------------|------------------|------------------| | Latin | 7.57 us / 3.34x | 7.27 us / 3.26x | | Arabic | 9.30 us / 2.40x | 8.95 us / 2.37x | | Devanagari | 15.37 us / 1.73x | 14.79 us / 1.66x | First sub-9 Arabic and sub-15 Devanagari means. Allocations unchanged (7,776 / 6,480 / 8,496 B). Tests 5,579/0. --- src/SixLabors.Fonts/GlyphShapingClass.cs | 7 ++ src/SixLabors.Fonts/ShapingBuffer.cs | 74 +++++++++++++++++++ .../AdvancedTypographicUtils.cs | 21 +++++- .../GSub/LookupType4SubTable.cs | 2 +- .../SkippingGlyphIterator.cs | 2 +- 5 files changed, 102 insertions(+), 4 deletions(-) diff --git a/src/SixLabors.Fonts/GlyphShapingClass.cs b/src/SixLabors.Fonts/GlyphShapingClass.cs index 113c661c9..d844b8424 100644 --- a/src/SixLabors.Fonts/GlyphShapingClass.cs +++ b/src/SixLabors.Fonts/GlyphShapingClass.cs @@ -33,6 +33,13 @@ public GlyphShapingClass(bool isMark, bool isBase, bool isLigature, ushort markA | (isMark ? MarkProp : 0) | (markAttachmentType << MarkAttachmentTypeShift)); + /// + /// Initializes a new instance of the struct from + /// an already packed props word, the round-trip used by cached classifications. + /// + /// The packed class props word. + public GlyphShapingClass(ushort props) => this.Props = props; + /// /// Gets the class packed into a single word: the low byte carries the glyph class /// bits and the high byte the mark attachment class, so a skip decision is bitwise diff --git a/src/SixLabors.Fonts/ShapingBuffer.cs b/src/SixLabors.Fonts/ShapingBuffer.cs index cd89f9b17..c3425f00c 100644 --- a/src/SixLabors.Fonts/ShapingBuffer.cs +++ b/src/SixLabors.Fonts/ShapingBuffer.cs @@ -122,6 +122,38 @@ internal sealed class ShapingBuffer /// private FontMetrics? glyphIdCacheOwner; + /// + /// The bit offset of the packed class props word in a shaping class cache entry. + /// + private const int ShapingClassCachePropsShift = 16; + + /// + /// The shaping class cache entry bit distinguishing a populated slot from an + /// empty one, since a zero entry could otherwise read as a valid all-zero lookup. + /// + private const ulong ShapingClassCacheMarkerFlag = 1UL << 63; + + /// + /// The shaping class cache entry bits forming the lookup key: the marker and the + /// glyph id. + /// + private const ulong ShapingClassCacheTagMask = ShapingClassCacheMarkerFlag | ((1UL << ShapingClassCachePropsShift) - 1); + + /// + /// Direct-mapped glyph-id-to-class cache: one word per slot packing the glyph id + /// key alongside the packed class props word, so a repeat classification is one + /// load and one masked compare instead of a class definition table walk. Only + /// table-derived classes enter the cache; the codepoint fallback classification + /// depends on record state and stays out. Zero marks a slot empty. + /// + private readonly ulong[] shapingClassCacheEntries = new ulong[256]; + + /// + /// The font metrics instance the shaping class cache entries belong to. + /// Populating from a different font clears the cache before use. + /// + private FontMetrics? shapingClassCacheOwner; + /// /// The bidi runs recorded for inline placeholders, keyed by codepoint offset. /// Placeholder state lives here rather than on every glyph record because only @@ -1019,6 +1051,48 @@ public bool TryGetGlyphIdCached(FontMetrics fontMetrics, CodePoint codePoint, Co return found; } + /// + /// Looks up a table-derived shaping class through a direct-mapped cache in front + /// of the font's class definition tables, whose walks bisect range records per + /// query. A hit is one load and one masked compare. No synchronization is needed: + /// a pooled buffer is exclusively owned for the duration of a shaping pass. + /// + /// The font metrics the class belongs to. + /// The glyph id to look up. + /// When this method returns, contains the cached class if found. + /// if a cached class was found. + public bool TryGetShapingClassCached(FontMetrics fontMetrics, ushort glyphId, out GlyphShapingClass shapingClass) + { + if (!ReferenceEquals(this.shapingClassCacheOwner, fontMetrics)) + { + Array.Clear(this.shapingClassCacheEntries); + this.shapingClassCacheOwner = fontMetrics; + } + + ulong entry = this.shapingClassCacheEntries[glyphId & 0xFF]; + if ((entry & ShapingClassCacheTagMask) == (ShapingClassCacheMarkerFlag | glyphId)) + { + shapingClass = new((ushort)(entry >> ShapingClassCachePropsShift)); + return true; + } + + shapingClass = default; + return false; + } + + /// + /// Stores a table-derived shaping class in the direct-mapped class cache. Must + /// only be called for classes computed purely from the font's class definition + /// tables, after has established the cache + /// owner for the same font. + /// + /// The glyph id the class was computed for. + /// The computed class. + public void SetShapingClassCached(ushort glyphId, GlyphShapingClass shapingClass) + => this.shapingClassCacheEntries[glyphId & 0xFF] = ShapingClassCacheMarkerFlag + | glyphId + | ((ulong)shapingClass.Props << ShapingClassCachePropsShift); + /// /// Resolves glyph metrics through a direct-mapped cache in front of the font's own /// resolver. The tag packs the same key fields the font's cache hashes, so a hit diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs index 4ef775840..c73f3ec9f 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs @@ -541,13 +541,16 @@ public static bool IsMarkGlyph(FontMetrics fontMetrics, ushort glyphId, ref Glyp /// /// Gets the glyph shaping class (mark, base, ligature, mark attachment type) for the specified glyph, /// using GDEF table data if available or falling back to Unicode properties. - /// Results are cached on the instance. + /// Results are cached on the instance, with + /// table-derived classes additionally cached on the buffer so re-classification + /// after a substitution changes a glyph id skips the table walks. /// /// The font metrics. + /// The glyph shaping buffer carrying the class cache. /// The glyph identifier. /// The glyph shaping data, used for caching and Unicode fallback. /// The . - public static GlyphShapingClass GetGlyphShapingClass(FontMetrics fontMetrics, ushort glyphId, ref GlyphShapingData shapingData) + public static GlyphShapingClass GetGlyphShapingClass(FontMetrics fontMetrics, ShapingBuffer buffer, ushort glyphId, ref GlyphShapingData shapingData) { // Cache the shaping class on the GlyphShapingData to avoid repeated GDEF lookups. // The cache key stores the glyph id; -1 means "not cached". @@ -566,15 +569,24 @@ public static GlyphShapingClass GetGlyphShapingClass(FontMetrics fontMetrics, us ShapingProbe.ClassifyMisses++; } + if (buffer.TryGetShapingClassCached(fontMetrics, glyphId, out GlyphShapingClass cached)) + { + shapingData.CachedShapingClass = cached; + shapingData.ShapingClassCacheKey = glyphId; + return cached; + } + bool isMark; bool isBase; bool isLigature; + bool tableDerived = false; ushort markAttachmentType = 0; if (fontMetrics.TryGetGlyphClass(glyphId, out GlyphClassDef? glyphClass)) { isMark = glyphClass == GlyphClassDef.MarkGlyph; isBase = glyphClass == GlyphClassDef.BaseGlyph; isLigature = glyphClass == GlyphClassDef.LigatureGlyph; + tableDerived = true; if (fontMetrics.TryGetMarkAttachmentClass(glyphId, out GlyphClassDef? markAttachmentClass)) { markAttachmentType = (ushort)markAttachmentClass; @@ -589,6 +601,11 @@ public static GlyphShapingClass GetGlyphShapingClass(FontMetrics fontMetrics, us } GlyphShapingClass result = new(isMark, isBase, isLigature, markAttachmentType); + if (tableDerived) + { + buffer.SetShapingClassCached(glyphId, result); + } + shapingData.CachedShapingClass = result; shapingData.ShapingClassCacheKey = glyphId; return result; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs index c1fa1d986..3b48b11b6 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs @@ -212,7 +212,7 @@ public override bool TrySubstitution( // // This in fact happened to a font... See https://bugzilla.gnome.org/show_bug.cgi?id=437633 ref GlyphShapingData data = ref buffer[index]; - GlyphShapingClass shapingClass = AdvancedTypographicUtils.GetGlyphShapingClass(fontMetrics, glyphId, ref data); + GlyphShapingClass shapingClass = AdvancedTypographicUtils.GetGlyphShapingClass(fontMetrics, buffer, glyphId, ref data); bool isBaseLigature = shapingClass.IsBase; bool isMarkLigature = shapingClass.IsMark; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs index b07df54f4..872ae72b9 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs @@ -164,7 +164,7 @@ private readonly bool ShouldIgnore(int index) // inline so the common hit path avoids the classification call entirely. ushort props = data.ShapingClassCacheKey == data.GlyphId ? data.CachedShapingClass.Props - : AdvancedTypographicUtils.GetGlyphShapingClass(this.fontMetrics, data.GlyphId, ref data).Props; + : AdvancedTypographicUtils.GetGlyphShapingClass(this.fontMetrics, this.Collection, data.GlyphId, ref data).Props; if ((props & this.ignoreClassMask) != 0) { From cc11f8f8c14eac5c776463697b77fc5d1c832b1b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 17:26:38 +1000 Subject: [PATCH 38/94] Make syllable machine matching allocation free Add a stack-only match enumerator to the state machine and walk it directly in the Indic, Myanmar, and Universal shapers, replacing the per-operation match list and per-syllable match objects. Each shaper translates its machine tag rows into a per-state syllable type table once, so match handling no longer maps a rule name string per glyph. The existing list-based Match is rewritten over the enumerator and retained for the generator tooling. | Scenario | Before | After | |------------|----------------------------|----------------------------| | Latin | 7.27 us / 3.26x / 7,776 B | 7.25 us / 3.25x / 7,776 B | | Arabic | 8.95 us / 2.37x / 6,480 B | 8.89 us / 2.32x / 6,480 B | | Devanagari | 14.79 us / 1.66x / 8,496 B | 14.69 us / 1.65x / 7,576 B | Devanagari allocations -10.8 percent. Tests 5,579/0. --- .../Shapers/IndicShaper.cs | 23 ++- .../Shapers/MyanmarShaper.cs | 23 ++- .../Shapers/UniversalShaper.cs | 12 +- .../AdvancedTypographic/SyllableTypeMap.cs | 24 ++- .../StateAutomation/StateMachine.cs | 194 +++++++++++++----- 5 files changed, 203 insertions(+), 73 deletions(-) diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs index 3795cd3d9..848d6dc41 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs @@ -21,6 +21,13 @@ internal sealed class IndicShaper : DefaultShaper private static readonly StateMachine StateMachine = new(StateTable, AcceptingStates, Tags); + /// + /// The syllable type for each machine state, translated from the tag rows once so + /// match handling never maps rule name strings. + /// + private static readonly SyllableType[] StateSyllableTypes = + SyllableTypeMap.FromMachineTags(Tags); + /// /// Maps Indic shaping category codes to compact DFA symbol indices. /// @@ -299,7 +306,8 @@ private void SetupSyllables(ShapingBuffer buffer, int index, int count) int syllable = 0; int last = 0; - foreach (StateMatch match in StateMachine.Match(values)) + StateMachine.MatchEnumerator match = StateMachine.EnumerateMatches(values); + while (match.MoveNext()) { if (match.StartIndex > last) { @@ -316,19 +324,18 @@ private void SetupSyllables(ShapingBuffer buffer, int index, int count) ++syllable; + SyllableType syllableType = StateSyllableTypes[match.TagState]; + if (syllableType == SyllableType.BrokenCluster) + { + this.hasBrokenClusters = true; + } + // Create shaper info. for (int i = match.StartIndex; i <= match.EndIndex; i++) { ref GlyphShapingData data = ref buffer[i + index]; CodePoint codePoint = data.CodePoint; - SyllableType syllableType = SyllableTypeMap.FromTag(match.Tags[0]); - - if (syllableType == SyllableType.BrokenCluster) - { - this.hasBrokenClusters = true; - } - data.Syllable.IndicCategory = (Categories)IndicShapingCategory(codePoint); data.Syllable.IndicPosition = (Positions)IndicShapingPosition(codePoint); data.Syllable.Type = syllableType; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs index a2482f2de..f836078db 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs @@ -22,6 +22,13 @@ internal sealed class MyanmarShaper : DefaultShaper Unicode.Resources.MyanmarShapingData.AcceptingStates, Unicode.Resources.MyanmarShapingData.Tags); + /// + /// The syllable type for each machine state, translated from the tag rows once so + /// match handling never maps rule name strings. + /// + private static readonly SyllableType[] StateSyllableTypes = + SyllableTypeMap.FromMachineTags(Unicode.Resources.MyanmarShapingData.Tags); + /// /// Maps Myanmar shaping category codes to compact DFA symbol indices. /// @@ -154,7 +161,8 @@ private void SetupSyllables(ShapingBuffer buffer, int index, int count) int syllable = 0; int last = 0; - foreach (StateMatch match in StateMachine.Match(values)) + StateMachine.MatchEnumerator match = StateMachine.EnumerateMatches(values); + while (match.MoveNext()) { if (match.StartIndex > last) { @@ -171,19 +179,18 @@ private void SetupSyllables(ShapingBuffer buffer, int index, int count) ++syllable; + SyllableType syllableType = StateSyllableTypes[match.TagState]; + if (syllableType == SyllableType.BrokenCluster) + { + this.hasBrokenClusters = true; + } + // Create shaper info. for (int i = match.StartIndex; i <= match.EndIndex; i++) { ref GlyphShapingData data = ref buffer[i + index]; CodePoint codePoint = data.CodePoint; - SyllableType syllableType = SyllableTypeMap.FromTag(match.Tags[0]); - - if (syllableType == SyllableType.BrokenCluster) - { - this.hasBrokenClusters = true; - } - data.Syllable.IndicCategory = (Categories)IndicShapingCategory(codePoint); data.Syllable.IndicPosition = (Positions)IndicShapingPosition(codePoint); data.Syllable.Type = syllableType; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs index 8550f2c8e..df48c2717 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs @@ -48,6 +48,13 @@ internal sealed class UniversalShaper : DefaultShaper private static readonly StateMachine StateMachine = new(UniversalShapingData.StateTable, UniversalShapingData.AcceptingStates, UniversalShapingData.Tags); + /// + /// The syllable type for each machine state, translated from the tag rows once so + /// match handling never maps rule name strings. + /// + private static readonly SyllableType[] StateSyllableTypes = + SyllableTypeMap.FromMachineTags(UniversalShapingData.Tags); + /// /// The 'rphf' (reph forms) feature tag. /// @@ -268,13 +275,14 @@ private void SetupSyllables(ShapingBuffer buffer, int index, int count) } int syllable = 0; - foreach (StateMatch match in StateMachine.Match(values)) + StateMachine.MatchEnumerator match = StateMachine.EnumerateMatches(values); + while (match.MoveNext()) { ++syllable; // Create shaper info. The symbol index is stored directly: it is the value // the state machine consumes and the key into the generated name table. - SyllableType syllableType = SyllableTypeMap.FromTag(match.Tags[0]); + SyllableType syllableType = StateSyllableTypes[match.TagState]; if (syllableType == SyllableType.BrokenCluster) { this.hasBrokenClusters = true; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/SyllableTypeMap.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/SyllableTypeMap.cs index aa67b53c4..4a0993446 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/SyllableTypeMap.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/SyllableTypeMap.cs @@ -5,8 +5,8 @@ namespace SixLabors.Fonts.Tables.AdvancedTypographic; /// /// Maps state machine grammar rule names to values. Invoked -/// once per matched syllable at the state machine boundary, so per-glyph storage and -/// comparison never touch the rule name strings. +/// when a shaper translates its machine's tag rows into a per-state table, so match +/// handling and per-glyph storage never touch the rule name strings. /// internal static class SyllableTypeMap { @@ -34,4 +34,24 @@ internal static class SyllableTypeMap "vowel_syllable" => SyllableType.VowelSyllable, _ => SyllableType.Other, }; + + /// + /// Builds the per-state syllable type table for a machine's tag rows, translated + /// once at machine construction so match handling reads an array element instead + /// of mapping a rule name string per match. States without a tag map to + /// . + /// + /// The machine's per-state tag rows. + /// The per-state table. + public static SyllableType[] FromMachineTags(string[][] tags) + { + SyllableType[] types = new SyllableType[tags.Length]; + for (int i = 0; i < tags.Length; i++) + { + string[] row = tags[i]; + types[i] = row.Length > 0 ? FromTag(row[0]) : SyllableType.None; + } + + return types; + } } diff --git a/src/UnicodeTrieGenerator/StateAutomation/StateMachine.cs b/src/UnicodeTrieGenerator/StateAutomation/StateMachine.cs index b2d7934f9..4c3386d2a 100644 --- a/src/UnicodeTrieGenerator/StateAutomation/StateMachine.cs +++ b/src/UnicodeTrieGenerator/StateAutomation/StateMachine.cs @@ -45,70 +45,32 @@ public StateMachine(int[][] stateTable, bool[] accepting, string[][] tags) /// The . public IEnumerable Match(ReadOnlySpan input) { - int state = InitialState; - int? startRun = null; - int? lastAccepting = null; - List matches = new(input.Length); - for (int i = 0; i < input.Length; i++) - { - int c = input[i]; - - int lastState = state; - state = this.StateTable[state][c]; - - if (state == FailState) - { - // yield the last match if any. - if (startRun != null && lastAccepting != null && lastAccepting >= startRun) - { - matches.Add(new StateMatch() - { - StartIndex = startRun.Value, - EndIndex = lastAccepting.Value, - Tags = this.Tags[lastState] - }); - } - - // reset the state as if we started over from the initial state - state = this.StateTable[InitialState][c]; - startRun = null; - } - - // start a run if not in the failure state - if (state != FailState && startRun == null) - { - startRun = i; - } - - // if accepting, mark the potential match end - if (this.Accepting[state]) - { - lastAccepting = i; - } - - // reset the state to the initial state if we get into the failure state - if (state == FailState) - { - state = InitialState; - } - } - - // yield the last match if any. - if (startRun != null && lastAccepting != null && lastAccepting >= startRun) + MatchEnumerator enumerator = this.EnumerateMatches(input); + while (enumerator.MoveNext()) { matches.Add(new StateMatch() { - StartIndex = startRun.Value, - EndIndex = lastAccepting.Value, - Tags = this.Tags[state] + StartIndex = enumerator.StartIndex, + EndIndex = enumerator.EndIndex, + Tags = this.Tags[enumerator.TagState] }); } return matches; } + /// + /// Returns an allocation-free enumerator over the pattern matches in the input + /// sequence. Each match exposes the accepting run bounds and the state whose tag + /// row identifies the match, so callers translate tags through precomputed + /// per-state tables instead of touching the tag strings. + /// + /// The input sequence. + /// The . + public MatchEnumerator EnumerateMatches(ReadOnlySpan input) => new(this, input); + /// /// For each match over the input sequence, action functions matching /// the tag definitions in the input pattern are called with the startIndex, @@ -129,6 +91,132 @@ public void Apply(int[] input, Dictionary + /// Enumerates pattern matches over an input sequence without allocating: the + /// traversal state lives in the struct and each match is exposed through the + /// bounds and tag-state properties rather than a match object. + /// + public ref struct MatchEnumerator + { + private readonly StateMachine machine; + private readonly ReadOnlySpan input; + private int position; + private int state; + + /// + /// The start index of the run in progress, or -1 when no run is open. + /// + private int startRun; + + /// + /// The index of the most recent accepting symbol, or -1 when none has been + /// seen. Emission requires it to fall inside the open run. + /// + private int lastAccepting; + + /// + /// Initializes a new instance of the struct + /// positioned before the first match. + /// + /// The state machine to run. + /// The input sequence. + public MatchEnumerator(StateMachine machine, ReadOnlySpan input) + { + this.machine = machine; + this.input = input; + this.position = 0; + this.state = InitialState; + this.startRun = -1; + this.lastAccepting = -1; + } + + /// + /// Gets the start index of the current match. + /// + public int StartIndex { get; private set; } + + /// + /// Gets the inclusive end index of the current match. + /// + public int EndIndex { get; private set; } + + /// + /// Gets the index of the state whose tag row identifies the current match. + /// + public int TagState { get; private set; } + + /// + /// Advances to the next match. + /// + /// if a match was found. + public bool MoveNext() + { + int[][] stateTable = this.machine.StateTable; + bool[] accepting = this.machine.Accepting; + + while (this.position < this.input.Length) + { + int c = this.input[this.position]; + + int lastState = this.state; + this.state = stateTable[this.state][c]; + + bool emit = false; + if (this.state == FailState) + { + // Yield the last match if any. + if (this.startRun != -1 && this.lastAccepting >= this.startRun) + { + this.StartIndex = this.startRun; + this.EndIndex = this.lastAccepting; + this.TagState = lastState; + emit = true; + } + + // Reset the state as if we started over from the initial state. + this.state = stateTable[InitialState][c]; + this.startRun = -1; + } + + // Start a run if not in the failure state. + if (this.state != FailState && this.startRun == -1) + { + this.startRun = this.position; + } + + // If accepting, mark the potential match end. + if (accepting[this.state]) + { + this.lastAccepting = this.position; + } + + // Reset the state to the initial state if we get into the failure state. + if (this.state == FailState) + { + this.state = InitialState; + } + + this.position++; + if (emit) + { + return true; + } + } + + // Yield the last match if any. + if (this.startRun != -1 && this.lastAccepting >= this.startRun) + { + this.StartIndex = this.startRun; + this.EndIndex = this.lastAccepting; + this.TagState = this.state; + this.startRun = -1; + return true; + } + + return false; + } + } } internal class StateMatch : IEquatable From 1895244081b3d4a7b03b22901d38266a40b55e02 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 17:47:03 +1000 Subject: [PATCH 39/94] Front the substitution feature resolution with a buffer cache Resolving a feature tag to its lookup list previously probed the table cache per query, hashing the language candidate array each time; the Indic shaper additionally resolves per syllable during reordering. A direct-mapped cache on the buffer, keyed by feature and script and owned by the resolving table, makes repeat resolutions one load and one compare. Scoped to substitution: the positioning path resolves too rarely to pay for the front cache and regressed the Arabic scenario when wired. | Scenario | Before | After | |------------|------------------|------------------| | Latin | 7.56 us / 3.34x | 7.44 us / 3.31x | | Arabic | 8.96 us / 2.37x | 9.04 us / 2.38x | | Devanagari | 14.40 us / 1.65x | 13.55 us / 1.54x | Before column re-measured at the prior commit in the same machine window. Allocations unchanged. Tests 5,579/0. --- src/SixLabors.Fonts/ShapingBuffer.cs | 103 ++++++++++++++++++ .../Tables/AdvancedTypographic/GSubTable.cs | 25 ++++- .../Shapers/IndicShaper.cs | 10 +- 3 files changed, 127 insertions(+), 11 deletions(-) diff --git a/src/SixLabors.Fonts/ShapingBuffer.cs b/src/SixLabors.Fonts/ShapingBuffer.cs index c3425f00c..c467245ef 100644 --- a/src/SixLabors.Fonts/ShapingBuffer.cs +++ b/src/SixLabors.Fonts/ShapingBuffer.cs @@ -154,6 +154,40 @@ internal sealed class ShapingBuffer /// private FontMetrics? shapingClassCacheOwner; + /// + /// The bit offset of the script class in a feature lookups cache tag. + /// + private const int FeatureLookupsCacheScriptShift = 32; + + /// + /// The feature lookups cache tag bit distinguishing a populated slot from an + /// empty one, since a zero tag could otherwise read as a valid all-zero lookup. + /// + private const ulong FeatureLookupsCacheMarkerFlag = 1UL << 63; + + /// + /// Validation tags for the substitution-phase feature lookups cache. A slot's tag + /// packs the marker, the feature tag, and the script class, so a hit is one load + /// and one compare instead of a dictionary probe that hashes the language + /// candidates per query. + /// + private readonly ulong[] subFeatureLookupsCacheTags = new ulong[128]; + + /// + /// The resolved lookup lists for each slot of + /// , held untyped because the + /// substitution and positioning tables declare distinct lookup list types. + /// + private readonly object?[] subFeatureLookupsCacheValues = new object?[128]; + + /// + /// The layout table instance the substitution-phase entries belong to. Consulting + /// from a different table clears the cache before use; a reset clears the owner + /// because the language candidates the entries were resolved under belong to the + /// pass. + /// + private object? subFeatureLookupsCacheOwner; + /// /// The bidi runs recorded for inline placeholders, keyed by codepoint offset. /// Placeholder state lives here rather than on every glyph record because only @@ -365,6 +399,10 @@ public void Reset(TextOptions textOptions) this.SegmentShapers.Clear(); this.TextOptions = textOptions; this.LanguageTags = ResolveLanguageTags(textOptions); + + // Cached feature resolutions were made under the previous pass's language + // candidates, so a new pass must not serve them. + this.subFeatureLookupsCacheOwner = null; } /// @@ -1093,6 +1131,71 @@ public void SetShapingClassCached(ushort glyphId, GlyphShapingClass shapingClass | glyphId | ((ulong)shapingClass.Props << ShapingClassCachePropsShift); + /// + /// Looks up a resolved feature lookup list through a direct-mapped cache in front + /// of the substitution table's own cache, whose probes hash the language + /// candidate array per query. A hit is one load and one compare. + /// + /// The layout table performing the resolution. + /// The feature tag. + /// The script class the feature resolves under. + /// When this method returns, contains the cached lookup list if found. + /// if a cached list was found. + public bool TryGetFeatureLookupsCached(object table, Tag feature, ScriptClass script, out object? lookups) + { + if (!ReferenceEquals(this.subFeatureLookupsCacheOwner, table)) + { + Array.Clear(this.subFeatureLookupsCacheTags); + this.subFeatureLookupsCacheOwner = table; + } + + ulong tag = FeatureLookupsCacheMarkerFlag + | feature.Value + | ((ulong)script << FeatureLookupsCacheScriptShift); + + int slot = FeatureLookupsCacheSlot(feature, script); + if (this.subFeatureLookupsCacheTags[slot] == tag) + { + lookups = this.subFeatureLookupsCacheValues[slot]; + return true; + } + + lookups = null; + return false; + } + + /// + /// Stores a resolved feature lookup list in the direct-mapped feature lookups + /// cache. Must be called after has + /// established the cache owner for the same table, and never for resolutions + /// that depend on live variation coordinates. + /// + /// The feature tag. + /// The script class the feature resolved under. + /// The resolved lookup list. + public void SetFeatureLookupsCached(Tag feature, ScriptClass script, object lookups) + { + int slot = FeatureLookupsCacheSlot(feature, script); + this.subFeatureLookupsCacheTags[slot] = FeatureLookupsCacheMarkerFlag + | feature.Value + | ((ulong)script << FeatureLookupsCacheScriptShift); + this.subFeatureLookupsCacheValues[slot] = lookups; + } + + /// + /// Computes the direct-map slot for a feature and script. Folds all four tag + /// bytes so features sharing trailing characters spread across slots rather than + /// thrashing one. + /// + /// The feature tag. + /// The script class. + /// The slot index. + private static int FeatureLookupsCacheSlot(Tag feature, ScriptClass script) + { + uint value = feature.Value; + return (int)((value ^ (value >> 8) ^ (value >> 16) ^ (value >> 24) ^ (uint)script) & 127); + } + /// /// Resolves glyph metrics through a direct-mapped cache in front of the font's own /// resolver. The tag packs the same key fields the font's cache hashes, so a hit diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs index 84806009f..c7710a5ec 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs @@ -252,7 +252,7 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas { Tag featureTag = stages[s].FeatureTag; var lookupProbe = ShapingProbe.Enter(); - bool found = this.TryGetFeatureLookups(fontMetrics, in featureTag, current, buffer.LanguageTags, out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? lookups); + bool found = this.TryGetFeatureLookups(fontMetrics, in featureTag, current, buffer, out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? lookups); ShapingProbe.Exit(ShapingProbe.LookupResolve, lookupProbe); if (!found || lookups is null) { @@ -421,14 +421,15 @@ private void ApplyMergedLookups( } /// - /// Tries to get the feature lookups for the given stage feature, script, and language. + /// Tries to get the feature lookups for the given stage feature, script, and the + /// buffer's language candidates. /// /// The font metrics. /// The feature tag for the current shaping stage. /// The script class. - /// - /// The candidate OpenType language system tags, most specific first. An empty array - /// selects the default language system. + /// + /// The glyph shaping buffer carrying the language candidates and the per-pass + /// resolution cache. /// /// When this method returns, contains the list of feature lookups if found. /// if lookups were found; otherwise, . @@ -436,7 +437,7 @@ public bool TryGetFeatureLookups( FontMetrics fontMetrics, in Tag stageFeature, ScriptClass script, - Tag[] languageTags, + ShapingBuffer buffer, [NotNullWhen(true)] out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? value) { if (this.ScriptList is null) @@ -445,6 +446,8 @@ public bool TryGetFeatureLookups( return false; } + Tag[] languageTags = buffer.LanguageTags; + // Feature variations resolve against the font's live variation coordinates, so // caching would mix results across differently configured variable fonts. if (this.FeatureVariations is not null) @@ -453,6 +456,15 @@ public bool TryGetFeatureLookups( return value.Count > 0; } + // The buffer fronts the table cache with a direct-mapped cache whose hit is + // one load and one compare, skipping the dictionary probe that hashes the + // language candidates per query. + if (buffer.TryGetFeatureLookupsCached(this, stageFeature, script, out object? cached)) + { + value = (List<(Tag Feature, ushort Index, LookupTable LookupTable)>)cached!; + return value.Count > 0; + } + // Resolution depends only on this table's data for a given feature, script, // and language candidates, so results, including empty ones, are cached for // the table's lifetime. The cached list is shared: consumers must not mutate @@ -464,6 +476,7 @@ public bool TryGetFeatureLookups( this.featureLookupsCache.TryAdd(key, value); } + buffer.SetFeatureLookupsCached(stageFeature, script, value); return value.Count > 0; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs index 848d6dc41..4dbf29d46 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs @@ -488,7 +488,7 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) // base consonants. if (start + 3 <= end && indicConfiguration.RephPosition != Positions.Ra_To_Become_Reph && - gSubTable?.TryGetFeatureLookups(fontMetrics, in RphfTag, this.ScriptClass, buffer.LanguageTags, out _) == true && + gSubTable?.TryGetFeatureLookups(fontMetrics, in RphfTag, this.ScriptClass, buffer, out _) == true && ((indicConfiguration.RephMode == RephMode.Implicit && !IsJoiner(ref buffer[start + 2])) || (indicConfiguration.RephMode == RephMode.Explicit && buffer[start + 2].Syllable.IndicCategory == Categories.ZWJ))) { @@ -887,7 +887,7 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) const int prefLen = 2; if (basePosition + prefLen < end && - gSubTable?.TryGetFeatureLookups(fontMetrics, in PrefTag, this.ScriptClass, buffer.LanguageTags, out _) == true) + gSubTable?.TryGetFeatureLookups(fontMetrics, in PrefTag, this.ScriptClass, buffer, out _) == true) { // Find a Halant,Ra sequence and mark it for pre-base reordering processing. for (int i = basePosition + 1; i + prefLen - 1 < end; i++) @@ -906,7 +906,7 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) // This allows distinguishing the following cases with MS Khmer fonts: // U+1784,U+17D2,U+179A,U+17D2,U+1782 // U+1784,U+17D2,U+1782,U+17D2,U+179A - if (gSubTable.TryGetFeatureLookups(fontMetrics, in CfarTag, this.ScriptClass, buffer.LanguageTags, out _)) + if (gSubTable.TryGetFeatureLookups(fontMetrics, in CfarTag, this.ScriptClass, buffer, out _)) { while (i < end) { @@ -1004,7 +1004,7 @@ private Positions ConsonantPosition(ShapingBuffer buffer, ushort virama, ushort private bool WouldSubstitute(ShapingBuffer buffer, in Tag featureTag, ReadOnlySpan glyphs) { if (!this.fontMetrics.TryGetGSubTable(out GSubTable? gSubTable) || - !gSubTable.TryGetFeatureLookups(this.fontMetrics, in featureTag, this.ScriptClass, buffer.LanguageTags, out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? lookups)) + !gSubTable.TryGetFeatureLookups(this.fontMetrics, in featureTag, this.ScriptClass, buffer, out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? lookups)) { return false; } @@ -1097,7 +1097,7 @@ private void FinalReorder(ShapingBuffer buffer, int index, int count) // applied (see below), the shaping engine performs some final glyph // reordering before applying all the remaining font features to the entire // cluster. - bool tryPref = gSubTable?.TryGetFeatureLookups(fontMetrics, in PrefTag, this.ScriptClass, buffer.LanguageTags, out _) == true; + bool tryPref = gSubTable?.TryGetFeatureLookups(fontMetrics, in PrefTag, this.ScriptClass, buffer, out _) == true; // Find base consonant again. int basePosition = start; From 9069631cbb1d4858a6c2efa287e70acbcf7bcaf2 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 17:52:53 +1000 Subject: [PATCH 40/94] Add ASCII fast path to grapheme counting ASCII text has single-char clusters except the CR LF pair: no ASCII character extends, prepends, or joins, and controls break on both sides. The count is therefore the length minus the CR LF pairs, computed with two vectorized scans instead of running the boundary state machine per character. Non-ASCII text falls through to the enumerator unchanged. | Scenario | Before | After | |------------|------------------|------------------| | Latin | 7.44 us / 3.31x | 6.64 us / 2.97x | | Arabic | 9.04 us / 2.38x | 9.02 us / 2.38x | | Devanagari | 13.55 us / 1.54x | 13.53 us / 1.51x | First sub-3x Latin. Allocations unchanged. Tests 5,579/0. --- .../Unicode/MemoryExtensions.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/SixLabors.Fonts/Unicode/MemoryExtensions.cs b/src/SixLabors.Fonts/Unicode/MemoryExtensions.cs index 72321bd58..502b13f1d 100644 --- a/src/SixLabors.Fonts/Unicode/MemoryExtensions.cs +++ b/src/SixLabors.Fonts/Unicode/MemoryExtensions.cs @@ -223,6 +223,37 @@ public static int GetTerminalCellWidth(this Span span, TerminalWidthOption /// The number of grapheme clusters. public static int GetGraphemeCount(this ReadOnlySpan span) { + // ASCII text has single-char clusters except the CR LF pair: no ASCII + // character extends, prepends, or joins, and controls break on both sides. + // The count is therefore the length minus the CR LF pairs, found with two + // vectorized scans instead of the boundary state machine. + if (!span.ContainsAnyExceptInRange((char)0, (char)0x7F)) + { + int pairs = 0; + int searchStart = 0; + while (true) + { + int offset = span[searchStart..].IndexOf('\r'); + if (offset < 0) + { + break; + } + + int absolute = searchStart + offset; + if (absolute + 1 < span.Length && span[absolute + 1] == '\n') + { + pairs++; + searchStart = absolute + 2; + } + else + { + searchStart = absolute + 1; + } + } + + return span.Length - pairs; + } + int count = 0; SpanGraphemeEnumerator enumerator = new(span, true); while (enumerator.MoveNext()) From 5b6d423dba641d5f58bad24ea525e4f5267d7a63 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 18:03:04 +1000 Subject: [PATCH 41/94] Skip parallel stream mirroring during substitution edits The metrics and positioning streams are undefined until seeded after substitution settles, so shifting them on every substitution-phase insert and removal moved garbage. Structural edits now mirror the parallel streams only on a positioning buffer, matching the contract that positioning state is invalid during substitution. | Scenario | Before | After | |------------|------------------|------------------| | Latin | 6.64 us / 2.97x | 6.57 us / 2.86x | | Arabic | 9.02 us / 2.38x | 8.96 us / 2.30x | | Devanagari | 13.53 us / 1.51x | 13.39 us / 1.51x | All scenarios improve slightly; kept as a structural contract match. Allocations unchanged. Tests 5,579/0. --- src/SixLabors.Fonts/ShapingBuffer.cs | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/SixLabors.Fonts/ShapingBuffer.cs b/src/SixLabors.Fonts/ShapingBuffer.cs index c467245ef..2bc9ee26b 100644 --- a/src/SixLabors.Fonts/ShapingBuffer.cs +++ b/src/SixLabors.Fonts/ShapingBuffer.cs @@ -1346,7 +1346,9 @@ private void InsertAt(int index, GlyphShapingData item) /// /// Inserts one record and its metrics entry at the given index, shifting later - /// entries in both streams right. + /// entries right. The metrics and positioning streams shift only on a positioning + /// buffer: substitution-phase edits precede stream seeding, so their contents are + /// undefined and copying them would be pure waste per edit. /// /// The zero-based index at which to insert. /// The record to insert. @@ -1361,24 +1363,34 @@ private void InsertAt(int index, GlyphShapingData item, GlyphMetricsEntry metric } Array.Copy(this.data, index, this.data, index + 1, this.count - index); - Array.Copy(this.metrics, index, this.metrics, index + 1, this.count - index); - Array.Copy(this.positions, index, this.positions, index + 1, this.count - index); + if (this.Role == ShapingBufferRole.Positioning) + { + Array.Copy(this.metrics, index, this.metrics, index + 1, this.count - index); + Array.Copy(this.positions, index, this.positions, index + 1, this.count - index); + this.positions[index] = default; + } + this.data[index] = item; this.metrics[index] = metricsEntry; - this.positions[index] = default; this.count++; } /// - /// Removes the record and metrics entry at the given index, shifting later entries - /// left. Stale entries beyond the count are overwritten by later appends. + /// Removes the record at the given index, shifting later entries left. The + /// metrics and positioning streams shift only on a positioning buffer, matching + /// the insertion contract. Stale entries beyond the count are overwritten by + /// later appends. /// /// The zero-based index to remove at. private void RemoveAt(int index) { Array.Copy(this.data, index + 1, this.data, index, this.count - index - 1); - Array.Copy(this.metrics, index + 1, this.metrics, index, this.count - index - 1); - Array.Copy(this.positions, index + 1, this.positions, index, this.count - index - 1); + if (this.Role == ShapingBufferRole.Positioning) + { + Array.Copy(this.metrics, index + 1, this.metrics, index, this.count - index - 1); + Array.Copy(this.positions, index + 1, this.positions, index, this.count - index - 1); + } + this.count--; } From 97b7007f4304eae81885d2c916f2d847ac86bb07 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 18:09:33 +1000 Subject: [PATCH 42/94] Derive syllable shaping properties once per glyph Syllable setup walked the shaping property trie three times per glyph: once for the machine symbol and again for each of the category and position lanes. The property word is now fetched once and stashed for the match loop, and the Universal shaper reuses the machine symbol it already computed, matching the reference model of a single property derivation per glyph during setup. | Scenario | Before | After | |------------|------------------|------------------| | Latin | 6.57 us / 2.86x | 6.60 us / 2.92x | | Arabic | 8.96 us / 2.30x | 8.94 us / 2.36x | | Devanagari | 13.39 us / 1.51x | 13.28 us / 1.46x | Latin and Arabic paths untouched, readings within drift. Allocations unchanged. Tests 5,579/0. --- .../Shapers/IndicShaper.cs | 33 +++++++++++++----- .../Shapers/MyanmarShaper.cs | 34 +++++++++++++------ .../Shapers/UniversalShaper.cs | 2 +- 3 files changed, 49 insertions(+), 20 deletions(-) diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs index 4dbf29d46..b728ad20d 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs @@ -15,6 +15,18 @@ namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers; /// internal sealed class IndicShaper : DefaultShaper { + /// + /// The bit shift extracting the shaping category from a packed Indic shaping + /// property word; the category occupies the upper byte. + /// + private const int IndicCategoryShift = 8; + + /// + /// The mask extracting the zero-based shaping position from a packed Indic + /// shaping property word; the position occupies the lower byte. + /// + private const int IndicPositionMask = 0xFF; + /// /// The state machine for Indic syllable identification. /// @@ -286,6 +298,7 @@ private void SetupSyllables(ShapingBuffer buffer, int index, int count) this.hasBrokenClusters = false; Span values = count <= 64 ? stackalloc int[count] : new int[count]; + Span shapingProps = count <= 64 ? stackalloc ushort[count] : new ushort[count]; for (int i = index; i < index + count; i++) { @@ -297,11 +310,13 @@ private void SetupSyllables(ShapingBuffer buffer, int index, int count) // machine expects its input alphabet to be dense 0..N-1, matching the // sequential IDs assigned in GenerateIndicShapingDataTrie. // - // CategoryToSymbolId[IndicShapingCategory(codePoint)] performs this mapping, ensuring that - // every codepoint is presented to the DFA using the correct compact - // symbol index. + // The property word is fetched once per glyph and stashed: the match + // loop below derives both the category and position lanes from it + // rather than walking the trie again. CodePoint codePoint = buffer[i].CodePoint; - values[i - index] = CategoryToSymbolId[IndicShapingCategory(codePoint)]; + ushort props = (ushort)UnicodeData.GetIndicShapingProperties((uint)codePoint.Value); + shapingProps[i - index] = props; + values[i - index] = CategoryToSymbolId[props >> IndicCategoryShift]; } int syllable = 0; @@ -334,10 +349,10 @@ private void SetupSyllables(ShapingBuffer buffer, int index, int count) for (int i = match.StartIndex; i <= match.EndIndex; i++) { ref GlyphShapingData data = ref buffer[i + index]; - CodePoint codePoint = data.CodePoint; + ushort props = shapingProps[i]; - data.Syllable.IndicCategory = (Categories)IndicShapingCategory(codePoint); - data.Syllable.IndicPosition = (Positions)IndicShapingPosition(codePoint); + data.Syllable.IndicCategory = (Categories)(props >> IndicCategoryShift); + data.Syllable.IndicPosition = (Positions)((props & IndicPositionMask) + 1); data.Syllable.Type = syllableType; data.Syllable.Number = syllable; } @@ -365,7 +380,7 @@ private void SetupSyllables(ShapingBuffer buffer, int index, int count) /// The code point. /// The shaping category value. private static int IndicShapingCategory(CodePoint codePoint) - => UnicodeData.GetIndicShapingProperties((uint)codePoint.Value) >> 8; + => UnicodeData.GetIndicShapingProperties((uint)codePoint.Value) >> IndicCategoryShift; /// /// Gets the Indic shaping position for a code point. The trie stores the position @@ -375,7 +390,7 @@ private static int IndicShapingCategory(CodePoint codePoint) /// The code point. /// The shaping position ordinal. private static int IndicShapingPosition(CodePoint codePoint) - => (UnicodeData.GetIndicShapingProperties((uint)codePoint.Value) & 0xFF) + 1; + => (UnicodeData.GetIndicShapingProperties((uint)codePoint.Value) & IndicPositionMask) + 1; /// /// Performs the initial reordering pass for Indic syllables, including base consonant diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs index f836078db..fa13ecc25 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs @@ -13,6 +13,18 @@ namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers; /// internal sealed class MyanmarShaper : DefaultShaper { + /// + /// The bit shift extracting the shaping category from a packed Indic shaping + /// property word; the category occupies the upper byte. + /// + private const int MyanmarCategoryShift = 8; + + /// + /// The mask extracting the zero-based shaping position from a packed Indic + /// shaping property word; the position occupies the lower byte. + /// + private const int MyanmarPositionMask = 0xFF; + /// /// The state machine for Myanmar syllable identification. /// @@ -140,6 +152,7 @@ private void SetupSyllables(ShapingBuffer buffer, int index, int count) this.hasBrokenClusters = false; Span values = count <= 64 ? stackalloc int[count] : new int[count]; + Span shapingProps = count <= 64 ? stackalloc ushort[count] : new ushort[count]; for (int i = index; i < index + count; i++) { @@ -151,12 +164,13 @@ private void SetupSyllables(ShapingBuffer buffer, int index, int count) // machine expects its input alphabet to be dense 0..N-1, matching the // sequential IDs assigned in GenerateMyanmarShapingData. // - // CategoryToSymbolId[(int)my] performs this mapping, ensuring that - // every codepoint is presented to the DFA using the correct compact - // symbol index. + // The property word is fetched once per glyph and stashed: the match + // loop below derives both the category and position lanes from it + // rather than walking the trie again. CodePoint codePoint = buffer[i].CodePoint; - MyanmarCategories my = (MyanmarCategories)IndicShapingCategory(codePoint); - values[i - index] = CategoryToSymbolId[(int)my]; + ushort props = (ushort)UnicodeData.GetIndicShapingProperties((uint)codePoint.Value); + shapingProps[i - index] = props; + values[i - index] = CategoryToSymbolId[props >> MyanmarCategoryShift]; } int syllable = 0; @@ -189,10 +203,10 @@ private void SetupSyllables(ShapingBuffer buffer, int index, int count) for (int i = match.StartIndex; i <= match.EndIndex; i++) { ref GlyphShapingData data = ref buffer[i + index]; - CodePoint codePoint = data.CodePoint; + ushort props = shapingProps[i]; - data.Syllable.IndicCategory = (Categories)IndicShapingCategory(codePoint); - data.Syllable.IndicPosition = (Positions)IndicShapingPosition(codePoint); + data.Syllable.IndicCategory = (Categories)(props >> MyanmarCategoryShift); + data.Syllable.IndicPosition = (Positions)((props & MyanmarPositionMask) + 1); data.Syllable.Type = syllableType; data.Syllable.Number = syllable; } @@ -491,7 +505,7 @@ private static int NextSyllable(ShapingBuffer buffer, int index, int count) /// The code point. /// The shaping category value. private static int IndicShapingCategory(CodePoint codePoint) - => UnicodeData.GetIndicShapingProperties((uint)codePoint.Value) >> 8; + => UnicodeData.GetIndicShapingProperties((uint)codePoint.Value) >> MyanmarCategoryShift; /// /// Gets the Indic shaping position for a code point. The trie stores the position @@ -501,7 +515,7 @@ private static int IndicShapingCategory(CodePoint codePoint) /// The code point. /// The shaping position ordinal. private static int IndicShapingPosition(CodePoint codePoint) - => (UnicodeData.GetIndicShapingProperties((uint)codePoint.Value) & 0xFF) + 1; + => (UnicodeData.GetIndicShapingProperties((uint)codePoint.Value) & MyanmarPositionMask) + 1; /// /// Builds a lookup table mapping Myanmar shaping category codes to compact DFA symbol indices. diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs index df48c2717..c77813476 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs @@ -291,7 +291,7 @@ private void SetupSyllables(ShapingBuffer buffer, int index, int count) for (int i = match.StartIndex; i <= match.EndIndex; i++) { ref GlyphShapingData data = ref buffer[i + index]; - data.Syllable.UseCategory = UnicodeData.GetUniversalShapingSymbolCount((uint)data.CodePoint.Value); + data.Syllable.UseCategory = values[i]; data.Syllable.Type = syllableType; data.Syllable.Number = syllable; } From 205909bf717272c5d149b4b26c515b74842438a3 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 18:15:45 +1000 Subject: [PATCH 43/94] Cache class lookups on range-based class definitions Format 2 class definitions resolved every query with a binary search over range records; pair positioning consults two class definitions per candidate pair and class-based contexts consult one per matched glyph. A direct-mapped cache in front of the search packs the glyph id key and class value into one word per slot. Aligned 32-bit writes are atomic, so concurrent shaping passes only ever lose an update; the empty-slot sentinel is unreachable because its glyph id is excluded from caching. | Scenario | Before | After | |------------|------------------|------------------| | Latin | 6.60 us / 2.92x | 6.42 us / 2.87x | | Arabic | 8.94 us / 2.36x | 8.82 us / 2.33x | | Devanagari | 13.28 us / 1.46x | 13.12 us / 1.49x | All-scenario improvement, all-time bests. Allocations unchanged. Tests 5,579/0. --- .../ClassDefinitionTable.cs | 70 +++++++++++++++++-- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ClassDefinitionTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ClassDefinitionTable.cs index 1d5399ce4..4c4b94623 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ClassDefinitionTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ClassDefinitionTable.cs @@ -134,14 +134,56 @@ public override int ClassIndexOf(ushort glyphId) /// internal sealed class ClassDefinitionFormat2Table : ClassDefinitionTable { + /// + /// The number of direct-mapped cache slots; must be a power of two so the slot + /// mask below selects one. + /// + private const int ClassCacheSlots = 128; + + /// + /// The mask selecting a cache slot from a glyph id. + /// + private const int ClassCacheSlotMask = ClassCacheSlots - 1; + + /// + /// The bit offset of the class value in a cache entry; the glyph id key occupies + /// the low half. + /// + private const int ClassCacheClassShift = 16; + + /// + /// The mask extracting the glyph id key from a cache entry. + /// + private const uint ClassCacheGlyphMask = 0xFFFF; + + /// + /// The one glyph id excluded from caching: the empty-slot sentinel carries this + /// id, so allowing it to cache could serve the sentinel as a real entry. + /// + private const ushort UncacheableGlyphId = 0xFFFF; + private readonly ClassRangeRecord[] records; + /// + /// Direct-mapped glyph-to-class cache in front of the range binary search: one + /// word per slot packing the glyph id key and the class value. Entries start at + /// the all-ones sentinel, which can never match because the sentinel glyph id is + /// excluded from caching. Aligned 32-bit writes are atomic, so concurrent + /// shaping passes can race on a slot and only ever lose an update, never read a + /// torn entry. + /// + private readonly uint[] classCache; + /// /// Initializes a new instance of the class. /// /// The array of class range records. private ClassDefinitionFormat2Table(ClassRangeRecord[] records) - => this.records = records; + { + this.records = records; + this.classCache = new uint[ClassCacheSlots]; + Array.Fill(this.classCache, uint.MaxValue); + } /// /// Loads a from the binary reader. @@ -186,9 +228,22 @@ public static ClassDefinitionFormat2Table Load(BigEndianBinaryReader reader) /// public override int ClassIndexOf(ushort glyphId) { + uint[] cache = this.classCache; + int slot = glyphId & ClassCacheSlotMask; + if (glyphId != UncacheableGlyphId) + { + uint entry = cache[slot]; + if ((entry & ClassCacheGlyphMask) == glyphId) + { + return (int)(entry >> ClassCacheClassShift); + } + } + // Records are ordered by StartGlyphId, so use binary search to find the - // candidate range whose StartGlyphId is <= glyphId. + // candidate range whose StartGlyphId is <= glyphId. Any glyph not included + // in the range of covered glyph IDs automatically belongs to Class 0. ClassRangeRecord[] records = this.records; + int result = 0; int lo = 0; int hi = records.Length - 1; while (lo <= hi) @@ -205,11 +260,16 @@ public override int ClassIndexOf(ushort glyphId) } else { - return rec.Class; + result = rec.Class; + break; } } - // Any glyph not included in the range of covered glyph IDs automatically belongs to Class 0. - return 0; + if (glyphId != UncacheableGlyphId) + { + cache[slot] = (uint)(glyphId | ((uint)result << ClassCacheClassShift)); + } + + return result; } } From a34a131354fba61a9213815f9d047c9b02fac6fe Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 18:26:23 +1000 Subject: [PATCH 44/94] Reuse the bidi analysis data per thread The pipeline constructed a fresh BidiData per shaping operation, re-allocating its builder backing arrays every time even though the builders are designed to grow to a high-water mark and reset. The data now lives alongside the per-thread algorithm instance that consumes it, making steady-state bidi analysis allocation free. | Scenario | Before | After | |------------|----------------------------|----------------------------| | Latin | 6.42 us / 2.87x / 7,776 B | 6.16 us / 2.69x / 6,704 B | | Arabic | 8.82 us / 2.33x / 6,480 B | 8.87 us / 2.33x / 5,648 B | | Devanagari | 13.12 us / 1.49x / 7,576 B | 13.06 us / 1.48x / 6,768 B | Allocations -13.8 / -12.8 / -10.7 percent. Tests 5,579/0. --- src/SixLabors.Fonts/TextShaper.Pipeline.cs | 2 +- src/SixLabors.Fonts/Unicode/BidiData.cs | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/SixLabors.Fonts/TextShaper.Pipeline.cs b/src/SixLabors.Fonts/TextShaper.Pipeline.cs index 4634c0f42..db075f256 100644 --- a/src/SixLabors.Fonts/TextShaper.Pipeline.cs +++ b/src/SixLabors.Fonts/TextShaper.Pipeline.cs @@ -167,7 +167,7 @@ private static ShapedText ShapeText( // Analyse the text for bidi directional runs. BidiAlgorithm bidi = BidiAlgorithm.Instance.Value!; - BidiData bidiData = new(); + BidiData bidiData = BidiData.Instance.Value!; bidiData.Init(text, (sbyte)options.TextDirection); if (options.TextBidiMode == TextBidiMode.Override) diff --git a/src/SixLabors.Fonts/Unicode/BidiData.cs b/src/SixLabors.Fonts/Unicode/BidiData.cs index 6d5c7612b..96dca9b6b 100644 --- a/src/SixLabors.Fonts/Unicode/BidiData.cs +++ b/src/SixLabors.Fonts/Unicode/BidiData.cs @@ -17,6 +17,14 @@ internal class BidiData private ArrayBuilder tempLevelBuffer; private readonly List paragraphPositions = new(); + /// + /// Gets a reusable per-thread instance. The internal builders grow to the + /// workload's high-water mark and resets them, so reuse makes + /// steady-state analysis allocation free, mirroring the lifetime of the + /// per-thread algorithm instance that consumes this data. + /// + public static ThreadLocal Instance { get; } = new(() => new BidiData()); + public sbyte ParagraphEmbeddingLevel { get; private set; } public bool HasBrackets { get; private set; } From 6aaac182745aaaadc608f60b22840aa5bd0e54d0 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 18:32:45 +1000 Subject: [PATCH 45/94] Reuse shaper instances across segments and passes Both layout tables constructed a fresh shaper per segment per operation, paying the constructor and its feature and stage list allocations every time. Shapers now live in a small per-buffer cache keyed by script, script tag, and font, valid while the options instance whose values they captured is unchanged; a reset with a different options instance clears them. Per-segment shaper state is reassigned at each pause invocation, so instance reuse is safe in the exclusively owned buffer. | Scenario | Before | After | |------------|----------------------------|----------------------------| | Latin | 6.16 us / 2.69x / 6,704 B | 6.45 us / 2.86x / 6,216 B | | Arabic | 8.87 us / 2.33x / 5,648 B | 8.59 us / 2.27x / 4,368 B | | Devanagari | 13.06 us / 1.48x / 6,768 B | 12.54 us / 1.42x / 3,832 B | Allocations -7.3 / -22.7 / -43.4 percent; best-ever Arabic and Devanagari means. Latin mean within its observed run-to-run band, ratio flat. Tests 5,579/0. --- src/SixLabors.Fonts/ShapingBuffer.cs | 43 +++++++++++++++++++ .../Tables/AdvancedTypographic/GPosTable.cs | 2 +- .../Tables/AdvancedTypographic/GSubTable.cs | 2 +- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/SixLabors.Fonts/ShapingBuffer.cs b/src/SixLabors.Fonts/ShapingBuffer.cs index 2bc9ee26b..d560374c9 100644 --- a/src/SixLabors.Fonts/ShapingBuffer.cs +++ b/src/SixLabors.Fonts/ShapingBuffer.cs @@ -195,6 +195,15 @@ internal sealed class ShapingBuffer /// private readonly List<(int CodePointIndex, BidiRun Run)> placeholderBidiRuns = new(); + /// + /// Shaper instances reused across segments and passes, keyed by script, script + /// tag, and font. Safe to reuse because the pooled buffer is exclusively owned + /// and shaper per-segment state is reassigned at each pause invocation. Cleared + /// when a reset adopts a different options instance, whose values the shapers + /// captured at construction. + /// + private readonly List<(ScriptClass Script, Tag ScriptTag, FontMetrics FontMetrics, Tables.AdvancedTypographic.Shapers.BaseShaper Shaper)> shaperCache = new(4); + /// /// Initializes a new instance of the class. /// @@ -397,6 +406,14 @@ public void Reset(TextOptions textOptions) this.glyphDigest = default; this.placeholderBidiRuns.Clear(); this.SegmentShapers.Clear(); + + // Cached shapers captured option values at construction, so a different + // options instance invalidates them. + if (!ReferenceEquals(this.TextOptions, textOptions)) + { + this.shaperCache.Clear(); + } + this.TextOptions = textOptions; this.LanguageTags = ResolveLanguageTags(textOptions); @@ -1131,6 +1148,32 @@ public void SetShapingClassCached(ushort glyphId, GlyphShapingClass shapingClass | glyphId | ((ulong)shapingClass.Props << ShapingClassCachePropsShift); + /// + /// Gets a shaper for the given script and font, reusing a cached instance when + /// one was created for the same key. Instances persist across passes while the + /// options instance is unchanged, so steady-state shaping constructs no shapers. + /// + /// The script class to shape. + /// The resolved OpenType script tag. + /// The font metrics the shaper binds to. + /// The . + public Tables.AdvancedTypographic.Shapers.BaseShaper GetOrCreateShaper(ScriptClass script, Tag unicodeScriptTag, FontMetrics fontMetrics) + { + List<(ScriptClass Script, Tag ScriptTag, FontMetrics FontMetrics, Tables.AdvancedTypographic.Shapers.BaseShaper Shaper)> cache = this.shaperCache; + for (int i = 0; i < cache.Count; i++) + { + (ScriptClass cachedScript, Tag cachedTag, FontMetrics cachedMetrics, Tables.AdvancedTypographic.Shapers.BaseShaper cachedShaper) = cache[i]; + if (cachedScript == script && cachedTag == unicodeScriptTag && ReferenceEquals(cachedMetrics, fontMetrics)) + { + return cachedShaper; + } + } + + Tables.AdvancedTypographic.Shapers.BaseShaper shaper = Tables.AdvancedTypographic.Shapers.ShaperFactory.Create(script, unicodeScriptTag, fontMetrics, this.TextOptions); + cache.Add((script, unicodeScriptTag, fontMetrics, shaper)); + return shaper; + } + /// /// Looks up a resolved feature lookup list through a direct-mapped cache in front /// of the substitution table's own cache, whose probes hash the language diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs index 924eb6f6e..826599f83 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs @@ -251,7 +251,7 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas } Tag unicodeScriptTag = this.GetUnicodeScriptTag(current); - BaseShaper shaper = ShaperFactory.Create(current, unicodeScriptTag, fontMetrics, buffer.TextOptions); + BaseShaper shaper = buffer.GetOrCreateShaper(current, unicodeScriptTag, fontMetrics); // Plan positioning features for each glyph. Records seeded across buffers // had their feature registrations cleared, so this pass re-plans. diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs index c7710a5ec..be6383218 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs @@ -195,7 +195,7 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas Tag unicodeScriptTag = this.GetUnicodeScriptTag(current); var createProbe = ShapingProbe.Enter(); - BaseShaper shaper = ShaperFactory.Create(current, unicodeScriptTag, fontMetrics, buffer.TextOptions); + BaseShaper shaper = buffer.GetOrCreateShaper(current, unicodeScriptTag, fontMetrics); ShapingProbe.Exit(ShapingProbe.SubShaperCreate, createProbe); // Plan substitution features for each glyph. From a25577012bbb2de43e7d4dd695aaec6e51f99623 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 21:15:32 +1000 Subject: [PATCH 46/94] Add ASCII fast path to buffer population All-ASCII runs populate through a tight per-char loop: every ASCII char is one codepoint and, except the CR LF pair, one grapheme; no ASCII codepoint is default ignorable or participates in a variation sequence, so the boundary state machine, the codepoint decode, the lookahead decode, and the ignorable classification all fall away. Grapheme, text-run, and bidi-run bookkeeping is replicated exactly; passing no lookahead codepoint is glyph-identical because no ASCII successor can select a variation. Non-ASCII runs are unchanged. Gated on ratio evidence across seven runs under sustained machine load (ratios are the load-independent metric): Latin 2.04-2.41, mean 2.25, against a 2.73-2.86 clean baseline; Arabic mean 2.16 (baseline 2.24-2.27) and Devanagari mean 1.48 (baseline 1.42-1.43) on byte-identical paths. Allocations unchanged (6,216 / 4,368 / 3,832 B). Tests 5,579/0. Clean-machine validation pending; revert if it contradicts. --- src/SixLabors.Fonts/TextShaper.Pipeline.cs | 51 +++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/src/SixLabors.Fonts/TextShaper.Pipeline.cs b/src/SixLabors.Fonts/TextShaper.Pipeline.cs index db075f256..e3746c8c5 100644 --- a/src/SixLabors.Fonts/TextShaper.Pipeline.cs +++ b/src/SixLabors.Fonts/TextShaper.Pipeline.cs @@ -550,8 +550,55 @@ private static void PopulateAndSubstitute( // during glyph lookup, so the per-codepoint lookahead decode is skipped. bool hasVariationSequences = font.FontMetrics.HasUnicodeVariationSequences; - // Enumerate through each grapheme in the text. int graphemeIndex = start; + + // ASCII text takes a tight per-char path: every char is one codepoint and, + // except for the CR LF pair, one grapheme; no ASCII codepoint is default + // ignorable or participates in a variation sequence, so the boundary state + // machine, the codepoint decode, and the lookahead all fall away. Passing no + // following codepoint is safe because no ASCII successor can select a + // variation, so the resolved glyph is identical. + if (!text.ContainsAnyExceptInRange((char)0, (char)0x7F)) + { + FontMetrics asciiFontMetrics = font.FontMetrics; + for (int i = 0; i < text.Length; i++) + { + char c = text[i]; + + // A line feed directly after a carriage return continues that + // grapheme; everything else starts one. + if (c != '\n' || i == 0 || text[i - 1] != '\r') + { + while (textRunIndex < textRuns.Count - 1 && graphemeIndex == textRuns[textRunIndex].End) + { + textRunIndex++; + } + } + + if (codePointIndex == bidiRuns[bidiRunIndex].End) + { + bidiRunIndex++; + } + + bidiMap[codePointIndex] = bidiRunIndex; + + CodePoint current = new(c); + substitutions.TryGetGlyphIdCached(asciiFontMetrics, current, null, out ushort asciiGlyphId, out _); + substitutions.AddGlyph(asciiGlyphId, current, (TextDirection)bidiRuns[bidiRunIndex].Direction, (ushort)textRunIndex, codePointIndex); + codePointIndex++; + + // The grapheme ends here unless this char opens a CR LF pair. + if (c != '\r' || i + 1 >= text.Length || text[i + 1] != '\n') + { + graphemeIndex++; + } + } + + ShapingProbe.Exit(ShapingProbe.Populate, probe); + goto Substitute; + } + + // Enumerate through each grapheme in the text. SpanGraphemeEnumerator graphemeEnumerator = new(text); while (graphemeEnumerator.MoveNext()) { @@ -620,6 +667,8 @@ private static void PopulateAndSubstitute( ShapingProbe.Exit(ShapingProbe.Populate, probe); + Substitute: + // Apply the simple and complex substitutions. // TODO: Investigate HarfBuzz normalizer. probe = ShapingProbe.Enter(); From 5e9812680b44cfb4f908828f6ad99d63aec4ab14 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 21:33:25 +1000 Subject: [PATCH 47/94] Rename buffer cache accessors to standard naming Identifier-only rename to the standard TryGet/Get/Set patterns: TryGetGlyphIdCached to TryGetGlyphId, GetGlyphMetricsCached to GetGlyphMetrics, TryGetShapingClassCached/SetShapingClassCached to TryGetShapingClass/SetShapingClass, and TryGetFeatureLookupsCached/SetFeatureLookupsCached to TryGetFeatureLookups/SetFeatureLookups. Caching remains an implementation detail documented on the members. No behavior change; benchmark numbers carry over from the prior commit. Tests 5,579/0. --- src/SixLabors.Fonts/ShapingBuffer.cs | 22 +++++++++---------- .../AdvancedTypographicUtils.cs | 4 ++-- .../Tables/AdvancedTypographic/GSubTable.cs | 4 ++-- src/SixLabors.Fonts/TextShaper.Pipeline.cs | 4 ++-- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/SixLabors.Fonts/ShapingBuffer.cs b/src/SixLabors.Fonts/ShapingBuffer.cs index d560374c9..d2a1abe8b 100644 --- a/src/SixLabors.Fonts/ShapingBuffer.cs +++ b/src/SixLabors.Fonts/ShapingBuffer.cs @@ -372,7 +372,7 @@ public bool SeedMetricsInPlace(Font font) bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(codePoint, layoutMode) || (slot.AppliedFeatureMask & verticalMask) != 0; - FontGlyphMetrics glyphMetrics = this.GetGlyphMetricsCached(fontMetrics, codePoint, slot.GlyphId, textAttributes, textDecorations, layoutMode, colorFontSupport); + FontGlyphMetrics glyphMetrics = this.GetGlyphMetrics(fontMetrics, codePoint, slot.GlyphId, textAttributes, textDecorations, layoutMode, colorFontSupport); if (glyphMetrics.GlyphType == GlyphType.Fallback && !CodePoint.IsControl(codePoint)) { @@ -956,7 +956,7 @@ public bool TryAdd(Font font, ShapingBuffer workspace) bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(codePoint, layoutMode) || (source.AppliedFeatureMask & verticalMask) != 0; - FontGlyphMetrics glyphMetrics = this.GetGlyphMetricsCached(fontMetrics, codePoint, id, textAttributes, textDecorations, layoutMode, colorFontSupport); + FontGlyphMetrics glyphMetrics = this.GetGlyphMetrics(fontMetrics, codePoint, id, textAttributes, textDecorations, layoutMode, colorFontSupport); if (glyphMetrics.GlyphType == GlyphType.Fallback && !CodePoint.IsControl(codePoint)) { @@ -1025,7 +1025,7 @@ public bool TryUpdate(Font font, ShapingBuffer workspace) bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(codePoint, layoutMode) || (shape.AppliedFeatureMask & verticalMask) != 0; - FontGlyphMetrics glyphMetrics = this.GetGlyphMetricsCached(fontMetrics, codePoint, id, textAttributes, textDecorations, layoutMode, colorFontSupport); + FontGlyphMetrics glyphMetrics = this.GetGlyphMetrics(fontMetrics, codePoint, id, textAttributes, textDecorations, layoutMode, colorFontSupport); // If the glyphs are fallbacks we don't want them as // we've already captured them on the first run. @@ -1077,7 +1077,7 @@ public bool TryUpdate(Font font, ShapingBuffer workspace) /// When this method returns, contains the glyph id if found. /// When this method returns, indicates whether the following codepoint was consumed. /// if a glyph was found. - public bool TryGetGlyphIdCached(FontMetrics fontMetrics, CodePoint codePoint, CodePoint? nextCodePoint, out ushort glyphId, out bool skipNextCodePoint) + public bool TryGetGlyphId(FontMetrics fontMetrics, CodePoint codePoint, CodePoint? nextCodePoint, out ushort glyphId, out bool skipNextCodePoint) { if (!ReferenceEquals(this.glyphIdCacheOwner, fontMetrics)) { @@ -1116,7 +1116,7 @@ public bool TryGetGlyphIdCached(FontMetrics fontMetrics, CodePoint codePoint, Co /// The glyph id to look up. /// When this method returns, contains the cached class if found. /// if a cached class was found. - public bool TryGetShapingClassCached(FontMetrics fontMetrics, ushort glyphId, out GlyphShapingClass shapingClass) + public bool TryGetShapingClass(FontMetrics fontMetrics, ushort glyphId, out GlyphShapingClass shapingClass) { if (!ReferenceEquals(this.shapingClassCacheOwner, fontMetrics)) { @@ -1138,12 +1138,12 @@ public bool TryGetShapingClassCached(FontMetrics fontMetrics, ushort glyphId, ou /// /// Stores a table-derived shaping class in the direct-mapped class cache. Must /// only be called for classes computed purely from the font's class definition - /// tables, after has established the cache + /// tables, after has established the cache /// owner for the same font. /// /// The glyph id the class was computed for. /// The computed class. - public void SetShapingClassCached(ushort glyphId, GlyphShapingClass shapingClass) + public void SetShapingClass(ushort glyphId, GlyphShapingClass shapingClass) => this.shapingClassCacheEntries[glyphId & 0xFF] = ShapingClassCacheMarkerFlag | glyphId | ((ulong)shapingClass.Props << ShapingClassCachePropsShift); @@ -1184,7 +1184,7 @@ public Tables.AdvancedTypographic.Shapers.BaseShaper GetOrCreateShaper(ScriptCla /// The script class the feature resolves under. /// When this method returns, contains the cached lookup list if found. /// if a cached list was found. - public bool TryGetFeatureLookupsCached(object table, Tag feature, ScriptClass script, out object? lookups) + public bool TryGetFeatureLookups(object table, Tag feature, ScriptClass script, out object? lookups) { if (!ReferenceEquals(this.subFeatureLookupsCacheOwner, table)) { @@ -1209,14 +1209,14 @@ public bool TryGetFeatureLookupsCached(object table, Tag feature, ScriptClass sc /// /// Stores a resolved feature lookup list in the direct-mapped feature lookups - /// cache. Must be called after has + /// cache. Must be called after has /// established the cache owner for the same table, and never for resolutions /// that depend on live variation coordinates. /// /// The feature tag. /// The script class the feature resolved under. /// The resolved lookup list. - public void SetFeatureLookupsCached(Tag feature, ScriptClass script, object lookups) + public void SetFeatureLookups(Tag feature, ScriptClass script, object lookups) { int slot = FeatureLookupsCacheSlot(feature, script); this.subFeatureLookupsCacheTags[slot] = FeatureLookupsCacheMarkerFlag @@ -1253,7 +1253,7 @@ private static int FeatureLookupsCacheSlot(Tag feature, ScriptClass script) /// The layout mode. /// The color font support level. /// The resolved . - private FontGlyphMetrics GetGlyphMetricsCached( + private FontGlyphMetrics GetGlyphMetrics( FontMetrics fontMetrics, CodePoint codePoint, ushort glyphId, diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs index c73f3ec9f..293448996 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs @@ -569,7 +569,7 @@ public static GlyphShapingClass GetGlyphShapingClass(FontMetrics fontMetrics, Sh ShapingProbe.ClassifyMisses++; } - if (buffer.TryGetShapingClassCached(fontMetrics, glyphId, out GlyphShapingClass cached)) + if (buffer.TryGetShapingClass(fontMetrics, glyphId, out GlyphShapingClass cached)) { shapingData.CachedShapingClass = cached; shapingData.ShapingClassCacheKey = glyphId; @@ -603,7 +603,7 @@ public static GlyphShapingClass GetGlyphShapingClass(FontMetrics fontMetrics, Sh GlyphShapingClass result = new(isMark, isBase, isLigature, markAttachmentType); if (tableDerived) { - buffer.SetShapingClassCached(glyphId, result); + buffer.SetShapingClass(glyphId, result); } shapingData.CachedShapingClass = result; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs index be6383218..4b85c46b9 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs @@ -459,7 +459,7 @@ public bool TryGetFeatureLookups( // The buffer fronts the table cache with a direct-mapped cache whose hit is // one load and one compare, skipping the dictionary probe that hashes the // language candidates per query. - if (buffer.TryGetFeatureLookupsCached(this, stageFeature, script, out object? cached)) + if (buffer.TryGetFeatureLookups(this, stageFeature, script, out object? cached)) { value = (List<(Tag Feature, ushort Index, LookupTable LookupTable)>)cached!; return value.Count > 0; @@ -476,7 +476,7 @@ public bool TryGetFeatureLookups( this.featureLookupsCache.TryAdd(key, value); } - buffer.SetFeatureLookupsCached(stageFeature, script, value); + buffer.SetFeatureLookups(stageFeature, script, value); return value.Count > 0; } diff --git a/src/SixLabors.Fonts/TextShaper.Pipeline.cs b/src/SixLabors.Fonts/TextShaper.Pipeline.cs index e3746c8c5..f7adbfe6e 100644 --- a/src/SixLabors.Fonts/TextShaper.Pipeline.cs +++ b/src/SixLabors.Fonts/TextShaper.Pipeline.cs @@ -583,7 +583,7 @@ private static void PopulateAndSubstitute( bidiMap[codePointIndex] = bidiRunIndex; CodePoint current = new(c); - substitutions.TryGetGlyphIdCached(asciiFontMetrics, current, null, out ushort asciiGlyphId, out _); + substitutions.TryGetGlyphId(asciiFontMetrics, current, null, out ushort asciiGlyphId, out _); substitutions.AddGlyph(asciiGlyphId, current, (TextDirection)bidiRuns[bidiRunIndex].Direction, (ushort)textRunIndex, codePointIndex); codePointIndex++; @@ -641,7 +641,7 @@ private static void PopulateAndSubstitute( charIndex += charsConsumed; // Get the glyph id for the codepoint and add to the buffer. - bool hasGlyph = substitutions.TryGetGlyphIdCached(font.FontMetrics, current, next, out ushort glyphId, out skipNextCodePoint); + bool hasGlyph = substitutions.TryGetGlyphId(font.FontMetrics, current, next, out ushort glyphId, out skipNextCodePoint); // Unsupported default-ignorable code points such as FE0F should not block // GSUB sequences like emoji ZWJ ligatures. Preserve joiners explicitly. From e8e7a6bee0f48bfc1d8f8f7ec511a9e04398b719 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 21:34:16 +1000 Subject: [PATCH 48/94] Revert "Add ASCII fast path to buffer population" This reverts commit a25577012bbb2de43e7d4dd695aaec6e51f99623. --- src/SixLabors.Fonts/TextShaper.Pipeline.cs | 51 +--------------------- 1 file changed, 1 insertion(+), 50 deletions(-) diff --git a/src/SixLabors.Fonts/TextShaper.Pipeline.cs b/src/SixLabors.Fonts/TextShaper.Pipeline.cs index f7adbfe6e..32f01a1c4 100644 --- a/src/SixLabors.Fonts/TextShaper.Pipeline.cs +++ b/src/SixLabors.Fonts/TextShaper.Pipeline.cs @@ -550,55 +550,8 @@ private static void PopulateAndSubstitute( // during glyph lookup, so the per-codepoint lookahead decode is skipped. bool hasVariationSequences = font.FontMetrics.HasUnicodeVariationSequences; - int graphemeIndex = start; - - // ASCII text takes a tight per-char path: every char is one codepoint and, - // except for the CR LF pair, one grapheme; no ASCII codepoint is default - // ignorable or participates in a variation sequence, so the boundary state - // machine, the codepoint decode, and the lookahead all fall away. Passing no - // following codepoint is safe because no ASCII successor can select a - // variation, so the resolved glyph is identical. - if (!text.ContainsAnyExceptInRange((char)0, (char)0x7F)) - { - FontMetrics asciiFontMetrics = font.FontMetrics; - for (int i = 0; i < text.Length; i++) - { - char c = text[i]; - - // A line feed directly after a carriage return continues that - // grapheme; everything else starts one. - if (c != '\n' || i == 0 || text[i - 1] != '\r') - { - while (textRunIndex < textRuns.Count - 1 && graphemeIndex == textRuns[textRunIndex].End) - { - textRunIndex++; - } - } - - if (codePointIndex == bidiRuns[bidiRunIndex].End) - { - bidiRunIndex++; - } - - bidiMap[codePointIndex] = bidiRunIndex; - - CodePoint current = new(c); - substitutions.TryGetGlyphId(asciiFontMetrics, current, null, out ushort asciiGlyphId, out _); - substitutions.AddGlyph(asciiGlyphId, current, (TextDirection)bidiRuns[bidiRunIndex].Direction, (ushort)textRunIndex, codePointIndex); - codePointIndex++; - - // The grapheme ends here unless this char opens a CR LF pair. - if (c != '\r' || i + 1 >= text.Length || text[i + 1] != '\n') - { - graphemeIndex++; - } - } - - ShapingProbe.Exit(ShapingProbe.Populate, probe); - goto Substitute; - } - // Enumerate through each grapheme in the text. + int graphemeIndex = start; SpanGraphemeEnumerator graphemeEnumerator = new(text); while (graphemeEnumerator.MoveNext()) { @@ -667,8 +620,6 @@ private static void PopulateAndSubstitute( ShapingProbe.Exit(ShapingProbe.Populate, probe); - Substitute: - // Apply the simple and complex substitutions. // TODO: Investigate HarfBuzz normalizer. probe = ShapingProbe.Enter(); From 7a3cb3ad978b5dd6c7c00d599006d1931f304310 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 22:08:43 +1000 Subject: [PATCH 49/94] Introduce the shape plan Port of the HarfBuzz shape plan model (hb_shape_plan / hb_ot_map). A ShapePlan is the prebuilt artifact for one (font, script, script tag, language candidates, options) identity: the shaper, its stage list, and per layout table the pause-delimited stage groups with merged lookup lists resolved once, each entry recording every registering feature so per-pass masks combine at apply time exactly as the per-operation merge did. Groups build lazily after the first plan invocation registers the shaper stages, and a stage count change rebuilds both tables groups. Plans live in a per-buffer cache keyed by (script, tag, font) with options-instance retention; variable-font plans rebuild per operation and never cache. Both table walks now read prebuilt structure instead of re-resolving and re-merging per operation; the Indic reorder queries answer from the plan with a table fallback. Deleted outright: the feature resolution front cache; the standalone shaper cache is absorbed into the plan cache. The group build bodies are isolated so the steady-state hit path carries no closure allocation. | Scenario | Before | After | |------------|----------------------------|----------------------------| | Latin | 6.22 us / 2.73x / 6,216 B | 6.07 us / 2.61x / 6,216 B | | Arabic | 8.56 us / 2.24x / 4,368 B | 7.78 us / 1.99x / 4,368 B | | Devanagari | 12.58 us / 1.43x / 3,832 B | 12.00 us / 1.34x / 3,832 B | First sub-2x Arabic and sub-12 Devanagari; allocations byte identical. Clean controls, 12-iteration protocol. Tests 5,585/0. --- src/SixLabors.Fonts/ShapingBuffer.cs | 159 ++----- .../Tables/AdvancedTypographic/GPosTable.cs | 142 +++---- .../Tables/AdvancedTypographic/GSubTable.cs | 141 +++---- .../Tables/AdvancedTypographic/ShapePlan.cs | 392 ++++++++++++++++++ .../ShapePlanLookupResolver{TLookup}.cs | 15 + .../ShapePlanStageGroup{TLookup}.cs | 46 ++ .../Shapers/IndicShaper.cs | 25 +- 7 files changed, 618 insertions(+), 302 deletions(-) create mode 100644 src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlan.cs create mode 100644 src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanLookupResolver{TLookup}.cs create mode 100644 src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanStageGroup{TLookup}.cs diff --git a/src/SixLabors.Fonts/ShapingBuffer.cs b/src/SixLabors.Fonts/ShapingBuffer.cs index d2a1abe8b..e67c88715 100644 --- a/src/SixLabors.Fonts/ShapingBuffer.cs +++ b/src/SixLabors.Fonts/ShapingBuffer.cs @@ -154,40 +154,6 @@ internal sealed class ShapingBuffer /// private FontMetrics? shapingClassCacheOwner; - /// - /// The bit offset of the script class in a feature lookups cache tag. - /// - private const int FeatureLookupsCacheScriptShift = 32; - - /// - /// The feature lookups cache tag bit distinguishing a populated slot from an - /// empty one, since a zero tag could otherwise read as a valid all-zero lookup. - /// - private const ulong FeatureLookupsCacheMarkerFlag = 1UL << 63; - - /// - /// Validation tags for the substitution-phase feature lookups cache. A slot's tag - /// packs the marker, the feature tag, and the script class, so a hit is one load - /// and one compare instead of a dictionary probe that hashes the language - /// candidates per query. - /// - private readonly ulong[] subFeatureLookupsCacheTags = new ulong[128]; - - /// - /// The resolved lookup lists for each slot of - /// , held untyped because the - /// substitution and positioning tables declare distinct lookup list types. - /// - private readonly object?[] subFeatureLookupsCacheValues = new object?[128]; - - /// - /// The layout table instance the substitution-phase entries belong to. Consulting - /// from a different table clears the cache before use; a reset clears the owner - /// because the language candidates the entries were resolved under belong to the - /// pass. - /// - private object? subFeatureLookupsCacheOwner; - /// /// The bidi runs recorded for inline placeholders, keyed by codepoint offset. /// Placeholder state lives here rather than on every glyph record because only @@ -196,13 +162,13 @@ internal sealed class ShapingBuffer private readonly List<(int CodePointIndex, BidiRun Run)> placeholderBidiRuns = new(); /// - /// Shaper instances reused across segments and passes, keyed by script, script - /// tag, and font. Safe to reuse because the pooled buffer is exclusively owned - /// and shaper per-segment state is reassigned at each pause invocation. Cleared - /// when a reset adopts a different options instance, whose values the shapers - /// captured at construction. + /// Shape plans reused across segments and passes, keyed by script, script tag, + /// and font. Safe to reuse because the pooled buffer is exclusively owned and a + /// plan's per-segment shaper state is reassigned at each pause invocation. + /// Cleared when a reset adopts a different options instance, whose values the + /// plans captured when built. /// - private readonly List<(ScriptClass Script, Tag ScriptTag, FontMetrics FontMetrics, Tables.AdvancedTypographic.Shapers.BaseShaper Shaper)> shaperCache = new(4); + private readonly List<(ScriptClass Script, Tag ScriptTag, FontMetrics FontMetrics, Tables.AdvancedTypographic.ShapePlan Plan)> planCache = new(4); /// /// Initializes a new instance of the class. @@ -271,11 +237,18 @@ public ShapingBuffer(TextOptions textOptions, ShapingFeatureMap featureMap, Shap /// /// Gets the shaping segments recorded during substitution: each script segment's - /// final range, script, and the shaper that planned it. The in-place positioning + /// final range, script, and the plan that shaped it. The in-place positioning /// pass reuses these so one plan drives both tables; the list stays empty when /// records were seeded across buffers and positioning must segment for itself. /// - public List<(int Index, int Count, ScriptClass Script, Tables.AdvancedTypographic.Shapers.BaseShaper Shaper)> SegmentShapers { get; } = new(); + public List<(int Index, int Count, ScriptClass Script, Tables.AdvancedTypographic.ShapePlan Plan)> SegmentPlans { get; } = new(); + + /// + /// Gets or sets the plan for the segment currently being shaped. Pause actions + /// run inside segment processing and query it for prebuilt feature resolutions; + /// the layout tables set it around each segment and clear it after. + /// + public Tables.AdvancedTypographic.ShapePlan? CurrentPlan { get; set; } /// /// Gets the reusable scratch the substitution table uses to merge a stage group's @@ -405,21 +378,18 @@ public void Reset(TextOptions textOptions) this.LigatureId = 1; this.glyphDigest = default; this.placeholderBidiRuns.Clear(); - this.SegmentShapers.Clear(); + this.SegmentPlans.Clear(); + this.CurrentPlan = null; - // Cached shapers captured option values at construction, so a different - // options instance invalidates them. + // Cached plans captured option values when built, so a different options + // instance invalidates them. if (!ReferenceEquals(this.TextOptions, textOptions)) { - this.shaperCache.Clear(); + this.planCache.Clear(); } this.TextOptions = textOptions; this.LanguageTags = ResolveLanguageTags(textOptions); - - // Cached feature resolutions were made under the previous pass's language - // candidates, so a new pass must not serve them. - this.subFeatureLookupsCacheOwner = null; } /// @@ -431,7 +401,7 @@ public void Clear() this.count = 0; this.LigatureId = 1; this.placeholderBidiRuns.Clear(); - this.SegmentShapers.Clear(); + this.SegmentPlans.Clear(); } /// @@ -1150,93 +1120,34 @@ public void SetShapingClass(ushort glyphId, GlyphShapingClass shapingClass) /// /// Gets a shaper for the given script and font, reusing a cached instance when - /// one was created for the same key. Instances persist across passes while the - /// options instance is unchanged, so steady-state shaping constructs no shapers. + /// one was built for the same key. Plans persist across passes while the options + /// instance is unchanged, so steady-state shaping builds no plans and constructs + /// no shapers. Plans whose resolution depends on live variation coordinates are + /// rebuilt every call and never cached. /// /// The script class to shape. /// The resolved OpenType script tag. - /// The font metrics the shaper binds to. - /// The . - public Tables.AdvancedTypographic.Shapers.BaseShaper GetOrCreateShaper(ScriptClass script, Tag unicodeScriptTag, FontMetrics fontMetrics) + /// The font metrics the plan binds to. + /// The . + public Tables.AdvancedTypographic.ShapePlan GetOrCreatePlan(ScriptClass script, Tag unicodeScriptTag, FontMetrics fontMetrics) { - List<(ScriptClass Script, Tag ScriptTag, FontMetrics FontMetrics, Tables.AdvancedTypographic.Shapers.BaseShaper Shaper)> cache = this.shaperCache; + List<(ScriptClass Script, Tag ScriptTag, FontMetrics FontMetrics, Tables.AdvancedTypographic.ShapePlan Plan)> cache = this.planCache; for (int i = 0; i < cache.Count; i++) { - (ScriptClass cachedScript, Tag cachedTag, FontMetrics cachedMetrics, Tables.AdvancedTypographic.Shapers.BaseShaper cachedShaper) = cache[i]; + (ScriptClass cachedScript, Tag cachedTag, FontMetrics cachedMetrics, Tables.AdvancedTypographic.ShapePlan cachedPlan) = cache[i]; if (cachedScript == script && cachedTag == unicodeScriptTag && ReferenceEquals(cachedMetrics, fontMetrics)) { - return cachedShaper; + return cachedPlan; } } - Tables.AdvancedTypographic.Shapers.BaseShaper shaper = Tables.AdvancedTypographic.Shapers.ShaperFactory.Create(script, unicodeScriptTag, fontMetrics, this.TextOptions); - cache.Add((script, unicodeScriptTag, fontMetrics, shaper)); - return shaper; - } - - /// - /// Looks up a resolved feature lookup list through a direct-mapped cache in front - /// of the substitution table's own cache, whose probes hash the language - /// candidate array per query. A hit is one load and one compare. - /// - /// The layout table performing the resolution. - /// The feature tag. - /// The script class the feature resolves under. - /// When this method returns, contains the cached lookup list if found. - /// if a cached list was found. - public bool TryGetFeatureLookups(object table, Tag feature, ScriptClass script, out object? lookups) - { - if (!ReferenceEquals(this.subFeatureLookupsCacheOwner, table)) - { - Array.Clear(this.subFeatureLookupsCacheTags); - this.subFeatureLookupsCacheOwner = table; - } - - ulong tag = FeatureLookupsCacheMarkerFlag - | feature.Value - | ((ulong)script << FeatureLookupsCacheScriptShift); - - int slot = FeatureLookupsCacheSlot(feature, script); - if (this.subFeatureLookupsCacheTags[slot] == tag) + Tables.AdvancedTypographic.ShapePlan plan = Tables.AdvancedTypographic.ShapePlan.Build(fontMetrics, script, unicodeScriptTag, this.TextOptions, this.LanguageTags); + if (plan.IsCacheable) { - lookups = this.subFeatureLookupsCacheValues[slot]; - return true; + cache.Add((script, unicodeScriptTag, fontMetrics, plan)); } - lookups = null; - return false; - } - - /// - /// Stores a resolved feature lookup list in the direct-mapped feature lookups - /// cache. Must be called after has - /// established the cache owner for the same table, and never for resolutions - /// that depend on live variation coordinates. - /// - /// The feature tag. - /// The script class the feature resolved under. - /// The resolved lookup list. - public void SetFeatureLookups(Tag feature, ScriptClass script, object lookups) - { - int slot = FeatureLookupsCacheSlot(feature, script); - this.subFeatureLookupsCacheTags[slot] = FeatureLookupsCacheMarkerFlag - | feature.Value - | ((ulong)script << FeatureLookupsCacheScriptShift); - this.subFeatureLookupsCacheValues[slot] = lookups; - } - - /// - /// Computes the direct-map slot for a feature and script. Folds all four tag - /// bytes so features sharing trailing characters spread across slots rather than - /// thrashing one. - /// - /// The feature tag. - /// The script class. - /// The slot index. - private static int FeatureLookupsCacheSlot(Tag feature, ScriptClass script) - { - uint value = feature.Value; - return (int)((value ^ (value >> 8) ^ (value >> 16) ^ (value >> 24) ^ (uint)script) & 127); + return plan; } /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs index 826599f83..714233345 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs @@ -174,21 +174,20 @@ public bool TryUpdatePositions(FontMetrics fontMetrics, ShapingBuffer buffer, ou kerned = false; bool updated = false; - // Segments recorded during an in-place substitution pass carry their shaper - // and plan; reuse them so one plan drives both tables and positioning never + // Segments recorded during an in-place substitution pass carry their plan; + // reuse them so one plan drives both tables and positioning never // re-segments, re-creates, or re-plans. An empty list means records were // seeded across buffers and positioning must segment for itself below. - List<(int Index, int Count, ScriptClass Script, BaseShaper Shaper)> segments = buffer.SegmentShapers; + List<(int Index, int Count, ScriptClass Script, ShapePlan Plan)> segments = buffer.SegmentPlans; if (segments.Count > 0) { for (int s = 0; s < segments.Count; s++) { - (int index, int count, ScriptClass script, BaseShaper shaper) = segments[s]; + (int index, int count, ScriptClass script, ShapePlan shapePlan) = segments[s]; updated |= this.PositionSegment( fontMetrics, buffer, - shaper, - script, + shapePlan, index, count, maxOperationsCount, @@ -251,17 +250,18 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas } Tag unicodeScriptTag = this.GetUnicodeScriptTag(current); - BaseShaper shaper = buffer.GetOrCreateShaper(current, unicodeScriptTag, fontMetrics); + ShapePlan shapePlan = buffer.GetOrCreatePlan(current, unicodeScriptTag, fontMetrics); // Plan positioning features for each glyph. Records seeded across buffers // had their feature registrations cleared, so this pass re-plans. - shaper.Plan(buffer, index, count); + buffer.CurrentPlan = shapePlan; + shapePlan.Shaper.Plan(buffer, index, count); + buffer.CurrentPlan = null; updated |= this.PositionSegment( fontMetrics, buffer, - shaper, - current, + shapePlan, index, count, maxOperationsCount, @@ -278,17 +278,44 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas return updated; } + /// + /// Fills the merge scratch from a plan group's prebuilt lookup list, combining + /// the per-pass masks of every feature that registered each lookup. The prebuilt + /// list already carries lookup-index order and deduplication, so this is a copy + /// with mask lookups rather than a merge. + /// + /// The plan stage group to read. + /// The pass's feature bit assignment. + /// The merge scratch to fill. + private static void FillMergedFromPlan( + ShapePlanStageGroup group, + ShapingFeatureMap featureMap, + List<(Tag Feature, ushort Index, LookupTable LookupTable, ulong Mask)> merged) + { + merged.Clear(); + List<(Tag Feature, ushort Index, LookupTable LookupTable, Tag[] Contributing)> lookups = group.Lookups; + for (int i = 0; i < lookups.Count; i++) + { + (Tag feature, ushort lookupIndex, LookupTable lookupTable, Tag[] contributing) = lookups[i]; + ulong mask = 0; + for (int c = 0; c < contributing.Length; c++) + { + mask |= featureMap.GetMask(contributing[c]); + } + + merged.Add((feature, lookupIndex, lookupTable, mask)); + } + } + /// /// Applies the positioning stages of a planned segment: mark zeroing, the /// pause-delimited stage groups in lookup-index order, attachment resolution, and - /// position materialization. The caller supplies a shaper whose plan already - /// covers the segment, either fresh from re-planning or reused from the - /// substitution pass. + /// position materialization. The caller supplies the plan that covers the + /// segment, either freshly re-planned or reused from the substitution pass. /// /// The font metrics. /// The glyph positioning buffer. - /// The shaper whose plan covers the segment. - /// The script class the segment resolved to. + /// The plan covering the segment. /// The starting index of the segment. /// The number of glyphs in the segment. /// The maximum allowable operations count. @@ -299,8 +326,7 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas private bool PositionSegment( FontMetrics fontMetrics, ShapingBuffer buffer, - BaseShaper shaper, - ScriptClass script, + ShapePlan shapePlan, int index, int count, int maxOperationsCount, @@ -310,7 +336,7 @@ private bool PositionSegment( { bool updated = false; - if (shaper.MarkZeroingMode == MarkZeroingMode.PreGPos) + if (shapePlan.Shaper.MarkZeroingMode == MarkZeroingMode.PreGPos) { ZeroMarkAdvances(fontMetrics, buffer, index, count); } @@ -320,72 +346,22 @@ private bool PositionSegment( // feature's lookups apply together in lookup-list order, the order the // specification defines for lookups within a single application pass. A // lookup registered by several of the group's features applies once with - // their glyph masks combined. - List shapingStages = shaper.GetShapingStages(); + // their glyph masks combined. Group boundaries and merged lookup lists are + // prebuilt on the plan; only the per-pass masks are combined here. + buffer.CurrentPlan = shapePlan; + List> groups = shapePlan.GetOrBuildGPosStageGroups(this); + List shapingStages = shapePlan.Stages; SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, default, 0); List<(Tag Feature, ushort Index, LookupTable LookupTable, ulong Mask)> merged = buffer.GPosLookupScratch; - - int stageIndex = 0; - while (stageIndex < shapingStages.Count) + for (int g = 0; g < groups.Count; g++) { - shapingStages[stageIndex].PreProcessFeature(buffer, index, count); - - // Extend the group while its interior holds no actions: a post action - // closes the group after its stage and a pre action opens a new one. - int groupEnd = stageIndex; - while (true) - { - groupEnd++; - if (shapingStages[groupEnd - 1].HasPostAction || groupEnd >= shapingStages.Count || shapingStages[groupEnd].HasPreAction) - { - break; - } - } + ShapePlanStageGroup group = groups[g]; - // Merge the group's lookups into lookup-index order. Insertion keeps - // the scratch sorted; a lookup already present from another feature - // gains that feature's mask instead of a second entry. - merged.Clear(); - for (int s = stageIndex; s < groupEnd; s++) - { - Tag featureTag = shapingStages[s].FeatureTag; - var lookupProbe = ShapingProbe.Enter(); - bool found = this.TryGetFeatureLookups(fontMetrics, in featureTag, script, buffer.LanguageTags, out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? lookups); - ShapingProbe.Exit(ShapingProbe.LookupResolve, lookupProbe); - if (!found || lookups is null) - { - continue; - } + shapingStages[group.Start].PreProcessFeature(buffer, index, count); - ulong mask = buffer.FeatureMap.GetMask(featureTag); - foreach ((Tag Feature, ushort Index, LookupTable LookupTable) featureLookup in lookups) - { - int insertAt = merged.Count; - bool alreadyMerged = false; - while (insertAt > 0) - { - (Tag Feature, ushort Index, LookupTable LookupTable, ulong Mask) prior = merged[insertAt - 1]; - if (prior.Index == featureLookup.Index) - { - merged[insertAt - 1] = (prior.Feature, prior.Index, prior.LookupTable, prior.Mask | mask); - alreadyMerged = true; - break; - } - - if (prior.Index < featureLookup.Index) - { - break; - } - - insertAt--; - } - - if (!alreadyMerged) - { - merged.Insert(insertAt, (featureLookup.Feature, featureLookup.Index, featureLookup.LookupTable, mask)); - } - } - } + var lookupProbe = ShapingProbe.Enter(); + FillMergedFromPlan(group, buffer.FeatureMap, merged); + ShapingProbe.Exit(ShapingProbe.LookupResolve, lookupProbe); for (int m = 0; m < merged.Count; m++) { @@ -431,12 +407,12 @@ private bool PositionSegment( ShapingProbe.ExitFeature("GPOS", feature, featureStart, featureApplies); } - shapingStages[groupEnd - 1].PostProcessFeature(buffer, index, count); - stageIndex = groupEnd; + shapingStages[group.End - 1].PostProcessFeature(buffer, index, count); } EndLookups: - if (shaper.MarkZeroingMode == MarkZeroingMode.PostGpos) + buffer.CurrentPlan = null; + if (shapePlan.Shaper.MarkZeroingMode == MarkZeroingMode.PostGpos) { ZeroMarkAdvances(fontMetrics, buffer, index, count); } @@ -460,7 +436,7 @@ private bool PositionSegment( /// /// When this method returns, contains the list of feature lookups if found. /// if lookups were found; otherwise, . - private bool TryGetFeatureLookups( + public bool TryGetFeatureLookups( FontMetrics fontMetrics, in Tag stageFeature, ScriptClass script, diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs index 4b85c46b9..5a5eee3f5 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs @@ -195,9 +195,12 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas Tag unicodeScriptTag = this.GetUnicodeScriptTag(current); var createProbe = ShapingProbe.Enter(); - BaseShaper shaper = buffer.GetOrCreateShaper(current, unicodeScriptTag, fontMetrics); + ShapePlan shapePlan = buffer.GetOrCreatePlan(current, unicodeScriptTag, fontMetrics); ShapingProbe.Exit(ShapingProbe.SubShaperCreate, createProbe); + buffer.CurrentPlan = shapePlan; + BaseShaper shaper = shapePlan.Shaper; + // Plan substitution features for each glyph. // Shapers can adjust the count during initialization and feature processing so we must capture // the current count to allow resetting indexes and processing counts. @@ -214,17 +217,19 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas // feature's lookups apply together in lookup-list order, the order the // specification defines for lookups within a single application pass. A // lookup registered by several of the group's features applies once with - // their glyph masks combined. - List stages = shaper.GetShapingStages(); + // their glyph masks combined. Group boundaries and merged lookup lists + // are prebuilt on the plan; only the per-pass masks are combined here. + List> groups = shapePlan.GetOrBuildGSubStageGroups(); + List stages = shapePlan.Stages; SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, default, 0); List<(Tag Feature, ushort Index, LookupTable LookupTable, ulong Mask)> merged = buffer.GSubLookupScratch; - - int stageIndex = 0; - while (stageIndex < stages.Count) + for (int g = 0; g < groups.Count; g++) { + ShapePlanStageGroup group = groups[g]; + collectionCount = buffer.Count; var preProbe = ShapingProbe.Enter(); - stages[stageIndex].PreProcessFeature(buffer, index, count); + stages[group.Start].PreProcessFeature(buffer, index, count); ShapingProbe.Exit(ShapingProbe.SubStagePrePost, preProbe); // Account for substitutions changing the length of the buffer. @@ -232,62 +237,9 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas count += delta; i += delta; - // Extend the group while its interior holds no actions: a post action - // closes the group after its stage and a pre action opens a new one. - int groupEnd = stageIndex; - while (true) - { - groupEnd++; - if (stages[groupEnd - 1].HasPostAction || groupEnd >= stages.Count || stages[groupEnd].HasPreAction) - { - break; - } - } - - // Merge the group's lookups into lookup-index order. Insertion keeps - // the scratch sorted; a lookup already present from another feature - // gains that feature's mask instead of a second entry. - merged.Clear(); - for (int s = stageIndex; s < groupEnd; s++) - { - Tag featureTag = stages[s].FeatureTag; - var lookupProbe = ShapingProbe.Enter(); - bool found = this.TryGetFeatureLookups(fontMetrics, in featureTag, current, buffer, out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? lookups); - ShapingProbe.Exit(ShapingProbe.LookupResolve, lookupProbe); - if (!found || lookups is null) - { - continue; - } - - ulong mask = buffer.FeatureMap.GetMask(featureTag); - foreach ((Tag Feature, ushort Index, LookupTable LookupTable) featureLookup in lookups) - { - int insertAt = merged.Count; - bool alreadyMerged = false; - while (insertAt > 0) - { - (Tag Feature, ushort Index, LookupTable LookupTable, ulong Mask) prior = merged[insertAt - 1]; - if (prior.Index == featureLookup.Index) - { - merged[insertAt - 1] = (prior.Feature, prior.Index, prior.LookupTable, prior.Mask | mask); - alreadyMerged = true; - break; - } - - if (prior.Index < featureLookup.Index) - { - break; - } - - insertAt--; - } - - if (!alreadyMerged) - { - merged.Insert(insertAt, (featureLookup.Feature, featureLookup.Index, featureLookup.LookupTable, mask)); - } - } - } + var lookupProbe = ShapingProbe.Enter(); + FillMergedFromPlan(group, buffer.FeatureMap, merged); + ShapingProbe.Exit(ShapingProbe.LookupResolve, lookupProbe); var applyProbe = ShapingProbe.Enter(); this.ApplyMergedLookups( @@ -306,21 +258,20 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas collectionCount = buffer.Count; var postProbe = ShapingProbe.Enter(); - stages[groupEnd - 1].PostProcessFeature(buffer, index, count); + stages[group.End - 1].PostProcessFeature(buffer, index, count); ShapingProbe.Exit(ShapingProbe.SubStagePrePost, postProbe); // Account for substitutions changing the length of the buffer. delta = buffer.Count - collectionCount; count += delta; i += delta; - - stageIndex = groupEnd; } + buffer.CurrentPlan = null; + // Record the segment with its post-substitution range so the in-place - // positioning pass can reuse the shaper and its plan; one plan then - // drives both tables. - buffer.SegmentShapers.Add((index, count, current, shaper)); + // positioning pass can reuse the plan; one plan then drives both tables. + buffer.SegmentPlans.Add((index, count, current, shapePlan)); } } @@ -421,15 +372,43 @@ private void ApplyMergedLookups( } /// - /// Tries to get the feature lookups for the given stage feature, script, and the - /// buffer's language candidates. + /// Fills the merge scratch from a plan group's prebuilt lookup list, combining + /// the per-pass masks of every feature that registered each lookup. The prebuilt + /// list already carries lookup-index order and deduplication, so this is a copy + /// with mask lookups rather than a merge. + /// + /// The plan stage group to read. + /// The pass's feature bit assignment. + /// The merge scratch to fill. + private static void FillMergedFromPlan( + ShapePlanStageGroup group, + ShapingFeatureMap featureMap, + List<(Tag Feature, ushort Index, LookupTable LookupTable, ulong Mask)> merged) + { + merged.Clear(); + List<(Tag Feature, ushort Index, LookupTable LookupTable, Tag[] Contributing)> lookups = group.Lookups; + for (int i = 0; i < lookups.Count; i++) + { + (Tag feature, ushort lookupIndex, LookupTable lookupTable, Tag[] contributing) = lookups[i]; + ulong mask = 0; + for (int c = 0; c < contributing.Length; c++) + { + mask |= featureMap.GetMask(contributing[c]); + } + + merged.Add((feature, lookupIndex, lookupTable, mask)); + } + } + + /// + /// Tries to get the feature lookups for the given stage feature, script, and language. /// /// The font metrics. /// The feature tag for the current shaping stage. /// The script class. - /// - /// The glyph shaping buffer carrying the language candidates and the per-pass - /// resolution cache. + /// + /// The candidate OpenType language system tags, most specific first. An empty array + /// selects the default language system. /// /// When this method returns, contains the list of feature lookups if found. /// if lookups were found; otherwise, . @@ -437,7 +416,7 @@ public bool TryGetFeatureLookups( FontMetrics fontMetrics, in Tag stageFeature, ScriptClass script, - ShapingBuffer buffer, + Tag[] languageTags, [NotNullWhen(true)] out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? value) { if (this.ScriptList is null) @@ -446,8 +425,6 @@ public bool TryGetFeatureLookups( return false; } - Tag[] languageTags = buffer.LanguageTags; - // Feature variations resolve against the font's live variation coordinates, so // caching would mix results across differently configured variable fonts. if (this.FeatureVariations is not null) @@ -456,15 +433,6 @@ public bool TryGetFeatureLookups( return value.Count > 0; } - // The buffer fronts the table cache with a direct-mapped cache whose hit is - // one load and one compare, skipping the dictionary probe that hashes the - // language candidates per query. - if (buffer.TryGetFeatureLookups(this, stageFeature, script, out object? cached)) - { - value = (List<(Tag Feature, ushort Index, LookupTable LookupTable)>)cached!; - return value.Count > 0; - } - // Resolution depends only on this table's data for a given feature, script, // and language candidates, so results, including empty ones, are cached for // the table's lifetime. The cached list is shared: consumers must not mutate @@ -476,7 +444,6 @@ public bool TryGetFeatureLookups( this.featureLookupsCache.TryAdd(key, value); } - buffer.SetFeatureLookups(stageFeature, script, value); return value.Count > 0; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlan.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlan.cs new file mode 100644 index 000000000..e9e240a37 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlan.cs @@ -0,0 +1,392 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using SixLabors.Fonts.Tables.AdvancedTypographic.Shapers; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic; + +/// +/// The prebuilt shaping plan for one (font, script, options) identity: the shaper, +/// its stage list, and per layout table the pause-delimited stage groups with their +/// merged lookup lists. Structure that does not depend on the text is resolved once +/// here; per-glyph feature assignment and mask values remain per-pass state. Plans +/// are immutable after construction apart from the lazily built positioning groups, +/// and are only shared within a single buffer, which is exclusively owned. +/// +internal sealed class ShapePlan +{ + /// + /// The substitution table the plan resolved against, or + /// when the font has none; retained for pause-time feature queries that fall + /// outside the resolved set. + /// + private readonly GSubTable? gsubTable; + + /// + /// The features resolved while building the substitution groups, kept as a flat + /// list because a plan resolves a few dozen at most. Pause-time queries from the + /// complex shapers read these instead of re-resolving per syllable. + /// + private readonly List<(Tag Feature, List<(Tag Feature, ushort Index, GSub.LookupTable LookupTable)>? Lookups)> resolvedGsubFeatures = new(); + + /// + /// The lazily built substitution stage groups. Shapers register their stages + /// while planning a segment, not at construction, so groups can only be built + /// after the first plan invocation; a stage count change afterwards triggers a + /// rebuild so conditionally registered stages are never missed. + /// + private List>? gsubStageGroups; + + /// + /// The lazily built positioning stage groups. Positioning tables are only + /// reachable from the positioning pass, so the first positioning use builds + /// them. + /// + private List>? gposStageGroups; + + /// + /// The stage count the groups were built over. A later plan invocation that + /// grows the stage list invalidates both tables' groups. + /// + private int builtStageCount = -1; + + /// + /// Initializes a new instance of the class. Use + /// , which also resolves the substitution groups. + /// + /// The font the plan binds to. + /// The script class the plan shapes. + /// The resolved OpenType script tag. + /// The shaper constructed for the plan. + /// The shaper's stage list. + /// The language system candidates the plan resolved under. + /// The substitution table, or . + private ShapePlan( + FontMetrics fontMetrics, + ScriptClass script, + Tag scriptTag, + BaseShaper shaper, + List stages, + Tag[] languageTags, + GSubTable? gsubTable) + { + this.FontMetrics = fontMetrics; + this.Script = script; + this.ScriptTag = scriptTag; + this.Shaper = shaper; + this.Stages = stages; + this.LanguageTags = languageTags; + this.gsubTable = gsubTable; + } + + /// + /// Gets the font the plan binds to. + /// + public FontMetrics FontMetrics { get; } + + /// + /// Gets the script class the plan shapes. + /// + public ScriptClass Script { get; } + + /// + /// Gets the resolved OpenType script tag. + /// + public Tag ScriptTag { get; } + + /// + /// Gets the shaper constructed for the plan. Its per-segment state is reassigned + /// at every pause invocation, so reusing the instance across segments and passes + /// is safe within the owning buffer. + /// + public BaseShaper Shaper { get; } + + /// + /// Gets the shaper's live stage list; group boundaries index into it. Shapers + /// append stages while planning, so the list is only complete after a plan + /// invocation and additions are deduplicated by feature tag. + /// + public List Stages { get; } + + /// + /// Gets the language system candidates the plan resolved under. + /// + public Tag[] LanguageTags { get; } + + /// + /// Gets a value indicating whether the plan may be cached. A substitution table + /// carrying feature variations resolves against live variation coordinates, so + /// its plans must be rebuilt per operation. + /// + public bool IsCacheable => this.gsubTable?.FeatureVariations is null; + + /// + /// Builds the plan for the given identity: constructs the shaper and captures + /// its live stage list. Stage groups are built lazily by the first + /// call after a plan invocation has + /// registered the shaper's stages. + /// + /// The font the plan binds to. + /// The script class the plan shapes. + /// The resolved OpenType script tag. + /// The options whose values the shaper captures. + /// The language system candidates to resolve under. + /// The built . + public static ShapePlan Build( + FontMetrics fontMetrics, + ScriptClass script, + Tag scriptTag, + TextOptions textOptions, + Tag[] languageTags) + { + BaseShaper shaper = ShaperFactory.Create(script, scriptTag, fontMetrics, textOptions); + List stages = shaper.GetShapingStages(); + + _ = fontMetrics.TryGetGSubTable(out GSubTable? gsubTable); + return new(fontMetrics, script, scriptTag, shaper, stages, languageTags, gsubTable); + } + + /// + /// Gets the substitution stage groups, building them from the live stage list + /// on first use and rebuilding both tables' groups if a later plan invocation + /// registered additional stages. + /// + /// The substitution stage groups. + public List> GetOrBuildGSubStageGroups() + { + // The build body lives in a separate method because its resolver captures + // state: keeping the lambda out of this method keeps the steady-state hit + // path free of closure allocation, which would otherwise occur on every + // call at method entry regardless of the early return. + if (this.gsubStageGroups is not null && this.Stages.Count == this.builtStageCount) + { + return this.gsubStageGroups; + } + + return this.BuildGSubStageGroups(); + } + + /// + /// Gets the positioning stage groups, building them on first use over the same + /// stage boundaries as the substitution groups. A positioning table carrying + /// feature variations resolves against live variation coordinates, so its + /// groups are rebuilt on every call instead of stored. + /// + /// The positioning table to resolve against. + /// The positioning stage groups. + public List> GetOrBuildGPosStageGroups(GPosTable gposTable) + { + // Ensure the group boundaries reflect the current stage list before the + // positioning groups are built or served. The build body lives in a + // separate method for the same closure-allocation reason as the + // substitution side. + _ = this.GetOrBuildGSubStageGroups(); + + if (this.gposStageGroups is not null && gposTable.FeatureVariations is null) + { + return this.gposStageGroups; + } + + return this.BuildGPosStageGroups(gposTable); + } + + /// + /// Builds the substitution stage groups from the live stage list, invalidating + /// everything built over the old boundaries first: the positioning groups and + /// the resolved feature snapshot both index the same partition. + /// + /// The substitution stage groups. + private List> BuildGSubStageGroups() + { + this.builtStageCount = this.Stages.Count; + this.gposStageGroups = null; + this.resolvedGsubFeatures.Clear(); + + List> groups = new(); + GSubTable? gsubTable = this.gsubTable; + FontMetrics fontMetrics = this.FontMetrics; + ScriptClass script = this.Script; + Tag[] languageTags = this.LanguageTags; + BuildStageGroups( + this.Stages, + groups, + (in Tag featureTag, out List<(Tag Feature, ushort Index, GSub.LookupTable LookupTable)>? lookups) => + { + if (gsubTable is null) + { + lookups = null; + return false; + } + + bool found = gsubTable.TryGetFeatureLookups(fontMetrics, in featureTag, script, languageTags, out lookups); + this.resolvedGsubFeatures.Add((featureTag, lookups)); + return found; + }); + + this.gsubStageGroups = groups; + return groups; + } + + /// + /// Builds the positioning stage groups over the current stage boundaries and + /// stores them unless the table's resolution depends on live variation + /// coordinates. + /// + /// The positioning table to resolve against. + /// The positioning stage groups. + private List> BuildGPosStageGroups(GPosTable gposTable) + { + List> groups = new(); + FontMetrics fontMetrics = this.FontMetrics; + ScriptClass script = this.Script; + Tag[] languageTags = this.LanguageTags; + BuildStageGroups( + this.Stages, + groups, + (in Tag featureTag, out List<(Tag Feature, ushort Index, GPos.LookupTable LookupTable)>? lookups) => + gposTable.TryGetFeatureLookups(fontMetrics, in featureTag, script, languageTags, out lookups)); + + if (gposTable.FeatureVariations is null) + { + this.gposStageGroups = groups; + } + + return groups; + } + + /// + /// Gets the resolved substitution lookups for a feature, serving pause-time + /// queries from the complex shapers. Features the plan resolved while building + /// its groups answer from the plan; anything else falls through to the table so + /// behavior never depends on the plan's coverage. + /// + /// The feature tag to query. + /// When this method returns, contains the feature's lookups if any. + /// if the feature resolves to at least one lookup. + public bool TryGetGSubFeatureLookups(in Tag featureTag, [NotNullWhen(true)] out List<(Tag Feature, ushort Index, GSub.LookupTable LookupTable)>? lookups) + { + List<(Tag Feature, List<(Tag Feature, ushort Index, GSub.LookupTable LookupTable)>? Lookups)> resolved = this.resolvedGsubFeatures; + for (int i = 0; i < resolved.Count; i++) + { + if (resolved[i].Feature == featureTag) + { + lookups = resolved[i].Lookups; + return lookups is not null && lookups.Count > 0; + } + } + + if (this.gsubTable is null) + { + lookups = null; + return false; + } + + return this.gsubTable.TryGetFeatureLookups(this.FontMetrics, in featureTag, this.Script, this.LanguageTags, out lookups); + } + + /// + /// Computes the pause-delimited stage groups over the stage list and resolves + /// each group's features into a merged lookup list sorted by lookup index. A + /// lookup registered by several of a group's features gains each feature in its + /// contributing set instead of a second entry. + /// + /// The layout table's lookup type. + /// The stage list to group. + /// The group list to fill. + /// The per-feature lookup resolver for the table. + private static void BuildStageGroups( + List stages, + List> groups, + ShapePlanLookupResolver resolver) + { + // The stage list is a flat sequence of (feature, optional pre action, + // optional post action) entries. Application order requires that every + // lookup registered by stages before a pause action has applied before + // that action runs, so the sequence partitions into groups: a stage with + // a pre action starts a new group, and a stage with a post action ends + // its group. Within one group all features apply together, ordered by + // lookup index rather than by stage, because the specification orders + // lookups within a single application pass by their position in the + // font's lookup list. + int stageIndex = 0; + while (stageIndex < stages.Count) + { + // Walk forward from the group's first stage until something closes + // it: the current stage carries a post action, the list ends, or the + // next stage carries a pre action and therefore opens the next group. + int groupEnd = stageIndex; + while (true) + { + groupEnd++; + if (stages[groupEnd - 1].HasPostAction || groupEnd >= stages.Count || stages[groupEnd].HasPreAction) + { + break; + } + } + + // Resolve each stage feature in the group to its lookups and fold + // them into one list ordered by lookup index. The scan below runs + // backwards from the tail because resolved lookups arrive mostly + // ascending, so the insertion point is almost always at or near the + // end. + ShapePlanStageGroup group = new(stageIndex, groupEnd); + List<(Tag Feature, ushort Index, TLookup LookupTable, Tag[] Contributing)> merged = group.Lookups; + for (int s = stageIndex; s < groupEnd; s++) + { + Tag featureTag = stages[s].FeatureTag; + if (!resolver(in featureTag, out List<(Tag Feature, ushort Index, TLookup LookupTable)>? lookups) || lookups is null) + { + continue; + } + + foreach ((Tag Feature, ushort Index, TLookup LookupTable) featureLookup in lookups) + { + // Scan from the tail toward the head. Three outcomes: the + // lookup index is already present, so this feature joins its + // contributing set and no entry is added; a smaller index is + // found, so the new entry inserts directly after it; or the + // head is reached, so the new entry inserts first. + int insertAt = merged.Count; + bool alreadyMerged = false; + while (insertAt > 0) + { + (Tag Feature, ushort Index, TLookup LookupTable, Tag[] Contributing) prior = merged[insertAt - 1]; + if (prior.Index == featureLookup.Index) + { + // Same lookup registered by another of the group's + // features: record the feature so apply time can + // combine both features' masks, exactly as two + // separate entries would have. + Tag[] contributing = new Tag[prior.Contributing.Length + 1]; + prior.Contributing.CopyTo(contributing, 0); + contributing[^1] = featureTag; + merged[insertAt - 1] = (prior.Feature, prior.Index, prior.LookupTable, contributing); + alreadyMerged = true; + break; + } + + if (prior.Index < featureLookup.Index) + { + break; + } + + insertAt--; + } + + if (!alreadyMerged) + { + merged.Insert(insertAt, (featureLookup.Feature, featureLookup.Index, featureLookup.LookupTable, new[] { featureTag })); + } + } + } + + groups.Add(group); + + // The next group starts where this one ended; every stage belongs to + // exactly one group. + stageIndex = groupEnd; + } + } +} diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanLookupResolver{TLookup}.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanLookupResolver{TLookup}.cs new file mode 100644 index 000000000..744db382c --- /dev/null +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanLookupResolver{TLookup}.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.AdvancedTypographic; + +/// +/// Resolves a feature tag to its lookups for one layout table while a shape plan is +/// being built. Runs only at plan build; application reads the plan's prebuilt +/// lists. +/// +/// The layout table's lookup type. +/// The feature tag to resolve. +/// When this method returns, contains the resolved lookups if any. +/// if the feature resolved to at least one lookup. +internal delegate bool ShapePlanLookupResolver(in Tag featureTag, out List<(Tag Feature, ushort Index, TLookup LookupTable)>? lookups); diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanStageGroup{TLookup}.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanStageGroup{TLookup}.cs new file mode 100644 index 000000000..aae5bc52e --- /dev/null +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanStageGroup{TLookup}.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.AdvancedTypographic; + +/// +/// One pause-delimited stage group of a shape plan: the stage index range it covers +/// and the group's lookups merged into lookup-index order, each entry recording every +/// feature that registered it so per-pass masks can be combined at apply time. +/// +/// The layout table's lookup type. +internal sealed class ShapePlanStageGroup +{ + /// + /// Initializes a new instance of the + /// class covering the given stage range. + /// + /// The index of the first stage in the group. + /// The exclusive index of the last stage in the group. + public ShapePlanStageGroup(int start, int end) + { + this.Start = start; + this.End = end; + this.Lookups = new(); + } + + /// + /// Gets the index of the first stage in the group; its pre action opens the + /// group. + /// + public int Start { get; } + + /// + /// Gets the exclusive index of the last stage in the group; the previous stage's + /// post action closes the group. + /// + public int End { get; } + + /// + /// Gets the group's lookups merged across its stage features into lookup-index + /// order. A lookup registered by several features appears once, with every + /// registering feature recorded so apply-time masks combine exactly as separate + /// entries would have. + /// + public List<(Tag Feature, ushort Index, TLookup LookupTable, Tag[] Contributing)> Lookups { get; } +} diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs index b728ad20d..757ba53d4 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs @@ -478,7 +478,6 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) } } - _ = fontMetrics.TryGetGSubTable(out GSubTable? gSubTable); while (start < max) { if (buffer[start].Syllable.Type is SyllableType.SymbolCluster or SyllableType.NonIndicCluster) @@ -503,7 +502,7 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) // base consonants. if (start + 3 <= end && indicConfiguration.RephPosition != Positions.Ra_To_Become_Reph && - gSubTable?.TryGetFeatureLookups(fontMetrics, in RphfTag, this.ScriptClass, buffer, out _) == true && + buffer.CurrentPlan?.TryGetGSubFeatureLookups(in RphfTag, out _) == true && ((indicConfiguration.RephMode == RephMode.Implicit && !IsJoiner(ref buffer[start + 2])) || (indicConfiguration.RephMode == RephMode.Explicit && buffer[start + 2].Syllable.IndicCategory == Categories.ZWJ))) { @@ -902,7 +901,7 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) const int prefLen = 2; if (basePosition + prefLen < end && - gSubTable?.TryGetFeatureLookups(fontMetrics, in PrefTag, this.ScriptClass, buffer, out _) == true) + buffer.CurrentPlan?.TryGetGSubFeatureLookups(in PrefTag, out _) == true) { // Find a Halant,Ra sequence and mark it for pre-base reordering processing. for (int i = basePosition + 1; i + prefLen - 1 < end; i++) @@ -921,7 +920,7 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) // This allows distinguishing the following cases with MS Khmer fonts: // U+1784,U+17D2,U+179A,U+17D2,U+1782 // U+1784,U+17D2,U+1782,U+17D2,U+179A - if (gSubTable.TryGetFeatureLookups(fontMetrics, in CfarTag, this.ScriptClass, buffer, out _)) + if (buffer.CurrentPlan?.TryGetGSubFeatureLookups(in CfarTag, out _) == true) { while (i < end) { @@ -1018,8 +1017,19 @@ private Positions ConsonantPosition(ShapingBuffer buffer, ushort virama, ushort /// if a substitution would occur. private bool WouldSubstitute(ShapingBuffer buffer, in Tag featureTag, ReadOnlySpan glyphs) { - if (!this.fontMetrics.TryGetGSubTable(out GSubTable? gSubTable) || - !gSubTable.TryGetFeatureLookups(this.fontMetrics, in featureTag, this.ScriptClass, buffer, out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? lookups)) + // Pause actions always run inside segment processing, where the segment's + // plan is current; the table fallback keeps behavior correct if this is + // ever reached outside that window. + List<(Tag Feature, ushort Index, LookupTable LookupTable)>? lookups; + if (buffer.CurrentPlan is { } plan) + { + if (!plan.TryGetGSubFeatureLookups(in featureTag, out lookups)) + { + return false; + } + } + else if (!this.fontMetrics.TryGetGSubTable(out GSubTable? gSubTable) || + !gSubTable.TryGetFeatureLookups(this.fontMetrics, in featureTag, this.ScriptClass, buffer.LanguageTags, out lookups)) { return false; } @@ -1103,7 +1113,6 @@ private void FinalReorder(ShapingBuffer buffer, int index, int count) int start = index; int end = NextSyllable(buffer, index, max); FontMetrics fontMetrics = this.fontMetrics; - _ = fontMetrics.TryGetGSubTable(out GSubTable? gSubTable); while (start < max) { // 4. Final reordering: @@ -1112,7 +1121,7 @@ private void FinalReorder(ShapingBuffer buffer, int index, int count) // applied (see below), the shaping engine performs some final glyph // reordering before applying all the remaining font features to the entire // cluster. - bool tryPref = gSubTable?.TryGetFeatureLookups(fontMetrics, in PrefTag, this.ScriptClass, buffer, out _) == true; + bool tryPref = buffer.CurrentPlan?.TryGetGSubFeatureLookups(in PrefTag, out _) == true; // Find base consonant again. int basePosition = start; From 87e53ad72c6b6acc64369639e6182677159600f4 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Jul 2026 22:53:08 +1000 Subject: [PATCH 50/94] Move feature mask ownership into the shape plan Feature mask bits were assigned by a per-pass map that reassigned them every pass, forcing per-operation mask resolution everywhere and coupling the two buffers of a pass through a shared instance. Bits are now owned by the plan: assigned in stage-list order when the groups are built, append-only afterwards, with the vertical trio in reserved bits identical across every plan. Group entries freeze their combined masks at build, so application reads pure prebuilt structure. A plan that exhausts its bits assigns nothing and the feature disables cleanly. Nothing is ambient: pause actions receive the plan, registration receives the feature bit from the shaper that owns it, the applying lookup mask threads through substitution application for sequence gating, and applied-mask recording reduces to the fixed vertical bits because vertical detection is its only consumer. The per-pass feature map type is deleted. | Scenario | Before | After | |------------|----------------------------|----------------------------| | Latin | 6.07 us / 2.61x / 6,216 B | 5.56 us / 2.49x / 6,216 B | | Arabic | 7.78 us / 1.99x / 4,368 B | 7.37 us / 1.93x / 4,368 B | | Devanagari | 12.00 us / 1.34x / 3,832 B | 11.29 us / 1.27x / 3,832 B | All scenarios clear the keep bar; first sub-6 Latin. Allocations byte identical. Clean controls, 12-iteration protocol. Tests 5,585/0. --- .../Exceptions/FontsThrowHelper.cs | 12 ++ src/SixLabors.Fonts/GlyphShapingData.cs | 15 +- src/SixLabors.Fonts/ShapingBuffer.cs | 111 ++++-------- src/SixLabors.Fonts/ShapingScratch.cs | 15 +- .../AdvancedTypographicUtils.cs | 16 +- .../Tables/AdvancedTypographic/GPosTable.cs | 47 +---- .../GSub/LookupListTable.cs | 6 +- .../GSub/LookupType1SubTable.cs | 2 + .../GSub/LookupType2SubTable.cs | 1 + .../GSub/LookupType3SubTable.cs | 1 + .../GSub/LookupType4SubTable.cs | 3 +- .../GSub/LookupType5SubTable.cs | 6 + .../GSub/LookupType6SubTable.cs | 6 + .../GSub/LookupType8SubTable.cs | 1 + .../GSub/NotImplementedSubTable.cs | 1 + .../Tables/AdvancedTypographic/GSubTable.cs | 49 +---- .../Tables/AdvancedTypographic/ShapePlan.cs | 53 +++--- .../AdvancedTypographic/ShapePlanFeatures.cs | 170 ++++++++++++++++++ .../ShapePlanStageGroup{TLookup}.cs | 12 +- .../Shapers/ArabicShaper.cs | 14 +- .../AdvancedTypographic/Shapers/BaseShaper.cs | 8 + .../Shapers/DefaultShaper.cs | 6 +- .../Shapers/HangulShaper.cs | 34 ++-- .../Shapers/IndicShaper.cs | 89 +++++---- .../Shapers/MyanmarShaper.cs | 6 +- .../Shapers/ShapingStage.cs | 20 ++- .../Shapers/UniversalShaper.cs | 19 +- .../AdvancedTypographic/ShapingFeatureMap.cs | 134 -------------- src/SixLabors.Fonts/TextShaper.Pipeline.cs | 7 +- 29 files changed, 420 insertions(+), 444 deletions(-) create mode 100644 src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanFeatures.cs delete mode 100644 src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapingFeatureMap.cs diff --git a/src/SixLabors.Fonts/Exceptions/FontsThrowHelper.cs b/src/SixLabors.Fonts/Exceptions/FontsThrowHelper.cs index 958bdf58b..f9ff9a3f3 100644 --- a/src/SixLabors.Fonts/Exceptions/FontsThrowHelper.cs +++ b/src/SixLabors.Fonts/Exceptions/FontsThrowHelper.cs @@ -24,4 +24,16 @@ public static T ThrowGlyphMissingException(CodePoint codePoint) [MethodImpl(MethodImplOptions.NoInlining)] public static void ThrowDefaultInstance() => throw new FontException("Cannot use the default value type instance to create a font."); + + /// + /// Throws an recording that shaping + /// state was queried outside a segment window, where no shape plan is current. + /// Kept out of the accessor so it stays inlinable. + /// + /// The declared result type of the failed accessor. + /// Never returns; the type satisfies the caller's flow analysis. + [MethodImpl(MethodImplOptions.NoInlining)] + public static T ThrowNoCurrentShapePlan() + => throw new InvalidOperationException( + "No shape plan is current; the operation is only valid while a segment is being shaped."); } diff --git a/src/SixLabors.Fonts/GlyphShapingData.cs b/src/SixLabors.Fonts/GlyphShapingData.cs index d77a05c29..608d2ee97 100644 --- a/src/SixLabors.Fonts/GlyphShapingData.cs +++ b/src/SixLabors.Fonts/GlyphShapingData.cs @@ -240,9 +240,10 @@ public int LigatureComponent /// /// Gets or sets the mask of features a shaper has registered for this glyph, enabled - /// or not. Bits are assigned by the shaping pass's . - /// Enabling a feature only ever reveals a registered bit; a feature that was never - /// registered for the glyph cannot be enabled. + /// or not. Bits are assigned by the owning plan's + /// . Enabling a feature + /// only ever reveals a registered bit; a feature that was never registered for the + /// glyph cannot be enabled. /// public ulong RegisteredFeatureMask { get; set; } @@ -255,10 +256,10 @@ public int LigatureComponent /// /// Gets or sets the mask of features whose lookups actually changed this glyph. - /// Read after shaping, for example to detect that a vertical alternate was - /// substituted. Survives the copy into the positioning collection, which is why the - /// substitution and positioning collections must share one - /// . + /// Only the vertical trio is recorded, in the reserved bits every plan shares, + /// because vertical alternate detection is the sole consumer; the record + /// therefore survives the copy into the positioning buffer regardless of which + /// plan wrote it. /// public ulong AppliedFeatureMask { get; set; } diff --git a/src/SixLabors.Fonts/ShapingBuffer.cs b/src/SixLabors.Fonts/ShapingBuffer.cs index e67c88715..623c0276b 100644 --- a/src/SixLabors.Fonts/ShapingBuffer.cs +++ b/src/SixLabors.Fonts/ShapingBuffer.cs @@ -168,18 +168,16 @@ internal sealed class ShapingBuffer /// Cleared when a reset adopts a different options instance, whose values the /// plans captured when built. /// - private readonly List<(ScriptClass Script, Tag ScriptTag, FontMetrics FontMetrics, Tables.AdvancedTypographic.ShapePlan Plan)> planCache = new(4); + private readonly List<(ScriptClass Script, Tag ScriptTag, FontMetrics FontMetrics, ShapePlan Plan)> planCache = new(4); /// /// Initializes a new instance of the class. /// /// The text options. - /// The feature bit assignment shared by the shaping pass. /// The shaping phase this buffer serves. - public ShapingBuffer(TextOptions textOptions, ShapingFeatureMap featureMap, ShapingBufferRole role) + public ShapingBuffer(TextOptions textOptions, ShapingBufferRole role) { this.TextOptions = textOptions; - this.FeatureMap = featureMap; this.Role = role; this.LanguageTags = ResolveLanguageTags(textOptions); } @@ -208,13 +206,6 @@ public ShapingBuffer(TextOptions textOptions, ShapingFeatureMap featureMap, Shap /// public Tag[] LanguageTags { get; private set; } - /// - /// Gets the feature bit assignment shared by every buffer of the shaping pass. - /// See for the mask model and why the instance must - /// be shared across the substitution and positioning phases. - /// - public ShapingFeatureMap FeatureMap { get; } - /// /// Gets the approximate membership filter over every glyph id the buffer has ever /// contained. The digest only grows: substituted-away ids remain, keeping a @@ -241,28 +232,7 @@ public ShapingBuffer(TextOptions textOptions, ShapingFeatureMap featureMap, Shap /// pass reuses these so one plan drives both tables; the list stays empty when /// records were seeded across buffers and positioning must segment for itself. /// - public List<(int Index, int Count, ScriptClass Script, Tables.AdvancedTypographic.ShapePlan Plan)> SegmentPlans { get; } = new(); - - /// - /// Gets or sets the plan for the segment currently being shaped. Pause actions - /// run inside segment processing and query it for prebuilt feature resolutions; - /// the layout tables set it around each segment and clear it after. - /// - public Tables.AdvancedTypographic.ShapePlan? CurrentPlan { get; set; } - - /// - /// Gets the reusable scratch the substitution table uses to merge a stage group's - /// lookups into lookup-index order. Cleared by each group merge; kept on the pooled - /// buffer so application allocates nothing. - /// - public List<(Tag Feature, ushort Index, Tables.AdvancedTypographic.GSub.LookupTable LookupTable, ulong Mask)> GSubLookupScratch { get; } = new(16); - - /// - /// Gets the reusable scratch the positioning table uses to merge a stage group's - /// lookups into lookup-index order. Cleared by each group merge; kept on the pooled - /// buffer so application allocates nothing. - /// - public List<(Tag Feature, ushort Index, Tables.AdvancedTypographic.GPos.LookupTable LookupTable, ulong Mask)> GPosLookupScratch { get; } = new(16); + public List<(int Index, int Count, ScriptClass Script, ShapePlan Plan)> SegmentPlans { get; } = new(); /// /// Gets an interior reference to the glyph shaping data at the specified index. @@ -331,7 +301,7 @@ public bool SeedMetricsInPlace(Font font) LayoutMode layoutMode = this.TextOptions.LayoutMode; ColorFontSupport colorFontSupport = this.TextOptions.ColorFontSupport; - ulong verticalMask = this.GetVerticalFeatureMask(); + ulong verticalMask = ShapePlanFeatures.VerticalFeatureMask; for (int i = 0; i < this.count; i++) { @@ -379,7 +349,6 @@ public void Reset(TextOptions textOptions) this.glyphDigest = default; this.placeholderBidiRuns.Clear(); this.SegmentPlans.Clear(); - this.CurrentPlan = null; // Cached plans captured option values when built, so a different options // instance invalidates them. @@ -419,7 +388,9 @@ public void SetGlyphId(int index, ushort glyphId) } /// - /// Adds the shaping feature to the record at the given index. + /// Adds the shaping feature to the record at the given index. The caller + /// supplies the feature's plan-assigned mask bit; the shaper that registers a + /// feature owns the plan whose bit it is, so the mask is always in scope. /// /// /// Registration only ever accumulates: adding a disabled entry for an already @@ -427,9 +398,9 @@ public void SetGlyphId(int index, ushort glyphId) /// /// The zero-based index of the record. /// The feature to apply. - public void AddShapingFeature(int index, TagEntry feature) + /// The feature's plan-assigned mask bit. + public void AddShapingFeature(int index, TagEntry feature, ulong mask) { - ulong mask = this.FeatureMap.GetOrAddMask(feature.Tag); ref GlyphShapingData item = ref this.data[index]; item.RegisteredFeatureMask |= mask; if (feature.Enabled) @@ -439,16 +410,17 @@ public void AddShapingFeature(int index, TagEntry feature) } /// - /// Adds the shaping feature to every record in the given range, resolving the - /// feature's mask bit once for the whole range. Shaper plans register each stage - /// feature across the full run, so the per-glyph work must be a single bitwise OR. + /// Adds the shaping feature to every record in the given range. The caller + /// resolves the feature's mask bit once for the whole range: shaper plans + /// register each stage feature across the full run, so the per-glyph work must + /// be a single bitwise OR. /// /// The zero-based index of the first record. /// The number of records in the range. /// The feature to apply. - public void AddShapingFeatureRange(int index, int count, TagEntry feature) + /// The feature's plan-assigned mask bit. + public void AddShapingFeatureRange(int index, int count, TagEntry feature, ulong mask) { - ulong mask = this.FeatureMap.GetOrAddMask(feature.Tag); int end = index + count; for (int i = index; i < end; i++) { @@ -462,32 +434,32 @@ public void AddShapingFeatureRange(int index, int count, TagEntry feature) } /// - /// Enables a previously added shaping feature. + /// Enables a previously added shaping feature by its plan-assigned mask bit. /// /// /// Intersecting with the registered mask preserves the contract that enabling a /// feature a shaper never added for this record is a no-op. /// /// The zero-based index of the record. - /// The feature to enable. - public void EnableShapingFeature(int index, Tag feature) + /// The feature's plan-assigned mask bit. + public void EnableShapingFeature(int index, ulong mask) { ref GlyphShapingData item = ref this.data[index]; - item.FeatureMask |= item.RegisteredFeatureMask & this.FeatureMap.GetMask(feature); + item.FeatureMask |= item.RegisteredFeatureMask & mask; } /// - /// Disables a previously added shaping feature. + /// Disables a previously added shaping feature by its plan-assigned mask bit. /// /// - /// An unregistered tag yields a zero mask whose complement clears nothing. + /// An unassigned feature yields a zero mask whose complement clears nothing. /// /// The zero-based index of the record. - /// The feature to disable. - public void DisableShapingFeature(int index, Tag feature) + /// The feature's plan-assigned mask bit. + public void DisableShapingFeature(int index, ulong mask) { ref GlyphShapingData item = ref this.data[index]; - item.FeatureMask &= ~this.FeatureMap.GetMask(feature); + item.FeatureMask &= ~mask; } /// @@ -716,7 +688,7 @@ public void Replace(int index, ushort glyphId, Tag feature) current.LigatureId = 0; current.LigatureComponent = -1; current.IsSubstituted = true; - current.AppliedFeatureMask |= this.FeatureMap.GetOrAddMask(feature); + current.AppliedFeatureMask |= ShapePlanFeatures.GetVerticalMask(feature); } /// @@ -764,7 +736,7 @@ public void Replace(int index, ReadOnlySpan removalIndices, ushort glyphId, current.IsLigated = true; current.LigatureComponent = -1; current.IsSubstituted = true; - current.AppliedFeatureMask |= this.FeatureMap.GetOrAddMask(feature); + current.AppliedFeatureMask |= ShapePlanFeatures.GetVerticalMask(feature); } /// @@ -810,7 +782,7 @@ public void Replace(int index, int count, ushort glyphId, Tag feature) current.LigatureId = 0; current.LigatureComponent = -1; current.IsSubstituted = true; - current.AppliedFeatureMask |= this.FeatureMap.GetOrAddMask(feature); + current.AppliedFeatureMask |= ShapePlanFeatures.GetVerticalMask(feature); } /// @@ -835,7 +807,7 @@ public void Replace(int index, ReadOnlySpan glyphIds, Tag feature) if (glyphIds.Length > 1) { GlyphShapingData template = this.data[index]; - ulong mask = this.FeatureMap.GetOrAddMask(feature); + ulong mask = ShapePlanFeatures.GetVerticalMask(feature); for (int i = 1; i < glyphIds.Length; i++) { GlyphShapingData inserted = new(template, false) @@ -889,7 +861,7 @@ public bool TryAdd(Font font, ShapingBuffer workspace) LayoutMode layoutMode = this.TextOptions.LayoutMode; ColorFontSupport colorFontSupport = this.TextOptions.ColorFontSupport; - ulong verticalMask = this.GetVerticalFeatureMask(); + ulong verticalMask = ShapePlanFeatures.VerticalFeatureMask; for (int i = 0; i < workspace.count; i++) { @@ -967,7 +939,7 @@ public bool TryUpdate(Font font, ShapingBuffer workspace) ColorFontSupport colorFontSupport = this.TextOptions.ColorFontSupport; bool hasFallBacks = false; - ulong verticalMask = this.GetVerticalFeatureMask(); + ulong verticalMask = ShapePlanFeatures.VerticalFeatureMask; for (int i = 0; i < this.count; i++) { @@ -1128,20 +1100,20 @@ public void SetShapingClass(ushort glyphId, GlyphShapingClass shapingClass) /// The script class to shape. /// The resolved OpenType script tag. /// The font metrics the plan binds to. - /// The . - public Tables.AdvancedTypographic.ShapePlan GetOrCreatePlan(ScriptClass script, Tag unicodeScriptTag, FontMetrics fontMetrics) + /// The . + public ShapePlan GetOrCreatePlan(ScriptClass script, Tag unicodeScriptTag, FontMetrics fontMetrics) { - List<(ScriptClass Script, Tag ScriptTag, FontMetrics FontMetrics, Tables.AdvancedTypographic.ShapePlan Plan)> cache = this.planCache; + List<(ScriptClass Script, Tag ScriptTag, FontMetrics FontMetrics, ShapePlan Plan)> cache = this.planCache; for (int i = 0; i < cache.Count; i++) { - (ScriptClass cachedScript, Tag cachedTag, FontMetrics cachedMetrics, Tables.AdvancedTypographic.ShapePlan cachedPlan) = cache[i]; + (ScriptClass cachedScript, Tag cachedTag, FontMetrics cachedMetrics, ShapePlan cachedPlan) = cache[i]; if (cachedScript == script && cachedTag == unicodeScriptTag && ReferenceEquals(cachedMetrics, fontMetrics)) { return cachedPlan; } } - Tables.AdvancedTypographic.ShapePlan plan = Tables.AdvancedTypographic.ShapePlan.Build(fontMetrics, script, unicodeScriptTag, this.TextOptions, this.LanguageTags); + ShapePlan plan = ShapePlan.Build(fontMetrics, script, unicodeScriptTag, this.TextOptions, this.LanguageTags); if (plan.IsCacheable) { cache.Add((script, unicodeScriptTag, fontMetrics, plan)); @@ -1226,7 +1198,7 @@ public void Advance(FontMetrics fontMetrics, int index, ushort glyphId, short dx } bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(m.CodePoint, this.TextOptions.LayoutMode) - || (this.data[index].AppliedFeatureMask & this.GetVerticalFeatureMask()) != 0; + || (this.data[index].AppliedFeatureMask & ShapePlanFeatures.VerticalFeatureMask) != 0; // Advance heights grow downward but font-space grows upward, hence the negation. this.positions[index].Bounds.Width += dx; @@ -1246,17 +1218,6 @@ public void Advance(FontMetrics fontMetrics, int index, ushort glyphId, short dx public bool ShouldProcess(FontMetrics fontMetrics, int index) => !this.positions[index].IsPositioned && this.metrics[index].Metrics.FontMetrics == fontMetrics; - /// - /// Gets the combined mask of the three vertical alternate features. Computed from - /// the shared feature map so it stays valid for applied bits written during - /// substitution and read after the seed into the positioning phase. - /// - /// The combined mask, or zero when no vertical feature was registered. - public ulong GetVerticalFeatureMask() - => this.FeatureMap.GetMask(KnownFeatureTags.VerticalAlternates) - | this.FeatureMap.GetMask(KnownFeatureTags.VerticalAlternatesForRotation) - | this.FeatureMap.GetMask(KnownFeatureTags.VerticalKerning); - /// /// Resolves the candidate OpenType language system tags for the options' culture. /// A null culture takes the ambient current culture; the invariant culture diff --git a/src/SixLabors.Fonts/ShapingScratch.cs b/src/SixLabors.Fonts/ShapingScratch.cs index a5d7ae7c3..d9d170ef3 100644 --- a/src/SixLabors.Fonts/ShapingScratch.cs +++ b/src/SixLabors.Fonts/ShapingScratch.cs @@ -19,12 +19,7 @@ namespace SixLabors.Fonts; /// internal sealed class ShapingScratch { - /// - /// The pass-wide feature bit assignment, reset per call. - /// - private ShapingFeatureMap? featureMap; - - /// + /// /// The per-font-run workspace buffer glyphs are substituted in. /// private ShapingBuffer? workspace; @@ -41,15 +36,13 @@ internal sealed class ShapingScratch /// The reusable buffers, sharing one feature map. public (ShapingBuffer Workspace, ShapingBuffer Result) Prepare(TextOptions options) { - if (this.featureMap is null) + if (this.workspace is null) { - this.featureMap = new(); - this.workspace = new(options, this.featureMap, ShapingBufferRole.Substitution); - this.result = new(options, this.featureMap, ShapingBufferRole.Positioning); + this.workspace = new(options, ShapingBufferRole.Substitution); + this.result = new(options, ShapingBufferRole.Positioning); } else { - this.featureMap.Reset(); this.workspace!.Reset(options); this.result!.Reset(options); diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs index 293448996..afc31c6c8 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs @@ -98,6 +98,7 @@ public static int GetMaxAllowableShapingOperationsCount(int length) /// The font metrics. /// The GSUB table. /// The feature tag being applied. + /// The applying lookup's combined mask, inherited by the nested lookups. /// The lookup flags for glyph filtering. /// The mark filtering set index. /// The sequence lookup records specifying which lookups to apply at which positions. @@ -109,6 +110,7 @@ public static bool ApplyLookupList( FontMetrics fontMetrics, GSubTable table, Tag feature, + ulong lookupMask, LookupFlags lookupFlags, ushort markFilteringSet, SequenceLookupRecord[] records, @@ -131,7 +133,7 @@ public static bool ApplyLookupList( iterator.Index = index; iterator.Increment(sequenceIndex); GSub.LookupTable lookup = table.LookupList.LookupTables[lookupIndex]; - _ = lookup.TrySubstitution(fontMetrics, table, buffer, feature, iterator.Index, count - (iterator.Index - index)); + _ = lookup.TrySubstitution(fontMetrics, table, buffer, feature, lookupMask, iterator.Index, count - (iterator.Index - index)); // Account for substitutions changing the length of the buffer. if (buffer.Count != currentCount) @@ -188,18 +190,16 @@ public static bool ApplyLookupList( } /// - /// Matches an input glyph sequence by glyph ID, verifying that each glyph has the specified feature enabled. + /// Matches an input glyph sequence by glyph ID, verifying that each glyph has the applying lookup enabled. /// /// The skipping glyph iterator. - /// The feature tag that must be enabled on matched glyphs. + /// The applying lookup's combined mask; matched glyphs must have it enabled. /// The initial increment from the iterator's current position. /// The array of glyph IDs to match. /// A span to store matched glyph indices, or default if not needed. /// if the entire sequence was matched; otherwise, . - public static bool MatchInputSequence(SkippingGlyphIterator iterator, Tag feature, ushort increment, ushort[] sequence, Span matches) + public static bool MatchInputSequence(SkippingGlyphIterator iterator, ulong featureMask, ushort increment, ushort[] sequence, Span matches) { - ulong featureMask = iterator.Collection.FeatureMap.GetMask(feature); - // The mask travels as match state so the lambda stays static: a capturing // lambda here would allocate a closure and delegate on every ligature attempt. return Match( @@ -478,7 +478,7 @@ public static void ApplyAnchor( markPosition.Bounds.X = baseXY.XCoordinate - markXY.XCoordinate; markPosition.Bounds.Y = baseXY.YCoordinate - markXY.YCoordinate; markPosition.MarkAttachment = baseGlyphIndex; - markData.AppliedFeatureMask |= buffer.FeatureMap.GetOrAddMask(feature); + markData.AppliedFeatureMask |= ShapePlanFeatures.GetVerticalMask(feature); } /// @@ -512,7 +512,7 @@ public static void ApplyPosition( } ref GlyphShapingData current = ref buffer[index]; - current.AppliedFeatureMask |= buffer.FeatureMap.GetOrAddMask(feature); + current.AppliedFeatureMask |= ShapePlanFeatures.GetVerticalMask(feature); } /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs index 714233345..475b28c9e 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs @@ -254,9 +254,7 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas // Plan positioning features for each glyph. Records seeded across buffers // had their feature registrations cleared, so this pass re-plans. - buffer.CurrentPlan = shapePlan; shapePlan.Shaper.Plan(buffer, index, count); - buffer.CurrentPlan = null; updated |= this.PositionSegment( fontMetrics, @@ -278,35 +276,6 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas return updated; } - /// - /// Fills the merge scratch from a plan group's prebuilt lookup list, combining - /// the per-pass masks of every feature that registered each lookup. The prebuilt - /// list already carries lookup-index order and deduplication, so this is a copy - /// with mask lookups rather than a merge. - /// - /// The plan stage group to read. - /// The pass's feature bit assignment. - /// The merge scratch to fill. - private static void FillMergedFromPlan( - ShapePlanStageGroup group, - ShapingFeatureMap featureMap, - List<(Tag Feature, ushort Index, LookupTable LookupTable, ulong Mask)> merged) - { - merged.Clear(); - List<(Tag Feature, ushort Index, LookupTable LookupTable, Tag[] Contributing)> lookups = group.Lookups; - for (int i = 0; i < lookups.Count; i++) - { - (Tag feature, ushort lookupIndex, LookupTable lookupTable, Tag[] contributing) = lookups[i]; - ulong mask = 0; - for (int c = 0; c < contributing.Length; c++) - { - mask |= featureMap.GetMask(contributing[c]); - } - - merged.Add((feature, lookupIndex, lookupTable, mask)); - } - } - /// /// Applies the positioning stages of a planned segment: mark zeroing, the /// pause-delimited stage groups in lookup-index order, attachment resolution, and @@ -346,22 +315,17 @@ private bool PositionSegment( // feature's lookups apply together in lookup-list order, the order the // specification defines for lookups within a single application pass. A // lookup registered by several of the group's features applies once with - // their glyph masks combined. Group boundaries and merged lookup lists are - // prebuilt on the plan; only the per-pass masks are combined here. - buffer.CurrentPlan = shapePlan; + // their glyph masks combined. Group boundaries, merged lookup lists, and + // entry masks are all prebuilt on the plan. List> groups = shapePlan.GetOrBuildGPosStageGroups(this); List shapingStages = shapePlan.Stages; SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, default, 0); - List<(Tag Feature, ushort Index, LookupTable LookupTable, ulong Mask)> merged = buffer.GPosLookupScratch; for (int g = 0; g < groups.Count; g++) { ShapePlanStageGroup group = groups[g]; + List<(Tag Feature, ushort Index, LookupTable LookupTable, ulong Mask)> merged = group.Lookups; - shapingStages[group.Start].PreProcessFeature(buffer, index, count); - - var lookupProbe = ShapingProbe.Enter(); - FillMergedFromPlan(group, buffer.FeatureMap, merged); - ShapingProbe.Exit(ShapingProbe.LookupResolve, lookupProbe); + shapingStages[group.Start].PreProcessFeature(shapePlan, buffer, index, count); for (int m = 0; m < merged.Count; m++) { @@ -407,11 +371,10 @@ private bool PositionSegment( ShapingProbe.ExitFeature("GPOS", feature, featureStart, featureApplies); } - shapingStages[group.End - 1].PostProcessFeature(buffer, index, count); + shapingStages[group.End - 1].PostProcessFeature(shapePlan, buffer, index, count); } EndLookups: - buffer.CurrentPlan = null; if (shapePlan.Shaper.MarkZeroingMode == MarkZeroingMode.PostGpos) { ZeroMarkAdvances(fontMetrics, buffer, index, count); diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs index 4bb5c7a04..fd5598c2e 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs @@ -194,6 +194,7 @@ public static LookupTable Load(BigEndianBinaryReader reader, long offset) /// The GSUB table. /// The glyph substitution buffer. /// The feature tag to apply. + /// The applying lookup's combined mask. /// The index in the buffer at which to attempt substitution. /// The number of glyphs in the input sequence to consider. /// if a substitution was performed; otherwise, . @@ -202,6 +203,7 @@ public bool TrySubstitution( GSubTable table, ShapingBuffer buffer, Tag feature, + ulong lookupMask, int index, int count) { @@ -221,7 +223,7 @@ public bool TrySubstitution( ShapingProbe.SubTableProbes++; } - if (subTable.TrySubstitution(fontMetrics, table, buffer, feature, index, count)) + if (subTable.TrySubstitution(fontMetrics, table, buffer, feature, lookupMask, index, count)) { // A lookup is finished for a glyph after the client locates the target // glyph or glyph context and performs a substitution, if specified. @@ -341,6 +343,7 @@ protected LookupSubTable(LookupFlags lookupFlags, ushort markFilteringSet) /// The GSUB table. /// The glyph substitution buffer. /// The feature tag to apply. + /// The applying lookup's combined mask. /// The index in the buffer at which to attempt substitution. /// The number of glyphs in the input sequence to consider. /// if a substitution was performed; otherwise, . @@ -349,6 +352,7 @@ public abstract bool TrySubstitution( GSubTable table, ShapingBuffer buffer, Tag feature, + ulong lookupMask, int index, int count); diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs index 6077e27be..33319ffb0 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs @@ -102,6 +102,7 @@ public override bool TrySubstitution( GSubTable table, ShapingBuffer buffer, Tag feature, + ulong lookupMask, int index, int count) { @@ -196,6 +197,7 @@ public override bool TrySubstitution( GSubTable table, ShapingBuffer buffer, Tag feature, + ulong lookupMask, int index, int count) { diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs index c57c8d7ef..193ba8afc 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs @@ -124,6 +124,7 @@ public override bool TrySubstitution( GSubTable table, ShapingBuffer buffer, Tag feature, + ulong lookupMask, int index, int count) { diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs index fdd17a4df..589478dc0 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs @@ -123,6 +123,7 @@ public override bool TrySubstitution( GSubTable table, ShapingBuffer buffer, Tag feature, + ulong lookupMask, int index, int count) { diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs index 3b48b11b6..1de22a502 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs @@ -154,6 +154,7 @@ public override bool TrySubstitution( GSubTable table, ShapingBuffer buffer, Tag feature, + ulong lookupMask, int index, int count) { @@ -182,7 +183,7 @@ public override bool TrySubstitution( continue; } - if (!AdvancedTypographicUtils.MatchInputSequence(iterator, feature, 1, ligatureTable.ComponentGlyphs, matchBuffer)) + if (!AdvancedTypographicUtils.MatchInputSequence(iterator, lookupMask, 1, ligatureTable.ComponentGlyphs, matchBuffer)) { continue; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs index 32058d6de..7043bf376 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs @@ -88,6 +88,7 @@ public override bool TrySubstitution( GSubTable table, ShapingBuffer buffer, Tag feature, + ulong lookupMask, int index, int count) { @@ -126,6 +127,7 @@ public override bool TrySubstitution( fontMetrics, table, feature, + lookupMask, this.LookupFlags, this.MarkFilteringSet, ruleTable.SequenceLookupRecords, @@ -241,6 +243,7 @@ public override bool TrySubstitution( GSubTable table, ShapingBuffer buffer, Tag feature, + ulong lookupMask, int index, int count) { @@ -289,6 +292,7 @@ public override bool TrySubstitution( fontMetrics, table, feature, + lookupMask, this.LookupFlags, this.MarkFilteringSet, ruleTable.SequenceLookupRecords, @@ -411,6 +415,7 @@ public override bool TrySubstitution( GSubTable table, ShapingBuffer buffer, Tag feature, + ulong lookupMask, int index, int count) { @@ -432,6 +437,7 @@ public override bool TrySubstitution( fontMetrics, table, feature, + lookupMask, this.LookupFlags, this.MarkFilteringSet, this.sequenceLookupRecords, diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs index 419cb2dd1..c9f9a9a44 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs @@ -91,6 +91,7 @@ public override bool TrySubstitution( GSubTable table, ShapingBuffer buffer, Tag feature, + ulong lookupMask, int index, int count) { @@ -130,6 +131,7 @@ public override bool TrySubstitution( fontMetrics, table, feature, + lookupMask, this.LookupFlags, this.MarkFilteringSet, ruleTable.SequenceLookupRecords, @@ -279,6 +281,7 @@ public override bool TrySubstitution( GSubTable table, ShapingBuffer buffer, Tag feature, + ulong lookupMask, int index, int count) { @@ -320,6 +323,7 @@ public override bool TrySubstitution( fontMetrics, table, feature, + lookupMask, this.LookupFlags, this.MarkFilteringSet, ruleTable.SequenceLookupRecords, @@ -474,6 +478,7 @@ public override bool TrySubstitution( GSubTable table, ShapingBuffer buffer, Tag feature, + ulong lookupMask, int index, int count) { @@ -502,6 +507,7 @@ public override bool TrySubstitution( fontMetrics, table, feature, + lookupMask, this.LookupFlags, this.MarkFilteringSet, this.sequenceLookupRecords, diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType8SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType8SubTable.cs index 2257ba838..deea97d08 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType8SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType8SubTable.cs @@ -157,6 +157,7 @@ public override bool TrySubstitution( GSubTable table, ShapingBuffer buffer, Tag feature, + ulong lookupMask, int index, int count) { diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/NotImplementedSubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/NotImplementedSubTable.cs index 76743a765..297364d42 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/NotImplementedSubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/NotImplementedSubTable.cs @@ -23,6 +23,7 @@ public override bool TrySubstitution( GSubTable table, ShapingBuffer buffer, Tag feature, + ulong lookupMask, int index, int count) => false; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs index 5a5eee3f5..bbf6b4ba9 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs @@ -198,7 +198,6 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas ShapePlan shapePlan = buffer.GetOrCreatePlan(current, unicodeScriptTag, fontMetrics); ShapingProbe.Exit(ShapingProbe.SubShaperCreate, createProbe); - buffer.CurrentPlan = shapePlan; BaseShaper shaper = shapePlan.Shaper; // Plan substitution features for each glyph. @@ -217,19 +216,18 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas // feature's lookups apply together in lookup-list order, the order the // specification defines for lookups within a single application pass. A // lookup registered by several of the group's features applies once with - // their glyph masks combined. Group boundaries and merged lookup lists - // are prebuilt on the plan; only the per-pass masks are combined here. + // their glyph masks combined. Group boundaries, merged lookup lists, and + // entry masks are all prebuilt on the plan. List> groups = shapePlan.GetOrBuildGSubStageGroups(); List stages = shapePlan.Stages; SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, default, 0); - List<(Tag Feature, ushort Index, LookupTable LookupTable, ulong Mask)> merged = buffer.GSubLookupScratch; for (int g = 0; g < groups.Count; g++) { ShapePlanStageGroup group = groups[g]; collectionCount = buffer.Count; var preProbe = ShapingProbe.Enter(); - stages[group.Start].PreProcessFeature(buffer, index, count); + stages[group.Start].PreProcessFeature(shapePlan, buffer, index, count); ShapingProbe.Exit(ShapingProbe.SubStagePrePost, preProbe); // Account for substitutions changing the length of the buffer. @@ -237,16 +235,12 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas count += delta; i += delta; - var lookupProbe = ShapingProbe.Enter(); - FillMergedFromPlan(group, buffer.FeatureMap, merged); - ShapingProbe.Exit(ShapingProbe.LookupResolve, lookupProbe); - var applyProbe = ShapingProbe.Enter(); this.ApplyMergedLookups( fontMetrics, buffer, ref iterator, - merged, + group.Lookups, index, ref count, ref i, @@ -258,7 +252,7 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas collectionCount = buffer.Count; var postProbe = ShapingProbe.Enter(); - stages[group.End - 1].PostProcessFeature(buffer, index, count); + stages[group.End - 1].PostProcessFeature(shapePlan, buffer, index, count); ShapingProbe.Exit(ShapingProbe.SubStagePrePost, postProbe); // Account for substitutions changing the length of the buffer. @@ -267,8 +261,6 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas i += delta; } - buffer.CurrentPlan = null; - // Record the segment with its post-substitution range so the in-place // positioning pass can reuse the plan; one plan then drives both tables. buffer.SegmentPlans.Add((index, count, current, shapePlan)); @@ -357,7 +349,7 @@ private void ApplyMergedLookups( } collectionCount = buffer.Count; - featureLookupTable.TrySubstitution(fontMetrics, this, buffer, feature, iterator.Index, count - (iterator.Index - index)); + featureLookupTable.TrySubstitution(fontMetrics, this, buffer, feature, featureMask, iterator.Index, count - (iterator.Index - index)); featureApplies++; iterator.Next(); @@ -371,35 +363,6 @@ private void ApplyMergedLookups( } } - /// - /// Fills the merge scratch from a plan group's prebuilt lookup list, combining - /// the per-pass masks of every feature that registered each lookup. The prebuilt - /// list already carries lookup-index order and deduplication, so this is a copy - /// with mask lookups rather than a merge. - /// - /// The plan stage group to read. - /// The pass's feature bit assignment. - /// The merge scratch to fill. - private static void FillMergedFromPlan( - ShapePlanStageGroup group, - ShapingFeatureMap featureMap, - List<(Tag Feature, ushort Index, LookupTable LookupTable, ulong Mask)> merged) - { - merged.Clear(); - List<(Tag Feature, ushort Index, LookupTable LookupTable, Tag[] Contributing)> lookups = group.Lookups; - for (int i = 0; i < lookups.Count; i++) - { - (Tag feature, ushort lookupIndex, LookupTable lookupTable, Tag[] contributing) = lookups[i]; - ulong mask = 0; - for (int c = 0; c < contributing.Length; c++) - { - mask |= featureMap.GetMask(contributing[c]); - } - - merged.Add((feature, lookupIndex, lookupTable, mask)); - } - } - /// /// Tries to get the feature lookups for the given stage feature, script, and language. /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlan.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlan.cs index e9e240a37..758e2381e 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlan.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlan.cs @@ -115,6 +115,13 @@ private ShapePlan( /// public Tag[] LanguageTags { get; } + /// + /// Gets the plan's feature bit assignment, shared with the shaper that created + /// it. Bits are assigned in stage-list order when the groups are built and + /// append-only afterwards, so plans of the same identity assign identical bits. + /// + public ShapePlanFeatures Features => this.Shaper.Features; + /// /// Gets a value indicating whether the plan may be cached. A substitution table /// carrying feature variations resolves against live variation coordinates, so @@ -204,12 +211,21 @@ public static ShapePlan Build( this.gposStageGroups = null; this.resolvedGsubFeatures.Clear(); + // Assign every stage feature its mask bit in stage-list order before any + // group is resolved. This single assignment point keeps layouts identical + // between plans of the same identity, which keeps applied masks portable + // across the pass's buffers. + for (int s = 0; s < this.Stages.Count; s++) + { + _ = this.Features.GetOrAddMask(this.Stages[s].FeatureTag); + } + List> groups = new(); GSubTable? gsubTable = this.gsubTable; FontMetrics fontMetrics = this.FontMetrics; ScriptClass script = this.Script; Tag[] languageTags = this.LanguageTags; - BuildStageGroups( + this.BuildStageGroups( this.Stages, groups, (in Tag featureTag, out List<(Tag Feature, ushort Index, GSub.LookupTable LookupTable)>? lookups) => @@ -242,7 +258,7 @@ public static ShapePlan Build( FontMetrics fontMetrics = this.FontMetrics; ScriptClass script = this.Script; Tag[] languageTags = this.LanguageTags; - BuildStageGroups( + this.BuildStageGroups( this.Stages, groups, (in Tag featureTag, out List<(Tag Feature, ushort Index, GPos.LookupTable LookupTable)>? lookups) => @@ -289,14 +305,14 @@ public bool TryGetGSubFeatureLookups(in Tag featureTag, [NotNullWhen(true)] out /// /// Computes the pause-delimited stage groups over the stage list and resolves /// each group's features into a merged lookup list sorted by lookup index. A - /// lookup registered by several of a group's features gains each feature in its - /// contributing set instead of a second entry. + /// lookup registered by several of a group's features gains their combined + /// plan-assigned mask instead of a second entry. /// /// The layout table's lookup type. /// The stage list to group. /// The group list to fill. /// The per-feature lookup resolver for the table. - private static void BuildStageGroups( + private void BuildStageGroups( List stages, List> groups, ShapePlanLookupResolver resolver) @@ -327,12 +343,13 @@ private static void BuildStageGroups( } // Resolve each stage feature in the group to its lookups and fold - // them into one list ordered by lookup index. The scan below runs + // them into one list ordered by lookup index, freezing each entry's + // combined mask from the plan's assignments. The scan below runs // backwards from the tail because resolved lookups arrive mostly // ascending, so the insertion point is almost always at or near the // end. ShapePlanStageGroup group = new(stageIndex, groupEnd); - List<(Tag Feature, ushort Index, TLookup LookupTable, Tag[] Contributing)> merged = group.Lookups; + List<(Tag Feature, ushort Index, TLookup LookupTable, ulong Mask)> merged = group.Lookups; for (int s = stageIndex; s < groupEnd; s++) { Tag featureTag = stages[s].FeatureTag; @@ -341,28 +358,22 @@ private static void BuildStageGroups( continue; } + ulong featureMask = this.Features.GetOrAddMask(featureTag); foreach ((Tag Feature, ushort Index, TLookup LookupTable) featureLookup in lookups) { // Scan from the tail toward the head. Three outcomes: the - // lookup index is already present, so this feature joins its - // contributing set and no entry is added; a smaller index is - // found, so the new entry inserts directly after it; or the - // head is reached, so the new entry inserts first. + // lookup index is already present, so this feature's mask + // joins the entry; a smaller index is found, so the new entry + // inserts directly after it; or the head is reached, so the + // new entry inserts first. int insertAt = merged.Count; bool alreadyMerged = false; while (insertAt > 0) { - (Tag Feature, ushort Index, TLookup LookupTable, Tag[] Contributing) prior = merged[insertAt - 1]; + (Tag Feature, ushort Index, TLookup LookupTable, ulong Mask) prior = merged[insertAt - 1]; if (prior.Index == featureLookup.Index) { - // Same lookup registered by another of the group's - // features: record the feature so apply time can - // combine both features' masks, exactly as two - // separate entries would have. - Tag[] contributing = new Tag[prior.Contributing.Length + 1]; - prior.Contributing.CopyTo(contributing, 0); - contributing[^1] = featureTag; - merged[insertAt - 1] = (prior.Feature, prior.Index, prior.LookupTable, contributing); + merged[insertAt - 1] = (prior.Feature, prior.Index, prior.LookupTable, prior.Mask | featureMask); alreadyMerged = true; break; } @@ -377,7 +388,7 @@ private static void BuildStageGroups( if (!alreadyMerged) { - merged.Insert(insertAt, (featureLookup.Feature, featureLookup.Index, featureLookup.LookupTable, new[] { featureTag })); + merged.Insert(insertAt, (featureLookup.Feature, featureLookup.Index, featureLookup.LookupTable, featureMask)); } } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanFeatures.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanFeatures.cs new file mode 100644 index 000000000..467db5ffd --- /dev/null +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanFeatures.cs @@ -0,0 +1,170 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.AdvancedTypographic; + +/// +/// The feature bit assignment owned by one shape plan: each OpenType feature the +/// plan touches receives a bit within a 64 bit mask, so per-glyph feature state is +/// stored and tested as plain bitwise operations. Bits are assigned in stage-list +/// order when the plan's groups are built and append-only afterwards, so two plans +/// of the same identity assign identical bits and applied masks stay portable +/// between the passes' buffers. The vertical trio occupies reserved bits identical +/// across every plan. +/// +internal sealed class ShapePlanFeatures +{ + /// + /// The fixed mask bit for the vertical alternates feature. The vertical trio + /// keeps reserved bits identical across every plan so applied-mask consumers + /// that span plans, such as the copy-out's vertical detection, never depend on + /// any single plan's layout. + /// + public const ulong VerticalAlternatesMask = 1UL << 0; + + /// + /// The fixed mask bit for the vertical alternates for rotation feature. + /// + public const ulong VerticalAlternatesForRotationMask = 1UL << 1; + + /// + /// The fixed mask bit for the vertical kerning feature. + /// + public const ulong VerticalKerningMask = 1UL << 2; + + /// + /// The combined mask of the three vertical alternate features, constant across + /// plans by the reserved-bit contract above. + /// + public const ulong VerticalFeatureMask = VerticalAlternatesMask | VerticalAlternatesForRotationMask | VerticalKerningMask; + + /// + /// The first bit available to assigned features; lower bits are reserved for + /// the vertical trio. + /// + private const int FirstAssignableBit = 3; + + /// + /// The number of assignable feature bits after the reserved bits. + /// + private const int AssignableBitCount = 64 - FirstAssignableBit; + + /// + /// The assigned feature tag values, indexed by bit position above the reserved + /// bits. Stored as raw values so lookups take the runtime's + /// vectorized primitive search path. + /// + private readonly List featureTags = new(32); + + /// + /// The most recently resolved tag value. Queries strongly repeat the same + /// feature during one feature's application, so a single-entry memo answers + /// almost every query without a list search. Zero means the memo is empty; the + /// zero tag is never a valid feature. + /// + private uint lastTagValue; + + /// + /// The mask paired with . + /// + private ulong lastMask; + + /// + /// Gets the fixed vertical-trio mask bit for a feature, or zero for any other + /// feature. Applied-mask consumers only ever read the vertical bits, which are + /// reserved and constant across plans, so applied recording needs no plan state. + /// + /// The feature tag whose lookups applied. + /// The fixed vertical mask bit, or zero. + public static ulong GetVerticalMask(Tag tag) + { + if (tag == KnownFeatureTags.VerticalAlternates) + { + return VerticalAlternatesMask; + } + + if (tag == KnownFeatureTags.VerticalAlternatesForRotation) + { + return VerticalAlternatesForRotationMask; + } + + if (tag == KnownFeatureTags.VerticalKerning) + { + return VerticalKerningMask; + } + + return 0; + } + + /// + /// Gets the mask bit for the given feature tag, or zero when the tag has no + /// assignment. The vertical trio answers from the reserved bits; a zero result + /// is safe at every consumption site: testing it enables or matches nothing and + /// clearing it clears nothing. + /// + /// The feature tag. + /// The single-bit mask, or zero. + public ulong GetMask(Tag tag) + { + if (tag.Value == this.lastTagValue) + { + return this.lastMask; + } + + if (tag == KnownFeatureTags.VerticalAlternates) + { + return VerticalAlternatesMask; + } + + if (tag == KnownFeatureTags.VerticalAlternatesForRotation) + { + return VerticalAlternatesForRotationMask; + } + + if (tag == KnownFeatureTags.VerticalKerning) + { + return VerticalKerningMask; + } + + int index = this.featureTags.IndexOf(tag.Value); + ulong mask = index < 0 ? 0 : 1UL << (FirstAssignableBit + index); + + // A zero mask is never memoized: the tag may gain an assignment later and + // the memo must not serve a stale zero after that. + if (mask != 0) + { + this.lastTagValue = tag.Value; + this.lastMask = mask; + } + + return mask; + } + + /// + /// Gets the mask bit for the given feature tag, assigning the next free bit + /// when the tag is new to this plan. A plan that has exhausted its bits assigns + /// nothing and returns zero, which disables the feature: a zero mask registers + /// nothing, enables nothing, and matches nothing at every consumption site. + /// + /// The feature tag. + /// The single-bit mask, or zero when the bits are exhausted. + public ulong GetOrAddMask(Tag tag) + { + ulong mask = this.GetMask(tag); + if (mask != 0) + { + return mask; + } + + if (this.featureTags.Count == AssignableBitCount) + { + return 0; + } + + this.featureTags.Add(tag.Value); + mask = 1UL << (FirstAssignableBit + this.featureTags.Count - 1); + this.lastTagValue = tag.Value; + this.lastMask = mask; + return mask; + } +} diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanStageGroup{TLookup}.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanStageGroup{TLookup}.cs index aae5bc52e..2d2ddd970 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanStageGroup{TLookup}.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanStageGroup{TLookup}.cs @@ -5,8 +5,9 @@ namespace SixLabors.Fonts.Tables.AdvancedTypographic; /// /// One pause-delimited stage group of a shape plan: the stage index range it covers -/// and the group's lookups merged into lookup-index order, each entry recording every -/// feature that registered it so per-pass masks can be combined at apply time. +/// and the group's lookups merged into lookup-index order, each entry carrying the +/// combined plan-assigned mask of every feature that registered it, frozen when the +/// plan is built. /// /// The layout table's lookup type. internal sealed class ShapePlanStageGroup @@ -38,9 +39,8 @@ public ShapePlanStageGroup(int start, int end) /// /// Gets the group's lookups merged across its stage features into lookup-index - /// order. A lookup registered by several features appears once, with every - /// registering feature recorded so apply-time masks combine exactly as separate - /// entries would have. + /// order. A lookup registered by several features appears once with their + /// plan-assigned masks combined, so application consumes the list directly. /// - public List<(Tag Feature, ushort Index, TLookup LookupTable, Tag[] Contributing)> Lookups { get; } + public List<(Tag Feature, ushort Index, TLookup LookupTable, ulong Mask)> Lookups { get; } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs index 0b283e6a8..d3dfb3719 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs @@ -200,25 +200,25 @@ protected override void AssignFeatures(ShapingBuffer buffer, int index, int coun switch (actions[i]) { case Fina: - buffer.EnableShapingFeature(i + index, FinaTag); + buffer.EnableShapingFeature(i + index, this.Features.GetMask(FinaTag)); break; case Fin2: - buffer.EnableShapingFeature(i + index, Fin2Tag); + buffer.EnableShapingFeature(i + index, this.Features.GetMask(Fin2Tag)); break; case Fin3: - buffer.EnableShapingFeature(i + index, Fin3Tag); + buffer.EnableShapingFeature(i + index, this.Features.GetMask(Fin3Tag)); break; case Isol: - buffer.EnableShapingFeature(i + index, IsolTag); + buffer.EnableShapingFeature(i + index, this.Features.GetMask(IsolTag)); break; case Init: - buffer.EnableShapingFeature(i + index, InitTag); + buffer.EnableShapingFeature(i + index, this.Features.GetMask(InitTag)); break; case Medi: - buffer.EnableShapingFeature(i + index, MediTag); + buffer.EnableShapingFeature(i + index, this.Features.GetMask(MediTag)); break; case Med2: - buffer.EnableShapingFeature(i + index, Med2Tag); + buffer.EnableShapingFeature(i + index, this.Features.GetMask(Med2Tag)); break; } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs index 2132900b7..2733d1624 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs @@ -11,6 +11,14 @@ namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers; /// internal abstract class BaseShaper { + /// + /// Gets the feature bit assignment for the plan this shaper belongs to. The + /// shaper creates it and the owning plan adopts it, so both hold the same + /// non-null instance for their whole lifetime; a shaper exists only inside a + /// plan, so feature state is never reachable without one. + /// + public ShapePlanFeatures Features { get; } = new(); + /// /// Gets or sets the script classification for this shaper. /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs index 1e7b97dad..d30f9bedb 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs @@ -293,8 +293,8 @@ protected void AddFeature( int count, Tag feature, bool enabled = true, - Action? preAction = null, - Action? postAction = null) + Action? preAction = null, + Action? postAction = null) { if (this.kerningMode == KerningMode.None) { @@ -304,7 +304,7 @@ protected void AddFeature( } } - buffer.AddShapingFeatureRange(index, count, new TagEntry(feature, enabled)); + buffer.AddShapingFeatureRange(index, count, new TagEntry(feature, enabled), this.Features.GetOrAddMask(feature)); // First registration wins, matching the previous set semantics: a duplicate // tag keeps the originally supplied pre and post actions. diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs index 760bdcc45..76a05d6c1 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs @@ -193,7 +193,7 @@ protected override void AssignFeatures(ShapingBuffer buffer, int index, int coun // 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. - buffer.DisableShapingFeature(i, CaltTag); + buffer.DisableShapingFeature(i, this.Features.GetMask(CaltTag)); } // Apply the state machine to map glyphs to features. @@ -268,22 +268,22 @@ protected override void AssignFeatures(ShapingBuffer buffer, int index, int coun switch (GetSyllableType(codePoint)) { case L: - buffer.EnableShapingFeature(i, LjmoTag); + buffer.EnableShapingFeature(i, this.Features.GetMask(LjmoTag)); break; case V: - buffer.EnableShapingFeature(i, VjmoTag); + buffer.EnableShapingFeature(i, this.Features.GetMask(VjmoTag)); break; case T: - buffer.EnableShapingFeature(i, TjmoTag); + buffer.EnableShapingFeature(i, this.Features.GetMask(TjmoTag)); break; case LV: - buffer.EnableShapingFeature(i, LjmoTag); - buffer.EnableShapingFeature(i, VjmoTag); + buffer.EnableShapingFeature(i, this.Features.GetMask(LjmoTag)); + buffer.EnableShapingFeature(i, this.Features.GetMask(VjmoTag)); break; case LVT: - buffer.EnableShapingFeature(i, LjmoTag); - buffer.EnableShapingFeature(i, VjmoTag); - buffer.EnableShapingFeature(i, TjmoTag); + buffer.EnableShapingFeature(i, this.Features.GetMask(LjmoTag)); + buffer.EnableShapingFeature(i, this.Features.GetMask(VjmoTag)); + buffer.EnableShapingFeature(i, this.Features.GetMask(TjmoTag)); break; } } @@ -364,8 +364,8 @@ private int DecomposeGlyph(ShapingBuffer buffer, ref GlyphShapingData data, int ii[0] = ljmo; buffer.Replace(index, ii, KnownFeatureTags.GlyphCompositionDecomposition); - buffer.EnableShapingFeature(index, LjmoTag); - buffer.EnableShapingFeature(index + 1, VjmoTag); + buffer.EnableShapingFeature(index, this.Features.GetMask(LjmoTag)); + buffer.EnableShapingFeature(index + 1, this.Features.GetMask(VjmoTag)); return index + 1; } @@ -375,9 +375,9 @@ private int DecomposeGlyph(ShapingBuffer buffer, ref GlyphShapingData data, int iii[0] = ljmo; buffer.Replace(index, iii, KnownFeatureTags.GlyphCompositionDecomposition); - buffer.EnableShapingFeature(index, LjmoTag); - buffer.EnableShapingFeature(index + 1, VjmoTag); - buffer.EnableShapingFeature(index + 2, TjmoTag); + buffer.EnableShapingFeature(index, this.Features.GetMask(LjmoTag)); + buffer.EnableShapingFeature(index + 1, this.Features.GetMask(VjmoTag)); + buffer.EnableShapingFeature(index + 2, this.Features.GetMask(TjmoTag)); return index + 2; } @@ -456,17 +456,17 @@ private int ComposeGlyph(ShapingBuffer buffer, int index, int type, Span // Didn't compose (either a non-combining component or unsupported by font). if (ljmo >= 0) { - buffer.EnableShapingFeature(ljmo, LjmoTag); + buffer.EnableShapingFeature(ljmo, this.Features.GetMask(LjmoTag)); } if (vjmo >= 0) { - buffer.EnableShapingFeature(vjmo, VjmoTag); + buffer.EnableShapingFeature(vjmo, this.Features.GetMask(VjmoTag)); } if (tjmo >= 0) { - buffer.EnableShapingFeature(tjmo, TjmoTag); + buffer.EnableShapingFeature(tjmo, this.Features.GetMask(TjmoTag)); } if (prevType == LV) diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs index 757ba53d4..8f1228355 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs @@ -285,10 +285,11 @@ protected override void AssignFeatures(ShapingBuffer buffer, int index, int coun /// /// Identifies Indic syllables using the state machine and assigns shaping info to each glyph. /// + /// The plan whose segment is being shaped. /// The glyph shaping buffer. /// The zero-based start index. /// The number of elements to process. - private void SetupSyllables(ShapingBuffer buffer, int index, int count) + private void SetupSyllables(ShapePlan plan, ShapingBuffer buffer, int index, int count) { if (buffer.Role != ShapingBufferRole.Substitution) { @@ -396,10 +397,11 @@ private static int IndicShapingPosition(CodePoint codePoint) /// Performs the initial reordering pass for Indic syllables, including base consonant /// identification, reph handling, matra reordering, and feature assignment. /// + /// The plan whose segment is being shaped. /// The glyph shaping buffer. /// The zero-based start index. /// The number of elements to process. - private void InitialReorder(ShapingBuffer buffer, int index, int count) + private void InitialReorder(ShapePlan plan, ShapingBuffer buffer, int index, int count) { if (buffer.Role != ShapingBufferRole.Substitution) { @@ -423,7 +425,7 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) if (data.Syllable.IndicPosition == Positions.Base_C) { - data.Syllable.IndicPosition = this.ConsonantPosition(buffer, viramaId, data.GlyphId); + data.Syllable.IndicPosition = this.ConsonantPosition(plan, buffer, viramaId, data.GlyphId); } } } @@ -502,7 +504,7 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) // base consonants. if (start + 3 <= end && indicConfiguration.RephPosition != Positions.Ra_To_Become_Reph && - buffer.CurrentPlan?.TryGetGSubFeatureLookups(in RphfTag, out _) == true && + plan.TryGetGSubFeatureLookups(in RphfTag, out _) && ((indicConfiguration.RephMode == RephMode.Implicit && !IsJoiner(ref buffer[start + 2])) || (indicConfiguration.RephMode == RephMode.Explicit && buffer[start + 2].Syllable.IndicCategory == Categories.ZWJ))) { @@ -511,8 +513,8 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) probeGlyphs[1] = buffer[start + 1].GlyphId; probeGlyphs[2] = buffer[start + 2].GlyphId; - if ((indicConfiguration.RephMode == RephMode.Explicit && this.WouldSubstitute(buffer, in RphfTag, probeGlyphs)) || - this.WouldSubstitute(buffer, in RphfTag, probeGlyphs[..2])) + if ((indicConfiguration.RephMode == RephMode.Explicit && this.WouldSubstitute(plan, buffer, in RphfTag, probeGlyphs)) || + this.WouldSubstitute(plan, buffer, in RphfTag, probeGlyphs[..2])) { limit += 2; while (limit < end && IsJoiner(ref buffer[limit])) @@ -846,26 +848,26 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) break; } - buffer.EnableShapingFeature(i, RphfTag); + buffer.EnableShapingFeature(i, this.Features.GetMask(RphfTag)); } // Pre-base bool blwf = !this.isOldSpec && indicConfiguration.BlwfMode == BlwfMode.Pre_And_Post; for (int i = start; i < basePosition; i++) { - buffer.EnableShapingFeature(i, HalfTag); + buffer.EnableShapingFeature(i, this.Features.GetMask(HalfTag)); if (blwf) { - buffer.EnableShapingFeature(i, BlwfTag); + buffer.EnableShapingFeature(i, this.Features.GetMask(BlwfTag)); } } // Post-base for (int i = basePosition + 1; i < end; i++) { - buffer.EnableShapingFeature(i, AbvfTag); - buffer.EnableShapingFeature(i, PstfTag); - buffer.EnableShapingFeature(i, BlwfTag); + buffer.EnableShapingFeature(i, this.Features.GetMask(AbvfTag)); + buffer.EnableShapingFeature(i, this.Features.GetMask(PstfTag)); + buffer.EnableShapingFeature(i, this.Features.GetMask(BlwfTag)); } if (this.isOldSpec && this.ScriptClass == ScriptClass.Devanagari) @@ -893,26 +895,26 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) buffer[i + 1].Syllable.IndicCategory == Categories.H && (i + 1 == basePosition || buffer[i + 2].Syllable.IndicCategory == Categories.ZWJ)) { - buffer.EnableShapingFeature(i, BlwfTag); - buffer.EnableShapingFeature(i + 1, BlwfTag); + buffer.EnableShapingFeature(i, this.Features.GetMask(BlwfTag)); + buffer.EnableShapingFeature(i + 1, this.Features.GetMask(BlwfTag)); } } } const int prefLen = 2; if (basePosition + prefLen < end && - buffer.CurrentPlan?.TryGetGSubFeatureLookups(in PrefTag, out _) == true) + plan.TryGetGSubFeatureLookups(in PrefTag, out _)) { // Find a Halant,Ra sequence and mark it for pre-base reordering processing. for (int i = basePosition + 1; i + prefLen - 1 < end; i++) { probeGlyphs[0] = buffer[i].GlyphId; probeGlyphs[1] = buffer[i + 1].GlyphId; - if (this.WouldSubstitute(buffer, in PrefTag, probeGlyphs[..2])) + if (this.WouldSubstitute(plan, buffer, in PrefTag, probeGlyphs[..2])) { for (int j = 0; j < prefLen; j++) { - buffer.EnableShapingFeature(i++, PrefTag); + buffer.EnableShapingFeature(i++, this.Features.GetMask(PrefTag)); } // Mark the subsequent stuff with 'cfar'. Used in Khmer. @@ -920,11 +922,11 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) // This allows distinguishing the following cases with MS Khmer fonts: // U+1784,U+17D2,U+179A,U+17D2,U+1782 // U+1784,U+17D2,U+1782,U+17D2,U+179A - if (buffer.CurrentPlan?.TryGetGSubFeatureLookups(in CfarTag, out _) == true) + if (plan.TryGetGSubFeatureLookups(in CfarTag, out _)) { while (i < end) { - buffer.EnableShapingFeature(i, CfarTag); + buffer.EnableShapingFeature(i, this.Features.GetMask(CfarTag)); i++; } } @@ -954,7 +956,7 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) // A ZWNJ disables HALF. if (nonJoiner) { - buffer.DisableShapingFeature(j, HalfTag); + buffer.DisableShapingFeature(j, this.Features.GetMask(HalfTag)); } } while (j > start && !IsConsonant(ref buffer[j])); @@ -972,33 +974,34 @@ private void InitialReorder(ShapingBuffer buffer, int index, int count) /// virama-consonant and consonant-virama pairs would be substituted by the /// below-base, vattu, post-base, or pre-base forming features. /// + /// The plan whose segment is being shaped. /// The glyph shaping buffer providing the language tags. /// The virama glyph id. /// The consonant glyph id. /// The consonant's positional class. - private Positions ConsonantPosition(ShapingBuffer buffer, ushort virama, ushort consonant) + private Positions ConsonantPosition(ShapePlan plan, ShapingBuffer buffer, ushort virama, ushort consonant) { Span glyphs = stackalloc ushort[3]; glyphs[0] = virama; glyphs[1] = consonant; glyphs[2] = virama; - if (this.WouldSubstitute(buffer, in BlwfTag, glyphs[..2]) || - this.WouldSubstitute(buffer, in BlwfTag, glyphs.Slice(1, 2)) || - this.WouldSubstitute(buffer, in VatuTag, glyphs[..2]) || - this.WouldSubstitute(buffer, in VatuTag, glyphs.Slice(1, 2))) + if (this.WouldSubstitute(plan, buffer, in BlwfTag, glyphs[..2]) || + this.WouldSubstitute(plan, buffer, in BlwfTag, glyphs.Slice(1, 2)) || + this.WouldSubstitute(plan, buffer, in VatuTag, glyphs[..2]) || + this.WouldSubstitute(plan, buffer, in VatuTag, glyphs.Slice(1, 2))) { return Positions.Below_C; } - if (this.WouldSubstitute(buffer, in PstfTag, glyphs[..2]) || - this.WouldSubstitute(buffer, in PstfTag, glyphs.Slice(1, 2))) + if (this.WouldSubstitute(plan, buffer, in PstfTag, glyphs[..2]) || + this.WouldSubstitute(plan, buffer, in PstfTag, glyphs.Slice(1, 2))) { return Positions.Post_C; } - if (this.WouldSubstitute(buffer, in PrefTag, glyphs[..2]) || - this.WouldSubstitute(buffer, in PrefTag, glyphs.Slice(1, 2))) + if (this.WouldSubstitute(plan, buffer, in PrefTag, glyphs[..2]) || + this.WouldSubstitute(plan, buffer, in PrefTag, glyphs.Slice(1, 2))) { return Positions.Post_C; } @@ -1011,25 +1014,16 @@ private Positions ConsonantPosition(ShapingBuffer buffer, ushort virama, ushort /// produce a substitution, querying the feature's lookups directly without /// running any substitution. /// + /// The plan whose segment is being shaped. /// The glyph shaping buffer providing the language tags. /// The feature tag to test. /// The glyph id sequence to test. /// if a substitution would occur. - private bool WouldSubstitute(ShapingBuffer buffer, in Tag featureTag, ReadOnlySpan glyphs) + private bool WouldSubstitute(ShapePlan plan, ShapingBuffer buffer, in Tag featureTag, ReadOnlySpan glyphs) { // Pause actions always run inside segment processing, where the segment's - // plan is current; the table fallback keeps behavior correct if this is - // ever reached outside that window. - List<(Tag Feature, ushort Index, LookupTable LookupTable)>? lookups; - if (buffer.CurrentPlan is { } plan) - { - if (!plan.TryGetGSubFeatureLookups(in featureTag, out lookups)) - { - return false; - } - } - else if (!this.fontMetrics.TryGetGSubTable(out GSubTable? gSubTable) || - !gSubTable.TryGetFeatureLookups(this.fontMetrics, in featureTag, this.ScriptClass, buffer.LanguageTags, out lookups)) + // plan is current; the accessor enforces that invariant. + if (!plan.TryGetGSubFeatureLookups(in featureTag, out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? lookups)) { return false; } @@ -1099,10 +1093,11 @@ private static int NextSyllable(ShapingBuffer buffer, int index, int count) /// Performs the final reordering pass for Indic syllables, repositioning reph, /// pre-base consonants, and pre-base matras after basic shaping. /// + /// The plan whose segment is being shaped. /// The glyph shaping buffer. /// The zero-based start index. /// The number of elements to process. - private void FinalReorder(ShapingBuffer buffer, int index, int count) + private void FinalReorder(ShapePlan plan, ShapingBuffer buffer, int index, int count) { if (buffer.Role != ShapingBufferRole.Substitution) { @@ -1121,7 +1116,7 @@ private void FinalReorder(ShapingBuffer buffer, int index, int count) // applied (see below), the shaping engine performs some final glyph // reordering before applying all the remaining font features to the entire // cluster. - bool tryPref = buffer.CurrentPlan?.TryGetGSubFeatureLookups(in PrefTag, out _) == true; + bool tryPref = plan.TryGetGSubFeatureLookups(in PrefTag, out _); // Find base consonant again. int basePosition = start; @@ -1134,7 +1129,7 @@ private void FinalReorder(ShapingBuffer buffer, int index, int count) for (int i = basePosition + 1; i < end; i++) { ref GlyphShapingData current = ref buffer[i]; - if ((current.FeatureMask & buffer.FeatureMap.GetMask(PrefTag)) != 0) + if ((current.FeatureMask & this.Features.GetMask(PrefTag)) != 0) { if (!current.IsSubstituted && current.IsLigated && !current.IsDecomposed) { @@ -1444,7 +1439,7 @@ private void FinalReorder(ShapingBuffer buffer, int index, int count) for (int i = basePosition + 1; i < end; i++) { ref GlyphShapingData current = ref buffer[i]; - if ((current.FeatureMask & buffer.FeatureMap.GetMask(PrefTag)) != 0) + if ((current.FeatureMask & this.Features.GetMask(PrefTag)) != 0) { // 1. Only reorder a glyph produced by substitution during application // of the feature. (Note that a font may shape a Ra consonant with @@ -1516,7 +1511,7 @@ private void FinalReorder(ShapingBuffer buffer, int index, int count) if (buffer[start].Syllable.IndicPosition == Positions.Pre_M && (start == 0 || CodePoint.GetGeneralCategory(buffer[start - 1].CodePoint) is not UnicodeCategory.NonSpacingMark and not UnicodeCategory.Format)) { - buffer.EnableShapingFeature(start, InitTag); + buffer.EnableShapingFeature(start, this.Features.GetMask(InitTag)); } start = end; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs index fa13ecc25..b3c08880f 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs @@ -139,10 +139,11 @@ protected override void PlanFeatures(ShapingBuffer buffer, int index, int count) /// /// Identifies Myanmar syllables using the state machine and assigns shaping info to each glyph. /// + /// The plan whose segment is being shaped. /// The glyph shaping buffer. /// The zero-based start index. /// The number of elements to process. - private void SetupSyllables(ShapingBuffer buffer, int index, int count) + private void SetupSyllables(ShapePlan plan, ShapingBuffer buffer, int index, int count) { if (buffer.Role != ShapingBufferRole.Substitution) { @@ -232,10 +233,11 @@ private void SetupSyllables(ShapingBuffer buffer, int index, int count) /// Performs the initial reordering pass for Myanmar consonant syllables, including /// dotted circle insertion for broken clusters. /// + /// The plan whose segment is being shaped. /// The glyph shaping buffer. /// The zero-based start index. /// The number of elements to process. - private void InitialReorder(ShapingBuffer buffer, int index, int count) + private void InitialReorder(ShapePlan plan, ShapingBuffer buffer, int index, int count) { if (buffer.Role != ShapingBufferRole.Substitution) { diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShapingStage.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShapingStage.cs index a8f01282b..006dd5bc5 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShapingStage.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShapingStage.cs @@ -13,14 +13,16 @@ namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers; internal readonly struct ShapingStage : IEquatable { /// - /// The optional action to invoke before the feature is applied. + /// The optional action to invoke before the feature is applied. Actions receive + /// the plan whose segment is being shaped, so pause work never depends on + /// ambient state. /// - private readonly Action? preAction; + private readonly Action? preAction; /// /// The optional action to invoke after the feature is applied. /// - private readonly Action? postAction; + private readonly Action? postAction; /// /// Initializes a new instance of the struct. @@ -28,7 +30,7 @@ namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers; /// The OpenType feature tag for this stage. /// An optional action to invoke before the feature is applied. /// An optional action to invoke after the feature is applied. - public ShapingStage(Tag featureTag, Action? preAction, Action? postAction) + public ShapingStage(Tag featureTag, Action? preAction, Action? postAction) { this.FeatureTag = featureTag; this.preAction = preAction; @@ -57,20 +59,22 @@ public ShapingStage(Tag featureTag, Action? preAction, /// /// Invokes the pre-processing action for this shaping stage, if one was provided. /// + /// The plan whose segment is being shaped. /// The glyph shaping buffer. /// The zero-based index of the first element. /// The number of elements. - public void PreProcessFeature(ShapingBuffer buffer, int index, int count) - => this.preAction?.Invoke(buffer, index, count); + public void PreProcessFeature(ShapePlan plan, ShapingBuffer buffer, int index, int count) + => this.preAction?.Invoke(plan, buffer, index, count); /// /// Invokes the post-processing action for this shaping stage, if one was provided. /// + /// The plan whose segment is being shaped. /// The glyph shaping buffer. /// The zero-based index of the first element. /// The number of elements. - public void PostProcessFeature(ShapingBuffer buffer, int index, int count) - => this.postAction?.Invoke(buffer, index, count); + public void PostProcessFeature(ShapePlan plan, ShapingBuffer buffer, int index, int count) + => this.postAction?.Invoke(plan, buffer, index, count); /// public override bool Equals(object? obj) diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs index c77813476..6f79d21c2 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs @@ -255,10 +255,11 @@ private void DecomposeSplitVowels(ShapingBuffer buffer, int index, int count) /// /// Identifies syllables using the Universal Shaping Engine state machine and assigns shaping info to each glyph. /// + /// The plan whose segment is being shaped. /// The glyph shaping buffer. /// The zero-based start index. /// The number of elements to process. - private void SetupSyllables(ShapingBuffer buffer, int index, int count) + private void SetupSyllables(ShapePlan plan, ShapingBuffer buffer, int index, int count) { if (buffer.Role != ShapingBufferRole.Substitution) { @@ -303,7 +304,7 @@ private void SetupSyllables(ShapingBuffer buffer, int index, int count) for (int i = match.StartIndex; i < match.StartIndex + limit; i++) { - buffer.AddShapingFeature(i + index, new TagEntry(RcltTag, true)); + buffer.AddShapingFeature(i + index, new TagEntry(RcltTag, true), this.Features.GetOrAddMask(RcltTag)); } } } @@ -311,10 +312,11 @@ private void SetupSyllables(ShapingBuffer buffer, int index, int count) /// /// Clears substitution flags on all glyphs in the range, preparing for the next substitution pass. /// + /// The plan whose segment is being shaped. /// The glyph shaping buffer. /// The zero-based start index. /// The number of elements to process. - private static void ClearSubstitutionFlags(ShapingBuffer buffer, int index, int count) + private static void ClearSubstitutionFlags(ShapePlan plan, ShapingBuffer buffer, int index, int count) { if (buffer.Role != ShapingBufferRole.Substitution) { @@ -332,10 +334,11 @@ private static void ClearSubstitutionFlags(ShapingBuffer buffer, int index, int /// /// Records glyphs substituted by the 'rphf' feature by marking their category as repha ("R"). /// + /// The plan whose segment is being shaped. /// The glyph shaping buffer. /// The zero-based start index. /// The number of elements to process. - private static void RecordRhpf(ShapingBuffer buffer, int index, int count) + private static void RecordRhpf(ShapePlan plan, ShapingBuffer buffer, int index, int count) { if (buffer.Role != ShapingBufferRole.Substitution) { @@ -343,7 +346,7 @@ private static void RecordRhpf(ShapingBuffer buffer, int index, int count) } int end = index + count; - ulong rphfMask = buffer.FeatureMap.GetMask(RphfTag); + ulong rphfMask = plan.Features.GetMask(RphfTag); for (int i = index; i < end; i++) { ref GlyphShapingData data = ref buffer[i]; @@ -361,10 +364,11 @@ private static void RecordRhpf(ShapingBuffer buffer, int index, int count) /// /// Records glyphs substituted by the 'pref' feature by marking their category as pre-base vowel ("VPre"). /// + /// The plan whose segment is being shaped. /// The glyph shaping buffer. /// The zero-based start index. /// The number of elements to process. - private static void RecordPref(ShapingBuffer buffer, int index, int count) + private static void RecordPref(ShapePlan plan, ShapingBuffer buffer, int index, int count) { if (buffer.Role != ShapingBufferRole.Substitution) { @@ -390,10 +394,11 @@ private static void RecordPref(ShapingBuffer buffer, int index, int count) /// Reorders glyphs within syllables, handling repha movement, pre-base vowel movement, /// and dotted circle insertion for broken clusters. /// + /// The plan whose segment is being shaped. /// The glyph shaping buffer. /// The zero-based start index. /// The number of elements to process. - private void Reorder(ShapingBuffer buffer, int index, int count) + private void Reorder(ShapePlan plan, ShapingBuffer buffer, int index, int count) { if (buffer.Role != ShapingBufferRole.Substitution) { diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapingFeatureMap.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapingFeatureMap.cs deleted file mode 100644 index 882feb1d0..000000000 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapingFeatureMap.cs +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.Fonts.Tables.AdvancedTypographic; - -/// -/// Assigns each OpenType feature tag touched during a single shaping pass a bit within a -/// 64 bit mask, so per-glyph feature state can be stored and tested as plain bitwise -/// operations instead of per-glyph collections. -/// -/// -/// -/// Every glyph carries mask words instead of per-glyph collections, and lookup -/// application gates a glyph with one bitwise AND. The glyph side lives in the three -/// mask fields on : -/// -/// -/// : features a -/// shaper added for the glyph, enabled or not. Enabling is only ever an unhide of a -/// registered feature, never an introduction. -/// : the enabled subset, -/// the analog of hb_glyph_info_t.mask. This is the word the per-glyph application gate -/// tests. -/// : features whose -/// lookups actually changed the glyph, read after shaping, for example to detect that a -/// vertical alternate was substituted. -/// -/// -/// One instance is shared by the substitution and positioning collections of a shaping -/// pass. The sharing is load bearing: applied bits are written while substituting, the -/// glyph data is then copied into the positioning collection, and the positioning stages -/// and the layout walk read those bits later. A per-collection map would renumber the -/// bits across that copy and silently corrupt the applied state. -/// -/// -/// Bits are assigned first come, in registration order. A pass touches the shaper's -/// stage features, the font's required features, and the caller's -/// ; the largest shaper plans sit far below the 64 -/// available bits, so exhaustion indicates a defect rather than a workload and throws -/// loudly instead of shaping incorrectly. -/// -/// -internal sealed class ShapingFeatureMap -{ - /// - /// The registered tag values, indexed by assigned bit position. Stored as raw - /// values rather than so lookups take the - /// runtime's vectorized primitive search path, which custom structs never qualify - /// for. - /// - private readonly List tags = new(16); - - /// - /// The most recently resolved tag value. Queries within a pass strongly repeat the - /// same feature (every per-glyph apply during one feature's application resolves that - /// feature's tag), so a single-entry memo answers almost every query without a list - /// search. Zero means the memo is empty; the zero tag is never a valid feature. - /// - private uint lastTagValue; - - /// - /// The mask paired with . - /// - private ulong lastMask; - - /// - /// Resets the map for reuse by a new shaping pass, emptying the tag registry and - /// the single-entry memo. - /// - public void Reset() - { - this.tags.Clear(); - this.lastTagValue = 0; - this.lastMask = 0; - } - - /// - /// Gets the mask bit for the given feature tag, or zero when the tag has not been - /// registered. A zero result is safe at every consumption site: testing it enables - /// or matches nothing and clearing it clears nothing. - /// - /// The feature tag. - /// The single-bit mask, or zero. - public ulong GetMask(Tag tag) - { - if (tag.Value == this.lastTagValue) - { - return this.lastMask; - } - - int index = this.tags.IndexOf(tag.Value); - ulong mask = index < 0 ? 0 : 1UL << index; - - // A zero mask is never memoized: the tag may be registered later in the pass - // and the memo must not serve a stale zero after that registration. - if (mask != 0) - { - this.lastTagValue = tag.Value; - this.lastMask = mask; - } - - return mask; - } - - /// - /// Gets the mask bit for the given feature tag, assigning the next free bit when the - /// tag is new to this pass. - /// - /// The feature tag. - /// The single-bit mask. - /// - /// Thrown when a single shaping pass registers more than 64 distinct features. - /// - public ulong GetOrAddMask(Tag tag) - { - ulong mask = this.GetMask(tag); - if (mask != 0) - { - return mask; - } - - if (this.tags.Count == 64) - { - throw new InvalidOperationException( - "A single shaping pass registered more than 64 distinct OpenType features."); - } - - this.tags.Add(tag.Value); - mask = 1UL << (this.tags.Count - 1); - this.lastTagValue = tag.Value; - this.lastMask = mask; - return mask; - } -} diff --git a/src/SixLabors.Fonts/TextShaper.Pipeline.cs b/src/SixLabors.Fonts/TextShaper.Pipeline.cs index 32f01a1c4..f304ce433 100644 --- a/src/SixLabors.Fonts/TextShaper.Pipeline.cs +++ b/src/SixLabors.Fonts/TextShaper.Pipeline.cs @@ -141,9 +141,8 @@ internal static ShapedText ShapeText(ReadOnlySpan text, TextOptions option } /// - /// Shapes using caller-supplied shaping collections. Both - /// collections must share one and already reflect - /// . + /// Shapes using caller-supplied shaping buffers that + /// already reflect . /// /// The text to process. /// The text options used while shaping. @@ -387,7 +386,7 @@ or BidiCharacterType.FirstStrongIsolate // deduplicates into a run table and per-glyph state splits into parallel // identity and geometry arrays of pure values, so the scratch can go back to // the pool before consumption and no metrics reference survives shaping. - ulong verticalMask = shaped.GetVerticalFeatureMask(); + ulong verticalMask = ShapePlanFeatures.VerticalFeatureMask; int count = shaped.Count; ShapedGlyphInfo[] infos = new ShapedGlyphInfo[count]; ShapedGlyphPosition[] positions = new ShapedGlyphPosition[count]; From 96c5a74b88546b65c910f08340aaeededae40199 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 25 Jul 2026 00:21:42 +1000 Subject: [PATCH 51/94] Port the global-bit feature classification model Features that apply to every glyph of a plan's segments now share one reserved global mask bit; only per-glyph-varying features receive distinct bits (at most ~11 per plan). Classification is monotonic: a global feature can be demoted to varying or disabled outright, and either move is terminal, so repeated per-segment planning converges. Plan-wide disables collect no lookups: Indic and Khmer disable standard ligatures; Hangul disables contextual alternates. Registration API: EnableFeature registers a global feature, AddFeature a varying one. Group build reads classifications recorded at registration and skips zero-mask features. Fixes found during the reference audit: the universal shaper's syllable setup enabled rclt where it meant rphf, and rphf was registered enabled across the whole run instead of only each syllable's first three glyphs; substituted-repha recording now tests the enabled mask rather than the registered mask. Benchmark (12 iterations, clean control pairs; ratios are the load-independent metric): | Scenario | Before | After | Allocated | |------------|------------------|------------------|-----------| | Latin | 5.56 us / 2.49x | 5.69 us / 2.49x | 6,216 B | | Arabic | 7.37 us / 1.93x | 7.57 us / 1.93x | 4,368 B | | Devanagari | 11.84 us / 1.30x | 12.05 us / 1.30x | 3,832 B | Devanagari before-row re-measured at prior HEAD in the same session as the after-row (committed baseline 11.29 us / 1.27x was a faster window; ratio at parity both nights). Allocations byte-identical. Suite 5,585/0 including the differential oracle. Unlocks narrowing the per-glyph feature masks to 32 bits. --- src/SixLabors.Fonts/ShapingBuffer.cs | 22 --- .../Tables/AdvancedTypographic/ShapePlan.cs | 32 ++-- .../AdvancedTypographic/ShapePlanFeatures.cs | 179 +++++++++++++----- .../Shapers/ArabicShaper.cs | 12 +- .../Shapers/DefaultShaper.cs | 98 ++++++++-- .../Shapers/HangulShaper.cs | 20 +- .../Shapers/IndicShaper.cs | 43 +++-- .../Shapers/MyanmarShaper.cs | 24 +-- .../Shapers/UniversalShaper.cs | 53 +++--- 9 files changed, 315 insertions(+), 168 deletions(-) diff --git a/src/SixLabors.Fonts/ShapingBuffer.cs b/src/SixLabors.Fonts/ShapingBuffer.cs index 623c0276b..12be48570 100644 --- a/src/SixLabors.Fonts/ShapingBuffer.cs +++ b/src/SixLabors.Fonts/ShapingBuffer.cs @@ -387,28 +387,6 @@ public void SetGlyphId(int index, ushort glyphId) this.data[index].GlyphId = glyphId; } - /// - /// Adds the shaping feature to the record at the given index. The caller - /// supplies the feature's plan-assigned mask bit; the shaper that registers a - /// feature owns the plan whose bit it is, so the mask is always in scope. - /// - /// - /// Registration only ever accumulates: adding a disabled entry for an already - /// enabled feature must not clear the enabled bit. - /// - /// The zero-based index of the record. - /// The feature to apply. - /// The feature's plan-assigned mask bit. - public void AddShapingFeature(int index, TagEntry feature, ulong mask) - { - ref GlyphShapingData item = ref this.data[index]; - item.RegisteredFeatureMask |= mask; - if (feature.Enabled) - { - item.FeatureMask |= mask; - } - } - /// /// Adds the shaping feature to every record in the given range. The caller /// resolves the feature's mask bit once for the whole range: shaper plans diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlan.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlan.cs index 758e2381e..237c1b5c6 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlan.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlan.cs @@ -116,9 +116,10 @@ private ShapePlan( public Tag[] LanguageTags { get; } /// - /// Gets the plan's feature bit assignment, shared with the shaper that created - /// it. Bits are assigned in stage-list order when the groups are built and - /// append-only afterwards, so plans of the same identity assign identical bits. + /// Gets the plan's feature classification and bit assignment, shared with the + /// shaper that created it. Global features share one reserved bit; varying + /// features gain distinct bits in registration order, append-only, so plans of + /// the same identity assign identical layouts. /// public ShapePlanFeatures Features => this.Shaper.Features; @@ -211,15 +212,6 @@ public static ShapePlan Build( this.gposStageGroups = null; this.resolvedGsubFeatures.Clear(); - // Assign every stage feature its mask bit in stage-list order before any - // group is resolved. This single assignment point keeps layouts identical - // between plans of the same identity, which keeps applied masks portable - // across the pass's buffers. - for (int s = 0; s < this.Stages.Count; s++) - { - _ = this.Features.GetOrAddMask(this.Stages[s].FeatureTag); - } - List> groups = new(); GSubTable? gsubTable = this.gsubTable; FontMetrics fontMetrics = this.FontMetrics; @@ -344,21 +336,27 @@ private void BuildStageGroups( // Resolve each stage feature in the group to its lookups and fold // them into one list ordered by lookup index, freezing each entry's - // combined mask from the plan's assignments. The scan below runs - // backwards from the tail because resolved lookups arrive mostly - // ascending, so the insertion point is almost always at or near the - // end. + // combined mask from the classification recorded at registration. A + // zero mask means the feature is disabled for the plan or has no bit, + // so its lookups are never collected. The scan below runs backwards + // from the tail because resolved lookups arrive mostly ascending, so + // the insertion point is almost always at or near the end. ShapePlanStageGroup group = new(stageIndex, groupEnd); List<(Tag Feature, ushort Index, TLookup LookupTable, ulong Mask)> merged = group.Lookups; for (int s = stageIndex; s < groupEnd; s++) { Tag featureTag = stages[s].FeatureTag; + ulong featureMask = this.Features.GetMask(featureTag); + if (featureMask == 0) + { + continue; + } + if (!resolver(in featureTag, out List<(Tag Feature, ushort Index, TLookup LookupTable)>? lookups) || lookups is null) { continue; } - ulong featureMask = this.Features.GetOrAddMask(featureTag); foreach ((Tag Feature, ushort Index, TLookup LookupTable) featureLookup in lookups) { // Scan from the tail toward the head. Three outcomes: the diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanFeatures.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanFeatures.cs index 467db5ffd..dd4b82ea4 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanFeatures.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanFeatures.cs @@ -4,13 +4,15 @@ namespace SixLabors.Fonts.Tables.AdvancedTypographic; /// -/// The feature bit assignment owned by one shape plan: each OpenType feature the -/// plan touches receives a bit within a 64 bit mask, so per-glyph feature state is -/// stored and tested as plain bitwise operations. Bits are assigned in stage-list -/// order when the plan's groups are built and append-only afterwards, so two plans -/// of the same identity assign identical bits and applied masks stay portable -/// between the passes' buffers. The vertical trio occupies reserved bits identical -/// across every plan. +/// The feature classification and bit assignment owned by one shape plan. Features +/// that apply to every glyph of the plan's segments share a single reserved global +/// bit, so any number of them fits one mask; only features whose per-glyph state +/// varies receive distinct bits, assigned in registration order and append-only, so +/// plans of the same identity assign identical layouts. Classification moves in one +/// direction only: a global feature may be demoted to varying or disabled outright, +/// and either move is terminal, which keeps repeated per-segment planning +/// convergent. The vertical trio occupies reserved bits in the applied-mask space, +/// which is independent of the registration space managed here. /// internal sealed class ShapePlanFeatures { @@ -39,22 +41,45 @@ internal sealed class ShapePlanFeatures public const ulong VerticalFeatureMask = VerticalAlternatesMask | VerticalAlternatesForRotationMask | VerticalKerningMask; /// - /// The first bit available to assigned features; lower bits are reserved for - /// the vertical trio. + /// The single mask bit shared by every global feature: one that applies to all + /// glyphs of the plan's segments. Every planned glyph carries this bit, so the + /// lookups of any number of global features match with one bit between them and + /// the distinct bits are kept for features whose per-glyph state varies. + /// + public const ulong GlobalFeatureMask = 1UL << 63; + + /// + /// The first bit available to varying features; lower bits stay clear of the + /// applied-mask space's reserved trio so a mask value's space is identifiable + /// at a glance when debugging. /// private const int FirstAssignableBit = 3; /// - /// The number of assignable feature bits after the reserved bits. + /// The number of assignable varying-feature bits: everything between the first + /// assignable bit and the reserved global bit. /// - private const int AssignableBitCount = 64 - FirstAssignableBit; + private const int AssignableBitCount = 63 - FirstAssignableBit; /// - /// The assigned feature tag values, indexed by bit position above the reserved + /// The varying feature tag values, indexed by bit position above the reserved /// bits. Stored as raw values so lookups take the runtime's /// vectorized primitive search path. /// - private readonly List featureTags = new(32); + private readonly List featureTags = new(16); + + /// + /// The global feature tag values; each resolves to the shared + /// bit. + /// + private readonly List globalTags = new(32); + + /// + /// The disabled feature tag values. A disabled feature resolves to a zero mask + /// and never re-registers, so its lookups are never collected and its state + /// survives repeated per-segment planning. + /// + private readonly List disabledTags = new(1); /// /// The most recently resolved tag value. Queries strongly repeat the same @@ -97,13 +122,13 @@ public static ulong GetVerticalMask(Tag tag) } /// - /// Gets the mask bit for the given feature tag, or zero when the tag has no - /// assignment. The vertical trio answers from the reserved bits; a zero result - /// is safe at every consumption site: testing it enables or matches nothing and - /// clearing it clears nothing. + /// Gets the mask for the given feature tag: a varying feature's distinct bit, + /// the shared global bit for a global feature, or zero when the tag is disabled + /// or unknown. A zero result is safe at every consumption site: testing it + /// enables or matches nothing and clearing it clears nothing. /// /// The feature tag. - /// The single-bit mask, or zero. + /// The feature's mask, or zero. public ulong GetMask(Tag tag) { if (tag.Value == this.lastTagValue) @@ -111,60 +136,128 @@ public ulong GetMask(Tag tag) return this.lastMask; } - if (tag == KnownFeatureTags.VerticalAlternates) + if (this.disabledTags.Count > 0 && this.disabledTags.Contains(tag.Value)) { - return VerticalAlternatesMask; + return 0; } - if (tag == KnownFeatureTags.VerticalAlternatesForRotation) + int index = this.featureTags.IndexOf(tag.Value); + if (index >= 0) { - return VerticalAlternatesForRotationMask; + ulong mask = 1UL << (FirstAssignableBit + index); + this.lastTagValue = tag.Value; + this.lastMask = mask; + return mask; } - if (tag == KnownFeatureTags.VerticalKerning) + if (this.globalTags.Contains(tag.Value)) { - return VerticalKerningMask; + this.lastTagValue = tag.Value; + this.lastMask = GlobalFeatureMask; + return GlobalFeatureMask; } - int index = this.featureTags.IndexOf(tag.Value); - ulong mask = index < 0 ? 0 : 1UL << (FirstAssignableBit + index); - // A zero mask is never memoized: the tag may gain an assignment later and // the memo must not serve a stale zero after that. - if (mask != 0) + return 0; + } + + /// + /// Gets the distinct bit for a varying feature, assigning the next free bit + /// when the tag has none. A feature currently classified global is demoted to + /// varying, keeping the terminal classification: repeated per-segment planning + /// re-registers the same features, and a demoted feature must stay demoted. A + /// disabled feature stays disabled, and a plan that has exhausted its bits + /// assigns nothing; both cases return zero, which registers nothing, enables + /// nothing, and matches nothing at every consumption site. + /// + /// The feature tag. + /// The distinct bit, or zero when disabled or exhausted. + public ulong GetOrAddMask(Tag tag) + { + if (this.disabledTags.Count > 0 && this.disabledTags.Contains(tag.Value)) { + return 0; + } + + int index = this.featureTags.IndexOf(tag.Value); + if (index >= 0) + { + ulong mask = 1UL << (FirstAssignableBit + index); this.lastTagValue = tag.Value; this.lastMask = mask; + return mask; + } + + // Demote a global registration: removal is a no-op when the tag was never + // global. The allocation below then gives the feature its distinct bit. + this.globalTags.Remove(tag.Value); + + if (this.featureTags.Count == AssignableBitCount) + { + return 0; } - return mask; + this.featureTags.Add(tag.Value); + ulong added = 1UL << (FirstAssignableBit + this.featureTags.Count - 1); + this.lastTagValue = tag.Value; + this.lastMask = added; + return added; } /// - /// Gets the mask bit for the given feature tag, assigning the next free bit - /// when the tag is new to this plan. A plan that has exhausted its bits assigns - /// nothing and returns zero, which disables the feature: a zero mask registers - /// nothing, enables nothing, and matches nothing at every consumption site. + /// Gets the shared global bit for a feature that applies to every glyph, + /// recording the tag as global when it is new to this plan. A feature already + /// demoted to varying keeps its distinct bit and a disabled feature stays + /// disabled: both classifications are terminal so repeated per-segment planning + /// converges instead of oscillating. /// /// The feature tag. - /// The single-bit mask, or zero when the bits are exhausted. - public ulong GetOrAddMask(Tag tag) + /// The feature's mask, or zero when the feature is disabled. + public ulong GetOrAddGlobalMask(Tag tag) { - ulong mask = this.GetMask(tag); - if (mask != 0) + if (this.disabledTags.Count > 0 && this.disabledTags.Contains(tag.Value)) + { + return 0; + } + + int index = this.featureTags.IndexOf(tag.Value); + if (index >= 0) { + ulong mask = 1UL << (FirstAssignableBit + index); + this.lastTagValue = tag.Value; + this.lastMask = mask; return mask; } - if (this.featureTags.Count == AssignableBitCount) + if (!this.globalTags.Contains(tag.Value)) { - return 0; + this.globalTags.Add(tag.Value); } - this.featureTags.Add(tag.Value); - mask = 1UL << (FirstAssignableBit + this.featureTags.Count - 1); this.lastTagValue = tag.Value; - this.lastMask = mask; - return mask; + this.lastMask = GlobalFeatureMask; + return GlobalFeatureMask; + } + + /// + /// Disables a feature for the whole plan: its mask resolves to zero from now + /// on, so no lookups are collected for it and no per-glyph state can enable it. + /// Disabling is terminal; later registrations of the tag do not resurrect it. + /// + /// The feature tag. + public void DisableFeature(Tag tag) + { + this.globalTags.Remove(tag.Value); + if (!this.disabledTags.Contains(tag.Value)) + { + this.disabledTags.Add(tag.Value); + } + + if (this.lastTagValue == tag.Value) + { + this.lastTagValue = 0; + this.lastMask = 0; + } } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs index d3dfb3719..7a3871b7a 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs @@ -134,8 +134,8 @@ public ArabicShaper(ScriptClass script, TextOptions textOptions) /// protected override void PlanFeatures(ShapingBuffer buffer, int index, int count) { - this.AddFeature(buffer, index, count, CcmpTag); - this.AddFeature(buffer, index, count, LoclTag); + this.EnableFeature(buffer, index, count, CcmpTag); + this.EnableFeature(buffer, index, count, LoclTag); this.AddFeature(buffer, index, count, IsolTag, false); this.AddFeature(buffer, index, count, FinaTag, false); @@ -150,12 +150,12 @@ protected override void PlanFeatures(ShapingBuffer buffer, int index, int count) // DefaultShaper; forced vertical Arabic needs them here as well. if (buffer.TextOptions.LayoutMode.IsVertical()) { - this.AddFeature(buffer, index, count, CaltTag); - this.AddFeature(buffer, index, count, LigaTag); - this.AddFeature(buffer, index, count, CligTag); + this.EnableFeature(buffer, index, count, CaltTag); + this.EnableFeature(buffer, index, count, LigaTag); + this.EnableFeature(buffer, index, count, CligTag); } - this.AddFeature(buffer, index, count, MsetTag); + this.EnableFeature(buffer, index, count, MsetTag); } /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs index d30f9bedb..386d3c5d0 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs @@ -176,12 +176,12 @@ protected override void PlanFeatures(ShapingBuffer buffer, int index, int count) protected override void PlanPreprocessingFeatures(ShapingBuffer buffer, int index, int count) { // Add variation Features. - this.AddFeature(buffer, index, count, RvnrTag); + this.EnableFeature(buffer, index, count, RvnrTag); - // Add directional features once per direction span. The plan registers a - // direction's features across the whole span in a single range registration, - // producing the same per-glyph masks as per-glyph registration while - // resolving each feature's mask bit once. + // Add directional features once per direction span. A segment may span + // direction runs, so these stay varying features whose bits cover only + // their own span; a global classification would let one direction's + // lookups match glyphs of the other. int end = index + count; int spanStart = index; while (spanStart < end) @@ -216,11 +216,11 @@ protected override void PlanPreprocessingFeatures(ShapingBuffer buffer, int inde protected override void PlanPostprocessingFeatures(ShapingBuffer buffer, int index, int count) { // Add common features. - this.AddFeature(buffer, index, count, CcmpTag); - this.AddFeature(buffer, index, count, LoclTag); - this.AddFeature(buffer, index, count, RligTag); - this.AddFeature(buffer, index, count, MarkTag); - this.AddFeature(buffer, index, count, MkmkTag); + this.EnableFeature(buffer, index, count, CcmpTag); + this.EnableFeature(buffer, index, count, LoclTag); + this.EnableFeature(buffer, index, count, RligTag); + this.EnableFeature(buffer, index, count, MarkTag); + this.EnableFeature(buffer, index, count, MkmkTag); LayoutMode layoutMode = buffer.TextOptions.LayoutMode; bool isVerticalLayout = false; @@ -234,12 +234,12 @@ protected override void PlanPostprocessingFeatures(ShapingBuffer buffer, int ind if (!isVerticalLayout) { // Add horizontal features. - this.AddFeature(buffer, index, count, CaltTag); - this.AddFeature(buffer, index, count, CligTag); - this.AddFeature(buffer, index, count, LigaTag); - this.AddFeature(buffer, index, count, RcltTag); - this.AddFeature(buffer, index, count, CursTag); - this.AddFeature(buffer, index, count, KernTag); + this.EnableFeature(buffer, index, count, CaltTag); + this.EnableFeature(buffer, index, count, CligTag); + this.EnableFeature(buffer, index, count, LigaTag); + this.EnableFeature(buffer, index, count, RcltTag); + this.EnableFeature(buffer, index, count, CursTag); + this.EnableFeature(buffer, index, count, KernTag); } else { @@ -251,7 +251,7 @@ protected override void PlanPostprocessingFeatures(ShapingBuffer buffer, int ind // matter which script/langsys it is listed (or not) under. // See various bugs referenced from: // https://github.com/harfbuzz/harfbuzz/issues/63 - this.AddFeature(buffer, index, count, VertTag); + this.EnableFeature(buffer, index, count, VertTag); } // Add user defined features. @@ -260,7 +260,7 @@ protected override void PlanPostprocessingFeatures(ShapingBuffer buffer, int ind // We've already dealt with fractional features. if (feature != FracTag && feature != NumrTag && feature != DnomTag) { - this.AddFeature(buffer, index, count, feature); + this.EnableFeature(buffer, index, count, feature); } } } @@ -305,9 +305,67 @@ protected void AddFeature( } buffer.AddShapingFeatureRange(index, count, new TagEntry(feature, enabled), this.Features.GetOrAddMask(feature)); + this.AddStage(feature, preAction, postAction); + } + + /// + /// Registers a global feature over the given range: one that applies to every + /// glyph of the plan's segments and therefore shares the plan's single global + /// mask bit instead of consuming a distinct bit. Features whose per-glyph + /// state varies register through instead. + /// + /// The glyph shaping buffer. + /// The zero-based index of the first element. + /// The number of elements. + /// The feature tag to enable. + protected void EnableFeature(ShapingBuffer buffer, int index, int count, Tag feature) + => this.EnableFeature(buffer, index, count, feature, null, null); - // First registration wins, matching the previous set semantics: a duplicate - // tag keeps the originally supplied pre and post actions. + /// + /// Registers a global feature over the given range and attaches the supplied + /// stage actions. The feature applies to every glyph of the plan's segments + /// and therefore shares the plan's single global mask bit instead of consuming + /// a distinct bit. + /// + /// The glyph shaping buffer. + /// The zero-based index of the first element. + /// The number of elements. + /// The feature tag to enable. + /// The action to invoke before the feature is applied, or . + /// The action to invoke after the feature is applied, or . + protected void EnableFeature( + ShapingBuffer buffer, + int index, + int count, + Tag feature, + Action? preAction, + Action? postAction) + { + if (this.kerningMode == KerningMode.None) + { + if (feature == KernTag || feature == VKernTag) + { + return; + } + } + + buffer.AddShapingFeatureRange(index, count, new TagEntry(feature, true), this.Features.GetOrAddGlobalMask(feature)); + this.AddStage(feature, preAction, postAction); + } + + /// + /// Appends a shaping stage for the feature unless one already exists. The + /// first registration wins, matching the previous set semantics: a duplicate + /// tag keeps the originally supplied pre and post actions. + /// + /// The feature tag the stage applies. + /// The action to invoke before the feature is applied, or . + /// The action to invoke after the feature is applied, or . + private void AddStage( + Tag feature, + Action? preAction, + Action? postAction) + { List stages = this.shapingStages; for (int i = 0; i < stages.Count; i++) { diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs index 76a05d6c1..ac7bf6f35 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs @@ -186,16 +186,20 @@ protected override void PlanFeatures(ShapingBuffer buffer, int index, int count) } /// - protected override void AssignFeatures(ShapingBuffer buffer, int index, int count) + protected override void PlanPostprocessingFeatures(ShapingBuffer buffer, int index, int 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. - buffer.DisableShapingFeature(i, this.Features.GetMask(CaltTag)); - } + base.PlanPostprocessingFeatures(buffer, index, count); + + // Uniscribe does not apply contextual alternates for Hangul, and certain + // fonts (Noto Sans CJK, Source Han Sans, etc) apply all of the jamo + // lookups through the feature, which is not desirable. The feature is + // disabled for the whole plan, so its lookups are never collected. + this.Features.DisableFeature(CaltTag); + } + /// + protected override void AssignFeatures(ShapingBuffer buffer, int index, int count) + { // Apply the state machine to map glyphs to features. if (buffer.Role == ShapingBufferRole.Substitution) { diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs index 8f1228355..df47df033 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs @@ -209,32 +209,43 @@ public IndicShaper(ScriptClass script, Tag unicodeScriptTag, TextOptions textOpt /// protected override void PlanFeatures(ShapingBuffer buffer, int index, int count) { - this.AddFeature(buffer, index, count, LoclTag, preAction: this.SetupSyllables); - this.AddFeature(buffer, index, count, CcmpTag); + this.EnableFeature(buffer, index, count, LoclTag, this.SetupSyllables, null); + this.EnableFeature(buffer, index, count, CcmpTag); - this.AddFeature(buffer, index, count, NuktTag, preAction: this.InitialReorder); - this.AddFeature(buffer, index, count, AkhnTag); + this.EnableFeature(buffer, index, count, NuktTag, this.InitialReorder, null); + this.EnableFeature(buffer, index, count, AkhnTag); this.AddFeature(buffer, index, count, RphfTag, false); - this.AddFeature(buffer, index, count, RkrfTag); + this.EnableFeature(buffer, index, count, RkrfTag); this.AddFeature(buffer, index, count, PrefTag, false); this.AddFeature(buffer, index, count, BlwfTag, false); this.AddFeature(buffer, index, count, AbvfTag, false); this.AddFeature(buffer, index, count, HalfTag, false); this.AddFeature(buffer, index, count, PstfTag, false); - this.AddFeature(buffer, index, count, VatuTag); - this.AddFeature(buffer, index, count, CjctTag); - this.AddFeature(buffer, index, count, CfarTag, false, postAction: this.FinalReorder); + this.EnableFeature(buffer, index, count, VatuTag); + this.EnableFeature(buffer, index, count, CjctTag); + this.AddFeature(buffer, index, count, CfarTag, false, null, this.FinalReorder); this.AddFeature(buffer, index, count, InitTag, false); - this.AddFeature(buffer, index, count, PresTag); - this.AddFeature(buffer, index, count, AbvsTag); - this.AddFeature(buffer, index, count, BlwsTag); - this.AddFeature(buffer, index, count, PstsTag); - this.AddFeature(buffer, index, count, HalnTag); - this.AddFeature(buffer, index, count, DistTag); - this.AddFeature(buffer, index, count, AbvmTag); - this.AddFeature(buffer, index, count, BlwmTag); + this.EnableFeature(buffer, index, count, PresTag); + this.EnableFeature(buffer, index, count, AbvsTag); + this.EnableFeature(buffer, index, count, BlwsTag); + this.EnableFeature(buffer, index, count, PstsTag); + this.EnableFeature(buffer, index, count, HalnTag); + this.EnableFeature(buffer, index, count, DistTag); + this.EnableFeature(buffer, index, count, AbvmTag); + this.EnableFeature(buffer, index, count, BlwmTag); + } + + /// + protected override void PlanPostprocessingFeatures(ShapingBuffer buffer, int index, int count) + { + base.PlanPostprocessingFeatures(buffer, index, count); + + // Standard ligature substitution interferes with the conjunct forms these + // scripts build through their dedicated features, so the feature is + // disabled for the whole plan and its lookups are never collected. + this.Features.DisableFeature(LigaTag); } /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs index b3c08880f..a87aba923 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs @@ -122,18 +122,18 @@ public MyanmarShaper(ScriptClass script, TextOptions textOptions, FontMetrics fo /// protected override void PlanFeatures(ShapingBuffer buffer, int index, int count) { - this.AddFeature(buffer, index, count, LoclTag, preAction: this.SetupSyllables); - this.AddFeature(buffer, index, count, CcmpTag); - - this.AddFeature(buffer, index, count, RphfTag, preAction: this.InitialReorder); - this.AddFeature(buffer, index, count, PrefTag); - this.AddFeature(buffer, index, count, BlwfTag); - this.AddFeature(buffer, index, count, PstfTag); - - this.AddFeature(buffer, index, count, PresTag); - this.AddFeature(buffer, index, count, AbvsTag); - this.AddFeature(buffer, index, count, BlwsTag); - this.AddFeature(buffer, index, count, PstsTag); + this.EnableFeature(buffer, index, count, LoclTag, this.SetupSyllables, null); + this.EnableFeature(buffer, index, count, CcmpTag); + + this.EnableFeature(buffer, index, count, RphfTag, this.InitialReorder, null); + this.EnableFeature(buffer, index, count, PrefTag); + this.EnableFeature(buffer, index, count, BlwfTag); + this.EnableFeature(buffer, index, count, PstfTag); + + this.EnableFeature(buffer, index, count, PresTag); + this.EnableFeature(buffer, index, count, AbvsTag); + this.EnableFeature(buffer, index, count, BlwsTag); + this.EnableFeature(buffer, index, count, PstsTag); } /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs index 6f79d21c2..6f9414358 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs @@ -174,32 +174,34 @@ public UniversalShaper(ScriptClass script, TextOptions textOptions, FontMetrics protected override void PlanFeatures(ShapingBuffer buffer, int index, int count) { // Default glyph pre-processing group - this.AddFeature(buffer, index, count, LoclTag, preAction: this.SetupSyllables); - this.AddFeature(buffer, index, count, CcmpTag); - this.AddFeature(buffer, index, count, NuktTag); - this.AddFeature(buffer, index, count, AkhnTag); + this.EnableFeature(buffer, index, count, LoclTag, this.SetupSyllables, null); + this.EnableFeature(buffer, index, count, CcmpTag); + this.EnableFeature(buffer, index, count, NuktTag); + this.EnableFeature(buffer, index, count, AkhnTag); - // Reordering group - this.AddFeature(buffer, index, count, RphfTag, true, ClearSubstitutionFlags, RecordRhpf); - this.AddFeature(buffer, index, count, PrefTag, true, ClearSubstitutionFlags, RecordPref); + // Reordering group. The repha feature varies per glyph: syllable setup + // enables it on each syllable's leading glyphs only, so a repha forms + // there and nowhere else. + this.AddFeature(buffer, index, count, RphfTag, false, ClearSubstitutionFlags, RecordRhpf); + this.EnableFeature(buffer, index, count, PrefTag, ClearSubstitutionFlags, RecordPref); // Orthographic unit shaping group - this.AddFeature(buffer, index, count, RkrfTag); - this.AddFeature(buffer, index, count, AbvfTag); - this.AddFeature(buffer, index, count, BlwfTag); - this.AddFeature(buffer, index, count, HalfTag); - this.AddFeature(buffer, index, count, PstfTag); - this.AddFeature(buffer, index, count, VatuTag); - this.AddFeature(buffer, index, count, CjctTag, postAction: this.Reorder); + this.EnableFeature(buffer, index, count, RkrfTag); + this.EnableFeature(buffer, index, count, AbvfTag); + this.EnableFeature(buffer, index, count, BlwfTag); + this.EnableFeature(buffer, index, count, HalfTag); + this.EnableFeature(buffer, index, count, PstfTag); + this.EnableFeature(buffer, index, count, VatuTag); + this.EnableFeature(buffer, index, count, CjctTag, null, this.Reorder); // Standard topographic presentation and positional feature application - this.AddFeature(buffer, index, count, AbvsTag); - this.AddFeature(buffer, index, count, BlwsTag); - this.AddFeature(buffer, index, count, PresTag); - this.AddFeature(buffer, index, count, PstsTag); - this.AddFeature(buffer, index, count, DistTag); - this.AddFeature(buffer, index, count, AbvmTag); - this.AddFeature(buffer, index, count, BlwmTag); + this.EnableFeature(buffer, index, count, AbvsTag); + this.EnableFeature(buffer, index, count, BlwsTag); + this.EnableFeature(buffer, index, count, PresTag); + this.EnableFeature(buffer, index, count, PstsTag); + this.EnableFeature(buffer, index, count, DistTag); + this.EnableFeature(buffer, index, count, AbvmTag); + this.EnableFeature(buffer, index, count, BlwmTag); } /// @@ -276,6 +278,7 @@ private void SetupSyllables(ShapePlan plan, ShapingBuffer buffer, int index, int } int syllable = 0; + ulong rphfMask = this.Features.GetMask(RphfTag); StateMachine.MatchEnumerator match = StateMachine.EnumerateMatches(values); while (match.MoveNext()) { @@ -297,14 +300,16 @@ private void SetupSyllables(ShapePlan plan, ShapingBuffer buffer, int index, int data.Syllable.Number = syllable; } - // Assign rphf feature + // Enable the repha feature on the syllable's leading glyphs only: a + // repha can form there and nowhere else, so the feature stays off for + // the rest of the syllable. int limit = buffer[match.StartIndex + index].Syllable.UseCategory == CategoryR ? 1 : Math.Min(3, match.EndIndex - match.StartIndex); for (int i = match.StartIndex; i < match.StartIndex + limit; i++) { - buffer.AddShapingFeature(i + index, new TagEntry(RcltTag, true), this.Features.GetOrAddMask(RcltTag)); + buffer.EnableShapingFeature(i + index, rphfMask); } } } @@ -350,7 +355,7 @@ private static void RecordRhpf(ShapePlan plan, ShapingBuffer buffer, int index, for (int i = index; i < end; i++) { ref GlyphShapingData data = ref buffer[i]; - if (data.IsSubstituted && (data.RegisteredFeatureMask & rphfMask) != 0) + if (data.IsSubstituted && (data.FeatureMask & rphfMask) != 0) { // Mark a substituted repha. if (data.Syllable.Type != SyllableType.None) From a546de9a8509b513a761127434d7269f8ab2db07 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 25 Jul 2026 13:32:27 +1000 Subject: [PATCH 52/94] Demote Hangul contextual alternates instead of disabling them The feature keeps a distinct varying bit whose mask is off by default: no glyph applies it unless a later range registration enables it, and feature assignment clears it on jamo after composition even then, so the jamo lookups certain fonts hide behind the feature can never fire there while range-scoped requests keep working elsewhere. Output verified glyph-for-glyph against HarfBuzz 14.2.1 on the Hangul suite inputs, including the probe matrix that separated this feature from language-system selection: the localized punctuation forms seen under lang=ko come from the KOR langsys locl feature, not from contextual alternates, and both engines agree under the default language. Suite 5,585/0. No benchmark scenario shapes Hangul; the clean gate run for this tree: Latin 5.06 us / 2.23x / 6,216 B, Arabic 7.62 us / 1.94x / 4,368 B, Devanagari 11.74 us / 1.32x / 3,832 B. --- .../Shapers/HangulShaper.cs | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs index ac7bf6f35..cb1f70cbf 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs @@ -190,16 +190,20 @@ protected override void PlanPostprocessingFeatures(ShapingBuffer buffer, int ind { base.PlanPostprocessingFeatures(buffer, index, count); - // Uniscribe does not apply contextual alternates for Hangul, and certain - // fonts (Noto Sans CJK, Source Han Sans, etc) apply all of the jamo - // lookups through the feature, which is not desirable. The feature is - // disabled for the whole plan, so its lookups are never collected. - this.Features.DisableFeature(CaltTag); + // Certain fonts (Noto Sans CJK, Source Han Sans, etc) apply all of the + // jamo lookups through contextual alternates, which is not desirable. + // The feature is demoted from global to a varying feature whose mask is + // off by default: no glyph applies it unless a later range registration + // enables it, and feature assignment clears it on jamo even then, so the + // jamo lookups such fonts hide behind the feature can never fire there. + buffer.AddShapingFeatureRange(index, count, new TagEntry(CaltTag, false), this.Features.GetOrAddMask(CaltTag)); } /// protected override void AssignFeatures(ShapingBuffer buffer, int index, int count) { + int entryCount = buffer.Count; + // Apply the state machine to map glyphs to features. if (buffer.Role == ShapingBufferRole.Substitution) { @@ -292,6 +296,21 @@ protected override void AssignFeatures(ShapingBuffer buffer, int index, int coun } } } + + // Keep contextual alternates away from jamo, running after composition + // and decomposition so the check sees the segment's final code points: + // composed syllables keep the feature while the jamo lookups some fonts + // hide behind it can never fire on the jamo themselves. + count += buffer.Count - entryCount; + int end = index + count; + for (int i = index; i < end && i < buffer.Count; i++) + { + int type = GetSyllableType(buffer[i].CodePoint); + if (type is L or V or T) + { + buffer.DisableShapingFeature(i, this.Features.GetMask(CaltTag)); + } + } } /// From ffc7b3e8c68605345bd0f09e3fc91e45a1816ca3 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 25 Jul 2026 13:32:52 +1000 Subject: [PATCH 53/94] Narrow the per-glyph feature masks to 32 bits The global-bit classification bounds distinct bits at roughly eleven per plan, so the three per-glyph masks drop from 8 to 4 bytes each, removing 12 bytes from the glyph record. The shared global bit moves from bit 63 to bit 31 and 28 assignable varying bits remain above the reserved vertical trio. Benchmark (12 iterations, clean control pair, measured with the ASCII grapheme fast path also in tree; ratios are the load-independent metric): | Scenario | Before | After | Allocated | |------------|------------------|------------------|-----------| | Latin | 5.69 us / 2.49x | 5.06 us / 2.23x | 6,216 B | | Arabic | 7.57 us / 1.93x | 7.62 us / 1.94x | 4,368 B | | Devanagari | 12.05 us / 1.30x | 11.74 us / 1.32x | 3,832 B | The Latin gain carries the ASCII fast path committed separately; this change is judged on non-regression, and Arabic and Devanagari ratios sit within measurement noise of their baselines. Allocations are byte-identical. Suite 5,585/0 including the differential oracle. --- src/SixLabors.Fonts/GlyphShapingData.cs | 6 ++-- src/SixLabors.Fonts/ShapingBuffer.cs | 14 ++++----- .../AdvancedTypographicUtils.cs | 4 +-- .../Tables/AdvancedTypographic/GPosTable.cs | 4 +-- .../GSub/LookupListTable.cs | 4 +-- .../GSub/LookupType1SubTable.cs | 4 +-- .../GSub/LookupType2SubTable.cs | 2 +- .../GSub/LookupType3SubTable.cs | 2 +- .../GSub/LookupType4SubTable.cs | 2 +- .../GSub/LookupType5SubTable.cs | 6 ++-- .../GSub/LookupType6SubTable.cs | 6 ++-- .../GSub/LookupType8SubTable.cs | 2 +- .../GSub/NotImplementedSubTable.cs | 2 +- .../Tables/AdvancedTypographic/GSubTable.cs | 4 +-- .../Tables/AdvancedTypographic/ShapePlan.cs | 6 ++-- .../AdvancedTypographic/ShapePlanFeatures.cs | 30 +++++++++---------- .../ShapePlanStageGroup{TLookup}.cs | 2 +- .../Shapers/UniversalShaper.cs | 4 +-- src/SixLabors.Fonts/TextShaper.Pipeline.cs | 2 +- 19 files changed, 53 insertions(+), 53 deletions(-) diff --git a/src/SixLabors.Fonts/GlyphShapingData.cs b/src/SixLabors.Fonts/GlyphShapingData.cs index 608d2ee97..b8ea745c4 100644 --- a/src/SixLabors.Fonts/GlyphShapingData.cs +++ b/src/SixLabors.Fonts/GlyphShapingData.cs @@ -245,14 +245,14 @@ public int LigatureComponent /// only ever reveals a registered bit; a feature that was never registered for the /// glyph cannot be enabled. /// - public ulong RegisteredFeatureMask { get; set; } + public uint RegisteredFeatureMask { get; set; } /// /// Gets or sets the mask of features currently enabled for this glyph: the subset of /// a lookup application gate tests with a single /// bitwise AND. /// - public ulong FeatureMask { get; set; } + public uint FeatureMask { get; set; } /// /// Gets or sets the mask of features whose lookups actually changed this glyph. @@ -261,7 +261,7 @@ public int LigatureComponent /// therefore survives the copy into the positioning buffer regardless of which /// plan wrote it. /// - public ulong AppliedFeatureMask { get; set; } + public uint AppliedFeatureMask { get; set; } /// /// Gets or sets a value indicating whether this glyph is the result of a substitution. diff --git a/src/SixLabors.Fonts/ShapingBuffer.cs b/src/SixLabors.Fonts/ShapingBuffer.cs index 12be48570..4bc2968be 100644 --- a/src/SixLabors.Fonts/ShapingBuffer.cs +++ b/src/SixLabors.Fonts/ShapingBuffer.cs @@ -301,7 +301,7 @@ public bool SeedMetricsInPlace(Font font) LayoutMode layoutMode = this.TextOptions.LayoutMode; ColorFontSupport colorFontSupport = this.TextOptions.ColorFontSupport; - ulong verticalMask = ShapePlanFeatures.VerticalFeatureMask; + uint verticalMask = ShapePlanFeatures.VerticalFeatureMask; for (int i = 0; i < this.count; i++) { @@ -397,7 +397,7 @@ public void SetGlyphId(int index, ushort glyphId) /// The number of records in the range. /// The feature to apply. /// The feature's plan-assigned mask bit. - public void AddShapingFeatureRange(int index, int count, TagEntry feature, ulong mask) + public void AddShapingFeatureRange(int index, int count, TagEntry feature, uint mask) { int end = index + count; for (int i = index; i < end; i++) @@ -420,7 +420,7 @@ public void AddShapingFeatureRange(int index, int count, TagEntry feature, ulong /// /// The zero-based index of the record. /// The feature's plan-assigned mask bit. - public void EnableShapingFeature(int index, ulong mask) + public void EnableShapingFeature(int index, uint mask) { ref GlyphShapingData item = ref this.data[index]; item.FeatureMask |= item.RegisteredFeatureMask & mask; @@ -434,7 +434,7 @@ public void EnableShapingFeature(int index, ulong mask) /// /// The zero-based index of the record. /// The feature's plan-assigned mask bit. - public void DisableShapingFeature(int index, ulong mask) + public void DisableShapingFeature(int index, uint mask) { ref GlyphShapingData item = ref this.data[index]; item.FeatureMask &= ~mask; @@ -785,7 +785,7 @@ public void Replace(int index, ReadOnlySpan glyphIds, Tag feature) if (glyphIds.Length > 1) { GlyphShapingData template = this.data[index]; - ulong mask = ShapePlanFeatures.GetVerticalMask(feature); + uint mask = ShapePlanFeatures.GetVerticalMask(feature); for (int i = 1; i < glyphIds.Length; i++) { GlyphShapingData inserted = new(template, false) @@ -839,7 +839,7 @@ public bool TryAdd(Font font, ShapingBuffer workspace) LayoutMode layoutMode = this.TextOptions.LayoutMode; ColorFontSupport colorFontSupport = this.TextOptions.ColorFontSupport; - ulong verticalMask = ShapePlanFeatures.VerticalFeatureMask; + uint verticalMask = ShapePlanFeatures.VerticalFeatureMask; for (int i = 0; i < workspace.count; i++) { @@ -917,7 +917,7 @@ public bool TryUpdate(Font font, ShapingBuffer workspace) ColorFontSupport colorFontSupport = this.TextOptions.ColorFontSupport; bool hasFallBacks = false; - ulong verticalMask = ShapePlanFeatures.VerticalFeatureMask; + uint verticalMask = ShapePlanFeatures.VerticalFeatureMask; for (int i = 0; i < this.count; i++) { diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs index afc31c6c8..8b7ade56f 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs @@ -110,7 +110,7 @@ public static bool ApplyLookupList( FontMetrics fontMetrics, GSubTable table, Tag feature, - ulong lookupMask, + uint lookupMask, LookupFlags lookupFlags, ushort markFilteringSet, SequenceLookupRecord[] records, @@ -198,7 +198,7 @@ public static bool ApplyLookupList( /// The array of glyph IDs to match. /// A span to store matched glyph indices, or default if not needed. /// if the entire sequence was matched; otherwise, . - public static bool MatchInputSequence(SkippingGlyphIterator iterator, ulong featureMask, ushort increment, ushort[] sequence, Span matches) + public static bool MatchInputSequence(SkippingGlyphIterator iterator, uint featureMask, ushort increment, ushort[] sequence, Span matches) { // The mask travels as match state so the lambda stays static: a capturing // lambda here would allocate a closure and delegate on every ligature attempt. diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs index 475b28c9e..81236b0f7 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs @@ -323,13 +323,13 @@ private bool PositionSegment( for (int g = 0; g < groups.Count; g++) { ShapePlanStageGroup group = groups[g]; - List<(Tag Feature, ushort Index, LookupTable LookupTable, ulong Mask)> merged = group.Lookups; + List<(Tag Feature, ushort Index, LookupTable LookupTable, uint Mask)> merged = group.Lookups; shapingStages[group.Start].PreProcessFeature(shapePlan, buffer, index, count); for (int m = 0; m < merged.Count; m++) { - (Tag feature, ushort _, LookupTable featureLookupTable, ulong featureMask) = merged[m]; + (Tag feature, ushort _, LookupTable featureLookupTable, uint featureMask) = merged[m]; // Skip the whole lookup when its coverage cannot intersect any // glyph id the buffer has ever contained; most fonts carry diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs index fd5598c2e..6a852c5f8 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs @@ -203,7 +203,7 @@ public bool TrySubstitution( GSubTable table, ShapingBuffer buffer, Tag feature, - ulong lookupMask, + uint lookupMask, int index, int count) { @@ -352,7 +352,7 @@ public abstract bool TrySubstitution( GSubTable table, ShapingBuffer buffer, Tag feature, - ulong lookupMask, + uint lookupMask, int index, int count); diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs index 33319ffb0..109106d29 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs @@ -102,7 +102,7 @@ public override bool TrySubstitution( GSubTable table, ShapingBuffer buffer, Tag feature, - ulong lookupMask, + uint lookupMask, int index, int count) { @@ -197,7 +197,7 @@ public override bool TrySubstitution( GSubTable table, ShapingBuffer buffer, Tag feature, - ulong lookupMask, + uint lookupMask, int index, int count) { diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs index 193ba8afc..55c4ced03 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs @@ -124,7 +124,7 @@ public override bool TrySubstitution( GSubTable table, ShapingBuffer buffer, Tag feature, - ulong lookupMask, + uint lookupMask, int index, int count) { diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs index 589478dc0..05fe6d825 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs @@ -123,7 +123,7 @@ public override bool TrySubstitution( GSubTable table, ShapingBuffer buffer, Tag feature, - ulong lookupMask, + uint lookupMask, int index, int count) { diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs index 1de22a502..23f37253f 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs @@ -154,7 +154,7 @@ public override bool TrySubstitution( GSubTable table, ShapingBuffer buffer, Tag feature, - ulong lookupMask, + uint lookupMask, int index, int count) { diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs index 7043bf376..ed5cae556 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs @@ -88,7 +88,7 @@ public override bool TrySubstitution( GSubTable table, ShapingBuffer buffer, Tag feature, - ulong lookupMask, + uint lookupMask, int index, int count) { @@ -243,7 +243,7 @@ public override bool TrySubstitution( GSubTable table, ShapingBuffer buffer, Tag feature, - ulong lookupMask, + uint lookupMask, int index, int count) { @@ -415,7 +415,7 @@ public override bool TrySubstitution( GSubTable table, ShapingBuffer buffer, Tag feature, - ulong lookupMask, + uint lookupMask, int index, int count) { diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs index c9f9a9a44..48d8a93ae 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs @@ -91,7 +91,7 @@ public override bool TrySubstitution( GSubTable table, ShapingBuffer buffer, Tag feature, - ulong lookupMask, + uint lookupMask, int index, int count) { @@ -281,7 +281,7 @@ public override bool TrySubstitution( GSubTable table, ShapingBuffer buffer, Tag feature, - ulong lookupMask, + uint lookupMask, int index, int count) { @@ -478,7 +478,7 @@ public override bool TrySubstitution( GSubTable table, ShapingBuffer buffer, Tag feature, - ulong lookupMask, + uint lookupMask, int index, int count) { diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType8SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType8SubTable.cs index deea97d08..40fae5165 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType8SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType8SubTable.cs @@ -157,7 +157,7 @@ public override bool TrySubstitution( GSubTable table, ShapingBuffer buffer, Tag feature, - ulong lookupMask, + uint lookupMask, int index, int count) { diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/NotImplementedSubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/NotImplementedSubTable.cs index 297364d42..c0ae15fdb 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/NotImplementedSubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/NotImplementedSubTable.cs @@ -23,7 +23,7 @@ public override bool TrySubstitution( GSubTable table, ShapingBuffer buffer, Tag feature, - ulong lookupMask, + uint lookupMask, int index, int count) => false; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs index bbf6b4ba9..22b790e9c 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs @@ -287,7 +287,7 @@ private void ApplyMergedLookups( FontMetrics fontMetrics, ShapingBuffer buffer, ref SkippingGlyphIterator iterator, - List<(Tag Feature, ushort Index, LookupTable LookupTable, ulong Mask)> merged, + List<(Tag Feature, ushort Index, LookupTable LookupTable, uint Mask)> merged, int index, ref int count, ref int i, @@ -298,7 +298,7 @@ private void ApplyMergedLookups( { for (int m = 0; m < merged.Count; m++) { - (Tag feature, ushort _, LookupTable featureLookupTable, ulong featureMask) = merged[m]; + (Tag feature, ushort _, LookupTable featureLookupTable, uint featureMask) = merged[m]; // Skip the whole lookup when its coverage cannot intersect any glyph id // the buffer has ever contained; most fonts carry many lookups for diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlan.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlan.cs index 237c1b5c6..718849d50 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlan.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlan.cs @@ -342,11 +342,11 @@ private void BuildStageGroups( // from the tail because resolved lookups arrive mostly ascending, so // the insertion point is almost always at or near the end. ShapePlanStageGroup group = new(stageIndex, groupEnd); - List<(Tag Feature, ushort Index, TLookup LookupTable, ulong Mask)> merged = group.Lookups; + List<(Tag Feature, ushort Index, TLookup LookupTable, uint Mask)> merged = group.Lookups; for (int s = stageIndex; s < groupEnd; s++) { Tag featureTag = stages[s].FeatureTag; - ulong featureMask = this.Features.GetMask(featureTag); + uint featureMask = this.Features.GetMask(featureTag); if (featureMask == 0) { continue; @@ -368,7 +368,7 @@ private void BuildStageGroups( bool alreadyMerged = false; while (insertAt > 0) { - (Tag Feature, ushort Index, TLookup LookupTable, ulong Mask) prior = merged[insertAt - 1]; + (Tag Feature, ushort Index, TLookup LookupTable, uint Mask) prior = merged[insertAt - 1]; if (prior.Index == featureLookup.Index) { merged[insertAt - 1] = (prior.Feature, prior.Index, prior.LookupTable, prior.Mask | featureMask); diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanFeatures.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanFeatures.cs index dd4b82ea4..e459b9654 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanFeatures.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanFeatures.cs @@ -22,23 +22,23 @@ internal sealed class ShapePlanFeatures /// that span plans, such as the copy-out's vertical detection, never depend on /// any single plan's layout. /// - public const ulong VerticalAlternatesMask = 1UL << 0; + public const uint VerticalAlternatesMask = 1U << 0; /// /// The fixed mask bit for the vertical alternates for rotation feature. /// - public const ulong VerticalAlternatesForRotationMask = 1UL << 1; + public const uint VerticalAlternatesForRotationMask = 1U << 1; /// /// The fixed mask bit for the vertical kerning feature. /// - public const ulong VerticalKerningMask = 1UL << 2; + public const uint VerticalKerningMask = 1U << 2; /// /// The combined mask of the three vertical alternate features, constant across /// plans by the reserved-bit contract above. /// - public const ulong VerticalFeatureMask = VerticalAlternatesMask | VerticalAlternatesForRotationMask | VerticalKerningMask; + public const uint VerticalFeatureMask = VerticalAlternatesMask | VerticalAlternatesForRotationMask | VerticalKerningMask; /// /// The single mask bit shared by every global feature: one that applies to all @@ -46,7 +46,7 @@ internal sealed class ShapePlanFeatures /// lookups of any number of global features match with one bit between them and /// the distinct bits are kept for features whose per-glyph state varies. /// - public const ulong GlobalFeatureMask = 1UL << 63; + public const uint GlobalFeatureMask = 1U << 31; /// /// The first bit available to varying features; lower bits stay clear of the @@ -59,7 +59,7 @@ internal sealed class ShapePlanFeatures /// The number of assignable varying-feature bits: everything between the first /// assignable bit and the reserved global bit. /// - private const int AssignableBitCount = 63 - FirstAssignableBit; + private const int AssignableBitCount = 31 - FirstAssignableBit; /// /// The varying feature tag values, indexed by bit position above the reserved @@ -92,7 +92,7 @@ internal sealed class ShapePlanFeatures /// /// The mask paired with . /// - private ulong lastMask; + private uint lastMask; /// /// Gets the fixed vertical-trio mask bit for a feature, or zero for any other @@ -101,7 +101,7 @@ internal sealed class ShapePlanFeatures /// /// The feature tag whose lookups applied. /// The fixed vertical mask bit, or zero. - public static ulong GetVerticalMask(Tag tag) + public static uint GetVerticalMask(Tag tag) { if (tag == KnownFeatureTags.VerticalAlternates) { @@ -129,7 +129,7 @@ public static ulong GetVerticalMask(Tag tag) /// /// The feature tag. /// The feature's mask, or zero. - public ulong GetMask(Tag tag) + public uint GetMask(Tag tag) { if (tag.Value == this.lastTagValue) { @@ -144,7 +144,7 @@ public ulong GetMask(Tag tag) int index = this.featureTags.IndexOf(tag.Value); if (index >= 0) { - ulong mask = 1UL << (FirstAssignableBit + index); + uint mask = 1U << (FirstAssignableBit + index); this.lastTagValue = tag.Value; this.lastMask = mask; return mask; @@ -173,7 +173,7 @@ public ulong GetMask(Tag tag) /// /// The feature tag. /// The distinct bit, or zero when disabled or exhausted. - public ulong GetOrAddMask(Tag tag) + public uint GetOrAddMask(Tag tag) { if (this.disabledTags.Count > 0 && this.disabledTags.Contains(tag.Value)) { @@ -183,7 +183,7 @@ public ulong GetOrAddMask(Tag tag) int index = this.featureTags.IndexOf(tag.Value); if (index >= 0) { - ulong mask = 1UL << (FirstAssignableBit + index); + uint mask = 1U << (FirstAssignableBit + index); this.lastTagValue = tag.Value; this.lastMask = mask; return mask; @@ -199,7 +199,7 @@ public ulong GetOrAddMask(Tag tag) } this.featureTags.Add(tag.Value); - ulong added = 1UL << (FirstAssignableBit + this.featureTags.Count - 1); + uint added = 1U << (FirstAssignableBit + this.featureTags.Count - 1); this.lastTagValue = tag.Value; this.lastMask = added; return added; @@ -214,7 +214,7 @@ public ulong GetOrAddMask(Tag tag) /// /// The feature tag. /// The feature's mask, or zero when the feature is disabled. - public ulong GetOrAddGlobalMask(Tag tag) + public uint GetOrAddGlobalMask(Tag tag) { if (this.disabledTags.Count > 0 && this.disabledTags.Contains(tag.Value)) { @@ -224,7 +224,7 @@ public ulong GetOrAddGlobalMask(Tag tag) int index = this.featureTags.IndexOf(tag.Value); if (index >= 0) { - ulong mask = 1UL << (FirstAssignableBit + index); + uint mask = 1U << (FirstAssignableBit + index); this.lastTagValue = tag.Value; this.lastMask = mask; return mask; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanStageGroup{TLookup}.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanStageGroup{TLookup}.cs index 2d2ddd970..cb71f547f 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanStageGroup{TLookup}.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanStageGroup{TLookup}.cs @@ -42,5 +42,5 @@ public ShapePlanStageGroup(int start, int end) /// order. A lookup registered by several features appears once with their /// plan-assigned masks combined, so application consumes the list directly. /// - public List<(Tag Feature, ushort Index, TLookup LookupTable, ulong Mask)> Lookups { get; } + public List<(Tag Feature, ushort Index, TLookup LookupTable, uint Mask)> Lookups { get; } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs index 6f9414358..678991c2a 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs @@ -278,7 +278,7 @@ private void SetupSyllables(ShapePlan plan, ShapingBuffer buffer, int index, int } int syllable = 0; - ulong rphfMask = this.Features.GetMask(RphfTag); + uint rphfMask = this.Features.GetMask(RphfTag); StateMachine.MatchEnumerator match = StateMachine.EnumerateMatches(values); while (match.MoveNext()) { @@ -351,7 +351,7 @@ private static void RecordRhpf(ShapePlan plan, ShapingBuffer buffer, int index, } int end = index + count; - ulong rphfMask = plan.Features.GetMask(RphfTag); + uint rphfMask = plan.Features.GetMask(RphfTag); for (int i = index; i < end; i++) { ref GlyphShapingData data = ref buffer[i]; diff --git a/src/SixLabors.Fonts/TextShaper.Pipeline.cs b/src/SixLabors.Fonts/TextShaper.Pipeline.cs index f304ce433..acafb8c34 100644 --- a/src/SixLabors.Fonts/TextShaper.Pipeline.cs +++ b/src/SixLabors.Fonts/TextShaper.Pipeline.cs @@ -386,7 +386,7 @@ or BidiCharacterType.FirstStrongIsolate // deduplicates into a run table and per-glyph state splits into parallel // identity and geometry arrays of pure values, so the scratch can go back to // the pool before consumption and no metrics reference survives shaping. - ulong verticalMask = ShapePlanFeatures.VerticalFeatureMask; + uint verticalMask = ShapePlanFeatures.VerticalFeatureMask; int count = shaped.Count; ShapedGlyphInfo[] infos = new ShapedGlyphInfo[count]; ShapedGlyphPosition[] positions = new ShapedGlyphPosition[count]; From db7a483f7ce174bd8269934288c2f7499188849d Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 25 Jul 2026 13:33:10 +1000 Subject: [PATCH 54/94] Short-circuit single-scalar ASCII grapheme clusters A printable ASCII value followed by another ASCII value, or by the end of the input, always forms a single-scalar cluster: no ASCII value extends, joins, or prepends, and any scalar that could attach to the cluster is necessarily non-ASCII. The enumerator answers such clusters directly with fixed metadata and one emoji-properties lookup for the keycap bases, skipping the boundary machine, both scalar decodes, and the remaining property lookups. The check is per cluster, so text containing newlines keeps the win on every other cluster. Benchmark (12 iterations, clean control pair, measured with the mask narrowing also in tree): | Scenario | Before | After | Allocated | |------------|------------------|------------------|-----------| | Latin | 5.69 us / 2.49x | 5.06 us / 2.23x | 6,216 B | | Arabic | 7.57 us / 1.93x | 7.62 us / 1.94x | 4,368 B | | Devanagari | 12.05 us / 1.30x | 11.74 us / 1.32x | 3,832 B | The Latin improvement is attributed to this change by cross-scenario signature: the narrowing affects every scenario roughly equally and Arabic and Devanagari sit at their baselines, while the gain appears only where the text is pure ASCII; a Devanagari row measured without this change (11.78 us, tight deviation) matches the row with it. Suite 5,585/0. --- .../Unicode/SpanGraphemeEnumerator.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/SixLabors.Fonts/Unicode/SpanGraphemeEnumerator.cs b/src/SixLabors.Fonts/Unicode/SpanGraphemeEnumerator.cs index c77bcaff7..59f0597ec 100644 --- a/src/SixLabors.Fonts/Unicode/SpanGraphemeEnumerator.cs +++ b/src/SixLabors.Fonts/Unicode/SpanGraphemeEnumerator.cs @@ -83,6 +83,37 @@ internal SpanGraphemeEnumerator(ReadOnlySpan source, bool countOnly) /// public bool MoveNext() { + if (this.source.IsEmpty) + { + return false; + } + + // A printable ASCII value followed by another ASCII value, or by the end + // of the input, always forms a single-scalar cluster: no ASCII value + // extends, joins, or prepends, and any scalar that could attach to the + // cluster is necessarily non-ASCII. Such a cluster's metadata is fixed + // apart from the emoji flag carried by the keycap bases, so the boundary + // machine below never needs to run for it. + char first = this.source[0]; + if (first is >= ' ' and <= '~' && (this.source.Length == 1 || this.source[1] < '\u0080')) + { + if (!this.countOnly) + { + CodePoint codePoint = new(first); + GraphemeClusterFlags flags = GraphemeClusterFlags.IsSingleCodePoint; + if ((CodePoint.GetEmojiProperties(codePoint) & EmojiProperties.Emoji) != 0) + { + flags |= GraphemeClusterFlags.ContainsEmoji; + } + + this.Current = new GraphemeCluster(this.source[..1], this.sourceOffset, 1, 1, flags, codePoint); + } + + this.source = this.source[1..]; + this.sourceOffset++; + return true; + } + // GB9c is a stateful rule: whether the next consonant can join depends on // the InCB classes already consumed into the current cluster. Keep that state // outside Processor so Processor remains a simple UTF-16/code-point reader. From 103178268c7194cc95e3f212d34ac9320325e665 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 25 Jul 2026 13:55:44 +1000 Subject: [PATCH 55/94] Seed the global feature bit at glyph record initialization Global features apply to every glyph by definition and share one constant mask bit, so every record is born carrying it and global registration records only the classification and the stage: the per-segment range walks that previously OR-ed the same bit across the whole run for every global feature are deleted. Varying features keep their range registrations. This also fixes a latent multi-segment defect: the global registration walk re-enabled the demoted Hangul contextual-alternates bit on second and later segments, because per-glyph registration only ever accumulates. Registration no longer touches glyph state, so the default-off demotion now holds across segments. Benchmark (12 iterations, clean control pairs at idle, RatioSD <= 0.02): | Scenario | Before | After | Allocated | |------------|------------------|------------------|-----------| | Latin | 5.03 us / 2.24x | 4.93 us / 2.17x | 6,216 B | | Arabic | 7.53 us / 1.98x | 7.52 us / 1.98x | 4,368 B | | Devanagari | 11.72 us / 1.33x | 11.42 us / 1.28x | 3,832 B | First sub-5 us Latin. Allocations byte-identical. Suite 5,585/0 including the differential oracle. --- src/SixLabors.Fonts/GlyphShapingData.cs | 31 +++++++++++++------ .../Shapers/DefaultShaper.cs | 11 ++++--- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/SixLabors.Fonts/GlyphShapingData.cs b/src/SixLabors.Fonts/GlyphShapingData.cs index b8ea745c4..17d15f86e 100644 --- a/src/SixLabors.Fonts/GlyphShapingData.cs +++ b/src/SixLabors.Fonts/GlyphShapingData.cs @@ -93,10 +93,18 @@ internal struct GlyphShapingData private byte direction; /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the struct. The + /// feature masks are seeded with the shared global bit: global features apply to + /// every glyph by definition, so their registration never walks glyph ranges and + /// every record is born carrying the bit their lookups gate on. /// /// The index of the text run this glyph belongs to. - public GlyphShapingData(ushort textRunIndex) => this.TextRunIndex = textRunIndex; + public GlyphShapingData(ushort textRunIndex) + { + this.TextRunIndex = textRunIndex; + this.RegisteredFeatureMask = Tables.AdvancedTypographic.ShapePlanFeatures.GlobalFeatureMask; + this.FeatureMask = Tables.AdvancedTypographic.ShapePlanFeatures.GlobalFeatureMask; + } /// /// Initializes a new instance of the struct. @@ -125,6 +133,11 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) this.RegisteredFeatureMask = data.RegisteredFeatureMask; this.FeatureMask = data.FeatureMask; } + else + { + this.RegisteredFeatureMask = Tables.AdvancedTypographic.ShapePlanFeatures.GlobalFeatureMask; + this.FeatureMask = Tables.AdvancedTypographic.ShapePlanFeatures.GlobalFeatureMask; + } this.AppliedFeatureMask = data.AppliedFeatureMask; @@ -296,16 +309,16 @@ private string DebuggerDisplay .Invariant($" {this.GlyphId} : {this.CodePoint.ToDebuggerDisplay()} : {CodePoint.GetScriptClass(this.CodePoint)} : {this.Direction} : run {this.TextRunIndex} : {this.LigatureId} : {this.LigatureComponent} : {this.IsDecomposed}"); /// - /// Clears the registered and enabled feature masks while preserving the applied - /// mask, matching the semantics of copying with cleared features. Positioning - /// reuses the substituted glyph data and re-plans its own features, but the applied - /// record of what substitution did must survive for consumers such as vertical - /// alternate detection. + /// Resets the registered and enabled feature masks to the seeded global bit + /// while preserving the applied mask, matching the semantics of copying with + /// cleared features. Positioning reuses the substituted glyph data and re-plans + /// its own varying features, but the applied record of what substitution did + /// must survive for consumers such as vertical alternate detection. /// public void ClearFeatures() { - this.RegisteredFeatureMask = 0; - this.FeatureMask = 0; + this.RegisteredFeatureMask = Tables.AdvancedTypographic.ShapePlanFeatures.GlobalFeatureMask; + this.FeatureMask = Tables.AdvancedTypographic.ShapePlanFeatures.GlobalFeatureMask; } public string ToDebuggerDisplay() => this.DebuggerDisplay; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs index 386d3c5d0..db034bc16 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs @@ -322,10 +322,11 @@ protected void EnableFeature(ShapingBuffer buffer, int index, int count, Tag fea => this.EnableFeature(buffer, index, count, feature, null, null); /// - /// Registers a global feature over the given range and attaches the supplied - /// stage actions. The feature applies to every glyph of the plan's segments - /// and therefore shares the plan's single global mask bit instead of consuming - /// a distinct bit. + /// Registers a global feature and attaches the supplied stage actions. The + /// feature applies to every glyph of the plan's segments and therefore shares + /// the plan's single global mask bit, which every glyph record is born + /// carrying: registration records the classification and the stage without + /// touching any glyph. /// /// The glyph shaping buffer. /// The zero-based index of the first element. @@ -349,7 +350,7 @@ protected void EnableFeature( } } - buffer.AddShapingFeatureRange(index, count, new TagEntry(feature, true), this.Features.GetOrAddGlobalMask(feature)); + _ = this.Features.GetOrAddGlobalMask(feature); this.AddStage(feature, preAction, postAction); } From 335eabe97079948e1e04501997e9d425741abc82 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 25 Jul 2026 16:27:30 +1000 Subject: [PATCH 56/94] Classify ASCII bidi properties from compile-time constant tables Bidirectional analysis answers ASCII values from three 128-entry constant-data spans (character type, paired bracket type, and paired bracket value) that the runtime reads directly from metadata, skipping the property trie lookups and the embedding and isolate bookkeeping no ASCII value can trigger. The bytes are generated from the general classification path and a guard test asserts the two never diverge, so a Unicode data update cannot silently invalidate the constants. Benchmark (30 iterations, clean control pairs at idle, both windows; before is the pre-change baseline at the same protocol where available): | Scenario | Before | After | Allocated | |------------|------------------|------------------|-----------| | Latin | 4.93 us / 2.17x | 4.78 us / 2.13x | 6,216 B | | Arabic | 7.52 us / 1.98x | 7.25 us / 1.90x | 4,368 B | | Devanagari | 11.42 us / 1.28x | 11.07 us / 1.26x | 3,832 B | Every scenario improves 3.1 to 3.6 percent (the Arabic and Devanagari texts benefit through their ASCII spaces); kept under the owner- directed constant-data form after the per-scenario -5 percent bar was explicitly waived for this all-scenario win. Suite 5,586/0 including the new divergence guard. --- src/SixLabors.Fonts/Unicode/BidiData.cs | 76 +++++++++++++++++++ .../Unicode/BidiDataTests.cs | 41 ++++++++++ 2 files changed, 117 insertions(+) create mode 100644 tests/SixLabors.Fonts.Tests/Unicode/BidiDataTests.cs diff --git a/src/SixLabors.Fonts/Unicode/BidiData.cs b/src/SixLabors.Fonts/Unicode/BidiData.cs index 96dca9b6b..97543698a 100644 --- a/src/SixLabors.Fonts/Unicode/BidiData.cs +++ b/src/SixLabors.Fonts/Unicode/BidiData.cs @@ -17,6 +17,60 @@ internal class BidiData private ArrayBuilder tempLevelBuffer; private readonly List paragraphPositions = new(); + /// + /// Gets the bidi character type for every ASCII value as compile-time constant + /// data the runtime reads directly from metadata. The bytes are generated from + /// the general classification path and a test asserts the two never diverge. + /// ASCII contains no embedding, override, or isolate initiators, so the fast + /// path never needs the flag bookkeeping the general path performs. + /// + public static ReadOnlySpan AsciiCharacterTypes => + [ + 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x0B, 0x0A, 0x0B, 0x0C, 0x0A, 0x09, 0x09, + 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x0A, 0x0A, 0x0A, 0x0B, + 0x0C, 0x0D, 0x0D, 0x05, 0x05, 0x05, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x04, 0x07, 0x04, 0x07, 0x07, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x07, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, + 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, + 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x0D, 0x0D, 0x0D, 0x09 + ]; + + /// + /// Gets the paired bracket type for every ASCII value as compile-time constant + /// data; the ASCII brackets are the parentheses, square brackets, and curly + /// braces. + /// + public static ReadOnlySpan AsciiPairedBracketTypes => + [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00 + ]; + + /// + /// Gets the paired bracket value for every ASCII value as compile-time constant + /// data: the canonical pairing key the bracket-pair stage matches opening and + /// closing brackets by, zero for non-brackets. The ASCII brackets canonicalize + /// to themselves, so each pair's key is its closing bracket's value. + /// + public static ReadOnlySpan AsciiPairedBracketValues => + [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x29, 0x29, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5D, 0x00, 0x5D, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7D, 0x00, 0x7D, 0x00, 0x00 + ]; + /// /// Gets a reusable per-thread instance. The internal builders grow to the /// workload's high-water mark and resets them, so reuse makes @@ -124,6 +178,28 @@ public void Init(ReadOnlySpan text, sbyte paragraphEmbeddingLevel) while (codePointEnumerator.MoveNext()) { CodePoint codePoint = codePointEnumerator.Current; + + // ASCII values answer from the precomputed tables: one classification, + // one bracket type, and one pairing value per code point instead of the + // property lookups below. ASCII contains no embedding or isolate + // initiators, so the flag bookkeeping is skipped as well. + int asciiValue = codePoint.Value; + if ((uint)asciiValue < 128) + { + this.types[i] = (BidiCharacterType)AsciiCharacterTypes[asciiValue]; + + BidiPairedBracketType asciiPbt = (BidiPairedBracketType)AsciiPairedBracketTypes[asciiValue]; + this.pairedBracketTypes[i] = asciiPbt; + if (asciiPbt != BidiPairedBracketType.None) + { + this.pairedBracketValues[i] = AsciiPairedBracketValues[asciiValue]; + this.HasBrackets = true; + } + + i++; + continue; + } + BidiClass bidi = CodePoint.GetBidiClass(codePoint); // Look up BidiCharacterType diff --git a/tests/SixLabors.Fonts.Tests/Unicode/BidiDataTests.cs b/tests/SixLabors.Fonts.Tests/Unicode/BidiDataTests.cs new file mode 100644 index 000000000..e61bfe7c3 --- /dev/null +++ b/tests/SixLabors.Fonts.Tests/Unicode/BidiDataTests.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts.Tests.Unicode; + +public class BidiDataTests +{ + /// + /// The ASCII bidi tables are compile-time constant data generated from the + /// general classification path. This guard asserts the constants match that + /// path for every ASCII value, so a Unicode data update can never silently + /// diverge the two. + /// + [Fact] + public void AsciiBidiTablesMatchGeneralClassification() + { + for (int c = 0; c < 128; c++) + { + CodePoint codePoint = new(c); + BidiClass bidi = CodePoint.GetBidiClass(codePoint); + + Assert.Equal((byte)bidi.CharacterType, BidiData.AsciiCharacterTypes[c]); + Assert.Equal((byte)bidi.PairedBracketType, BidiData.AsciiPairedBracketTypes[c]); + + int expectedPairedValue = 0; + if (bidi.PairedBracketType == BidiPairedBracketType.Open) + { + Assert.True(bidi.TryGetPairedBracket(out CodePoint paired)); + expectedPairedValue = CodePoint.GetCanonicalType(paired).Value; + } + else if (bidi.PairedBracketType == BidiPairedBracketType.Close) + { + expectedPairedValue = CodePoint.GetCanonicalType(codePoint).Value; + } + + Assert.Equal(expectedPairedValue, BidiData.AsciiPairedBracketValues[c]); + } + } +} From 35419ba4b9150c5d3eceb7c18fee558f8b3ad4b2 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 25 Jul 2026 16:46:53 +1000 Subject: [PATCH 57/94] Generate the ASCII bidi tables from the Unicode source data The three constant-data spans move out of the hand-maintained source into BidiData.AsciiTables.Generated.cs, emitted by the Unicode trie generator: character types derive from the UnicodeData bidi category field, bracket types and pairing keys from BidiBrackets, so no byte of the tables is hand-written. Regenerate with dotnet run --project src/UnicodeTrieGenerator. The guard test derives its expectations through the runtime classification path and passed against the generated data, proving the emitted bytes identical to the previous constants: the compiled binary data is unchanged, so the standing validated numbers carry (Latin 4.78 us / 2.13x / 6,216 B, Arabic 7.25 us / 1.90x / 4,368 B, Devanagari 11.07 us / 1.26x / 3,832 B; 30-iteration clean window). Suite 5,588/0. Regenerating also surfaced pre-existing drift in IndicShapingTrie.Generated.cs against the current rules data; that file was restored untouched and the drift is flagged for review. --- .../Unicode/BidiData.AsciiTables.Generated.cs | 66 +++++++ src/SixLabors.Fonts/Unicode/BidiData.cs | 56 +----- .../Generator.AsciiBidiTables.cs | 172 ++++++++++++++++++ src/UnicodeTrieGenerator/Program.cs | 2 + 4 files changed, 241 insertions(+), 55 deletions(-) create mode 100644 src/SixLabors.Fonts/Unicode/BidiData.AsciiTables.Generated.cs create mode 100644 src/UnicodeTrieGenerator/Generator.AsciiBidiTables.cs diff --git a/src/SixLabors.Fonts/Unicode/BidiData.AsciiTables.Generated.cs b/src/SixLabors.Fonts/Unicode/BidiData.AsciiTables.Generated.cs new file mode 100644 index 000000000..3243d8b60 --- /dev/null +++ b/src/SixLabors.Fonts/Unicode/BidiData.AsciiTables.Generated.cs @@ -0,0 +1,66 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// +namespace SixLabors.Fonts.Unicode; + +/// +/// Contains the generated ASCII bidi classification tables. +/// +internal partial class BidiData +{ + /// + /// Gets the bidi character type for every ASCII value as compile-time constant + /// data the runtime reads directly from metadata. ASCII contains no embedding, + /// override, or isolate initiators, so the fast path never needs the flag + /// bookkeeping the general path performs. Generated from the UnicodeData bidi + /// category field; a test asserts the general classification path agrees. + /// + public static ReadOnlySpan AsciiCharacterTypes => + [ + 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x0B, 0x0A, 0x0B, 0x0C, 0x0A, 0x09, 0x09, + 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x0A, 0x0A, 0x0A, 0x0B, + 0x0C, 0x0D, 0x0D, 0x05, 0x05, 0x05, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x04, 0x07, 0x04, 0x07, 0x07, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x07, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, + 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, + 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x0D, 0x0D, 0x0D, 0x09 + ]; + + /// + /// Gets the paired bracket type for every ASCII value as compile-time constant + /// data; the ASCII brackets are the parentheses, square brackets, and curly + /// braces. Generated from BidiBrackets; a test asserts the general + /// classification path agrees. + /// + public static ReadOnlySpan AsciiPairedBracketTypes => + [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00 + ]; + + /// + /// Gets the paired bracket value for every ASCII value as compile-time constant + /// data: the canonical pairing key the bracket-pair stage matches opening and + /// closing brackets by, zero for non-brackets. Generated from BidiBrackets; a + /// test asserts the general classification path agrees. + /// + public static ReadOnlySpan AsciiPairedBracketValues => + [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x29, 0x29, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5D, 0x00, 0x5D, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7D, 0x00, 0x7D, 0x00, 0x00 + ]; +} diff --git a/src/SixLabors.Fonts/Unicode/BidiData.cs b/src/SixLabors.Fonts/Unicode/BidiData.cs index 97543698a..074546a74 100644 --- a/src/SixLabors.Fonts/Unicode/BidiData.cs +++ b/src/SixLabors.Fonts/Unicode/BidiData.cs @@ -7,7 +7,7 @@ namespace SixLabors.Fonts.Unicode; /// Represents a unicode string and all associated attributes /// for each character required for the Bidi algorithm /// -internal class BidiData +internal partial class BidiData { private ArrayBuilder types; private ArrayBuilder pairedBracketTypes; @@ -17,60 +17,6 @@ internal class BidiData private ArrayBuilder tempLevelBuffer; private readonly List paragraphPositions = new(); - /// - /// Gets the bidi character type for every ASCII value as compile-time constant - /// data the runtime reads directly from metadata. The bytes are generated from - /// the general classification path and a test asserts the two never diverge. - /// ASCII contains no embedding, override, or isolate initiators, so the fast - /// path never needs the flag bookkeeping the general path performs. - /// - public static ReadOnlySpan AsciiCharacterTypes => - [ - 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x0B, 0x0A, 0x0B, 0x0C, 0x0A, 0x09, 0x09, - 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x0A, 0x0A, 0x0A, 0x0B, - 0x0C, 0x0D, 0x0D, 0x05, 0x05, 0x05, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x04, 0x07, 0x04, 0x07, 0x07, - 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x07, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, - 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, - 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x0D, 0x0D, 0x0D, 0x09 - ]; - - /// - /// Gets the paired bracket type for every ASCII value as compile-time constant - /// data; the ASCII brackets are the parentheses, square brackets, and curly - /// braces. - /// - public static ReadOnlySpan AsciiPairedBracketTypes => - [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00 - ]; - - /// - /// Gets the paired bracket value for every ASCII value as compile-time constant - /// data: the canonical pairing key the bracket-pair stage matches opening and - /// closing brackets by, zero for non-brackets. The ASCII brackets canonicalize - /// to themselves, so each pair's key is its closing bracket's value. - /// - public static ReadOnlySpan AsciiPairedBracketValues => - [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x29, 0x29, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5D, 0x00, 0x5D, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7D, 0x00, 0x7D, 0x00, 0x00 - ]; - /// /// Gets a reusable per-thread instance. The internal builders grow to the /// workload's high-water mark and resets them, so reuse makes diff --git a/src/UnicodeTrieGenerator/Generator.AsciiBidiTables.cs b/src/UnicodeTrieGenerator/Generator.AsciiBidiTables.cs new file mode 100644 index 000000000..7d101a1f1 --- /dev/null +++ b/src/UnicodeTrieGenerator/Generator.AsciiBidiTables.cs @@ -0,0 +1,172 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Globalization; +using System.Text; +using System.Text.RegularExpressions; +using SixLabors.Fonts.Tests.Unicode; +using SixLabors.Fonts.Unicode; + +namespace UnicodeTrieGenerator; + +/// +/// Contains the generator for the ASCII bidi classification tables. +/// +public static partial class Generator +{ + private const string UnicodeOutputRelativePath = @"src\SixLabors.Fonts\Unicode"; + + /// + /// The number of values the ASCII tables cover. + /// + private const int AsciiTableLength = 128; + + /// + /// Generates the compile-time constant ASCII bidi tables consumed by the + /// bidirectional analysis fast path. Every byte derives from the Unicode + /// source data: the character types from the UnicodeData bidi category field, + /// the bracket types and pairings from BidiBrackets, and the pairing keys from + /// the canonical mapping the same rows define. A unit test asserts the + /// generated data matches the general classification path at runtime. + /// + public static void GenerateAsciiBidiTables() + { + byte[] characterTypes = new byte[AsciiTableLength]; + byte[] pairedBracketTypes = new byte[AsciiTableLength]; + byte[] pairedBracketValues = new byte[AsciiTableLength]; + + using (StreamReader sr = GetStreamReader("UnicodeData.txt")) + { + string? line; + while ((line = sr.ReadLine()) != null) + { + string[] parts = line.Split(';'); + if (parts.Length > 4) + { + int codePoint = ParseHexInt(parts[0]); + if (codePoint < AsciiTableLength) + { + _ = UnicodeTypeMaps.BidiCharacterTypeMap.TryGetValue(parts[4], out BidiCharacterType cls); + characterTypes[codePoint] = (byte)cls; + } + } + } + } + + Regex regex = BidiBracketRowRegex(); + using (StreamReader sr = GetStreamReader("BidiBrackets.txt")) + { + string? line; + while ((line = sr.ReadLine()) != null) + { + Match match = regex.Match(line); + if (match.Success) + { + int point = ParseHexInt(match.Groups[1].Value); + int otherPoint = ParseHexInt(match.Groups[2].Value); + if (point < AsciiTableLength) + { + _ = BidiPairedBracketTypeMap.TryGetValue(match.Groups[3].Value, out BidiPairedBracketType kind); + pairedBracketTypes[point] = (byte)kind; + + // The pairing key is the canonical form of the closing + // bracket: the paired point for an opening bracket and the + // point itself for a closing one. No ASCII value carries a + // canonical decomposition in UnicodeData, so the canonical + // form is the value itself. + int pairingKey = kind == BidiPairedBracketType.Open ? otherPoint : point; + pairedBracketValues[point] = checked((byte)pairingKey); + } + } + } + } + + StringBuilder sb = new(); + sb.AppendLine("// Copyright (c) Six Labors."); + sb.AppendLine("// Licensed under the Six Labors Split License."); + sb.AppendLine(); + sb.AppendLine("// "); + sb.AppendLine("namespace SixLabors.Fonts.Unicode;"); + sb.AppendLine(); + sb.AppendLine("/// "); + sb.AppendLine("/// Contains the generated ASCII bidi classification tables."); + sb.AppendLine("/// "); + sb.AppendLine("internal partial class BidiData"); + sb.AppendLine("{"); + + const string characterTypesDoc = """ + /// + /// Gets the bidi character type for every ASCII value as compile-time constant + /// data the runtime reads directly from metadata. ASCII contains no embedding, + /// override, or isolate initiators, so the fast path never needs the flag + /// bookkeeping the general path performs. Generated from the UnicodeData bidi + /// category field; a test asserts the general classification path agrees. + /// + """; + + const string pairedBracketTypesDoc = """ + /// + /// Gets the paired bracket type for every ASCII value as compile-time constant + /// data; the ASCII brackets are the parentheses, square brackets, and curly + /// braces. Generated from BidiBrackets; a test asserts the general + /// classification path agrees. + /// + """; + + const string pairedBracketValuesDoc = """ + /// + /// Gets the paired bracket value for every ASCII value as compile-time constant + /// data: the canonical pairing key the bracket-pair stage matches opening and + /// closing brackets by, zero for non-brackets. Generated from BidiBrackets; a + /// test asserts the general classification path agrees. + /// + """; + + AppendTable(sb, "AsciiCharacterTypes", characterTypes, characterTypesDoc); + sb.AppendLine(); + AppendTable(sb, "AsciiPairedBracketTypes", pairedBracketTypes, pairedBracketTypesDoc); + sb.AppendLine(); + AppendTable(sb, "AsciiPairedBracketValues", pairedBracketValues, pairedBracketValuesDoc); + + sb.AppendLine("}"); + + string path = GetFullPath(Path.Combine(UnicodeOutputRelativePath, "BidiData.AsciiTables.Generated.cs")); + File.WriteAllText(path, sb.ToString()); + } + + /// + /// Appends one constant-data span property: the supplied documentation block + /// followed by the byte values in rows of sixteen. + /// + /// The builder receiving the generated source. + /// The property name. + /// The table values. + /// The XML documentation block for the property. + private static void AppendTable(StringBuilder sb, string name, byte[] values, string documentation) + { + foreach (string docLine in documentation.Split('\n')) + { + sb.Append(" ").AppendLine(docLine.TrimEnd('\r')); + } + + sb.AppendLine(CultureInfo.InvariantCulture, $" public static ReadOnlySpan {name} =>"); + sb.AppendLine(" ["); + for (int row = 0; row < values.Length; row += 16) + { + sb.Append(" "); + for (int col = 0; col < 16; col++) + { + int index = row + col; + sb.Append(CultureInfo.InvariantCulture, $"0x{values[index]:X2}"); + if (index != values.Length - 1) + { + sb.Append(col == 15 ? "," : ", "); + } + } + + sb.AppendLine(); + } + + sb.AppendLine(" ];"); + } +} diff --git a/src/UnicodeTrieGenerator/Program.cs b/src/UnicodeTrieGenerator/Program.cs index 97ea98e3d..de94f1b2c 100644 --- a/src/UnicodeTrieGenerator/Program.cs +++ b/src/UnicodeTrieGenerator/Program.cs @@ -7,5 +7,7 @@ Generator.GenerateUnicodeTries(); Console.WriteLine("Generating OpenType Language Tag Map"); Generator.GenerateOpenTypeLanguageTagMap(); +Console.WriteLine("Generating ASCII Bidi Tables"); +Generator.GenerateAsciiBidiTables(); Console.WriteLine("Done"); Console.ReadLine(); From 05364699fedba550ff197e5786f4f2ee3659ffbd Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 25 Jul 2026 17:26:19 +1000 Subject: [PATCH 58/94] Express the ASCII fast-path gates through the shared helpers The grapheme enumerator follower test and the bidi table gate now go through UnicodeUtility.IsAsciiCodePoint and CodePoint.IsAscii instead of open-coded range literals, so every fast path expresses the same predicate the same way. The grapheme enumerator keeps its printable range for the leading character and now documents why it is narrower than the ASCII check: ASCII controls carry real boundary rules and must reach the machine. The predicates are value-identical to the ranges they replace, so the compiled behavior is unchanged and the standing validated numbers carry (Latin 4.78 us / 2.13x, Arabic 7.25 us / 1.90x, Devanagari 11.07 us / 1.26x). Suite 5,588/0. --- src/SixLabors.Fonts/Unicode/BidiData.cs | 4 ++-- src/SixLabors.Fonts/Unicode/SpanGraphemeEnumerator.cs | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/SixLabors.Fonts/Unicode/BidiData.cs b/src/SixLabors.Fonts/Unicode/BidiData.cs index 074546a74..ea1eebf11 100644 --- a/src/SixLabors.Fonts/Unicode/BidiData.cs +++ b/src/SixLabors.Fonts/Unicode/BidiData.cs @@ -129,9 +129,9 @@ public void Init(ReadOnlySpan text, sbyte paragraphEmbeddingLevel) // one bracket type, and one pairing value per code point instead of the // property lookups below. ASCII contains no embedding or isolate // initiators, so the flag bookkeeping is skipped as well. - int asciiValue = codePoint.Value; - if ((uint)asciiValue < 128) + if (codePoint.IsAscii) { + int asciiValue = codePoint.Value; this.types[i] = (BidiCharacterType)AsciiCharacterTypes[asciiValue]; BidiPairedBracketType asciiPbt = (BidiPairedBracketType)AsciiPairedBracketTypes[asciiValue]; diff --git a/src/SixLabors.Fonts/Unicode/SpanGraphemeEnumerator.cs b/src/SixLabors.Fonts/Unicode/SpanGraphemeEnumerator.cs index 59f0597ec..e304378bd 100644 --- a/src/SixLabors.Fonts/Unicode/SpanGraphemeEnumerator.cs +++ b/src/SixLabors.Fonts/Unicode/SpanGraphemeEnumerator.cs @@ -93,9 +93,13 @@ public bool MoveNext() // extends, joins, or prepends, and any scalar that could attach to the // cluster is necessarily non-ASCII. Such a cluster's metadata is fixed // apart from the emoji flag carried by the keycap bases, so the boundary - // machine below never needs to run for it. + // machine below never needs to run for it. The leading test is the + // printable range, deliberately narrower than the ASCII check used for + // the follower: ASCII controls carry real boundary rules (a carriage + // return joins a following line feed into one cluster) and must reach + // the machine. char first = this.source[0]; - if (first is >= ' ' and <= '~' && (this.source.Length == 1 || this.source[1] < '\u0080')) + if (first is >= ' ' and <= '~' && (this.source.Length == 1 || UnicodeUtility.IsAsciiCodePoint(this.source[1]))) { if (!this.countOnly) { From 723e0f3173f8beac9300070955ee7acda94d6dd0 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 25 Jul 2026 20:01:43 +1000 Subject: [PATCH 59/94] Stream substitution through an output pass Substitution no longer edits the glyph array in place per mutation: each lookup applies as one pass whose cursor consumes the input side and appends to an output side, so a length change costs one streaming pass instead of shifting the tail per edit. The sides alias until output would overtake unread input - an equal-length pass writes every record onto itself, a shrinking pass compacts forward within the primary storage, and a second array engages only for growth. A pass that changes nothing closes with cursor resets alone, and the pass begins at its segment, adopting everything before it untouched. Replacements consume through the cursor for the substitution types whose output replaces exactly what they match; contextual and reverse lookups keep in-place application, and nested lookups are always in place because the outer contextual match owns the cursor. Ignored records stream through the pass untouched instead of being stepped over. Input-side indices are stable for the whole pass, which the matchers rely on; the backtrack side split with the nested position fixups follows as its own change. Benchmark (30 iterations, three same-evening clean-control windows; the representative clean row per scenario is shown - the Devanagari row of the final window carried a load blip and its two clean prior windows read 1.27x and 1.28x): | Scenario | Before | After | Allocated | |------------|------------------|------------------|-----------| | Latin | 4.78 us / 2.13x | 4.83 us / 2.15x | 6,216 B | | Arabic | 7.25 us / 1.90x | 7.33 us / 1.93x | 4,368 B | | Devanagari | 11.07 us / 1.26x | 11.15 us / 1.27x | 3,832 B | Structure port judged on non-regression: every scenario within the allowance on clean rows, allocations byte-identical, suite 5,588/0 including the differential oracle on the first live run. --- src/SixLabors.Fonts/ShapingBuffer.cs | 439 +++++++++++++++++- .../AdvancedTypographicUtils.cs | 5 + .../GSub/LookupListTable.cs | 13 + .../GSub/LookupType1SubTable.cs | 6 + .../GSub/LookupType2SubTable.cs | 3 + .../GSub/LookupType3SubTable.cs | 3 + .../GSub/LookupType4SubTable.cs | 3 + .../Tables/AdvancedTypographic/GSubTable.cs | 57 ++- .../SkippingGlyphIterator.cs | 10 + 9 files changed, 522 insertions(+), 17 deletions(-) diff --git a/src/SixLabors.Fonts/ShapingBuffer.cs b/src/SixLabors.Fonts/ShapingBuffer.cs index 4bc2968be..734649ac8 100644 --- a/src/SixLabors.Fonts/ShapingBuffer.cs +++ b/src/SixLabors.Fonts/ShapingBuffer.cs @@ -170,6 +170,45 @@ internal sealed class ShapingBuffer /// private readonly List<(ScriptClass Script, Tag ScriptTag, FontMetrics FontMetrics, ShapePlan Plan)> planCache = new(4); + /// + /// The output-side record storage for substitution passes. Allocated on the + /// first pass whose output grows past its read cursor and retained at the + /// workload's high-water mark afterwards; passes whose output never outgrows + /// the input keep writing into the primary storage and never touch this. + /// + private GlyphShapingData[] outData = []; + + /// + /// The number of records produced on the output side of the active pass. + /// + private int outCount; + + /// + /// The read cursor of the active pass: the next input record to consume. + /// + private int readIndex; + + /// + /// Whether a substitution pass is active. Outside a pass the buffer mutates + /// in place exactly as before, which the pause callbacks rely on. + /// + private bool passActive; + + /// + /// Whether the active pass's output has diverged into . + /// While false the output region aliases the head of the primary storage: + /// equal-length passes write every record onto itself and copy nothing, and + /// shrinking passes move records forward within the primary storage. + /// Divergence occurs only when output would overtake the read cursor. + /// + private bool passDiverged; + + /// + /// The depth of nested lookup application within contextual matches. Nested + /// replacements mutate the input side in place regardless of their type. + /// + private int nestedApplicationDepth; + /// /// Initializes a new instance of the class. /// @@ -194,6 +233,39 @@ public ShapingBuffer(TextOptions textOptions, ShapingBufferRole role) /// public int Count => this.count; + /// + /// Gets the number of records already produced by the active pass; matching + /// walks backtrack context against these records, not the unconsumed input. + /// + public int PassOutputCount => this.outCount; + + /// + /// Gets a value indicating whether a substitution pass is active. + /// + public bool IsPassActive => this.passActive; + + /// + /// Gets or sets a value indicating whether the applying lookup consumes + /// records through the pass cursor. The substitution driver sets this for + /// top-level lookups whose replacements consume exactly the records they + /// match; contextual lookups leave it clear so their nested replacements + /// mutate the input side in place and the driver alone advances the cursor. + /// + public bool DirectConsume { get; set; } + + /// + /// Gets the read cursor of the active pass: the input-side position of the + /// next record to consume. + /// + public int ReadIndex => this.readIndex; + + /// + /// Gets a value indicating whether lookup application is currently nested + /// inside a contextual match. Nested replacements never consume through the + /// pass cursor, whatever their type: the outer contextual owns the cursor. + /// + public bool IsNestedApplication => this.nestedApplicationDepth > 0; + /// /// Gets the text options used by this buffer. /// @@ -661,6 +733,17 @@ public bool TryGetGlyphShapingDataAtOffset(int offset, [NotNullWhen(true)] out I public void Replace(int index, ushort glyphId, Tag feature) { this.glyphDigest.Add(glyphId); + if (this.passActive && this.DirectConsume && index == this.readIndex) + { + ref GlyphShapingData produced = ref this.ProduceFromCursor(); + produced.GlyphId = glyphId; + produced.LigatureId = 0; + produced.LigatureComponent = -1; + produced.IsSubstituted = true; + produced.AppliedFeatureMask |= ShapePlanFeatures.GetVerticalMask(feature); + return; + } + ref GlyphShapingData current = ref this.data[index]; current.GlyphId = glyphId; current.LigatureId = 0; @@ -680,7 +763,8 @@ public void Replace(int index, ushort glyphId, Tag feature) /// The feature to apply to the record at the specified index. public void Replace(int index, ReadOnlySpan removalIndices, ushort glyphId, int ligatureId, Tag feature) { - // Remove the glyphs at each index. + // Gather the merged codepoint bookkeeping from the component records + // before any of them move. int codePointCount = 0; CodePoint codePoint = default; for (int i = removalIndices.Length - 1; i >= 0; i--) @@ -695,13 +779,56 @@ public void Replace(int index, ReadOnlySpan removalIndices, ushort glyphId, codePoint = currentCodePoint; } } + } + + this.glyphDigest.Add(glyphId); + if (this.passActive && this.DirectConsume && index == this.readIndex) + { + // Produce the ligature from the cursor, then stream the span it + // matched over: component records are consumed without output and + // everything between them, such as marks, is copied through. + ref GlyphShapingData produced = ref this.ProduceFromCursor(); + if (codePoint != default) + { + produced.CodePoint = codePoint; + } + + produced.CodePointCount += codePointCount; + produced.GlyphId = glyphId; + produced.LigatureId = ligatureId; + produced.IsLigated = true; + produced.LigatureComponent = -1; + produced.IsSubstituted = true; + produced.AppliedFeatureMask |= ShapePlanFeatures.GetVerticalMask(feature); + + if (removalIndices.Length > 0) + { + int removal = 0; + int last = removalIndices[removalIndices.Length - 1]; + for (int position = index + 1; position <= last; position++) + { + if (removal < removalIndices.Length && removalIndices[removal] == position) + { + this.SkipGlyph(); + removal++; + } + else + { + this.CopyGlyph(); + } + } + } - this.RemoveAt(match); + return; + } + + for (int i = removalIndices.Length - 1; i >= 0; i--) + { + this.RemoveAt(removalIndices[i]); } // Assign our new id at the index. The reference is taken after every removal // so it addresses the record's final slot. - this.glyphDigest.Add(glyphId); ref GlyphShapingData current = ref this.data[index]; if (codePoint != default) { @@ -727,7 +854,8 @@ public void Replace(int index, ReadOnlySpan removalIndices, ushort glyphId, /// The feature to apply to the record at the specified index. public void Replace(int index, int count, ushort glyphId, Tag feature) { - // Remove the glyphs at each index. + // Gather the merged codepoint bookkeeping from the following records + // before any of them move. int codePointCount = 0; CodePoint codePoint = default; for (int i = count; i > 0; i--) @@ -742,13 +870,41 @@ public void Replace(int index, int count, ushort glyphId, Tag feature) codePoint = currentCodePoint; } } + } + + this.glyphDigest.Add(glyphId); + if (this.passActive && this.DirectConsume && index == this.readIndex) + { + // Produce the replacement from the cursor, then consume the + // contiguous following records without output. + ref GlyphShapingData produced = ref this.ProduceFromCursor(); + if (codePoint != default) + { + produced.CodePoint = codePoint; + } + + produced.CodePointCount += codePointCount; + produced.GlyphId = glyphId; + produced.LigatureId = 0; + produced.LigatureComponent = -1; + produced.IsSubstituted = true; + produced.AppliedFeatureMask |= ShapePlanFeatures.GetVerticalMask(feature); + + for (int i = 0; i < count; i++) + { + this.SkipGlyph(); + } - this.RemoveAt(match); + return; + } + + for (int i = count; i > 0; i--) + { + this.RemoveAt(index + i); } // Assign our new id at the index. The reference is taken after every removal // so it addresses the record's final slot. - this.glyphDigest.Add(glyphId); ref GlyphShapingData current = ref this.data[index]; if (codePoint != default) { @@ -771,6 +927,47 @@ public void Replace(int index, int count, ushort glyphId, Tag feature) /// The feature to apply to the record at the specified index. public void Replace(int index, ReadOnlySpan glyphIds, Tag feature) { + if (this.passActive && this.DirectConsume && index == this.readIndex) + { + if (glyphIds.Length == 0) + { + // 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.SkipGlyph(); + return; + } + + ref GlyphShapingData first = ref this.ProduceFromCursor(); + first.GlyphId = glyphIds[0]; + first.LigatureComponent = 0; + first.IsSubstituted = true; + first.IsDecomposed = true; + this.glyphDigest.Add(glyphIds[0]); + + if (glyphIds.Length > 1) + { + // The produced record is captured by value as the template: the + // appends below may diverge or grow the output storage, which + // would invalidate a reference into it. + GlyphShapingData template = first; + uint mask = ShapePlanFeatures.GetVerticalMask(feature); + for (int i = 1; i < glyphIds.Length; i++) + { + GlyphShapingData appended = new(template, false) + { + GlyphId = glyphIds[i], + LigatureComponent = i, + }; + + appended.AppliedFeatureMask |= mask; + this.glyphDigest.Add(glyphIds[i]); + this.AppendOutputGlyph(in appended); + } + } + + return; + } + if (glyphIds.Length > 0) { this.glyphDigest.Add(glyphIds[0]); @@ -1287,6 +1484,236 @@ private void RemoveAt(int index) this.count--; } + /// + /// Gets a reference to a record on the output side of the active pass. + /// + /// The zero-based output-side index. + /// A reference to the record. + public ref GlyphShapingData PassOutputAt(int index) + { + if (this.passDiverged) + { + return ref this.outData[index]; + } + + return ref this.data[index]; + } + + /// + /// Begins a substitution pass with both cursors at the given position: the + /// records before it are untouched by the pass by construction, so the + /// aliased output region simply adopts them. Output aliases the primary + /// storage until a write would overtake unread input. + /// + /// The position at which the pass begins. + public void BeginOutputPass(int startIndex) + { + this.passActive = true; + this.passDiverged = false; + this.outCount = startIndex; + this.readIndex = startIndex; + } + + /// + /// Ends the active pass: unconsumed input records stream to the output side, + /// the output becomes the buffer content, and in-place semantics resume. A + /// pass that is still aliased and level has changed nothing structural, so + /// the tail is already in place and the pass closes with cursor resets alone. + /// + public void EndOutputPass() + { + if (!this.passDiverged && this.outCount == this.readIndex) + { + this.passActive = false; + this.readIndex = 0; + this.outCount = 0; + return; + } + + while (this.readIndex < this.count) + { + this.CopyGlyph(); + } + + if (this.passDiverged) + { + GlyphShapingData[] produced = this.outData; + this.outData = this.data; + this.data = produced; + } + + this.count = this.outCount; + this.passActive = false; + this.passDiverged = false; + this.readIndex = 0; + this.outCount = 0; + } + + /// + /// Copies the record at the read cursor to the output side and advances both + /// cursors. While the sides are aliased and level this is two cursor + /// increments and nothing else; the copying forms live in the cold method so + /// the overwhelmingly common no-op inlines into the pass walk. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void CopyGlyph() + { + if (!this.passDiverged && this.outCount == this.readIndex) + { + this.outCount++; + this.readIndex++; + return; + } + + this.CopyGlyphMoved(); + } + + /// + /// Copies the record at the read cursor to the output side when the sides + /// have shifted or diverged. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private void CopyGlyphMoved() + { + if (this.passDiverged) + { + this.EnsureOutCapacity(this.outCount + 1); + this.outData[this.outCount] = this.data[this.readIndex]; + } + else + { + this.data[this.outCount] = this.data[this.readIndex]; + } + + this.outCount++; + this.readIndex++; + } + + /// + /// Consumes the record at the read cursor without producing output, deleting + /// it from the pass result. The sides stay aliased: output only ever trails + /// the cursor after a deletion. + /// + public void SkipGlyph() => this.readIndex++; + + /// + /// Moves the pass position so that the given number of records sit on the + /// output side. Advancing streams records forward; rewinding returns produced + /// records to the input side ahead of the read cursor, exactly reversing the + /// stream. + /// + /// The output-side record count to move to. + public void MoveTo(int outputPosition) + { + while (this.outCount < outputPosition && this.readIndex < this.count) + { + this.CopyGlyph(); + } + + if (outputPosition < this.outCount) + { + int rewound = this.outCount - outputPosition; + this.readIndex -= rewound; + if (this.passDiverged) + { + Array.Copy(this.outData, outputPosition, this.data, this.readIndex, rewound); + } + else + { + Array.Copy(this.data, outputPosition, this.data, this.readIndex, rewound); + } + + this.outCount = outputPosition; + } + } + + /// + /// Enters a nested lookup application within a contextual match. + /// + public void PushNestedApplication() => this.nestedApplicationDepth++; + + /// + /// Leaves a nested lookup application within a contextual match. + /// + public void PopNestedApplication() => this.nestedApplicationDepth--; + + /// + /// Consumes the record at the read cursor onto the output side and returns a + /// reference to the produced record for mutation. While the sides are aliased + /// and level the record is produced onto itself. + /// + /// A reference to the produced record. + private ref GlyphShapingData ProduceFromCursor() + { + if (this.passDiverged) + { + this.EnsureOutCapacity(this.outCount + 1); + this.outData[this.outCount] = this.data[this.readIndex]; + this.readIndex++; + return ref this.outData[this.outCount++]; + } + + if (this.outCount != this.readIndex) + { + this.data[this.outCount] = this.data[this.readIndex]; + } + + this.readIndex++; + return ref this.data[this.outCount++]; + } + + /// + /// Appends a record to the output side without consuming input, diverging + /// first if the write would otherwise overtake unread input. + /// + /// The record to append. + private void AppendOutputGlyph(in GlyphShapingData record) + { + if (!this.passDiverged && this.outCount >= this.readIndex) + { + this.Diverge(); + } + + if (this.passDiverged) + { + this.EnsureOutCapacity(this.outCount + 1); + this.outData[this.outCount++] = record; + } + else + { + this.data[this.outCount++] = record; + } + } + + /// + /// Diverges the active pass into the output storage: everything produced so + /// far is copied out of the primary storage, and later writes stream there. + /// Called only when output would otherwise overtake unread input. + /// + private void Diverge() + { + if (this.outData.Length < this.data.Length) + { + this.outData = new GlyphShapingData[this.data.Length]; + } + + Array.Copy(this.data, this.outData, this.outCount); + this.passDiverged = true; + } + + /// + /// Grows the output storage to hold at least the required record count. + /// + /// The required record capacity. + private void EnsureOutCapacity(int required) + { + if (required > this.outData.Length) + { + int length = Math.Max(this.outData.Length * 2, required); + Array.Resize(ref this.outData, length); + } + } + #pragma warning disable SA1401 // Fields exposed so callers can take interior references into buffer storage. /// /// One glyph's metrics-phase state: the resolving font, its point size, and the diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs index 8b7ade56f..fdffa3485 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs @@ -126,6 +126,10 @@ public static bool ApplyLookupList( int currentCount = buffer.Count; + // Nested lookups mutate the input side in place whatever their type: the + // contextual match that recursed here owns the pass cursor. + buffer.PushNestedApplication(); + foreach (SequenceLookupRecord lookupRecord in records) { ushort sequenceIndex = lookupRecord.SequenceIndex; @@ -143,6 +147,7 @@ public static bool ApplyLookupList( } } + buffer.PopNestedApplication(); return true; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs index 6a852c5f8..5e69a007d 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs @@ -223,14 +223,19 @@ public bool TrySubstitution( ShapingProbe.SubTableProbes++; } + // Nested applications never consume through the pass cursor: the outer + // contextual match owns it, and its replacements run in place. + buffer.DirectConsume = !buffer.IsNestedApplication && subTable.ConsumesDirectly; if (subTable.TrySubstitution(fontMetrics, table, buffer, feature, lookupMask, index, count)) { // A lookup is finished for a glyph after the client locates the target // glyph or glyph context and performs a substitution, if specified. + buffer.DirectConsume = false; return true; } } + buffer.DirectConsume = false; return false; } @@ -322,6 +327,14 @@ protected LookupSubTable(LookupFlags lookupFlags, ushort markFilteringSet) /// public ushort MarkFilteringSet { get; } + /// + /// Gets a value indicating whether the subtable's replacements consume exactly + /// the records they match, so top-level application may produce them through + /// the pass cursor. Contextual and reverse subtables leave this false: their + /// replacements run in place and the driver alone advances the cursor. + /// + public virtual bool ConsumesDirectly => false; + /// /// Gets or sets the approximate membership filter for the glyphs this subtable can /// affect. Assigned once by the owning during construction. diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs index 109106d29..2f3d54878 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs @@ -65,6 +65,9 @@ private LookupType1Format1SubTable(ushort deltaGlyphId, CoverageTable coverageTa this.coverageTable = coverageTable; } + /// + public override bool ConsumesDirectly => true; + /// /// Loads the single substitution format 1 subtable from the given offset. /// @@ -157,6 +160,9 @@ private LookupType1Format2SubTable(ushort[] substituteGlyphs, CoverageTable cove this.coverageTable = coverageTable; } + /// + public override bool ConsumesDirectly => true; + /// /// Loads the single substitution format 2 subtable from the given offset. /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs index 55c4ced03..47de5c001 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs @@ -62,6 +62,9 @@ private LookupType2Format1SubTable(SequenceTable[] sequenceTables, CoverageTable this.coverageTable = coverageTable; } + /// + public override bool ConsumesDirectly => true; + /// /// Loads the multiple substitution format 1 subtable from the given offset. /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs index 05fe6d825..8ab905edb 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs @@ -62,6 +62,9 @@ private LookupType3Format1SubTable(AlternateSetTable[] alternateSetTables, Cover this.coverageTable = coverageTable; } + /// + public override bool ConsumesDirectly => true; + /// /// Loads the alternate substitution format 1 subtable from the given offset. /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs index 23f37253f..33588e0c4 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs @@ -64,6 +64,9 @@ private LookupType4Format1SubTable(LigatureSetTable[] ligatureSetTables, Coverag this.coverageTable = coverageTable; } + /// + public override bool ConsumesDirectly => true; + /// /// Loads the ligature substitution format 1 subtable from the given offset. /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs index 22b790e9c..5d31a26b2 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs @@ -322,10 +322,27 @@ private void ApplyMergedLookups( long featureStart = ShapingProbe.Timestamp(); long featureApplies = 0; - while (iterator.Index < index + count) + // One output pass per lookup: the cursor consumes the input side and + // every record streams to the output side exactly once, so a length + // change costs one streaming pass instead of one shift per mutation. + // The pass begins at the segment, adopting everything before it, and + // a pass that changes nothing closes without touching the tail. + // Input-side indices are stable for the whole pass, which the + // matchers rely on. + buffer.BeginOutputPass(index); + + int segmentEnd = index + count; + while (buffer.ReadIndex < segmentEnd && buffer.ReadIndex < buffer.Count) { if (buffer.Count >= maxCount || currentOperations++ >= maxOperationsCount) { + // The pass must always close: stream the remainder and + // reconcile the segment bookkeeping before bailing out. + int limitBefore = buffer.Count; + buffer.EndOutputPass(); + count += buffer.Count - limitBefore; + i += buffer.Count - limitBefore; + collectionCount = buffer.Count; return; } @@ -336,10 +353,15 @@ private void ApplyMergedLookups( // The digest cheaply rejects glyphs no subtable of this lookup can // affect; a maybe falls through to the exact coverage test inside. - ref GlyphShapingData glyphData = ref buffer[iterator.Index]; - if ((glyphData.FeatureMask & featureMask) == 0 || !featureLookupTable.Digest.MightContain(glyphData.GlyphId)) + // Ignored records stream through untouched rather than being + // stepped over, so the output side always receives every record. + int position = buffer.ReadIndex; + ref GlyphShapingData glyphData = ref buffer[position]; + if ((glyphData.FeatureMask & featureMask) == 0 + || !featureLookupTable.Digest.MightContain(glyphData.GlyphId) + || iterator.IsIgnored(position)) { - iterator.Next(); + buffer.CopyGlyph(); continue; } @@ -348,17 +370,30 @@ private void ApplyMergedLookups( ShapingProbe.SubstitutionAttempts++; } - collectionCount = buffer.Count; - featureLookupTable.TrySubstitution(fontMetrics, this, buffer, feature, featureMask, iterator.Index, count - (iterator.Index - index)); + int beforeCount = buffer.Count; + featureLookupTable.TrySubstitution(fontMetrics, this, buffer, feature, featureMask, position, segmentEnd - position); featureApplies++; - iterator.Next(); - // Account for substitutions changing the length of the buffer. - int delta = buffer.Count - collectionCount; - count += delta; - i += delta; + // In-place mutations from contextual nesting change the input side + // directly and move the segment bound; cursor consumption surfaces + // only when the pass ends. + int inPlaceDelta = buffer.Count - beforeCount; + segmentEnd += inPlaceDelta; + count += inPlaceDelta; + i += inPlaceDelta; + + if (buffer.ReadIndex == position) + { + buffer.CopyGlyph(); + } } + int passBefore = buffer.Count; + buffer.EndOutputPass(); + count += buffer.Count - passBefore; + i += buffer.Count - passBefore; + collectionCount = buffer.Count; + ShapingProbe.ExitFeature("GSUB", feature, featureStart, featureApplies); } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs index 872ae72b9..ae64241d8 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs @@ -151,6 +151,16 @@ private void Move(int direction) } } + /// + /// Determines whether the glyph at the given index is ignored under the + /// current lookup flags without moving the iterator. The pass driver tests + /// each record with this before attempting a lookup, copying ignored records + /// through to the output side untouched. + /// + /// The index of the glyph to check. + /// if the glyph is ignored; otherwise, . + public readonly bool IsIgnored(int index) => !this.skipsNothing && this.ShouldIgnore(index); + /// /// Determines whether the glyph at the given index should be ignored based on the current lookup flags. /// From 8a3013ca76fde068f6f8ce6162184dfd4ac6de88 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 25 Jul 2026 22:55:59 +1000 Subject: [PATCH 60/94] Hide default ignorables after positioning Default ignorable codepoints (joiners, variation selectors, bidi controls, and similar format characters) now render invisibly the way HarfBuzz renders them: they stay real records through substitution and positioning so ligature suppression, joining forms, and half forms see them, then a single post-positioning pipeline pass zeroes both advances and the offset on the axis of movement and swaps each glyph to its font's space glyph. Records a lookup substituted keep their glyphs, matching _hb_glyph_info_is_default_ignorable's substituted gate; fonts offering no space glyph have the records deleted through a new DeleteGlyphsInPlace buffer primitive porting hb_buffer_t::delete_glyphs_inplace (forward compaction, codepoint coverage folds into the neighbouring record). Classification happens once as records enter the buffer, stored as a record bit plus a buffer-level flag (the unicode_props model), so ignorable-free text pays one flag test per shape. The classification list now equals HarfBuzz's is_default_ignorable exactly - U+180F removed, with a negative test row pinning the exclusion - and the predicate is restructured into the reference's plane/page switch so non-ASCII populate no longer walks a linear compare chain. The render-as-whitespace carve-outs (Hangul fillers, shorthand format controls) keep their spacing glyphs. The shaper is now the single owner of the invisibility rule: UnicodeUtility.ShouldNotBeRendered is deleted, FontGlyphMetrics.ShouldSkipGlyphRendering reduces to line terminators, and tracking exclusions reduce to tabs and line terminators - the old codepoint test suppressed a whole grapheme's tracking when a joiner ended it. ShapingBuffer's Count, PassOutputCount, and IsPassActive field/auto-property split is completed so the properties are the single storage. 40 joiner test rows updated to the HarfBuzz output they previously diverged from; all 48 joiner rows across Kannada, Devanagari, Bengali, Malayalam, and Khmer verified glyph-for-glyph against HarfBuzzSharp 14.2.1 before updating, and three permanent differential oracle rows added comparing glyph ids and advances (Latin ZWNJ/ZWJ in ligature contexts, Arabic lam+joiner+alef, Devanagari half-form joiners). Benchmark (clean window, controls 2.28/3.86/9.03 us at idle): | Scenario | Before | After | |------------|-------------------------|-------------------------| | Latin | 4.83 us / 2.15x / 6,216 B | 4.79 us / 2.11x / 6,216 B | | Arabic | 7.33 us / 1.93x / 4,368 B | 7.43 us / 1.92x / 4,368 B | | Devanagari | 11.15 us / 1.27x / 3,832 B | 11.45 us / 1.27x / 3,832 B | Allocations byte-identical. Suite 5,588/0 -> 5,591/0. --- src/SixLabors.Fonts/FontGlyphMetrics.cs | 5 +- src/SixLabors.Fonts/GlyphShapingData.cs | 35 +++ src/SixLabors.Fonts/ShapingBuffer.cs | 279 +++++++++++------- src/SixLabors.Fonts/ShapingScratch.cs | 4 +- .../Tables/AdvancedTypographic/BaseTable.cs | 6 +- .../TextLayout.LineBreaking.cs | 5 +- src/SixLabors.Fonts/TextMeasurer.cs | 18 +- src/SixLabors.Fonts/TextRun.cs | 2 +- src/SixLabors.Fonts/TextShaper.Pipeline.cs | 76 +++++ src/SixLabors.Fonts/Unicode/UnicodeUtility.cs | 229 ++++---------- .../HarfBuzzDifferentialTests.cs | 7 + .../GSub/GSubTableTests.Indic.cs | 113 +++---- .../Unicode/UnicodeUtilityTests.cs | 2 +- 13 files changed, 407 insertions(+), 374 deletions(-) diff --git a/src/SixLabors.Fonts/FontGlyphMetrics.cs b/src/SixLabors.Fonts/FontGlyphMetrics.cs index 52b0e33d8..613a4066d 100644 --- a/src/SixLabors.Fonts/FontGlyphMetrics.cs +++ b/src/SixLabors.Fonts/FontGlyphMetrics.cs @@ -884,13 +884,14 @@ private static void EmitDecoration( } /// - /// Gets a value indicating whether the specified code point should be skipped when rendering. + /// Gets a value indicating whether the specified code point should be skipped when + /// rendering: line terminators participate in layout but have no glyph to draw. /// /// The code point. /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] protected internal static bool ShouldSkipGlyphRendering(CodePoint codePoint) - => UnicodeUtility.ShouldNotBeRendered(codePoint); + => CodePoint.IsNewLine(codePoint); /// /// Returns the size to render/measure the glyph based on the given size and resolution in px units. diff --git a/src/SixLabors.Fonts/GlyphShapingData.cs b/src/SixLabors.Fonts/GlyphShapingData.cs index 17d15f86e..ea2e81fe9 100644 --- a/src/SixLabors.Fonts/GlyphShapingData.cs +++ b/src/SixLabors.Fonts/GlyphShapingData.cs @@ -45,6 +45,16 @@ internal struct GlyphShapingData /// private const ushort PlaceholderFlag = 1 << 3; + /// + /// The bit recording . + /// + private const ushort DefaultIgnorableFlag = 1 << 4; + + /// + /// The bit recording . + /// + private const ushort HiddenFlag = 1 << 5; + /// /// The bit recording that /// holds the glyph id was computed for. A default @@ -125,6 +135,8 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) this.IsSubstituted = data.IsSubstituted; this.IsDecomposed = data.IsDecomposed; this.IsPlaceholder = data.IsPlaceholder; + this.IsDefaultIgnorable = data.IsDefaultIgnorable; + this.IsHidden = data.IsHidden; this.Syllable = data.Syllable; @@ -304,6 +316,29 @@ public bool IsPlaceholder set => this.flags = value ? (ushort)(this.flags | PlaceholderFlag) : (ushort)(this.flags & ~PlaceholderFlag); } + /// + /// Gets or sets a value indicating whether the glyph's codepoint is a default + /// ignorable that renders invisibly. Classified once as the record enters the + /// buffer; the carve-outs that render as regular spacing glyphs, such as the + /// Hangul fillers, never receive the bit. + /// + public bool IsDefaultIgnorable + { + readonly get => (this.flags & DefaultIgnorableFlag) != 0; + set => this.flags = value ? (ushort)(this.flags | DefaultIgnorableFlag) : (ushort)(this.flags & ~DefaultIgnorableFlag); + } + + /// + /// Gets or sets a value indicating whether the hide stage replaced this glyph + /// with the invisible glyph at zero advance. Consumers read this recorded + /// decision instead of re-deriving it from the codepoint. + /// + public bool IsHidden + { + readonly get => (this.flags & HiddenFlag) != 0; + set => this.flags = value ? (ushort)(this.flags | HiddenFlag) : (ushort)(this.flags & ~HiddenFlag); + } + private string DebuggerDisplay => FormattableString .Invariant($" {this.GlyphId} : {this.CodePoint.ToDebuggerDisplay()} : {CodePoint.GetScriptClass(this.CodePoint)} : {this.Direction} : run {this.TextRunIndex} : {this.LigatureId} : {this.LigatureComponent} : {this.IsDecomposed}"); diff --git a/src/SixLabors.Fonts/ShapingBuffer.cs b/src/SixLabors.Fonts/ShapingBuffer.cs index 734649ac8..f75cdf6bf 100644 --- a/src/SixLabors.Fonts/ShapingBuffer.cs +++ b/src/SixLabors.Fonts/ShapingBuffer.cs @@ -3,7 +3,6 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Numerics; using System.Runtime.CompilerServices; using SixLabors.Fonts.Tables.AdvancedTypographic; using SixLabors.Fonts.Unicode; @@ -27,7 +26,7 @@ namespace SixLabors.Fonts; internal sealed class ShapingBuffer { /// - /// The flat glyph storage. Only the first records are live; + /// The flat glyph storage. Only the first records are live; /// records beyond the count are stale leftovers awaiting overwrite. /// private GlyphShapingData[] data = new GlyphShapingData[64]; @@ -47,11 +46,6 @@ internal sealed class ShapingBuffer /// private GlyphShapingPosition[] positions = new GlyphShapingPosition[64]; - /// - /// The live record count. - /// - private int count; - /// /// The approximate membership filter over every glyph id the buffer has ever /// contained. See for the growth contract. @@ -159,7 +153,7 @@ internal sealed class ShapingBuffer /// Placeholder state lives here rather than on every glyph record because only /// placeholders carry a bidi run of their own, and only the copy-out reads it. /// - private readonly List<(int CodePointIndex, BidiRun Run)> placeholderBidiRuns = new(); + private readonly List<(int CodePointIndex, BidiRun Run)> placeholderBidiRuns = []; /// /// Shape plans reused across segments and passes, keyed by script, script tag, @@ -178,22 +172,6 @@ internal sealed class ShapingBuffer /// private GlyphShapingData[] outData = []; - /// - /// The number of records produced on the output side of the active pass. - /// - private int outCount; - - /// - /// The read cursor of the active pass: the next input record to consume. - /// - private int readIndex; - - /// - /// Whether a substitution pass is active. Outside a pass the buffer mutates - /// in place exactly as before, which the pause callbacks rely on. - /// - private bool passActive; - /// /// Whether the active pass's output has diverged into . /// While false the output region aliases the head of the primary storage: @@ -231,18 +209,18 @@ public ShapingBuffer(TextOptions textOptions, ShapingBufferRole role) /// Gets the number of live glyph records. Substitution can leave this greater or /// smaller than the input codepoint count. /// - public int Count => this.count; + public int Count { get; private set; } /// /// Gets the number of records already produced by the active pass; matching /// walks backtrack context against these records, not the unconsumed input. /// - public int PassOutputCount => this.outCount; + public int PassOutputCount { get; private set; } /// /// Gets a value indicating whether a substitution pass is active. /// - public bool IsPassActive => this.passActive; + public bool IsPassActive { get; private set; } /// /// Gets or sets a value indicating whether the applying lookup consumes @@ -257,7 +235,14 @@ public ShapingBuffer(TextOptions textOptions, ShapingBufferRole role) /// Gets the read cursor of the active pass: the input-side position of the /// next record to consume. /// - public int ReadIndex => this.readIndex; + public int ReadIndex { get; private set; } + + /// + /// Gets or sets a value indicating whether any record carries a default + /// ignorable codepoint. Recorded as records enter the buffer so the + /// hide-ignorables stage can skip plain text without a scan. + /// + public bool HasDefaultIgnorables { get; set; } /// /// Gets a value indicating whether lookup application is currently nested @@ -304,7 +289,7 @@ public ShapingBuffer(TextOptions textOptions, ShapingBufferRole role) /// pass reuses these so one plan drives both tables; the list stays empty when /// records were seeded across buffers and positioning must segment for itself. /// - public List<(int Index, int Count, ScriptClass Script, ShapePlan Plan)> SegmentPlans { get; } = new(); + public List<(int Index, int Count, ScriptClass Script, ShapePlan Plan)> SegmentPlans { get; } = []; /// /// Gets an interior reference to the glyph shaping data at the specified index. @@ -375,7 +360,7 @@ public bool SeedMetricsInPlace(Font font) uint verticalMask = ShapePlanFeatures.VerticalFeatureMask; - for (int i = 0; i < this.count; i++) + for (int i = 0; i < this.Count; i++) { ref GlyphShapingData slot = ref this.data[i]; CodePoint codePoint = slot.CodePoint; @@ -416,7 +401,7 @@ public bool SeedMetricsInPlace(Font font) /// The text options for the new pass. public void Reset(TextOptions textOptions) { - this.count = 0; + this.Count = 0; this.LigatureId = 1; this.glyphDigest = default; this.placeholderBidiRuns.Clear(); @@ -439,8 +424,9 @@ public void Reset(TextOptions textOptions) /// public void Clear() { - this.count = 0; + this.Count = 0; this.LigatureId = 1; + this.HasDefaultIgnorables = false; this.placeholderBidiRuns.Clear(); this.SegmentPlans.Clear(); } @@ -520,9 +506,12 @@ public void DisableShapingFeature(int index, uint mask) public void AddGlyph(GlyphShapingData data, int offset) { this.glyphDigest.Add(data.GlyphId); + this.HasDefaultIgnorables |= data.IsDefaultIgnorable; ref GlyphShapingData slot = ref this.Append(); - slot = new(data, false); - slot.CodePointIndex = offset; + slot = new(data, false) + { + CodePointIndex = offset + }; } /// @@ -544,6 +533,18 @@ public void AddGlyph(ushort glyphId, CodePoint codePoint, TextDirection directio Direction = direction, GlyphId = glyphId, }; + + // The render-as-whitespace carve-outs are default ignorables that fonts + // implement as regular spacing glyphs, such as the Hangul fillers; those + // keep their glyphs. + uint value = (uint)codePoint.Value; + if (value >= 0x80 + && UnicodeUtility.IsDefaultIgnorableCodePoint(value) + && !UnicodeUtility.ShouldRenderWhiteSpaceOnly(codePoint)) + { + slot.IsDefaultIgnorable = true; + this.HasDefaultIgnorables = true; + } } /// @@ -654,8 +655,8 @@ public void MoveGlyph(int fromIndex, int toIndex) /// The zero-based index at which to stop reversing (exclusive). public void ReverseRange(int startIndex, int endIndex) { - int s = Math.Min(startIndex, this.count); - int e = Math.Min(endIndex, this.count); + int s = Math.Min(startIndex, this.Count); + int e = Math.Min(endIndex, this.Count); if (e < s + 2) { @@ -707,7 +708,7 @@ public void Sort(int startIndex, int endIndex, Comparison comp public bool TryGetGlyphShapingDataAtOffset(int offset, [NotNullWhen(true)] out IReadOnlyList? data) { List match = []; - for (int i = 0; i < this.count; i++) + for (int i = 0; i < this.Count; i++) { if (this.data[i].CodePointIndex == offset) { @@ -733,7 +734,7 @@ public bool TryGetGlyphShapingDataAtOffset(int offset, [NotNullWhen(true)] out I public void Replace(int index, ushort glyphId, Tag feature) { this.glyphDigest.Add(glyphId); - if (this.passActive && this.DirectConsume && index == this.readIndex) + if (this.IsPassActive && this.DirectConsume && index == this.ReadIndex) { ref GlyphShapingData produced = ref this.ProduceFromCursor(); produced.GlyphId = glyphId; @@ -782,7 +783,7 @@ public void Replace(int index, ReadOnlySpan removalIndices, ushort glyphId, } this.glyphDigest.Add(glyphId); - if (this.passActive && this.DirectConsume && index == this.readIndex) + if (this.IsPassActive && this.DirectConsume && index == this.ReadIndex) { // Produce the ligature from the cursor, then stream the span it // matched over: component records are consumed without output and @@ -804,7 +805,7 @@ public void Replace(int index, ReadOnlySpan removalIndices, ushort glyphId, if (removalIndices.Length > 0) { int removal = 0; - int last = removalIndices[removalIndices.Length - 1]; + int last = removalIndices[^1]; for (int position = index + 1; position <= last; position++) { if (removal < removalIndices.Length && removalIndices[removal] == position) @@ -873,7 +874,7 @@ public void Replace(int index, int count, ushort glyphId, Tag feature) } this.glyphDigest.Add(glyphId); - if (this.passActive && this.DirectConsume && index == this.readIndex) + if (this.IsPassActive && this.DirectConsume && index == this.ReadIndex) { // Produce the replacement from the cursor, then consume the // contiguous following records without output. @@ -927,7 +928,7 @@ public void Replace(int index, int count, ushort glyphId, Tag feature) /// The feature to apply to the record at the specified index. public void Replace(int index, ReadOnlySpan glyphIds, Tag feature) { - if (this.passActive && this.DirectConsume && index == this.readIndex) + if (this.IsPassActive && this.DirectConsume && index == this.ReadIndex) { if (glyphIds.Length == 0) { @@ -1036,9 +1037,13 @@ public bool TryAdd(Font font, ShapingBuffer workspace) LayoutMode layoutMode = this.TextOptions.LayoutMode; ColorFontSupport colorFontSupport = this.TextOptions.ColorFontSupport; + // The hide-ignorables stage runs against this buffer, so the workspace's + // knowledge of default ignorables must travel with its records. + this.HasDefaultIgnorables |= workspace.HasDefaultIgnorables; + uint verticalMask = ShapePlanFeatures.VerticalFeatureMask; - for (int i = 0; i < workspace.count; i++) + for (int i = 0; i < workspace.Count; i++) { ref GlyphShapingData source = ref workspace.data[i]; CodePoint codePoint = source.CodePoint; @@ -1055,14 +1060,14 @@ public bool TryAdd(Font font, ShapingBuffer workspace) ref GlyphShapingData placeholderSlot = ref this.Append(); placeholderSlot = source; placeholderSlot.ClearFeatures(); - this.positions[this.count - 1] = new(layoutMode.IsVertical() + this.positions[this.Count - 1] = new(layoutMode.IsVertical() ? new(0, 0, 0, placeholderMetrics.AdvanceHeight) : new(0, 0, placeholderMetrics.AdvanceWidth, 0)) { IsPositioned = true, }; - this.metrics[this.count - 1] = new(font, font.Size, placeholderMetrics); + this.metrics[this.Count - 1] = new(font, font.Size, placeholderMetrics); continue; } @@ -1086,11 +1091,11 @@ public bool TryAdd(Font font, ShapingBuffer workspace) ref GlyphShapingData slot = ref this.Append(); slot = source; slot.ClearFeatures(); - this.positions[this.count - 1] = new(isVertical + this.positions[this.Count - 1] = new(isVertical ? new(0, 0, 0, glyphMetrics.AdvanceHeight) : new(0, 0, glyphMetrics.AdvanceWidth, 0)); - this.metrics[this.count - 1] = new(font, font.Size, glyphMetrics); + this.metrics[this.Count - 1] = new(font, font.Size, glyphMetrics); } return !hasFallBacks; @@ -1114,9 +1119,13 @@ public bool TryUpdate(Font font, ShapingBuffer workspace) ColorFontSupport colorFontSupport = this.TextOptions.ColorFontSupport; bool hasFallBacks = false; + // The hide-ignorables stage runs against this buffer, so the workspace's + // knowledge of default ignorables must travel with its records. + this.HasDefaultIgnorables |= workspace.HasDefaultIgnorables; + uint verticalMask = ShapePlanFeatures.VerticalFeatureMask; - for (int i = 0; i < this.count; i++) + for (int i = 0; i < this.Count; i++) { if (this.metrics[i].Metrics.GlyphType != GlyphType.Fallback) { @@ -1414,14 +1423,14 @@ private static Tag[] ResolveLanguageTags(TextOptions textOptions) /// The appended record. private ref GlyphShapingData Append() { - if (this.count == this.data.Length) + if (this.Count == this.data.Length) { Array.Resize(ref this.data, this.data.Length * 2); Array.Resize(ref this.metrics, this.metrics.Length * 2); Array.Resize(ref this.positions, this.positions.Length * 2); } - return ref this.data[this.count++]; + return ref this.data[this.Count++]; } /// @@ -1429,10 +1438,7 @@ private ref GlyphShapingData Append() /// /// The zero-based index at which to insert. /// The record to insert. - private void InsertAt(int index, GlyphShapingData item) - { - this.InsertAt(index, item, default); - } + private void InsertAt(int index, GlyphShapingData item) => this.InsertAt(index, item, default); /// /// Inserts one record and its metrics entry at the given index, shifting later @@ -1445,24 +1451,24 @@ private void InsertAt(int index, GlyphShapingData item) /// The metrics entry to insert. private void InsertAt(int index, GlyphShapingData item, GlyphMetricsEntry metricsEntry) { - if (this.count == this.data.Length) + if (this.Count == this.data.Length) { Array.Resize(ref this.data, this.data.Length * 2); Array.Resize(ref this.metrics, this.metrics.Length * 2); Array.Resize(ref this.positions, this.positions.Length * 2); } - Array.Copy(this.data, index, this.data, index + 1, this.count - index); + Array.Copy(this.data, index, this.data, index + 1, this.Count - index); if (this.Role == ShapingBufferRole.Positioning) { - Array.Copy(this.metrics, index, this.metrics, index + 1, this.count - index); - Array.Copy(this.positions, index, this.positions, index + 1, this.count - index); + Array.Copy(this.metrics, index, this.metrics, index + 1, this.Count - index); + Array.Copy(this.positions, index, this.positions, index + 1, this.Count - index); this.positions[index] = default; } this.data[index] = item; this.metrics[index] = metricsEntry; - this.count++; + this.Count++; } /// @@ -1474,14 +1480,73 @@ private void InsertAt(int index, GlyphShapingData item, GlyphMetricsEntry metric /// The zero-based index to remove at. private void RemoveAt(int index) { - Array.Copy(this.data, index + 1, this.data, index, this.count - index - 1); + Array.Copy(this.data, index + 1, this.data, index, this.Count - index - 1); if (this.Role == ShapingBufferRole.Positioning) { - Array.Copy(this.metrics, index + 1, this.metrics, index, this.count - index - 1); - Array.Copy(this.positions, index + 1, this.positions, index, this.count - index - 1); + Array.Copy(this.metrics, index + 1, this.metrics, index, this.Count - index - 1); + Array.Copy(this.positions, index + 1, this.positions, index, this.Count - index - 1); + } + + this.Count--; + } + + /// + /// Deletes every record matching the filter in one forward compaction pass, + /// keeping the parallel streams aligned. A deleted record's codepoint coverage + /// folds into the preceding kept record, or into the next kept record when + /// nothing precedes it, so the codepoint-to-glyph projection stays total. + /// + /// The predicate selecting records to delete. + public void DeleteGlyphsInPlace(Func filter) + { + bool positioning = this.Role == ShapingBufferRole.Positioning; + int kept = 0; + int pendingCodePointIndex = -1; + int pendingCodePointCount = 0; + for (int i = 0; i < this.Count; i++) + { + if (filter(this.data[i])) + { + ref GlyphShapingData deleted = ref this.data[i]; + if (kept > 0) + { + this.data[kept - 1].CodePointCount += deleted.CodePointCount; + } + else + { + if (pendingCodePointIndex < 0) + { + pendingCodePointIndex = deleted.CodePointIndex; + } + + pendingCodePointCount += deleted.CodePointCount; + } + + continue; + } + + if (kept != i) + { + this.data[kept] = this.data[i]; + if (positioning) + { + this.metrics[kept] = this.metrics[i]; + this.positions[kept] = this.positions[i]; + } + } + + if (pendingCodePointCount > 0) + { + ref GlyphShapingData first = ref this.data[kept]; + first.CodePointIndex = pendingCodePointIndex; + first.CodePointCount += pendingCodePointCount; + pendingCodePointCount = 0; + } + + kept++; } - this.count--; + this.Count = kept; } /// @@ -1508,10 +1573,10 @@ public ref GlyphShapingData PassOutputAt(int index) /// The position at which the pass begins. public void BeginOutputPass(int startIndex) { - this.passActive = true; + this.IsPassActive = true; this.passDiverged = false; - this.outCount = startIndex; - this.readIndex = startIndex; + this.PassOutputCount = startIndex; + this.ReadIndex = startIndex; } /// @@ -1522,31 +1587,29 @@ public void BeginOutputPass(int startIndex) /// public void EndOutputPass() { - if (!this.passDiverged && this.outCount == this.readIndex) + if (!this.passDiverged && this.PassOutputCount == this.ReadIndex) { - this.passActive = false; - this.readIndex = 0; - this.outCount = 0; + this.IsPassActive = false; + this.ReadIndex = 0; + this.PassOutputCount = 0; return; } - while (this.readIndex < this.count) + while (this.ReadIndex < this.Count) { this.CopyGlyph(); } if (this.passDiverged) { - GlyphShapingData[] produced = this.outData; - this.outData = this.data; - this.data = produced; + (this.data, this.outData) = (this.outData, this.data); } - this.count = this.outCount; - this.passActive = false; + this.Count = this.PassOutputCount; + this.IsPassActive = false; this.passDiverged = false; - this.readIndex = 0; - this.outCount = 0; + this.ReadIndex = 0; + this.PassOutputCount = 0; } /// @@ -1558,10 +1621,10 @@ public void EndOutputPass() [MethodImpl(MethodImplOptions.AggressiveInlining)] public void CopyGlyph() { - if (!this.passDiverged && this.outCount == this.readIndex) + if (!this.passDiverged && this.PassOutputCount == this.ReadIndex) { - this.outCount++; - this.readIndex++; + this.PassOutputCount++; + this.ReadIndex++; return; } @@ -1577,16 +1640,16 @@ private void CopyGlyphMoved() { if (this.passDiverged) { - this.EnsureOutCapacity(this.outCount + 1); - this.outData[this.outCount] = this.data[this.readIndex]; + this.EnsureOutCapacity(this.PassOutputCount + 1); + this.outData[this.PassOutputCount] = this.data[this.ReadIndex]; } else { - this.data[this.outCount] = this.data[this.readIndex]; + this.data[this.PassOutputCount] = this.data[this.ReadIndex]; } - this.outCount++; - this.readIndex++; + this.PassOutputCount++; + this.ReadIndex++; } /// @@ -1594,7 +1657,7 @@ private void CopyGlyphMoved() /// it from the pass result. The sides stay aliased: output only ever trails /// the cursor after a deletion. /// - public void SkipGlyph() => this.readIndex++; + public void SkipGlyph() => this.ReadIndex++; /// /// Moves the pass position so that the given number of records sit on the @@ -1605,25 +1668,25 @@ private void CopyGlyphMoved() /// The output-side record count to move to. public void MoveTo(int outputPosition) { - while (this.outCount < outputPosition && this.readIndex < this.count) + while (this.PassOutputCount < outputPosition && this.ReadIndex < this.Count) { this.CopyGlyph(); } - if (outputPosition < this.outCount) + if (outputPosition < this.PassOutputCount) { - int rewound = this.outCount - outputPosition; - this.readIndex -= rewound; + int rewound = this.PassOutputCount - outputPosition; + this.ReadIndex -= rewound; if (this.passDiverged) { - Array.Copy(this.outData, outputPosition, this.data, this.readIndex, rewound); + Array.Copy(this.outData, outputPosition, this.data, this.ReadIndex, rewound); } else { - Array.Copy(this.data, outputPosition, this.data, this.readIndex, rewound); + Array.Copy(this.data, outputPosition, this.data, this.ReadIndex, rewound); } - this.outCount = outputPosition; + this.PassOutputCount = outputPosition; } } @@ -1647,19 +1710,19 @@ private ref GlyphShapingData ProduceFromCursor() { if (this.passDiverged) { - this.EnsureOutCapacity(this.outCount + 1); - this.outData[this.outCount] = this.data[this.readIndex]; - this.readIndex++; - return ref this.outData[this.outCount++]; + this.EnsureOutCapacity(this.PassOutputCount + 1); + this.outData[this.PassOutputCount] = this.data[this.ReadIndex]; + this.ReadIndex++; + return ref this.outData[this.PassOutputCount++]; } - if (this.outCount != this.readIndex) + if (this.PassOutputCount != this.ReadIndex) { - this.data[this.outCount] = this.data[this.readIndex]; + this.data[this.PassOutputCount] = this.data[this.ReadIndex]; } - this.readIndex++; - return ref this.data[this.outCount++]; + this.ReadIndex++; + return ref this.data[this.PassOutputCount++]; } /// @@ -1669,19 +1732,19 @@ private ref GlyphShapingData ProduceFromCursor() /// The record to append. private void AppendOutputGlyph(in GlyphShapingData record) { - if (!this.passDiverged && this.outCount >= this.readIndex) + if (!this.passDiverged && this.PassOutputCount >= this.ReadIndex) { this.Diverge(); } if (this.passDiverged) { - this.EnsureOutCapacity(this.outCount + 1); - this.outData[this.outCount++] = record; + this.EnsureOutCapacity(this.PassOutputCount + 1); + this.outData[this.PassOutputCount++] = record; } else { - this.data[this.outCount++] = record; + this.data[this.PassOutputCount++] = record; } } @@ -1697,7 +1760,7 @@ private void Diverge() this.outData = new GlyphShapingData[this.data.Length]; } - Array.Copy(this.data, this.outData, this.outCount); + Array.Copy(this.data, this.outData, this.PassOutputCount); this.passDiverged = true; } @@ -1714,7 +1777,6 @@ private void EnsureOutCapacity(int required) } } -#pragma warning disable SA1401 // Fields exposed so callers can take interior references into buffer storage. /// /// One glyph's metrics-phase state: the resolving font, its point size, and the /// resolved metrics instance. Stored in a stream parallel to the glyph records. @@ -1769,5 +1831,4 @@ public readonly ushort GetAdvanceWidth(in GlyphShapingPosition position) public readonly ushort GetAdvanceHeight(in GlyphShapingPosition position) => position.Bounds.IsDirtyWH ? (ushort)position.Bounds.Height : this.Metrics.AdvanceHeight; } -#pragma warning restore SA1401 } diff --git a/src/SixLabors.Fonts/ShapingScratch.cs b/src/SixLabors.Fonts/ShapingScratch.cs index d9d170ef3..89e945be6 100644 --- a/src/SixLabors.Fonts/ShapingScratch.cs +++ b/src/SixLabors.Fonts/ShapingScratch.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.Fonts.Tables.AdvancedTypographic; - namespace SixLabors.Fonts; /// @@ -19,7 +17,7 @@ namespace SixLabors.Fonts; /// internal sealed class ShapingScratch { - /// + /// /// The per-font-run workspace buffer glyphs are substituted in. /// private ShapingBuffer? workspace; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/BaseTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/BaseTable.cs index 3a41ba6f0..1e0a63ea3 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/BaseTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/BaseTable.cs @@ -257,7 +257,7 @@ public static BaseAxisTable Load(BigEndianBinaryReader reader, long offset) // +--------+----------------------------+--------------------------------------------------------------------+ // | Tag | baselineTags[baseTagCount] | Array of 4-byte baseline identification tags, in alphabetical order | // +--------+----------------------------+--------------------------------------------------------------------+ - Tag[] baselineTags = Array.Empty(); + Tag[] baselineTags = []; if (baseTagListOffset != 0) { reader.Seek(offset + baseTagListOffset, SeekOrigin.Begin); @@ -292,7 +292,7 @@ public static BaseAxisTable Load(BigEndianBinaryReader reader, long offset) reader.Seek(scriptListStart, SeekOrigin.Begin); ushort baseScriptCount = reader.ReadUInt16(); - var scriptTags = new Tag[baseScriptCount]; + Tag[] scriptTags = new Tag[baseScriptCount]; ushort[] scriptOffsets = new ushort[baseScriptCount]; for (int i = 0; i < scriptTags.Length; i++) { @@ -300,7 +300,7 @@ public static BaseAxisTable Load(BigEndianBinaryReader reader, long offset) scriptOffsets[i] = reader.ReadOffset16(); } - var scripts = new BaseScriptEntry[baseScriptCount]; + BaseScriptEntry[] scripts = new BaseScriptEntry[baseScriptCount]; for (int i = 0; i < scripts.Length; i++) { scripts[i] = BaseScriptEntry.Load(scriptTags[i], reader, scriptListStart + scriptOffsets[i]); diff --git a/src/SixLabors.Fonts/TextLayout.LineBreaking.cs b/src/SixLabors.Fonts/TextLayout.LineBreaking.cs index 0df35991e..e12b0286a 100644 --- a/src/SixLabors.Fonts/TextLayout.LineBreaking.cs +++ b/src/SixLabors.Fonts/TextLayout.LineBreaking.cs @@ -308,8 +308,9 @@ VerticalOrientationType.Rotate or // Tracking should only be added once per grapheme, so only on the last codepoint of the grapheme. if (isLastInGrapheme && options.Tracking != 0 && i == decomposedAdvances.Length - 1) { - // Tracking should not be applied to tab characters or non-rendered codepoints. - if (!CodePoint.IsTabulation(codePoint) && !UnicodeUtility.ShouldNotBeRendered(codePoint)) + // Tracking applies per grapheme; tab characters and line + // terminators never receive it. + if (!CodePoint.IsTabulation(codePoint) && !CodePoint.IsNewLine(codePoint)) { if (isHorizontalLayout || shouldRotate) { diff --git a/src/SixLabors.Fonts/TextMeasurer.cs b/src/SixLabors.Fonts/TextMeasurer.cs index 0d04d9da4..529758ee8 100644 --- a/src/SixLabors.Fonts/TextMeasurer.cs +++ b/src/SixLabors.Fonts/TextMeasurer.cs @@ -670,17 +670,15 @@ private static FontRectangle GetGlyphAdvance(FontGlyphMetrics metrics, GlyphOpti Vector2 scale = new(scaledSize / metrics.ScaleFactor.X, scaledSize / metrics.ScaleFactor.Y); float emHeight = metrics.UnitsPerEm * scale.Y; - switch (options.GetGlyphLayoutMode(metrics.CodePoint)) + return options.GetGlyphLayoutMode(metrics.CodePoint) switch { - case GlyphLayoutMode.Vertical: - return new FontRectangle(0, 0, metrics.AdvanceWidth * scale.X, metrics.AdvanceHeight * scale.Y); - case GlyphLayoutMode.VerticalRotated: - // A rotated glyph advances along the column by its horizontal advance and its - // line box lies across the column. - return new FontRectangle(0, 0, emHeight, metrics.AdvanceWidth * scale.X); - default: - return new FontRectangle(0, 0, metrics.AdvanceWidth * scale.X, emHeight); - } + GlyphLayoutMode.Vertical => new FontRectangle(0, 0, metrics.AdvanceWidth * scale.X, metrics.AdvanceHeight * scale.Y), + + // A rotated glyph advances along the column by its horizontal advance and its + // line box lies across the column. + GlyphLayoutMode.VerticalRotated => new FontRectangle(0, 0, emHeight, metrics.AdvanceWidth * scale.X), + _ => new FontRectangle(0, 0, metrics.AdvanceWidth * scale.X, emHeight), + }; } /// diff --git a/src/SixLabors.Fonts/TextRun.cs b/src/SixLabors.Fonts/TextRun.cs index 21e533f16..2b8fee86d 100644 --- a/src/SixLabors.Fonts/TextRun.cs +++ b/src/SixLabors.Fonts/TextRun.cs @@ -133,7 +133,7 @@ internal void ResolveFontWeight(FontWeight? defaultWeight) this.ResolvedFontWeight = this.FontWeight ?? defaultWeight; if (!this.ResolvedFontWeight.HasValue && (font.RequestedStyle & FontStyle.Bold) == FontStyle.Bold) { - this.ResolvedFontWeight = SixLabors.Fonts.FontWeight.Bold; + this.ResolvedFontWeight = Fonts.FontWeight.Bold; } bool applied = false; diff --git a/src/SixLabors.Fonts/TextShaper.Pipeline.cs b/src/SixLabors.Fonts/TextShaper.Pipeline.cs index acafb8c34..7fb076a2f 100644 --- a/src/SixLabors.Fonts/TextShaper.Pipeline.cs +++ b/src/SixLabors.Fonts/TextShaper.Pipeline.cs @@ -380,6 +380,8 @@ or BidiCharacterType.FirstStrongIsolate font.FontMetrics.UpdatePositions(shaped); } + HideDefaultIgnorables(shaped); + ShapingProbe.Exit(ShapingProbe.Positioning, probe); // Copy the shaped result out of the pooled collections: run-constant state @@ -454,6 +456,80 @@ or BidiCharacterType.FirstStrongIsolate return new ShapedText([.. runs], infos, positions, bidiRuns, bidiMap, layoutMode); } + /// + /// Renders default ignorable records invisibly after positioning: both advances + /// and the offset on the axis of movement zero out, and the glyph swaps to its + /// font's invisible glyph. Records whose font offers no invisible glyph are + /// deleted instead. Records a lookup substituted keep their glyphs: the + /// substitution is a deliberate rendering the font produced from the ignorable. + /// + /// The positioned buffer. + private static void HideDefaultIgnorables(ShapingBuffer shaped) + { + if (!shaped.HasDefaultIgnorables) + { + return; + } + + CodePoint space = new(0x0020); + LayoutMode layoutMode = shaped.TextOptions.LayoutMode; + ColorFontSupport colorFontSupport = shaped.TextOptions.ColorFontSupport; + bool isVertical = layoutMode.IsVertical(); + Font? invisibleFont = null; + ushort invisible = 0; + bool hasInvisible = false; + bool hasUnreplaceable = false; + for (int i = 0; i < shaped.Count; i++) + { + ref GlyphShapingData data = ref shaped[i]; + if (!data.IsDefaultIgnorable || data.IsSubstituted) + { + continue; + } + + // Writing through the bounds setters marks them dirty, which is what + // the projection's advance reads honor. The cross-axis offset stays, + // so adjustments from positioning survive. + ref GlyphShapingPosition position = ref shaped.PositionAt(i); + position.Bounds.Width = 0; + position.Bounds.Height = 0; + if (isVertical) + { + position.Bounds.Y = 0; + } + else + { + position.Bounds.X = 0; + } + + ref ShapingBuffer.GlyphMetricsEntry entry = ref shaped.MetricsAt(i); + Font font = entry.Font; + if (!ReferenceEquals(font, invisibleFont)) + { + invisibleFont = font; + hasInvisible = font is not null && font.FontMetrics.TryGetGlyphId(space, out invisible); + } + + if (!hasInvisible) + { + hasUnreplaceable = true; + continue; + } + + // The projection reads the glyph id and default advance from the + // metrics entry, so the invisible glyph's metrics replace it. + TextRun textRun = shaped.TextRuns[data.TextRunIndex]; + entry.Metrics = font!.FontMetrics.GetGlyphMetrics(space, invisible, textRun.TextAttributes, textRun.TextDecorations, layoutMode, colorFontSupport); + shaped.SetGlyphId(i, invisible); + data.IsHidden = true; + } + + if (hasUnreplaceable) + { + shaped.DeleteGlyphsInPlace(static data => data.IsDefaultIgnorable && !data.IsSubstituted && !data.IsHidden); + } + } + /// /// Shapes a single font run โ€” maps codepoints in to glyph ids using /// , then runs GSUB substitution and GPOS positioning. Codepoints that diff --git a/src/SixLabors.Fonts/Unicode/UnicodeUtility.cs b/src/SixLabors.Fonts/Unicode/UnicodeUtility.cs index 95ff31c46..d2b5b46b5 100644 --- a/src/SixLabors.Fonts/Unicode/UnicodeUtility.cs +++ b/src/SixLabors.Fonts/Unicode/UnicodeUtility.cs @@ -147,169 +147,65 @@ public static uint GetVerticalMirror(uint value) /// 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; + uint plane = value >> 16; + if (plane == 0) + { + uint page = value >> 8; + switch (page) + { + case 0x00: + // SOFT HYPHEN + return value == 0x00AD; + case 0x03: + // COMBINING GRAPHEME JOINER + return value == 0x034F; + case 0x06: + // ARABIC LETTER MARK + return value == 0x061C; + case 0x11: + // HANGUL CHOSEONG FILLER..HANGUL JUNGSEONG FILLER + return IsInRangeInclusive(value, 0x115F, 0x1160); + case 0x17: + // KHMER VOWEL INHERENT AQ..KHMER VOWEL INHERENT AA + return IsInRangeInclusive(value, 0x17B4, 0x17B5); + case 0x18: + // MONGOLIAN FREE VARIATION SELECTOR ONE..MONGOLIAN VOWEL SEPARATOR + return IsInRangeInclusive(value, 0x180B, 0x180E); + case 0x20: + // ZERO WIDTH SPACE..RIGHT-TO-LEFT MARK, + // LEFT-TO-RIGHT EMBEDDING..RIGHT-TO-LEFT OVERRIDE, + // WORD JOINER..NOMINAL DIGIT SHAPES + return IsInRangeInclusive(value, 0x200B, 0x200F) + || IsInRangeInclusive(value, 0x202A, 0x202E) + || IsInRangeInclusive(value, 0x2060, 0x206F); + case 0x31: + // HANGUL FILLER + return value == 0x3164; + case 0xFE: + // VARIATION SELECTOR-1..VARIATION SELECTOR-16, ZERO WIDTH NO-BREAK SPACE + return IsInRangeInclusive(value, 0xFE00, 0xFE0F) || value == 0xFEFF; + case 0xFF: + // HALFWIDTH HANGUL FILLER, .. + return value == 0xFFA0 || IsInRangeInclusive(value, 0xFFF0, 0xFFF8); + default: + return false; + } + } + + switch (plane) + { + case 0x01: + // SHORTHAND FORMAT LETTER OVERLAP..SHORTHAND FORMAT UP STEP, + // MUSICAL SYMBOL BEGIN BEAM..MUSICAL SYMBOL END PHRASE + return IsInRangeInclusive(value, 0x1BCA0, 0x1BCA3) + || IsInRangeInclusive(value, 0x1D173, 0x1D17A); + case 0x0E: + // ..: LANGUAGE TAG, TAG SPACE..CANCEL TAG, + // VARIATION SELECTOR-17..VARIATION SELECTOR-256, and the reserved ranges. + return IsInRangeInclusive(value, 0xE0000, 0xE0FFF); + default: + return false; } - - // 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; } /// @@ -344,15 +240,6 @@ public static bool ShouldRenderWhiteSpaceOnly(in CodePoint codePoint) return false; } - /// - /// Gets a value indicating whether the specified code point should not be rendered. - /// - /// The code point. - /// The . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool ShouldNotBeRendered(in CodePoint codePoint) - => CodePoint.IsNewLine(codePoint) || (IsDefaultIgnorableCodePoint((uint)codePoint.Value) && !ShouldRenderWhiteSpaceOnly(codePoint)); - /// /// Returns the Unicode plane (0 through 16, inclusive) which contains this code point. /// diff --git a/tests/SixLabors.Fonts.Tests/HarfBuzzDifferentialTests.cs b/tests/SixLabors.Fonts.Tests/HarfBuzzDifferentialTests.cs index a16939851..77d9c7956 100644 --- a/tests/SixLabors.Fonts.Tests/HarfBuzzDifferentialTests.cs +++ b/tests/SixLabors.Fonts.Tests/HarfBuzzDifferentialTests.cs @@ -30,6 +30,13 @@ public static TheoryData ShapingCases() // Devanagari: conjuncts, matras, and reordering. { TestFonts.NotoSansDevanagariRegular, "เค•เฅเคทเคคเฅเคฐเคฟเคฏ เคฆเฅเคตเคพเคฐเคพ เคชเฅเคฐเค•เฅƒเคคเคฟ เค•เฅ€ เคฐเค•เฅเคทเคพ เค•เคฐเฅเคคเคตเฅเคฏ เคนเฅˆ", false }, { TestFonts.NotoSansDevanagariRegular, "เคถเฅเคฐเคฆเฅเคงเคพเค‚เคœเคฒเคฟ", false }, + + // Joiners inside shaping contexts: the joiner must steer the shaping + // (ligature suppression/formation, joining forms, half forms) and then + // render invisibly at zero advance. + { TestFonts.OpenSansFile, "of\u200Cfice fi\u200Dnal fluff", false }, + { TestFonts.ArabicFontFile, "\u0644\u200C\u0627 \u0644\u200D\u0627", true }, + { TestFonts.NotoSansDevanagariRegular, "\u0915\u094D\u200D\u0937 \u0915\u094D\u200C\u0937", false }, }; [Theory] diff --git a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GSub/GSubTableTests.Indic.cs b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GSub/GSubTableTests.Indic.cs index 76e2c0f77..51358c1bb 100644 --- a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GSub/GSubTableTests.Indic.cs +++ b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GSub/GSubTableTests.Indic.cs @@ -122,8 +122,7 @@ private static Font CreateFont(string testFont) [InlineData(KannadaFont.Sans, "\u0cb2\u0ccb\u0c82", new int[] { 264, 61, 71, 4 })] [InlineData(KannadaFont.Sans, "\u0cb5\u0ccb\u0c82", new int[] { 266, 275, 71, 4 })] - // Harfbuzz replaces the default ignorable with id 91 with a space (3) and sets the advance to 0. We skip it entirely on rendering. - [InlineData(KannadaFont.Sans, "\u0c86\u0ccd\u0caf\u0c95\u0ccd\u0cb7\u0cbf\u0cb8\u0ccd\u200c", new int[] { 7, 122, 285, 200 })] + [InlineData(KannadaFont.Sans, "\u0c86\u0ccd\u0caf\u0c95\u0ccd\u0cb7\u0cbf\u0cb8\u0ccd\u200c", new int[] { 7, 122, 285, 200, 3 })] public void CanShapeKannadaText(KannadaFont font, string input, int[] expectedGlyphIndices) { ColorGlyphRenderer renderer = new(); @@ -215,45 +214,34 @@ public void CanShapeTamilText(string input, int[] expectedGlyphIndices) [Theory] [InlineData("\u0930\u094d\u0939", new int[] { 61, 181 })] - // Harfbuzz replaces the default ignorable with id 133 with a space (3) and sets the advance to 0. We skip it entirely on rendering. - [InlineData("\u0930\u094d\u200c\u0939", new int[] { 52, 81, 61 })] + [InlineData("\u0930\u094d\u200c\u0939", new int[] { 52, 81, 3, 61 })] [InlineData("\u0930\u094d\u200d\u0939", new int[] { 209, 61 })] [InlineData("\u0931\u094d\u0939", new int[] { 209, 61 })] - // Harfbuzz replaces the default ignorable with id 133 with a space (3) and sets the advance to 0. We skip it entirely on rendering. - [InlineData("\u0931\u094d\u200c\u0939", new int[] { 53, 81, 61 })] + [InlineData("\u0931\u094d\u200c\u0939", new int[] { 53, 81, 3, 61 })] - // Harfbuzz replaces the default ignorable with id 134 with a space (3) and sets the advance to 0. We skip it entirely on rendering. - [InlineData("\u0931\u094d\u200d\u0939", new int[] { 209, 61 })] + [InlineData("\u0931\u094d\u200d\u0939", new int[] { 209, 3, 61 })] [InlineData("\u0915\u094d\u0915", new int[] { 183, 25 })] - // Harfbuzz replaces the default ignorable with id 134 with a space (3) and sets the advance to 0. We skip it entirely on rendering. - [InlineData("\u0915\u094d\u200d", new int[] { 183 })] + [InlineData("\u0915\u094d\u200d", new int[] { 183, 3 })] - // Harfbuzz replaces the default ignorable with id 133 with a space (3) and sets the advance to 0. We skip it entirely on rendering. - [InlineData("\u0915\u094d\u200c\u0915", new int[] { 25, 81, 25 })] + [InlineData("\u0915\u094d\u200c\u0915", new int[] { 25, 81, 3, 25 })] - // Harfbuzz replaces the default ignorable with id 134 with a space (3) and sets the advance to 0. We skip it entirely on rendering. - [InlineData("\u0915\u094d\u200d\u0915", new int[] { 183, 25 })] + [InlineData("\u0915\u094d\u200d\u0915", new int[] { 183, 3, 25 })] [InlineData("\u0915\u094d\u0915\u093f", new int[] { 558, 183, 25 })] - // Harfbuzz replaces the default ignorable with id 133 with a space (3) and sets the advance to 0. We skip it entirely on rendering. - [InlineData("\u0915\u094d\u200c\u0915\u093f", new int[] { 25, 81, 561, 25 })] + [InlineData("\u0915\u094d\u200c\u0915\u093f", new int[] { 25, 81, 3, 561, 25 })] - // Harfbuzz replaces the default ignorable with id 134 with a space (3) and sets the advance to 0. We skip it entirely on rendering. - [InlineData("\u0915\u094d\u200d\u0915\u093f", new int[] { 558, 183, 25 })] + [InlineData("\u0915\u094d\u200d\u0915\u093f", new int[] { 558, 183, 3, 25 })] [InlineData("\u0915\u094d\u0937", new int[] { 179 })] - // Harfbuzz replaces the default ignorable with id 133 with a space (3) and sets the advance to 0. We skip it entirely on rendering. - [InlineData("\u0915\u094d\u200c\u0937", new int[] { 25, 81, 59 })] + [InlineData("\u0915\u094d\u200c\u0937", new int[] { 25, 81, 3, 59 })] - // Harfbuzz replaces the default ignorable with id 134 with a space (3) and sets the advance to 0. We skip it entirely on rendering. - [InlineData("\u0915\u094d\u200d\u0937", new int[] { 183, 59 })] + [InlineData("\u0915\u094d\u200d\u0937", new int[] { 183, 3, 59 })] [InlineData("\u0926\u094d\u0938\u093f", new int[] { 42, 81, 563, 60 })] - // Harfbuzz replaces the default ignorable with id 133 with a space (3) and sets the advance to 0. We skip it entirely on rendering. - [InlineData("\u0926\u094d\u200c\u0938\u093f", new int[] { 42, 81, 563, 60 })] + [InlineData("\u0926\u094d\u200c\u0938\u093f", new int[] { 42, 81, 3, 563, 60 })] [InlineData("\u0926\u094d\u200d\u0938\u093f", new int[] { 558, 200, 60 })] public void CanShapeDevanagariTextWithJoiners(string input, int[] expectedGlyphIndices) { @@ -276,17 +264,14 @@ public void CanShapeDevanagariTextWithJoiners(string input, int[] expectedGlyphI [InlineData("\u0915\u094d\u0930", new int[] { 254 })] [InlineData("\u0915\u094d\u0930\u094d\u0915", new int[] { 327, 25 })] - // Harfbuzz replaces the default ignorable with id 134 with a space (3) and sets the advance to 0. We skip it entirely on rendering. - [InlineData("\u0915\u094d\u0930\u094d\u200d", new int[] { 327 })] + [InlineData("\u0915\u094d\u0930\u094d\u200d", new int[] { 327, 3 })] [InlineData("\u0915\u094d\u0937", new int[] { 179 })] [InlineData("\u0915\u094d\u0937\u094d", new int[] { 179, 81 })] - // Harfbuzz replaces the default ignorable with id 133 with a space (3) and sets the advance to 0. We skip it entirely on rendering. - [InlineData("\u0915\u094d\u200c\u0937", new int[] { 25, 81, 59 })] + [InlineData("\u0915\u094d\u200c\u0937", new int[] { 25, 81, 3, 59 })] - // Harfbuzz replaces the default ignorable with id 134 with a space (3) and sets the advance to 0. We skip it entirely on rendering. - [InlineData("\u0915\u094d\u200d", new int[] { 183 })] - [InlineData("\u0915\u094d\u200d\u0937", new int[] { 183, 59 })] + [InlineData("\u0915\u094d\u200d", new int[] { 183, 3 })] + [InlineData("\u0915\u094d\u200d\u0937", new int[] { 183, 3, 59 })] [InlineData("\u091b\u094d\u0930\u094d\u0915", new int[] { 334, 25 })] [InlineData("\u091c\u094d\u091e\u094d", new int[] { 180, 81 })] [InlineData("\u091f\u094d\u0930\u0941", new int[] { 35, 657 })] @@ -299,15 +284,12 @@ public void CanShapeDevanagariTextWithJoiners(string input, int[] expectedGlyphI [InlineData("\u0926\u094d\u0926\u093f", new int[] { 560, 511 })] [InlineData("\u0930\u094d\u0905\u094d", new int[] { 9, 81, 181 })] - // Harfbuzz replaces the default ignorable with id 133 with a space (3) and sets the advance to 0. We skip it entirely on rendering. - [InlineData("\u0930\u094d\u0905\u094d\u200c", new int[] { 9, 81, 181 })] + [InlineData("\u0930\u094d\u0905\u094d\u200c", new int[] { 9, 81, 3, 181 })] - // Harfbuzz replaces the default ignorable with id 134 with a space (3) and sets the advance to 0. We skip it entirely on rendering. - [InlineData("\u0930\u094d\u0905\u094d\u200d", new int[] { 52, 81, 9, 81 })] + [InlineData("\u0930\u094d\u0905\u094d\u200d", new int[] { 52, 81, 9, 81, 3 })] [InlineData("\u0930\u094d\u0906\u094d\u0930\u094d", new int[] { 10, 81, 181, 52, 81 })] - // Harfbuzz replaces the default ignorable with id 133 with a space (3) and sets the advance to 0. We skip it entirely on rendering. - [InlineData("\u0915\u200c\u093f", new int[] { 561, 25 })] + [InlineData("\u0915\u200c\u093f", new int[] { 561, 25, 3 })] [InlineData("\u093d\u0902", new int[] { 65, 6 })] [InlineData("\u0930\u0941\u0901\u0903", new int[] { 413, 5, 7 })] [InlineData("\u0031\u093f", new int[] { 558, 748 })] @@ -348,11 +330,9 @@ public void CanShapeDevanagariTextWithDottedCircle(string input, int[] expectedG [Theory] [InlineData("\u0924\u094d\u0930\u094d\u0915", new int[] { 347, 25 })] - // Harfbuzz replaces the default ignorable with id 134 with a space (3) and sets the advance to 0. We skip it entirely on rendering. - [InlineData("\u0924\u094d\u0930\u094d\u200d\u0915", new int[] { 347, 25 })] + [InlineData("\u0924\u094d\u0930\u094d\u200d\u0915", new int[] { 347, 3, 25 })] - // Harfbuzz replaces the default ignorable with id 133 with a space (3) and sets the advance to 0. We skip it entirely on rendering. - [InlineData("\u0924\u094d\u0930\u094d\u200c\u0915", new int[] { 269, 81, 25 })] + [InlineData("\u0924\u094d\u0930\u094d\u200c\u0915", new int[] { 269, 81, 3, 25 })] public void CanShapeDevanagariTextWithEyelash(string input, int[] expectedGlyphIndices) { ColorGlyphRenderer renderer = new(); @@ -375,35 +355,30 @@ public void CanShapeDevanagariTextWithEyelash(string input, int[] expectedGlyphI [InlineData("\u0995\u09cd\u09b0", new int[] { 199 })] [InlineData("\u0995\u09cd\u09b0\u09cd\u0995", new int[] { 199, 64, 19 })] - // Harfbuzz replaces the default ignorable with id 573 with a space (3) and sets the advance to 0. We skip it entirely on rendering. - [InlineData("\u0995\u09cd\u200c\u0995", new int[] { 19, 64, 19 })] + [InlineData("\u0995\u09cd\u200c\u0995", new int[] { 19, 64, 3, 19 })] - // Harfbuzz replaces the default ignorable with id 574 with a space (3) and sets the advance to 0. We skip it entirely on rendering. - [InlineData("\u0995\u09cd\u200d\u0995", new int[] { 130, 19 })] + [InlineData("\u0995\u09cd\u200d\u0995", new int[] { 130, 3, 19 })] [InlineData("\u09a6\u09cd\u09af", new int[] { 36, 198 })] [InlineData("\u09a8\u09cd\u0995", new int[] { 149, 19 })] [InlineData("\u09a8\u09cd\u09a7", new int[] { 360 })] [InlineData("\u09a8\u09cd\u09af", new int[] { 38, 198 })] [InlineData("\u09a8\u09cd\u09b0", new int[] { 219 })] - // Harfbuzz replaces the default ignorable with id 573 with a space (3) and sets the advance to 0. We skip it entirely on rendering. - [InlineData("\u09a8\u09cd\u200c\u0995", new int[] { 38, 64, 19 })] - [InlineData("\u09a8\u09cd\u200c\u09a7", new int[] { 38, 64, 37 })] - [InlineData("\u09a8\u09cd\u200c\u09ac", new int[] { 38, 64, 41 })] - [InlineData("\u09a8\u09cd\u200c\u09b0", new int[] { 38, 64, 45 })] - - // Harfbuzz replaces the default ignorable with id 574 with a space (3) and sets the advance to 0. We skip it entirely on rendering. - [InlineData("\u09a8\u09cd\u200d\u0995", new int[] { 149, 19 })] - [InlineData("\u09a8\u09cd\u200d\u09a7", new int[] { 149, 37 })] - [InlineData("\u09a8\u09cd\u200d\u09ac", new int[] { 149, 41 })] - [InlineData("\u09a8\u09cd\u200d\u09b0", new int[] { 149, 45 })] + [InlineData("\u09a8\u09cd\u200c\u0995", new int[] { 38, 64, 3, 19 })] + [InlineData("\u09a8\u09cd\u200c\u09a7", new int[] { 38, 64, 3, 37 })] + [InlineData("\u09a8\u09cd\u200c\u09ac", new int[] { 38, 64, 3, 41 })] + [InlineData("\u09a8\u09cd\u200c\u09b0", new int[] { 38, 64, 3, 45 })] + + [InlineData("\u09a8\u09cd\u200d\u0995", new int[] { 149, 3, 19 })] + [InlineData("\u09a8\u09cd\u200d\u09a7", new int[] { 149, 3, 37 })] + [InlineData("\u09a8\u09cd\u200d\u09ac", new int[] { 149, 3, 41 })] + [InlineData("\u09a8\u09cd\u200d\u09b0", new int[] { 149, 3, 45 })] [InlineData("\u09af\u09cd", new int[] { 44, 64 })] [InlineData("\u09b0\u09cd\u0995", new int[] { 19, 127 })] [InlineData("\u09b0\u09cd\u0995\u09bf", new int[] { 54, 19, 127 })] [InlineData("\u09b0\u09cd\u0995\u09cc", new int[] { 446, 19, 127, 66 })] - // Harfbuzz replaces the default ignorable with id 574 with a space (3) and sets the advance to 0. We skip it entirely on rendering. - [InlineData("\u09b0\u09cd\u09a8\u09cd\u200d", new int[] { 45, 64, 38, 64 })] + [InlineData("\u09b0\u09cd\u09a8\u09cd\u200d", new int[] { 45, 64, 38, 64, 3 })] [InlineData("\u09b0\u09cd\u09ac\u09cd\u09ac", new int[] { 263, 127 })] [InlineData("\u09b6\u09cd\u09af", new int[] { 47, 198 })] [InlineData("\u09b7\u09cd\u09af", new int[] { 48, 198 })] @@ -421,8 +396,7 @@ public void CanShapeDevanagariTextWithEyelash(string input, int[] expectedGlyphI [InlineData("\u09a4\u09cd\u09af\u09c1", new int[] { 34, 518, 198 })] [InlineData("\u099a\u09cd\u09af\u09cd\u09b0", new int[] { 135, 225 })] - // Harfbuzz replaces the default ignorable with id 574 with a space (3) and sets the advance to 0. We skip it entirely on rendering. - [InlineData("\u0995\u09cd\u200d\u09b7", new int[] { 130, 48 })] + [InlineData("\u0995\u09cd\u200d\u09b7", new int[] { 130, 3, 48 })] public void CanShapeBengaliText(string input, int[] expectedGlyphIndices) { ColorGlyphRenderer renderer = new(); @@ -544,8 +518,7 @@ public void CanShapeGujaratiText(string input, int[] expectedGlyphIndices) [InlineData("\u0d05\u0d25\u0d4e\u0d35\u0d4d\u0d35\u0d02", new int[] { 6, 36, 208, 73, 4 })] [InlineData("\u0d15\u0d4d\u200d", new int[] { 101 })] - // Harfbuzz replaces the default ignorable with id 103 with a space (3) and sets the advance to 0. We skip it entirely on rendering. - [InlineData("\u0d15\u0d3e\u0d2f\u0d4d\u200c\u0d15\u0d31\u0d3f", new int[] { 20, 59, 46, 72, 20, 48, 60 })] + [InlineData("\u0d15\u0d3e\u0d2f\u0d4d\u200c\u0d15\u0d31\u0d3f", new int[] { 20, 59, 46, 72, 3, 20, 48, 60 })] [InlineData("\u0d15\u0d3e\u0d30\u0d4d\u200d\u0d15\u0d4d\u0d15\u0d4b\u0d1f\u0d15\u0d28\u0d4d\u200d", new int[] { 20, 59, 98, 67, 147, 59, 30, 20, 97 })] [InlineData("\u0d15\u0d41\u0d31\u0d4d\u0d31\u0d4d\u0d2f\u0d3e\u0d1f\u0d3f", new int[] { 20, 62, 203, 229, 59, 30, 60 })] [InlineData("\u0d15\u0d46", new int[] { 66, 20 })] @@ -592,24 +565,21 @@ public void CanShapeGujaratiText(string input, int[] expectedGlyphIndices) [InlineData("\u0d35\u0d4d\u0d35", new int[] { 208 })] [InlineData("\u0d37\u0d4d\u0d1f\u0d4d\u0d30\u0d40", new int[] { 54, 72, 146, 30, 61 })] - // Harfbuzz replaces the default ignorable with id 103 with a space (3) and sets the advance to 0. We skip it entirely on rendering. - [InlineData("\u0d38\u0d4b\u0d2b\u0d4d\u0d31\u0d4d\u0d31\u0d4d\u200c\u0d35\u0d46\u0d2f\u0d30\u0d4d\u200d", new int[] { 67, 55, 59, 42, 72, 203, 72, 66, 52, 46, 98 })] + [InlineData("\u0d38\u0d4b\u0d2b\u0d4d\u0d31\u0d4d\u0d31\u0d4d\u200c\u0d35\u0d46\u0d2f\u0d30\u0d4d\u200d", new int[] { 67, 55, 59, 42, 72, 203, 72, 3, 66, 52, 46, 98 })] [InlineData("\u0d38\u0d4d\u0d2a\u0d4d\u0d30\u0d3f", new int[] { 55, 72, 146, 41, 60 })] [InlineData("\u0d38\u0d4d\u0d2a\u0d4d\u0d30\u0d47", new int[] { 55, 72, 67, 146, 41 })] [InlineData("\u0d38\u0d4d\u0d2a\u0d4d\u0d32\u0d47", new int[] { 55, 72, 67, 192 })] [InlineData("\u0d38\u0d4d\u0d35\u0d3e\u0d24\u0d28\u0d4d\u0d24\u0d4d\u0d30\u0d4d\u0d2f\u0d02", new int[] { 55, 145, 59, 35, 146, 189, 144, 4 })] - // Harfbuzz replaces the default ignorable with id 103 with a space (3) and sets the advance to 0. We skip it entirely on rendering. - [InlineData("\u0d39\u0d3e\u0d30\u0d4d\u200d\u0d21\u0d4d\u200c\u0d35\u0d46\u0d2f\u0d30\u0d4d\u200d", new int[] { 56, 59, 98, 32, 72, 66, 52, 46, 98 })] + [InlineData("\u0d39\u0d3e\u0d30\u0d4d\u200d\u0d21\u0d4d\u200c\u0d35\u0d46\u0d2f\u0d30\u0d4d\u200d", new int[] { 56, 59, 98, 32, 72, 3, 66, 52, 46, 98 })] [InlineData("\u0d33\u0d4d\u200d", new int[] { 100 })] [InlineData("\u0d33\u0d4d\u0d2f\u0d02", new int[] { 50, 229, 4 })] [InlineData("\u0d33\u0d4d\u0d33", new int[] { 206 })] [InlineData("\u0d32\u0d4d\u200d\u0d2a\u0d4d\u0d2a\u0d47", new int[] { 99, 67, 191 })] - // Harfbuzz replaces the default ignorable with id 103 with a space (3) and sets the advance to 0. We skip it entirely on rendering. - [InlineData("\u0d36\u0d3f\u0d02\u200c", new int[] { 53, 60, 4 })] - [InlineData("\u0d15\u0d4b\u0d02\u200c", new int[] { 67, 20, 59, 4 })] - [InlineData("\u0d2f\u200d\u0d4d\u0d2f", new int[] { 46, 144 })] + [InlineData("\u0d36\u0d3f\u0d02\u200c", new int[] { 53, 60, 3, 4 })] + [InlineData("\u0d15\u0d4b\u0d02\u200c", new int[] { 67, 20, 59, 3, 4 })] + [InlineData("\u0d2f\u200d\u0d4d\u0d2f", new int[] { 46, 3, 144 })] [InlineData("\u0d38\u0d4d\u0d31\u0d4d\u0d31\u0d4d", new int[] { 214, 72 })] public void CanShapeMalayalamText(string input, int[] expectedGlyphIndices) { @@ -660,8 +630,7 @@ public void CanShapeOriyaText(string input, int[] expectedGlyphIndices) [InlineData("\u1784\u17d2\u1782\u17d2\u179a", new int[] { 189, 48, 146 })] [InlineData("\u1798\u17c9\u17d2\u179b\u17c1\u17c7", new int[] { 107, 68, 115, 172, 113 })] - // Harfbuzz replaces the default ignorable with id 262 with a space (3) and sets the advance to 0. We skip it entirely on rendering. - [InlineData("\u1798\u200c\u17c9\u17d2\u179b\u17c1\u17c7", new int[] { 107, 68, 115, 172, 113 })] + [InlineData("\u1798\u200c\u17c9\u17d2\u179b\u17c1\u17c7", new int[] { 107, 68, 3, 115, 172, 113 })] [InlineData("\u1794\u17ca\u17d0", new int[] { 64, 116, 122 })] [InlineData("\u1793\u17c2\u17ce", new int[] { 108, 63, 120 })] [InlineData("\u1780\u17c1\u17d2\u179a", new int[] { 107, 171, 44 })] diff --git a/tests/SixLabors.Fonts.Tests/Unicode/UnicodeUtilityTests.cs b/tests/SixLabors.Fonts.Tests/Unicode/UnicodeUtilityTests.cs index 51f6f1996..51fd5d36b 100644 --- a/tests/SixLabors.Fonts.Tests/Unicode/UnicodeUtilityTests.cs +++ b/tests/SixLabors.Fonts.Tests/Unicode/UnicodeUtilityTests.cs @@ -15,7 +15,6 @@ public class UnicodeUtilityTests [InlineData(0x17B4, 0x17B5)] [InlineData(0x180B, 0x180D)] [InlineData(0x180E, 0x180E)] - [InlineData(0x180F, 0x180F)] [InlineData(0x200B, 0x200F)] [InlineData(0x202A, 0x202E)] [InlineData(0x2060, 0x2064)] @@ -45,6 +44,7 @@ public void CanDetectDefaultIgnorableCodePoint(uint min, uint max) [Theory] [InlineData(0x0u, 0x7Fu)] // ASCII + [InlineData(0x180Fu, 0x180Fu)] // MONGOLIAN FREE VARIATION SELECTOR FOUR: deliberately excluded public void NoFalsePositiveDefaultIgnorableCodePoint(uint min, uint max) { for (uint i = min; i <= max; i++) From 4c60de61ed7a39eaf272f2712a3fe661902706cb Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 26 Jul 2026 00:26:51 +1000 Subject: [PATCH 61/94] Shape into a caller-owned buffer The public API gains the reusable buffer model shared by HarfBuzz's hb_buffer_t and Avalonia's shaper contract: TextShapingBuffer is a caller-owned buffer of ShapedGlyph records whose storage grows to its high-water mark and is retained, and Shape(text, options, buffer) replaces its contents by projecting directly off the internal positioned buffer. The list-returning overload becomes a thin wrapper over it. Steady-state shaping through the buffer overload allocates nothing - proven at 0.0 bytes per call over 200,000 calls and enforced by a suite acceptance test. Every consumer now runs one scoped pipeline: ShapedText becomes a view over pooled scratch storage valid only within a shaping scope, and TextLayout, TextBlock, and TextRenderer consume it through ComposeLogicalLine(text, options), whose ShapedTextScope owns the pooled state until disposal - pooling never surfaces outside the shaper. Paths whose results retain run references build their runs per call; the shaping overloads reuse a scratch-owned synthesized run. The bidi algorithm and analysis data move off thread statics into the same scratch so all pipeline state shares one lifetime mechanism. Allocation fixes found by measurement on the way to zero: language tags were re-resolved on every buffer reset (480 B/call, now cached by options identity like the plan cache); GetGraphemeCount's ASCII gate used ContainsAnyExceptInRange, which allocates 96 B/call on net8 (now Ascii.IsValid); and the fallback-font lambda's closure display class allocated at method entry on every call (24 B, now a plain helper). The benchmark shapes into one reused buffer on both sides, matching how HarfBuzz is driven. Benchmark (clean window, controls 2.31/3.87/8.99 us at idle): | Scenario | Before | After | |------------|----------------------------|-------------------------| | Latin | 4.79 us / 2.11x / 6,216 B | 3.91 us / 1.69x / 0 B | | Arabic | 7.43 us / 1.92x / 4,368 B | 6.74 us / 1.74x / 152 B | | Devanagari | 11.45 us / 1.27x / 3,832 B | 10.69 us / 1.19x / 192 B | The remaining non-Latin bytes are the bidi run coalescing enumerator on the non-uniform-direction path; logged as the next kill. Suite 5,591/0 -> 5,596/0. --- src/SixLabors.Fonts/Rendering/TextRenderer.cs | 3 +- src/SixLabors.Fonts/ShapedText.cs | 20 +- src/SixLabors.Fonts/ShapingBuffer.cs | 10 +- src/SixLabors.Fonts/ShapingScratch.cs | 191 +++++++++++++++++- src/SixLabors.Fonts/TextBlock.cs | 3 +- .../TextLayout.LineBreaking.cs | 19 +- src/SixLabors.Fonts/TextShaper.Pipeline.cs | 167 ++++++++++----- src/SixLabors.Fonts/TextShaper.cs | 93 ++++++--- src/SixLabors.Fonts/TextShapingBuffer.cs | 74 +++++++ src/SixLabors.Fonts/Unicode/BidiAlgorithm.cs | 6 - src/SixLabors.Fonts/Unicode/BidiData.cs | 8 - .../Unicode/MemoryExtensions.cs | 2 +- .../ShapeTextBenchmark.cs | 10 +- .../SixLabors.Fonts.Tests/TextLayoutTests.cs | 3 +- .../SixLabors.Fonts.Tests/TextShaperTests.cs | 81 ++++++++ 15 files changed, 576 insertions(+), 114 deletions(-) create mode 100644 src/SixLabors.Fonts/TextShapingBuffer.cs diff --git a/src/SixLabors.Fonts/Rendering/TextRenderer.cs b/src/SixLabors.Fonts/Rendering/TextRenderer.cs index e9695846a..658906ce0 100644 --- a/src/SixLabors.Fonts/Rendering/TextRenderer.cs +++ b/src/SixLabors.Fonts/Rendering/TextRenderer.cs @@ -83,8 +83,7 @@ public void Render(ReadOnlySpan text, TextOptions options) return; } - ShapedText shaped = TextShaper.ShapeText(text, options); - LogicalTextLine logicalLine = TextLayout.ComposeLogicalLine(shaped, text, options); + LogicalTextLine logicalLine = TextLayout.ComposeLogicalLine(text, options); this.RenderText(logicalLine, options); } diff --git a/src/SixLabors.Fonts/ShapedText.cs b/src/SixLabors.Fonts/ShapedText.cs index 6d3a44ca9..2d335eeac 100644 --- a/src/SixLabors.Fonts/ShapedText.cs +++ b/src/SixLabors.Fonts/ShapedText.cs @@ -6,11 +6,11 @@ namespace SixLabors.Fonts; /// -/// Contains the width-independent result of shaping text before logical line -/// composition: a run table holding run-constant state once, and parallel per-glyph -/// info and position arrays holding identities and pure numbers. The result is -/// inert - it references no pipeline state - and consumers needing glyph metrics -/// query the owning font by glyph id. +/// The width-independent result of shaping text before logical line composition: +/// a run table holding run-constant state once, and parallel per-glyph info and +/// position arrays holding identities and pure numbers. The arrays are views over +/// pooled scratch storage and may exceed the live counts; the view is valid only +/// while the renting scope holds its scratch, so consumers copy what they retain. /// internal readonly struct ShapedText { @@ -20,6 +20,7 @@ internal readonly struct ShapedText /// The shaped run table. /// The per-glyph identity records, parallel to . /// The per-glyph geometry records, parallel to . + /// The number of live entries in the per-glyph arrays. /// The resolved bidi runs covering the shaped text. /// /// The code point index to bidi-run index mapping built during shaping. Entries for @@ -30,6 +31,7 @@ public ShapedText( ShapedTextRun[] runs, ShapedGlyphInfo[] infos, ShapedGlyphPosition[] positions, + int glyphCount, BidiRun[] bidiRuns, int[] bidiMap, LayoutMode layoutMode) @@ -37,11 +39,17 @@ public ShapedText( this.Runs = runs; this.Infos = infos; this.Positions = positions; + this.GlyphCount = glyphCount; this.BidiRuns = bidiRuns; this.BidiMap = bidiMap; this.LayoutMode = layoutMode; } + /// + /// Gets the number of live entries in and . + /// + public int GlyphCount { get; } + /// /// Gets the shaped run table: run-constant state referenced per glyph by /// . @@ -108,7 +116,7 @@ public bool TryGetGlyphsAtOffset( isVerticalSubstitution = false; isDecomposed = false; - for (int i = searchIndex; i < infos.Length; i++) + for (int i = searchIndex; i < this.GlyphCount; i++) { if (infos[i].CodePointIndex == offset) { diff --git a/src/SixLabors.Fonts/ShapingBuffer.cs b/src/SixLabors.Fonts/ShapingBuffer.cs index f75cdf6bf..7cda2327b 100644 --- a/src/SixLabors.Fonts/ShapingBuffer.cs +++ b/src/SixLabors.Fonts/ShapingBuffer.cs @@ -407,15 +407,15 @@ public void Reset(TextOptions textOptions) this.placeholderBidiRuns.Clear(); this.SegmentPlans.Clear(); - // Cached plans captured option values when built, so a different options - // instance invalidates them. + // Cached plans and language tags captured option values when built, so only + // a different options instance invalidates them; this is the same identity + // contract the shaper and plan caches follow. if (!ReferenceEquals(this.TextOptions, textOptions)) { this.planCache.Clear(); + this.TextOptions = textOptions; + this.LanguageTags = ResolveLanguageTags(textOptions); } - - this.TextOptions = textOptions; - this.LanguageTags = ResolveLanguageTags(textOptions); } /// diff --git a/src/SixLabors.Fonts/ShapingScratch.cs b/src/SixLabors.Fonts/ShapingScratch.cs index 89e945be6..9640a7743 100644 --- a/src/SixLabors.Fonts/ShapingScratch.cs +++ b/src/SixLabors.Fonts/ShapingScratch.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.Fonts.Unicode; + namespace SixLabors.Fonts; /// @@ -27,6 +29,175 @@ internal sealed class ShapingScratch /// private ShapingBuffer? result; + /// + /// The synthesized run covering the whole text when the options carry no + /// user-defined runs, reused across passes together with its single-element + /// list. + /// + private readonly TextRun[] defaultRun = [new()]; + + /// + /// The reusable bidi algorithm instance backing . + /// + private BidiAlgorithm? bidiAlgorithm; + + /// + /// The reusable bidi analysis data backing . + /// + private BidiData? bidiData; + + /// + /// The resolved bidi runs of the current pass, grown to the high-water count. + /// + private BidiRun[] bidiRuns = new BidiRun[4]; + + /// + /// The per-glyph identity records of the current pass's projection, grown to + /// the high-water glyph count. + /// + private ShapedGlyphInfo[] infos = []; + + /// + /// The per-glyph geometry records of the current pass's projection, parallel + /// to . + /// + private ShapedGlyphPosition[] positions = []; + + /// + /// The run table of the current pass's projection, grown to the high-water + /// run count. + /// + private ShapedTextRun[] runs = new ShapedTextRun[4]; + + /// + /// Gets the reusable bidi algorithm instance. Its work buffers grow to the + /// workload's high-water mark and are reused across passes. + /// + public BidiAlgorithm BidiAlgorithm => this.bidiAlgorithm ??= new(); + + /// + /// Gets the reusable bidi analysis data. Its builders grow to the workload's + /// high-water mark and resets them per pass. + /// + public BidiData BidiData => this.bidiData ??= new(); + + /// + /// Gets the resolved bidi runs of the current pass. Only the first + /// entries are live. + /// + public BidiRun[] BidiRuns => this.bidiRuns; + + /// + /// Gets the number of live entries in . + /// + public int BidiRunCount { get; private set; } + + /// + /// Gets the run table of the current pass's projection. Only the first + /// entries are live. + /// + public ShapedTextRun[] Runs => this.runs; + + /// + /// Gets the number of live entries in . + /// + public int RunCount { get; private set; } + + /// + /// Gets the codepoint index to bidi-run index mapping storage of the current + /// pass, as last prepared by . + /// + public int[] BidiMap { get; private set; } = []; + + /// + /// Empties the bidi run storage for a new pass. + /// + public void ClearBidiRuns() => this.BidiRunCount = 0; + + /// + /// Appends a resolved bidi run. + /// + /// The run to append. + public void AddBidiRun(in BidiRun run) + { + if (this.BidiRunCount == this.bidiRuns.Length) + { + Array.Resize(ref this.bidiRuns, this.bidiRuns.Length * 2); + } + + this.bidiRuns[this.BidiRunCount++] = run; + } + + /// + /// Empties the projected run table for a new pass. + /// + public void ClearRuns() => this.RunCount = 0; + + /// + /// Appends a projected run table entry. + /// + /// The entry to append. + public void AddRun(in ShapedTextRun run) + { + if (this.RunCount == this.runs.Length) + { + Array.Resize(ref this.runs, this.runs.Length * 2); + } + + this.runs[this.RunCount++] = run; + } + + /// + /// Gets the parallel projection storage for the given glyph count. + /// + /// The glyph capacity required. + /// The identity and geometry storage; entries beyond the count are undefined. + public (ShapedGlyphInfo[] Infos, ShapedGlyphPosition[] Positions) GetProjection(int count) + { + if (this.infos.Length < count) + { + int capacity = Math.Max(count, Math.Max(64, this.infos.Length * 2)); + this.infos = new ShapedGlyphInfo[capacity]; + this.positions = new ShapedGlyphPosition[capacity]; + } + + return (this.infos, this.positions); + } + + /// + /// Gets the codepoint index to bidi-run index mapping for the given text + /// length, every entry reset to the unvisited -1 sentinel. + /// + /// The codepoint capacity required. + /// The mapping storage; entries beyond the length are undefined. + public int[] GetBidiMap(int length) + { + if (this.BidiMap.Length < length) + { + this.BidiMap = new int[Math.Max(length, Math.Max(64, this.BidiMap.Length * 2))]; + } + + Array.Fill(this.BidiMap, -1, 0, length); + return this.BidiMap; + } + + /// + /// Gets the reusable single-run list covering the whole text, configured for + /// the given options. + /// + /// The grapheme count of the text. + /// The text options supplying the font. + /// The run list. + public IReadOnlyList GetDefaultTextRuns(int end, TextOptions options) + { + TextRun run = this.defaultRun[0]; + run.Start = 0; + run.End = end; + run.Font = options.Font; + run.ResolveFontWeight(options.FontWeight); + return this.defaultRun; + } + /// /// Gets the reusable shaping buffers, reset for a new pass over the given options. /// @@ -34,22 +205,26 @@ internal sealed class ShapingScratch /// The reusable buffers, sharing one feature map. public (ShapingBuffer Workspace, ShapingBuffer Result) Prepare(TextOptions options) { - if (this.workspace is null) + ShapingBuffer? workspace = this.workspace; + ShapingBuffer? result = this.result; + if (workspace is null || result is null) { - this.workspace = new(options, ShapingBufferRole.Substitution); - this.result = new(options, ShapingBufferRole.Positioning); + workspace = new(options, ShapingBufferRole.Substitution); + result = new(options, ShapingBufferRole.Positioning); + this.workspace = workspace; + this.result = result; } else { - this.workspace!.Reset(options); - this.result!.Reset(options); + workspace.Reset(options); + result.Reset(options); // The single-run fast path flips the workspace to the positioning role in // place; a pooled scratch must hand out buffers in their home roles. - this.workspace.SetRole(ShapingBufferRole.Substitution); - this.result.SetRole(ShapingBufferRole.Positioning); + workspace.SetRole(ShapingBufferRole.Substitution); + result.SetRole(ShapingBufferRole.Positioning); } - return (this.workspace!, this.result!); + return (workspace, result); } } diff --git a/src/SixLabors.Fonts/TextBlock.cs b/src/SixLabors.Fonts/TextBlock.cs index b1c5a2a26..6b0a86755 100644 --- a/src/SixLabors.Fonts/TextBlock.cs +++ b/src/SixLabors.Fonts/TextBlock.cs @@ -58,8 +58,7 @@ public TextBlock(ReadOnlySpan text, TextOptions options) return; } - ShapedText shaped = TextShaper.ShapeText(text, options); - this.LogicalLine = TextLayout.ComposeLogicalLine(shaped, text, options); + this.LogicalLine = TextLayout.ComposeLogicalLine(text, options); } /// diff --git a/src/SixLabors.Fonts/TextLayout.LineBreaking.cs b/src/SixLabors.Fonts/TextLayout.LineBreaking.cs index e12b0286a..0db9c5cb3 100644 --- a/src/SixLabors.Fonts/TextLayout.LineBreaking.cs +++ b/src/SixLabors.Fonts/TextLayout.LineBreaking.cs @@ -15,6 +15,23 @@ internal static partial class TextLayout private const int StandardHyphen = 0x2010; private const int StandardEllipsis = 0x2026; + /// + /// Shapes the text and composes the logical before + /// width-dependent line breaking. Shaping runs on pooled pipeline state scoped + /// entirely to this call; composition copies everything the result retains. + /// + /// The source text. + /// The text shaping and layout options. + /// The logical text line and line break opportunities before line breaking. + public static LogicalTextLine ComposeLogicalLine(ReadOnlySpan text, TextOptions options) + { + // Composition retains run references in its layout data, so the runs are + // built per call rather than reused from pooled state. + IReadOnlyList runs = TextShaper.BuildTextRuns(text, options); + using TextShaper.ShapedTextScope scope = TextShaper.ShapeText(text, options, runs); + return ComposeLogicalLine(scope.Shaped, text, options); + } + /// /// Composes the logical from shaped glyph data before width-dependent line breaking. /// @@ -22,7 +39,7 @@ internal static partial class TextLayout /// The original source text. /// The text shaping and layout options. /// The logical text line and line break opportunities before line breaking. - public static LogicalTextLine ComposeLogicalLine( + private static LogicalTextLine ComposeLogicalLine( in ShapedText shapedText, ReadOnlySpan text, TextOptions options) diff --git a/src/SixLabors.Fonts/TextShaper.Pipeline.cs b/src/SixLabors.Fonts/TextShaper.Pipeline.cs index 7fb076a2f..1da7c9a56 100644 --- a/src/SixLabors.Fonts/TextShaper.Pipeline.cs +++ b/src/SixLabors.Fonts/TextShaper.Pipeline.cs @@ -112,52 +112,78 @@ internal static IReadOnlyList BuildTextRuns(ReadOnlySpan text, Te } /// - /// Shapes into shaping state that is independent of the wrapping length. + /// Materializes the fallback fonts for the pass. Lives apart from the pipeline + /// body so shaping without fallbacks never pays for the construction. + /// + /// The text options carrying the fallback families. + /// The fallback fonts. + private static Font[] BuildFallbackFonts(TextOptions options) + { + IReadOnlyList families = options.FallbackFontFamilies; + Font[] fonts = new Font[families.Count]; + for (int i = 0; i < fonts.Length; i++) + { + fonts[i] = new Font(families[i], options.Font.Size, options.Font.RequestedStyle); + } + + return fonts; + } + + /// + /// Shapes into a scope that owns the pooled pipeline + /// state backing the shaped views. /// - /// - /// Performs the font-run build, bidi analysis, GSUB/GPOS shaping (including fallback font - /// resolution for unmapped codepoints). The result contains the positioned glyph buffer - /// and bidi state used by logical line composition. - /// /// The text to process. /// The text options used while shaping. - /// The wrapping-independent shaping state. - internal static ShapedText ShapeText(ReadOnlySpan text, TextOptions options) + /// + /// The resolved text runs when the caller retains run references beyond the + /// scope, or to let the pass reuse pooled run state. + /// + /// The scoped shaping result. + internal static ShapedTextScope ShapeText( + ReadOnlySpan text, + TextOptions options, + IReadOnlyList? prebuiltRuns) { - // The single pooling site for the shaping pipeline: rent the reusable pipeline - // state, shape, copy the result out by value, and return the state before the - // caller sees the result. Every consumer of shaping goes through here and - // shares the pooled machinery without knowing it exists. ShapingScratch scratch = ScratchPool.Get(); try { - (ShapingBuffer substitutions, ShapingBuffer positionings) = scratch.Prepare(options); - return ShapeText(text, options, substitutions, positionings); + ShapingBuffer shaped = ShapeCore(text, options, scratch, prebuiltRuns); + return new(ProjectShapedText(shaped, options.LayoutMode, scratch), scratch); } - finally + catch { ScratchPool.Return(scratch); + throw; } } /// - /// Shapes using caller-supplied shaping buffers that - /// already reflect . + /// Runs the shaping pipeline through positioning and the post-positioning + /// passes, leaving the positioned glyph records in the returned buffer and the + /// bidi state in the scratch. The projections that copy results out for their + /// consumers sit on top. /// /// The text to process. /// The text options used while shaping. - /// The substitution buffer to shape into. - /// The positioning buffer to shape into. - /// The wrapping-independent shaping state. - private static ShapedText ShapeText( + /// The rented pipeline state. + /// + /// The resolved text runs when the caller retains run references beyond the + /// scratch scope, or to let the pass reuse scratch-owned + /// run state. + /// + /// The positioned buffer. + private static ShapingBuffer ShapeCore( ReadOnlySpan text, TextOptions options, - ShapingBuffer substitutions, - ShapingBuffer positionings) + ShapingScratch scratch, + IReadOnlyList? prebuiltRuns) { + (ShapingBuffer substitutions, ShapingBuffer positionings) = scratch.Prepare(options); + // Gather the font and fallbacks. Font[] fallbackFonts = (options.FallbackFontFamilies?.Count > 0) - ? [.. options.FallbackFontFamilies.Select(x => new Font(x, options.Font.Size, options.Font.RequestedStyle))] + ? BuildFallbackFonts(options) : []; LayoutMode layoutMode = options.LayoutMode; @@ -165,8 +191,8 @@ private static ShapedText ShapeText( var probe = ShapingProbe.Enter(); // Analyse the text for bidi directional runs. - BidiAlgorithm bidi = BidiAlgorithm.Instance.Value!; - BidiData bidiData = BidiData.Instance.Value!; + BidiAlgorithm bidi = scratch.BidiAlgorithm; + BidiData bidiData = scratch.BidiData; bidiData.Init(text, (sbyte)options.TextDirection); if (options.TextBidiMode == TextBidiMode.Override) @@ -203,28 +229,35 @@ or BidiCharacterType.FirstStrongIsolate // a left-to-right (or auto) paragraph direction, every resolved level is zero. // This is the overwhelmingly common case for Latin text and skips the full UAX#9 // pass. An overridden or right-to-left paragraph always resolves levels. - BidiRun[] bidiRuns; + scratch.ClearBidiRuns(); if (options.TextDirection != TextDirection.RightToLeft && options.TextBidiMode != TextBidiMode.Override && bidiData.IsUniformLeftToRight) { - bidiRuns = [new BidiRun(BidiCharacterType.LeftToRight, 0, 0, bidiData.Types.Length)]; + scratch.AddBidiRun(new BidiRun(BidiCharacterType.LeftToRight, 0, 0, bidiData.Types.Length)); } else { bidi.Process(bidiData); - bidiRuns = [.. BidiRun.CoalesceLevels(bidi.ResolvedLevels)]; + foreach (BidiRun run in BidiRun.CoalesceLevels(bidi.ResolvedLevels)) + { + scratch.AddBidiRun(in run); + } } - int[] bidiMap = new int[bidiData.Types.Length]; - Array.Fill(bidiMap, -1); + BidiRun[] bidiRuns = scratch.BidiRuns; + int[] bidiMap = scratch.GetBidiMap(bidiData.Types.Length); ShapingProbe.Exit(ShapingProbe.Bidi, probe); probe = ShapingProbe.Enter(); // Incrementally build out buffer of glyphs. Both buffers share the run list so - // per-glyph run indices agree when records are seeded across them. - IReadOnlyList textRuns = BuildTextRuns(text, options); + // per-glyph run indices agree when records are seeded across them. Callers + // retaining run references beyond the scratch scope supply their own runs; + // otherwise the synthesized whole-text run reuses scratch state. + IReadOnlyList textRuns = prebuiltRuns ?? ((options.TextRuns?.Count > 0) + ? BuildTextRuns(text, options) + : scratch.GetDefaultTextRuns(text.GetGraphemeCount(), options)); substitutions.SetTextRuns(textRuns); positionings.SetTextRuns(textRuns); ShapingProbe.Exit(ShapingProbe.BuildTextRuns, probe); @@ -384,15 +417,25 @@ or BidiCharacterType.FirstStrongIsolate ShapingProbe.Exit(ShapingProbe.Positioning, probe); - // Copy the shaped result out of the pooled collections: run-constant state - // deduplicates into a run table and per-glyph state splits into parallel - // identity and geometry arrays of pure values, so the scratch can go back to - // the pool before consumption and no metrics reference survives shaping. + return shaped; + } + + /// + /// Copies the shaped result out of the pooled collections: run-constant state + /// deduplicates into a run table and per-glyph state splits into parallel + /// identity and geometry arrays of pure values held by the scratch, so the + /// views stay valid exactly as long as the caller holds the scratch. + /// + /// The positioned buffer. + /// The layout mode used while shaping. + /// The rented pipeline state holding the bidi results and projection storage. + /// The wrapping-independent shaping state, valid within the scratch scope. + private static ShapedText ProjectShapedText(ShapingBuffer shaped, LayoutMode layoutMode, ShapingScratch scratch) + { uint verticalMask = ShapePlanFeatures.VerticalFeatureMask; int count = shaped.Count; - ShapedGlyphInfo[] infos = new ShapedGlyphInfo[count]; - ShapedGlyphPosition[] positions = new ShapedGlyphPosition[count]; - List runs = []; + (ShapedGlyphInfo[] infos, ShapedGlyphPosition[] positions) = scratch.GetProjection(count); + scratch.ClearRuns(); Font? runFont = null; int runTextRunIndex = -1; @@ -414,7 +457,7 @@ or BidiCharacterType.FirstStrongIsolate runFont = entry.Font; runTextRunIndex = shaping.TextRunIndex; runBidiRun = shapingBidiRun; - runs.Add(new(entry.Font, entry.PointSize, shaped.TextRuns[shaping.TextRunIndex], shapingBidiRun)); + scratch.AddRun(new(entry.Font, entry.PointSize, shaped.TextRuns[shaping.TextRunIndex], shapingBidiRun)); } ShapedGlyphFlags flags = ShapedGlyphFlags.None; @@ -443,7 +486,7 @@ or BidiCharacterType.FirstStrongIsolate shaping.CodePoint, shaping.CodePointCount, entry.Metrics.GlyphId, - (ushort)(runs.Count - 1), + (ushort)(scratch.RunCount - 1), flags); positions[i] = new( @@ -453,7 +496,7 @@ or BidiCharacterType.FirstStrongIsolate entry.Metrics.Offset); } - return new ShapedText([.. runs], infos, positions, bidiRuns, bidiMap, layoutMode); + return new ShapedText(scratch.Runs, infos, positions, count, scratch.BidiRuns, scratch.BidiMap, layoutMode); } /// @@ -510,7 +553,7 @@ private static void HideDefaultIgnorables(ShapingBuffer shaped) hasInvisible = font is not null && font.FontMetrics.TryGetGlyphId(space, out invisible); } - if (!hasInvisible) + if (font is null || !hasInvisible) { hasUnreplaceable = true; continue; @@ -519,7 +562,7 @@ private static void HideDefaultIgnorables(ShapingBuffer shaped) // The projection reads the glyph id and default advance from the // metrics entry, so the invisible glyph's metrics replace it. TextRun textRun = shaped.TextRuns[data.TextRunIndex]; - entry.Metrics = font!.FontMetrics.GetGlyphMetrics(space, invisible, textRun.TextAttributes, textRun.TextDecorations, layoutMode, colorFontSupport); + entry.Metrics = font.FontMetrics.GetGlyphMetrics(space, invisible, textRun.TextAttributes, textRun.TextDecorations, layoutMode, colorFontSupport); shaped.SetGlyphId(i, invisible); data.IsHidden = true; } @@ -762,6 +805,40 @@ private static void SubstituteBidiMirrors(FontMetrics fontMetrics, ShapingBuffer } } + /// + /// A shaping result scoped to the pooled pipeline state backing its views. + /// Disposal returns the state to the pool and ends the views' validity, so + /// consumers copy what they retain before the scope closes. + /// + internal readonly ref struct ShapedTextScope + { + /// + /// The pooled pipeline state owned by the scope. + /// + private readonly ShapingScratch scratch; + + /// + /// Initializes a new instance of the struct. + /// + /// The shaped views. + /// The pooled pipeline state backing the views. + public ShapedTextScope(ShapedText shaped, ShapingScratch scratch) + { + this.Shaped = shaped; + this.scratch = scratch; + } + + /// + /// Gets the wrapping-independent shaping state, valid until disposal. + /// + public ShapedText Shaped { get; } + + /// + /// Returns the pooled pipeline state, ending the views' validity. + /// + public void Dispose() => ScratchPool.Return(this.scratch); + } + /// /// The pooling policy for instances: scratch state is /// reset on acquisition by , so returned diff --git a/src/SixLabors.Fonts/TextShaper.cs b/src/SixLabors.Fonts/TextShaper.cs index e7f5d94bf..ba652ec8d 100644 --- a/src/SixLabors.Fonts/TextShaper.cs +++ b/src/SixLabors.Fonts/TextShaper.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; + namespace SixLabors.Fonts; /// @@ -59,36 +61,77 @@ public static IReadOnlyList Shape(ReadOnlySpan text, TextOpti return []; } - ShapedText shaped = ShapeText(text, options); + TextShapingBuffer buffer = new(); + Shape(text, options, buffer); + return buffer.Glyphs.ToArray(); + } + + /// + public static void Shape(string text, TextOptions options, TextShapingBuffer buffer) + { + Guard.NotNull(text, nameof(text)); + + Shape(text.AsSpan(), options, buffer); + } + + /// + /// Shapes the text into a positioned glyph stream, replacing the contents of the + /// supplied buffer. Reusing one buffer across calls keeps steady-state shaping + /// free of allocation; see + /// for the shaping semantics and honored options. + /// + /// The text to shape. + /// The text options. + /// The buffer receiving the shaped glyphs in logical order. + public static void Shape(ReadOnlySpan text, TextOptions options, TextShapingBuffer buffer) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(buffer, nameof(buffer)); + + if (text.IsEmpty) + { + buffer.Clear(); + return; + } - var probe = ShapingProbe.Enter(); - ShapedGlyphInfo[] infos = shaped.Infos; - ShapedGlyphPosition[] positions = shaped.Positions; - ShapedTextRun[] runs = shaped.Runs; - List glyphs = new(infos.Length); - for (int i = 0; i < infos.Length; i++) + ShapingScratch scratch = ScratchPool.Get(); + try { - ref readonly ShapedGlyphInfo info = ref infos[i]; - if (info.IsPlaceholder) + ShapingBuffer shaped = ShapeCore(text, options, scratch, null); + + var probe = ShapingProbe.Enter(); + int count = shaped.Count; + Span destination = buffer.Reserve(count); + int written = 0; + for (int i = 0; i < count; i++) { - // Placeholder runs reserve layout space for inline objects; they carry - // no glyph. - continue; + ref GlyphShapingData shaping = ref shaped[i]; + if (shaping.IsPlaceholder) + { + // Placeholder runs reserve layout space for inline objects; they + // carry no glyph. + continue; + } + + ref ShapingBuffer.GlyphMetricsEntry entry = ref shaped.MetricsAt(i); + ref GlyphShapingPosition position = ref shaped.PositionAt(i); + destination[written++] = new ShapedGlyph( + entry.Font, + entry.Metrics.GlyphId, + shaping.CodePoint, + shaping.CodePointIndex, + shaping.CodePointCount, + entry.GetAdvanceWidth(in position), + entry.GetAdvanceHeight(in position), + new Vector2(position.Bounds.X, position.Bounds.Y) + entry.Metrics.Offset); } - ref readonly ShapedGlyphPosition position = ref positions[i]; - glyphs.Add(new ShapedGlyph( - runs[info.RunIndex].Font, - info.GlyphId, - info.CodePoint, - info.CodePointIndex, - info.CodePointCount, - position.AdvanceWidth, - position.AdvanceHeight, - position.Bearing + position.Offset)); + buffer.Commit(written); + ShapingProbe.Exit(ShapingProbe.Projection, probe); + } + finally + { + ScratchPool.Return(scratch); } - - ShapingProbe.Exit(ShapingProbe.Projection, probe); - return glyphs; } } diff --git a/src/SixLabors.Fonts/TextShapingBuffer.cs b/src/SixLabors.Fonts/TextShapingBuffer.cs new file mode 100644 index 000000000..08081e103 --- /dev/null +++ b/src/SixLabors.Fonts/TextShapingBuffer.cs @@ -0,0 +1,74 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts; + +/// +/// A reusable, caller-owned buffer that receives the glyph stream of a shaping call. +/// Each call replaces the contents; storage grows to its high-water mark and is +/// retained, so repeated shaping through one instance does not allocate. +/// +/// +/// An instance is not thread safe: use one buffer per shaping thread and reuse it +/// across calls. +/// +public sealed class TextShapingBuffer +{ + /// + /// The flat glyph storage. Only the first records are live; + /// capacity beyond the count is retained scratch. + /// + private ShapedGlyph[] glyphs = []; + + /// + /// Gets the number of shaped glyphs the last shaping call produced. + /// + public int Count { get; private set; } + + /// + /// Gets the shaped glyphs in logical order. + /// + public ReadOnlySpan Glyphs => this.glyphs.AsSpan(0, this.Count); + + /// + /// Gets a read-only reference to the shaped glyph at the given index. + /// + /// The zero-based glyph index. + public ref readonly ShapedGlyph this[int index] + { + get + { + Guard.MustBeBetweenOrEqualTo(index, 0, this.Count - 1, nameof(index)); + return ref this.glyphs[index]; + } + } + + /// + /// Removes all glyphs while retaining the storage. + /// + public void Clear() => this.Count = 0; + + /// + /// Begins replacing the contents: empties the buffer, ensures capacity for the + /// given record count, and returns the writable storage. The written records + /// become visible when publishes their count. + /// + /// The record capacity to reserve. + /// The writable storage span. + internal Span Reserve(int capacity) + { + this.Count = 0; + if (this.glyphs.Length < capacity) + { + this.glyphs = new ShapedGlyph[Math.Max(capacity, Math.Max(64, this.glyphs.Length * 2))]; + } + + return this.glyphs.AsSpan(0, capacity); + } + + /// + /// Publishes the number of records written to the reserved storage. + /// + /// The record count. + internal void Commit(int count) => this.Count = count; +} diff --git a/src/SixLabors.Fonts/Unicode/BidiAlgorithm.cs b/src/SixLabors.Fonts/Unicode/BidiAlgorithm.cs index 832543fc5..a807f2821 100644 --- a/src/SixLabors.Fonts/Unicode/BidiAlgorithm.cs +++ b/src/SixLabors.Fonts/Unicode/BidiAlgorithm.cs @@ -178,12 +178,6 @@ public BidiAlgorithm() { } - /// - /// Gets a per-thread instance that can be re-used as often - /// as necessary. - /// - public static ThreadLocal Instance { get; } = new ThreadLocal(() => new BidiAlgorithm()); - /// /// Gets the resolved levels. /// diff --git a/src/SixLabors.Fonts/Unicode/BidiData.cs b/src/SixLabors.Fonts/Unicode/BidiData.cs index ea1eebf11..357145649 100644 --- a/src/SixLabors.Fonts/Unicode/BidiData.cs +++ b/src/SixLabors.Fonts/Unicode/BidiData.cs @@ -17,14 +17,6 @@ internal partial class BidiData private ArrayBuilder tempLevelBuffer; private readonly List paragraphPositions = new(); - /// - /// Gets a reusable per-thread instance. The internal builders grow to the - /// workload's high-water mark and resets them, so reuse makes - /// steady-state analysis allocation free, mirroring the lifetime of the - /// per-thread algorithm instance that consumes this data. - /// - public static ThreadLocal Instance { get; } = new(() => new BidiData()); - public sbyte ParagraphEmbeddingLevel { get; private set; } public bool HasBrackets { get; private set; } diff --git a/src/SixLabors.Fonts/Unicode/MemoryExtensions.cs b/src/SixLabors.Fonts/Unicode/MemoryExtensions.cs index 502b13f1d..bae33497c 100644 --- a/src/SixLabors.Fonts/Unicode/MemoryExtensions.cs +++ b/src/SixLabors.Fonts/Unicode/MemoryExtensions.cs @@ -227,7 +227,7 @@ public static int GetGraphemeCount(this ReadOnlySpan span) // character extends, prepends, or joins, and controls break on both sides. // The count is therefore the length minus the CR LF pairs, found with two // vectorized scans instead of the boundary state machine. - if (!span.ContainsAnyExceptInRange((char)0, (char)0x7F)) + if (System.Text.Ascii.IsValid(span)) { int pairs = 0; int searchStart = 0; diff --git a/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/ShapeTextBenchmark.cs b/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/ShapeTextBenchmark.cs index cebaf59f2..83a57ac31 100644 --- a/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/ShapeTextBenchmark.cs +++ b/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/ShapeTextBenchmark.cs @@ -47,6 +47,7 @@ public class ShapeTextBenchmark : IDisposable { private string text = string.Empty; private TextOptions textOptions = null!; + private TextShapingBuffer shapingBuffer = null!; private Blob? blob; private HBFace? face; private HBFont? hbFont; @@ -83,6 +84,7 @@ public void SetUp() Font font = new FontCollection().Add(fontPath).CreateFont(16); this.textOptions = new TextOptions(font); + this.shapingBuffer = new TextShapingBuffer(); this.blob = Blob.FromFile(fontPath); this.face = new HBFace(this.blob, 0); @@ -92,16 +94,18 @@ public void SetUp() } /// - /// Shapes the text with and sums the resulting advances. + /// Shapes the text with , reusing one buffer as production + /// text stacks do, and sums the resulting advances. /// /// The advance sum, returned so the shaped stream is fully consumed. [Benchmark] public int ShapeSixLaborsFonts() { - IReadOnlyList glyphs = TextShaper.Shape(this.text, this.textOptions); + TextShaper.Shape(this.text, this.textOptions, this.shapingBuffer); + ReadOnlySpan glyphs = this.shapingBuffer.Glyphs; int advanceSum = 0; - for (int i = 0; i < glyphs.Count; i++) + for (int i = 0; i < glyphs.Length; i++) { advanceSum += glyphs[i].AdvanceWidth; } diff --git a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs index 942176810..9e0195b13 100644 --- a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs +++ b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs @@ -1006,8 +1006,7 @@ public void TextPlaceholder_SharesInsertionCodePointOffset() ] }; - ShapedText shapedText = TextShaper.ShapeText(text.AsSpan(), options); - LogicalTextLine logicalLine = TextLayout.ComposeLogicalLine(shapedText, text.AsSpan(), options); + LogicalTextLine logicalLine = TextLayout.ComposeLogicalLine(text.AsSpan(), options); GlyphLayoutData placeholder = default; GlyphLayoutData following = default; diff --git a/tests/SixLabors.Fonts.Tests/TextShaperTests.cs b/tests/SixLabors.Fonts.Tests/TextShaperTests.cs index f9dc9e6bc..bce7b4c74 100644 --- a/tests/SixLabors.Fonts.Tests/TextShaperTests.cs +++ b/tests/SixLabors.Fonts.Tests/TextShaperTests.cs @@ -211,4 +211,85 @@ public void Shape_Culture_MatchesHarfBuzzLanguageTagExpectations(string? culture Assert.Single(glyphs); Assert.Equal(expectedGlyphId, glyphs[0].GlyphId); } + + [Fact] + public void Shape_ReusedBuffer_MatchesFreshShaping() + { + // One shared buffer across interleaved scripts and repeated rounds must + // produce records identical to the allocating overload every time, with + // each call fully replacing the previous contents. + Font latin = new FontCollection().Add(TestFonts.OpenSansFile).CreateFont(72); + Font arabic = new FontCollection().Add(TestFonts.ArabicFontFile).CreateFont(72); + Font devanagari = new FontCollection().Add(TestFonts.NotoSansDevanagariRegular).CreateFont(72); + + (Font Font, string Text)[] cases = + [ + (latin, "The quick brown fox; fifty fluffy waffles."), + (arabic, "ุณู„ุงู… ุนู„ูŠูƒู… ูˆุฑุญู…ุฉ ุงู„ู„ู‡"), + (devanagari, "เค•เฅเคทเคคเฅเคฐเคฟเคฏ เคฆเฅเคตเคพเคฐเคพ เคชเฅเคฐเค•เฅƒเคคเคฟ เค•เฅ€ เคฐเค•เฅเคทเคพ"), + (latin, "a"), + ]; + + TextShapingBuffer buffer = new(); + for (int round = 0; round < 3; round++) + { + foreach ((Font font, string text) in cases) + { + TextOptions options = new(font); + IReadOnlyList expected = TextShaper.Shape(text, options); + TextShaper.Shape(text, options, buffer); + + Assert.Equal(expected.Count, buffer.Count); + for (int i = 0; i < expected.Count; i++) + { + ShapedGlyph expectedGlyph = expected[i]; + ShapedGlyph actual = buffer[i]; + Assert.Same(expectedGlyph.Font, actual.Font); + Assert.Equal(expectedGlyph.GlyphId, actual.GlyphId); + Assert.Equal(expectedGlyph.CodePoint, actual.CodePoint); + Assert.Equal(expectedGlyph.CodePointIndex, actual.CodePointIndex); + Assert.Equal(expectedGlyph.CodePointCount, actual.CodePointCount); + Assert.Equal(expectedGlyph.AdvanceWidth, actual.AdvanceWidth); + Assert.Equal(expectedGlyph.AdvanceHeight, actual.AdvanceHeight); + Assert.Equal(expectedGlyph.Offset, actual.Offset); + } + } + } + } + + [Fact] + public void Shape_EmptyTextIntoBuffer_ClearsPreviousContents() + { + Font font = new FontCollection().Add(TestFonts.OpenSansFile).CreateFont(72); + TextShapingBuffer buffer = new(); + + TextShaper.Shape("Hxp", new TextOptions(font), buffer); + Assert.Equal(3, buffer.Count); + + TextShaper.Shape(string.Empty, new TextOptions(font), buffer); + Assert.Equal(0, buffer.Count); + Assert.True(buffer.Glyphs.IsEmpty); + } + + [Fact] + public void Shape_ReusedBuffer_SteadyStateDoesNotAllocate() + { + // After a warm-up call has grown every pooled structure to its high-water + // mark, repeated shaping through the same buffer must allocate nothing. + Font font = new FontCollection().Add(TestFonts.OpenSansFile).CreateFont(72); + TextOptions options = new(font); + const string text = "The quick brown fox; fifty fluffy waffles."; + TextShapingBuffer buffer = new(); + + for (int i = 0; i < 16; i++) + { + TextShaper.Shape(text, options, buffer); + } + + long before = GC.GetAllocatedBytesForCurrentThread(); + TextShaper.Shape(text, options, buffer); + long allocated = GC.GetAllocatedBytesForCurrentThread() - before; + + Assert.Equal(0, allocated); + } } From 3ae5fa483911545e5efbbecdd4cc67ecf8484dd9 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 26 Jul 2026 01:02:32 +1000 Subject: [PATCH 62/94] Eliminate the remaining per-call shaping allocations Three allocators found by ETW allocation profiling, each killed at its source: The bidi run coalescing used an iterator method whose state machine allocated per pass on every non-uniform-direction text; the trivial levels walk is now inlined into the pipeline and the orphaned iterator is deleted. The Arabic joining state machine allocated a per-call actions array sized to the glyph count; it now uses buffer-hosted scratch storage grown to the high-water mark, with every slot written before read. The Indic, Myanmar, and Universal shapers passed instance method groups as pause callbacks during feature planning, allocating a fresh delegate per conversion per pass - three per Devanagari shape. The conversions now happen once in each shaper's constructor, matching the shapers' own instance-cached lifetime. Steady-state shaping through the caller-owned buffer now allocates nothing on all three benchmark scripts, proven over 200,000 calls per script and enforced by the acceptance test, which becomes a three-script theory. Benchmark (clean window, controls 2.26/3.79/8.77 us at idle): | Scenario | Before | After | |------------|---------------------------|--------------------------| | Latin | 3.91 us / 1.69x / 0 B | 3.72 us / 1.65x / 0 B | | Arabic | 6.74 us / 1.74x / 152 B | 6.49 us / 1.71x / 0 B | | Devanagari | 10.69 us / 1.19x / 192 B | 10.29 us / 1.17x / 0 B | Suite 5,596/0 -> 5,598/0. --- src/SixLabors.Fonts/ShapingBuffer.cs | 23 ++++++++++++++ .../Shapers/ArabicShaper.cs | 4 +-- .../Shapers/IndicShaper.cs | 27 +++++++++++++++-- .../Shapers/MyanmarShaper.cs | 18 +++++++++-- .../Shapers/UniversalShaper.cs | 22 ++++++++++++-- src/SixLabors.Fonts/TextShaper.Pipeline.cs | 22 ++++++++++++-- src/SixLabors.Fonts/Unicode/BidiRun.cs | 30 ------------------- .../SixLabors.Fonts.Tests/TextShaperTests.cs | 19 ++++++++---- 8 files changed, 118 insertions(+), 47 deletions(-) diff --git a/src/SixLabors.Fonts/ShapingBuffer.cs b/src/SixLabors.Fonts/ShapingBuffer.cs index 7cda2327b..4647c6d40 100644 --- a/src/SixLabors.Fonts/ShapingBuffer.cs +++ b/src/SixLabors.Fonts/ShapingBuffer.cs @@ -46,6 +46,12 @@ internal sealed class ShapingBuffer /// private GlyphShapingPosition[] positions = new GlyphShapingPosition[64]; + /// + /// Shaper scratch storage handed out by , grown + /// to the workload's high-water mark. Contents are undefined between passes. + /// + private byte[] shaperScratch = []; + /// /// The approximate membership filter over every glyph id the buffer has ever /// contained. See for the growth contract. @@ -431,6 +437,23 @@ public void Clear() this.SegmentPlans.Clear(); } + /// + /// Gets shaper scratch storage of at least the given length, grown to the + /// workload's high-water mark and retained. Contents are undefined on entry; + /// callers write every slot they read. + /// + /// The capacity required. + /// The scratch storage; entries beyond the length are undefined. + public byte[] GetShaperScratch(int length) + { + if (this.shaperScratch.Length < length) + { + this.shaperScratch = new byte[Math.Max(length, Math.Max(64, this.shaperScratch.Length * 2))]; + } + + return this.shaperScratch; + } + /// /// Sets the glyph id at the specified index, recording the id in /// . Callers outside the buffer must use this rather than diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs index 7a3871b7a..22efd7612 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs @@ -165,7 +165,7 @@ protected override void AssignFeatures(ShapingBuffer buffer, int index, int coun int prev = -1; int state = 0; - byte[] actions = new byte[count]; + byte[] actions = buffer.GetShaperScratch(count); // Apply the state machine to map glyphs to features. for (int i = 0; i < count; i++) @@ -195,7 +195,7 @@ protected override void AssignFeatures(ShapingBuffer buffer, int index, int coun } // Apply the chosen features to their respective glyphs. - for (int i = 0; i < actions.Length; i++) + for (int i = 0; i < count; i++) { switch (actions[i]) { diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs index df47df033..a4857258c 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs @@ -181,6 +181,24 @@ internal sealed class IndicShaper : DefaultShaper /// private bool hasBrokenClusters; + /// + /// The syllable setup pause, converted to a delegate once so per-pass feature + /// planning never allocates for the conversion. + /// + private readonly Action setupSyllablesAction; + + /// + /// The initial reorder pause, converted to a delegate once so per-pass feature + /// planning never allocates for the conversion. + /// + private readonly Action initialReorderAction; + + /// + /// The final reorder pause, converted to a delegate once so per-pass feature + /// planning never allocates for the conversion. + /// + private readonly Action finalReorderAction; + /// /// Initializes a new instance of the class. /// @@ -192,6 +210,9 @@ public IndicShaper(ScriptClass script, Tag unicodeScriptTag, TextOptions textOpt : base(script, MarkZeroingMode.None, textOptions) { this.fontMetrics = fontMetrics; + this.setupSyllablesAction = this.SetupSyllables; + this.initialReorderAction = this.InitialReorder; + this.finalReorderAction = this.FinalReorder; if (IndicConfigurations.TryGetValue(script, out ShapingConfiguration value)) { @@ -209,10 +230,10 @@ public IndicShaper(ScriptClass script, Tag unicodeScriptTag, TextOptions textOpt /// protected override void PlanFeatures(ShapingBuffer buffer, int index, int count) { - this.EnableFeature(buffer, index, count, LoclTag, this.SetupSyllables, null); + this.EnableFeature(buffer, index, count, LoclTag, this.setupSyllablesAction, null); this.EnableFeature(buffer, index, count, CcmpTag); - this.EnableFeature(buffer, index, count, NuktTag, this.InitialReorder, null); + this.EnableFeature(buffer, index, count, NuktTag, this.initialReorderAction, null); this.EnableFeature(buffer, index, count, AkhnTag); this.AddFeature(buffer, index, count, RphfTag, false); @@ -224,7 +245,7 @@ protected override void PlanFeatures(ShapingBuffer buffer, int index, int count) this.AddFeature(buffer, index, count, PstfTag, false); this.EnableFeature(buffer, index, count, VatuTag); this.EnableFeature(buffer, index, count, CjctTag); - this.AddFeature(buffer, index, count, CfarTag, false, null, this.FinalReorder); + this.AddFeature(buffer, index, count, CfarTag, false, null, this.finalReorderAction); this.AddFeature(buffer, index, count, InitTag, false); this.EnableFeature(buffer, index, count, PresTag); diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs index a87aba923..36a2adeb0 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs @@ -106,6 +106,18 @@ internal sealed class MyanmarShaper : DefaultShaper /// private bool hasBrokenClusters; + /// + /// The syllable setup pause, converted to a delegate once so per-pass feature + /// planning never allocates for the conversion. + /// + private readonly Action setupSyllablesAction; + + /// + /// The initial reorder pause, converted to a delegate once so per-pass feature + /// planning never allocates for the conversion. + /// + private readonly Action initialReorderAction; + /// /// Initializes a new instance of the class. /// @@ -117,15 +129,17 @@ public MyanmarShaper(ScriptClass script, TextOptions textOptions, FontMetrics fo { this.textOptions = textOptions; this.fontMetrics = fontMetrics; + this.setupSyllablesAction = this.SetupSyllables; + this.initialReorderAction = this.InitialReorder; } /// protected override void PlanFeatures(ShapingBuffer buffer, int index, int count) { - this.EnableFeature(buffer, index, count, LoclTag, this.SetupSyllables, null); + this.EnableFeature(buffer, index, count, LoclTag, this.setupSyllablesAction, null); this.EnableFeature(buffer, index, count, CcmpTag); - this.EnableFeature(buffer, index, count, RphfTag, this.InitialReorder, null); + this.EnableFeature(buffer, index, count, RphfTag, this.initialReorderAction, null); this.EnableFeature(buffer, index, count, PrefTag); this.EnableFeature(buffer, index, count, BlwfTag); this.EnableFeature(buffer, index, count, PstfTag); diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs index 678991c2a..607e28933 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs @@ -160,6 +160,18 @@ internal sealed class UniversalShaper : DefaultShaper /// private bool hasBrokenClusters; + /// + /// The syllable setup pause, converted to a delegate once so per-pass feature + /// planning never allocates for the conversion. + /// + private readonly Action setupSyllablesAction; + + /// + /// The reorder pause, converted to a delegate once so per-pass feature + /// planning never allocates for the conversion. + /// + private readonly Action reorderAction; + /// /// Initializes a new instance of the class. /// @@ -168,13 +180,17 @@ internal sealed class UniversalShaper : DefaultShaper /// The font metrics for glyph lookups. public UniversalShaper(ScriptClass script, TextOptions textOptions, FontMetrics fontMetrics) : base(script, MarkZeroingMode.PreGPos, textOptions) - => this.fontMetrics = fontMetrics; + { + this.fontMetrics = fontMetrics; + this.setupSyllablesAction = this.SetupSyllables; + this.reorderAction = this.Reorder; + } /// protected override void PlanFeatures(ShapingBuffer buffer, int index, int count) { // Default glyph pre-processing group - this.EnableFeature(buffer, index, count, LoclTag, this.SetupSyllables, null); + this.EnableFeature(buffer, index, count, LoclTag, this.setupSyllablesAction, null); this.EnableFeature(buffer, index, count, CcmpTag); this.EnableFeature(buffer, index, count, NuktTag); this.EnableFeature(buffer, index, count, AkhnTag); @@ -192,7 +208,7 @@ protected override void PlanFeatures(ShapingBuffer buffer, int index, int count) this.EnableFeature(buffer, index, count, HalfTag); this.EnableFeature(buffer, index, count, PstfTag); this.EnableFeature(buffer, index, count, VatuTag); - this.EnableFeature(buffer, index, count, CjctTag, null, this.Reorder); + this.EnableFeature(buffer, index, count, CjctTag, null, this.reorderAction); // Standard topographic presentation and positional feature application this.EnableFeature(buffer, index, count, AbvsTag); diff --git a/src/SixLabors.Fonts/TextShaper.Pipeline.cs b/src/SixLabors.Fonts/TextShaper.Pipeline.cs index 1da7c9a56..fe0a29665 100644 --- a/src/SixLabors.Fonts/TextShaper.Pipeline.cs +++ b/src/SixLabors.Fonts/TextShaper.Pipeline.cs @@ -239,9 +239,27 @@ or BidiCharacterType.FirstStrongIsolate else { bidi.Process(bidiData); - foreach (BidiRun run in BidiRun.CoalesceLevels(bidi.ResolvedLevels)) + + // Coalesce equal-level neighbours into runs inline: the levels walk is + // trivial and an enumerator here would be the pass's only allocation. + ArraySlice levels = bidi.ResolvedLevels; + if (levels.Length > 0) { - scratch.AddBidiRun(in run); + int startRun = 0; + sbyte runLevel = levels[0]; + for (int i = 1; i < levels.Length; i++) + { + if (levels[i] == runLevel) + { + continue; + } + + scratch.AddBidiRun(new BidiRun((runLevel & 0x01) == 0 ? BidiCharacterType.LeftToRight : BidiCharacterType.RightToLeft, runLevel, startRun, i - startRun)); + startRun = i; + runLevel = levels[i]; + } + + scratch.AddBidiRun(new BidiRun((runLevel & 0x01) == 0 ? BidiCharacterType.LeftToRight : BidiCharacterType.RightToLeft, runLevel, startRun, levels.Length - startRun)); } } diff --git a/src/SixLabors.Fonts/Unicode/BidiRun.cs b/src/SixLabors.Fonts/Unicode/BidiRun.cs index 03a8377d1..4a81c9967 100644 --- a/src/SixLabors.Fonts/Unicode/BidiRun.cs +++ b/src/SixLabors.Fonts/Unicode/BidiRun.cs @@ -29,36 +29,6 @@ public BidiRun(BidiCharacterType direction, int level, int start, int length) public override string ToString() => $"{this.Start} - {this.End} - {this.Direction}"; - public static IEnumerable CoalesceLevels(ReadOnlyArraySlice levels) - { - if (levels.Length == 0) - { - yield break; - } - - int startRun = 0; - sbyte runLevel = levels[0]; - BidiCharacterType direction; - for (int i = 1; i < levels.Length; i++) - { - if (levels[i] == runLevel) - { - continue; - } - - // End of this run - direction = (runLevel & 0x01) == 0 ? BidiCharacterType.LeftToRight : BidiCharacterType.RightToLeft; - yield return new BidiRun(direction, runLevel, startRun, i - startRun); - - // Move to next run - startRun = i; - runLevel = levels[i]; - } - - direction = (runLevel & 0x01) == 0 ? BidiCharacterType.LeftToRight : BidiCharacterType.RightToLeft; - yield return new BidiRun(direction, runLevel, startRun, levels.Length - startRun); - } - public override bool Equals(object? obj) => obj is BidiRun run && this.Equals(run); diff --git a/tests/SixLabors.Fonts.Tests/TextShaperTests.cs b/tests/SixLabors.Fonts.Tests/TextShaperTests.cs index bce7b4c74..7cf1b3700 100644 --- a/tests/SixLabors.Fonts.Tests/TextShaperTests.cs +++ b/tests/SixLabors.Fonts.Tests/TextShaperTests.cs @@ -271,14 +271,23 @@ public void Shape_EmptyTextIntoBuffer_ClearsPreviousContents() Assert.True(buffer.Glyphs.IsEmpty); } - [Fact] - public void Shape_ReusedBuffer_SteadyStateDoesNotAllocate() + [Theory] + [InlineData("latin")] + [InlineData("arabic")] + [InlineData("devanagari")] + public void Shape_ReusedBuffer_SteadyStateDoesNotAllocate(string scenario) { - // After a warm-up call has grown every pooled structure to its high-water + // After warm-up calls have grown every pooled structure to its high-water // mark, repeated shaping through the same buffer must allocate nothing. - Font font = new FontCollection().Add(TestFonts.OpenSansFile).CreateFont(72); + (string fontFile, string text) = scenario switch + { + "arabic" => (TestFonts.ArabicFontFile, "ุณู„ุงู… ุนู„ูŠูƒู… ูˆุฑุญู…ุฉ ุงู„ู„ู‡ ูˆุจุฑูƒุงุชู‡ ู„ุง ุฅู„ู‡ ุฅู„ุง ุงู„ู„ู‡"), + "devanagari" => (TestFonts.NotoSansDevanagariRegular, "เค•เฅเคทเคคเฅเคฐเคฟเคฏ เคฆเฅเคตเคพเคฐเคพ เคชเฅเคฐเค•เฅƒเคคเคฟ เค•เฅ€ เคฐเค•เฅเคทเคพ เค•เคฐเฅเคคเคตเฅเคฏ เคนเฅˆ"), + _ => (TestFonts.OpenSansFile, "The quick brown fox; fifty fluffy waffles."), + }; + + Font font = new FontCollection().Add(fontFile).CreateFont(72); TextOptions options = new(font); - const string text = "The quick brown fox; fifty fluffy waffles."; TextShapingBuffer buffer = new(); for (int i = 0; i < 16; i++) From fc9b72525e28816a3b4d761735c580dfa03a3eff Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 26 Jul 2026 12:11:27 +1000 Subject: [PATCH 63/94] Resolve joiner transparency inside sequence matching Sequence matching previously treated lookup-ignored glyphs as the only skippable class, so zero width joiners and other default ignorables broke ligature and contextual matches they should pass through. Matching now resolves each candidate against the applying lookup's match state: records failing the lookup mask or the latched syllable are refused, and default ignorables are transparent unless the lookup demands them, with ZWNJ, ZWJ, and hidden-ignorable gates derived from how each feature was registered by its shaper. Duplicate registrations of a lookup OR their masks and AND their joiner gates. The match cores walk test-then-skip and report where the matched input ends, so lookahead resumes at the true match end instead of a count-based jump past skipped glyphs. When the stamped matcher can see no transparent record the cores keep a solid-glyph walk with the original shape. Joiner and hidden classes are assigned once at buffer entry alongside the existing ignorable classification, and every codepoint now enters the buffer; unmapped ignorables ride as notdef until the hiding pass claims them. GPOS interior context matches previously ran under an all-ones mask; the applying lookup's mask now rides the buffer alongside the packed matcher flag bytes, all stamped once per lookup, so positioning gates interior and nested matches exactly as substitution does while the positioning dispatch keeps its original signatures. Emoji ZWJ sequences, joiner-in-ligature, and half-form differential rows against HarfBuzzSharp all match; three new oracle rows pin the emoji forms (ids and advances). Benchmark (15 iterations, clean windows, controls at idle). The .NET 8 regression is accepted as the cost of correct joiner semantics: matching now pays the reference's own per-record mask and syllable gates. .NET 10 absorbs the cost and beats the pre-leg baseline on every row. | Scenario | Before (.NET 8) | After (.NET 8) | After (.NET 10) | |------------|-----------------------|-----------------------|-----------------------| | Latin | 3.68 us / 1.63x / 0 B | 4.01 us / 1.78x / 0 B | 3.54 us / 1.58x / 0 B | | Arabic | 6.42 us / 1.69x / 0 B | 6.92 us / 1.78x / 0 B | 6.06 us / 1.59x / 0 B | | Devanagari | 10.54 us / 1.21x / 0 B| 11.43 us / 1.28x / 0 B| 10.37 us / 1.17x / 0 B| Suite 5,598/0 -> 5,603/0. --- src/SixLabors.Fonts/GlyphShapingData.cs | 52 ++ src/SixLabors.Fonts/ShapingBuffer.cs | 115 ++++- .../AdvancedTypographicUtils.cs | 458 ++++++++++++++---- .../GPos/LookupType7SubTable.cs | 6 +- .../GPos/LookupType8SubTable.cs | 6 +- .../Tables/AdvancedTypographic/GPosTable.cs | 5 +- .../GSub/LookupType5SubTable.cs | 6 +- .../GSub/LookupType6SubTable.cs | 7 +- .../Tables/AdvancedTypographic/GSubTable.cs | 5 +- .../Tables/AdvancedTypographic/ShapePlan.cs | 20 +- .../AdvancedTypographic/ShapePlanFeatures.cs | 48 ++ .../ShapePlanStageGroup{TLookup}.cs | 6 +- .../Shapers/ArabicShaper.cs | 29 +- .../Shapers/DefaultShaper.cs | 66 ++- .../Shapers/IndicShaper.cs | 46 +- .../Shapers/MyanmarShaper.cs | 24 +- .../Shapers/UniversalShaper.cs | 34 +- .../ShapingFeatureFlags.cs | 41 ++ .../SkippingGlyphIterator.cs | 211 +++++++- src/SixLabors.Fonts/TextShaper.Pipeline.cs | 20 +- .../HarfBuzzDifferentialTests.cs | 7 + .../SixLabors.Fonts.Tests/TextShaperTests.cs | 20 +- 22 files changed, 1027 insertions(+), 205 deletions(-) create mode 100644 src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapingFeatureFlags.cs diff --git a/src/SixLabors.Fonts/GlyphShapingData.cs b/src/SixLabors.Fonts/GlyphShapingData.cs index ea2e81fe9..0ea2476d5 100644 --- a/src/SixLabors.Fonts/GlyphShapingData.cs +++ b/src/SixLabors.Fonts/GlyphShapingData.cs @@ -55,6 +55,21 @@ internal struct GlyphShapingData /// private const ushort HiddenFlag = 1 << 5; + /// + /// The bit recording . + /// + private const ushort ZwnjFlag = 1 << 7; + + /// + /// The bit recording . + /// + private const ushort ZwjFlag = 1 << 8; + + /// + /// The bit recording . + /// + private const ushort HiddenIgnorableFlag = 1 << 9; + /// /// The bit recording that /// holds the glyph id was computed for. A default @@ -137,6 +152,9 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) this.IsPlaceholder = data.IsPlaceholder; this.IsDefaultIgnorable = data.IsDefaultIgnorable; this.IsHidden = data.IsHidden; + this.IsZwnj = data.IsZwnj; + this.IsZwj = data.IsZwj; + this.IsHiddenIgnorable = data.IsHiddenIgnorable; this.Syllable = data.Syllable; @@ -339,6 +357,40 @@ public bool IsHidden set => this.flags = value ? (ushort)(this.flags | HiddenFlag) : (ushort)(this.flags & ~HiddenFlag); } + /// + /// Gets or sets a value indicating whether the codepoint is the zero width + /// non-joiner. Classified once as the record enters the buffer; sequence + /// matching consults the bit when deciding joiner transparency. + /// + public bool IsZwnj + { + readonly get => (this.flags & ZwnjFlag) != 0; + set => this.flags = value ? (ushort)(this.flags | ZwnjFlag) : (ushort)(this.flags & ~ZwnjFlag); + } + + /// + /// Gets or sets a value indicating whether the codepoint is the zero width + /// joiner. Classified once as the record enters the buffer; sequence matching + /// consults the bit when deciding joiner transparency. + /// + public bool IsZwj + { + readonly get => (this.flags & ZwjFlag) != 0; + set => this.flags = value ? (ushort)(this.flags | ZwjFlag) : (ushort)(this.flags & ~ZwjFlag); + } + + /// + /// Gets or sets a value indicating whether the codepoint is a default + /// ignorable that must stay matchable during substitution while positioning + /// treats it as transparent: the Mongolian free variation selectors, the tag + /// characters, and the combining grapheme joiner. + /// + public bool IsHiddenIgnorable + { + readonly get => (this.flags & HiddenIgnorableFlag) != 0; + set => this.flags = value ? (ushort)(this.flags | HiddenIgnorableFlag) : (ushort)(this.flags & ~HiddenIgnorableFlag); + } + private string DebuggerDisplay => FormattableString .Invariant($" {this.GlyphId} : {this.CodePoint.ToDebuggerDisplay()} : {CodePoint.GetScriptClass(this.CodePoint)} : {this.Direction} : run {this.TextRunIndex} : {this.LigatureId} : {this.LigatureComponent} : {this.IsDecomposed}"); diff --git a/src/SixLabors.Fonts/ShapingBuffer.cs b/src/SixLabors.Fonts/ShapingBuffer.cs index 4647c6d40..a31d8fc85 100644 --- a/src/SixLabors.Fonts/ShapingBuffer.cs +++ b/src/SixLabors.Fonts/ShapingBuffer.cs @@ -193,6 +193,24 @@ internal sealed class ShapingBuffer /// private int nestedApplicationDepth; + /// + /// Whether the packed matcher flag bytes have been computed at least once; + /// until then the stamped lookup state always rebuilds them. + /// + private bool packedFlagsValid; + + /// + /// The role the packed matcher flag bytes were computed under; a role change + /// invalidates them. + /// + private ShapingBufferRole packedFlagsRole; + + /// + /// Whether the buffer held default ignorables when the packed matcher flag + /// bytes were computed; ignorables appearing invalidates them. + /// + private bool packedFlagsHadIgnorables; + /// /// Initializes a new instance of the class. /// @@ -250,6 +268,44 @@ public ShapingBuffer(TextOptions textOptions, ShapingBufferRole role) /// public bool HasDefaultIgnorables { get; set; } + /// + /// Gets a value indicating whether the applying lookup skips the zero width + /// non-joiner during context matching instead of matching it. + /// + public bool LookupAutoZwnj { get; private set; } = true; + + /// + /// Gets a value indicating whether the applying lookup skips the zero width + /// joiner during sequence matching instead of matching it. + /// + public bool LookupAutoZwj { get; private set; } = true; + + /// + /// Gets a value indicating whether the applying lookup never matches across + /// syllable boundaries. + /// + public bool LookupPerSyllable { get; private set; } + + /// + /// Gets the applying lookup's combined mask. Nested lookups inside + /// contextual matches apply under the outer lookup's mask, so the stamped + /// value holds for the whole application. + /// + public uint LookupMask { get; private set; } = uint.MaxValue; + + /// + /// Gets the packed matcher flags for input sequence matching under the + /// applying lookup, precomputed when the lookup is stamped so every match + /// attempt copies them instead of re-deriving them. + /// + public byte InputMatchFlags { get; private set; } + + /// + /// Gets the packed matcher flags for backtrack and lookahead matching under + /// the applying lookup, precomputed when the lookup is stamped. + /// + public byte ContextMatchFlags { get; private set; } + /// /// Gets a value indicating whether lookup application is currently nested /// inside a contextual match. Nested replacements never consume through the @@ -437,6 +493,42 @@ public void Clear() this.SegmentPlans.Clear(); } + /// + /// Sets the applying lookup's mask, joiner handling, and syllable scope for + /// the duration of its application. The drivers stamp this before each + /// merged lookup entry; sequence matching reads it through the skipping + /// iterator. + /// + /// The lookup's combined mask. + /// Whether the lookup skips the zero width non-joiner during context matching. + /// Whether the lookup skips the zero width joiner. + /// Whether matching is confined to one syllable. + public void SetLookupMatchState(uint mask, bool autoZwnj, bool autoZwj, bool perSyllable) + { + this.LookupMask = mask; + + // Consecutive lookups mostly share their joiner handling, so the packed + // bytes are only rebuilt when one of their five inputs actually changed. + if (this.packedFlagsValid + && autoZwnj == this.LookupAutoZwnj + && autoZwj == this.LookupAutoZwj + && perSyllable == this.LookupPerSyllable + && this.packedFlagsRole == this.Role + && this.packedFlagsHadIgnorables == this.HasDefaultIgnorables) + { + return; + } + + this.LookupAutoZwnj = autoZwnj; + this.LookupAutoZwj = autoZwj; + this.LookupPerSyllable = perSyllable; + this.packedFlagsValid = true; + this.packedFlagsRole = this.Role; + this.packedFlagsHadIgnorables = this.HasDefaultIgnorables; + this.InputMatchFlags = SkippingGlyphIterator.PackMatchFlags(this, false); + this.ContextMatchFlags = SkippingGlyphIterator.PackMatchFlags(this, true); + } + /// /// Gets shaper scratch storage of at least the given length, grown to the /// workload's high-water mark and retained. Contents are undefined on entry; @@ -559,7 +651,10 @@ public void AddGlyph(ushort glyphId, CodePoint codePoint, TextDirection directio // The render-as-whitespace carve-outs are default ignorables that fonts // implement as regular spacing glyphs, such as the Hangul fillers; those - // keep their glyphs. + // keep their glyphs. The joiners and the substitution-visible ignorables + // (Mongolian free variation selectors, tag characters, the combining + // grapheme joiner) carry their own bits for the matcher's transparency + // rules. uint value = (uint)codePoint.Value; if (value >= 0x80 && UnicodeUtility.IsDefaultIgnorableCodePoint(value) @@ -567,6 +662,24 @@ public void AddGlyph(ushort glyphId, CodePoint codePoint, TextDirection directio { slot.IsDefaultIgnorable = true; this.HasDefaultIgnorables = true; + + if (CodePoint.IsZeroWidthNonJoiner(codePoint)) + { + slot.IsZwnj = true; + } + else if (CodePoint.IsZeroWidthJoiner(codePoint)) + { + slot.IsZwj = true; + } + else if (value is (>= 0x180B and <= 0x180D) or 0x180F + or (>= 0xE0020 and <= 0xE007F) + or 0x034F) + { + // MONGOLIAN FREE VARIATION SELECTOR ONE..FOUR, TAG SPACE..CANCEL + // TAG, and COMBINING GRAPHEME JOINER: substitution must still see + // these, while positioning treats them as transparent. + slot.IsHiddenIgnorable = true; + } } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs index fdffa3485..7f0a7a44e 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs @@ -205,90 +205,163 @@ public static bool ApplyLookupList( /// if the entire sequence was matched; otherwise, . public static bool MatchInputSequence(SkippingGlyphIterator iterator, uint featureMask, ushort increment, ushort[] sequence, Span matches) { - // The mask travels as match state so the lambda stays static: a capturing - // lambda here would allocate a closure and delegate on every ligature attempt. + iterator.SetMatchContext(featureMask, false); return Match( increment, sequence, iterator, - featureMask, - static (component, data, mask) => - { - if ((data.FeatureMask & mask) == 0) - { - return false; - } - - return component == data.GlyphId; - }, + static (component, data) => component == data.GlyphId, matches); } /// - /// Matches a glyph sequence by glyph ID. + /// Matches a glyph sequence by glyph ID under the given matcher context. + /// + /// The skipping glyph iterator. + /// The initial increment from the iterator's current position. + /// The array of glyph IDs to match. + /// The applying lookup's mask; ignored during context matching. + /// Whether this matches backtrack or lookahead context. + /// if the entire sequence was matched; otherwise, . + public static bool MatchSequence(SkippingGlyphIterator iterator, int increment, ushort[] sequence, uint mask, bool contextMatch) + => MatchSequence(iterator, increment, sequence, mask, contextMatch, out _); + + /// + /// Matches a glyph sequence by glyph ID under the given matcher context, + /// reporting the position one past the final matched element. /// /// The skipping glyph iterator. /// The initial increment from the iterator's current position. /// The array of glyph IDs to match. + /// The applying lookup's mask; ignored during context matching. + /// Whether this matches backtrack or lookahead context. + /// The position one past the final matched element. /// if the entire sequence was matched; otherwise, . - public static bool MatchSequence(SkippingGlyphIterator iterator, int increment, ushort[] sequence) - => Match( + public static bool MatchSequence(SkippingGlyphIterator iterator, int increment, ushort[] sequence, uint mask, bool contextMatch, out int matchEnd) + { + iterator.SetMatchContext(mask, contextMatch); + return Match( increment, sequence, iterator, - (component, data) => component == data.GlyphId, - default); + static (component, data) => component == data.GlyphId, + default, + out matchEnd); + } + + /// + /// Matches a glyph sequence by class values using a class definition table + /// under the given matcher context. + /// + /// The skipping glyph iterator. + /// The initial increment from the iterator's current position. + /// The array of class values to match. + /// The class definition table used to map glyph IDs to class values. + /// The applying lookup's mask; ignored during context matching. + /// Whether this matches backtrack or lookahead context. + /// if the entire sequence was matched; otherwise, . + public static bool MatchClassSequence( + SkippingGlyphIterator iterator, + int increment, + ushort[] sequence, + ClassDefinitionTable classDefinitionTable, + uint mask, + bool contextMatch) + => MatchClassSequence(iterator, increment, sequence, classDefinitionTable, mask, contextMatch, out _); /// - /// Matches a glyph sequence by class values using a class definition table. + /// Matches a glyph sequence by class values under the given matcher context, + /// reporting the position one past the final matched element. /// /// The skipping glyph iterator. /// The initial increment from the iterator's current position. /// The array of class values to match. /// The class definition table used to map glyph IDs to class values. + /// The applying lookup's mask; ignored during context matching. + /// Whether this matches backtrack or lookahead context. + /// The position one past the final matched element. /// if the entire sequence was matched; otherwise, . public static bool MatchClassSequence( SkippingGlyphIterator iterator, int increment, ushort[] sequence, - ClassDefinitionTable classDefinitionTable) + ClassDefinitionTable classDefinitionTable, + uint mask, + bool contextMatch, + out int matchEnd) + { + iterator.SetMatchContext(mask, contextMatch); // The class table travels as match state so the lambda stays static: a // capturing lambda here would allocate a closure and delegate on every // contextual rule attempt. - => Match( + return Match( increment, sequence, iterator, classDefinitionTable, static (component, data, table) => component == table.ClassIndexOf(data.GlyphId), - default); + default, + out matchEnd); + } /// - /// Matches a forward glyph sequence using coverage tables. + /// Matches a forward glyph sequence using coverage tables under the given + /// matcher context. /// /// The skipping glyph iterator. /// The array of coverage tables to match against. /// The starting index in the buffer. /// The exclusive end index in the buffer. + /// The applying lookup's mask; ignored during context matching. + /// Whether this matches lookahead context. /// if all coverage tables matched; otherwise, . public static bool MatchCoverageSequence( SkippingGlyphIterator iterator, CoverageTable[] coverageTable, int startIndex, - int endExclusive) - => Match( + int endExclusive, + uint mask, + bool contextMatch) + => MatchCoverageSequence(iterator, coverageTable, startIndex, endExclusive, mask, contextMatch, out _); + + /// + /// Matches a forward glyph sequence using coverage tables under the given + /// matcher context, reporting the position one past the final matched element. + /// + /// The skipping glyph iterator. + /// The array of coverage tables to match against. + /// The starting index in the buffer. + /// The exclusive end index in the buffer. + /// The applying lookup's mask; ignored during context matching. + /// Whether this matches lookahead context. + /// The position one past the final matched element. + /// if all coverage tables matched; otherwise, . + public static bool MatchCoverageSequence( + SkippingGlyphIterator iterator, + CoverageTable[] coverageTable, + int startIndex, + int endExclusive, + uint mask, + bool contextMatch, + out int matchEnd) + { + iterator.SetMatchContext(mask, contextMatch); + return Match( iterator, startIndex, coverageTable, MatchDirection.Forward, endExclusive, - (component, data) => component.CoverageIndexOf(data.GlyphId) >= 0, - default); + static (component, data) => component.CoverageIndexOf(data.GlyphId) >= 0, + default, + out matchEnd); + } /// /// Matches a backward (backtrack) glyph sequence using coverage tables. - /// Per the spec, backtrack[0] matches i-1, then i-2, and so on. + /// Per the spec, backtrack[0] matches i-1, then i-2, and so on. Backtrack is + /// always context, so joiners are transparent to it. /// /// The skipping glyph iterator. /// The array of backtrack coverage tables to match against. @@ -300,37 +373,51 @@ public static bool MatchBacktrackCoverageSequence( CoverageTable[] backtrack, int startIndex, int endExclusive) - => Match( + { + iterator.SetMatchContext(0, true); + return Match( iterator, startIndex, backtrack, MatchDirection.Backward, endExclusive, - (component, data) => component.CoverageIndexOf(data.GlyphId) >= 0, - default); + static (component, data) => component.CoverageIndexOf(data.GlyphId) >= 0, + default, + out _); + } /// /// Applies a chained sequence rule by matching backtrack, input, and lookahead glyph ID sequences. + /// The input matches under the applying lookup's mask and joiner handling; + /// backtrack and lookahead match as context, transparent to joiners. /// /// The skipping glyph iterator. /// The chained sequence rule table to apply. + /// The applying lookup's mask. /// if all sequences matched; otherwise, . - public static bool ApplyChainedSequenceRule(SkippingGlyphIterator iterator, ChainedSequenceRuleTable rule) + public static bool ApplyChainedSequenceRule(SkippingGlyphIterator iterator, ChainedSequenceRuleTable rule, uint mask) { + int matchEnd = iterator.Index + 1; if (rule.InputSequence.Length > 0 - && !MatchSequence(iterator, 1, rule.InputSequence)) + && !MatchSequence(iterator, 1, rule.InputSequence, mask, false, out matchEnd)) { return false; } - if (rule.LookaheadSequence.Length > 0 - && !MatchSequence(iterator, 1 + rule.InputSequence.Length, rule.LookaheadSequence)) + if (rule.LookaheadSequence.Length > 0) { - return false; + // Lookahead starts exactly one past the final matched input element, + // not a stepped jump over the input count. + SkippingGlyphIterator lookahead = iterator; + lookahead.Index = matchEnd - 1; + if (!MatchSequence(lookahead, 1, rule.LookaheadSequence, 0, true)) + { + return false; + } } if (rule.BacktrackSequence.Length > 0 - && !MatchSequence(iterator, -rule.BacktrackSequence.Length, rule.BacktrackSequence)) + && !MatchSequence(iterator, -rule.BacktrackSequence.Length, rule.BacktrackSequence, 0, true)) { return false; } @@ -340,34 +427,45 @@ public static bool ApplyChainedSequenceRule(SkippingGlyphIterator iterator, Chai /// /// Applies a chained class sequence rule by matching backtrack, input, and lookahead class sequences. + /// The input matches under the applying lookup's mask and joiner handling; + /// backtrack and lookahead match as context, transparent to joiners. /// /// The skipping glyph iterator. /// The chained class sequence rule table to apply. /// The class definition table for the input sequence. /// The class definition table for the backtrack sequence. /// The class definition table for the lookahead sequence. + /// The applying lookup's mask. /// if all sequences matched; otherwise, . public static bool ApplyChainedClassSequenceRule( SkippingGlyphIterator iterator, ChainedClassSequenceRuleTable rule, ClassDefinitionTable inputClassDefinitionTable, ClassDefinitionTable backtrackClassDefinitionTable, - ClassDefinitionTable lookaheadClassDefinitionTable) + ClassDefinitionTable lookaheadClassDefinitionTable, + uint mask) { + int matchEnd = iterator.Index + 1; if (rule.InputSequence.Length > 0 && - !MatchClassSequence(iterator, 1, rule.InputSequence, inputClassDefinitionTable)) + !MatchClassSequence(iterator, 1, rule.InputSequence, inputClassDefinitionTable, mask, false, out matchEnd)) { return false; } - if (rule.LookaheadSequence.Length > 0 - && !MatchClassSequence(iterator, 1 + rule.InputSequence.Length, rule.LookaheadSequence, lookaheadClassDefinitionTable)) + if (rule.LookaheadSequence.Length > 0) { - return false; + // Lookahead starts exactly one past the final matched input element, + // not a stepped jump over the input count. + SkippingGlyphIterator lookahead = iterator; + lookahead.Index = matchEnd - 1; + if (!MatchClassSequence(lookahead, 1, rule.LookaheadSequence, lookaheadClassDefinitionTable, 0, true)) + { + return false; + } } if (rule.BacktrackSequence.Length > 0 - && !MatchClassSequence(iterator, -rule.BacktrackSequence.Length, rule.BacktrackSequence, backtrackClassDefinitionTable)) + && !MatchClassSequence(iterator, -rule.BacktrackSequence.Length, rule.BacktrackSequence, backtrackClassDefinitionTable, 0, true)) { return false; } @@ -387,6 +485,7 @@ public static bool ApplyChainedClassSequenceRule( /// The array of input coverage tables. /// The array of backtrack coverage tables. /// The array of lookahead coverage tables. + /// The applying lookup's mask; the input matches under it. /// if all coverages matched; otherwise, . public static bool CheckAllCoverages( FontMetrics fontMetrics, @@ -397,7 +496,8 @@ public static bool CheckAllCoverages( int count, CoverageTable[] input, CoverageTable[] backtrack, - CoverageTable[] lookahead) + CoverageTable[] lookahead, + uint mask) { int endExclusive = index + count; @@ -407,11 +507,13 @@ public static bool CheckAllCoverages( ShapingProbe.ContextIterators++; } - // Compute backtrack start using skippy prev(), not index-1. + // Compute backtrack start using skippy prev(), not index-1: context steps + // are transparent to joiners, so the step lands on the first solid glyph. int backtrackStart = index; if (backtrack.Length > 0) { SkippingGlyphIterator backIt = iterator; + backIt.SetMatchContext(0, true); backIt.Index = index; backtrackStart = backIt.Prev(); // first backtrack glyph (i-1 in skippy space) } @@ -421,24 +523,14 @@ public static bool CheckAllCoverages( return false; } - // Input starts at the current glyph position. - if (!MatchCoverageSequence(iterator, input, index, endExclusive)) + // Input starts at the current glyph position; lookahead starts exactly + // one past the final matched input element. + if (!MatchCoverageSequence(iterator, input, index, endExclusive, mask, false, out int matchEnd)) { return false; } - // Compute lookahead start by advancing through the input sequence using skippy Next(), - // not by raw index arithmetic. - int lookaheadStart = index; - if (lookahead.Length > 0) - { - SkippingGlyphIterator fwdIt = iterator; - fwdIt.Index = index; - fwdIt.Increment(input.Length); // advance input.Length steps in skippy space - lookaheadStart = fwdIt.Index; - } - - if (!MatchCoverageSequence(iterator, lookahead, lookaheadStart, endExclusive)) + if (!MatchCoverageSequence(iterator, lookahead, matchEnd, endExclusive, 0, true)) { return false; } @@ -647,31 +739,119 @@ private static bool Match( TState state, Func condition, Span matches) + => Match(increment, sequence, iterator, state, condition, matches, out _); + + /// + /// Matches a sequence of elements against glyphs using an increment-based + /// approach with match state, reporting the position one past the final + /// matched element so lookahead can start exactly where the input ended. + /// + /// The type of sequence elements to match. + /// The type of state passed to the condition. + /// The initial increment from the iterator's current position. + /// The array of elements to match. + /// The skipping glyph iterator. + /// The state passed to the condition. + /// The condition function to test each element against glyph data. + /// A span to store matched glyph indices, or default if not needed. + /// The position one past the final matched element. + /// if all elements in the sequence were matched; otherwise, . + private static bool Match( + int increment, + T[] sequence, + SkippingGlyphIterator iterator, + TState state, + Func condition, + Span matches, + out int matchEnd) { int position = iterator.Index; - int offset = iterator.Increment(increment); ShapingBuffer buffer = iterator.Collection; + matchEnd = position + 1; + int i = 0; + + // A buffer without a transparent record in reach keeps the solid-glyph + // walk: every stepped-to record must match or the rule is refused. + if (!iterator.MatchTransparencyActive) + { + int solidOffset = iterator.Increment(increment); + if (solidOffset < 0) + { + return false; + } + + while (i < sequence.Length && i < MaxContextLength && solidOffset < buffer.Count) + { + ref GlyphShapingData solidData = ref buffer[solidOffset]; + if (!iterator.MayMatch(ref solidData) || !condition(sequence[i], solidData, state)) + { + break; + } + + if (matches.Length == MaxContextLength) + { + matches[i] = solidOffset; + } + + i++; + matchEnd = solidOffset + 1; + solidOffset = iterator.Next(); + } + + iterator.Index = position; + return i == sequence.Length; + } + + // A single forward step enters the walk directly so a transparent record + // at the next position still gets its chance to match the first sequence + // element; larger jumps step over positions other matchers consumed. + int offset; + if (increment == 1) + { + offset = position + 1; + iterator.Index = offset; + } + else + { + offset = iterator.Increment(increment); + } if (offset < 0) { return false; } - int i = 0; + // A transparent record is stepped over unless it matches the sequence + // position itself; a solid record that fails the shape test refuses the + // whole match. while (i < sequence.Length && i < MaxContextLength && offset < buffer.Count) { - if (!condition(sequence[i], buffer[offset], state)) + if (iterator.IsPropertySkipped(offset)) { - break; + offset = ++iterator.Index; + continue; + } + + ref GlyphShapingData data = ref buffer[offset]; + if (iterator.MayMatch(ref data) && condition(sequence[i], data, state)) + { + if (matches.Length == MaxContextLength) + { + matches[i] = offset; + } + + i++; + matchEnd = offset + 1; + offset = ++iterator.Index; + continue; } - if (matches.Length == MaxContextLength) + if (!iterator.IsTransparent(ref data)) { - matches[i] = iterator.Index; + break; } - i++; - offset = iterator.Next(); + offset = ++iterator.Index; } iterator.Index = position; @@ -694,31 +874,116 @@ private static bool Match( SkippingGlyphIterator iterator, Func condition, Span matches) + => Match(increment, sequence, iterator, condition, matches, out _); + + /// + /// Matches a sequence of elements against glyphs using an increment-based + /// approach, reporting the position one past the final matched element so + /// lookahead can start exactly where the input ended. + /// + /// The type of sequence elements to match. + /// The initial increment from the iterator's current position. + /// The array of elements to match. + /// The skipping glyph iterator. + /// The condition function to test each element against glyph data. + /// A span to store matched glyph indices, or default if not needed. + /// The position one past the final matched element. + /// if all elements in the sequence were matched; otherwise, . + private static bool Match( + int increment, + T[] sequence, + SkippingGlyphIterator iterator, + Func condition, + Span matches, + out int matchEnd) { int position = iterator.Index; - int offset = iterator.Increment(increment); ShapingBuffer buffer = iterator.Collection; + matchEnd = position + 1; + int i = 0; + + // A buffer without a transparent record in reach keeps the solid-glyph + // walk: every stepped-to record must match or the rule is refused. + if (!iterator.MatchTransparencyActive) + { + int solidOffset = iterator.Increment(increment); + if (solidOffset < 0) + { + return false; + } + + while (i < sequence.Length && i < MaxContextLength && solidOffset < buffer.Count) + { + ref GlyphShapingData solidData = ref buffer[solidOffset]; + if (!iterator.MayMatch(ref solidData) || !condition(sequence[i], solidData)) + { + break; + } + + if (matches.Length == MaxContextLength) + { + matches[i] = solidOffset; + } + + i++; + matchEnd = solidOffset + 1; + solidOffset = iterator.Next(); + } + + iterator.Index = position; + return i == sequence.Length; + } + + // A single forward step enters the walk directly so a transparent record + // at the next position still gets its chance to match the first sequence + // element; larger jumps step over positions other matchers consumed. + int offset; + if (increment == 1) + { + offset = position + 1; + iterator.Index = offset; + } + else + { + offset = iterator.Increment(increment); + } if (offset < 0) { return false; } - int i = 0; + // A transparent record is stepped over unless it matches the sequence + // position itself; a solid record that fails the shape test refuses the + // whole match. while (i < sequence.Length && i < MaxContextLength && offset < buffer.Count) { - if (!condition(sequence[i], buffer[offset])) + if (iterator.IsPropertySkipped(offset)) { - break; + offset = ++iterator.Index; + continue; + } + + ref GlyphShapingData data = ref buffer[offset]; + if (iterator.MayMatch(ref data) && condition(sequence[i], data)) + { + if (matches.Length == MaxContextLength) + { + matches[i] = offset; + } + + i++; + matchEnd = offset + 1; + offset = ++iterator.Index; + continue; } - if (matches.Length == MaxContextLength) + if (!iterator.IsTransparent(ref data)) { - matches[i] = iterator.Index; + break; } - i++; - offset = iterator.Next(); + offset = ++iterator.Index; } iterator.Index = position; @@ -736,6 +1001,7 @@ private static bool Match( /// The exclusive end index in the buffer. /// The condition function to test each element against glyph data. /// A span to store matched glyph indices, or default if not needed. + /// The position one past the final matched element; meaningful for forward matching. /// if all elements in the sequence were matched; otherwise, . private static bool Match( SkippingGlyphIterator iterator, @@ -744,45 +1010,61 @@ private static bool Match( MatchDirection direction, int endExclusive, Func condition, - Span matches) + Span matches, + out int matchEnd) { + matchEnd = startIndex; if (sequence.Length == 0) { return true; } int saved = iterator.Index; - iterator.Index = startIndex; + int offset = startIndex; + int step = direction == MatchDirection.Forward ? 1 : -1; ShapingBuffer buffer = iterator.Collection; int limit = Math.Min(endExclusive, buffer.Count); - for (int i = 0; i < sequence.Length && i < MaxContextLength; i++) + // A transparent record is stepped over unless it matches the sequence + // position itself; a solid record that fails the shape test refuses the + // whole match. + int i = 0; + while (i < sequence.Length && i < MaxContextLength) { - if (iterator.Index < 0 || iterator.Index >= limit) + if (offset < 0 || offset >= limit) { iterator.Index = saved; return false; } - ref GlyphShapingData data = ref buffer[iterator.Index]; - if (!condition(sequence[i], data)) + if (iterator.IsPropertySkipped(offset)) { - iterator.Index = saved; - return false; + offset += step; + continue; } - if (matches.Length == MaxContextLength) + ref GlyphShapingData data = ref buffer[offset]; + if (iterator.MayMatch(ref data) && condition(sequence[i], data)) { - matches[i] = iterator.Index; + if (matches.Length == MaxContextLength) + { + matches[i] = offset; + } + + i++; + matchEnd = offset + 1; + offset += step; + continue; } - if (i + 1 < sequence.Length) + if (!iterator.IsTransparent(ref data)) { - iterator.Index = direction == MatchDirection.Forward - ? iterator.Next() - : iterator.Prev(); + iterator.Index = saved; + return false; } + + offset += step; } iterator.Index = saved; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType7SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType7SubTable.cs index db11ced15..9eddfe14f 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType7SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType7SubTable.cs @@ -111,7 +111,7 @@ public override bool TryUpdatePosition( continue; } - if (!AdvancedTypographicUtils.MatchSequence(iterator, 1, ruleTable.InputSequence)) + if (!AdvancedTypographicUtils.MatchSequence(iterator, 1, ruleTable.InputSequence, buffer.LookupMask, false)) { continue; } @@ -223,7 +223,7 @@ public override bool TryUpdatePosition( continue; } - if (!AdvancedTypographicUtils.MatchClassSequence(iterator, 1, ruleTable.InputSequence, this.classDefinitionTable)) + if (!AdvancedTypographicUtils.MatchClassSequence(iterator, 1, ruleTable.InputSequence, this.classDefinitionTable, buffer.LookupMask, false)) { continue; } @@ -317,7 +317,7 @@ public override bool TryUpdatePosition( } SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, this.LookupFlags, this.MarkFilteringSet); - if (!AdvancedTypographicUtils.MatchCoverageSequence(iterator, this.coverageTables, index, index + count)) + if (!AdvancedTypographicUtils.MatchCoverageSequence(iterator, this.coverageTables, index, index + count, buffer.LookupMask, false)) { return false; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType8SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType8SubTable.cs index ae01b5985..832d6b181 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType8SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType8SubTable.cs @@ -120,7 +120,7 @@ public override bool TryUpdatePosition( for (int lookupIndex = 0; lookupIndex < rules.Length; lookupIndex++) { ChainedSequenceRuleTable rule = rules[lookupIndex]; - if (!AdvancedTypographicUtils.ApplyChainedSequenceRule(iterator, rule)) + if (!AdvancedTypographicUtils.ApplyChainedSequenceRule(iterator, rule, buffer.LookupMask)) { continue; } @@ -254,7 +254,7 @@ public override bool TryUpdatePosition( { ChainedClassSequenceRuleTable rule = rules[lookupIndex]; - if (!AdvancedTypographicUtils.ApplyChainedClassSequenceRule(iterator, rule, this.inputClassDefinitionTable, this.backtrackClassDefinitionTable, this.lookaheadClassDefinitionTable)) + if (!AdvancedTypographicUtils.ApplyChainedClassSequenceRule(iterator, rule, this.inputClassDefinitionTable, this.backtrackClassDefinitionTable, this.lookaheadClassDefinitionTable, buffer.LookupMask)) { continue; } @@ -368,7 +368,7 @@ public override bool TryUpdatePosition( return false; } - if (!AdvancedTypographicUtils.CheckAllCoverages(fontMetrics, this.LookupFlags, this.MarkFilteringSet, buffer, index, count, this.inputCoverageTables, this.backtrackCoverageTables, this.lookaheadCoverageTables)) + if (!AdvancedTypographicUtils.CheckAllCoverages(fontMetrics, this.LookupFlags, this.MarkFilteringSet, buffer, index, count, this.inputCoverageTables, this.backtrackCoverageTables, this.lookaheadCoverageTables, buffer.LookupMask)) { return false; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs index 81236b0f7..bfa682366 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs @@ -323,13 +323,14 @@ private bool PositionSegment( for (int g = 0; g < groups.Count; g++) { ShapePlanStageGroup group = groups[g]; - List<(Tag Feature, ushort Index, LookupTable LookupTable, uint Mask)> merged = group.Lookups; + List<(Tag Feature, ushort Index, LookupTable LookupTable, uint Mask, bool AutoZwnj, bool AutoZwj, bool PerSyllable)> merged = group.Lookups; shapingStages[group.Start].PreProcessFeature(shapePlan, buffer, index, count); for (int m = 0; m < merged.Count; m++) { - (Tag feature, ushort _, LookupTable featureLookupTable, uint featureMask) = merged[m]; + (Tag feature, ushort _, LookupTable featureLookupTable, uint featureMask, bool autoZwnj, bool autoZwj, bool perSyllable) = merged[m]; + buffer.SetLookupMatchState(featureMask, autoZwnj, autoZwj, perSyllable); // Skip the whole lookup when its coverage cannot intersect any // glyph id the buffer has ever contained; most fonts carry diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs index ed5cae556..8cc880d82 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs @@ -117,7 +117,7 @@ public override bool TrySubstitution( continue; } - if (!AdvancedTypographicUtils.MatchSequence(iterator, 1, ruleTable.InputSequence)) + if (!AdvancedTypographicUtils.MatchSequence(iterator, 1, ruleTable.InputSequence, lookupMask, false)) { continue; } @@ -282,7 +282,7 @@ public override bool TrySubstitution( continue; } - if (!AdvancedTypographicUtils.MatchClassSequence(iterator, 1, ruleTable.InputSequence, this.classDefinitionTable)) + if (!AdvancedTypographicUtils.MatchClassSequence(iterator, 1, ruleTable.InputSequence, this.classDefinitionTable, lookupMask, false)) { continue; } @@ -427,7 +427,7 @@ public override bool TrySubstitution( // https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#53-context-substitution-format-3-coverage-based-glyph-contexts SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, this.LookupFlags, this.MarkFilteringSet); - if (!AdvancedTypographicUtils.MatchCoverageSequence(iterator, this.coverageTables, index, index + count)) + if (!AdvancedTypographicUtils.MatchCoverageSequence(iterator, this.coverageTables, index, index + count, lookupMask, false)) { return false; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs index 48d8a93ae..47db4e705 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs @@ -122,7 +122,7 @@ public override bool TrySubstitution( for (int i = 0; i < rules.Length; i++) { ChainedSequenceRuleTable ruleTable = rules[i]; - if (!AdvancedTypographicUtils.ApplyChainedSequenceRule(iterator, ruleTable)) + if (!AdvancedTypographicUtils.ApplyChainedSequenceRule(iterator, ruleTable, lookupMask)) { continue; } @@ -314,7 +314,7 @@ public override bool TrySubstitution( { ChainedClassSequenceRuleTable ruleTable = rules[lookupIndex]; - if (!AdvancedTypographicUtils.ApplyChainedClassSequenceRule(iterator, ruleTable, this.inputClassDefinitionTable, this.backtrackClassDefinitionTable, this.lookaheadClassDefinitionTable)) + if (!AdvancedTypographicUtils.ApplyChainedClassSequenceRule(iterator, ruleTable, this.inputClassDefinitionTable, this.backtrackClassDefinitionTable, this.lookaheadClassDefinitionTable, lookupMask)) { continue; } @@ -497,7 +497,8 @@ public override bool TrySubstitution( count, this.inputCoverageTables, this.backtrackCoverageTables, - this.lookaheadCoverageTables)) + this.lookaheadCoverageTables, + lookupMask)) { return false; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs index 5d31a26b2..c02cb69d6 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs @@ -287,7 +287,7 @@ private void ApplyMergedLookups( FontMetrics fontMetrics, ShapingBuffer buffer, ref SkippingGlyphIterator iterator, - List<(Tag Feature, ushort Index, LookupTable LookupTable, uint Mask)> merged, + List<(Tag Feature, ushort Index, LookupTable LookupTable, uint Mask, bool AutoZwnj, bool AutoZwj, bool PerSyllable)> merged, int index, ref int count, ref int i, @@ -298,7 +298,7 @@ private void ApplyMergedLookups( { for (int m = 0; m < merged.Count; m++) { - (Tag feature, ushort _, LookupTable featureLookupTable, uint featureMask) = merged[m]; + (Tag feature, ushort _, LookupTable featureLookupTable, uint featureMask, bool autoZwnj, bool autoZwj, bool perSyllable) = merged[m]; // Skip the whole lookup when its coverage cannot intersect any glyph id // the buffer has ever contained; most fonts carry many lookups for @@ -318,6 +318,7 @@ private void ApplyMergedLookups( continue; } + buffer.SetLookupMatchState(featureMask, autoZwnj, autoZwj, perSyllable); iterator.Reset(index, featureLookupTable.LookupFlags, featureLookupTable.MarkFilteringSet); long featureStart = ShapingProbe.Timestamp(); long featureApplies = 0; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlan.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlan.cs index 718849d50..439f3ecb4 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlan.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlan.cs @@ -342,7 +342,7 @@ private void BuildStageGroups( // from the tail because resolved lookups arrive mostly ascending, so // the insertion point is almost always at or near the end. ShapePlanStageGroup group = new(stageIndex, groupEnd); - List<(Tag Feature, ushort Index, TLookup LookupTable, uint Mask)> merged = group.Lookups; + List<(Tag Feature, ushort Index, TLookup LookupTable, uint Mask, bool AutoZwnj, bool AutoZwj, bool PerSyllable)> merged = group.Lookups; for (int s = stageIndex; s < groupEnd; s++) { Tag featureTag = stages[s].FeatureTag; @@ -357,21 +357,27 @@ private void BuildStageGroups( continue; } + ShapingFeatureFlags featureFlags = this.Features.GetFlags(featureTag); + bool autoZwnj = (featureFlags & ShapingFeatureFlags.ManualZwnj) == 0; + bool autoZwj = (featureFlags & ShapingFeatureFlags.ManualZwj) == 0; + bool perSyllable = (featureFlags & ShapingFeatureFlags.PerSyllable) != 0; + foreach ((Tag Feature, ushort Index, TLookup LookupTable) featureLookup in lookups) { // Scan from the tail toward the head. Three outcomes: the // lookup index is already present, so this feature's mask - // joins the entry; a smaller index is found, so the new entry - // inserts directly after it; or the head is reached, so the - // new entry inserts first. + // joins the entry and the joiner handling intersects; a + // smaller index is found, so the new entry inserts directly + // after it; or the head is reached, so the new entry inserts + // first. int insertAt = merged.Count; bool alreadyMerged = false; while (insertAt > 0) { - (Tag Feature, ushort Index, TLookup LookupTable, uint Mask) prior = merged[insertAt - 1]; + (Tag Feature, ushort Index, TLookup LookupTable, uint Mask, bool AutoZwnj, bool AutoZwj, bool PerSyllable) prior = merged[insertAt - 1]; if (prior.Index == featureLookup.Index) { - merged[insertAt - 1] = (prior.Feature, prior.Index, prior.LookupTable, prior.Mask | featureMask); + merged[insertAt - 1] = (prior.Feature, prior.Index, prior.LookupTable, prior.Mask | featureMask, prior.AutoZwnj && autoZwnj, prior.AutoZwj && autoZwj, prior.PerSyllable); alreadyMerged = true; break; } @@ -386,7 +392,7 @@ private void BuildStageGroups( if (!alreadyMerged) { - merged.Insert(insertAt, (featureLookup.Feature, featureLookup.Index, featureLookup.LookupTable, featureMask)); + merged.Insert(insertAt, (featureLookup.Feature, featureLookup.Index, featureLookup.LookupTable, featureMask, autoZwnj, autoZwj, perSyllable)); } } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanFeatures.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanFeatures.cs index e459b9654..577651f9d 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanFeatures.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanFeatures.cs @@ -81,6 +81,18 @@ internal sealed class ShapePlanFeatures /// private readonly List disabledTags = new(1); + /// + /// The tag values carrying registration flags, parallel to + /// . Flags are recorded at registration and read only + /// at plan build, so a linear search suffices. + /// + private readonly List flagTags = new(16); + + /// + /// The registration flags, parallel to . + /// + private readonly List flagValues = new(16); + /// /// The most recently resolved tag value. Queries strongly repeat the same /// feature during one feature's application, so a single-entry memo answers @@ -121,6 +133,42 @@ public static uint GetVerticalMask(Tag tag) return 0; } + /// + /// Records registration flags for a feature, combining with any flags already + /// recorded. Flags accumulate the way masks do: a feature registered by several + /// sites keeps every site's requirements. + /// + /// The feature tag. + /// The flags to record. + public void AddFlags(Tag tag, ShapingFeatureFlags flags) + { + if (flags == ShapingFeatureFlags.None) + { + return; + } + + int index = this.flagTags.IndexOf(tag.Value); + if (index >= 0) + { + this.flagValues[index] |= flags; + return; + } + + this.flagTags.Add(tag.Value); + this.flagValues.Add(flags); + } + + /// + /// Gets the registration flags recorded for a feature, or none. + /// + /// The feature tag. + /// The recorded flags. + public ShapingFeatureFlags GetFlags(Tag tag) + { + int index = this.flagTags.IndexOf(tag.Value); + return index >= 0 ? this.flagValues[index] : ShapingFeatureFlags.None; + } + /// /// Gets the mask for the given feature tag: a varying feature's distinct bit, /// the shared global bit for a global feature, or zero when the tag is disabled diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanStageGroup{TLookup}.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanStageGroup{TLookup}.cs index cb71f547f..134d69d93 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanStageGroup{TLookup}.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapePlanStageGroup{TLookup}.cs @@ -40,7 +40,9 @@ public ShapePlanStageGroup(int start, int end) /// /// Gets the group's lookups merged across its stage features into lookup-index /// order. A lookup registered by several features appears once with their - /// plan-assigned masks combined, so application consumes the list directly. + /// plan-assigned masks combined and their joiner handling intersected: a + /// lookup skips a joiner automatically only when every registering feature + /// allows it. Application consumes the list directly. /// - public List<(Tag Feature, ushort Index, TLookup LookupTable, uint Mask)> Lookups { get; } + public List<(Tag Feature, ushort Index, TLookup LookupTable, uint Mask, bool AutoZwnj, bool AutoZwj, bool PerSyllable)> Lookups { get; } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs index 22efd7612..e7b42ccc1 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs @@ -134,16 +134,23 @@ public ArabicShaper(ScriptClass script, TextOptions textOptions) /// protected override void PlanFeatures(ShapingBuffer buffer, int index, int count) { - this.EnableFeature(buffer, index, count, CcmpTag); - this.EnableFeature(buffer, index, count, LoclTag); - - this.AddFeature(buffer, index, count, IsolTag, false); - this.AddFeature(buffer, index, count, FinaTag, false); - this.AddFeature(buffer, index, count, Fin2Tag, false); - this.AddFeature(buffer, index, count, Fin3Tag, false); - this.AddFeature(buffer, index, count, MediTag, false); - this.AddFeature(buffer, index, count, Med2Tag, false); - this.AddFeature(buffer, index, count, InitTag, false); + this.EnableFeature(buffer, index, count, CcmpTag, ShapingFeatureFlags.ManualZwj); + this.EnableFeature(buffer, index, count, LoclTag, ShapingFeatureFlags.ManualZwj); + + this.AddFeature(buffer, index, count, IsolTag, ShapingFeatureFlags.ManualZwj, false, null, null); + this.AddFeature(buffer, index, count, FinaTag, ShapingFeatureFlags.ManualZwj, false, null, null); + this.AddFeature(buffer, index, count, Fin2Tag, ShapingFeatureFlags.ManualZwj, false, null, null); + this.AddFeature(buffer, index, count, Fin3Tag, ShapingFeatureFlags.ManualZwj, false, null, null); + this.AddFeature(buffer, index, count, MediTag, ShapingFeatureFlags.ManualZwj, false, null, null); + this.AddFeature(buffer, index, count, Med2Tag, ShapingFeatureFlags.ManualZwj, false, null, null); + this.AddFeature(buffer, index, count, InitTag, ShapingFeatureFlags.ManualZwj, false, null, null); + + // The ligature trio and the required composition and ligature features + // match the joiners themselves for this script's shaping model. + this.Features.AddFlags(RligTag, ShapingFeatureFlags.ManualZwj); + this.Features.AddFlags(CaltTag, ShapingFeatureFlags.ManualZwj); + this.Features.AddFlags(LigaTag, ShapingFeatureFlags.ManualZwj); + this.Features.AddFlags(CligTag, ShapingFeatureFlags.ManualZwj); // HarfBuzz plans these as Arabic-script features, independently of the // generic horizontal feature list. Horizontal runs already get them from @@ -155,7 +162,7 @@ protected override void PlanFeatures(ShapingBuffer buffer, int index, int count) this.EnableFeature(buffer, index, count, CligTag); } - this.EnableFeature(buffer, index, count, MsetTag); + this.EnableFeature(buffer, index, count, MsetTag, ShapingFeatureFlags.ManualZwj); } /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs index db034bc16..aeff42b30 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs @@ -308,11 +308,37 @@ protected void AddFeature( this.AddStage(feature, preAction, postAction); } + /// + /// Adds a varying shaping feature with registration flags controlling its + /// lookups' joiner handling and syllable scope. + /// + /// The glyph shaping buffer. + /// The zero-based index of the first element. + /// The number of elements. + /// The feature tag to add. + /// The registration flags. + /// Whether the feature is initially enabled. + /// The action to invoke before the feature is applied, or . + /// The action to invoke after the feature is applied, or . + protected void AddFeature( + ShapingBuffer buffer, + int index, + int count, + Tag feature, + ShapingFeatureFlags flags, + bool enabled, + Action? preAction, + Action? postAction) + { + this.Features.AddFlags(feature, flags); + this.AddFeature(buffer, index, count, feature, enabled, preAction, postAction); + } + /// /// Registers a global feature over the given range: one that applies to every /// glyph of the plan's segments and therefore shares the plan's single global /// mask bit instead of consuming a distinct bit. Features whose per-glyph - /// state varies register through instead. + /// state varies register through the add-feature overloads instead. /// /// The glyph shaping buffer. /// The zero-based index of the first element. @@ -321,6 +347,44 @@ protected void AddFeature( protected void EnableFeature(ShapingBuffer buffer, int index, int count, Tag feature) => this.EnableFeature(buffer, index, count, feature, null, null); + /// + /// Registers a global feature with registration flags controlling its lookups' + /// joiner handling and syllable scope. + /// + /// The glyph shaping buffer. + /// The zero-based index of the first element. + /// The number of elements. + /// The feature tag to enable. + /// The registration flags. + protected void EnableFeature(ShapingBuffer buffer, int index, int count, Tag feature, ShapingFeatureFlags flags) + { + this.Features.AddFlags(feature, flags); + this.EnableFeature(buffer, index, count, feature, null, null); + } + + /// + /// Registers a global feature with registration flags and stage actions. + /// + /// The glyph shaping buffer. + /// The zero-based index of the first element. + /// The number of elements. + /// The feature tag to enable. + /// The registration flags. + /// The action to invoke before the feature is applied, or . + /// The action to invoke after the feature is applied, or . + protected void EnableFeature( + ShapingBuffer buffer, + int index, + int count, + Tag feature, + ShapingFeatureFlags flags, + Action? preAction, + Action? postAction) + { + this.Features.AddFlags(feature, flags); + this.EnableFeature(buffer, index, count, feature, preAction, postAction); + } + /// /// Registers a global feature and attaches the supplied stage actions. The /// feature applies to every glyph of the plan's segments and therefore shares diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs index a4857258c..3e8f16ce7 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs @@ -230,29 +230,29 @@ public IndicShaper(ScriptClass script, Tag unicodeScriptTag, TextOptions textOpt /// protected override void PlanFeatures(ShapingBuffer buffer, int index, int count) { - this.EnableFeature(buffer, index, count, LoclTag, this.setupSyllablesAction, null); - this.EnableFeature(buffer, index, count, CcmpTag); - - this.EnableFeature(buffer, index, count, NuktTag, this.initialReorderAction, null); - this.EnableFeature(buffer, index, count, AkhnTag); - - this.AddFeature(buffer, index, count, RphfTag, false); - this.EnableFeature(buffer, index, count, RkrfTag); - this.AddFeature(buffer, index, count, PrefTag, false); - this.AddFeature(buffer, index, count, BlwfTag, false); - this.AddFeature(buffer, index, count, AbvfTag, false); - this.AddFeature(buffer, index, count, HalfTag, false); - this.AddFeature(buffer, index, count, PstfTag, false); - this.EnableFeature(buffer, index, count, VatuTag); - this.EnableFeature(buffer, index, count, CjctTag); - this.AddFeature(buffer, index, count, CfarTag, false, null, this.finalReorderAction); - - this.AddFeature(buffer, index, count, InitTag, false); - this.EnableFeature(buffer, index, count, PresTag); - this.EnableFeature(buffer, index, count, AbvsTag); - this.EnableFeature(buffer, index, count, BlwsTag); - this.EnableFeature(buffer, index, count, PstsTag); - this.EnableFeature(buffer, index, count, HalnTag); + this.EnableFeature(buffer, index, count, LoclTag, ShapingFeatureFlags.PerSyllable, this.setupSyllablesAction, null); + this.EnableFeature(buffer, index, count, CcmpTag, ShapingFeatureFlags.PerSyllable); + + this.EnableFeature(buffer, index, count, NuktTag, ShapingFeatureFlags.ManualJoiners | ShapingFeatureFlags.PerSyllable, this.initialReorderAction, null); + this.EnableFeature(buffer, index, count, AkhnTag, ShapingFeatureFlags.ManualJoiners | ShapingFeatureFlags.PerSyllable); + + this.AddFeature(buffer, index, count, RphfTag, ShapingFeatureFlags.ManualJoiners | ShapingFeatureFlags.PerSyllable, false, null, null); + this.EnableFeature(buffer, index, count, RkrfTag, ShapingFeatureFlags.ManualJoiners | ShapingFeatureFlags.PerSyllable); + this.AddFeature(buffer, index, count, PrefTag, ShapingFeatureFlags.ManualJoiners | ShapingFeatureFlags.PerSyllable, false, null, null); + this.AddFeature(buffer, index, count, BlwfTag, ShapingFeatureFlags.ManualJoiners | ShapingFeatureFlags.PerSyllable, false, null, null); + this.AddFeature(buffer, index, count, AbvfTag, ShapingFeatureFlags.ManualJoiners | ShapingFeatureFlags.PerSyllable, false, null, null); + this.AddFeature(buffer, index, count, HalfTag, ShapingFeatureFlags.ManualJoiners | ShapingFeatureFlags.PerSyllable, false, null, null); + this.AddFeature(buffer, index, count, PstfTag, ShapingFeatureFlags.ManualJoiners | ShapingFeatureFlags.PerSyllable, false, null, null); + this.EnableFeature(buffer, index, count, VatuTag, ShapingFeatureFlags.ManualJoiners | ShapingFeatureFlags.PerSyllable); + this.EnableFeature(buffer, index, count, CjctTag, ShapingFeatureFlags.ManualJoiners | ShapingFeatureFlags.PerSyllable); + this.AddFeature(buffer, index, count, CfarTag, ShapingFeatureFlags.ManualJoiners | ShapingFeatureFlags.PerSyllable, false, null, this.finalReorderAction); + + this.AddFeature(buffer, index, count, InitTag, ShapingFeatureFlags.ManualJoiners | ShapingFeatureFlags.PerSyllable, false, null, null); + this.EnableFeature(buffer, index, count, PresTag, ShapingFeatureFlags.ManualJoiners | ShapingFeatureFlags.PerSyllable); + this.EnableFeature(buffer, index, count, AbvsTag, ShapingFeatureFlags.ManualJoiners | ShapingFeatureFlags.PerSyllable); + this.EnableFeature(buffer, index, count, BlwsTag, ShapingFeatureFlags.ManualJoiners | ShapingFeatureFlags.PerSyllable); + this.EnableFeature(buffer, index, count, PstsTag, ShapingFeatureFlags.ManualJoiners | ShapingFeatureFlags.PerSyllable); + this.EnableFeature(buffer, index, count, HalnTag, ShapingFeatureFlags.ManualJoiners | ShapingFeatureFlags.PerSyllable); this.EnableFeature(buffer, index, count, DistTag); this.EnableFeature(buffer, index, count, AbvmTag); this.EnableFeature(buffer, index, count, BlwmTag); diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs index 36a2adeb0..5379e832f 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs @@ -136,18 +136,18 @@ public MyanmarShaper(ScriptClass script, TextOptions textOptions, FontMetrics fo /// protected override void PlanFeatures(ShapingBuffer buffer, int index, int count) { - this.EnableFeature(buffer, index, count, LoclTag, this.setupSyllablesAction, null); - this.EnableFeature(buffer, index, count, CcmpTag); - - this.EnableFeature(buffer, index, count, RphfTag, this.initialReorderAction, null); - this.EnableFeature(buffer, index, count, PrefTag); - this.EnableFeature(buffer, index, count, BlwfTag); - this.EnableFeature(buffer, index, count, PstfTag); - - this.EnableFeature(buffer, index, count, PresTag); - this.EnableFeature(buffer, index, count, AbvsTag); - this.EnableFeature(buffer, index, count, BlwsTag); - this.EnableFeature(buffer, index, count, PstsTag); + this.EnableFeature(buffer, index, count, LoclTag, ShapingFeatureFlags.PerSyllable, this.setupSyllablesAction, null); + this.EnableFeature(buffer, index, count, CcmpTag, ShapingFeatureFlags.PerSyllable); + + this.EnableFeature(buffer, index, count, RphfTag, ShapingFeatureFlags.ManualZwj | ShapingFeatureFlags.PerSyllable, this.initialReorderAction, null); + this.EnableFeature(buffer, index, count, PrefTag, ShapingFeatureFlags.ManualZwj | ShapingFeatureFlags.PerSyllable); + this.EnableFeature(buffer, index, count, BlwfTag, ShapingFeatureFlags.ManualZwj | ShapingFeatureFlags.PerSyllable); + this.EnableFeature(buffer, index, count, PstfTag, ShapingFeatureFlags.ManualZwj | ShapingFeatureFlags.PerSyllable); + + this.EnableFeature(buffer, index, count, PresTag, ShapingFeatureFlags.ManualZwj); + this.EnableFeature(buffer, index, count, AbvsTag, ShapingFeatureFlags.ManualZwj); + this.EnableFeature(buffer, index, count, BlwsTag, ShapingFeatureFlags.ManualZwj); + this.EnableFeature(buffer, index, count, PstsTag, ShapingFeatureFlags.ManualZwj); } /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs index 607e28933..7ac62a35c 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs @@ -190,31 +190,31 @@ public UniversalShaper(ScriptClass script, TextOptions textOptions, FontMetrics protected override void PlanFeatures(ShapingBuffer buffer, int index, int count) { // Default glyph pre-processing group - this.EnableFeature(buffer, index, count, LoclTag, this.setupSyllablesAction, null); - this.EnableFeature(buffer, index, count, CcmpTag); - this.EnableFeature(buffer, index, count, NuktTag); - this.EnableFeature(buffer, index, count, AkhnTag); + this.EnableFeature(buffer, index, count, LoclTag, ShapingFeatureFlags.PerSyllable, this.setupSyllablesAction, null); + this.EnableFeature(buffer, index, count, CcmpTag, ShapingFeatureFlags.PerSyllable); + this.EnableFeature(buffer, index, count, NuktTag, ShapingFeatureFlags.PerSyllable); + this.EnableFeature(buffer, index, count, AkhnTag, ShapingFeatureFlags.ManualZwj | ShapingFeatureFlags.PerSyllable); // Reordering group. The repha feature varies per glyph: syllable setup // enables it on each syllable's leading glyphs only, so a repha forms // there and nowhere else. - this.AddFeature(buffer, index, count, RphfTag, false, ClearSubstitutionFlags, RecordRhpf); - this.EnableFeature(buffer, index, count, PrefTag, ClearSubstitutionFlags, RecordPref); + this.AddFeature(buffer, index, count, RphfTag, ShapingFeatureFlags.ManualZwj | ShapingFeatureFlags.PerSyllable, false, ClearSubstitutionFlags, RecordRhpf); + this.EnableFeature(buffer, index, count, PrefTag, ShapingFeatureFlags.ManualZwj | ShapingFeatureFlags.PerSyllable, ClearSubstitutionFlags, RecordPref); // Orthographic unit shaping group - this.EnableFeature(buffer, index, count, RkrfTag); - this.EnableFeature(buffer, index, count, AbvfTag); - this.EnableFeature(buffer, index, count, BlwfTag); - this.EnableFeature(buffer, index, count, HalfTag); - this.EnableFeature(buffer, index, count, PstfTag); - this.EnableFeature(buffer, index, count, VatuTag); - this.EnableFeature(buffer, index, count, CjctTag, null, this.reorderAction); + this.EnableFeature(buffer, index, count, RkrfTag, ShapingFeatureFlags.ManualZwj | ShapingFeatureFlags.PerSyllable); + this.EnableFeature(buffer, index, count, AbvfTag, ShapingFeatureFlags.ManualZwj | ShapingFeatureFlags.PerSyllable); + this.EnableFeature(buffer, index, count, BlwfTag, ShapingFeatureFlags.ManualZwj | ShapingFeatureFlags.PerSyllable); + this.EnableFeature(buffer, index, count, HalfTag, ShapingFeatureFlags.ManualZwj | ShapingFeatureFlags.PerSyllable); + this.EnableFeature(buffer, index, count, PstfTag, ShapingFeatureFlags.ManualZwj | ShapingFeatureFlags.PerSyllable); + this.EnableFeature(buffer, index, count, VatuTag, ShapingFeatureFlags.ManualZwj | ShapingFeatureFlags.PerSyllable); + this.EnableFeature(buffer, index, count, CjctTag, ShapingFeatureFlags.ManualZwj | ShapingFeatureFlags.PerSyllable, null, this.reorderAction); // Standard topographic presentation and positional feature application - this.EnableFeature(buffer, index, count, AbvsTag); - this.EnableFeature(buffer, index, count, BlwsTag); - this.EnableFeature(buffer, index, count, PresTag); - this.EnableFeature(buffer, index, count, PstsTag); + this.EnableFeature(buffer, index, count, AbvsTag, ShapingFeatureFlags.ManualZwj); + this.EnableFeature(buffer, index, count, BlwsTag, ShapingFeatureFlags.ManualZwj); + this.EnableFeature(buffer, index, count, PresTag, ShapingFeatureFlags.ManualZwj); + this.EnableFeature(buffer, index, count, PstsTag, ShapingFeatureFlags.ManualZwj); this.EnableFeature(buffer, index, count, DistTag); this.EnableFeature(buffer, index, count, AbvmTag); this.EnableFeature(buffer, index, count, BlwmTag); diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapingFeatureFlags.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapingFeatureFlags.cs new file mode 100644 index 000000000..6fc3438c0 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ShapingFeatureFlags.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.AdvancedTypographic; + +/// +/// Per-feature registration flags controlling how the feature's lookups treat +/// joiners and syllable boundaries during sequence matching. +/// +[Flags] +internal enum ShapingFeatureFlags : byte +{ + /// + /// No flags: the feature's lookups skip the zero width joiner automatically, + /// match the zero width non-joiner, and match across syllables. + /// + None = 0, + + /// + /// The feature's lookups match the zero width non-joiner themselves instead of + /// the matcher treating it as transparent during context matching. + /// + ManualZwnj = 1 << 0, + + /// + /// The feature's lookups match the zero width joiner themselves instead of the + /// matcher treating it as transparent. + /// + ManualZwj = 1 << 1, + + /// + /// The feature's lookups match both joiners themselves. + /// + ManualJoiners = ManualZwnj | ManualZwj, + + /// + /// The feature's lookups never match across syllable boundaries: matching + /// latches the syllable at the cursor and refuses records of any other. + /// + PerSyllable = 1 << 2, +} diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs index ae64241d8..f6db91d84 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs @@ -27,6 +27,72 @@ internal struct SkippingGlyphIterator /// private bool skipsNothing; + /// + /// True when stepping is plain index arithmetic: the lookup flags cannot + /// ignore any glyph and no default-ignorable transparency is active. Folded + /// into one test so the per-step fast path stays a single branch. + /// + private bool stepsDirectly; + + /// + /// The bit recording that default-ignorable + /// transparency is active for the duration of sequence matching; plain + /// stepping outside a match keeps its historical semantics. + /// + private const byte TransparencyActiveFlag = 1 << 0; + + /// + /// The bit recording that the zero width non-joiner + /// is transparent rather than matchable. + /// + private const byte IgnoreZwnjFlag = 1 << 1; + + /// + /// The bit recording that the zero width joiner is + /// transparent rather than matchable. + /// + private const byte IgnoreZwjFlag = 1 << 2; + + /// + /// The bit recording that the substitution-visible + /// ignorables are transparent; set during positioning. + /// + private const byte IgnoreHiddenFlag = 1 << 3; + + /// + /// The bit recording that matching refuses records + /// outside the latched syllable. + /// + private const byte SyllableGateFlag = 1 << 4; + + /// + /// The bit recording that the applying lookup + /// matches per syllable, so stamping latches the syllable at the cursor. + /// + private const byte SyllableLatchFlag = 1 << 5; + + /// + /// The mask matched records must carry a bit of; all bits during context + /// matching, the applying lookup's mask otherwise. + /// + private uint matchMask; + + /// + /// Packed matcher state addressed through the named flag constants above, + /// keeping the struct narrow for the per-attempt copies matching makes. + /// + private byte matchFlags; + + /// + /// The latched syllable serial compared under . + /// + private byte syllableNumber; + + /// + /// The latched syllable type compared under . + /// + private byte syllableType; + /// /// Initializes a new instance of the struct. /// @@ -52,6 +118,8 @@ public SkippingGlyphIterator( this.useMarkFilteringSet = (lookupFlags & LookupFlags.UseMarkFilteringSet) != 0; this.markFilteringSet = markFilteringSet; this.skipsNothing = this.ignoreClassMask == 0 && this.markAttachmentType == 0 && !this.useMarkFilteringSet; + this.stepsDirectly = this.skipsNothing; + this.matchMask = uint.MaxValue; } /// @@ -64,6 +132,12 @@ public SkippingGlyphIterator( /// public int Index { get; set; } + /// + /// Gets a value indicating whether the stamped matcher can encounter a + /// transparent record at all; matching keeps its solid-glyph walk when not. + /// + public readonly bool MatchTransparencyActive => (this.matchFlags & TransparencyActiveFlag) != 0; + /// /// Advances to the next non-skipped glyph in the forward direction. /// @@ -122,6 +196,9 @@ public void Reset(int index, LookupFlags lookupFlags, ushort markFilteringSet) this.useMarkFilteringSet = (lookupFlags & LookupFlags.UseMarkFilteringSet) != 0; this.markFilteringSet = markFilteringSet; this.skipsNothing = this.ignoreClassMask == 0 && this.markAttachmentType == 0 && !this.useMarkFilteringSet; + this.stepsDirectly = this.skipsNothing; + this.matchMask = uint.MaxValue; + this.matchFlags = 0; } /// @@ -132,19 +209,31 @@ private void Move(int direction) { this.Index += direction; - // When the flags cannot ignore anything, ShouldIgnore is provably false for - // every glyph: the class mask test is against zero and the mark branches are - // disabled. Skip the per-glyph fetch and classification entirely. - if (this.skipsNothing) + // When the flags cannot ignore anything and no transparency is active, + // ShouldIgnore is provably false for every glyph: the class mask test is + // against zero and the mark branches are disabled. Skip the per-glyph + // fetch and classification entirely. + if (this.stepsDirectly) { return; } while (this.Index >= 0 && this.Index < this.Collection.Count) { - if (!this.ShouldIgnore(this.Index)) + // The class-mask test only runs when the flags can actually ignore + // something; a skips-nothing iterator steps straight to transparency. + if (this.skipsNothing || !this.ShouldIgnore(this.Index)) { - break; + if ((this.matchFlags & TransparencyActiveFlag) == 0) + { + break; + } + + ref GlyphShapingData data = ref this.Collection[this.Index]; + if (!this.IsTransparent(ref data)) + { + break; + } } this.Index += direction; @@ -161,6 +250,116 @@ private void Move(int direction) /// if the glyph is ignored; otherwise, . public readonly bool IsIgnored(int index) => !this.skipsNothing && this.ShouldIgnore(index); + /// + /// Packs the matcher flags for the buffer's applying lookup, computed once + /// when the lookup is stamped rather than on every match attempt: which + /// joiner classes are transparent, whether transparency can apply at all, + /// and whether stamping latches a syllable. + /// + /// The buffer carrying the applying lookup's state. + /// Whether the flags serve backtrack or lookahead matching. + /// The packed matcher flags. + public static byte PackMatchFlags(ShapingBuffer buffer, bool contextMatch) + { + bool positioning = buffer.Role == ShapingBufferRole.Positioning; + + // A buffer holding no default ignorables cannot contain a transparent + // record, so matching keeps its solid-glyph fast paths. + byte flags = 0; + if (buffer.HasDefaultIgnorables) + { + flags |= TransparencyActiveFlag; + } + + if (positioning || (contextMatch && buffer.LookupAutoZwnj)) + { + flags |= IgnoreZwnjFlag; + } + + if (contextMatch || buffer.LookupAutoZwj) + { + flags |= IgnoreZwjFlag; + } + + if (positioning) + { + flags |= IgnoreHiddenFlag; + } + + if (!positioning && buffer.LookupPerSyllable) + { + flags |= SyllableLatchFlag; + } + + return flags; + } + + /// + /// Activates default-ignorable transparency for sequence matching under the + /// applying lookup's joiner handling: a default ignorable whose joiner bits + /// the lookup treats as transparent is stepped over unless it matches the + /// sequence position itself. Latches the syllable at the current index when + /// the lookup matches per syllable. Copies of the iterator carry the stamped + /// state; the caller's iterator is unaffected by helpers stamping their own + /// copies. + /// + /// The applying lookup's mask; ignored during context matching. + /// Whether this matcher walks backtrack or lookahead context. + public void SetMatchContext(uint mask, bool contextMatch) + { + ShapingBuffer buffer = this.Collection; + byte flags = contextMatch ? buffer.ContextMatchFlags : buffer.InputMatchFlags; + + this.matchMask = contextMatch ? uint.MaxValue : mask; + if ((flags & SyllableLatchFlag) != 0 && (uint)this.Index < (uint)buffer.Count) + { + SyllableInfo syllable = buffer[this.Index].Syllable; + this.syllableNumber = (byte)syllable.Number; + this.syllableType = (byte)syllable.Type; + if (syllable.Type != SyllableType.None || syllable.Number != 0) + { + flags |= SyllableGateFlag; + } + } + + this.matchFlags = flags; + this.stepsDirectly = this.skipsNothing && (flags & TransparencyActiveFlag) == 0; + } + + /// + /// Determines whether the record is transparent to the current matcher: a + /// default ignorable whose joiner bits the matcher ignores. Transparent + /// records are stepped over during matching unless they match the sequence + /// position themselves. + /// + /// The record to test. + /// when the record may be stepped over. + public readonly bool IsTransparent(ref GlyphShapingData data) + => (this.matchFlags & TransparencyActiveFlag) != 0 + && data.IsDefaultIgnorable + && ((this.matchFlags & IgnoreZwnjFlag) != 0 || !data.IsZwnj) + && ((this.matchFlags & IgnoreZwjFlag) != 0 || !data.IsZwj) + && ((this.matchFlags & IgnoreHiddenFlag) != 0 || !data.IsHiddenIgnorable); + + /// + /// Determines whether the record passes the matcher's mask and syllable + /// gates; the shape test itself runs only for records that do. + /// + /// The record to test. + /// when the record may be match-tested. + public readonly bool MayMatch(ref GlyphShapingData data) + => (data.FeatureMask & this.matchMask) != 0 + && ((this.matchFlags & SyllableGateFlag) == 0 || (data.Syllable.Number == this.syllableNumber && (byte)data.Syllable.Type == this.syllableType)); + + /// + /// Determines whether the glyph at the given index fails the lookup's glyph + /// property check and is therefore always skipped, whatever the sequence + /// expects. + /// + /// The index of the glyph to check. + /// when the glyph never participates. + public readonly bool IsPropertySkipped(int index) => !this.skipsNothing && this.ShouldIgnore(index); + /// /// Determines whether the glyph at the given index should be ignored based on the current lookup flags. /// diff --git a/src/SixLabors.Fonts/TextShaper.Pipeline.cs b/src/SixLabors.Fonts/TextShaper.Pipeline.cs index fe0a29665..573d1ca1e 100644 --- a/src/SixLabors.Fonts/TextShaper.Pipeline.cs +++ b/src/SixLabors.Fonts/TextShaper.Pipeline.cs @@ -729,21 +729,11 @@ private static void PopulateAndSubstitute( charIndex += charsConsumed; - // Get the glyph id for the codepoint and add to the buffer. - bool hasGlyph = substitutions.TryGetGlyphId(font.FontMetrics, current, next, out ushort glyphId, out skipNextCodePoint); - - // Unsupported default-ignorable code points such as FE0F should not block - // GSUB sequences like emoji ZWJ ligatures. Preserve joiners explicitly. - if (!hasGlyph && - UnicodeUtility.IsDefaultIgnorableCodePoint((uint)current.Value) && - !UnicodeUtility.ShouldRenderWhiteSpaceOnly(current) && - !CodePoint.IsZeroWidthJoiner(current) && - !CodePoint.IsZeroWidthNonJoiner(current)) - { - codePointIndex++; - graphemeCodePointIndex++; - continue; - } + // Get the glyph id for the codepoint and add to the buffer. Every + // codepoint enters the buffer, including unmapped default + // ignorables as the missing glyph: sequence matching treats them + // as transparent and the hide stage replaces them at the end. + substitutions.TryGetGlyphId(font.FontMetrics, current, next, out ushort glyphId, out skipNextCodePoint); substitutions.AddGlyph(glyphId, current, (TextDirection)bidiRuns[bidiRunIndex].Direction, (ushort)textRunIndex, codePointIndex); diff --git a/tests/SixLabors.Fonts.Tests/HarfBuzzDifferentialTests.cs b/tests/SixLabors.Fonts.Tests/HarfBuzzDifferentialTests.cs index 77d9c7956..d768f5855 100644 --- a/tests/SixLabors.Fonts.Tests/HarfBuzzDifferentialTests.cs +++ b/tests/SixLabors.Fonts.Tests/HarfBuzzDifferentialTests.cs @@ -31,6 +31,13 @@ public static TheoryData ShapingCases() { TestFonts.NotoSansDevanagariRegular, "เค•เฅเคทเคคเฅเคฐเคฟเคฏ เคฆเฅเคตเคพเคฐเคพ เคชเฅเคฐเค•เฅƒเคคเคฟ เค•เฅ€ เคฐเค•เฅเคทเคพ เค•เคฐเฅเคคเคตเฅเคฏ เคนเฅˆ", false }, { TestFonts.NotoSansDevanagariRegular, "เคถเฅเคฐเคฆเฅเคงเคพเค‚เคœเคฒเคฟ", false }, + // Emoji joiner sequences: the zero width joiner and variation selector + // participate in the fonts' sequence lookups (including contextual + // rules whose lookahead spans them) and then render invisibly. + { TestFonts.SegoeuiEmojiFile, "\U0001F469\U0001F3FB\u200D\U0001F91D\u200D\U0001F469\U0001F3FC", false }, + { TestFonts.NotoColorEmojiRegular, "\u2764\uFE0F\u200D\U0001F525", false }, + { TestFonts.NotoColorEmojiRegular, "\u2764\uFE0F\u200D\U0001FA79", false }, + // Joiners inside shaping contexts: the joiner must steer the shaping // (ligature suppression/formation, joining forms, half forms) and then // render invisibly at zero advance. diff --git a/tests/SixLabors.Fonts.Tests/TextShaperTests.cs b/tests/SixLabors.Fonts.Tests/TextShaperTests.cs index 7cf1b3700..e420b0322 100644 --- a/tests/SixLabors.Fonts.Tests/TextShaperTests.cs +++ b/tests/SixLabors.Fonts.Tests/TextShaperTests.cs @@ -290,15 +290,23 @@ public void Shape_ReusedBuffer_SteadyStateDoesNotAllocate(string scenario) TextOptions options = new(font); TextShapingBuffer buffer = new(); - for (int i = 0; i < 16; i++) + // Parallel tests share the pipeline's scratch pool, so any single call may + // rent state another test left cold for this font and pay its one-time + // population. Steady state is the minimum over several attempts: one + // zero-allocation call proves the warmed path allocates nothing. + long minimum = long.MaxValue; + for (int attempt = 0; attempt < 10 && minimum != 0; attempt++) { + for (int i = 0; i < 8; i++) + { + TextShaper.Shape(text, options, buffer); + } + + long before = GC.GetAllocatedBytesForCurrentThread(); TextShaper.Shape(text, options, buffer); + minimum = Math.Min(minimum, GC.GetAllocatedBytesForCurrentThread() - before); } - long before = GC.GetAllocatedBytesForCurrentThread(); - TextShaper.Shape(text, options, buffer); - long allocated = GC.GetAllocatedBytesForCurrentThread() - before; - - Assert.Equal(0, allocated); + Assert.Equal(0, minimum); } } From d4b10dcd7aef22315fd54f31227337f1c7a6c028 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 26 Jul 2026 12:11:47 +1000 Subject: [PATCH 64/94] Update BenchmarkDotNet and benchmark the host runtime BenchmarkDotNet 0.13.12 predates this machine's Windows build and the .NET 10 runtime: it reported an unknown processor and its runtime moniker list stops at net90. 0.15.8 identifies the hardware and adds the net10_0 toolchain. The benchmark configuration previously pinned its jobs to .NET 8, so every run measured that runtime regardless of host; the jobs now take the host runtime, so the target framework switch selects the measured runtime per run. --- tests/Directory.Build.targets | 2 +- .../SixLabors.Fonts.Benchmarks/Config.cs | 14 ++++++++++++-- .../SixLabors.Fonts.Benchmarks.csproj | 2 +- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/Directory.Build.targets b/tests/Directory.Build.targets index 907e37fa7..b9467eaa2 100644 --- a/tests/Directory.Build.targets +++ b/tests/Directory.Build.targets @@ -18,7 +18,7 @@ - + diff --git a/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/Config.cs b/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/Config.cs index 4ffd1b34a..58f72c95f 100644 --- a/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/Config.cs +++ b/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/Config.cs @@ -35,16 +35,26 @@ public Config() this.SummaryStyle = SummaryStyle.Default.WithMaxParameterColumnWidth(50); } + /// + /// Gets the core runtime matching the host process, so running the suite + /// from a given target framework benchmarks that same runtime. + /// + private static CoreRuntime HostRuntime => Environment.Version.Major switch + { + 10 => CoreRuntime.Core10_0, + _ => CoreRuntime.Core80, + }; + public class Standard : Config { public Standard() => this.AddJob( - Job.Default.WithRuntime(CoreRuntime.Core80).WithArguments([new MsBuildArgument("/p:DebugType=portable")])); + Job.Default.WithRuntime(HostRuntime).WithArguments([new MsBuildArgument("/p:DebugType=portable")])); } public class Short : Config { public Short() => this.AddJob( - Job.Default.WithRuntime(CoreRuntime.Core80) + Job.Default.WithRuntime(HostRuntime) .WithLaunchCount(1) .WithWarmupCount(3) .WithIterationCount(3) diff --git a/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks.csproj b/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks.csproj index e446d31a4..3d6ec4c95 100644 --- a/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks.csproj +++ b/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks.csproj @@ -33,7 +33,7 @@ - + From 1e684f7d96fa1ef7893f0455964bbb9deaa584c1 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 26 Jul 2026 12:47:27 +1000 Subject: [PATCH 65/94] Collect feature registrations once per plan Every planning pass re-registered the plan's features from scratch: bit assignment, stage list, and joiner-flag scans for a dozen or more features per segment, plus one whole-segment mask walk per added feature - all deterministic for the plan's identity and therefore identical on every shape of the same configuration. The first pass now collects: registrations run exactly as before while whole-segment feature additions fold their registered and enabled bits into two accumulated masks on the shaper. Later passes skip the three registration phases, apply the fold in a single walk over the segment, and run only the per-text work: direction-span masks through the new mask setup hook (registration for a direction first seen on a later text still lands through the same idempotent path), and the shapers' feature assignment. A mixed-vertical layout decides features from the glyphs themselves, so it never latches and re-plans every pass. The Hangul contextual-alternates demotion routes through the standard feature addition so its whole-segment mask folds with the rest. Benchmark (15 iterations, clean windows, controls at idle): | Scenario | Before (.NET 8) | After (.NET 8) | |------------|-----------------------|-----------------------| | Latin | 4.01 us / 1.78x / 0 B | 3.80 us / 1.70x / 0 B | | Arabic | 6.92 us / 1.78x / 0 B | 6.39 us / 1.68x / 0 B | | Devanagari | 11.43 us / 1.28x / 0 B| 10.49 us / 1.17x / 0 B | | Scenario | Before (.NET 10) | After (.NET 10) | |------------|-----------------------|-----------------------| | Latin | 3.54 us / 1.57x / 0 B | 3.29 us / 1.49x / 0 B | | Arabic | 6.06 us / 1.59x / 0 B | 5.54 us / 1.48x / 0 B | | Devanagari | 10.37 us / 1.17x / 0 B| 9.54 us / 1.09x / 0 B | Suite 5,603/0; emoji ZWJ and joiner differentials vs HarfBuzzSharp all match; zero steady-state allocation preserved on all three scripts. --- src/SixLabors.Fonts/ShapingBuffer.cs | 20 +++ .../AdvancedTypographic/Shapers/BaseShaper.cs | 120 +++++++++++++++--- .../Shapers/DefaultShaper.cs | 47 ++++++- .../Shapers/HangulShaper.cs | 2 +- 4 files changed, 162 insertions(+), 27 deletions(-) diff --git a/src/SixLabors.Fonts/ShapingBuffer.cs b/src/SixLabors.Fonts/ShapingBuffer.cs index a31d8fc85..eebabca03 100644 --- a/src/SixLabors.Fonts/ShapingBuffer.cs +++ b/src/SixLabors.Fonts/ShapingBuffer.cs @@ -584,6 +584,26 @@ public void AddShapingFeatureRange(int index, int count, TagEntry feature, uint } } + /// + /// Applies the plan's folded whole-segment feature masks in a single walk: + /// the registered and enabled bits every replayed planning pass would + /// otherwise add one feature range at a time. + /// + /// The zero-based index of the first record. + /// The number of records. + /// The fold of the registered feature bits. + /// The fold of the enabled feature bits. + public void AddShapingFeatureMasks(int index, int count, uint registeredMask, uint enabledMask) + { + int end = index + count; + for (int i = index; i < end; i++) + { + ref GlyphShapingData item = ref this.data[i]; + item.RegisteredFeatureMask |= registeredMask; + item.FeatureMask |= enabledMask; + } + } + /// /// Enables a previously added shaping feature by its plan-assigned mask bit. /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs index 2733d1624..9c4dd61fb 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs @@ -19,6 +19,46 @@ internal abstract class BaseShaper /// public ShapePlanFeatures Features { get; } = new(); + /// + /// Gets a value indicating whether the first planning pass has collected the + /// plan's feature registrations. Registration is deterministic for a plan's + /// identity, so later passes replay the collected masks instead of + /// re-registering. + /// + public bool FeaturesCollected { get; private set; } + + /// + /// Gets or sets a value indicating whether the first planning pass is + /// currently collecting registrations; whole-segment feature additions fold + /// their masks only while this is set. Shapers clear it around additions + /// whose ranges depend on the text, such as direction spans. + /// + public bool CollectingFeatures { get; protected set; } + + /// + /// Gets or sets the registered-mask fold of every whole-segment feature the + /// collecting pass added, replayed over later segments in one walk. + /// + public uint FoldedRegisteredMask { get; protected set; } + + /// + /// Gets or sets the enabled-mask fold paired with + /// . + /// + public uint FoldedEnabledMask { get; protected set; } + + /// + /// Gets the segment start the collecting pass is planning; feature additions + /// fold only when their range is exactly the planned segment. + /// + public int CollectSegmentIndex { get; private set; } + + /// + /// Gets the segment length the collecting pass is planning, tracking count + /// adjustments between phases. + /// + public int CollectSegmentCount { get; private set; } + /// /// Gets or sets the script classification for this shaper. /// @@ -37,31 +77,73 @@ internal abstract class BaseShaper /// The number of elements to assign. public void Plan(ShapingBuffer buffer, int index, int count) { - int collectionCount = buffer.Count; - - var preProbe = ShapingProbe.Enter(); - this.PlanPreprocessingFeatures(buffer, index, count); - ShapingProbe.Exit(ShapingProbe.PlanPre, preProbe); - - RecalculateCount(buffer, ref collectionCount, ref count); - - var mainProbe = ShapingProbe.Enter(); - this.PlanFeatures(buffer, index, count); - ShapingProbe.Exit(ShapingProbe.PlanMain, mainProbe); - - RecalculateCount(buffer, ref collectionCount, ref count); - - var postProbe = ShapingProbe.Enter(); - this.PlanPostprocessingFeatures(buffer, index, count); - ShapingProbe.Exit(ShapingProbe.PlanPost, postProbe); - - RecalculateCount(buffer, ref collectionCount, ref count); + // Registration is deterministic for a plan's identity, so the first pass + // collects it once: feature bits, stages, joiner flags, and the fold of + // every whole-segment mask. Later passes replay the fold in one walk and + // run only the per-text work. A mixed-vertical layout decides features + // from the glyphs themselves, so it re-plans every pass. + if (!this.FeaturesCollected) + { + int collectionCount = buffer.Count; + this.CollectingFeatures = true; + this.CollectSegmentIndex = index; + this.CollectSegmentCount = count; + + var preProbe = ShapingProbe.Enter(); + this.PlanPreprocessingFeatures(buffer, index, count); + ShapingProbe.Exit(ShapingProbe.PlanPre, preProbe); + + RecalculateCount(buffer, ref collectionCount, ref count); + this.CollectSegmentCount = count; + + var mainProbe = ShapingProbe.Enter(); + this.PlanFeatures(buffer, index, count); + ShapingProbe.Exit(ShapingProbe.PlanMain, mainProbe); + + RecalculateCount(buffer, ref collectionCount, ref count); + this.CollectSegmentCount = count; + + var postProbe = ShapingProbe.Enter(); + this.PlanPostprocessingFeatures(buffer, index, count); + ShapingProbe.Exit(ShapingProbe.PlanPost, postProbe); + + RecalculateCount(buffer, ref collectionCount, ref count); + + this.CollectingFeatures = false; + this.FeaturesCollected = !buffer.TextOptions.LayoutMode.IsVerticalMixed(); + } + else + { + var foldProbe = ShapingProbe.Enter(); + if (this.FoldedRegisteredMask != 0) + { + buffer.AddShapingFeatureMasks(index, count, this.FoldedRegisteredMask, this.FoldedEnabledMask); + } + + ShapingProbe.Exit(ShapingProbe.PlanPre, foldProbe); + } + + var masksProbe = ShapingProbe.Enter(); + this.SetupMasks(buffer, index, count); + ShapingProbe.Exit(ShapingProbe.PlanPost, masksProbe); var assignProbe = ShapingProbe.Enter(); this.AssignFeatures(buffer, index, count); ShapingProbe.Exit(ShapingProbe.PlanAssign, assignProbe); } + /// + /// Applies the per-text feature masks that cannot fold into the collected + /// whole-segment masks, such as spans that follow the resolved direction of + /// the records. Runs on every planning pass, after collection or replay. + /// + /// The glyph shaping buffer. + /// The zero-based index of the elements to assign. + /// The number of elements to assign. + protected virtual void SetupMasks(ShapingBuffer buffer, int index, int count) + { + } + /// /// Assigns the features to each glyph within the buffer. /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs index aeff42b30..73ac99761 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs @@ -178,10 +178,31 @@ protected override void PlanPreprocessingFeatures(ShapingBuffer buffer, int inde // Add variation Features. this.EnableFeature(buffer, index, count, RvnrTag); - // Add directional features once per direction span. A segment may span - // direction runs, so these stay varying features whose bits cover only - // their own span; a global classification would let one direction's - // lookups match glyphs of the other. + this.AddDirectionalFeatures(buffer, index, count); + + // TODO: Fractional feature should be assigned here but disabled. + // They should then be enabled in AssignFeatures. + } + + /// + protected override void SetupMasks(ShapingBuffer buffer, int index, int count) + => this.AddDirectionalFeatures(buffer, index, count); + + /// + /// Adds the directional features once per direction span. A segment may span + /// direction runs, so these stay varying features whose bits cover only + /// their own span; a global classification would let one direction's + /// lookups match glyphs of the other. Their masks depend on each text's + /// resolved directions, so they never fold and re-apply on every pass. + /// + /// The glyph shaping buffer. + /// The zero-based index of the first element. + /// The number of elements. + private void AddDirectionalFeatures(ShapingBuffer buffer, int index, int count) + { + bool collecting = this.CollectingFeatures; + this.CollectingFeatures = false; + int end = index + count; int spanStart = index; while (spanStart < end) @@ -208,8 +229,7 @@ protected override void PlanPreprocessingFeatures(ShapingBuffer buffer, int inde spanStart = spanEnd; } - // TODO: Fractional feature should be assigned here but disabled. - // They should then be enabled in AssignFeatures. + this.CollectingFeatures = collecting; } /// @@ -304,7 +324,20 @@ protected void AddFeature( } } - buffer.AddShapingFeatureRange(index, count, new TagEntry(feature, enabled), this.Features.GetOrAddMask(feature)); + uint mask = this.Features.GetOrAddMask(feature); + + // A whole-segment addition during the collecting pass folds into the + // masks the replay pass applies in one walk. + if (this.CollectingFeatures && index == this.CollectSegmentIndex && count == this.CollectSegmentCount) + { + this.FoldedRegisteredMask |= mask; + if (enabled) + { + this.FoldedEnabledMask |= mask; + } + } + + buffer.AddShapingFeatureRange(index, count, new TagEntry(feature, enabled), mask); this.AddStage(feature, preAction, postAction); } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs index cb1f70cbf..e8fcd168d 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs @@ -196,7 +196,7 @@ protected override void PlanPostprocessingFeatures(ShapingBuffer buffer, int ind // off by default: no glyph applies it unless a later range registration // enables it, and feature assignment clears it on jamo even then, so the // jamo lookups such fonts hide behind the feature can never fire there. - buffer.AddShapingFeatureRange(index, count, new TagEntry(CaltTag, false), this.Features.GetOrAddMask(CaltTag)); + this.AddFeature(buffer, index, count, CaltTag, false); } /// From 64aedac06289529ec23b7b7a6a82f8ace22ec7c0 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 26 Jul 2026 13:03:09 +1000 Subject: [PATCH 66/94] Remove the shaping probe instrumentation The hand-rolled phase and counter probe rode the hot paths gated on a mutable static the JIT cannot fold: both table drivers paid a load and branch per considered lookup and per glyph gate, the skipping iterator per step, and the drivers a timestamp call per applied feature, all with probes disabled. Real profilers cover its job, and the instrumentation has no place in the shipped pipeline whatever it costs. The suite count drops by two because the buffer convention tests generate one case per source file and the probe's file is gone. Suite 5,601/0; emoji ZWJ and joiner differentials vs HarfBuzzSharp all match; zero steady-state allocation preserved on all three scripts. --- src/SixLabors.Fonts/ShapingProbe.cs | 194 ------------------ .../AdvancedTypographicUtils.cs | 24 --- .../GPos/LookupListTable.cs | 5 - .../Tables/AdvancedTypographic/GPosTable.cs | 5 - .../GSub/LookupListTable.cs | 5 - .../Tables/AdvancedTypographic/GSubTable.cs | 35 ---- .../AdvancedTypographic/Shapers/BaseShaper.cs | 13 -- .../SkippingGlyphIterator.cs | 5 - src/SixLabors.Fonts/TextShaper.Pipeline.cs | 25 --- src/SixLabors.Fonts/TextShaper.cs | 2 - 10 files changed, 313 deletions(-) delete mode 100644 src/SixLabors.Fonts/ShapingProbe.cs diff --git a/src/SixLabors.Fonts/ShapingProbe.cs b/src/SixLabors.Fonts/ShapingProbe.cs deleted file mode 100644 index b5dd7557d..000000000 --- a/src/SixLabors.Fonts/ShapingProbe.cs +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -// TEMPORARY diagnostic probe for shaping performance attribution. Delete before commit. -using System.Diagnostics; -using SixLabors.Fonts.Tables.AdvancedTypographic; - -namespace SixLabors.Fonts; - -#pragma warning disable CS1591, SA1600, SA1401, CA2211, SA1201 - -public static class ShapingProbe -{ - public const int BuildTextRuns = 0; - public const int Bidi = 1; - public const int Populate = 2; - public const int Mirrors = 3; - public const int Substitution = 4; - public const int MetricsAdd = 5; - public const int Positioning = 6; - public const int Projection = 7; - public const int LookupResolve = 8; - public const int SubShaperCreate = 9; - public const int SubShaperPlan = 10; - public const int SubStagePrePost = 11; - public const int SubStageApply = 12; - public const int PlanPre = 13; - public const int PlanMain = 14; - public const int PlanPost = 15; - public const int PlanAssign = 16; - public const int GraphemeCount = 17; - - private static readonly string[] Names = - [ - "BuildTextRuns", - "Bidi", - "Populate (glyph ids)", - "Bidi mirrors", - "GSUB substitution", - "Metrics add (clones)", - "GPOS positioning", - "Projection (ShapedGlyph)", - " of which lookup resolve", - " sub: shaper create", - " sub: shaper plan", - " sub: stage pre/post", - " sub: stage apply", - " plan: preprocessing", - " plan: features", - " plan: postprocessing", - " plan: assign", - " runs: grapheme count", - ]; - - private static readonly long[] Ticks = new long[Names.Length]; - private static readonly long[] Bytes = new long[Names.Length]; - - public static bool Enabled { get; set; } - - /// - /// Gets or sets a value indicating whether phase probes also sample allocated - /// bytes. Byte sampling costs far more than timestamping, so disable it when only - /// phase timings matter: nested probe pairs otherwise inflate every enclosing - /// phase's measured time. - /// - public static bool BytesEnabled { get; set; } = true; - - /// - /// Gets or sets a value indicating whether per-feature timing detail is recorded. - /// Feature exits build a key string and touch three dictionaries per feature per - /// pass, which distorts the enclosing substitution and positioning phase timings - /// when only phases matter. - /// - public static bool FeatureDetailEnabled { get; set; } = true; - - public static readonly Dictionary FeatureTicks = []; - public static readonly Dictionary FeatureApplies = []; - - public static long IteratorSteps; - - public static long ContextIterators; - - public static long ClassifyCalls; - - public static long ClassifyMisses; - - public static long SubTableProbes; - - public static long StageFeatureCalls; - public static long LookupsConsidered; - public static long LookupsSkippedByDigest; - public static long GlyphGateChecks; - public static long SubstitutionAttempts; - - public static void PrintCounters(int iterations) - => Console.WriteLine( - $"iterSteps/op={IteratorSteps / (double)iterations:F1} ctxIters/op={ContextIterators / (double)iterations:F1} classify/op={ClassifyCalls / (double)iterations:F1} classifyMiss/op={ClassifyMisses / (double)iterations:F1} subProbes/op={SubTableProbes / (double)iterations:F1} " + - $"lookups/op={LookupsConsidered / (double)iterations:F1} " + - $"digestSkipped/op={LookupsSkippedByDigest / (double)iterations:F1} " + - $"glyphGates/op={GlyphGateChecks / (double)iterations:F1} " + - $"substAttempts/op={SubstitutionAttempts / (double)iterations:F1}"); - - public static void ResetCounters() - => StageFeatureCalls = LookupsConsidered = LookupsSkippedByDigest = GlyphGateChecks = SubstitutionAttempts = - IteratorSteps = ContextIterators = ClassifyCalls = ClassifyMisses = SubTableProbes = 0; - - public static long FeatureStepsStart; - - public static long Timestamp() - { - FeatureStepsStart = IteratorSteps; - return Enabled ? Stopwatch.GetTimestamp() : 0; - } - - public static readonly Dictionary FeatureSteps = []; - - public static void ExitFeature(string table, Tag feature, long start, long applies) - { - if (!Enabled || !FeatureDetailEnabled) - { - return; - } - - string key = $"{table}:{feature}"; - FeatureTicks[key] = FeatureTicks.GetValueOrDefault(key) + Stopwatch.GetTimestamp() - start; - FeatureApplies[key] = FeatureApplies.GetValueOrDefault(key) + applies; - FeatureSteps[key] = FeatureSteps.GetValueOrDefault(key) + IteratorSteps - FeatureStepsStart; - } - - public static void PrintFeatures(int iterations) - { - foreach (KeyValuePair pair in FeatureTicks.OrderByDescending(x => x.Value)) - { - double us = pair.Value * 1_000_000.0 / Stopwatch.Frequency / iterations; - Console.WriteLine($" {pair.Key,-16} {us,8:F2} us/op applies/op={FeatureApplies.GetValueOrDefault(pair.Key) / (double)iterations:F1} steps/op={FeatureSteps.GetValueOrDefault(pair.Key) / (double)iterations:F1}"); - } - - FeatureTicks.Clear(); - FeatureApplies.Clear(); - FeatureSteps.Clear(); - } - - public static (long Ticks, long Bytes) Enter() - { - if (!Enabled) - { - return default; - } - - return (Stopwatch.GetTimestamp(), BytesEnabled ? GC.GetAllocatedBytesForCurrentThread() : 0); - } - - public static void Exit(int phase, (long Ticks, long Bytes) entry) - { - if (!Enabled) - { - return; - } - - if (BytesEnabled) - { - Bytes[phase] += GC.GetAllocatedBytesForCurrentThread() - entry.Bytes; - } - - Ticks[phase] += Stopwatch.GetTimestamp() - entry.Ticks; - } - - public static void Reset() - { - Array.Clear(Ticks); - Array.Clear(Bytes); - } - - public static void Print(int iterations) - { - long totalTicks = 0; - long totalBytes = 0; - for (int i = 0; i < Names.Length; i++) - { - totalTicks += Ticks[i]; - totalBytes += Bytes[i]; - } - - Console.WriteLine($"{"Phase",-26} {"us/op",10} {"%time",7} {"B/op",10} {"%alloc",7}"); - for (int i = 0; i < Names.Length; i++) - { - double us = Ticks[i] * 1_000_000.0 / Stopwatch.Frequency / iterations; - double bytes = (double)Bytes[i] / iterations; - Console.WriteLine($"{Names[i],-26} {us,10:F2} {(totalTicks > 0 ? Ticks[i] * 100.0 / totalTicks : 0),6:F1}% {bytes,10:F0} {(totalBytes > 0 ? Bytes[i] * 100.0 / totalBytes : 0),6:F1}%"); - } - - Console.WriteLine($"{"TOTAL",-26} {totalTicks * 1_000_000.0 / Stopwatch.Frequency / iterations,10:F2} {(double)totalBytes / iterations,10:F0}"); - } -} diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs index 7f0a7a44e..5e278a879 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs @@ -119,11 +119,6 @@ public static bool ApplyLookupList( int count) { SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, lookupFlags, markFilteringSet); - if (ShapingProbe.Enabled) - { - ShapingProbe.ContextIterators++; - } - int currentCount = buffer.Count; // Nested lookups mutate the input side in place whatever their type: the @@ -176,11 +171,6 @@ public static bool ApplyLookupList( int count) { SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, lookupFlags, markFilteringSet); - if (ShapingProbe.Enabled) - { - ShapingProbe.ContextIterators++; - } - foreach (SequenceLookupRecord lookupRecord in records) { ushort sequenceIndex = lookupRecord.SequenceIndex; @@ -502,10 +492,6 @@ public static bool CheckAllCoverages( int endExclusive = index + count; SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, lookupFlags, markFilteringSet); - if (ShapingProbe.Enabled) - { - ShapingProbe.ContextIterators++; - } // Compute backtrack start using skippy prev(), not index-1: context steps // are transparent to joiners, so the step lands on the first solid glyph. @@ -651,21 +637,11 @@ public static GlyphShapingClass GetGlyphShapingClass(FontMetrics fontMetrics, Sh { // Cache the shaping class on the GlyphShapingData to avoid repeated GDEF lookups. // The cache key stores the glyph id; -1 means "not cached". - if (ShapingProbe.Enabled) - { - ShapingProbe.ClassifyCalls++; - } - if (shapingData.ShapingClassCacheKey == glyphId) { return shapingData.CachedShapingClass; } - if (ShapingProbe.Enabled) - { - ShapingProbe.ClassifyMisses++; - } - if (buffer.TryGetShapingClass(fontMetrics, glyphId, out GlyphShapingClass cached)) { shapingData.CachedShapingClass = cached; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupListTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupListTable.cs index c94748f39..cc1676a0f 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupListTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupListTable.cs @@ -239,11 +239,6 @@ public bool TryUpdatePosition( continue; } - if (ShapingProbe.Enabled) - { - ShapingProbe.SubTableProbes++; - } - // A lookup is finished for a glyph after the client locates the target // glyph or glyph context and performs a positioning action, if specified. if (subTable.TryUpdatePosition(fontMetrics, table, buffer, feature, index, count)) diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs index bfa682366..8a05ad6ef 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs @@ -341,8 +341,6 @@ private bool PositionSegment( } iterator.Reset(index, featureLookupTable.LookupFlags, featureLookupTable.MarkFilteringSet); - long featureStart = ShapingProbe.Timestamp(); - long featureApplies = 0; while (iterator.Index < index + count) { @@ -363,13 +361,10 @@ private bool PositionSegment( } bool success = featureLookupTable.TryUpdatePosition(fontMetrics, this, buffer, feature, iterator.Index, count - (iterator.Index - index)); - featureApplies++; kerned |= success && (feature == KernTag || feature == VKernTag); updated |= success; iterator.Next(); } - - ShapingProbe.ExitFeature("GPOS", feature, featureStart, featureApplies); } shapingStages[group.End - 1].PostProcessFeature(shapePlan, buffer, index, count); diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs index 5e69a007d..08e878e4d 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs @@ -218,11 +218,6 @@ public bool TrySubstitution( continue; } - if (ShapingProbe.Enabled) - { - ShapingProbe.SubTableProbes++; - } - // Nested applications never consume through the pass cursor: the outer // contextual match owns it, and its replacements run in place. buffer.DirectConsume = !buffer.IsNestedApplication && subTable.ConsumesDirectly; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs index c02cb69d6..0bfefa970 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs @@ -194,9 +194,7 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas } Tag unicodeScriptTag = this.GetUnicodeScriptTag(current); - var createProbe = ShapingProbe.Enter(); ShapePlan shapePlan = buffer.GetOrCreatePlan(current, unicodeScriptTag, fontMetrics); - ShapingProbe.Exit(ShapingProbe.SubShaperCreate, createProbe); BaseShaper shaper = shapePlan.Shaper; @@ -204,9 +202,7 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas // Shapers can adjust the count during initialization and feature processing so we must capture // the current count to allow resetting indexes and processing counts. int collectionCount = buffer.Count; - var planProbe = ShapingProbe.Enter(); shaper.Plan(buffer, index, count); - ShapingProbe.Exit(ShapingProbe.SubShaperPlan, planProbe); int delta = buffer.Count - collectionCount; i += delta; count += delta; @@ -226,16 +222,13 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas ShapePlanStageGroup group = groups[g]; collectionCount = buffer.Count; - var preProbe = ShapingProbe.Enter(); stages[group.Start].PreProcessFeature(shapePlan, buffer, index, count); - ShapingProbe.Exit(ShapingProbe.SubStagePrePost, preProbe); // Account for substitutions changing the length of the buffer. delta = buffer.Count - collectionCount; count += delta; i += delta; - var applyProbe = ShapingProbe.Enter(); this.ApplyMergedLookups( fontMetrics, buffer, @@ -248,12 +241,9 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas maxCount, maxOperationsCount, ref currentOperations); - ShapingProbe.Exit(ShapingProbe.SubStageApply, applyProbe); collectionCount = buffer.Count; - var postProbe = ShapingProbe.Enter(); stages[group.End - 1].PostProcessFeature(shapePlan, buffer, index, count); - ShapingProbe.Exit(ShapingProbe.SubStagePrePost, postProbe); // Account for substitutions changing the length of the buffer. delta = buffer.Count - collectionCount; @@ -303,25 +293,13 @@ private void ApplyMergedLookups( // Skip the whole lookup when its coverage cannot intersect any glyph id // the buffer has ever contained; most fonts carry many lookups for // glyphs a given text never produces. - if (ShapingProbe.Enabled) - { - ShapingProbe.LookupsConsidered++; - } - if (!featureLookupTable.Digest.MightIntersect(buffer.GlyphDigest)) { - if (ShapingProbe.Enabled) - { - ShapingProbe.LookupsSkippedByDigest++; - } - continue; } buffer.SetLookupMatchState(featureMask, autoZwnj, autoZwj, perSyllable); iterator.Reset(index, featureLookupTable.LookupFlags, featureLookupTable.MarkFilteringSet); - long featureStart = ShapingProbe.Timestamp(); - long featureApplies = 0; // One output pass per lookup: the cursor consumes the input side and // every record streams to the output side exactly once, so a length @@ -347,11 +325,6 @@ private void ApplyMergedLookups( return; } - if (ShapingProbe.Enabled) - { - ShapingProbe.GlyphGateChecks++; - } - // The digest cheaply rejects glyphs no subtable of this lookup can // affect; a maybe falls through to the exact coverage test inside. // Ignored records stream through untouched rather than being @@ -366,14 +339,8 @@ private void ApplyMergedLookups( continue; } - if (ShapingProbe.Enabled) - { - ShapingProbe.SubstitutionAttempts++; - } - int beforeCount = buffer.Count; featureLookupTable.TrySubstitution(fontMetrics, this, buffer, feature, featureMask, position, segmentEnd - position); - featureApplies++; // In-place mutations from contextual nesting change the input side // directly and move the segment bound; cursor consumption surfaces @@ -394,8 +361,6 @@ private void ApplyMergedLookups( count += buffer.Count - passBefore; i += buffer.Count - passBefore; collectionCount = buffer.Count; - - ShapingProbe.ExitFeature("GSUB", feature, featureStart, featureApplies); } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs index 9c4dd61fb..378598f8e 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs @@ -89,23 +89,17 @@ public void Plan(ShapingBuffer buffer, int index, int count) this.CollectSegmentIndex = index; this.CollectSegmentCount = count; - var preProbe = ShapingProbe.Enter(); this.PlanPreprocessingFeatures(buffer, index, count); - ShapingProbe.Exit(ShapingProbe.PlanPre, preProbe); RecalculateCount(buffer, ref collectionCount, ref count); this.CollectSegmentCount = count; - var mainProbe = ShapingProbe.Enter(); this.PlanFeatures(buffer, index, count); - ShapingProbe.Exit(ShapingProbe.PlanMain, mainProbe); RecalculateCount(buffer, ref collectionCount, ref count); this.CollectSegmentCount = count; - var postProbe = ShapingProbe.Enter(); this.PlanPostprocessingFeatures(buffer, index, count); - ShapingProbe.Exit(ShapingProbe.PlanPost, postProbe); RecalculateCount(buffer, ref collectionCount, ref count); @@ -114,22 +108,15 @@ public void Plan(ShapingBuffer buffer, int index, int count) } else { - var foldProbe = ShapingProbe.Enter(); if (this.FoldedRegisteredMask != 0) { buffer.AddShapingFeatureMasks(index, count, this.FoldedRegisteredMask, this.FoldedEnabledMask); } - - ShapingProbe.Exit(ShapingProbe.PlanPre, foldProbe); } - var masksProbe = ShapingProbe.Enter(); this.SetupMasks(buffer, index, count); - ShapingProbe.Exit(ShapingProbe.PlanPost, masksProbe); - var assignProbe = ShapingProbe.Enter(); this.AssignFeatures(buffer, index, count); - ShapingProbe.Exit(ShapingProbe.PlanAssign, assignProbe); } /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs index f6db91d84..a1d006086 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs @@ -144,11 +144,6 @@ public SkippingGlyphIterator( /// The new index after advancing. public int Next() { - if (ShapingProbe.Enabled) - { - ShapingProbe.IteratorSteps++; - } - this.Move(1); return this.Index; } diff --git a/src/SixLabors.Fonts/TextShaper.Pipeline.cs b/src/SixLabors.Fonts/TextShaper.Pipeline.cs index 573d1ca1e..c8173b3b8 100644 --- a/src/SixLabors.Fonts/TextShaper.Pipeline.cs +++ b/src/SixLabors.Fonts/TextShaper.Pipeline.cs @@ -37,9 +37,7 @@ public static partial class TextShaper internal static IReadOnlyList BuildTextRuns(ReadOnlySpan text, TextOptions options) { int start = 0; - var graphemeProbe = ShapingProbe.Enter(); int end = text.GetGraphemeCount(); - ShapingProbe.Exit(ShapingProbe.GraphemeCount, graphemeProbe); if (end == 0) { return []; @@ -188,8 +186,6 @@ private static ShapingBuffer ShapeCore( LayoutMode layoutMode = options.LayoutMode; - var probe = ShapingProbe.Enter(); - // Analyse the text for bidi directional runs. BidiAlgorithm bidi = scratch.BidiAlgorithm; BidiData bidiData = scratch.BidiData; @@ -265,9 +261,6 @@ or BidiCharacterType.FirstStrongIsolate BidiRun[] bidiRuns = scratch.BidiRuns; int[] bidiMap = scratch.GetBidiMap(bidiData.Types.Length); - ShapingProbe.Exit(ShapingProbe.Bidi, probe); - - probe = ShapingProbe.Enter(); // Incrementally build out buffer of glyphs. Both buffers share the run list so // per-glyph run indices agree when records are seeded across them. Callers @@ -278,7 +271,6 @@ or BidiCharacterType.FirstStrongIsolate : scratch.GetDefaultTextRuns(text.GetGraphemeCount(), options)); substitutions.SetTextRuns(textRuns); positionings.SetTextRuns(textRuns); - ShapingProbe.Exit(ShapingProbe.BuildTextRuns, probe); // First do multiple font runs using the individual text runs. bool complete = true; @@ -306,9 +298,7 @@ or BidiCharacterType.FirstStrongIsolate bidiMap, substitutions); - var seedProbe = ShapingProbe.Enter(); complete = substitutions.SeedMetricsInPlace(onlyRun.ResolvedFont); - ShapingProbe.Exit(ShapingProbe.MetricsAdd, seedProbe); if (complete || fallbackFonts.Length == 0) { @@ -318,9 +308,7 @@ or BidiCharacterType.FirstStrongIsolate } else { - seedProbe = ShapingProbe.Enter(); complete = positionings.TryAdd(onlyRun.ResolvedFont, substitutions); - ShapingProbe.Exit(ShapingProbe.MetricsAdd, seedProbe); } goto FallbackPasses; @@ -410,7 +398,6 @@ or BidiCharacterType.FirstStrongIsolate // Update the positions of the glyphs in the completed buffer. // Each set of metrics is associated with single font and will only be updated // by that font so it's safe to use a single buffer. - probe = ShapingProbe.Enter(); Font? lastFont = null; for (int i = 0; i < textRuns.Count; i++) { @@ -433,8 +420,6 @@ or BidiCharacterType.FirstStrongIsolate HideDefaultIgnorables(shaped); - ShapingProbe.Exit(ShapingProbe.Positioning, probe); - return shaped; } @@ -641,11 +626,9 @@ private static bool DoFontRun( bidiMap, substitutions); - var seedProbe = ShapingProbe.Enter(); bool result = !isFallbackRun ? positionings.TryAdd(font, substitutions) : positionings.TryUpdate(font, substitutions); - ShapingProbe.Exit(ShapingProbe.MetricsAdd, seedProbe); return result; } @@ -680,8 +663,6 @@ private static void PopulateAndSubstitute( // overwriting the glyph ids. substitutions.Clear(); - var probe = ShapingProbe.Enter(); - // A font without variation sequences never consumes the following codepoint // during glyph lookup, so the per-codepoint lookahead decode is skipped. bool hasVariationSequences = font.FontMetrics.HasUnicodeVariationSequences; @@ -744,17 +725,11 @@ private static void PopulateAndSubstitute( graphemeIndex++; } - ShapingProbe.Exit(ShapingProbe.Populate, probe); - // Apply the simple and complex substitutions. // TODO: Investigate HarfBuzz normalizer. - probe = ShapingProbe.Enter(); SubstituteBidiMirrors(font.FontMetrics, substitutions); - ShapingProbe.Exit(ShapingProbe.Mirrors, probe); - probe = ShapingProbe.Enter(); font.FontMetrics.ApplySubstitution(substitutions); - ShapingProbe.Exit(ShapingProbe.Substitution, probe); } /// diff --git a/src/SixLabors.Fonts/TextShaper.cs b/src/SixLabors.Fonts/TextShaper.cs index ba652ec8d..eeea1bbb8 100644 --- a/src/SixLabors.Fonts/TextShaper.cs +++ b/src/SixLabors.Fonts/TextShaper.cs @@ -99,7 +99,6 @@ public static void Shape(ReadOnlySpan text, TextOptions options, TextShapi { ShapingBuffer shaped = ShapeCore(text, options, scratch, null); - var probe = ShapingProbe.Enter(); int count = shaped.Count; Span destination = buffer.Reserve(count); int written = 0; @@ -127,7 +126,6 @@ public static void Shape(ReadOnlySpan text, TextOptions options, TextShapi } buffer.Commit(written); - ShapingProbe.Exit(ShapingProbe.Projection, probe); } finally { From e5ad84b8423da33639d96f6a8e08a7a5fd28f8dd Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 26 Jul 2026 13:52:41 +1000 Subject: [PATCH 67/94] Apply nested lookups at the positions the context matched Contextual and chained lookups re-derived where to apply their nested lookups instead of remembering it: the substitution path stepped a freshly built iterator forward from the first input glyph, and the positioning path added the sequence index to the first glyph's index outright. Neither reproduces the walk the match performed. The iterator step skips only what the lookup's flags ignore, so it lands on a joiner the match had stepped over as transparent, and the raw index arithmetic ignores skipping altogether. A nested lookup then applies to the wrong record - a latent defect for any context containing a default ignorable, which the joiner transparency work made reachable. The match now records the position of every input element it consumes and nested lookups address those positions directly. When a nested substitution changes the buffer's length the remaining positions are reconciled: growth is taken as records inserted after the applying position, a shrink as the positions following it having been consumed, and the trailing positions shift by the same delta, with the shrink clamped so it can never rewind past the applying position. Nested application is also capped at a nesting depth. Contextual lookups may recurse into one another, so a font can describe unbounded nesting; the buffer already counted the depth and now refuses to recurse past the limit, which matters more with the matched positions held per frame. Measurement: the benchmark harness was unusable in this window - the native control ran 25 percent above its idle value across four consecutive runs with 5 to 9 percent error, while local probes on the same machine in the same minutes ran at full speed, so the noise is in the harness, not the machine. Judged instead on the interleaved local harness (best of five over 100,000 shapes, twice per scenario, against a worktree built from the parent commit): Latin +0.2, Arabic -0.8, Devanagari +0.3 percent - parity, as expected for replacing an iterator walk with an array index. A paired benchmark run of both trees inside the same degraded window agreed within its own error. The published numbers stand until a clean window refreshes them. Suite 5,601/0; emoji ZWJ and joiner differentials vs HarfBuzzSharp all match; zero steady-state allocation preserved on all three scripts. --- src/SixLabors.Fonts/ShapingBuffer.cs | 7 + .../AdvancedTypographicUtils.cs | 235 +++++++++++++++--- .../GPos/LookupType7SubTable.cs | 35 ++- .../GPos/LookupType8SubTable.cs | 75 +++++- .../GSub/LookupType5SubTable.cs | 28 ++- .../GSub/LookupType6SubTable.cs | 29 ++- 6 files changed, 340 insertions(+), 69 deletions(-) diff --git a/src/SixLabors.Fonts/ShapingBuffer.cs b/src/SixLabors.Fonts/ShapingBuffer.cs index eebabca03..5ac6f7814 100644 --- a/src/SixLabors.Fonts/ShapingBuffer.cs +++ b/src/SixLabors.Fonts/ShapingBuffer.cs @@ -313,6 +313,13 @@ public ShapingBuffer(TextOptions textOptions, ShapingBufferRole role) /// public bool IsNestedApplication => this.nestedApplicationDepth > 0; + /// + /// Gets a value indicating whether a further nested lookup application would + /// exceed the maximum nesting depth. A font may chain contextual lookups + /// into each other without bound, so recursion is capped rather than trusted. + /// + public bool NestingLimitReached => this.nestedApplicationDepth >= Tables.AdvancedTypographic.AdvancedTypographicUtils.MaxNestingLevel; + /// /// Gets the text options used by this buffer. /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs index 5e278a879..aed5547e0 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs @@ -18,6 +18,13 @@ internal static class AdvancedTypographicUtils /// public const int MaxContextLength = 64; + /// + /// The maximum depth of nested lookup application. Contextual lookups may + /// recurse into one another, so a font can describe unbounded nesting; + /// application stops at this depth instead. + /// + public const int MaxNestingLevel = 64; + /// /// The maximum length factor multiplied by buffer count to compute max allowable buffer size. /// @@ -103,7 +110,8 @@ public static int GetMaxAllowableShapingOperationsCount(int length) /// The mark filtering set index. /// The sequence lookup records specifying which lookups to apply at which positions. /// The glyph substitution buffer. - /// The starting index in the buffer. + /// The buffer positions the input sequence matched at, reconciled as nested lookups change the buffer's length. + /// The number of matched positions. /// The number of glyphs in the input sequence. /// if the lookups were applied. public static bool ApplyLookupList( @@ -115,10 +123,16 @@ public static bool ApplyLookupList( ushort markFilteringSet, SequenceLookupRecord[] records, ShapingBuffer buffer, - int index, + Span matchPositions, + int matchCount, int count) { - SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, lookupFlags, markFilteringSet); + if (buffer.NestingLimitReached) + { + return false; + } + + int startIndex = matchPositions[0]; int currentCount = buffer.Count; // Nested lookups mutate the input side in place whatever their type: the @@ -127,60 +141,138 @@ public static bool ApplyLookupList( foreach (SequenceLookupRecord lookupRecord in records) { - ushort sequenceIndex = lookupRecord.SequenceIndex; - ushort lookupIndex = lookupRecord.LookupListIndex; - iterator.Index = index; - iterator.Increment(sequenceIndex); - GSub.LookupTable lookup = table.LookupList.LookupTables[lookupIndex]; - _ = lookup.TrySubstitution(fontMetrics, table, buffer, feature, lookupMask, iterator.Index, count - (iterator.Index - index)); - - // Account for substitutions changing the length of the buffer. - if (buffer.Count != currentCount) + int sequenceIndex = lookupRecord.SequenceIndex; + if (sequenceIndex >= matchCount) + { + continue; + } + + int position = matchPositions[sequenceIndex]; + + // An earlier nested lookup can delete enough records to strand a + // later sequence position past the buffer's end. + if (position >= buffer.Count) { - count -= currentCount - buffer.Count; - currentCount = buffer.Count; + continue; } + + GSub.LookupTable lookup = table.LookupList.LookupTables[lookupRecord.LookupListIndex]; + _ = lookup.TrySubstitution(fontMetrics, table, buffer, feature, lookupMask, position, count - (position - startIndex)); + + // Account for substitutions changing the length of the buffer, both + // in the window the remaining lookups see and in the matched + // positions themselves. + int delta = buffer.Count - currentCount; + if (delta == 0) + { + continue; + } + + count += delta; + currentCount = buffer.Count; + matchCount = FixupMatchPositions(matchPositions, matchCount, sequenceIndex, delta); } buffer.PopNestedApplication(); return true; } + /// + /// Reconciles the matched positions after a nested lookup changed the + /// buffer's length. A growth is taken as records inserted directly after the + /// applying position, and a shrink as the positions following it having been + /// consumed; the trailing positions then shift by the same delta. + /// + /// The matched positions to reconcile. + /// The number of matched positions. + /// The position the nested lookup applied at. + /// The buffer's length change. + /// The new number of matched positions. + private static int FixupMatchPositions(Span matchPositions, int matchCount, int sequenceIndex, int delta) + { + int next = sequenceIndex + 1; + if (delta > 0) + { + if (delta + matchCount > MaxContextLength) + { + return matchCount; + } + } + else + { + // A shrink can never consume more positions than the match holds. + delta = Math.Max(delta, next - matchCount); + next -= delta; + } + + int tail = matchCount - next; + if (tail > 0) + { + matchPositions.Slice(next, tail).CopyTo(matchPositions[(next + delta)..]); + } + + next += delta; + matchCount += delta; + + for (int j = sequenceIndex + 1; j < next; j++) + { + matchPositions[j] = matchPositions[j - 1] + 1; + } + + for (; next < matchCount; next++) + { + matchPositions[next] += delta; + } + + return matchCount; + } + /// /// Applies nested lookups from sequence lookup records for GPOS contextual/chaining lookups. /// /// The font metrics. /// The GPOS table. /// The feature tag being applied. - /// The lookup flags for glyph filtering. - /// The mark filtering set index. /// The sequence lookup records specifying which lookups to apply at which positions. /// The glyph positioning buffer. - /// The starting index in the buffer. + /// The buffer positions the input sequence matched at. + /// The number of matched positions. /// The number of glyphs in the input sequence. /// if the lookups were applied. public static bool ApplyLookupList( FontMetrics fontMetrics, GPosTable table, Tag feature, - LookupFlags lookupFlags, - ushort markFilteringSet, SequenceLookupRecord[] records, ShapingBuffer buffer, - int index, + ReadOnlySpan matchPositions, + int matchCount, int count) { - SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, lookupFlags, markFilteringSet); + if (buffer.NestingLimitReached) + { + return false; + } + + // Positioning never changes the buffer's length, so the matched + // positions stand for the whole record list. + int startIndex = matchPositions[0]; + buffer.PushNestedApplication(); + foreach (SequenceLookupRecord lookupRecord in records) { - ushort sequenceIndex = lookupRecord.SequenceIndex; - ushort lookupIndex = lookupRecord.LookupListIndex; - iterator.Index = index; - iterator.Increment(sequenceIndex); - LookupTable lookup = table.LookupList.LookupTables[lookupIndex]; - _ = lookup.TryUpdatePosition(fontMetrics, table, buffer, feature, iterator.Index, count - (iterator.Index - index)); + int sequenceIndex = lookupRecord.SequenceIndex; + if (sequenceIndex >= matchCount) + { + continue; + } + + int position = matchPositions[sequenceIndex]; + LookupTable lookup = table.LookupList.LookupTables[lookupRecord.LookupListIndex]; + _ = lookup.TryUpdatePosition(fontMetrics, table, buffer, feature, position, count - (position - startIndex)); } + buffer.PopNestedApplication(); return true; } @@ -228,6 +320,22 @@ public static bool MatchSequence(SkippingGlyphIterator iterator, int increment, /// The position one past the final matched element. /// if the entire sequence was matched; otherwise, . public static bool MatchSequence(SkippingGlyphIterator iterator, int increment, ushort[] sequence, uint mask, bool contextMatch, out int matchEnd) + => MatchSequence(iterator, increment, sequence, mask, contextMatch, default, out matchEnd); + + /// + /// Matches a glyph sequence by glyph ID under the given matcher context, + /// recording where each element matched so nested lookups apply to the + /// records the match actually consumed rather than to a re-derived walk. + /// + /// The skipping glyph iterator. + /// The initial increment from the iterator's current position. + /// The array of glyph IDs to match. + /// The applying lookup's mask; ignored during context matching. + /// Whether this matches backtrack or lookahead context. + /// The span receiving the matched positions, or default when they are not needed. + /// The position one past the final matched element. + /// if the entire sequence was matched; otherwise, . + public static bool MatchSequence(SkippingGlyphIterator iterator, int increment, ushort[] sequence, uint mask, bool contextMatch, Span matches, out int matchEnd) { iterator.SetMatchContext(mask, contextMatch); return Match( @@ -235,7 +343,7 @@ public static bool MatchSequence(SkippingGlyphIterator iterator, int increment, sequence, iterator, static (component, data) => component == data.GlyphId, - default, + matches, out matchEnd); } @@ -279,6 +387,31 @@ public static bool MatchClassSequence( uint mask, bool contextMatch, out int matchEnd) + => MatchClassSequence(iterator, increment, sequence, classDefinitionTable, mask, contextMatch, default, out matchEnd); + + /// + /// Matches a glyph sequence by class values under the given matcher context, + /// recording where each element matched so nested lookups apply to the + /// records the match actually consumed. + /// + /// The skipping glyph iterator. + /// The initial increment from the iterator's current position. + /// The array of class values to match. + /// The class definition table used to map glyph IDs to class values. + /// The applying lookup's mask; ignored during context matching. + /// Whether this matches backtrack or lookahead context. + /// The span receiving the matched positions, or default when they are not needed. + /// The position one past the final matched element. + /// if the entire sequence was matched; otherwise, . + public static bool MatchClassSequence( + SkippingGlyphIterator iterator, + int increment, + ushort[] sequence, + ClassDefinitionTable classDefinitionTable, + uint mask, + bool contextMatch, + Span matches, + out int matchEnd) { iterator.SetMatchContext(mask, contextMatch); @@ -291,7 +424,7 @@ public static bool MatchClassSequence( iterator, classDefinitionTable, static (component, data, table) => component == table.ClassIndexOf(data.GlyphId), - default, + matches, out matchEnd); } @@ -335,6 +468,31 @@ public static bool MatchCoverageSequence( uint mask, bool contextMatch, out int matchEnd) + => MatchCoverageSequence(iterator, coverageTable, startIndex, endExclusive, mask, contextMatch, default, out matchEnd); + + /// + /// Matches a forward glyph sequence using coverage tables under the given + /// matcher context, recording where each element matched so nested lookups + /// apply to the records the match actually consumed. + /// + /// The skipping glyph iterator. + /// The array of coverage tables to match against. + /// The starting index in the buffer. + /// The exclusive end index in the buffer. + /// The applying lookup's mask; ignored during context matching. + /// Whether this matches lookahead context. + /// The span receiving the matched positions, or default when they are not needed. + /// The position one past the final matched element. + /// if all coverage tables matched; otherwise, . + public static bool MatchCoverageSequence( + SkippingGlyphIterator iterator, + CoverageTable[] coverageTable, + int startIndex, + int endExclusive, + uint mask, + bool contextMatch, + Span matches, + out int matchEnd) { iterator.SetMatchContext(mask, contextMatch); return Match( @@ -344,7 +502,7 @@ public static bool MatchCoverageSequence( MatchDirection.Forward, endExclusive, static (component, data) => component.CoverageIndexOf(data.GlyphId) >= 0, - default, + matches, out matchEnd); } @@ -384,12 +542,13 @@ public static bool MatchBacktrackCoverageSequence( /// The skipping glyph iterator. /// The chained sequence rule table to apply. /// The applying lookup's mask. + /// The span receiving the matched input positions, offset by one for the coverage-matched first glyph. /// if all sequences matched; otherwise, . - public static bool ApplyChainedSequenceRule(SkippingGlyphIterator iterator, ChainedSequenceRuleTable rule, uint mask) + public static bool ApplyChainedSequenceRule(SkippingGlyphIterator iterator, ChainedSequenceRuleTable rule, uint mask, Span matches) { int matchEnd = iterator.Index + 1; if (rule.InputSequence.Length > 0 - && !MatchSequence(iterator, 1, rule.InputSequence, mask, false, out matchEnd)) + && !MatchSequence(iterator, 1, rule.InputSequence, mask, false, matches, out matchEnd)) { return false; } @@ -426,6 +585,7 @@ public static bool ApplyChainedSequenceRule(SkippingGlyphIterator iterator, Chai /// The class definition table for the backtrack sequence. /// The class definition table for the lookahead sequence. /// The applying lookup's mask. + /// The span receiving the matched input positions, offset by one for the coverage-matched first glyph. /// if all sequences matched; otherwise, . public static bool ApplyChainedClassSequenceRule( SkippingGlyphIterator iterator, @@ -433,11 +593,12 @@ public static bool ApplyChainedClassSequenceRule( ClassDefinitionTable inputClassDefinitionTable, ClassDefinitionTable backtrackClassDefinitionTable, ClassDefinitionTable lookaheadClassDefinitionTable, - uint mask) + uint mask, + Span matches) { int matchEnd = iterator.Index + 1; if (rule.InputSequence.Length > 0 && - !MatchClassSequence(iterator, 1, rule.InputSequence, inputClassDefinitionTable, mask, false, out matchEnd)) + !MatchClassSequence(iterator, 1, rule.InputSequence, inputClassDefinitionTable, mask, false, matches, out matchEnd)) { return false; } @@ -476,6 +637,7 @@ public static bool ApplyChainedClassSequenceRule( /// The array of backtrack coverage tables. /// The array of lookahead coverage tables. /// The applying lookup's mask; the input matches under it. + /// The span receiving the matched input positions; the input coverage array covers the whole input including its first glyph. /// if all coverages matched; otherwise, . public static bool CheckAllCoverages( FontMetrics fontMetrics, @@ -487,7 +649,8 @@ public static bool CheckAllCoverages( CoverageTable[] input, CoverageTable[] backtrack, CoverageTable[] lookahead, - uint mask) + uint mask, + Span matches) { int endExclusive = index + count; @@ -511,7 +674,7 @@ public static bool CheckAllCoverages( // Input starts at the current glyph position; lookahead starts exactly // one past the final matched input element. - if (!MatchCoverageSequence(iterator, input, index, endExclusive, mask, false, out int matchEnd)) + if (!MatchCoverageSequence(iterator, input, index, endExclusive, mask, false, matches, out int matchEnd)) { return false; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType7SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType7SubTable.cs index 9eddfe14f..ffe74ff5a 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType7SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType7SubTable.cs @@ -102,6 +102,11 @@ public override bool TryUpdatePosition( // https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#example-7-contextual-substitution-format-1 SequenceRuleSetTable ruleSetTable = this.seqRuleSetTables[offset]; SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, this.LookupFlags, this.MarkFilteringSet); + + // Slot zero holds the coverage-matched glyph; the input match fills the + // rest, so nested lookups address the records the match consumed. + Span matchPositions = stackalloc int[AdvancedTypographicUtils.MaxContextLength + 1]; + matchPositions[0] = index; foreach (SequenceRuleTable ruleTable in ruleSetTable.SequenceRuleTables) { int remaining = count - 1; @@ -111,7 +116,7 @@ public override bool TryUpdatePosition( continue; } - if (!AdvancedTypographicUtils.MatchSequence(iterator, 1, ruleTable.InputSequence, buffer.LookupMask, false)) + if (!AdvancedTypographicUtils.MatchSequence(iterator, 1, ruleTable.InputSequence, buffer.LookupMask, false, matchPositions[1..], out _)) { continue; } @@ -121,11 +126,10 @@ public override bool TryUpdatePosition( fontMetrics, table, feature, - this.LookupFlags, - this.MarkFilteringSet, ruleTable.SequenceLookupRecords, buffer, - index, + matchPositions, + ruleTable.InputSequence.Length + 1, count); } @@ -214,6 +218,11 @@ public override bool TryUpdatePosition( ClassSequenceRuleSetTable ruleSetTable = this.sequenceRuleSetTables[offset]; SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, this.LookupFlags, this.MarkFilteringSet); + + // Slot zero holds the coverage-matched glyph; the input match fills the + // rest, so nested lookups address the records the match consumed. + Span matchPositions = stackalloc int[AdvancedTypographicUtils.MaxContextLength + 1]; + matchPositions[0] = index; foreach (ClassSequenceRuleTable ruleTable in ruleSetTable.SequenceRuleTables) { int remaining = count - 1; @@ -223,7 +232,7 @@ public override bool TryUpdatePosition( continue; } - if (!AdvancedTypographicUtils.MatchClassSequence(iterator, 1, ruleTable.InputSequence, this.classDefinitionTable, buffer.LookupMask, false)) + if (!AdvancedTypographicUtils.MatchClassSequence(iterator, 1, ruleTable.InputSequence, this.classDefinitionTable, buffer.LookupMask, false, matchPositions[1..], out _)) { continue; } @@ -233,11 +242,10 @@ public override bool TryUpdatePosition( fontMetrics, table, feature, - this.LookupFlags, - this.MarkFilteringSet, ruleTable.SequenceLookupRecords, buffer, - index, + matchPositions, + ruleTable.InputSequence.Length + 1, count); } @@ -316,8 +324,12 @@ public override bool TryUpdatePosition( return false; } + // The input coverage array covers the whole input including its + // first glyph, so the match fills every position nested lookups + // address. SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, this.LookupFlags, this.MarkFilteringSet); - if (!AdvancedTypographicUtils.MatchCoverageSequence(iterator, this.coverageTables, index, index + count, buffer.LookupMask, false)) + Span matchPositions = stackalloc int[AdvancedTypographicUtils.MaxContextLength]; + if (!AdvancedTypographicUtils.MatchCoverageSequence(iterator, this.coverageTables, index, index + count, buffer.LookupMask, false, matchPositions, out _)) { return false; } @@ -326,11 +338,10 @@ public override bool TryUpdatePosition( fontMetrics, table, feature, - this.LookupFlags, - this.MarkFilteringSet, this.sequenceLookupRecords, buffer, - index, + matchPositions, + this.coverageTables.Length, count); } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType8SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType8SubTable.cs index 832d6b181..803bf135e 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType8SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType8SubTable.cs @@ -117,26 +117,45 @@ public override bool TryUpdatePosition( // Apply ruleset for the given glyph id. ChainedSequenceRuleTable[] rules = seqRuleSet.SequenceRuleTables; SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, this.LookupFlags, this.MarkFilteringSet); + + // Slot zero holds the coverage-matched glyph; the input match fills + // the rest, so nested lookups address the records the match + // consumed rather than a raw offset from the first. + Span matchPositions = stackalloc int[AdvancedTypographicUtils.MaxContextLength + 1]; + matchPositions[0] = index; for (int lookupIndex = 0; lookupIndex < rules.Length; lookupIndex++) { ChainedSequenceRuleTable rule = rules[lookupIndex]; - if (!AdvancedTypographicUtils.ApplyChainedSequenceRule(iterator, rule, buffer.LookupMask)) + if (!AdvancedTypographicUtils.ApplyChainedSequenceRule(iterator, rule, buffer.LookupMask, matchPositions[1..])) { continue; } + if (buffer.NestingLimitReached) + { + return false; + } + bool hasChanged = false; + int matchCount = rule.InputSequence.Length + 1; + buffer.PushNestedApplication(); for (int j = 0; j < rule.SequenceLookupRecords.Length; j++) { SequenceLookupRecord sequenceLookupRecord = rule.SequenceLookupRecords[j]; LookupTable lookup = table.LookupList.LookupTables[sequenceLookupRecord.LookupListIndex]; - ushort sequenceIndex = sequenceLookupRecord.SequenceIndex; - if (lookup.TryUpdatePosition(fontMetrics, table, buffer, feature, index + sequenceIndex, 1)) + int sequenceIndex = sequenceLookupRecord.SequenceIndex; + if (sequenceIndex >= matchCount) + { + continue; + } + + if (lookup.TryUpdatePosition(fontMetrics, table, buffer, feature, matchPositions[sequenceIndex], 1)) { hasChanged = true; } } + buffer.PopNestedApplication(); return hasChanged; } @@ -250,28 +269,47 @@ public override bool TryUpdatePosition( // Apply ruleset for the given glyph class id. SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, this.LookupFlags, this.MarkFilteringSet); + + // Slot zero holds the coverage-matched glyph; the input match fills + // the rest, so nested lookups address the records the match + // consumed rather than a raw offset from the first. + Span matchPositions = stackalloc int[AdvancedTypographicUtils.MaxContextLength + 1]; + matchPositions[0] = index; for (int lookupIndex = 0; lookupIndex < rules.Length; lookupIndex++) { ChainedClassSequenceRuleTable rule = rules[lookupIndex]; - if (!AdvancedTypographicUtils.ApplyChainedClassSequenceRule(iterator, rule, this.inputClassDefinitionTable, this.backtrackClassDefinitionTable, this.lookaheadClassDefinitionTable, buffer.LookupMask)) + if (!AdvancedTypographicUtils.ApplyChainedClassSequenceRule(iterator, rule, this.inputClassDefinitionTable, this.backtrackClassDefinitionTable, this.lookaheadClassDefinitionTable, buffer.LookupMask, matchPositions[1..])) { continue; } // It's a match. Perform position update and return true if anything changed. + if (buffer.NestingLimitReached) + { + return false; + } + bool hasChanged = false; + int matchCount = rule.InputSequence.Length + 1; + buffer.PushNestedApplication(); for (int j = 0; j < rule.SequenceLookupRecords.Length; j++) { SequenceLookupRecord sequenceLookupRecord = rule.SequenceLookupRecords[j]; LookupTable lookup = table.LookupList.LookupTables[sequenceLookupRecord.LookupListIndex]; - ushort sequenceIndex = sequenceLookupRecord.SequenceIndex; - if (lookup.TryUpdatePosition(fontMetrics, table, buffer, feature, index + sequenceIndex, 1)) + int sequenceIndex = sequenceLookupRecord.SequenceIndex; + if (sequenceIndex >= matchCount) + { + continue; + } + + if (lookup.TryUpdatePosition(fontMetrics, table, buffer, feature, matchPositions[sequenceIndex], 1)) { hasChanged = true; } } + buffer.PopNestedApplication(); return hasChanged; } @@ -368,25 +406,44 @@ public override bool TryUpdatePosition( return false; } - if (!AdvancedTypographicUtils.CheckAllCoverages(fontMetrics, this.LookupFlags, this.MarkFilteringSet, buffer, index, count, this.inputCoverageTables, this.backtrackCoverageTables, this.lookaheadCoverageTables, buffer.LookupMask)) + // The input coverage array covers the whole input including its + // first glyph, so the match fills every position nested lookups + // address. + Span matchPositions = stackalloc int[AdvancedTypographicUtils.MaxContextLength]; + if (!AdvancedTypographicUtils.CheckAllCoverages(fontMetrics, this.LookupFlags, this.MarkFilteringSet, buffer, index, count, this.inputCoverageTables, this.backtrackCoverageTables, this.lookaheadCoverageTables, buffer.LookupMask, matchPositions)) + { + return false; + } + + if (buffer.NestingLimitReached) { return false; } // It's a match. Perform position update and return true if anything changed. bool hasChanged = false; + int matchCount = this.inputCoverageTables.Length; + buffer.PushNestedApplication(); + foreach (SequenceLookupRecord lookupRecord in this.seqLookupRecords) { - ushort sequenceIndex = lookupRecord.SequenceIndex; + int sequenceIndex = lookupRecord.SequenceIndex; + if (sequenceIndex >= matchCount) + { + continue; + } + ushort lookupIndex = lookupRecord.LookupListIndex; + int position = matchPositions[sequenceIndex]; LookupTable lookup = table.LookupList.LookupTables[lookupIndex]; - if (lookup.TryUpdatePosition(fontMetrics, table, buffer, feature, index + sequenceIndex, count - sequenceIndex)) + if (lookup.TryUpdatePosition(fontMetrics, table, buffer, feature, position, count - (position - index))) { hasChanged = true; } } + buffer.PopNestedApplication(); return hasChanged; } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs index 8cc880d82..b2cf40daa 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs @@ -108,6 +108,11 @@ public override bool TrySubstitution( // https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#example-7-contextual-substitution-format-1 SequenceRuleSetTable ruleSetTable = this.seqRuleSetTables[offset]; SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, this.LookupFlags, this.MarkFilteringSet); + + // Slot zero holds the coverage-matched glyph; the sequence match fills + // the rest, so nested lookups address the records the match consumed. + Span matchPositions = stackalloc int[AdvancedTypographicUtils.MaxContextLength + 1]; + matchPositions[0] = index; foreach (SequenceRuleTable ruleTable in ruleSetTable.SequenceRuleTables) { int remaining = count - 1; @@ -117,7 +122,7 @@ public override bool TrySubstitution( continue; } - if (!AdvancedTypographicUtils.MatchSequence(iterator, 1, ruleTable.InputSequence, lookupMask, false)) + if (!AdvancedTypographicUtils.MatchSequence(iterator, 1, ruleTable.InputSequence, lookupMask, false, matchPositions[1..], out _)) { continue; } @@ -132,7 +137,8 @@ public override bool TrySubstitution( this.MarkFilteringSet, ruleTable.SequenceLookupRecords, buffer, - index, + matchPositions, + seqLength + 1, count); } @@ -273,6 +279,11 @@ public override bool TrySubstitution( } SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, this.LookupFlags, this.MarkFilteringSet); + + // Slot zero holds the coverage-matched glyph; the sequence match fills + // the rest, so nested lookups address the records the match consumed. + Span matchPositions = stackalloc int[AdvancedTypographicUtils.MaxContextLength + 1]; + matchPositions[0] = index; foreach (ClassSequenceRuleTable ruleTable in ruleSetTable.SequenceRuleTables) { int remaining = count - 1; @@ -282,7 +293,7 @@ public override bool TrySubstitution( continue; } - if (!AdvancedTypographicUtils.MatchClassSequence(iterator, 1, ruleTable.InputSequence, this.classDefinitionTable, lookupMask, false)) + if (!AdvancedTypographicUtils.MatchClassSequence(iterator, 1, ruleTable.InputSequence, this.classDefinitionTable, lookupMask, false, matchPositions[1..], out _)) { continue; } @@ -297,7 +308,8 @@ public override bool TrySubstitution( this.MarkFilteringSet, ruleTable.SequenceLookupRecords, buffer, - index, + matchPositions, + seqLength + 1, count); } @@ -426,8 +438,11 @@ public override bool TrySubstitution( } // https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#53-context-substitution-format-3-coverage-based-glyph-contexts + // The input coverage array covers the whole input including its first + // glyph, so the match fills every position nested lookups address. SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, this.LookupFlags, this.MarkFilteringSet); - if (!AdvancedTypographicUtils.MatchCoverageSequence(iterator, this.coverageTables, index, index + count, lookupMask, false)) + Span matchPositions = stackalloc int[AdvancedTypographicUtils.MaxContextLength]; + if (!AdvancedTypographicUtils.MatchCoverageSequence(iterator, this.coverageTables, index, index + count, lookupMask, false, matchPositions, out _)) { return false; } @@ -442,7 +457,8 @@ public override bool TrySubstitution( this.MarkFilteringSet, this.sequenceLookupRecords, buffer, - index, + matchPositions, + this.coverageTables.Length, count); } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs index 47db4e705..dacfb40ed 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs @@ -119,10 +119,15 @@ public override bool TrySubstitution( SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, this.LookupFlags, this.MarkFilteringSet); ChainedSequenceRuleSetTable seqRuleSet = this.seqRuleSetTables[offset]; ChainedSequenceRuleTable[] rules = seqRuleSet.SequenceRuleTables; + + // Slot zero holds the coverage-matched glyph; the input match fills the + // rest, so nested lookups address the records the match consumed. + Span matchPositions = stackalloc int[AdvancedTypographicUtils.MaxContextLength + 1]; + matchPositions[0] = index; for (int i = 0; i < rules.Length; i++) { ChainedSequenceRuleTable ruleTable = rules[i]; - if (!AdvancedTypographicUtils.ApplyChainedSequenceRule(iterator, ruleTable, lookupMask)) + if (!AdvancedTypographicUtils.ApplyChainedSequenceRule(iterator, ruleTable, lookupMask, matchPositions[1..])) { continue; } @@ -136,7 +141,8 @@ public override bool TrySubstitution( this.MarkFilteringSet, ruleTable.SequenceLookupRecords, buffer, - index, + matchPositions, + ruleTable.InputSequence.Length + 1, count); } @@ -310,11 +316,16 @@ public override bool TrySubstitution( // Apply ruleset for the given glyph class id. SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, this.LookupFlags, this.MarkFilteringSet); + + // Slot zero holds the coverage-matched glyph; the input match fills the + // rest, so nested lookups address the records the match consumed. + Span matchPositions = stackalloc int[AdvancedTypographicUtils.MaxContextLength + 1]; + matchPositions[0] = index; for (int lookupIndex = 0; lookupIndex < rules.Length; lookupIndex++) { ChainedClassSequenceRuleTable ruleTable = rules[lookupIndex]; - if (!AdvancedTypographicUtils.ApplyChainedClassSequenceRule(iterator, ruleTable, this.inputClassDefinitionTable, this.backtrackClassDefinitionTable, this.lookaheadClassDefinitionTable, lookupMask)) + if (!AdvancedTypographicUtils.ApplyChainedClassSequenceRule(iterator, ruleTable, this.inputClassDefinitionTable, this.backtrackClassDefinitionTable, this.lookaheadClassDefinitionTable, lookupMask, matchPositions[1..])) { continue; } @@ -328,7 +339,8 @@ public override bool TrySubstitution( this.MarkFilteringSet, ruleTable.SequenceLookupRecords, buffer, - index, + matchPositions, + ruleTable.InputSequence.Length + 1, count); } @@ -488,6 +500,9 @@ public override bool TrySubstitution( return false; } + // The input coverage array covers the whole input including its first + // glyph, so the match fills every position nested lookups address. + Span matchPositions = stackalloc int[AdvancedTypographicUtils.MaxContextLength]; if (!AdvancedTypographicUtils.CheckAllCoverages( fontMetrics, this.LookupFlags, @@ -498,7 +513,8 @@ public override bool TrySubstitution( this.inputCoverageTables, this.backtrackCoverageTables, this.lookaheadCoverageTables, - lookupMask)) + lookupMask, + matchPositions)) { return false; } @@ -513,7 +529,8 @@ public override bool TrySubstitution( this.MarkFilteringSet, this.sequenceLookupRecords, buffer, - index, + matchPositions, + this.inputCoverageTables.Length, count); } From 30a7223a320ac71acc4de0a5bcf0008c32dd2028 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 26 Jul 2026 14:07:37 +1000 Subject: [PATCH 68/94] Form fractions from the fraction slash without being asked Automatic fractions were neither automatic nor correct. They ran only when the caller had already listed numr and dnom (or frac) in the text options, so text that should form a fraction did not unless the caller knew to ask; they triggered on the solidus as well as the fraction slash, so "1/2" formed a fraction it has no business forming; and the two digit walks tested a codepoint captured once before the loop, so a single adjacent digit ran the walk to the edge of the buffer and tagged every record it passed regardless of what those records were. The features are now registered with every plan and left off, and each text turns them on over the digit runs a fraction slash joins: the numerator before it, the denominator after it, the slash itself, with the two swapping roles for right-to-left text. A slash without digits on both sides forms nothing. The buffer records whether a fraction slash entered it at all, so text that has none never walks for one. An explicitly requested feature still applies over the whole run, which is how a caller asks for the font's own fraction lookups to run wherever they match. Registering features that stay off for most text needs the drivers to stop paying for them: both now skip a lookup whose mask no record carries, before the coverage digest, so a pass is walked only when some record could match it. That gate is what keeps this change free on text with no fractions, and it applies to every conditional feature. Verified against HarfBuzz 14.2.1 on Roboto, 7 of 7 rows matching: automatic forming with no features requested, the solidus forming nothing, multi-digit runs on both sides, a slash with no digits, and the three caller-requested combinations. Four of those rows become permanent tests, and the existing requested-numerator-and-denominator row moves to the output HarfBuzz produces - it had encoded the old behaviour of excluding requested fraction features from the run. Measured against the parent commit on the interleaved local harness: Latin parity, Arabic +0.5 percent, Devanagari -2.5 percent. Suite 5,601/0 -> 5,605/0; emoji ZWJ and joiner differentials still match; zero steady-state allocation preserved on all three scripts. --- src/SixLabors.Fonts/ShapingBuffer.cs | 43 ++++ .../Tables/AdvancedTypographic/GPosTable.cs | 10 +- .../Tables/AdvancedTypographic/GSubTable.cs | 7 +- .../Shapers/DefaultShaper.cs | 192 ++++++++---------- .../Gsub/GSubTableTests.cs | 97 ++++++++- 5 files changed, 239 insertions(+), 110 deletions(-) diff --git a/src/SixLabors.Fonts/ShapingBuffer.cs b/src/SixLabors.Fonts/ShapingBuffer.cs index 5ac6f7814..289ce6b7d 100644 --- a/src/SixLabors.Fonts/ShapingBuffer.cs +++ b/src/SixLabors.Fonts/ShapingBuffer.cs @@ -103,6 +103,12 @@ internal sealed class ShapingBuffer /// private const ulong GlyphIdCacheMarkerFlag = 1UL << 63; + /// + /// The fraction slash, U+2044, which forms fractions from the digit runs + /// surrounding it. The solidus U+002F does not. + /// + private const uint FractionSlashCodePoint = 0x2044; + /// /// The glyph id cache entry bits forming the lookup key: the marker, the /// codepoint, and the encoded following codepoint. @@ -268,6 +274,22 @@ public ShapingBuffer(TextOptions textOptions, ShapingBufferRole role) /// public bool HasDefaultIgnorables { get; set; } + /// + /// Gets or sets a value indicating whether any record carries the fraction + /// slash. Recorded as records enter the buffer so automatic fraction + /// forming costs nothing for the text that does not use it. + /// + public bool HasFractionSlash { get; set; } + + /// + /// Gets the union of every feature bit enabled on any record, accumulated as + /// features are turned on. A lookup whose mask shares no bit with it cannot + /// match any record, so the drivers skip it without walking the buffer. The + /// union only ever grows within a pass, so it is a superset: it can cost a + /// walk that finds nothing, never skip a lookup that would have applied. + /// + public uint EnabledFeatureMaskUnion { get; private set; } = ShapePlanFeatures.GlobalFeatureMask; + /// /// Gets a value indicating whether the applying lookup skips the zero width /// non-joiner during context matching instead of matching it. @@ -472,6 +494,7 @@ public void Reset(TextOptions textOptions) { this.Count = 0; this.LigatureId = 1; + this.EnabledFeatureMaskUnion = ShapePlanFeatures.GlobalFeatureMask; this.glyphDigest = default; this.placeholderBidiRuns.Clear(); this.SegmentPlans.Clear(); @@ -495,7 +518,9 @@ public void Clear() { this.Count = 0; this.LigatureId = 1; + this.EnabledFeatureMaskUnion = ShapePlanFeatures.GlobalFeatureMask; this.HasDefaultIgnorables = false; + this.HasFractionSlash = false; this.placeholderBidiRuns.Clear(); this.SegmentPlans.Clear(); } @@ -579,6 +604,11 @@ public void SetGlyphId(int index, ushort glyphId) /// The feature's plan-assigned mask bit. public void AddShapingFeatureRange(int index, int count, TagEntry feature, uint mask) { + if (feature.Enabled) + { + this.EnabledFeatureMaskUnion |= mask; + } + int end = index + count; for (int i = index; i < end; i++) { @@ -602,6 +632,8 @@ public void AddShapingFeatureRange(int index, int count, TagEntry feature, uint /// The fold of the enabled feature bits. public void AddShapingFeatureMasks(int index, int count, uint registeredMask, uint enabledMask) { + this.EnabledFeatureMaskUnion |= enabledMask; + int end = index + count; for (int i = index; i < end; i++) { @@ -622,6 +654,7 @@ public void AddShapingFeatureMasks(int index, int count, uint registeredMask, ui /// The feature's plan-assigned mask bit. public void EnableShapingFeature(int index, uint mask) { + this.EnabledFeatureMaskUnion |= mask; ref GlyphShapingData item = ref this.data[index]; item.FeatureMask |= item.RegisteredFeatureMask & mask; } @@ -683,6 +716,14 @@ public void AddGlyph(ushort glyphId, CodePoint codePoint, TextDirection directio // grapheme joiner) carry their own bits for the matcher's transparency // rules. uint value = (uint)codePoint.Value; + + // The fraction slash is the only trigger for automatic fraction + // forming; recording it here keeps that stage free for other text. + if (value == FractionSlashCodePoint) + { + this.HasFractionSlash = true; + } + if (value >= 0x80 && UnicodeUtility.IsDefaultIgnorableCodePoint(value) && !UnicodeUtility.ShouldRenderWhiteSpaceOnly(codePoint)) @@ -1203,6 +1244,7 @@ public bool TryAdd(Font font, ShapingBuffer workspace) // The hide-ignorables stage runs against this buffer, so the workspace's // knowledge of default ignorables must travel with its records. this.HasDefaultIgnorables |= workspace.HasDefaultIgnorables; + this.HasFractionSlash |= workspace.HasFractionSlash; uint verticalMask = ShapePlanFeatures.VerticalFeatureMask; @@ -1285,6 +1327,7 @@ public bool TryUpdate(Font font, ShapingBuffer workspace) // The hide-ignorables stage runs against this buffer, so the workspace's // knowledge of default ignorables must travel with its records. this.HasDefaultIgnorables |= workspace.HasDefaultIgnorables; + this.HasFractionSlash |= workspace.HasFractionSlash; uint verticalMask = ShapePlanFeatures.VerticalFeatureMask; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs index 8a05ad6ef..c1a505f15 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs @@ -332,10 +332,12 @@ private bool PositionSegment( (Tag feature, ushort _, LookupTable featureLookupTable, uint featureMask, bool autoZwnj, bool autoZwj, bool perSyllable) = merged[m]; buffer.SetLookupMatchState(featureMask, autoZwnj, autoZwj, perSyllable); - // Skip the whole lookup when its coverage cannot intersect any - // glyph id the buffer has ever contained; most fonts carry - // many lookups for glyphs a given text never produces. - if (!featureLookupTable.Digest.MightIntersect(buffer.GlyphDigest)) + // Skip the whole lookup when its mask reaches no record, or when + // its coverage cannot intersect any glyph id the buffer has ever + // contained; most fonts carry many lookups for glyphs a given + // text never produces. + if ((featureMask & buffer.EnabledFeatureMaskUnion) == 0 + || !featureLookupTable.Digest.MightIntersect(buffer.GlyphDigest)) { continue; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs index 0bfefa970..de7ebced0 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs @@ -293,7 +293,12 @@ private void ApplyMergedLookups( // Skip the whole lookup when its coverage cannot intersect any glyph id // the buffer has ever contained; most fonts carry many lookups for // glyphs a given text never produces. - if (!featureLookupTable.Digest.MightIntersect(buffer.GlyphDigest)) + // A lookup whose mask no record carries cannot match anything, so + // the whole pass is skipped rather than walked. Features that are + // registered for every plan but enabled only by particular text, + // such as the fraction trio, cost nothing elsewhere. + if ((featureMask & buffer.EnabledFeatureMaskUnion) == 0 + || !featureLookupTable.Digest.MightIntersect(buffer.GlyphDigest)) { continue; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs index 73ac99761..74abaaaea 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs @@ -117,15 +117,11 @@ internal class DefaultShaper : BaseShaper protected static readonly Tag VKernTag = Tag.Parse("vkrn"); /// - /// The fraction slash code point (U+2044). + /// The fraction slash code point (U+2044), the only code point that forms + /// a fraction from the digit runs surrounding it. /// private static readonly CodePoint FractionSlash = new(0x2044); - /// - /// The solidus (slash) code point (U+002F). - /// - private static readonly CodePoint Slash = new(0x002F); - /// /// The shaping stages accumulated during feature planning, in registration order. /// Stage counts are small (โ‰ค ~16), so duplicate suppression scans the list by tag; @@ -179,14 +175,80 @@ protected override void PlanPreprocessingFeatures(ShapingBuffer buffer, int inde this.EnableFeature(buffer, index, count, RvnrTag); this.AddDirectionalFeatures(buffer, index, count); - - // TODO: Fractional feature should be assigned here but disabled. - // They should then be enabled in AssignFeatures. } /// protected override void SetupMasks(ShapingBuffer buffer, int index, int count) - => this.AddDirectionalFeatures(buffer, index, count); + { + this.AddDirectionalFeatures(buffer, index, count); + + if (buffer.HasFractionSlash) + { + this.SetupFractionMasks(buffer, index, count); + } + } + + /// + /// Turns on the fraction features over the digit runs a fraction slash + /// joins: the numerator before it, the denominator after it, and the slash + /// itself. A slash without digits on both sides forms no fraction. The + /// numerator and denominator features swap roles for right-to-left text, + /// where the leading run is the denominator. + /// + /// The glyph shaping buffer. + /// The zero-based index of the first element. + /// The number of elements. + private void SetupFractionMasks(ShapingBuffer buffer, int index, int count) + { + uint fracMask = this.Features.GetOrAddMask(FracTag); + uint numrMask = this.Features.GetOrAddMask(NumrTag); + uint dnomMask = this.Features.GetOrAddMask(DnomTag); + + int end = index + count; + for (int i = index; i < end; i++) + { + if (buffer[i].CodePoint != FractionSlash) + { + continue; + } + + int runStart = i; + while (runStart > index && CodePoint.IsDigit(buffer[runStart - 1].CodePoint)) + { + runStart--; + } + + int runEnd = i + 1; + while (runEnd < end && CodePoint.IsDigit(buffer[runEnd].CodePoint)) + { + runEnd++; + } + + // A slash missing digits on either side is just a slash. + if (runStart == i || runEnd == i + 1) + { + continue; + } + + bool leftToRight = buffer[i].Direction != TextDirection.RightToLeft; + uint preMask = leftToRight ? numrMask | fracMask : fracMask | dnomMask; + uint postMask = leftToRight ? fracMask | dnomMask : numrMask | fracMask; + + for (int j = runStart; j < i; j++) + { + buffer.EnableShapingFeature(j, preMask); + } + + buffer.EnableShapingFeature(i, fracMask); + + for (int j = i + 1; j < runEnd; j++) + { + buffer.EnableShapingFeature(j, postMask); + } + + i = runEnd - 1; + } + } /// /// Adds the directional features once per direction span. A segment may span @@ -274,11 +336,25 @@ protected override void PlanPostprocessingFeatures(ShapingBuffer buffer, int ind this.EnableFeature(buffer, index, count, VertTag); } + // Register the fraction trio without enabling it: the features exist + // for every plan, and each text turns them on over the digit runs that + // surround a fraction slash. A font without them has no lookups to + // apply, so registration alone costs nothing. + this.AddFeature(buffer, index, count, FracTag, false); + this.AddFeature(buffer, index, count, NumrTag, false); + this.AddFeature(buffer, index, count, DnomTag, false); + // Add user defined features. foreach (Tag feature in this.featureTags) { - // We've already dealt with fractional features. - if (feature != FracTag && feature != NumrTag && feature != DnomTag) + if (feature == FracTag || feature == NumrTag || feature == DnomTag) + { + // The trio is registered off for automatic fraction forming; an + // explicit request turns it on over the whole segment, so the + // font's own fraction lookups apply wherever they match. + this.AddFeature(buffer, index, count, feature, true); + } + else { this.EnableFeature(buffer, index, count, feature); } @@ -288,13 +364,6 @@ protected override void PlanPostprocessingFeatures(ShapingBuffer buffer, int ind /// protected override void AssignFeatures(ShapingBuffer buffer, int index, int count) { - // TODO: We shouldn't be relying on the feature list - // User defined fractional features require special treatment. - // https://docs.microsoft.com/en-us/typography/opentype/spec/features_fj#tag-frac - if (this.HasFractions()) - { - this.AssignFractionalFeatures(buffer, index, count); - } } /// @@ -478,89 +547,4 @@ private void AddStage( /// public override List GetShapingStages() => this.shapingStages; - - /// - /// Assigns fractional feature tags (numerator, denominator, fraction) to glyphs forming fraction sequences. - /// - /// The glyph shaping buffer. - /// The zero-based index of the first element. - /// The number of elements. - private void AssignFractionalFeatures(ShapingBuffer buffer, int index, int count) - { - // Enable contextual fractions. - for (int i = index; i < index + count; i++) - { - ref GlyphShapingData shapingData = ref buffer[i]; - if (shapingData.CodePoint == FractionSlash || shapingData.CodePoint == Slash) - { - int start = i; - int end = i + 1; - - // Apply numerator. - if (start > 0) - { - CodePoint numeratorCodePoint = buffer[start - 1].CodePoint; - while (start > 0 && CodePoint.IsDigit(numeratorCodePoint)) - { - this.AddFeature(buffer, start - 1, 1, NumrTag); - this.AddFeature(buffer, start - 1, 1, FracTag); - start--; - } - } - - // Apply denominator. - if (end < buffer.Count) - { - CodePoint denominatorCodePoint = buffer[end].CodePoint; - while (end < buffer.Count && CodePoint.IsDigit(denominatorCodePoint)) - { - this.AddFeature(buffer, end, 1, DnomTag); - this.AddFeature(buffer, end, 1, FracTag); - end++; - } - } - - // Apply fraction slash. - this.AddFeature(buffer, i, 1, FracTag); - i = end - 1; - } - } - } - - /// - /// Determines whether the user-specified feature tags include fractional features. - /// - /// if fractional features are present; otherwise, . - private bool HasFractions() - { - bool hasNmr = false; - bool hasDnom = false; - - // My kingdom for a binary search on IReadOnlyList - for (int i = 0; i < this.featureTags.Count; i++) - { - Tag feature = this.featureTags[i]; - if (feature == FracTag) - { - return true; - } - - if (feature == DnomTag) - { - hasDnom = true; - } - - if (feature == NumrTag) - { - hasNmr = true; - } - - if (hasDnom && hasNmr) - { - return true; - } - } - - return false; - } } diff --git a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.cs b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.cs index 6d8063b2f..ee8704f99 100644 --- a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.cs +++ b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.cs @@ -78,7 +78,12 @@ public void ContextualFractions_WithFractionSlash_Works() Font font = TestFonts.GetFont(TestFonts.RobotoRegular, 12); ColorGlyphRenderer renderer = new(); string testStr = "9โ„2"; - int[] expectedGlyphIndices = [580, 404, 453]; + + // Explicitly requested numerator and denominator features apply over the + // whole run, so the leading digit takes the denominator form the font + // maps last; the automatic fraction masking adds to that rather than + // replacing it. Verified against HarfBuzz 14.2.1. + int[] expectedGlyphIndices = [549, 404, 453]; // act TextRenderer.RenderTo(renderer, testStr, new TextOptions(font) { FeatureTags = new Tag[] { KnownFeatureTags.Numerators, KnownFeatureTags.Denominators } }); @@ -91,6 +96,96 @@ public void ContextualFractions_WithFractionSlash_Works() } } + [Fact] + public void ContextualFractions_FractionSlash_FormsWithoutRequestedFeatures() + { + // arrange + Font font = TestFonts.GetFont(TestFonts.RobotoRegular, 12); + ColorGlyphRenderer renderer = new(); + string testStr = "9โ„2"; + + // The fraction slash forms a fraction from the digits surrounding it + // with no feature requested by the caller. Verified against HarfBuzz 14.2.1. + int[] expectedGlyphIndices = [580, 404, 453]; + + // act + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font)); + + // assert + Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); + for (int i = 0; i < expectedGlyphIndices.Length; i++) + { + Assert.Equal(expectedGlyphIndices[i], renderer.GlyphKeys[i].GlyphId); + } + } + + [Fact] + public void ContextualFractions_Solidus_FormsNoFraction() + { + // arrange + Font font = TestFonts.GetFont(TestFonts.RobotoRegular, 12); + ColorGlyphRenderer renderer = new(); + string testStr = "9/2"; + + // Only the fraction slash forms fractions automatically; the solidus is + // an ordinary character. Verified against HarfBuzz 14.2.1. + int[] expectedGlyphIndices = [29, 19, 22]; + + // act + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font)); + + // assert + Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); + for (int i = 0; i < expectedGlyphIndices.Length; i++) + { + Assert.Equal(expectedGlyphIndices[i], renderer.GlyphKeys[i].GlyphId); + } + } + + [Fact] + public void ContextualFractions_FractionSlashWithoutDigits_FormsNoFraction() + { + // arrange + Font font = TestFonts.GetFont(TestFonts.RobotoRegular, 12); + ColorGlyphRenderer renderer = new(); + string testStr = "abcโ„def"; + + // A fraction slash needs digits on both sides. Verified against HarfBuzz 14.2.1. + int[] expectedGlyphIndices = [69, 70, 71, 404, 72, 73, 74]; + + // act + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font)); + + // assert + Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); + for (int i = 0; i < expectedGlyphIndices.Length; i++) + { + Assert.Equal(expectedGlyphIndices[i], renderer.GlyphKeys[i].GlyphId); + } + } + + [Fact] + public void ContextualFractions_MultiDigitRuns_FormFraction() + { + // arrange + Font font = TestFonts.GetFont(TestFonts.RobotoRegular, 12); + ColorGlyphRenderer renderer = new(); + string testStr = "12โ„34"; + + // Both digit runs are consumed whole. Verified against HarfBuzz 14.2.1. + int[] expectedGlyphIndices = [122, 115, 404, 543, 544]; + + // act + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font)); + + // assert + Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); + for (int i = 0; i < expectedGlyphIndices.Length; i++) + { + Assert.Equal(expectedGlyphIndices[i], renderer.GlyphKeys[i].GlyphId); + } + } + [Fact] public void ContextualFractions_WithSlash_Works() { From eb3729f7ebd5eca0c3d23f5d68b649eecd63ee0f Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 26 Jul 2026 19:07:09 +1000 Subject: [PATCH 69/94] Match backtrack against the records a pass has produced A chained rule's backtrack describes the glyphs preceding its input, and those glyphs are whatever earlier lookups in the same pass produced - not the input those lookups consumed. Matching walked the input side for all three sequences, so a rule whose backtrack covers a glyph an earlier lookup already replaced saw the original instead of the replacement. While a pass is aliased and level the two sides are the same records in the same slots, which is why this held for every text in the corpus: it takes a pass that has changed a length before the cursor for the sides to diverge. The matcher now carries which side it reads, backtrack points it at the produced side and starts one skipping step back from that side's length, and the walk bounds itself by that length. Property tests, joiner transparency, and the mask and syllable gates all follow the same side, so a backtrack step skips what the lookup ignores exactly as a forward step does. Measured against the parent commit on the interleaved local harness: Latin -1.0, Arabic -2.0, Devanagari -0.3 percent, all inside the harness's noise. Suite 5,605/0; fraction, emoji ZWJ, and joiner differentials against HarfBuzzSharp all match; zero steady-state allocation preserved. --- .../AdvancedTypographicUtils.cs | 108 ++++++++++++++---- .../SkippingGlyphIterator.cs | 57 ++++++++- 2 files changed, 142 insertions(+), 23 deletions(-) diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs index aed5547e0..2d03ca431 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs @@ -534,6 +534,62 @@ public static bool MatchBacktrackCoverageSequence( out _); } + /// + /// Matches a backtrack sequence by class value over the records the pass has + /// produced. The class table travels as match state so the lambda stays + /// static: a capturing lambda here would allocate on every rule attempt. + /// + /// The skipping glyph iterator, already pointed at the produced side. + /// The first backtrack position. + /// The array of class values to match. + /// The class definition table for the backtrack sequence. + /// if the entire sequence was matched; otherwise, . + private static bool MatchBacktrackClassSequence( + SkippingGlyphIterator iterator, + int startIndex, + ushort[] sequence, + ClassDefinitionTable classDefinitionTable) + { + if (sequence.Length == 0) + { + return true; + } + + int offset = startIndex; + int limit = iterator.RecordCount; + int i = 0; + while (i < sequence.Length && i < MaxContextLength) + { + if (offset < 0 || offset >= limit) + { + return false; + } + + if (iterator.IsPropertySkipped(offset)) + { + offset--; + continue; + } + + ref GlyphShapingData data = ref iterator.RecordAt(offset); + if (iterator.MayMatch(ref data) && sequence[i] == classDefinitionTable.ClassIndexOf(data.GlyphId)) + { + i++; + offset--; + continue; + } + + if (!iterator.IsTransparent(ref data)) + { + return false; + } + + offset--; + } + + return true; + } + /// /// Applies a chained sequence rule by matching backtrack, input, and lookahead glyph ID sequences. /// The input matches under the applying lookup's mask and joiner handling; @@ -565,10 +621,22 @@ public static bool ApplyChainedSequenceRule(SkippingGlyphIterator iterator, Chai } } - if (rule.BacktrackSequence.Length > 0 - && !MatchSequence(iterator, -rule.BacktrackSequence.Length, rule.BacktrackSequence, 0, true)) + if (rule.BacktrackSequence.Length > 0) { - return false; + SkippingGlyphIterator backIt = iterator; + int backtrackStart = backIt.StartBacktrack(); + if (!Match( + backIt, + backtrackStart, + rule.BacktrackSequence, + MatchDirection.Backward, + int.MaxValue, + static (component, data) => component == data.GlyphId, + default, + out _)) + { + return false; + } } return true; @@ -615,10 +683,14 @@ public static bool ApplyChainedClassSequenceRule( } } - if (rule.BacktrackSequence.Length > 0 - && !MatchClassSequence(iterator, -rule.BacktrackSequence.Length, rule.BacktrackSequence, backtrackClassDefinitionTable, 0, true)) + if (rule.BacktrackSequence.Length > 0) { - return false; + SkippingGlyphIterator backIt = iterator; + int backtrackStart = backIt.StartBacktrack(); + if (!MatchBacktrackClassSequence(backIt, backtrackStart, rule.BacktrackSequence, backtrackClassDefinitionTable)) + { + return false; + } } return true; @@ -656,20 +728,16 @@ public static bool CheckAllCoverages( SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, lookupFlags, markFilteringSet); - // Compute backtrack start using skippy prev(), not index-1: context steps - // are transparent to joiners, so the step lands on the first solid glyph. - int backtrackStart = index; + // Backtrack steps back through the records the pass produced, skipping + // as context so joiners are transparent to it. if (backtrack.Length > 0) { SkippingGlyphIterator backIt = iterator; - backIt.SetMatchContext(0, true); - backIt.Index = index; - backtrackStart = backIt.Prev(); // first backtrack glyph (i-1 in skippy space) - } - - if (!MatchBacktrackCoverageSequence(iterator, backtrack, backtrackStart, endExclusive)) - { - return false; + int backtrackStart = backIt.StartBacktrack(); + if (!MatchBacktrackCoverageSequence(backIt, backtrack, backtrackStart, int.MaxValue)) + { + return false; + } } // Input starts at the current glyph position; lookahead starts exactly @@ -1162,8 +1230,8 @@ private static bool Match( int offset = startIndex; int step = direction == MatchDirection.Forward ? 1 : -1; - ShapingBuffer buffer = iterator.Collection; - int limit = Math.Min(endExclusive, buffer.Count); + // Backtrack reads the records the pass produced, so that side bounds it. + int limit = Math.Min(endExclusive, iterator.RecordCount); // A transparent record is stepped over unless it matches the sequence // position itself; a solid record that fails the shape test refuses the @@ -1183,7 +1251,7 @@ private static bool Match( continue; } - ref GlyphShapingData data = ref buffer[offset]; + ref GlyphShapingData data = ref iterator.RecordAt(offset); if (iterator.MayMatch(ref data) && condition(sequence[i], data)) { if (matches.Length == MaxContextLength) diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs index a1d006086..a45775060 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs @@ -34,6 +34,14 @@ internal struct SkippingGlyphIterator /// private bool stepsDirectly; + /// + /// True when this matcher reads the records the active pass has produced + /// rather than the input it has yet to consume. Backtrack reads that side: + /// the records behind the cursor were consumed by the pass, and a rule must + /// see what earlier lookups produced rather than the input they replaced. + /// + private bool readsProducedSide; + /// /// The bit recording that default-ignorable /// transparency is active for the duration of sequence matching; plain @@ -138,6 +146,48 @@ public SkippingGlyphIterator( /// public readonly bool MatchTransparencyActive => (this.matchFlags & TransparencyActiveFlag) != 0; + /// + /// Gets the number of records on the side this matcher reads. + /// + public readonly int RecordCount + => this.readsProducedSide ? this.Collection.PassOutputCount : this.Collection.Count; + + /// + /// Gets a reference to the record at the given index on the side this + /// matcher reads. + /// + /// The zero-based index on the read side. + /// A reference to the record. + public readonly ref GlyphShapingData RecordAt(int index) + { + if (this.readsProducedSide) + { + return ref this.Collection.PassOutputAt(index); + } + + return ref this.Collection[index]; + } + + /// + /// Points this matcher at the side backtrack reads, stamps it as context, + /// and steps to the first backtrack position. During a substitution pass + /// that side is the records the pass has produced; outside one the records + /// behind the cursor are the buffer itself. + /// + /// The first backtrack position, or a negative value when none remains. + public int StartBacktrack() + { + ShapingBuffer buffer = this.Collection; + if (buffer.IsPassActive) + { + this.readsProducedSide = true; + this.Index = buffer.PassOutputCount; + } + + this.SetMatchContext(0, true); + return this.Prev(); + } + /// /// Advances to the next non-skipped glyph in the forward direction. /// @@ -213,7 +263,8 @@ private void Move(int direction) return; } - while (this.Index >= 0 && this.Index < this.Collection.Count) + int limit = this.RecordCount; + while (this.Index >= 0 && this.Index < limit) { // The class-mask test only runs when the flags can actually ignore // something; a skips-nothing iterator steps straight to transparency. @@ -224,7 +275,7 @@ private void Move(int direction) break; } - ref GlyphShapingData data = ref this.Collection[this.Index]; + ref GlyphShapingData data = ref this.RecordAt(this.Index); if (!this.IsTransparent(ref data)) { break; @@ -362,7 +413,7 @@ public readonly bool MayMatch(ref GlyphShapingData data) /// if the glyph should be skipped; otherwise, . private readonly bool ShouldIgnore(int index) { - ref GlyphShapingData data = ref this.Collection[index]; + ref GlyphShapingData data = ref this.RecordAt(index); // The shaping class is cached on the glyph keyed by glyph id; test the cache // inline so the common hit path avoids the classification call entirely. From 8d2f82df1437a3b22812e0283b8076209ce59e13 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 26 Jul 2026 19:39:52 +1000 Subject: [PATCH 70/94] Stream nested lookups through the pass cursor Contextual and chained lookups applied their nested lookups by mutating the input side in place while the outer match held the pass cursor. The reference brings the cursor to each record instead and lets the nested lookup consume and produce exactly as a top-level one does. The difference is visible output, not just cost: a sequence index can name a record an earlier nested lookup produced, which the input side has no name for, so a rule whose second lookup targets the output of its first never applied. Telugu conjuncts were the plain case - the reference forms one glyph where we left three. Nested application now positions the cursor at each matched record, and the matched positions are carried in the produced side's frame so they can name produced records. Reconciling them after a length change is the reference's own arithmetic: growth inserts positions after the applying record, a shrink drops the positions it consumed, the rest shift, and the end is clamped so a lookup that removed a great deal cannot rewind it behind the record it applied at. The cursor can now move backwards, which needs room ahead of the read position for records returning to the input side. A pass that produced more than it consumed has none, so the unread input moves up to open it, growing the input length while leaving the buffer's total unchanged. Two things had to hold for that to be safe. The streams parallel to the records are sized independently, because ending a pass swaps the record storage with the output storage and the two grow separately - the records could already outnumber their metrics and position slots, which this makes impossible. And the substitution driver no longer holds the segment's end as an index across the pass; it holds the number of records that follow the segment, which nothing the pass does can disturb, so the bound needs no correcting after a replacement, an insertion, or the room a rewind opens. Verified against HarfBuzz 14.2.1: the Telugu conjunct and a Format 3 contextual case both match where they previously did not, and the fraction, emoji ZWJ, and joiner rows all still match. Measured against the parent commit on the interleaved local harness: Latin parity, Arabic -2.5, Devanagari +0.9 percent. Suite 5,605/0; zero steady-state allocation preserved on all three scripts. --- src/SixLabors.Fonts/ShapingBuffer.cs | 71 ++++++++++++++++++ .../AdvancedTypographicUtils.cs | 75 ++++++++++++------- .../GPos/LookupType8SubTable.cs | 6 +- .../GSub/LookupListTable.cs | 7 +- .../GSub/LookupType5SubTable.cs | 18 ++--- .../GSub/LookupType6SubTable.cs | 19 ++--- .../Tables/AdvancedTypographic/GSubTable.cs | 33 ++++---- 7 files changed, 155 insertions(+), 74 deletions(-) diff --git a/src/SixLabors.Fonts/ShapingBuffer.cs b/src/SixLabors.Fonts/ShapingBuffer.cs index 289ce6b7d..0afdaf881 100644 --- a/src/SixLabors.Fonts/ShapingBuffer.cs +++ b/src/SixLabors.Fonts/ShapingBuffer.cs @@ -335,6 +335,18 @@ public ShapingBuffer(TextOptions textOptions, ShapingBufferRole role) /// public bool IsNestedApplication => this.nestedApplicationDepth > 0; + /// + /// Gets the number of records behind the pass position: those the pass has + /// produced while a pass is active, and those before the cursor otherwise. + /// Positions handed to are measured against this. + /// + public int PassBacktrackLength => this.IsPassActive ? this.PassOutputCount : this.ReadIndex; + + /// + /// Gets the number of records ahead of the pass position, still to be read. + /// + public int PassLookaheadLength => this.Count - this.ReadIndex; + /// /// Gets a value indicating whether a further nested lookup application would /// exceed the maximum nesting depth. A font may chain contextual lookups @@ -1812,6 +1824,7 @@ public void EndOutputPass() } this.Count = this.PassOutputCount; + this.EnsureCapacity(this.Count); this.IsPassActive = false; this.passDiverged = false; this.ReadIndex = 0; @@ -1882,6 +1895,16 @@ public void MoveTo(int outputPosition) if (outputPosition < this.PassOutputCount) { int rewound = this.PassOutputCount - outputPosition; + + // Produced records return to the input side ahead of the read + // cursor, which needs that many free slots behind it. A pass that + // produced more than it consumed has fewer, so the unread input + // moves up to open them. + if (rewound > this.ReadIndex) + { + this.ShiftInputForward(rewound - this.ReadIndex); + } + this.ReadIndex -= rewound; if (this.passDiverged) { @@ -1896,6 +1919,54 @@ public void MoveTo(int outputPosition) } } + /// + /// Moves the unread input up by the given number of slots, opening room + /// between the produced records and the read cursor. The records that fill + /// them come back from the produced side, so the buffer's total is + /// unchanged even though its input length grows. + /// + /// The number of slots to open. + private void ShiftInputForward(int count) + { + int unread = this.Count - this.ReadIndex; + this.EnsureCapacity(this.Count + count); + if (unread > 0) + { + Array.Copy(this.data, this.ReadIndex, this.data, this.ReadIndex + count, unread); + } + + this.ReadIndex += count; + this.Count += count; + } + + /// + /// Grows the record storage and the streams parallel to it. The three stay + /// the same length: every record addresses its metrics and its position by + /// its own index. + /// + /// The required record capacity. + private void EnsureCapacity(int required) + { + if (required > this.data.Length) + { + Array.Resize(ref this.data, Math.Max(this.data.Length * 2, required)); + } + + // The parallel streams are sized independently because ending a pass + // swaps the record storage with the output storage, which grew on its + // own; after such a swap the records can outnumber the slots that were + // allocated alongside them. + if (required > this.metrics.Length) + { + Array.Resize(ref this.metrics, Math.Max(this.metrics.Length * 2, required)); + } + + if (required > this.positions.Length) + { + Array.Resize(ref this.positions, Math.Max(this.positions.Length * 2, required)); + } + } + /// /// Enters a nested lookup application within a contextual match. /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs index 2d03ca431..cefed9dce 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs @@ -106,37 +106,40 @@ public static int GetMaxAllowableShapingOperationsCount(int length) /// The GSUB table. /// The feature tag being applied. /// The applying lookup's combined mask, inherited by the nested lookups. - /// The lookup flags for glyph filtering. - /// The mark filtering set index. /// The sequence lookup records specifying which lookups to apply at which positions. /// The glyph substitution buffer. /// The buffer positions the input sequence matched at, reconciled as nested lookups change the buffer's length. /// The number of matched positions. - /// The number of glyphs in the input sequence. + /// The position one past the final matched input record. /// if the lookups were applied. public static bool ApplyLookupList( FontMetrics fontMetrics, GSubTable table, Tag feature, uint lookupMask, - LookupFlags lookupFlags, - ushort markFilteringSet, SequenceLookupRecord[] records, ShapingBuffer buffer, Span matchPositions, int matchCount, - int count) + int matchEnd) { if (buffer.NestingLimitReached) { return false; } - int startIndex = matchPositions[0]; - int currentCount = buffer.Count; + // Matching walked the records still to be read; applying works against + // the pass position, where the produced side has its own length. A + // sequence index can name a record a nested lookup produced, which no + // input position describes, so the matched positions move into that + // frame once and stay in it. + int shift = buffer.PassBacktrackLength - buffer.ReadIndex; + int end = buffer.PassBacktrackLength + (matchEnd - buffer.ReadIndex); + for (int i = 0; i < matchCount; i++) + { + matchPositions[i] += shift; + } - // Nested lookups mutate the input side in place whatever their type: the - // contextual match that recursed here owns the pass cursor. buffer.PushNestedApplication(); foreach (SequenceLookupRecord lookupRecord in records) @@ -147,33 +150,47 @@ public static bool ApplyLookupList( continue; } + int total = buffer.PassBacktrackLength + buffer.PassLookaheadLength; int position = matchPositions[sequenceIndex]; - // An earlier nested lookup can delete enough records to strand a - // later sequence position past the buffer's end. - if (position >= buffer.Count) + // An earlier nested lookup can consume enough records to strand a + // later sequence position past everything that remains. + if (position >= total) { continue; } + // The nested lookup applies at the cursor, so the cursor goes to the + // record: forward over what it passes, back over what an earlier + // nested lookup already produced. + buffer.MoveTo(position); + GSub.LookupTable lookup = table.LookupList.LookupTables[lookupRecord.LookupListIndex]; - _ = lookup.TrySubstitution(fontMetrics, table, buffer, feature, lookupMask, position, count - (position - startIndex)); + _ = lookup.TrySubstitution(fontMetrics, table, buffer, feature, lookupMask, buffer.ReadIndex, buffer.PassLookaheadLength); - // Account for substitutions changing the length of the buffer, both - // in the window the remaining lookups see and in the matched - // positions themselves. - int delta = buffer.Count - currentCount; + int delta = buffer.PassBacktrackLength + buffer.PassLookaheadLength - total; if (delta == 0) { continue; } - count += delta; - currentCount = buffer.Count; + end += delta; + if (end < position) + { + // A nested lookup that consumed a great deal can pull the end + // behind the record it applied at; nothing before that record + // can have been consumed, so the end stops there. + delta += position - end; + end = position; + } + matchCount = FixupMatchPositions(matchPositions, matchCount, sequenceIndex, delta); } buffer.PopNestedApplication(); + + // Everything the rule matched is now behind the pass position. + buffer.MoveTo(end); return true; } @@ -599,10 +616,11 @@ private static bool MatchBacktrackClassSequence( /// The chained sequence rule table to apply. /// The applying lookup's mask. /// The span receiving the matched input positions, offset by one for the coverage-matched first glyph. + /// The position one past the final matched input record. /// if all sequences matched; otherwise, . - public static bool ApplyChainedSequenceRule(SkippingGlyphIterator iterator, ChainedSequenceRuleTable rule, uint mask, Span matches) + public static bool ApplyChainedSequenceRule(SkippingGlyphIterator iterator, ChainedSequenceRuleTable rule, uint mask, Span matches, out int matchEnd) { - int matchEnd = iterator.Index + 1; + matchEnd = iterator.Index + 1; if (rule.InputSequence.Length > 0 && !MatchSequence(iterator, 1, rule.InputSequence, mask, false, matches, out matchEnd)) { @@ -654,6 +672,7 @@ public static bool ApplyChainedSequenceRule(SkippingGlyphIterator iterator, Chai /// The class definition table for the lookahead sequence. /// The applying lookup's mask. /// The span receiving the matched input positions, offset by one for the coverage-matched first glyph. + /// The position one past the final matched input record. /// if all sequences matched; otherwise, . public static bool ApplyChainedClassSequenceRule( SkippingGlyphIterator iterator, @@ -662,9 +681,10 @@ public static bool ApplyChainedClassSequenceRule( ClassDefinitionTable backtrackClassDefinitionTable, ClassDefinitionTable lookaheadClassDefinitionTable, uint mask, - Span matches) + Span matches, + out int matchEnd) { - int matchEnd = iterator.Index + 1; + matchEnd = iterator.Index + 1; if (rule.InputSequence.Length > 0 && !MatchClassSequence(iterator, 1, rule.InputSequence, inputClassDefinitionTable, mask, false, matches, out matchEnd)) { @@ -710,6 +730,7 @@ public static bool ApplyChainedClassSequenceRule( /// The array of lookahead coverage tables. /// The applying lookup's mask; the input matches under it. /// The span receiving the matched input positions; the input coverage array covers the whole input including its first glyph. + /// The position one past the final matched input record. /// if all coverages matched; otherwise, . public static bool CheckAllCoverages( FontMetrics fontMetrics, @@ -722,7 +743,8 @@ public static bool CheckAllCoverages( CoverageTable[] backtrack, CoverageTable[] lookahead, uint mask, - Span matches) + Span matches, + out int matchEnd) { int endExclusive = index + count; @@ -730,6 +752,7 @@ public static bool CheckAllCoverages( // Backtrack steps back through the records the pass produced, skipping // as context so joiners are transparent to it. + matchEnd = index; if (backtrack.Length > 0) { SkippingGlyphIterator backIt = iterator; @@ -742,7 +765,7 @@ public static bool CheckAllCoverages( // Input starts at the current glyph position; lookahead starts exactly // one past the final matched input element. - if (!MatchCoverageSequence(iterator, input, index, endExclusive, mask, false, matches, out int matchEnd)) + if (!MatchCoverageSequence(iterator, input, index, endExclusive, mask, false, matches, out matchEnd)) { return false; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType8SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType8SubTable.cs index 803bf135e..e5dbd56e0 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType8SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType8SubTable.cs @@ -126,7 +126,7 @@ public override bool TryUpdatePosition( for (int lookupIndex = 0; lookupIndex < rules.Length; lookupIndex++) { ChainedSequenceRuleTable rule = rules[lookupIndex]; - if (!AdvancedTypographicUtils.ApplyChainedSequenceRule(iterator, rule, buffer.LookupMask, matchPositions[1..])) + if (!AdvancedTypographicUtils.ApplyChainedSequenceRule(iterator, rule, buffer.LookupMask, matchPositions[1..], out _)) { continue; } @@ -279,7 +279,7 @@ public override bool TryUpdatePosition( { ChainedClassSequenceRuleTable rule = rules[lookupIndex]; - if (!AdvancedTypographicUtils.ApplyChainedClassSequenceRule(iterator, rule, this.inputClassDefinitionTable, this.backtrackClassDefinitionTable, this.lookaheadClassDefinitionTable, buffer.LookupMask, matchPositions[1..])) + if (!AdvancedTypographicUtils.ApplyChainedClassSequenceRule(iterator, rule, this.inputClassDefinitionTable, this.backtrackClassDefinitionTable, this.lookaheadClassDefinitionTable, buffer.LookupMask, matchPositions[1..], out _)) { continue; } @@ -410,7 +410,7 @@ public override bool TryUpdatePosition( // first glyph, so the match fills every position nested lookups // address. Span matchPositions = stackalloc int[AdvancedTypographicUtils.MaxContextLength]; - if (!AdvancedTypographicUtils.CheckAllCoverages(fontMetrics, this.LookupFlags, this.MarkFilteringSet, buffer, index, count, this.inputCoverageTables, this.backtrackCoverageTables, this.lookaheadCoverageTables, buffer.LookupMask, matchPositions)) + if (!AdvancedTypographicUtils.CheckAllCoverages(fontMetrics, this.LookupFlags, this.MarkFilteringSet, buffer, index, count, this.inputCoverageTables, this.backtrackCoverageTables, this.lookaheadCoverageTables, buffer.LookupMask, matchPositions, out _)) { return false; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs index 08e878e4d..c9bc17f1a 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs @@ -218,9 +218,10 @@ public bool TrySubstitution( continue; } - // Nested applications never consume through the pass cursor: the outer - // contextual match owns it, and its replacements run in place. - buffer.DirectConsume = !buffer.IsNestedApplication && subTable.ConsumesDirectly; + // Nested applications consume through the pass cursor exactly as + // top-level ones do; the contextual match brings the cursor to each + // record before recursing. + buffer.DirectConsume = subTable.ConsumesDirectly; if (subTable.TrySubstitution(fontMetrics, table, buffer, feature, lookupMask, index, count)) { // A lookup is finished for a glyph after the client locates the target diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs index b2cf40daa..08f437ab2 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs @@ -122,7 +122,7 @@ public override bool TrySubstitution( continue; } - if (!AdvancedTypographicUtils.MatchSequence(iterator, 1, ruleTable.InputSequence, lookupMask, false, matchPositions[1..], out _)) + if (!AdvancedTypographicUtils.MatchSequence(iterator, 1, ruleTable.InputSequence, lookupMask, false, matchPositions[1..], out int matchEnd)) { continue; } @@ -133,13 +133,11 @@ public override bool TrySubstitution( table, feature, lookupMask, - this.LookupFlags, - this.MarkFilteringSet, ruleTable.SequenceLookupRecords, buffer, matchPositions, seqLength + 1, - count); + matchEnd); } return false; @@ -293,7 +291,7 @@ public override bool TrySubstitution( continue; } - if (!AdvancedTypographicUtils.MatchClassSequence(iterator, 1, ruleTable.InputSequence, this.classDefinitionTable, lookupMask, false, matchPositions[1..], out _)) + if (!AdvancedTypographicUtils.MatchClassSequence(iterator, 1, ruleTable.InputSequence, this.classDefinitionTable, lookupMask, false, matchPositions[1..], out int matchEnd)) { continue; } @@ -304,13 +302,11 @@ public override bool TrySubstitution( table, feature, lookupMask, - this.LookupFlags, - this.MarkFilteringSet, ruleTable.SequenceLookupRecords, buffer, matchPositions, seqLength + 1, - count); + matchEnd); } return false; @@ -442,7 +438,7 @@ public override bool TrySubstitution( // glyph, so the match fills every position nested lookups address. SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, this.LookupFlags, this.MarkFilteringSet); Span matchPositions = stackalloc int[AdvancedTypographicUtils.MaxContextLength]; - if (!AdvancedTypographicUtils.MatchCoverageSequence(iterator, this.coverageTables, index, index + count, lookupMask, false, matchPositions, out _)) + if (!AdvancedTypographicUtils.MatchCoverageSequence(iterator, this.coverageTables, index, index + count, lookupMask, false, matchPositions, out int matchEnd)) { return false; } @@ -453,13 +449,11 @@ public override bool TrySubstitution( table, feature, lookupMask, - this.LookupFlags, - this.MarkFilteringSet, this.sequenceLookupRecords, buffer, matchPositions, this.coverageTables.Length, - count); + matchEnd); } /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs index dacfb40ed..312a4c880 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs @@ -127,7 +127,7 @@ public override bool TrySubstitution( for (int i = 0; i < rules.Length; i++) { ChainedSequenceRuleTable ruleTable = rules[i]; - if (!AdvancedTypographicUtils.ApplyChainedSequenceRule(iterator, ruleTable, lookupMask, matchPositions[1..])) + if (!AdvancedTypographicUtils.ApplyChainedSequenceRule(iterator, ruleTable, lookupMask, matchPositions[1..], out int matchEnd)) { continue; } @@ -137,13 +137,11 @@ public override bool TrySubstitution( table, feature, lookupMask, - this.LookupFlags, - this.MarkFilteringSet, ruleTable.SequenceLookupRecords, buffer, matchPositions, ruleTable.InputSequence.Length + 1, - count); + matchEnd); } return false; @@ -325,7 +323,7 @@ public override bool TrySubstitution( { ChainedClassSequenceRuleTable ruleTable = rules[lookupIndex]; - if (!AdvancedTypographicUtils.ApplyChainedClassSequenceRule(iterator, ruleTable, this.inputClassDefinitionTable, this.backtrackClassDefinitionTable, this.lookaheadClassDefinitionTable, lookupMask, matchPositions[1..])) + if (!AdvancedTypographicUtils.ApplyChainedClassSequenceRule(iterator, ruleTable, this.inputClassDefinitionTable, this.backtrackClassDefinitionTable, this.lookaheadClassDefinitionTable, lookupMask, matchPositions[1..], out int matchEnd)) { continue; } @@ -335,13 +333,11 @@ public override bool TrySubstitution( table, feature, lookupMask, - this.LookupFlags, - this.MarkFilteringSet, ruleTable.SequenceLookupRecords, buffer, matchPositions, ruleTable.InputSequence.Length + 1, - count); + matchEnd); } return false; @@ -514,7 +510,8 @@ public override bool TrySubstitution( this.backtrackCoverageTables, this.lookaheadCoverageTables, lookupMask, - matchPositions)) + matchPositions, + out int matchEnd)) { return false; } @@ -525,13 +522,11 @@ public override bool TrySubstitution( table, feature, lookupMask, - this.LookupFlags, - this.MarkFilteringSet, this.sequenceLookupRecords, buffer, matchPositions, this.inputCoverageTables.Length, - count); + matchEnd); } /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs index de7ebced0..b73ada4e7 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs @@ -315,17 +315,21 @@ private void ApplyMergedLookups( // matchers rely on. buffer.BeginOutputPass(index); - int segmentEnd = index + count; - while (buffer.ReadIndex < segmentEnd && buffer.ReadIndex < buffer.Count) + // The segment's end is held as the number of records that follow it, + // which nothing the pass does can disturb: replacements, insertions, + // and the room a rewind opens all land before the tail. An absolute + // index would have to be corrected after each of them. + int totalBefore = buffer.Count; + int segmentTail = buffer.Count - (index + count); + while (buffer.ReadIndex < buffer.Count - segmentTail && buffer.ReadIndex < buffer.Count) { if (buffer.Count >= maxCount || currentOperations++ >= maxOperationsCount) { // The pass must always close: stream the remainder and // reconcile the segment bookkeeping before bailing out. - int limitBefore = buffer.Count; buffer.EndOutputPass(); - count += buffer.Count - limitBefore; - i += buffer.Count - limitBefore; + count += buffer.Count - totalBefore; + i += buffer.Count - totalBefore; collectionCount = buffer.Count; return; } @@ -344,16 +348,7 @@ private void ApplyMergedLookups( continue; } - int beforeCount = buffer.Count; - featureLookupTable.TrySubstitution(fontMetrics, this, buffer, feature, featureMask, position, segmentEnd - position); - - // In-place mutations from contextual nesting change the input side - // directly and move the segment bound; cursor consumption surfaces - // only when the pass ends. - int inPlaceDelta = buffer.Count - beforeCount; - segmentEnd += inPlaceDelta; - count += inPlaceDelta; - i += inPlaceDelta; + featureLookupTable.TrySubstitution(fontMetrics, this, buffer, feature, featureMask, position, buffer.Count - segmentTail - position); if (buffer.ReadIndex == position) { @@ -361,10 +356,12 @@ private void ApplyMergedLookups( } } - int passBefore = buffer.Count; buffer.EndOutputPass(); - count += buffer.Count - passBefore; - i += buffer.Count - passBefore; + + // The pass closed, so the buffer holds the records it produced: the + // whole segment's change surfaces once, here. + count += buffer.Count - totalBefore; + i += buffer.Count - totalBefore; collectionCount = buffer.Count; } } From 5f8c30c676aefa812604243223323f5764d59dc3 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 26 Jul 2026 19:58:35 +1000 Subject: [PATCH 71/94] Keep the pass bookkeeping off the per-glyph path Streaming nested lookups moved two pieces of bookkeeping into paths hot enough to measure: Devanagari lost five percent of its shaping time. The substitution driver had begun deriving the segment's end from the buffer on every glyph it gated, trading a local comparison for two property reads and a subtraction in the innermost loop of the pass. It holds the end as a plain index again and moves it only when the input side actually lengthens, which an in-place mutation and the room a rewind opens both do by the same amount, so the bound stays correct without being recomputed. Contextual application also rebased its matched positions on every rule, though the frames it converts between coincide until a lookup in the pass changes a length, which is most passes and all of Latin. The conversion is skipped when the two sides are level. Profiles before and after are the same shape, with no cost left at either site: substitution application falls from 11.3 to 9.1 percent of samples and sequence matching from 3.2 to 2.5. Measured against the commit before the streaming leg, interleaved, four pairs per scenario: Latin and Arabic at parity, Devanagari 1.5 percent faster. Suite 5,605/0; all differential rows against HarfBuzzSharp unchanged; zero steady-state allocation preserved. --- .../AdvancedTypographic/AdvancedTypographicUtils.cs | 10 ++++++++-- .../Tables/AdvancedTypographic/GSubTable.cs | 13 ++++++++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs index cefed9dce..2c98f4dc0 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs @@ -135,9 +135,15 @@ public static bool ApplyLookupList( // frame once and stay in it. int shift = buffer.PassBacktrackLength - buffer.ReadIndex; int end = buffer.PassBacktrackLength + (matchEnd - buffer.ReadIndex); - for (int i = 0; i < matchCount; i++) + + // The two sides sit level until a lookup in this pass changes a length, + // and level means the frames coincide. + if (shift != 0) { - matchPositions[i] += shift; + for (int i = 0; i < matchCount; i++) + { + matchPositions[i] += shift; + } } buffer.PushNestedApplication(); diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs index b73ada4e7..e19a6c001 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs @@ -320,8 +320,8 @@ private void ApplyMergedLookups( // and the room a rewind opens all land before the tail. An absolute // index would have to be corrected after each of them. int totalBefore = buffer.Count; - int segmentTail = buffer.Count - (index + count); - while (buffer.ReadIndex < buffer.Count - segmentTail && buffer.ReadIndex < buffer.Count) + int segmentEnd = index + count; + while (buffer.ReadIndex < segmentEnd && buffer.ReadIndex < buffer.Count) { if (buffer.Count >= maxCount || currentOperations++ >= maxOperationsCount) { @@ -348,7 +348,14 @@ private void ApplyMergedLookups( continue; } - featureLookupTable.TrySubstitution(fontMetrics, this, buffer, feature, featureMask, position, buffer.Count - segmentTail - position); + int lengthBefore = buffer.Count; + featureLookupTable.TrySubstitution(fontMetrics, this, buffer, feature, featureMask, position, segmentEnd - position); + + // Anything that lengthened the input side moved the records + // after the segment along with it: an in-place mutation, or the + // room a rewind opened. Both shift the segment's end by the + // same amount, so the bound stays a plain comparison. + segmentEnd += buffer.Count - lengthBefore; if (buffer.ReadIndex == position) { From cca1868318a41df346042faa88e5b491aed8c43c Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 26 Jul 2026 21:03:00 +1000 Subject: [PATCH 72/94] Map characters through the one subtable a font intends Running HarfBuzz's own in-house shaping corpus against this library found character mapping, not shaping, to be the largest source of disagreement. Three faults, all in cmap. Format 13 was not implemented. It maps a range of characters onto a single glyph, which is how a font answers for characters it has no artwork for, and a font whose only subtable uses it mapped nothing at all: every character came back as notdef. The corpus category built on such a font, 3,825 cases, failed entirely and now passes entirely. Lookup consulted every subtable in turn and took the first answer. A font that carries several encodings intends exactly one of them; the others exist for readers that cannot use it. Consulting them in turn resolves characters through tables the font does not intend for them, which is how a symbol font's characters were being answered from its Macintosh byte table. Selection now picks one subtable in the order the specification's implementations agree on: the symbol encoding, then the encodings that reach beyond the basic multilingual plane, then those that do not, and the Macintosh encoding only when a font offers nothing else. A symbol font addresses its glyphs one private use page up from the characters that reach them, so a lookup that misses is retried there. The reported character coverage follows the same subtable, so a font no longer advertises characters it will not resolve. The sample font's coverage drops from 257 characters to the 7 it maps, which is what HarfBuzz reports for it. Corpus: 10.82 percent of executed cases matching, to 89.49 percent (4,351 of 4,862). Suite 5,605/0 -> 5,607/0. --- .../Tables/General/CMap/Format13SubTable.cs | 173 ++++++++++++++++++ .../Tables/General/CMapTable.cs | 148 +++++++++++++-- .../FontCodePointsTests.cs | 16 +- tests/SixLabors.Fonts.Tests/GlyphTests.cs | 4 +- 4 files changed, 312 insertions(+), 29 deletions(-) create mode 100644 src/SixLabors.Fonts/Tables/General/CMap/Format13SubTable.cs diff --git a/src/SixLabors.Fonts/Tables/General/CMap/Format13SubTable.cs b/src/SixLabors.Fonts/Tables/General/CMap/Format13SubTable.cs new file mode 100644 index 000000000..19e096b59 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/General/CMap/Format13SubTable.cs @@ -0,0 +1,173 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Unicode; +using SixLabors.Fonts.WellKnownIds; + +namespace SixLabors.Fonts.Tables.General.CMap; + +/// +/// Format 13 maps ranges of character codes onto a single glyph each, rather than +/// onto a run of glyphs as format 12 does. Fonts that answer for characters they +/// have no artwork for use it, mapping every character of a range to one glyph +/// such as a blank or a last-resort shape. +/// +/// +internal sealed class Format13SubTable : CMapSubTable +{ + /// + /// Initializes a new instance of the class. + /// + /// The language code for this subtable. + /// The platform identifier. + /// The platform-specific encoding identifier. + /// The array of constant map groups. + public Format13SubTable(uint language, PlatformIDs platform, ushort encoding, ConstantMapGroup[] groups) + : base(platform, encoding, 13) + { + this.Language = language; + this.ConstantMapGroups = groups; + } + + /// + /// Gets the array of constant map groups defining character-to-glyph mappings. + /// + public ConstantMapGroup[] ConstantMapGroups { get; } + + /// + /// Gets the language code for this subtable. + /// + public uint Language { get; } + + /// + public override bool TryGetGlyphId(CodePoint codePoint, out ushort glyphId) + { + int charAsInt = codePoint.Value; + + for (int i = 0; i < this.ConstantMapGroups.Length; i++) + { + ref ConstantMapGroup group = ref this.ConstantMapGroups[i]; + + if (charAsInt >= group.StartCodePoint && charAsInt <= group.EndCodePoint) + { + glyphId = (ushort)group.GlyphId; + return true; + } + } + + glyphId = 0; + return false; + } + + /// + public override bool TryGetCodePoint(ushort glyphId, out CodePoint codePoint) + { + // The mapping is many to one, so a glyph names a whole range of + // characters and the first of them is the only sensible answer. + for (int i = 0; i < this.ConstantMapGroups.Length; i++) + { + ref ConstantMapGroup group = ref this.ConstantMapGroups[i]; + if (glyphId == group.GlyphId) + { + codePoint = new CodePoint((int)group.StartCodePoint); + return true; + } + } + + codePoint = default; + return false; + } + + /// + public override IEnumerable GetAvailableCodePoints() + => this.ConstantMapGroups.SelectMany(group => + { + int start = (int)group.StartCodePoint; + int end = (int)group.EndCodePoint; + return Enumerable.Range(start, end - start + 1); + }); + + /// + /// Loads one or more instances from the specified encoding records and reader. + /// + /// The encoding records that share this subtable. + /// The binary reader positioned after the format field. + /// An enumerable of instances, one per encoding record. + public static IEnumerable Load(IEnumerable encodings, BigEndianBinaryReader reader) + { + // 'cmap' Subtable Format 13: + // Type | Name | Description + // -----------------|-------------------|------------------------------------------------------------------------ + // uint16 | format | Subtable format; set to 13. + // uint16 | reserved | Reserved; set to 0 + // uint32 | length | Byte length of this subtable (including the header) + // uint32 | language | For requirements on use of the language field, see "Use of the language field in 'cmap' subtables" in this document. + // uint32 | numGroups | Number of groupings which follow + // ConstantMapGroup | groups[numGroups] | Array of ConstantMapGroup records. + + // format has already been read by this point skip it + ushort reserved = reader.ReadUInt16(); + uint length = reader.ReadUInt32(); + uint language = reader.ReadUInt32(); + uint numGroups = reader.ReadUInt32(); + + var groups = new ConstantMapGroup[numGroups]; + for (var i = 0; i < numGroups; i++) + { + groups[i] = ConstantMapGroup.Load(reader); + } + + foreach (EncodingRecord encoding in encodings) + { + yield return new Format13SubTable(language, encoding.PlatformID, encoding.EncodingID, groups); + } + } + + /// + /// Represents a constant map group record that maps a contiguous range of + /// character codes to a single glyph index. + /// + internal readonly struct ConstantMapGroup + { + /// + /// The first character code in this group. + /// + public readonly uint StartCodePoint; + + /// + /// The last character code in this group (inclusive). + /// + public readonly uint EndCodePoint; + + /// + /// The glyph index every character code in this group maps to. + /// + public readonly uint GlyphId; + + /// + /// Initializes a new instance of the struct. + /// + /// The first character code in this group. + /// The last character code in this group. + /// The glyph index every character code in this group maps to. + public ConstantMapGroup(uint startCodePoint, uint endCodePoint, uint glyphId) + { + this.StartCodePoint = startCodePoint; + this.EndCodePoint = endCodePoint; + this.GlyphId = glyphId; + } + + /// + /// Loads a from the specified reader. + /// + /// The binary reader positioned at the constant map group data. + /// The parsed . + public static ConstantMapGroup Load(BigEndianBinaryReader reader) + { + uint startCodePoint = reader.ReadUInt32(); + uint endCodePoint = reader.ReadUInt32(); + uint glyphId = reader.ReadUInt32(); + return new ConstantMapGroup(startCodePoint, endCodePoint, glyphId); + } + } +} diff --git a/src/SixLabors.Fonts/Tables/General/CMapTable.cs b/src/SixLabors.Fonts/Tables/General/CMapTable.cs index 57981314f..38ab58206 100644 --- a/src/SixLabors.Fonts/Tables/General/CMapTable.cs +++ b/src/SixLabors.Fonts/Tables/General/CMapTable.cs @@ -18,11 +18,43 @@ internal sealed class CMapTable : Table /// internal const string TableName = "cmap"; + /// + /// The Windows platform's symbol encoding identifier. A font declaring it + /// does not map Unicode: it maps its own character codes, and the codes it + /// uses live in 's private use page. + /// + private const ushort SymbolEncodingId = 0; + + /// + /// The first character of the private use page a symbol font maps into. + /// Such a font addresses its glyphs as U+F000 upwards while the text that + /// uses it holds the character codes those shadow, so a lookup that misses + /// is retried one page higher. + /// + private const int SymbolPageStart = 0xF000; + + /// + /// The highest character code a symbol font's page can shadow. The page is + /// a single byte wide, U+F000 to U+F0FF, so only a character that fits in a + /// byte has a counterpart there. + /// + private const int SymbolPageLastShadowed = 0xFF; + /// /// The format 14 subtables for Unicode variation sequences. /// private readonly Format14SubTable[] format14SubTables = Array.Empty(); + /// + /// The one subtable characters map through. + /// + private readonly CMapSubTable? characterMap; + + /// + /// Whether uses the symbol encoding. + /// + private readonly bool isSymbolic; + /// /// Cached codepoints available in the font. /// @@ -36,6 +68,8 @@ public CMapTable(IEnumerable tables) { this.Tables = tables.OrderBy(t => GetPreferredPlatformOrder(t.Platform)).ToArray(); this.format14SubTables = this.Tables.OfType().ToArray(); + this.characterMap = SelectCharacterMap(this.Tables, out bool symbolic); + this.isSymbolic = symbolic; } /// @@ -65,6 +99,62 @@ private static int GetPreferredPlatformOrder(PlatformIDs platform) _ => int.MaxValue }; + /// + /// Chooses the one subtable that maps characters to glyphs, in the order the + /// specification's implementations agree on: the Windows symbol encoding + /// first, then the 32-bit Unicode encodings, then the 16-bit ones, and the + /// Macintosh encoding only when a font offers nothing else. A font that + /// carries several is not consulted for more than one of them - a character + /// the chosen subtable does not map is unmapped, not a reason to consult a + /// less preferred encoding, which would resolve characters through a table + /// the font does not intend for them. + /// + /// The subtables the font declares. + /// Whether the chosen subtable uses the symbol encoding. + /// The subtable to map characters through, or when the font declares none. + private static CMapSubTable? SelectCharacterMap(CMapSubTable[] tables, out bool symbolic) + { + symbolic = false; + foreach (CMapSubTable table in tables) + { + if (table.Platform == PlatformIDs.Windows && table.Encoding == SymbolEncodingId) + { + symbolic = true; + return table; + } + } + + // Widest coverage first: an encoding that reaches beyond the basic + // multilingual plane is preferred to one that cannot, and a Unicode + // encoding to the Macintosh one, whose codes are bytes in a legacy + // character set rather than characters. + ReadOnlySpan<(PlatformIDs Platform, ushort Encoding)> preference = + [ + (PlatformIDs.Windows, 10), // Windows, full Unicode + (PlatformIDs.Unicode, 6), // Unicode 13.0 and later, full + (PlatformIDs.Unicode, 4), // Unicode 2.0 and later, full + (PlatformIDs.Windows, 1), // Windows, basic multilingual plane + (PlatformIDs.Unicode, 3), // Unicode 2.0 and later, plane zero + (PlatformIDs.Unicode, 2), // Unicode, ISO/IEC 10646 + (PlatformIDs.Unicode, 1), // Unicode 1.1 + (PlatformIDs.Unicode, 0), // Unicode 1.0 + (PlatformIDs.Macintosh, 0), // Macintosh, single byte codes + ]; + + foreach ((PlatformIDs platform, ushort encoding) in preference) + { + foreach (CMapSubTable table in tables) + { + if (table.Platform == platform && table.Encoding == encoding && table is not Format14SubTable) + { + return table; + } + } + } + + return null; + } + /// /// Tries to get the glyph ID for the given code point, optionally considering the next code point /// for Unicode Variation Sequence (UVS) matching. @@ -110,29 +200,32 @@ public bool TryGetGlyphId(CodePoint codePoint, CodePoint? nextCodePoint, out ush /// if a glyph ID was found; otherwise, . private bool TryGetGlyphId(CodePoint codePoint, out ushort glyphId) { - bool foundFallback = false; + glyphId = 0; + if (this.characterMap is null) + { + return false; + } - foreach (CMapSubTable t in this.Tables) + if (this.characterMap.TryGetGlyphId(codePoint, out glyphId) && glyphId > 0) { - // Keep looking until we have an index that's not the fallback. - // Regardless of the encoding scheme, character codes that do - // not correspond to any glyph in the font should be mapped to glyph index 0. - // The glyph at this location must be a special glyph representing a missing character, commonly known as .notdef. - if (!t.TryGetGlyphId(codePoint, out glyphId)) - { - continue; - } + return true; + } - if (glyphId > 0) + // The text asked for a character the symbol font does not map, which is + // expected: such a font maps the same glyphs one private use page up. + // Retry there, since the caller's character code is what that page + // shadows. + if (this.isSymbolic && codePoint.Value <= SymbolPageLastShadowed) + { + CodePoint shadowed = new(SymbolPageStart + codePoint.Value); + if (this.characterMap.TryGetGlyphId(shadowed, out glyphId) && glyphId > 0) { return true; } - - foundFallback = true; } glyphId = 0; - return foundFallback; + return false; } /// @@ -167,10 +260,28 @@ public ReadOnlyMemory GetAvailableCodePoints() } HashSet values = new(); - - foreach (int v in this.Tables.SelectMany(subtable => subtable.GetAvailableCodePoints())) + if (this.characterMap is not null) { - values.Add(v); + // Only the subtable characters map through: a codepoint another + // subtable lists is not one this font resolves. + foreach (int v in this.characterMap.GetAvailableCodePoints()) + { + values.Add(v); + } + + // A symbol font's page is reachable through the character codes it + // shadows as well as through the page itself, so both address the + // same glyph and both belong in the answer. + if (this.isSymbolic) + { + foreach (int v in this.characterMap.GetAvailableCodePoints()) + { + if (v >= SymbolPageStart && v <= SymbolPageStart + SymbolPageLastShadowed) + { + values.Add(v - SymbolPageStart); + } + } + } } return this.codepoints = values.OrderBy(v => v).Select(v => new CodePoint(v)).ToArray(); @@ -222,6 +333,9 @@ public static CMapTable Load(BigEndianBinaryReader reader) case 12: tables.AddRange(Format12SubTable.Load(encoding, reader)); break; + case 13: + tables.AddRange(Format13SubTable.Load(encoding, reader)); + break; case 14: tables.AddRange(Format14SubTable.Load(encoding, reader, offset)); break; diff --git a/tests/SixLabors.Fonts.Tests/FontCodePointsTests.cs b/tests/SixLabors.Fonts.Tests/FontCodePointsTests.cs index 0578a034e..87ff440ba 100644 --- a/tests/SixLabors.Fonts.Tests/FontCodePointsTests.cs +++ b/tests/SixLabors.Fonts.Tests/FontCodePointsTests.cs @@ -16,8 +16,8 @@ public void TtfTest() CodePoint[] codePointArray = codePoints.ToArray(); IEnumerable codePointValues = codePointArray.Select(x => x.Value); - // Compare with https://everythingfonts.com/ttfdump - Assert.Equal(257, codePoints.Length); + // The font maps seven characters. + Assert.Equal(7, codePoints.Length); // Compare with https://fontdrop.info/ Assert.Contains(0x0000, codePointValues); @@ -27,7 +27,6 @@ public void TtfTest() Assert.Contains(0x0042, codePointValues); Assert.Contains(0x0061, codePointValues); Assert.Contains(0x0062, codePointValues); - Assert.Contains(0xFFFF, codePointValues); HashSet glyphIds = []; foreach (CodePoint codePoint in codePoints.Span) @@ -36,8 +35,7 @@ public void TtfTest() glyphIds.Add(glyph.Value.GlyphMetrics.GlyphId); } - // Compare with https://fontdrop.info/ - Assert.Equal(8, glyphIds.Count); + Assert.Equal(7, glyphIds.Count); } [Fact] @@ -49,8 +47,8 @@ public void WoffTest() CodePoint[] codePointArray = codePoints.ToArray(); IEnumerable codePointValues = codePointArray.Select(x => x.Value); - // Compare with https://everythingfonts.com/ttfdump - Assert.Equal(257, codePoints.Length); + // The font maps seven characters. + Assert.Equal(7, codePoints.Length); // Compare with https://fontdrop.info/ Assert.Contains(0x0000, codePointValues); @@ -60,7 +58,6 @@ public void WoffTest() Assert.Contains(0x0042, codePointValues); Assert.Contains(0x0061, codePointValues); Assert.Contains(0x0062, codePointValues); - Assert.Contains(0xFFFF, codePointValues); HashSet glyphIds = []; foreach (CodePoint codePoint in codePoints.Span) @@ -69,7 +66,6 @@ public void WoffTest() glyphIds.Add(glyph.Value.GlyphMetrics.GlyphId); } - // Compare with https://fontdrop.info/ - Assert.Equal(8, glyphIds.Count); + Assert.Equal(7, glyphIds.Count); } } diff --git a/tests/SixLabors.Fonts.Tests/GlyphTests.cs b/tests/SixLabors.Fonts.Tests/GlyphTests.cs index a2639ea05..55ab6a367 100644 --- a/tests/SixLabors.Fonts.Tests/GlyphTests.cs +++ b/tests/SixLabors.Fonts.Tests/GlyphTests.cs @@ -102,10 +102,10 @@ public void LoadGlyph() Font font = TestFonts.GetFont(TestFonts.SimpleFontFile, 12); // Get letter A - Assert.True(font.TryGetGlyphs(new CodePoint(41), ColorFontSupport.None, out Glyph? glyph)); + Assert.True(font.TryGetGlyphs(new CodePoint(0x41), ColorFontSupport.None, out Glyph? glyph)); GlyphVector instance = ((TrueTypeGlyphMetrics)glyph.Value.GlyphMetrics).GetOutline(); - Assert.Equal(20, instance.ControlPoints.Count); + Assert.Equal(6, instance.ControlPoints.Count); } [Fact] From 6c78ede203549ab1a146a2fa25717888daf4b655 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 27 Jul 2026 12:11:46 +1000 Subject: [PATCH 73/94] Improve OpenType shaping parity Add source-derived normalization, script planning, Arabic preprocessing, dedicated Khmer shaping, and GPOS attachment corrections. Add generated shaping data, the pinned reference submodule, and corpus coverage. Correctness (2026-07-27; glyph ID, x/y offset, and x/y advance): | Metric | Before | After | | --- | ---: | ---: | | In-repo corpus failures | 203 | 134 | | Scratch exact cases | 4658/4862 (95.80%) | 4727/4862 (97.22%) | | Khmer exact cases | 89/117 | 117/117 | Verification: - Release non-corpus tests: 5640/5640 passed. - Release corpus tests: 4716/4850 passed. - Reused-buffer allocation tests: 3/3 passed. - Generated output idempotence: no unintended hash changes. The performance gate was not run because active vstest processes and non-idle CPU made a valid measurement impossible. --- .gitmodules | 3 + src/SixLabors.Fonts/GlyphShapingData.cs | 8 + src/SixLabors.Fonts/GlyphShapingPosition.cs | 9 +- src/SixLabors.Fonts/ShapedGlyph.cs | 68 +- src/SixLabors.Fonts/ShapingBuffer.cs | 146 +- src/SixLabors.Fonts/ShapingScratch.cs | 43 + src/SixLabors.Fonts/StreamFontMetrics.cs | 7 + .../GPos/LookupType3SubTable.cs | 92 +- .../GPos/LookupType4SubTable.cs | 27 +- .../GPos/LookupType5SubTable.cs | 27 +- .../GPos/LookupType6SubTable.cs | 9 +- .../Tables/AdvancedTypographic/GPosTable.cs | 101 +- .../Tables/AdvancedTypographic/GSubTable.cs | 64 +- .../AdvancedTypographic/ScriptItemizer.cs | 127 + .../Shapers/ArabicJoining.cs | 275 ++ .../Shapers/ArabicShaper.cs | 78 +- .../AdvancedTypographic/Shapers/BaseShaper.cs | 65 +- .../Shapers/DefaultShaper.cs | 25 +- .../Shapers/HangulShaper.cs | 11 +- .../Shapers/IndicShaper.cs | 63 +- .../Shapers/KhmerShaper.cs | 476 +++ .../Shapers/MyanmarShaper.cs | 6 + .../Shapers/NormalizationMode.cs | 37 + .../Shapers/ShaperFactory.cs | 98 +- .../Shapers/TextNormalizer.cs | 399 +++ .../AdvancedTypographic/Shapers/ThaiShaper.cs | 28 +- .../Shapers/UniversalShaper.cs | 52 +- .../Shapers/VowelConstraints.cs | 82 + .../AdvancedTypographic/SyllableTypeMap.cs | 1 + .../UnicodeScriptTagMap.cs | 18 +- src/SixLabors.Fonts/TextLine.cs | 17 +- src/SixLabors.Fonts/TextShaper.Pipeline.cs | 7 +- src/SixLabors.Fonts/TextShaper.cs | 112 +- src/SixLabors.Fonts/TextShapingBuffer.cs | 82 +- .../Unicode/ArabicJoiningClass.cs | 38 +- .../Unicode/ArabicJoiningType.cs | 10 +- .../Unicode/CodePoint.Normalization.cs | 292 ++ src/SixLabors.Fonts/Unicode/CodePoint.cs | 2 +- .../Resources/ArabicShapingTrie.Generated.cs | 296 +- .../CanonicalCombiningClassTrie.Generated.cs | 270 ++ .../Resources/IndicShapingData.Generated.cs | 549 +-- .../Unicode/Resources/IndicShapingData.cs | 83 +- .../Resources/IndicShapingTrie.Generated.cs | 30 +- .../Resources/KhmerShapingData.Generated.cs | 156 + .../Resources/MarkOrderingData.Generated.cs | 165 + .../Resources/NormalizationData.Generated.cs | 3097 +++++++++++++++++ .../ScriptDirectionData.Generated.cs | 71 + .../VowelConstraintData.Generated.cs | 224 ++ .../Unicode/ScriptHorizontalDirection.cs | 26 + src/SixLabors.Fonts/Unicode/UnicodeData.cs | 57 +- .../Generator.IndicShapingEngine.cs | 107 +- .../Generator.KhmerShapingEngine.cs | 35 + .../Generator.MarkOrdering.cs | 449 +++ .../Generator.Normalization.cs | 289 ++ .../Generator.ScriptDirection.cs | 173 + .../Generator.VowelConstraints.cs | 369 ++ src/UnicodeTrieGenerator/Generator.cs | 11 +- src/UnicodeTrieGenerator/Program.cs | 2 + .../Rules/CompositionExclusions.txt | 222 ++ .../IndicPositionalCategory-Additional.txt | 33 +- .../Rules/IndicShapingInvalidCluster.txt | 198 +- .../IndicSyllabicCategory-Additional.txt | 123 +- src/UnicodeTrieGenerator/Rules/indic.machine | 4 +- src/UnicodeTrieGenerator/Rules/khmer.machine | 17 + .../SixLabors.Fonts.Benchmarks/Config.cs | 30 + .../ShapeTextBenchmark.cs | 29 +- .../HarfBuzzCorpusTests.cs | 370 ++ .../HarfBuzzDifferentialTests.cs | 23 +- .../GPos/GPosTableTests.cs | 8 +- .../GSub/GSubTableTests.Indic.cs | 33 +- .../SixLabors.Fonts.Tests/TextShaperTests.cs | 157 +- tests/harfbuzz | 1 + 72 files changed, 9289 insertions(+), 1423 deletions(-) create mode 100644 src/SixLabors.Fonts/Tables/AdvancedTypographic/ScriptItemizer.cs create mode 100644 src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicJoining.cs create mode 100644 src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/KhmerShaper.cs create mode 100644 src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/NormalizationMode.cs create mode 100644 src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/TextNormalizer.cs create mode 100644 src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/VowelConstraints.cs create mode 100644 src/SixLabors.Fonts/Unicode/CodePoint.Normalization.cs create mode 100644 src/SixLabors.Fonts/Unicode/Resources/CanonicalCombiningClassTrie.Generated.cs create mode 100644 src/SixLabors.Fonts/Unicode/Resources/KhmerShapingData.Generated.cs create mode 100644 src/SixLabors.Fonts/Unicode/Resources/MarkOrderingData.Generated.cs create mode 100644 src/SixLabors.Fonts/Unicode/Resources/NormalizationData.Generated.cs create mode 100644 src/SixLabors.Fonts/Unicode/Resources/ScriptDirectionData.Generated.cs create mode 100644 src/SixLabors.Fonts/Unicode/Resources/VowelConstraintData.Generated.cs create mode 100644 src/SixLabors.Fonts/Unicode/ScriptHorizontalDirection.cs create mode 100644 src/UnicodeTrieGenerator/Generator.KhmerShapingEngine.cs create mode 100644 src/UnicodeTrieGenerator/Generator.MarkOrdering.cs create mode 100644 src/UnicodeTrieGenerator/Generator.Normalization.cs create mode 100644 src/UnicodeTrieGenerator/Generator.ScriptDirection.cs create mode 100644 src/UnicodeTrieGenerator/Generator.VowelConstraints.cs create mode 100644 src/UnicodeTrieGenerator/Rules/CompositionExclusions.txt create mode 100644 src/UnicodeTrieGenerator/Rules/khmer.machine create mode 100644 tests/SixLabors.Fonts.Tests/HarfBuzzCorpusTests.cs create mode 160000 tests/harfbuzz diff --git a/.gitmodules b/.gitmodules index 94d28dd52..5773a171a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "shared-infrastructure"] path = shared-infrastructure url = https://github.com/SixLabors/SharedInfrastructure +[submodule "tests/harfbuzz"] + path = tests/harfbuzz + url = https://github.com/harfbuzz/harfbuzz.git diff --git a/src/SixLabors.Fonts/GlyphShapingData.cs b/src/SixLabors.Fonts/GlyphShapingData.cs index 0ea2476d5..757d68f0e 100644 --- a/src/SixLabors.Fonts/GlyphShapingData.cs +++ b/src/SixLabors.Fonts/GlyphShapingData.cs @@ -140,6 +140,7 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) { this.GlyphId = data.GlyphId; this.CodePointIndex = data.CodePointIndex; + this.GraphemeIndex = data.GraphemeIndex; this.CodePoint = data.CodePoint; this.CodePointCount = data.CodePointCount; this.Direction = data.Direction; @@ -223,6 +224,13 @@ public int ShapingClassCacheKey /// public int CodePointIndex { get; set; } + /// + /// Gets or sets the zero-based index of the grapheme this glyph belongs to. The + /// text is walked grapheme by grapheme as the buffer is filled, so the grouping is + /// recorded there rather than worked out again from the characters later. + /// + public int GraphemeIndex { get; set; } + /// /// Gets or sets the leading codepoint. /// diff --git a/src/SixLabors.Fonts/GlyphShapingPosition.cs b/src/SixLabors.Fonts/GlyphShapingPosition.cs index f406b801f..179d3fa01 100644 --- a/src/SixLabors.Fonts/GlyphShapingPosition.cs +++ b/src/SixLabors.Fonts/GlyphShapingPosition.cs @@ -22,6 +22,11 @@ internal struct GlyphShapingPosition /// private const byte KernedFlag = 1 << 1; + /// + /// The relative cursive attachment value that identifies an unattached glyph. A nonzero value is the signed distance to the parent glyph. + /// + public const int NoCursiveAttachment = 0; + #pragma warning disable SA1401 // Fields exposed so positioning mutates embedded values in place. /// /// The shaping bounds. A field rather than a property so positioning lookups @@ -35,7 +40,7 @@ internal struct GlyphShapingPosition public int MarkAttachment; /// - /// The offset of any cursive attachment, or -1 when unattached. + /// The signed offset of any cursive attachment, or when unattached. /// public int CursiveAttachment; #pragma warning restore SA1401 @@ -55,7 +60,7 @@ public GlyphShapingPosition(GlyphShapingBounds bounds) { this.Bounds = bounds; this.MarkAttachment = -1; - this.CursiveAttachment = -1; + this.CursiveAttachment = NoCursiveAttachment; this.flags = 0; } diff --git a/src/SixLabors.Fonts/ShapedGlyph.cs b/src/SixLabors.Fonts/ShapedGlyph.cs index 3e3689bbe..498fe2ede 100644 --- a/src/SixLabors.Fonts/ShapedGlyph.cs +++ b/src/SixLabors.Fonts/ShapedGlyph.cs @@ -2,17 +2,16 @@ // Licensed under the Six Labors Split License. using System.Numerics; -using SixLabors.Fonts.Unicode; namespace SixLabors.Fonts; /// -/// Represents a single shaped glyph: the result of substitution and positioning, before -/// line breaking, visual reordering, or scaling. +/// Represents a single shaped glyph: the result of substitution and positioning, +/// before line breaking or scaling. /// /// -/// Advances and offsets are expressed in font design units for the glyph's -/// ; multiply by the font size over +/// Advances and offsets are expressed in font design units for the font the run was +/// shaped against; multiply by the font size over /// to convert to pixel units. /// public readonly struct ShapedGlyph @@ -20,84 +19,59 @@ public readonly struct ShapedGlyph /// /// Initializes a new instance of the struct. /// - /// The font face that resolved the glyph. - /// The glyph identifier within the font face. - /// The leading codepoint the glyph represents. - /// The codepoint index into the source text. - /// The number of codepoints the glyph represents. + /// The glyph identifier within the font. + /// The index of the character the glyph came from. /// The horizontal advance in font design units. /// The vertical advance in font design units. /// The placement offset in font design units. internal ShapedGlyph( - Font font, ushort glyphId, - CodePoint codePoint, int codePointIndex, - int codePointCount, ushort advanceWidth, ushort advanceHeight, Vector2 offset) { - this.Font = font; this.GlyphId = glyphId; - this.CodePoint = codePoint; this.CodePointIndex = codePointIndex; - this.CodePointCount = codePointCount; this.AdvanceWidth = advanceWidth; this.AdvanceHeight = advanceHeight; this.Offset = offset; } /// - /// Gets the font face that resolved the glyph: the primary font, a text run override, - /// or a fallback font. - /// - public Font Font { get; } - - /// - /// Gets the glyph identifier within . Glyph id 0 is the font's - /// missing glyph, produced when the font cannot map the codepoint. + /// Gets the glyph identifier within the font the run was shaped against. Glyph + /// id 0 is the font's missing glyph, produced when the font cannot map the + /// character. /// public ushort GlyphId { get; } /// - /// Gets the leading codepoint the glyph represents: the first codepoint for - /// ligatures. - /// - public CodePoint CodePoint { get; } - - /// - /// Gets the codepoint index into the source text of the first codepoint this glyph - /// represents, following the indexing convention. + /// Gets the index into the run's text of the first character this glyph came + /// from. /// /// - /// Ligatures emit one glyph indexed at the first codepoint with - /// covering the rest; marks and decompositions emit - /// multiple glyphs sharing one index. + /// Glyphs that came from the same characters carry the same value: several + /// characters that merged into one glyph, and several glyphs that came from one + /// character, are both read from the values repeating. /// public int CodePointIndex { get; } /// - /// Gets the number of codepoints the glyph represents. - /// - public int CodePointCount { get; } - - /// - /// Gets the horizontal advance in font design units, after positioning features have - /// been applied. + /// Gets the horizontal advance in font design units, after positioning features + /// have been applied. /// public ushort AdvanceWidth { get; } /// - /// Gets the vertical advance in font design units, after positioning features have - /// been applied. + /// Gets the vertical advance in font design units, after positioning features + /// have been applied. /// public ushort AdvanceHeight { get; } /// - /// Gets the placement offset in font design units, in Y-up font space. The offset - /// positions the glyph outline relative to its pen position and does not contribute - /// to the advance. + /// Gets the placement offset in font design units, in Y-up font space. The + /// offset positions the glyph outline relative to its pen position and does not + /// contribute to the advance. /// public Vector2 Offset { get; } } diff --git a/src/SixLabors.Fonts/ShapingBuffer.cs b/src/SixLabors.Fonts/ShapingBuffer.cs index 0afdaf881..75df90f89 100644 --- a/src/SixLabors.Fonts/ShapingBuffer.cs +++ b/src/SixLabors.Fonts/ShapingBuffer.cs @@ -109,6 +109,19 @@ internal sealed class ShapingBuffer /// private const uint FractionSlashCodePoint = 0x2044; + /// + /// The lowest character that can begin a constrained vowel sequence, which + /// is the first Devanagari vowel letter. Text made only of characters below + /// it cannot contain such a sequence. + /// + private const uint FirstVowelConstraintCharacter = 0x0905; + + /// + /// The dotted circle, U+25CC, which stands in for a base a mark has no + /// valid one to attach to. + /// + private const int DottedCircleCodePoint = 0x25CC; + /// /// The glyph id cache entry bits forming the lookup key: the marker, the /// codepoint, and the encoded following codepoint. @@ -174,7 +187,17 @@ internal sealed class ShapingBuffer /// Cleared when a reset adopts a different options instance, whose values the /// plans captured when built. /// - private readonly List<(ScriptClass Script, Tag ScriptTag, FontMetrics FontMetrics, ShapePlan Plan)> planCache = new(4); + private readonly List<(ScriptClass Script, Tag ScriptTag, FontMetrics FontMetrics, string Language, ShapePlan Plan)> planCache = new(4); + + /// + /// The language the cached language tags were resolved for. + /// + private string languageKey = string.Empty; + + /// + /// The feature tags the cached plans were built for. + /// + private IReadOnlyList? featureKey; /// /// The output-side record storage for substitution passes. Allocated on the @@ -281,6 +304,13 @@ public ShapingBuffer(TextOptions textOptions, ShapingBufferRole role) /// public bool HasFractionSlash { get; set; } + /// + /// Gets or sets a value indicating whether any record carries a character + /// that can begin a constrained vowel sequence. Recorded as records enter + /// the buffer so text that cannot contain one is never walked for it. + /// + public bool HasVowelConstraintCandidates { get; set; } + /// /// Gets the union of every feature bit enabled on any record, accumulated as /// features are turned on. A lookup whose mask shares no bit with it cannot @@ -511,14 +541,20 @@ public void Reset(TextOptions textOptions) this.placeholderBidiRuns.Clear(); this.SegmentPlans.Clear(); - // Cached plans and language tags captured option values when built, so only - // a different options instance invalidates them; this is the same identity - // contract the shaper and plan caches follow. - if (!ReferenceEquals(this.TextOptions, textOptions)) + // Cached plans and language tags captured option values when built, so what + // invalidates them is those values changing. A caller shaping run after run + // through one buffer hands the same options over each time with the members + // rewritten, so identity alone would never notice. + string language = textOptions.Culture?.Name ?? string.Empty; + if (!ReferenceEquals(this.TextOptions, textOptions) + || !string.Equals(this.languageKey, language, StringComparison.Ordinal) + || !ReferenceEquals(this.featureKey, textOptions.FeatureTags)) { this.planCache.Clear(); this.TextOptions = textOptions; this.LanguageTags = ResolveLanguageTags(textOptions); + this.languageKey = language; + this.featureKey = textOptions.FeatureTags; } } @@ -533,6 +569,7 @@ public void Clear() this.EnabledFeatureMaskUnion = ShapePlanFeatures.GlobalFeatureMask; this.HasDefaultIgnorables = false; this.HasFractionSlash = false; + this.HasVowelConstraintCandidates = false; this.placeholderBidiRuns.Clear(); this.SegmentPlans.Clear(); } @@ -709,13 +746,15 @@ public void AddGlyph(GlyphShapingData data, int offset) /// The resolved text direction for the codepoint. /// The index of the text run this glyph belongs to. /// The zero-based index within the input codepoint buffer. - public void AddGlyph(ushort glyphId, CodePoint codePoint, TextDirection direction, ushort textRunIndex, int offset) + /// The zero-based index of the grapheme the glyph belongs to. + public void AddGlyph(ushort glyphId, CodePoint codePoint, TextDirection direction, ushort textRunIndex, int offset, int graphemeIndex) { this.glyphDigest.Add(glyphId); ref GlyphShapingData slot = ref this.Append(); slot = new(textRunIndex) { CodePointIndex = offset, + GraphemeIndex = graphemeIndex, CodePoint = codePoint, Direction = direction, GlyphId = glyphId, @@ -736,6 +775,13 @@ public void AddGlyph(ushort glyphId, CodePoint codePoint, TextDirection directio this.HasFractionSlash = true; } + // Only a character that can begin a constrained vowel sequence makes the + // sequence worth looking for, and they all sit above this one. + if (value >= FirstVowelConstraintCharacter) + { + this.HasVowelConstraintCandidates = true; + } + if (value >= 0x80 && UnicodeUtility.IsDefaultIgnorableCodePoint(value) && !UnicodeUtility.ShouldRenderWhiteSpaceOnly(codePoint)) @@ -871,15 +917,22 @@ public void MoveGlyph(int fromIndex, int toIndex) /// The zero-based index at which to stop reversing (exclusive). public void ReverseRange(int startIndex, int endIndex) { - int s = Math.Min(startIndex, this.Count); - int e = Math.Min(endIndex, this.Count); + int s = Math.Max(0, Math.Min(startIndex, this.Count)); + int e = Math.Max(0, Math.Min(endIndex, this.Count)); if (e < s + 2) { return; } - Array.Reverse(this.data, s, e - s); + // A record is its shaping state, its metrics, and its position, held in + // step across three arrays; all three move together or the record comes + // apart. + int length = e - s; + + Array.Reverse(this.data, s, length); + Array.Reverse(this.metrics, s, length); + Array.Reverse(this.positions, s, length); } /// @@ -1136,6 +1189,39 @@ public void Replace(int index, int count, ushort glyphId, Tag feature) current.AppliedFeatureMask |= ShapePlanFeatures.GetVerticalMask(feature); } + /// + /// Joins the record at into the one at + /// , which takes the given glyph, and removes it. The + /// two records need not sit next to each other: records between them keep both + /// their place and their order. + /// + /// + /// The joined record covers the text of both, so the codepoint-to-glyph + /// projection stays total. The caller sets the codepoint, because the character + /// the pair stands for is the caller's to name. + /// + /// The zero-based index of the record that remains. + /// The zero-based index of the record that is folded in. + /// The glyph the remaining record takes. + /// The feature to apply to the remaining record. + public void MergeGlyph(int index, int mergeIndex, ushort glyphId, Tag feature) + { + this.glyphDigest.Add(glyphId); + + ref GlyphShapingData merged = ref this.data[mergeIndex]; + int codePointCount = merged.CodePointCount; + + this.RemoveAt(mergeIndex); + + ref GlyphShapingData current = ref this.data[index]; + current.CodePointCount += codePointCount; + current.GlyphId = glyphId; + current.LigatureId = 0; + current.LigatureComponent = -1; + current.IsSubstituted = true; + current.AppliedFeatureMask |= ShapePlanFeatures.GetVerticalMask(feature); + } + /// /// Replaces a single glyph id with a buffer of glyph ids. /// @@ -1257,6 +1343,7 @@ public bool TryAdd(Font font, ShapingBuffer workspace) // knowledge of default ignorables must travel with its records. this.HasDefaultIgnorables |= workspace.HasDefaultIgnorables; this.HasFractionSlash |= workspace.HasFractionSlash; + this.HasVowelConstraintCandidates |= workspace.HasVowelConstraintCandidates; uint verticalMask = ShapePlanFeatures.VerticalFeatureMask; @@ -1340,6 +1427,7 @@ public bool TryUpdate(Font font, ShapingBuffer workspace) // knowledge of default ignorables must travel with its records. this.HasDefaultIgnorables |= workspace.HasDefaultIgnorables; this.HasFractionSlash |= workspace.HasFractionSlash; + this.HasVowelConstraintCandidates |= workspace.HasVowelConstraintCandidates; uint verticalMask = ShapePlanFeatures.VerticalFeatureMask; @@ -1505,11 +1593,18 @@ public void SetShapingClass(ushort glyphId, GlyphShapingClass shapingClass) /// The . public ShapePlan GetOrCreatePlan(ScriptClass script, Tag unicodeScriptTag, FontMetrics fontMetrics) { - List<(ScriptClass Script, Tag ScriptTag, FontMetrics FontMetrics, ShapePlan Plan)> cache = this.planCache; + // The plan carries the language system the font's features were selected + // through, so a plan built for one language cannot stand in for another. + string language = this.TextOptions.Culture?.Name ?? string.Empty; + + List<(ScriptClass Script, Tag ScriptTag, FontMetrics FontMetrics, string Language, ShapePlan Plan)> cache = this.planCache; for (int i = 0; i < cache.Count; i++) { - (ScriptClass cachedScript, Tag cachedTag, FontMetrics cachedMetrics, ShapePlan cachedPlan) = cache[i]; - if (cachedScript == script && cachedTag == unicodeScriptTag && ReferenceEquals(cachedMetrics, fontMetrics)) + (ScriptClass cachedScript, Tag cachedTag, FontMetrics cachedMetrics, string cachedLanguage, ShapePlan cachedPlan) = cache[i]; + if (cachedScript == script + && cachedTag == unicodeScriptTag + && ReferenceEquals(cachedMetrics, fontMetrics) + && string.Equals(cachedLanguage, language, StringComparison.Ordinal)) { return cachedPlan; } @@ -1518,7 +1613,7 @@ public ShapePlan GetOrCreatePlan(ScriptClass script, Tag unicodeScriptTag, FontM ShapePlan plan = ShapePlan.Build(fontMetrics, script, unicodeScriptTag, this.TextOptions, this.LanguageTags); if (plan.IsCacheable) { - cache.Add((script, unicodeScriptTag, fontMetrics, plan)); + cache.Add((script, unicodeScriptTag, fontMetrics, language, plan)); } return plan; @@ -1967,6 +2062,31 @@ private void EnsureCapacity(int required) } } + /// + /// Places a dotted circle before the record at the given position. The + /// circle copies the following record's run and direction so it travels + /// with it, and stands for itself rather than continuing what it precedes. + /// + /// The position to insert before. + /// The dotted circle's glyph id. + public void InsertDottedCircle(int index, ushort glyphId) + { + this.EnsureCapacity(this.Count + 1); + Array.Copy(this.data, index, this.data, index + 1, this.Count - index); + this.Count++; + + GlyphShapingData following = this.data[index + 1]; + this.data[index] = new GlyphShapingData(following, true) + { + GlyphId = glyphId, + CodePoint = new CodePoint(DottedCircleCodePoint), + CodePointCount = 1, + LigatureComponent = -1, + }; + + this.glyphDigest.Add(glyphId); + } + /// /// Enters a nested lookup application within a contextual match. /// diff --git a/src/SixLabors.Fonts/ShapingScratch.cs b/src/SixLabors.Fonts/ShapingScratch.cs index 9640a7743..6e56a4b75 100644 --- a/src/SixLabors.Fonts/ShapingScratch.cs +++ b/src/SixLabors.Fonts/ShapingScratch.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Globalization; +using SixLabors.Fonts.Tables.AdvancedTypographic; using SixLabors.Fonts.Unicode; namespace SixLabors.Fonts; @@ -24,6 +26,12 @@ internal sealed class ShapingScratch /// private ShapingBuffer? workspace; + /// + /// The options the shaping pipeline is driven through, held for the lifetime of + /// the pooled scratch so shaping a run never builds them. + /// + private TextOptions? shapingOptions; + /// /// The accumulated result buffer glyphs are seeded and positioned in. /// @@ -92,6 +100,13 @@ internal sealed class ShapingScratch /// public int BidiRunCount { get; private set; } + /// + /// Gets or sets a value indicating whether the run of the current pass reads + /// right to left. Settled while the run's direction is resolved, so it holds even + /// when the caller left the direction to the text to say. + /// + public bool RunReadsRightToLeft { get; set; } + /// /// Gets the run table of the current pass's projection. Only the first /// entries are live. @@ -114,6 +129,29 @@ internal sealed class ShapingScratch /// public void ClearBidiRuns() => this.BidiRunCount = 0; + /// + /// Returns the options carrying the properties of one run, refreshed in place. + /// The run is taken as reading the one way throughout, so the direction stands + /// whichever way the text would read on its own. + /// + /// The font being shaped against. + /// The direction the run reads in. + /// The language the run is written in. + /// The feature tags to turn on for the run. + /// The options. + public TextOptions GetShapingOptions(Font font, TextDirection direction, CultureInfo language, Tag[] features) + { + TextOptions current = this.shapingOptions ??= new TextOptions(font); + + current.Font = font; + current.TextDirection = direction; + current.TextBidiMode = TextBidiMode.Override; + current.Culture = language; + current.FeatureTags = features; + + return current; + } + /// /// Appends a resolved bidi run. /// @@ -205,6 +243,11 @@ public IReadOnlyList GetDefaultTextRuns(int end, TextOptions options) /// The reusable buffers, sharing one feature map. public (ShapingBuffer Workspace, ShapingBuffer Result) Prepare(TextOptions options) { + // A pooled scratch carries nothing over from its last use. The direction is + // settled again for every pass, and a pass that never settles it must not read + // the answer the previous one left. + this.RunReadsRightToLeft = false; + ShapingBuffer? workspace = this.workspace; ShapingBuffer? result = this.result; if (workspace is null || result is null) diff --git a/src/SixLabors.Fonts/StreamFontMetrics.cs b/src/SixLabors.Fonts/StreamFontMetrics.cs index 0c10bd09e..29c13303f 100644 --- a/src/SixLabors.Fonts/StreamFontMetrics.cs +++ b/src/SixLabors.Fonts/StreamFontMetrics.cs @@ -469,7 +469,14 @@ internal override void ApplySubstitution(ShapingBuffer buffer) if (this.TryGetGSubTable(out GSubTable? gSubTable)) { gSubTable.ApplySubstitution(this, buffer); + return; } + + // A font carrying no substitution table has no lookups to apply, but its + // runs are still planned: preparing a run's text is what supplies the + // characters a script cannot be read without, and the positioning pass + // reuses the runs planning records. + ScriptItemizer.PlanRuns(this, buffer); } /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType3SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType3SubTable.cs index 7339c939a..cbbec3c47 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType3SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType3SubTable.cs @@ -105,6 +105,9 @@ public static LookupType3Format1SubTable Load(BigEndianBinaryReader reader, long public override void CollectDigest(ref GlyphSetDigest digest) => this.coverageTable.CollectDigest(ref digest); /// + /// + /// HarfBuzz 14.2.1, tests/harfbuzz/src/OT/Layout/GPOS/CursivePosFormat1.hh, OT::Layout::GPOS_impl::CursivePosFormat1_2::apply, reads the entry anchor from the current glyph and uses skipping_iterator_t::prev to find the preceding lookup-visible glyph whose exit anchor connects to it. The iterator's filtering behavior is defined by matcher_t::may_skip and skipping_iterator_t::match in tests/harfbuzz/src/hb-ot-layout-gsubgpos.hh. This OpenType lookup traversal rule is not derivable from the Unicode Character Database. + /// public override bool TryUpdatePosition( FontMetrics fontMetrics, GPosTable table, @@ -113,60 +116,57 @@ public override bool TryUpdatePosition( int index, int count) { - if (count <= 1) - { - return false; - } - - // Implements Cursive Attachment Positioning Subtable: - // https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#lookup-type-3-cursive-attachment-positioning-subtable ushort glyphId = buffer[index].GlyphId; if (glyphId == 0) { return false; } - int nextIndex = index + 1; - ushort nextGlyphId = buffer[nextIndex].GlyphId; - if (nextGlyphId == 0) + int coverage = this.coverageTable.CoverageIndexOf(glyphId); + if (coverage < 0 || coverage >= this.entryExitAnchors.Length) { return false; } - int coverageNext = this.coverageTable.CoverageIndexOf(nextGlyphId); - if (coverageNext < 0 || coverageNext >= this.entryExitAnchors.Length) + EntryExitAnchors currentRecord = this.entryExitAnchors[coverage]; + AnchorTable? entry = currentRecord.EntryAnchor; + if (entry is null) { return false; } - EntryExitAnchors nextRecord = this.entryExitAnchors[coverageNext]; - AnchorTable? entry = nextRecord.EntryAnchor; - if (entry is null) + // Cursive attachment joins the current glyph to the closest preceding + // glyph that the lookup can see. In particular, marks excluded by the + // lookup's filtering set are transparent to this search. + SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, this.LookupFlags, this.MarkFilteringSet); + int previousIndex = iterator.Prev(); + if (previousIndex < 0) { return false; } - int coverage = this.coverageTable.CoverageIndexOf(glyphId); - if (coverage < 0 || coverage >= this.entryExitAnchors.Length) + ushort previousGlyphId = buffer[previousIndex].GlyphId; + int previousCoverage = this.coverageTable.CoverageIndexOf(previousGlyphId); + if (previousCoverage < 0 || previousCoverage >= this.entryExitAnchors.Length) { return false; } - EntryExitAnchors curRecord = this.entryExitAnchors[coverage]; - AnchorTable? exit = curRecord.ExitAnchor; + EntryExitAnchors previousRecord = this.entryExitAnchors[previousCoverage]; + AnchorTable? exit = previousRecord.ExitAnchor; if (exit is null) { return false; } + ref GlyphShapingData previous = ref buffer[previousIndex]; ref GlyphShapingData current = ref buffer[index]; - ref GlyphShapingData next = ref buffer[nextIndex]; - AnchorXY exitXY = exit.GetAnchor(fontMetrics, ref current, buffer); - AnchorXY entryXY = entry.GetAnchor(fontMetrics, ref next, buffer); + AnchorXY exitXY = exit.GetAnchor(fontMetrics, ref previous, buffer); + AnchorXY entryXY = entry.GetAnchor(fontMetrics, ref current, buffer); + ref GlyphShapingPosition previousPosition = ref buffer.PositionAt(previousIndex); ref GlyphShapingPosition currentPosition = ref buffer.PositionAt(index); - ref GlyphShapingPosition nextPosition = ref buffer.PositionAt(nextIndex); bool isVerticalLayout = AdvancedTypographicUtils.IsVerticalGlyph(current.CodePoint, buffer.TextOptions.LayoutMode); if (!isVerticalLayout) @@ -174,33 +174,33 @@ public override bool TryUpdatePosition( // Horizontal if (current.Direction == TextDirection.LeftToRight) { - currentPosition.Bounds.Width = exitXY.XCoordinate + currentPosition.Bounds.X; + previousPosition.Bounds.Width = exitXY.XCoordinate + previousPosition.Bounds.X; - int delta = entryXY.XCoordinate + nextPosition.Bounds.X; - nextPosition.Bounds.Width -= delta; - nextPosition.Bounds.X -= delta; + int delta = entryXY.XCoordinate + currentPosition.Bounds.X; + currentPosition.Bounds.Width -= delta; + currentPosition.Bounds.X -= delta; } else { - int delta = exitXY.XCoordinate + currentPosition.Bounds.X; - currentPosition.Bounds.Width -= delta; - currentPosition.Bounds.X -= delta; + int delta = exitXY.XCoordinate + previousPosition.Bounds.X; + previousPosition.Bounds.Width -= delta; + previousPosition.Bounds.X -= delta; - nextPosition.Bounds.Width = entryXY.XCoordinate + nextPosition.Bounds.X; + currentPosition.Bounds.Width = entryXY.XCoordinate + currentPosition.Bounds.X; } } else { // Vertical layout modes advance top-to-bottom; column progression is handled by layout. - currentPosition.Bounds.Height = exitXY.YCoordinate + currentPosition.Bounds.Y; + previousPosition.Bounds.Height = exitXY.YCoordinate + previousPosition.Bounds.Y; - int delta = entryXY.YCoordinate + nextPosition.Bounds.Y; - nextPosition.Bounds.Height -= delta; - nextPosition.Bounds.Y -= delta; + int delta = entryXY.YCoordinate + currentPosition.Bounds.Y; + currentPosition.Bounds.Height -= delta; + currentPosition.Bounds.Y -= delta; } - int child = index; - int parent = nextIndex; + int child = previousIndex; + int parent = index; int xOffset = entryXY.XCoordinate - exitXY.XCoordinate; int yOffset = entryXY.YCoordinate - exitXY.YCoordinate; if ((this.LookupFlags & LookupFlags.RightToLeft) != LookupFlags.RightToLeft) @@ -216,7 +216,7 @@ public override bool TryUpdatePosition( // previous connection now attaches to new parent.Watch out for case // where new parent is on the path from old chain... bool horizontal = !isVerticalLayout; - ReverseCursiveMinorOffset(buffer, index, child, horizontal, parent); + ReverseCursiveMinorOffset(buffer, child, horizontal, parent); ref GlyphShapingPosition c = ref buffer.PositionAt(child); c.CursiveAttachment = parent - child; @@ -234,7 +234,7 @@ public override bool TryUpdatePosition( ref GlyphShapingPosition p = ref buffer.PositionAt(parent); if (p.CursiveAttachment == -c.CursiveAttachment) { - p.CursiveAttachment = 0; + p.CursiveAttachment = GlyphShapingPosition.NoCursiveAttachment; // Bounds.X/Y carry shaping placement offsets here. Clear only the // detached parent's minor axis. @@ -256,25 +256,19 @@ public override bool TryUpdatePosition( /// of a previous connection attaches to the new parent. /// /// The glyph positioning buffer. - /// The original glyph position that initiated the chain reversal. /// The current index in the chain being reversed. /// Whether the layout is horizontal. /// The new parent index to stop at. - private static void ReverseCursiveMinorOffset( - ShapingBuffer buffer, - int position, - int i, - bool horizontal, - int parent) + private static void ReverseCursiveMinorOffset(ShapingBuffer buffer, int i, bool horizontal, int parent) { ref GlyphShapingPosition c = ref buffer.PositionAt(i); int chain = c.CursiveAttachment; - if (chain <= 0) + if (chain == GlyphShapingPosition.NoCursiveAttachment) { return; } - c.CursiveAttachment = 0; + c.CursiveAttachment = GlyphShapingPosition.NoCursiveAttachment; int j = i + chain; @@ -284,7 +278,7 @@ private static void ReverseCursiveMinorOffset( return; } - ReverseCursiveMinorOffset(buffer, position, j, horizontal, parent); + ReverseCursiveMinorOffset(buffer, j, horizontal, parent); ref GlyphShapingPosition p = ref buffer.PositionAt(j); if (horizontal) diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType4SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType4SubTable.cs index b0e851686..f3792b843 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType4SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType4SubTable.cs @@ -113,6 +113,9 @@ public static LookupType4Format1SubTable Load(BigEndianBinaryReader reader, long public override void CollectDigest(ref GlyphSetDigest digest) => this.markCoverage.CollectDigest(ref digest); /// + /// + /// The backward search and multiple-substitution acceptance rule are transcribed from HarfBuzz 14.2.1, src/OT/Layout/GPOS/MarkBasePosFormat1.hh, symbol MarkBasePosFormat1_2::apply. Default-ignorable transparency is defined by matcher_t::may_skip in src/hb-ot-layout-gsubgpos.hh. These rules are shaping behavior and are not derivable from the Unicode Character Database. + /// public override bool TryUpdatePosition( FontMetrics fontMetrics, GPosTable table, @@ -135,15 +138,29 @@ public override bool TryUpdatePosition( return false; } - // Search backward for a base glyph. - int baseGlyphIndex = index; - while (--baseGlyphIndex >= 0) + SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, LookupFlags.IgnoreMarks, 0); + iterator.SetMatchContext(buffer.LookupMask, false); + + int baseGlyphIndex = iterator.Prev(); + while (baseGlyphIndex >= 0) { - ref GlyphShapingData data = ref buffer[baseGlyphIndex]; - if (!AdvancedTypographicUtils.IsMarkGlyph(fontMetrics, data.GlyphId, ref data) && data.LigatureComponent <= 0) + ref GlyphShapingData candidate = ref buffer[baseGlyphIndex]; + bool acceptsCandidate = !candidate.IsDecomposed + || candidate.LigatureComponent == 0 + || baseGlyphIndex == 0 + || AdvancedTypographicUtils.IsMarkGlyph(fontMetrics, buffer[baseGlyphIndex - 1].GlyphId, ref buffer[baseGlyphIndex - 1]) + || !buffer[baseGlyphIndex - 1].IsDecomposed + || candidate.LigatureId != buffer[baseGlyphIndex - 1].LigatureId + || candidate.LigatureComponent != buffer[baseGlyphIndex - 1].LigatureComponent + 1; + + // A later component may still be accepted when the font explicitly + // covers it; otherwise continue to the first component. + if (acceptsCandidate || this.baseCoverage.CoverageIndexOf(candidate.GlyphId) >= 0) { break; } + + baseGlyphIndex = iterator.Prev(); } if (baseGlyphIndex < 0) diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType5SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType5SubTable.cs index 3bbb1191a..bd21df919 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType5SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType5SubTable.cs @@ -116,6 +116,9 @@ public static LookupType5Format1SubTable Load(BigEndianBinaryReader reader, long public override void CollectDigest(ref GlyphSetDigest digest) => this.markCoverage.CollectDigest(ref digest); /// + /// + /// The backward search and component selection are transcribed from HarfBuzz 14.2.1, src/OT/Layout/GPOS/MarkLigPosFormat1.hh, symbol MarkLigPosFormat1_2::apply. Default-ignorable transparency is defined by matcher_t::may_skip in src/hb-ot-layout-gsubgpos.hh. These rules are shaping behavior and are not derivable from the Unicode Character Database. + /// public override bool TryUpdatePosition( FontMetrics fontMetrics, GPosTable table, @@ -138,16 +141,10 @@ public override bool TryUpdatePosition( return false; } - // Search backward for a base glyph. - int baseGlyphIndex = index; - while (--baseGlyphIndex >= 0) - { - ref GlyphShapingData data = ref buffer[baseGlyphIndex]; - if (!AdvancedTypographicUtils.IsMarkGlyph(fontMetrics, data.GlyphId, ref data)) - { - break; - } - } + SkippingGlyphIterator iterator = new(fontMetrics, buffer, index, LookupFlags.IgnoreMarks, 0); + iterator.SetMatchContext(buffer.LookupMask, false); + + int baseGlyphIndex = iterator.Prev(); if (baseGlyphIndex < 0) { @@ -166,11 +163,17 @@ public override bool TryUpdatePosition( // If yes, we can directly use the component index. If not, we attach the mark // glyph to the last component of the ligature. LigatureAttachTable ligatureAttach = this.ligatureArrayTable.LigatureAttachTables[ligatureIndex]; + int componentCount = ligatureAttach.ComponentRecords.Length; + if (componentCount == 0) + { + return false; + } + ref GlyphShapingData markGlyph = ref buffer[index]; ref GlyphShapingData ligGlyph = ref buffer[baseGlyphIndex]; int compIndex = ligGlyph.LigatureId > 0 && ligGlyph.LigatureId == markGlyph.LigatureId && markGlyph.LigatureComponent > 0 - ? Math.Min(markGlyph.LigatureComponent, ligGlyph.CodePointCount) - 1 - : ligGlyph.CodePointCount - 1; + ? Math.Min(markGlyph.LigatureComponent, componentCount) - 1 + : componentCount - 1; MarkRecord markRecord = this.markArrayTable.MarkRecords[markIndex]; AnchorTable baseAnchor = ligatureAttach.ComponentRecords[compIndex].LigatureAnchorTables[markRecord.MarkClass]; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType6SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType6SubTable.cs index a8c873caf..e04f6e0ff 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType6SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType6SubTable.cs @@ -115,6 +115,9 @@ public static LookupType6Format1SubTable Load(BigEndianBinaryReader reader, long public override void CollectDigest(ref GlyphSetDigest digest) => this.mark1Coverage.CollectDigest(ref digest); /// + /// + /// The backward search is transcribed from HarfBuzz 14.2.1, src/OT/Layout/GPOS/MarkMarkPosFormat1.hh, symbol MarkMarkPosFormat1_2::apply. Default-ignorable transparency is defined by matcher_t::may_skip in src/hb-ot-layout-gsubgpos.hh. These rules are shaping behavior and are not derivable from the Unicode Character Database. + /// public override bool TryUpdatePosition( FontMetrics fontMetrics, GPosTable table, @@ -137,12 +140,12 @@ public override bool TryUpdatePosition( return false; } - // Get the previous mark to attach to. - // HarfBuzz: search backwards for a suitable mark glyph until a non-mark glyph. - // It clears ignore flags when searching, but keeps mark attachment / filtering behavior. + // Clear the class-ignore flags while retaining mark attachment and + // filtering behavior for the preceding-mark search. LookupFlags searchFlags = this.LookupFlags & ~(LookupFlags.IgnoreMarks | LookupFlags.IgnoreBaseGlyphs | LookupFlags.IgnoreLigatures); SkippingGlyphIterator it = new(fontMetrics, buffer, index, searchFlags, this.MarkFilteringSet); + it.SetMatchContext(buffer.LookupMask, false); int j = it.Prev(); if (j < 0) diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs index c1a505f15..f63717bc3 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs @@ -211,7 +211,7 @@ public bool TryUpdatePositions(FontMetrics fontMetrics, ShapingBuffer buffer, ou continue; } - ScriptClass current = this.GetScriptClass(CodePoint.GetScriptClass(buffer[i].CodePoint)); + ScriptClass current = CodePoint.GetScriptClass(buffer[i].CodePoint); int index = i; int count = 1; @@ -227,7 +227,7 @@ public bool TryUpdatePositions(FontMetrics fontMetrics, ShapingBuffer buffer, ou break; } - ScriptClass next = this.GetScriptClass(CodePoint.GetScriptClass(nextData.CodePoint)); + ScriptClass next = CodePoint.GetScriptClass(nextData.CodePoint); if (next != current && current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClass.Inherited && next is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClass.Inherited) @@ -254,7 +254,7 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas // Plan positioning features for each glyph. Records seeded across buffers // had their feature registrations cleared, so this pass re-plans. - shapePlan.Shaper.Plan(buffer, index, count); + shapePlan.Shaper.Plan(fontMetrics, buffer, index, count); updated |= this.PositionSegment( fontMetrics, @@ -606,68 +606,73 @@ private static FeatureTable ResolveFeatureTable( } /// - /// Maps a script class to an effective script class, checking whether the font supports it. - /// Falls back to if the script is not present in the font. + /// Fixes cursive attachment positioning by propagating Y (or X for vertical) offsets. /// - /// The script class to check. - /// The effective script class. - private ScriptClass GetScriptClass(ScriptClass current) + /// + /// HarfBuzz 14.2.1, tests/harfbuzz/src/OT/Layout/GPOS/GPOS.hh, GPOS::position_finish_offsets and propagate_attachment_offsets, resolves the parent chain before adding the parent's minor-axis offset to a cursively attached glyph. The direction controls the outer traversal order, while recursion makes every parent complete before its child. This OpenType attachment propagation rule is not derivable from the Unicode Character Database. + /// + /// The glyph positioning buffer. + /// The starting index. + /// The number of glyphs to process. + private static void FixCursiveAttachment(ShapingBuffer buffer, int index, int count) { - if (current is ScriptClass.Common or ScriptClass.Unknown or ScriptClass.Inherited) + int end = index + count; + int currentIndex = index; + int increment = 1; + if (buffer[index].Direction == TextDirection.RightToLeft) { - return current; + currentIndex = end - 1; + end = index - 1; + increment = -1; } - if (this.ScriptList is null) - { - return ScriptClass.Default; - } - - Tag[] tags = UnicodeScriptTagMap.Instance[current]; - - for (int i = 0; i < tags.Length; i++) + while (currentIndex != end) { - if (this.ScriptList.TryGetValue(tags[i].Value, out ScriptListTable? _)) + ref GlyphShapingPosition position = ref buffer.PositionAt(currentIndex); + if (position.CursiveAttachment != GlyphShapingPosition.NoCursiveAttachment) { - return current; + PropagateCursiveAttachment(buffer, index, index + count, currentIndex, AdvancedTypographicUtils.MaxNestingLevel); } - } - // Script for `current` not present in the font: use default shaper. - return ScriptClass.Default; + currentIndex += increment; + } } /// - /// Fixes cursive attachment positioning by propagating Y (or X for vertical) offsets. + /// Resolves one cursive attachment chain and accumulates its parent's minor-axis offset into the child. /// /// The glyph positioning buffer. - /// The starting index. - /// The number of glyphs to process. - private static void FixCursiveAttachment(ShapingBuffer buffer, int index, int count) + /// The first index in the positioned segment. + /// The index immediately after the positioned segment. + /// The child glyph whose attachment is being resolved. + /// The number of parent links that may still be followed. + private static void PropagateCursiveAttachment(ShapingBuffer buffer, int start, int end, int currentIndex, int nestingLevel) { - LayoutMode layoutMode = buffer.TextOptions.LayoutMode; - for (int i = 0; i < count; i++) + ref GlyphShapingPosition position = ref buffer.PositionAt(currentIndex); + int chain = position.CursiveAttachment; + position.CursiveAttachment = GlyphShapingPosition.NoCursiveAttachment; + + int parentIndex = currentIndex + chain; + if (parentIndex < start || parentIndex >= end || nestingLevel == 0) { - int currentIndex = i + index; - ref GlyphShapingPosition position = ref buffer.PositionAt(currentIndex); - if (position.CursiveAttachment != -1) - { - int j = position.CursiveAttachment + currentIndex; - if (j < index || j >= index + count) - { - return; - } + return; + } - ref GlyphShapingPosition cursivePosition = ref buffer.PositionAt(j); - if (!AdvancedTypographicUtils.IsVerticalGlyph(buffer[currentIndex].CodePoint, layoutMode)) - { - position.Bounds.Y += cursivePosition.Bounds.Y; - } - else - { - position.Bounds.X += cursivePosition.Bounds.X; - } - } + ref GlyphShapingPosition parent = ref buffer.PositionAt(parentIndex); + if (parent.CursiveAttachment != GlyphShapingPosition.NoCursiveAttachment) + { + PropagateCursiveAttachment(buffer, start, end, parentIndex, nestingLevel - 1); + } + + // Cursive attachment only accumulates the cross-run axis. Main-axis + // advances were resolved by the lookup itself. + if (!AdvancedTypographicUtils.IsVerticalGlyph(buffer[currentIndex].CodePoint, buffer.TextOptions.LayoutMode)) + { + position.Bounds.Y += parent.Bounds.Y; + } + else + { + position.Bounds.X += parent.Bounds.X; } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs index e19a6c001..db1b16f56 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs @@ -162,36 +162,8 @@ public void ApplySubstitution(FontMetrics fontMetrics, ShapingBuffer buffer) { // Choose a shaper based on the script. // This determines which features to apply to which glyphs. - ScriptClass current = this.GetScriptClass(CodePoint.GetScriptClass(buffer[i].CodePoint)); - int index = i; - int count = 1; - while (i < buffer.Count - 1) - { - // We want to assign the same feature lookups to individual sections of the text rather - // than the text as a whole to ensure that different language shapers do not interfere - // with each other when the text contains multiple languages. - ScriptClass next = this.GetScriptClass(CodePoint.GetScriptClass(buffer[i + 1].CodePoint)); - if (next != current && - current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClass.Inherited && - next is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClass.Inherited) - { - break; - } - - if (current is ScriptClass.Common or ScriptClass.Unknown or ScriptClass.Inherited) - { - current = next; - } - - i++; - count++; - - if (i >= maxCount) - { - break; - } - } + ScriptClass current = ScriptItemizer.ReadRun(buffer, ref i, maxCount, out int count); Tag unicodeScriptTag = this.GetUnicodeScriptTag(current); ShapePlan shapePlan = buffer.GetOrCreatePlan(current, unicodeScriptTag, fontMetrics); @@ -202,7 +174,7 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas // Shapers can adjust the count during initialization and feature processing so we must capture // the current count to allow resetting indexes and processing counts. int collectionCount = buffer.Count; - shaper.Plan(buffer, index, count); + shaper.Plan(fontMetrics, buffer, index, count); int delta = buffer.Count - collectionCount; i += delta; count += delta; @@ -592,36 +564,4 @@ private static FeatureTable ResolveFeatureTable( return featureList.FeatureTables[featureIndex]; } - - /// - /// Maps a script class to an effective script class, checking whether the font supports it. - /// Falls back to if the script is not present in the font. - /// - /// The script class to check. - /// The effective script class. - private ScriptClass GetScriptClass(ScriptClass current) - { - if (current is ScriptClass.Common or ScriptClass.Unknown or ScriptClass.Inherited) - { - return current; - } - - if (this.ScriptList is null) - { - return ScriptClass.Default; - } - - Tag[] tags = UnicodeScriptTagMap.Instance[current]; - - for (int i = 0; i < tags.Length; i++) - { - if (this.ScriptList.TryGetValue(tags[i].Value, out ScriptListTable? _)) - { - return current; - } - } - - // Script for `current` not present in the font: use default shaper. - return ScriptClass.Default; - } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/ScriptItemizer.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ScriptItemizer.cs new file mode 100644 index 000000000..e965cf104 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/ScriptItemizer.cs @@ -0,0 +1,127 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Tables.AdvancedTypographic.Shapers; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic; + +/// +/// Splits a shaping buffer into runs of a single script and plans a shaper for +/// each run. +/// +/// Planning is what prepares a run's text and registers its features, so it +/// belongs to every font rather than to the fonts that carry a substitution +/// table. A font without one still needs the characters a script cannot be read +/// without, and positioning reuses the runs planning records. +/// +/// +internal static class ScriptItemizer +{ + /// + /// Reads the run of one script that begins at , + /// leaving on the run's last record. + /// + /// Feature lookups are assigned to runs rather than to the text as a whole so + /// that the shapers of two languages cannot interfere with one another. + /// Characters belonging to no script of their own join the run around them. + /// + /// + /// The glyph shaping buffer. + /// The zero-based index of the run's first record, left on its last. + /// The largest record count a run may reach. + /// When this method returns, contains the number of records in the run. + /// The script of the run. + public static ScriptClass ReadRun(ShapingBuffer buffer, ref int index, int maxCount, out int count) + { + ScriptClass current = CodePoint.GetScriptClass(buffer[index].CodePoint); + count = 1; + + while (index < buffer.Count - 1) + { + ScriptClass next = CodePoint.GetScriptClass(buffer[index + 1].CodePoint); + if (next != current && + current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClass.Inherited && + next is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClass.Inherited) + { + break; + } + + if (current is ScriptClass.Common or ScriptClass.Unknown or ScriptClass.Inherited) + { + current = next; + } + + index++; + count++; + + if (index >= maxCount) + { + break; + } + } + + return current; + } + + /// + /// Plans a shaper over every run of the buffer and records the runs for the + /// positioning pass. Used for a font that carries no substitution table, + /// where there are no lookups to apply but the text still has to be prepared. + /// + /// + /// Stage actions are applied without a substitution table to match HarfBuzz 14.2.1, src/hb-ot-layout.cc, symbol hb_ot_map_t::apply, called by hb_ot_shape_plan_t::substitute in src/hb-ot-shape.cc. The stage-action rule is shaping behavior and is not derivable from the Unicode Character Database. + /// + /// The font metrics. + /// The glyph shaping buffer. + public static void PlanRuns(FontMetrics fontMetrics, ShapingBuffer buffer) + { + // Set max constraints to prevent OutOfMemoryException or infinite loops from attacks. + int maxCount = AdvancedTypographicUtils.GetMaxAllowableShapingCollectionCount(buffer.Count); + + for (int i = 0; i < buffer.Count; i++) + { + int index = i; + ScriptClass script = ReadRun(buffer, ref i, maxCount, out int count); + + // With no substitution table the font offers no script of its own, so + // the run is planned against the default design. + ShapePlan shapePlan = buffer.GetOrCreatePlan(script, default, fontMetrics); + + // Preparing the text can insert records, so the run grows with it. + int collectionCount = buffer.Count; + + shapePlan.Shaper.Plan(fontMetrics, buffer, index, count); + + int delta = buffer.Count - collectionCount; + i += delta; + count += delta; + + // A substitution table with no lookups still walks every stage and + // invokes its actions. Preserve that path for fonts with no table, + // accounting for actions that insert or remove records. + List> groups = shapePlan.GetOrBuildGSubStageGroups(); + List stages = shapePlan.Stages; + for (int g = 0; g < groups.Count; g++) + { + ShapePlanStageGroup group = groups[g]; + + collectionCount = buffer.Count; + stages[group.Start].PreProcessFeature(shapePlan, buffer, index, count); + + delta = buffer.Count - collectionCount; + i += delta; + count += delta; + + collectionCount = buffer.Count; + stages[group.End - 1].PostProcessFeature(shapePlan, buffer, index, count); + + delta = buffer.Count - collectionCount; + i += delta; + count += delta; + } + + buffer.SegmentPlans.Add((index, count, script, shapePlan)); + } + } +} diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicJoining.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicJoining.cs new file mode 100644 index 000000000..ee0a1dba1 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicJoining.cs @@ -0,0 +1,275 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers; + +/// +/// Chooses the form each character of a cursive script takes from the characters +/// around it. Several scripts join this way, and the shapers that handle them all +/// settle the forms here. +/// +internal static class ArabicJoining +{ + /// + /// No form is chosen. + /// + private const byte None = 0; + + /// + /// Isolated form action. + /// + private const byte Isol = 1; + + /// + /// Final form action. + /// + private const byte Fina = 2; + + /// + /// Final form #2 action (for ALAPH). + /// + private const byte Fin2 = 3; + + /// + /// Final form #3 action (for ALAPH after DALATH RISH). + /// + private const byte Fin3 = 4; + + /// + /// Medial form action. + /// + private const byte Medi = 5; + + /// + /// Medial form #2 action (for ALAPH). + /// + private const byte Med2 = 6; + + /// + /// Initial form action. + /// + private const byte Init = 7; + + /// + /// The 'isol' (isolated forms) feature tag. + /// + public static readonly Tag IsolTag = Tag.Parse("isol"); + + /// + /// The 'fina' (terminal forms) feature tag. + /// + public static readonly Tag FinaTag = Tag.Parse("fina"); + + /// + /// The 'fin2' (terminal forms #2) feature tag. + /// + public static readonly Tag Fin2Tag = Tag.Parse("fin2"); + + /// + /// The 'fin3' (terminal forms #3) feature tag. + /// + public static readonly Tag Fin3Tag = Tag.Parse("fin3"); + + /// + /// The 'medi' (medial forms) feature tag. + /// + public static readonly Tag MediTag = Tag.Parse("medi"); + + /// + /// The 'med2' (medial forms #2) feature tag. + /// + public static readonly Tag Med2Tag = Tag.Parse("med2"); + + /// + /// The 'init' (initial forms) feature tag. + /// + public static readonly Tag InitTag = Tag.Parse("init"); + + /// + /// The joining state machine table. Each entry is [prevAction, curAction, nextState]. + /// Rows are states (0-6), columns are joining categories. + /// + private static readonly byte[,][] StateTable = + { + // # NonJoining, LeftJoining, RightJoining, DualJoining, ALAPH, DALATH RISH + // State 0: prev was U, not willing to join. + { new byte[] { None, None, 0 }, new byte[] { None, Isol, 2 }, new byte[] { None, Isol, 1 }, new byte[] { None, Isol, 2 }, new byte[] { None, Isol, 1 }, new byte[] { None, Isol, 6 } }, + + // State 1: prev was R or ISOL/ALAPH, not willing to join. + { new byte[] { None, None, 0 }, new byte[] { None, Isol, 2 }, new byte[] { None, Isol, 1 }, new byte[] { None, Isol, 2 }, new byte[] { None, Fin2, 5 }, new byte[] { None, Isol, 6 } }, + + // State 2: prev was D/L in ISOL form, willing to join. + { new byte[] { None, None, 0 }, new byte[] { None, Isol, 2 }, new byte[] { Init, Fina, 1 }, new byte[] { Init, Fina, 3 }, new byte[] { Init, Fina, 4 }, new byte[] { Init, Fina, 6 } }, + + // State 3: prev was D in FINA form, willing to join. + { new byte[] { None, None, 0 }, new byte[] { None, Isol, 2 }, new byte[] { Medi, Fina, 1 }, new byte[] { Medi, Fina, 3 }, new byte[] { Medi, Fina, 4 }, new byte[] { Medi, Fina, 6 } }, + + // State 4: prev was FINA ALAPH, not willing to join. + { new byte[] { None, None, 0 }, new byte[] { None, Isol, 2 }, new byte[] { Med2, Isol, 1 }, new byte[] { Med2, Isol, 2 }, new byte[] { Med2, Fin2, 5 }, new byte[] { Med2, Isol, 6 } }, + + // State 5: prev was FIN2/FIN3 ALAPH, not willing to join. + { new byte[] { None, None, 0 }, new byte[] { None, Isol, 2 }, new byte[] { Isol, Isol, 1 }, new byte[] { Isol, Isol, 2 }, new byte[] { Isol, Fin2, 5 }, new byte[] { Isol, Isol, 6 } }, + + // State 6: prev was DALATH/RISH, not willing to join. + { new byte[] { None, None, 0 }, new byte[] { None, Isol, 2 }, new byte[] { None, Isol, 1 }, new byte[] { None, Isol, 2 }, new byte[] { None, Fin3, 5 }, new byte[] { None, Isol, 6 } }, + }; + + /// + /// Determines whether the script's characters take their form from the ones + /// around them. Only these scripts carry the data the state machine reads. + /// + /// The script the text is written in. + /// when the script joins. + public static bool Joins(ScriptClass script) + => script switch + { + ScriptClass.Arabic + or ScriptClass.Mongolian + or ScriptClass.Syriac + or ScriptClass.Nko + or ScriptClass.PhagsPa + or ScriptClass.Mandaic + or ScriptClass.Manichaean + or ScriptClass.PsalterPahlavi + or ScriptClass.Adlam + or ScriptClass.HanifiRohingya + or ScriptClass.Chorasmian + or ScriptClass.Sogdian + or ScriptClass.OldUyghur => true, + _ => false, + }; + + /// + /// Walks the text and turns on, for each character, the feature naming the form + /// it takes among its neighbours. + /// + /// The glyph shaping buffer. + /// The zero-based index of the first record. + /// The number of records. + /// The script the text is written in. + /// The features holding the masks to turn on. + public static void Apply(ShapingBuffer buffer, int index, int count, ScriptClass script, ShapePlanFeatures features) + { + int prev = -1; + int state = 0; + byte[] actions = buffer.GetShaperScratch(count); + + for (int i = 0; i < count; i++) + { + ref GlyphShapingData data = ref buffer[i + index]; + ArabicJoiningClass joiningClass = CodePoint.GetArabicJoiningClass(data.CodePoint); + if (joiningClass.JoiningType == ArabicJoiningType.Transparent) + { + actions[i] = None; + continue; + } + + byte[] actionsWithState = StateTable[state, GetShapingClassIndex(joiningClass)]; + byte prevAction = actionsWithState[0]; + byte curAction = actionsWithState[1]; + state = actionsWithState[2]; + + if (prevAction != None && prev != -1) + { + actions[prev] = prevAction; + } + + actions[i] = curAction; + prev = i; + } + + if (script == ScriptClass.Mongolian) + { + CarryFormsToVariationSelectors(buffer, index, count, actions); + } + + for (int i = 0; i < count; i++) + { + switch (actions[i]) + { + case Fina: + buffer.EnableShapingFeature(i + index, features.GetMask(FinaTag)); + break; + case Fin2: + buffer.EnableShapingFeature(i + index, features.GetMask(Fin2Tag)); + break; + case Fin3: + buffer.EnableShapingFeature(i + index, features.GetMask(Fin3Tag)); + break; + case Isol: + buffer.EnableShapingFeature(i + index, features.GetMask(IsolTag)); + break; + case Init: + buffer.EnableShapingFeature(i + index, features.GetMask(InitTag)); + break; + case Medi: + buffer.EnableShapingFeature(i + index, features.GetMask(MediTag)); + break; + case Med2: + buffer.EnableShapingFeature(i + index, features.GetMask(Med2Tag)); + break; + } + } + } + + /// + /// Gives each free variation selector the form of the character it follows. A + /// selector chooses between shapes of that character, so it has to be drawn in + /// the same form as the character it qualifies. + /// + /// The glyph shaping buffer. + /// The zero-based index of the first record. + /// The number of records. + /// The form chosen for each record. + private static void CarryFormsToVariationSelectors(ShapingBuffer buffer, int index, int count, byte[] actions) + { + // U+180B..U+180D FREE VARIATION SELECTOR ONE..THREE and + // U+180F FREE VARIATION SELECTOR FOUR. + const int FirstFreeVariationSelector = 0x180B; + const int LastFreeVariationSelector = 0x180D; + const int FourthFreeVariationSelector = 0x180F; + + for (int i = 1; i < count; i++) + { + int value = buffer[i + index].CodePoint.Value; + if ((value >= FirstFreeVariationSelector && value <= LastFreeVariationSelector) + || value == FourthFreeVariationSelector) + { + actions[i] = actions[i - 1]; + } + } + } + + /// + /// Maps the joining properties of a character to the column of the state table + /// that describes it. + /// + /// The joining properties of the character. + /// The state table column index. + private static int GetShapingClassIndex(ArabicJoiningClass joiningClass) + { + // Two joining groups have rules of their own, and the columns that carry + // them stand in place of the ones the joining type would choose. + if (joiningClass.JoiningGroup == ArabicJoiningGroup.Alaph) + { + return 4; + } + + if (joiningClass.JoiningGroup == ArabicJoiningGroup.DalathRish) + { + return 5; + } + + return joiningClass.JoiningType switch + { + ArabicJoiningType.NonJoining => 0, + ArabicJoiningType.LeftJoining => 1, + ArabicJoiningType.RightJoining => 2, + ArabicJoiningType.DualJoining or ArabicJoiningType.JoinCausing => 3, + ArabicJoiningType.Transparent => 6, + _ => 0, + }; + } +} diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs index e7b42ccc1..d30e796df 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs @@ -170,82 +170,6 @@ protected override void AssignFeatures(ShapingBuffer buffer, int index, int coun { base.AssignFeatures(buffer, index, count); - int prev = -1; - int state = 0; - byte[] actions = buffer.GetShaperScratch(count); - - // Apply the state machine to map glyphs to features. - for (int i = 0; i < count; i++) - { - ref GlyphShapingData data = ref buffer[i + index]; - ArabicJoiningClass joiningClass = CodePoint.GetArabicJoiningClass(data.CodePoint); - ArabicJoiningType joiningType = joiningClass.JoiningType; - if (joiningType == ArabicJoiningType.Transparent) - { - actions[i] = None; - continue; - } - - int shapingClassIndex = GetShapingClassIndex(joiningType); - byte[] actionsWithState = StateTable[state, shapingClassIndex]; - byte prevAction = actionsWithState[0]; - byte curAction = actionsWithState[1]; - state = actionsWithState[2]; - - if (prevAction != None && prev != -1) - { - actions[prev] = prevAction; - } - - actions[i] = curAction; - prev = i; - } - - // Apply the chosen features to their respective glyphs. - for (int i = 0; i < count; i++) - { - switch (actions[i]) - { - case Fina: - buffer.EnableShapingFeature(i + index, this.Features.GetMask(FinaTag)); - break; - case Fin2: - buffer.EnableShapingFeature(i + index, this.Features.GetMask(Fin2Tag)); - break; - case Fin3: - buffer.EnableShapingFeature(i + index, this.Features.GetMask(Fin3Tag)); - break; - case Isol: - buffer.EnableShapingFeature(i + index, this.Features.GetMask(IsolTag)); - break; - case Init: - buffer.EnableShapingFeature(i + index, this.Features.GetMask(InitTag)); - break; - case Medi: - buffer.EnableShapingFeature(i + index, this.Features.GetMask(MediTag)); - break; - case Med2: - buffer.EnableShapingFeature(i + index, this.Features.GetMask(Med2Tag)); - break; - } - } + ArabicJoining.Apply(buffer, index, count, this.ScriptClass, this.Features); } - - /// - /// Maps an Arabic joining type to the corresponding column index in the state table. - /// - /// The Arabic joining type. - /// The state table column index. - private static int GetShapingClassIndex(ArabicJoiningType joiningType) => joiningType switch - { - ArabicJoiningType.NonJoining => 0, - ArabicJoiningType.LeftJoining => 1, - ArabicJoiningType.RightJoining => 2, - ArabicJoiningType.DualJoining or ArabicJoiningType.JoinCausing => 3, - - // TODO: ALAPH: 4 - // TODO: DALATH RISH': 5 - ArabicJoiningType.Transparent => 6, - _ => 0, - }; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs index 378598f8e..aaa721e01 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs @@ -69,14 +69,65 @@ internal abstract class BaseShaper /// public MarkZeroingMode MarkZeroingMode { get; protected set; } + /// + /// Gets or sets how far the text is taken apart and put back together before it + /// is shaped. Defaults to joining the marks the font offers joined forms for, + /// which is what a script without an engine of its own wants. + /// + public NormalizationMode NormalizationMode { get; protected set; } = NormalizationMode.ComposedDiacritics; + + /// + /// Takes a character apart into the pair it is canonically equivalent to. A + /// script whose characters spell a sound the font draws in pieces overrides this + /// to hand those pieces back. + /// + /// The character to take apart. + /// When this method returns, contains the leading character. + /// + /// When this method returns, contains the trailing character, or the default when + /// the character stands for a single other one. + /// + /// when the character comes apart. + public virtual bool TryDecompose(CodePoint codePoint, out CodePoint first, out CodePoint second) + => CodePoint.TryDecompose(codePoint, out first, out second); + + /// + /// Joins a pair of characters into the single character they are canonically + /// equivalent to. A script that must not have two of its marks joined back + /// together overrides this to refuse. + /// + /// The leading character. + /// The trailing character. + /// When this method returns, contains the joined character. + /// when the pair joins. + public virtual bool TryCompose(CodePoint first, CodePoint second, out CodePoint composed) + => CodePoint.TryCompose(first, second, out composed); + /// /// Assigns the features to each glyph within the buffer. /// + /// The font metrics, which decide what the font can draw. /// The glyph shaping buffer. /// The zero-based index of the elements to assign. /// The number of elements to assign. - public void Plan(ShapingBuffer buffer, int index, int count) + public void Plan(FontMetrics fontMetrics, ShapingBuffer buffer, int index, int count) { + // The text is prepared before any feature is planned, so every later + // stage sees the records this step inserts. It reads the text alone, so + // it runs once, on the pass that substitutes. + if (buffer.Role == ShapingBufferRole.Substitution) + { + int preprocessCount = buffer.Count; + + this.PreprocessText(buffer, index, count); + + count += buffer.Count - preprocessCount; + + // Normalizing settles which characters the run is spelled with, so it + // runs before any feature is planned against them, and once. + count += TextNormalizer.Normalize(this, fontMetrics, buffer, index, count); + } + // Registration is deterministic for a plan's identity, so the first pass // collects it once: feature bits, stages, joiner flags, and the fold of // every whole-segment mask. Later passes replay the fold in one walk and @@ -119,6 +170,18 @@ public void Plan(ShapingBuffer buffer, int index, int count) this.AssignFeatures(buffer, index, count); } + /// + /// Prepares the text of the segment before any feature is planned. A script + /// whose text can spell something it must not be read as separates the + /// characters here, where the records still stand as the text wrote them. + /// + /// The glyph shaping buffer. + /// The zero-based index of the first record. + /// The number of records. + protected virtual void PreprocessText(ShapingBuffer buffer, int index, int count) + { + } + /// /// Applies the per-text feature masks that cannot fold into the collected /// whole-segment masks, such as spans that follow the resolved direction of diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs index 74abaaaea..008da2e8e 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs @@ -76,6 +76,16 @@ internal class DefaultShaper : BaseShaper /// protected static readonly Tag MkmkTag = Tag.Parse("mkmk"); + /// + /// The 'abvm' (above-base mark positioning) feature tag. + /// + protected static readonly Tag AbvmTag = Tag.Parse("abvm"); + + /// + /// The 'blwm' (below-base mark positioning) feature tag. + /// + protected static readonly Tag BlwmTag = Tag.Parse("blwm"); + /// /// The 'calt' (contextual alternates) feature tag. /// @@ -101,6 +111,11 @@ internal class DefaultShaper : BaseShaper /// protected static readonly Tag CursTag = Tag.Parse("curs"); + /// + /// The 'dist' (distances) feature tag. + /// + protected static readonly Tag DistTag = Tag.Parse("dist"); + /// /// The 'kern' (kerning) feature tag. /// @@ -295,14 +310,19 @@ private void AddDirectionalFeatures(ShapingBuffer buffer, int index, int count) } /// + /// + /// The common and horizontal feature sets are transcribed from HarfBuzz 14.2.1, src/hb-ot-shape.cc, symbols common_features and horizontal_features. The feature sets are shaping behavior and are not derivable from the Unicode Character Database. + /// protected override void PlanPostprocessingFeatures(ShapingBuffer buffer, int index, int count) { // Add common features. + this.EnableFeature(buffer, index, count, AbvmTag); + this.EnableFeature(buffer, index, count, BlwmTag); this.EnableFeature(buffer, index, count, CcmpTag); this.EnableFeature(buffer, index, count, LoclTag); this.EnableFeature(buffer, index, count, RligTag); - this.EnableFeature(buffer, index, count, MarkTag); - this.EnableFeature(buffer, index, count, MkmkTag); + this.EnableFeature(buffer, index, count, MarkTag, ShapingFeatureFlags.ManualJoiners); + this.EnableFeature(buffer, index, count, MkmkTag, ShapingFeatureFlags.ManualJoiners); LayoutMode layoutMode = buffer.TextOptions.LayoutMode; bool isVerticalLayout = false; @@ -321,6 +341,7 @@ protected override void PlanPostprocessingFeatures(ShapingBuffer buffer, int ind this.EnableFeature(buffer, index, count, LigaTag); this.EnableFeature(buffer, index, count, RcltTag); this.EnableFeature(buffer, index, count, CursTag); + this.EnableFeature(buffer, index, count, DistTag); this.EnableFeature(buffer, index, count, KernTag); } else diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs index e8fcd168d..e62a31a44 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs @@ -175,7 +175,16 @@ internal sealed class HangulShaper : DefaultShaper /// The font metrics for glyph lookups. public HangulShaper(ScriptClass script, TextOptions textOptions, FontMetrics fontMetrics) : base(script, MarkZeroingMode.None, textOptions) - => this.fontMetrics = fontMetrics; + { + this.fontMetrics = fontMetrics; + + // The text is left exactly as it was written. This shaper composes and + // decomposes syllables itself, by the rules the script's own features + // describe, and a font of this script is not built to mix ready-made + // syllables with the letters they are built from. Taking the text apart + // beforehand would hand it text of both kinds at once. + this.NormalizationMode = NormalizationMode.None; + } /// protected override void PlanFeatures(ShapingBuffer buffer, int index, int count) diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs index 3e8f16ce7..7e7dd92f5 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs @@ -135,21 +135,6 @@ internal sealed class IndicShaper : DefaultShaper /// private static readonly Tag HalnTag = Tag.Parse("haln"); - /// - /// The 'dist' (distances) feature tag. - /// - private static readonly Tag DistTag = Tag.Parse("dist"); - - /// - /// The 'abvm' (above-base mark positioning) feature tag. - /// - private static readonly Tag AbvmTag = Tag.Parse("abvm"); - - /// - /// The 'blwm' (below-base mark positioning) feature tag. - /// - private static readonly Tag BlwmTag = Tag.Parse("blwm"); - /// /// Dotted circle code point (U+25CC) used as a placeholder base. /// @@ -214,6 +199,13 @@ public IndicShaper(ScriptClass script, Tag unicodeScriptTag, TextOptions textOpt this.initialReorderAction = this.InitialReorder; this.finalReorderAction = this.FinalReorder; + // Every character comes apart first, even one the font already draws whole. + // This shaper reads a syllable by its pieces and moves them about, so a vowel + // written as one character has to be split into the pieces that are reordered; + // were it left whole because the font can draw it, the reordering would never + // see it. + this.NormalizationMode = NormalizationMode.ComposedDiacriticsNoShortCircuit; + if (IndicConfigurations.TryGetValue(script, out ShapingConfiguration value)) { this.indicConfiguration = value; @@ -227,6 +219,10 @@ public IndicShaper(ScriptClass script, Tag unicodeScriptTag, TextOptions textOpt this.zeroContext = !this.isOldSpec && script != ScriptClass.Malayalam; } + /// + protected override void PreprocessText(ShapingBuffer buffer, int index, int count) + => VowelConstraints.Insert(buffer, this.fontMetrics, this.ScriptClass, index, count); + /// protected override void PlanFeatures(ShapingBuffer buffer, int index, int count) { @@ -253,9 +249,6 @@ protected override void PlanFeatures(ShapingBuffer buffer, int index, int count) this.EnableFeature(buffer, index, count, BlwsTag, ShapingFeatureFlags.ManualJoiners | ShapingFeatureFlags.PerSyllable); this.EnableFeature(buffer, index, count, PstsTag, ShapingFeatureFlags.ManualJoiners | ShapingFeatureFlags.PerSyllable); this.EnableFeature(buffer, index, count, HalnTag, ShapingFeatureFlags.ManualJoiners | ShapingFeatureFlags.PerSyllable); - this.EnableFeature(buffer, index, count, DistTag); - this.EnableFeature(buffer, index, count, AbvmTag); - this.EnableFeature(buffer, index, count, BlwmTag); } /// @@ -285,9 +278,7 @@ protected override void AssignFeatures(ShapingBuffer buffer, int index, int coun for (int i = end - 1; i >= index; i--) { ref GlyphShapingData data = ref buffer[i]; - if ((Decompositions.TryGetValue(data.CodePoint.Value, out int[]? decompositions) || - UniversalShapingData.Decompositions.TryGetValue(data.CodePoint.Value, out decompositions)) && - decompositions != null) + if (UniversalShapingData.Decompositions.TryGetValue(data.CodePoint.Value, out int[]? decompositions) && decompositions != null) { Span ids = decompositionIds[..decompositions.Length]; bool shouldDecompose = true; @@ -784,7 +775,7 @@ private void InitialReorder(ShapePlan plan, ShapingBuffer buffer, int index, int if (item.Syllable.Type != SyllableType.None) { Categories category = item.Syllable.IndicCategory; - if ((FlagUnsafe(category) & (JoinerFlags | Flag(Categories.N) | Flag(Categories.RS) | Flag(Categories.CM) | (HalantOrCoengFlags & FlagUnsafe(category)))) != 0) + if ((FlagUnsafe(category) & (JoinerFlags | Flag(Categories.N) | Flag(Categories.RS) | Flag(Categories.CM) | (HalantFlags & FlagUnsafe(category)))) != 0) { item.Syllable.IndicPosition = lastPosition; if (category == Categories.H && item.Syllable.IndicPosition == Positions.Pre_M) @@ -1092,8 +1083,8 @@ private static bool IsJoiner(ref GlyphShapingData data) /// /// The glyph shaping data. /// if the glyph is a halant or coeng. - private static bool IsHalantOrCoeng(ref GlyphShapingData data) - => (FlagUnsafe(data.Syllable.IndicCategory) & HalantOrCoengFlags) != 0; + private static bool IsHalant(ref GlyphShapingData data) + => (FlagUnsafe(data.Syllable.IndicCategory) & HalantFlags) != 0; /// /// Finds the start index of the next syllable in the buffer. @@ -1168,7 +1159,7 @@ private void FinalReorder(ShapePlan plan, ShapingBuffer buffer, int index, int c // Ok, this was a 'pref' candidate but didn't form any. // Base is around here... basePosition = i; - while (basePosition < end && IsHalantOrCoeng(ref buffer[basePosition])) + while (basePosition < end && IsHalant(ref buffer[basePosition])) { basePosition++; } @@ -1196,7 +1187,7 @@ private void FinalReorder(ShapePlan plan, ShapingBuffer buffer, int index, int c i++; } - if (i == end || !IsHalantOrCoeng(ref buffer[i])) + if (i == end || !IsHalant(ref buffer[i])) { break; } @@ -1239,7 +1230,7 @@ private void FinalReorder(ShapePlan plan, ShapingBuffer buffer, int index, int c if (basePosition < end) { - while (start < basePosition && (FlagUnsafe(buffer[basePosition].Syllable.IndicCategory) & (Flag(Categories.N) | HalantOrCoengFlags)) != 0) + while (start < basePosition && (FlagUnsafe(buffer[basePosition].Syllable.IndicCategory) & (Flag(Categories.N) | HalantFlags)) != 0) { basePosition--; } @@ -1265,7 +1256,7 @@ private void FinalReorder(ShapePlan plan, ShapingBuffer buffer, int index, int c // We want to position matra after them. if (this.ScriptClass is not ScriptClass.Malayalam and not ScriptClass.Tamil) { - while (newPos > start && (FlagUnsafe(buffer[newPos].Syllable.IndicCategory) & (Flag(Categories.M) | HalantOrCoengFlags)) == 0) + while (newPos > start && (FlagUnsafe(buffer[newPos].Syllable.IndicCategory) & (Flag(Categories.M) | HalantFlags)) == 0) { newPos--; } @@ -1274,7 +1265,7 @@ private void FinalReorder(ShapePlan plan, ShapingBuffer buffer, int index, int c // Otherwise only proceed if the Halant does // not belong to the Matra itself! ref GlyphShapingData current = ref buffer[newPos]; - if (IsHalantOrCoeng(ref current) && current.Syllable.IndicPosition != Positions.Pre_M) + if (IsHalant(ref current) && current.Syllable.IndicPosition != Positions.Pre_M) { // If ZWJ or ZWNJ follow this halant, position is moved after it. if (newPos + 1 < end && IsJoiner(ref buffer[newPos + 1])) @@ -1349,12 +1340,12 @@ private void FinalReorder(ShapePlan plan, ShapingBuffer buffer, int index, int c // fixed in shaping engine, there was no case where reph position // will be found on this step. newRephPos = start + 1; - while (newRephPos < basePosition && !IsHalantOrCoeng(ref buffer[newRephPos])) + while (newRephPos < basePosition && !IsHalant(ref buffer[newRephPos])) { newRephPos++; } - if (newRephPos < basePosition && IsHalantOrCoeng(ref buffer[newRephPos])) + if (newRephPos < basePosition && IsHalant(ref buffer[newRephPos])) { // ->If ZWJ or ZWNJ are following this halant, position is moved after it. if (newRephPos + 1 < basePosition && IsJoiner(ref buffer[newRephPos + 1])) @@ -1408,12 +1399,12 @@ private void FinalReorder(ShapePlan plan, ShapingBuffer buffer, int index, int c { // Copied from step 2. newRephPos = start + 1; - while (newRephPos < basePosition && !IsHalantOrCoeng(ref buffer[newRephPos])) + while (newRephPos < basePosition && !IsHalant(ref buffer[newRephPos])) { newRephPos++; } - if (newRephPos < basePosition && IsHalantOrCoeng(ref buffer[newRephPos])) + if (newRephPos < basePosition && IsHalant(ref buffer[newRephPos])) { // ->If ZWJ or ZWNJ are following this halant, position is moved after it. if (newRephPos + 1 < basePosition && IsJoiner(ref buffer[newRephPos + 1])) @@ -1439,7 +1430,7 @@ private void FinalReorder(ShapePlan plan, ShapingBuffer buffer, int index, int c // However, if it's a plain Consonant,Halant we shouldn't do that. // Uniscribe doesn't do this. // TEST: U+0930,U+094D,U+0915,U+094B,U+094D - if (IsHalantOrCoeng(ref buffer[newRephPos])) + if (IsHalant(ref buffer[newRephPos])) { for (int i = basePosition + 1; i < newRephPos; i++) { @@ -1495,7 +1486,7 @@ private void FinalReorder(ShapePlan plan, ShapingBuffer buffer, int index, int c // We want to position matra after them. if (this.ScriptClass is not ScriptClass.Malayalam and not ScriptClass.Tamil) { - while (newPos > start && (FlagUnsafe(buffer[newPos - 1].Syllable.IndicCategory) & (Flag(Categories.M) | HalantOrCoengFlags)) == 0) + while (newPos > start && (FlagUnsafe(buffer[newPos - 1].Syllable.IndicCategory) & (Flag(Categories.M) | HalantFlags)) == 0) { newPos--; } @@ -1517,7 +1508,7 @@ private void FinalReorder(ShapePlan plan, ShapingBuffer buffer, int index, int c } } - if (newPos > start && IsHalantOrCoeng(ref buffer[newPos - 1])) + if (newPos > start && IsHalant(ref buffer[newPos - 1])) { // -> If ZWJ or ZWNJ follow this halant, position is moved after it. if (newPos < end && IsJoiner(ref buffer[newPos])) diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/KhmerShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/KhmerShaper.cs new file mode 100644 index 000000000..f6df24574 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/KhmerShaper.cs @@ -0,0 +1,476 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Unicode; +using UnicodeTrieGenerator.StateAutomation; +using static SixLabors.Fonts.Unicode.Resources.IndicShapingData; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers; + +/// +/// Shapes Khmer syllables and assigns the script's required substitution features. +/// +/// +/// The behavior is transcribed from HarfBuzz 14.2.1, src/hb-ot-shaper-khmer.cc, symbols collect_features_khmer, override_features_khmer, setup_syllables_khmer, reorder_khmer, reorder_consonant_syllable, decompose_khmer, and compose_khmer, and src/hb-ot-shaper-khmer-machine.rl, symbol khmer_syllable_machine. The syllable grammar, feature assignment rules, and split-vowel decompositions are not derivable from the Unicode Character Database. +/// +internal sealed class KhmerShaper : DefaultShaper +{ + /// + /// The bit shift extracting the shaping category from the packed property word. + /// + private const int CategoryShift = 8; + + /// + /// The first split vowel whose leading piece is U+17C1. + /// + private const int FirstSplitVowel = 0x17BE; + + /// + /// The second split vowel whose leading piece is U+17C1. + /// + private const int SecondSplitVowel = 0x17BF; + + /// + /// The third split vowel whose leading piece is U+17C1. + /// + private const int ThirdSplitVowel = 0x17C0; + + /// + /// The fourth split vowel whose leading piece is U+17C1. + /// + private const int FourthSplitVowel = 0x17C4; + + /// + /// The fifth split vowel whose leading piece is U+17C1. + /// + private const int FifthSplitVowel = 0x17C5; + + /// + /// The pre-base piece inserted before each split vowel. + /// + private const int SplitVowelLeadingPiece = 0x17C1; + + /// + /// The dotted circle inserted as the missing base of a broken syllable. + /// + private const int DottedCircle = 0x25CC; + + /// + /// The state machine used to identify Khmer syllables. + /// + private static readonly StateMachine StateMachine = new(Unicode.Resources.KhmerShapingData.StateTable, Unicode.Resources.KhmerShapingData.AcceptingStates, Unicode.Resources.KhmerShapingData.Tags); + + /// + /// The syllable type assigned by each accepting state. + /// + private static readonly SyllableType[] StateSyllableTypes = SyllableTypeMap.FromMachineTags(Unicode.Resources.KhmerShapingData.Tags); + + /// + /// Maps sparse shaping categories to the state machine's compact alphabet. + /// + private static readonly int[] CategoryToSymbolId = BuildCategoryToSymbolId(); + + /// + /// The pre-base forms feature. + /// + private static readonly Tag PrefTag = Tag.Parse("pref"); + + /// + /// The below-base forms feature. + /// + private static readonly Tag BlwfTag = Tag.Parse("blwf"); + + /// + /// The above-base forms feature. + /// + private static readonly Tag AbvfTag = Tag.Parse("abvf"); + + /// + /// The post-base forms feature. + /// + private static readonly Tag PstfTag = Tag.Parse("pstf"); + + /// + /// The conjunct form after Ro feature. + /// + private static readonly Tag CfarTag = Tag.Parse("cfar"); + + /// + /// The pre-base substitutions feature. + /// + private static readonly Tag PresTag = Tag.Parse("pres"); + + /// + /// The above-base substitutions feature. + /// + private static readonly Tag AbvsTag = Tag.Parse("abvs"); + + /// + /// The below-base substitutions feature. + /// + private static readonly Tag BlwsTag = Tag.Parse("blws"); + + /// + /// The post-base substitutions feature. + /// + private static readonly Tag PstsTag = Tag.Parse("psts"); + + /// + /// The font metrics used to resolve the dotted-circle glyph. + /// + private readonly FontMetrics fontMetrics; + + /// + /// The combined syllable setup and initial reordering stage action. + /// + private readonly Action setupAndReorderAction; + + /// + /// The action that clears syllable state after the basic features. + /// + private readonly Action clearSyllablesAction; + + /// + /// Whether the current segment contains a syllable missing its base. + /// + private bool hasBrokenSyllables; + + /// + /// Initializes a new instance of the class. + /// + /// The script classification. + /// The text options. + /// The font metrics used for glyph lookup. + public KhmerShaper(ScriptClass script, TextOptions textOptions, FontMetrics fontMetrics) + : base(script, MarkZeroingMode.None, textOptions) + { + this.fontMetrics = fontMetrics; + this.setupAndReorderAction = this.SetupAndReorder; + this.clearSyllablesAction = ClearSyllables; + this.NormalizationMode = NormalizationMode.ComposedDiacriticsNoShortCircuit; + } + + /// + /// + /// The five split-vowel pairs are transcribed from HarfBuzz 14.2.1, src/hb-ot-shaper-khmer.cc, symbol decompose_khmer. They are not canonical decompositions and are not derivable from the Unicode Character Database. + /// + public override bool TryDecompose(CodePoint codePoint, out CodePoint first, out CodePoint second) + { + switch (codePoint.Value) + { + case FirstSplitVowel: + case SecondSplitVowel: + case ThirdSplitVowel: + case FourthSplitVowel: + case FifthSplitVowel: + first = new CodePoint(SplitVowelLeadingPiece); + second = codePoint; + return true; + default: + return base.TryDecompose(codePoint, out first, out second); + } + } + + /// + /// + /// The leading-mark exclusion is transcribed from HarfBuzz 14.2.1, src/hb-ot-shaper-khmer.cc, symbol compose_khmer. It is shaping behavior and is not derivable from the Unicode Character Database. + /// + public override bool TryCompose(CodePoint first, CodePoint second, out CodePoint composed) + { + if (CodePoint.IsMark(first)) + { + composed = default; + return false; + } + + return base.TryCompose(first, second, out composed); + } + + /// + protected override void PlanFeatures(ShapingBuffer buffer, int index, int count) + { + ShapingFeatureFlags basicFlags = ShapingFeatureFlags.ManualJoiners | ShapingFeatureFlags.PerSyllable; + + // Both source pauses precede the first lookup group and have no lookups between them, so one stage callback preserves their ordering without manufacturing an empty feature stage. + this.EnableFeature(buffer, index, count, LoclTag, ShapingFeatureFlags.PerSyllable, this.setupAndReorderAction, null); + this.EnableFeature(buffer, index, count, CcmpTag, ShapingFeatureFlags.PerSyllable); + + this.AddFeature(buffer, index, count, PrefTag, basicFlags, false, null, null); + this.AddFeature(buffer, index, count, BlwfTag, basicFlags, false, null, null); + this.AddFeature(buffer, index, count, AbvfTag, basicFlags, false, null, null); + this.AddFeature(buffer, index, count, PstfTag, basicFlags, false, null, null); + this.AddFeature(buffer, index, count, CfarTag, basicFlags, false, null, this.clearSyllablesAction); + + this.EnableFeature(buffer, index, count, PresTag, ShapingFeatureFlags.ManualJoiners); + this.EnableFeature(buffer, index, count, AbvsTag, ShapingFeatureFlags.ManualJoiners); + this.EnableFeature(buffer, index, count, BlwsTag, ShapingFeatureFlags.ManualJoiners); + this.EnableFeature(buffer, index, count, PstsTag, ShapingFeatureFlags.ManualJoiners); + } + + /// + protected override void PlanPostprocessingFeatures(ShapingBuffer buffer, int index, int count) + { + base.PlanPostprocessingFeatures(buffer, index, count); + + this.EnableFeature(buffer, index, count, CligTag); + this.Features.DisableFeature(LigaTag); + } + + /// + protected override void AssignFeatures(ShapingBuffer buffer, int index, int count) + { + } + + /// + /// Identifies syllables and performs their initial reordering before substitution lookups run. + /// + /// The plan whose segment is being shaped. + /// The glyph shaping buffer. + /// The zero-based start index. + /// The number of elements. + private void SetupAndReorder(ShapePlan plan, ShapingBuffer buffer, int index, int count) + { + if (buffer.Role != ShapingBufferRole.Substitution) + { + return; + } + + this.SetupSyllables(buffer, index, count); + this.Reorder(plan, buffer, index, count); + } + + /// + /// Assigns the syllable and category information consumed by feature matching and reordering. + /// + /// + /// The grammar is transcribed from HarfBuzz 14.2.1, src/hb-ot-shaper-khmer-machine.rl, symbol khmer_syllable_machine. It is not derivable from the Unicode Character Database. + /// + /// The glyph shaping buffer. + /// The zero-based start index. + /// The number of elements. + private void SetupSyllables(ShapingBuffer buffer, int index, int count) + { + this.hasBrokenSyllables = false; + + Span values = count <= 64 ? stackalloc int[count] : new int[count]; + Span categories = count <= 64 ? stackalloc byte[count] : new byte[count]; + for (int i = 0; i < count; i++) + { + int category = UnicodeData.GetIndicShapingProperties((uint)buffer[index + i].CodePoint.Value) >> CategoryShift; + categories[i] = (byte)category; + values[i] = CategoryToSymbolId[category]; + } + + int syllable = 0; + int last = 0; + StateMachine.MatchEnumerator match = StateMachine.EnumerateMatches(values); + while (match.MoveNext()) + { + // The fallback category is a one-character rule in the source machine, so any unmatched input also receives its own syllable number. + while (last < match.StartIndex) + { + syllable++; + ref GlyphShapingData unmatched = ref buffer[index + last]; + unmatched.Syllable.IndicCategory = Categories.X; + unmatched.Syllable.IndicPosition = Positions.End; + unmatched.Syllable.Type = SyllableType.NonIndicCluster; + unmatched.Syllable.Number = syllable; + last++; + } + + syllable++; + SyllableType syllableType = StateSyllableTypes[match.TagState]; + this.hasBrokenSyllables |= syllableType == SyllableType.BrokenCluster; + + for (int i = match.StartIndex; i <= match.EndIndex; i++) + { + ref GlyphShapingData data = ref buffer[index + i]; + data.Syllable.IndicCategory = (Categories)categories[i]; + data.Syllable.IndicPosition = Positions.End; + data.Syllable.Type = syllableType; + data.Syllable.Number = syllable; + } + + last = match.EndIndex + 1; + } + + while (last < count) + { + syllable++; + ref GlyphShapingData unmatched = ref buffer[index + last]; + unmatched.Syllable.IndicCategory = Categories.X; + unmatched.Syllable.IndicPosition = Positions.End; + unmatched.Syllable.Type = SyllableType.NonIndicCluster; + unmatched.Syllable.Number = syllable; + last++; + } + } + + /// + /// Inserts missing bases and reorders each consonant or broken syllable. + /// + /// + /// This pass is transcribed from HarfBuzz 14.2.1, src/hb-ot-shaper-khmer.cc, symbols reorder_khmer, reorder_syllable_khmer, and reorder_consonant_syllable. Its feature-mask assignment and movement rules are not derivable from the Unicode Character Database. + /// + /// The plan whose feature masks are assigned. + /// The glyph shaping buffer. + /// The zero-based start index. + /// The number of elements. + private void Reorder(ShapePlan plan, ShapingBuffer buffer, int index, int count) + { + int max = index + count; + int start = index; + int end = NextSyllable(buffer, start, max); + + if (this.hasBrokenSyllables && this.fontMetrics.TryGetGlyphId(new CodePoint(DottedCircle), out ushort circleId)) + { + while (start < max) + { + if (buffer[start].Syllable.Type == SyllableType.BrokenCluster) + { + buffer.InsertDottedCircle(start, circleId); + buffer[start].Syllable.IndicCategory = Categories.Dotted_Circle; + buffer[start].Syllable.IndicPosition = Positions.End; + end++; + max++; + } + + start = end; + end = NextSyllable(buffer, start, max); + } + + start = index; + end = NextSyllable(buffer, start, max); + } + + uint postBaseMask = plan.Features.GetMask(BlwfTag) | plan.Features.GetMask(AbvfTag) | plan.Features.GetMask(PstfTag); + uint prefMask = plan.Features.GetMask(PrefTag); + uint cfarMask = plan.Features.GetMask(CfarTag); + + while (start < max) + { + SyllableType type = buffer[start].Syllable.Type; + if (type is SyllableType.ConsonantSyllable or SyllableType.BrokenCluster) + { + ReorderConsonantSyllable(buffer, start, end, postBaseMask, prefMask, cfarMask); + } + + start = end; + end = NextSyllable(buffer, start, max); + } + } + + /// + /// Assigns basic feature masks and moves pre-base pieces within one syllable. + /// + /// The glyph shaping buffer. + /// The first record in the syllable. + /// The exclusive end of the syllable. + /// The combined below-, above-, and post-base forms mask. + /// The pre-base forms mask. + /// The conjunct form after Ro mask. + private static void ReorderConsonantSyllable(ShapingBuffer buffer, int start, int end, uint postBaseMask, uint prefMask, uint cfarMask) + { + for (int i = start + 1; i < end; i++) + { + buffer.EnableShapingFeature(i, postBaseMask); + } + + int coengCount = 0; + for (int i = start + 1; i < end; i++) + { + if (buffer[i].Syllable.IndicCategory == Categories.H && coengCount <= 2 && i + 1 < end) + { + coengCount++; + + if (buffer[i + 1].Syllable.IndicCategory == Categories.Ra) + { + buffer.EnableShapingFeature(i, prefMask); + buffer.EnableShapingFeature(i + 1, prefMask); + + // Move the two records independently so their order remains H, Ra at the start. + buffer.MoveGlyph(i, start); + buffer.MoveGlyph(i + 1, start + 1); + + for (int j = i + 2; j < end; j++) + { + buffer.EnableShapingFeature(j, cfarMask); + } + + coengCount = 2; + } + } + else if (buffer[i].Syllable.IndicCategory == Categories.VPre) + { + buffer.MoveGlyph(i, start); + } + } + } + + /// + /// Clears syllable state once the features constrained by it have run. + /// + /// The plan whose segment is being shaped. + /// The glyph shaping buffer. + /// The zero-based start index. + /// The number of elements. + private static void ClearSyllables(ShapePlan plan, ShapingBuffer buffer, int index, int count) + { + if (buffer.Role != ShapingBufferRole.Substitution) + { + return; + } + + int end = index + count; + for (int i = index; i < end; i++) + { + buffer[i].Syllable = default; + } + } + + /// + /// Finds the exclusive end of the syllable beginning at the given index. + /// + /// The glyph shaping buffer. + /// The first record in the syllable. + /// The exclusive segment end. + /// The exclusive end of the syllable. + private static int NextSyllable(ShapingBuffer buffer, int index, int end) + { + if (index >= end) + { + return index; + } + + int syllable = buffer[index].Syllable.Number; + while (++index < end && buffer[index].Syllable.Number == syllable) + { + } + + return index; + } + + /// + /// Builds the sparse-category to compact-symbol mapping consumed by the state machine. + /// + /// The category mapping. + private static int[] BuildCategoryToSymbolId() + { + Categories[] allCategories = Enum.GetValues(); + int maxCategory = 0; + foreach (Categories category in allCategories) + { + maxCategory = Math.Max(maxCategory, (int)category); + } + + // The default symbol is X, which is deliberately the first category and catches every category outside the Khmer alphabet. + int[] map = new int[maxCategory + 1]; + KhmerCategories[] categories = Enum.GetValues(); + for (int symbolId = 0; symbolId < categories.Length; symbolId++) + { + map[(int)categories[symbolId]] = symbolId; + } + + return map; + } +} diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs index 5379e832f..1fec2fdbf 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs @@ -131,6 +131,12 @@ public MyanmarShaper(ScriptClass script, TextOptions textOptions, FontMetrics fo this.fontMetrics = fontMetrics; this.setupSyllablesAction = this.SetupSyllables; this.initialReorderAction = this.InitialReorder; + + // Every character comes apart first, even one the font already draws whole. + // This shaper divides a run into syllables and moves their pieces about, so a + // character standing for several pieces must be split before the division can + // see them. + this.NormalizationMode = NormalizationMode.ComposedDiacriticsNoShortCircuit; } /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/NormalizationMode.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/NormalizationMode.cs new file mode 100644 index 000000000..3f3f1925d --- /dev/null +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/NormalizationMode.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers; + +/// +/// States how far a shaper wants its text taken apart and put back together before +/// it is shaped. +/// +internal enum NormalizationMode +{ + /// + /// Leaves the text as it was written. Used by a script whose own engine reads + /// the characters as they stand. + /// + None, + + /// + /// Takes the text apart as far as the font allows and leaves it apart. + /// + Decomposed, + + /// + /// Takes the text apart, orders the marks, then joins the marks back onto the + /// character they follow wherever the font offers the joined form. A character + /// standing on its own that the font already draws is left untouched. + /// + ComposedDiacritics, + + /// + /// As , but every character is taken apart + /// first, including one standing on its own that the font already draws. Used by + /// a script whose engine needs the parts of a character it would otherwise + /// never see. + /// + ComposedDiacriticsNoShortCircuit +} diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShaperFactory.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShaperFactory.cs index 96aed0d1d..2f2d1fdc0 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShaperFactory.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShaperFactory.cs @@ -13,6 +13,32 @@ internal static class ShaperFactory /// The 'mym2' (Myanmar v2) script tag used to distinguish Myanmar shaper versions. private static readonly Tag Mym2Tag = Tag.Parse("mym2"); + /// The 'latn' (Latin) script tag, which a font can carry in place of the one asked for. + private static readonly Tag LatnTag = Tag.Parse("latn"); + + /// + /// The last character of a script tag naming the third revision of its + /// script, the revision the universal engine describes. + /// + private const uint ThirdRevisionTagSuffix = (byte)'3'; + + /// + /// Determines whether the font offers nothing designed for the script, so + /// that the tag settled on is the default one or a Latin one. + /// + /// The script tag found in the font. + /// when the font was not designed for the script. + private static bool IsDefaultDesign(Tag unicodeScriptTag) + => unicodeScriptTag == default || unicodeScriptTag == LatnTag; + + /// + /// Determines whether the tag names the third revision of its script. + /// + /// The script tag found in the font. + /// when the tag names the third revision. + private static bool IsThirdRevision(Tag unicodeScriptTag) + => (unicodeScriptTag.Value & 0xFF) == ThirdRevisionTagSuffix; + /// /// Creates a shaper based on the given script language. /// @@ -30,13 +56,15 @@ public static BaseShaper Create( { // Arabic ScriptClass.Arabic - or ScriptClass.Mongolian or ScriptClass.Syriac - or ScriptClass.Nko - or ScriptClass.PhagsPa - or ScriptClass.Mandaic - or ScriptClass.Manichaean - or ScriptClass.PsalterPahlavi => new ArabicShaper(script, textOptions), + + // Arabic keeps its own shaper even when the font names no script of + // its own, because it is the one script given a fallback of its own. + // Only horizontal text is joined; upright text is shaped plainly. + => (unicodeScriptTag != default || script == ScriptClass.Arabic) + && textOptions.LayoutMode.IsHorizontal() + ? new ArabicShaper(script, textOptions) + : new DefaultShaper(script, textOptions), // Hebrew ScriptClass.Hebrew => new HebrewShaper(script, textOptions, fontMetrics, unicodeScriptTag != default), @@ -58,7 +86,19 @@ or ScriptClass.Malayalam or ScriptClass.Oriya or ScriptClass.Tamil or ScriptClass.Telugu - or ScriptClass.Khmer => new IndicShaper(script, unicodeScriptTag, textOptions, fontMetrics), + + // A font whose only script is the default one, or the Latin one we + // would otherwise pick arbitrarily, was not designed for the script, + // so it is shaped plainly. A font designed to the third revision of + // the script is shaped by the universal engine, which is what that + // revision describes. + => IsDefaultDesign(unicodeScriptTag) + ? new DefaultShaper(script, textOptions) + : IsThirdRevision(unicodeScriptTag) + ? new UniversalShaper(script, textOptions, fontMetrics) + : new IndicShaper(script, unicodeScriptTag, textOptions, fontMetrics), + + ScriptClass.Khmer => new KhmerShaper(script, textOptions, fontMetrics), // Myanmar ScriptClass.Myanmar @@ -127,7 +167,49 @@ or ScriptClass.BeriaErfe or ScriptClass.Sidetic or ScriptClass.TaiYo or ScriptClass.TolongSiki - => new UniversalShaper(script, textOptions, fontMetrics), + or ScriptClass.Ahom + or ScriptClass.Multani + or ScriptClass.Miao + or ScriptClass.Adlam + or ScriptClass.Bhaiksuki + or ScriptClass.Marchen + or ScriptClass.Newa + or ScriptClass.MasaramGondi + or ScriptClass.Soyombo + or ScriptClass.ZanabazarSquare + or ScriptClass.Dogra + or ScriptClass.GunjalaGondi + or ScriptClass.HanifiRohingya + or ScriptClass.Makasar + or ScriptClass.Medefaidrin + or ScriptClass.OldSogdian + or ScriptClass.Sogdian + or ScriptClass.Elymaic + or ScriptClass.Nandinagari + or ScriptClass.NyiakengPuachueHmong + or ScriptClass.Wancho + or ScriptClass.Chorasmian + or ScriptClass.DivesAkuru + or ScriptClass.KhitanSmallScript + or ScriptClass.Yezidi + or ScriptClass.CyproMinoan + or ScriptClass.OldUyghur + or ScriptClass.Tangsa + or ScriptClass.Toto + or ScriptClass.Vithkuqi + or ScriptClass.Mongolian + or ScriptClass.Nko + or ScriptClass.PhagsPa + or ScriptClass.Mandaic + or ScriptClass.Manichaean + or ScriptClass.PsalterPahlavi + + // A font whose only script is the default one, or the Latin one we + // would otherwise pick arbitrarily, was not designed for the script, + // so it is shaped plainly. + => IsDefaultDesign(unicodeScriptTag) + ? new DefaultShaper(script, textOptions) + : new UniversalShaper(script, textOptions, fontMetrics), _ => new DefaultShaper(script, textOptions), }; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/TextNormalizer.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/TextNormalizer.cs new file mode 100644 index 000000000..321d964b9 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/TextNormalizer.cs @@ -0,0 +1,399 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers; + +/// +/// Takes the characters of a run apart, orders the marks, and joins them back +/// together, so that text written in any of the ways the standard calls equivalent +/// is shaped the same way. +/// +/// +/// +/// This follows the Unicode normalization algorithm but departs from it in one +/// respect that matters for shaping: a character comes apart, and a pair joins, +/// only when the font can draw the result. A font offering the joined form gets it, +/// because a joined form usually carries better mark positioning than a font's own +/// mark attachment would give; a font offering only the parts gets the parts. +/// +/// +/// Derived from _hb_ot_shape_normalize in hb-ot-shape-normalize.cc of +/// HarfBuzz 14.2.1, including its three-round structure and the decision in each +/// round. +/// +/// +internal static class TextNormalizer +{ + /// + /// The longest run of marks that is ordered. A run longer than this is left + /// alone, because ordering it costs more than the ordering is worth. + /// + private const int MaxOrderedMarkRun = 32; + + /// + /// The most parts one character is taken apart into. A canonical chain reaches + /// three at its longest, so this leaves room to spare and bounds the gather. + /// + private const int MaxDecompositionParts = 8; + + /// + /// Orders two records by the class that places their marks. + /// + private static readonly Comparison MarkOrder = + static (a, b) => CodePoint.GetMarkOrderingClass(a.CodePoint) - CodePoint.GetMarkOrderingClass(b.CodePoint); + + /// + /// Normalizes the given run of the buffer. + /// + /// The shaper whose preference and joining rules apply. + /// The font metrics, which decide what the font can draw. + /// The glyph shaping buffer. + /// The zero-based index of the run's first record. + /// The number of records in the run. + /// The number of records the run gained or lost. + public static int Normalize(BaseShaper shaper, FontMetrics fontMetrics, ShapingBuffer buffer, int index, int count) + { + NormalizationMode mode = shaper.NormalizationMode; + if (mode == NormalizationMode.None || count == 0) + { + return 0; + } + + int before = buffer.Count; + + // A character standing on its own that the font already draws is left + // untouched, unless the shaper asked for everything to come apart. + bool mayShortCircuit = mode != NormalizationMode.ComposedDiacriticsNoShortCircuit; + bool allMarksSeenAlone = Decompose(shaper, fontMetrics, buffer, index, ref count, mayShortCircuit); + + if (!allMarksSeenAlone) + { + OrderMarks(buffer, index, count); + + if (mode is NormalizationMode.ComposedDiacritics or NormalizationMode.ComposedDiacriticsNoShortCircuit) + { + Compose(shaper, fontMetrics, buffer, index, ref count); + } + } + + return buffer.Count - before; + } + + /// + /// Takes the characters of the run apart. Characters that stand without a mark + /// after them are passed over when the mode allows it; a character followed by + /// marks always comes apart, so that the marks can be ordered against the parts. + /// + /// The shaper whose joining rules apply. + /// The font metrics. + /// The glyph shaping buffer. + /// The zero-based index of the run's first record. + /// The number of records in the run, updated as it changes. + /// Whether a character the font draws may be passed over. + /// + /// when no character of the run was followed by a mark, so + /// there is nothing to order and nothing to join. + /// + private static bool Decompose(BaseShaper shaper, FontMetrics fontMetrics, ShapingBuffer buffer, int index, ref int count, bool mayShortCircuit) + { + bool allSimple = true; + int i = index; + int end = index + count; + + while (i < end) + { + // Find where the marks following this character begin. One character is + // left ahead of them to carry them. + int markStart = i + 1; + while (markStart < end && !CodePoint.IsMark(buffer[markStart].CodePoint)) + { + markStart++; + } + + if (markStart < end) + { + markStart--; + } + + // Up to that point the characters stand alone. + while (i < markStart) + { + int delta = DecomposeOne(shaper, fontMetrics, buffer, i, mayShortCircuit, out int produced); + end += delta; + i += produced; + } + + if (i >= end) + { + break; + } + + allSimple = false; + + // The character and the marks after it come apart together. + int markEnd = i + 1; + while (markEnd < end && CodePoint.IsMark(buffer[markEnd].CodePoint)) + { + markEnd++; + } + + while (i < markEnd) + { + int delta = DecomposeOne(shaper, fontMetrics, buffer, i, false, out int produced); + end += delta; + markEnd += delta; + i += produced; + } + } + + count = end - index; + return allSimple; + } + + /// + /// Takes one character apart, replacing it with its parts when the font can draw + /// them. + /// + /// The shaper whose joining rules apply. + /// The font metrics. + /// The glyph shaping buffer. + /// The zero-based index of the record. + /// Whether a character the font draws may be passed over. + /// + /// When this method returns, contains the number of records now standing where the + /// character stood, which is how far the caller advances. + /// + /// The number of records the buffer gained. + private static int DecomposeOne(BaseShaper shaper, FontMetrics fontMetrics, ShapingBuffer buffer, int i, bool mayShortCircuit, out int produced) + { + CodePoint codePoint = buffer[i].CodePoint; + + // A character that cannot come apart is kept exactly as it stands, whether or + // not the font draws it, so the font is never asked about it. Testing that + // first matters: it is a search of a table held in read-only data, while asking + // the font costs a dictionary probe, and for a run of text with nothing to take + // apart the font would be asked once per character to no purpose. + if (!shaper.TryDecompose(codePoint, out CodePoint _, out CodePoint _)) + { + produced = 1; + return 0; + } + + if (mayShortCircuit && fontMetrics.TryGetGlyphId(codePoint, out _)) + { + produced = 1; + return 0; + } + + if (TryWriteDecomposition(shaper, fontMetrics, buffer, i, codePoint, out produced)) + { + return produced - 1; + } + + produced = 1; + return 0; + } + + /// + /// Writes the parts of a character over it, walking down the chain of pairs until + /// it reaches parts the font can draw. Nothing is written unless the whole chain + /// resolves, so a character the font cannot draw and cannot take apart is left as + /// it stands for the substitution passes to deal with. + /// + /// The shaper whose joining rules apply. + /// The font metrics. + /// The glyph shaping buffer. + /// The zero-based index of the record. + /// The character to take apart. + /// When this method returns, contains the number of records written. + /// when the character came apart. + private static bool TryWriteDecomposition(BaseShaper shaper, FontMetrics fontMetrics, ShapingBuffer buffer, int i, CodePoint codePoint, out int produced) + { + produced = 0; + + // The chain is gathered whole before anything is written, so a chain that + // turns out not to resolve leaves the record exactly as it was. + Span parts = stackalloc CodePoint[MaxDecompositionParts]; + Span glyphs = stackalloc ushort[MaxDecompositionParts]; + + int gathered = 0; + if (!TryGather(shaper, fontMetrics, codePoint, parts, glyphs, ref gathered)) + { + return false; + } + + // One part is a character standing for a single other one, so the record keeps + // its place and only changes what it is. + if (gathered == 1) + { + buffer.SetGlyphId(i, glyphs[0]); + buffer[i].CodePoint = parts[0]; + produced = 1; + return true; + } + + buffer.Replace(i, glyphs[..gathered], KnownFeatureTags.GlyphCompositionDecomposition); + for (int part = 0; part < gathered; part++) + { + buffer[i + part].CodePoint = parts[part]; + } + + produced = gathered; + return true; + } + + /// + /// Gathers the parts a character comes apart into, in the order they are written, + /// following the leading part down while the font cannot draw it. + /// + /// The shaper whose joining rules apply. + /// The font metrics. + /// The character to take apart. + /// The characters gathered so far. + /// The glyphs of those characters. + /// The number gathered so far, advanced as parts are added. + /// when the whole chain resolved to drawable parts. + private static bool TryGather(BaseShaper shaper, FontMetrics fontMetrics, CodePoint codePoint, Span parts, Span glyphs, ref int gathered) + { + if (!shaper.TryDecompose(codePoint, out CodePoint first, out CodePoint second)) + { + return false; + } + + // The trailing part has to be drawable for the pair to be usable at all, and + // there has to be room left to record it. + bool hasSecond = second.Value != 0; + ushort secondId = 0; + if (hasSecond && (!fontMetrics.TryGetGlyphId(second, out secondId) || gathered + 2 > parts.Length)) + { + return false; + } + + // A leading part the font can draw ends the walk. Otherwise the leading part + // comes apart in turn, and the trailing part follows whatever that yields. + if (fontMetrics.TryGetGlyphId(first, out ushort firstId)) + { + if (gathered + 1 > parts.Length) + { + return false; + } + + parts[gathered] = first; + glyphs[gathered] = firstId; + gathered++; + } + else if (!TryGather(shaper, fontMetrics, first, parts, glyphs, ref gathered)) + { + return false; + } + + if (hasSecond) + { + if (gathered + 1 > parts.Length) + { + return false; + } + + parts[gathered] = second; + glyphs[gathered] = secondId; + gathered++; + } + + return true; + } + + /// + /// Orders each run of marks by the class that places them, leaving the marks of + /// every script in the order they are drawn. + /// + /// The glyph shaping buffer. + /// The zero-based index of the run's first record. + /// The number of records in the run. + private static void OrderMarks(ShapingBuffer buffer, int index, int count) + { + int end = index + count; + for (int i = index; i < end; i++) + { + if (CodePoint.GetMarkOrderingClass(buffer[i].CodePoint) == 0) + { + continue; + } + + int runEnd = i + 1; + while (runEnd < end && CodePoint.GetMarkOrderingClass(buffer[runEnd].CodePoint) != 0) + { + runEnd++; + } + + if (runEnd - i <= MaxOrderedMarkRun) + { + buffer.Sort(i, runEnd, MarkOrder); + } + + i = runEnd; + } + } + + /// + /// Joins each mark onto the character it follows wherever the pair has a joined + /// form the font can draw. A mark only joins the character that starts its run, + /// and only when nothing between them outranks it, so the order settled by + /// is never broken. + /// + /// The shaper whose joining rules apply. + /// The font metrics. + /// The glyph shaping buffer. + /// The zero-based index of the run's first record. + /// The number of records in the run, updated as it changes. + private static void Compose(BaseShaper shaper, FontMetrics fontMetrics, ShapingBuffer buffer, int index, ref int count) + { + int end = index + count; + int starter = index; + + for (int i = index + 1; i < end; i++) + { + CodePoint codePoint = buffer[i].CodePoint; + + // A character that is not a mark never joins the character before it. + // Beyond sparing every neighbouring pair a lookup, this is what keeps a + // font's own syllables and the letters they are built from apart. + if (!CodePoint.IsMark(codePoint)) + { + if (CodePoint.GetMarkOrderingClass(codePoint) == 0) + { + starter = i; + } + + continue; + } + + int order = CodePoint.GetMarkOrderingClass(codePoint); + bool reachesStarter = starter == i - 1 + || CodePoint.GetMarkOrderingClass(buffer[i - 1].CodePoint) < order; + + if (reachesStarter + && shaper.TryCompose(buffer[starter].CodePoint, codePoint, out CodePoint composed) + && fontMetrics.TryGetGlyphId(composed, out ushort composedId)) + { + // The joined form takes the starter's place and carries the text of + // both. Only the mark goes: any marks standing between the two + // outrank it and keep both their place and their order. + buffer.MergeGlyph(starter, i, composedId, KnownFeatureTags.GlyphCompositionDecomposition); + buffer[starter].CodePoint = composed; + + end--; + i--; + continue; + } + + if (order == 0) + { + starter = i; + } + } + + count = end - index; + } +} diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ThaiShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ThaiShaper.cs index a94854a6d..b1ecd149e 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ThaiShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ThaiShaper.cs @@ -6,9 +6,13 @@ namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers; /// -/// Thai and Lao shaper. Handles SARA AM decomposition, NIKHAHIT/NIGGAHITA reordering, -/// and PUA-based fallback mark positioning for legacy fonts. -/// Based on HarfBuzz: +/// The shaper for Thai and Lao. +/// +/// SARA AM is decomposed into NIKHAHIT and SARA AA and the NIKHAHIT reordered +/// before the above-base marks it follows. A Thai font offering no features of +/// its own then has its marks positioned by substituting the private-use +/// characters a legacy font supplies for the purpose. +/// /// #pragma warning disable SA1201 // Nested types are grouped with the static data they define. internal class ThaiShaper : DefaultShaper @@ -212,21 +216,15 @@ public ThaiShaper(ScriptClass script, TextOptions textOptions, FontMetrics fontM } /// - protected override void AssignFeatures(ShapingBuffer buffer, int index, int count) + protected override void PreprocessText(ShapingBuffer buffer, int index, int count) { - base.AssignFeatures(buffer, index, count); - - if (buffer.Role != ShapingBufferRole.Substitution) - { - return; - } - - // Step 1: Always decompose SARA AM -> NIKHAHIT + SARA AA and reorder. - // This is needed even when the font has Thai/Lao GSUB tables. + // SARA AM is always decomposed into NIKHAHIT + SARA AA and reordered, + // which a font carrying Thai or Lao features needs just as much as one + // that carries none. count = PreprocessSaraAm(buffer, this.fontMetrics, index, count); - // Step 2: PUA-based fallback mark positioning. - // Only applied for Thai (not Lao) when the font lacks Thai GSUB features. + // The private-use fallback positions marks for a Thai font that offers no + // features to position them with. Lao has no such fallback. if (this.ScriptClass == ScriptClass.Thai && !this.hasGsub) { DoThaiPuaShaping(buffer, this.fontMetrics, index, count); diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs index 7ac62a35c..c3c4c7204 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs @@ -130,21 +130,6 @@ internal sealed class UniversalShaper : DefaultShaper /// private static readonly Tag PstsTag = Tag.Parse("psts"); - /// - /// The 'dist' (distances) feature tag. - /// - private static readonly Tag DistTag = Tag.Parse("dist"); - - /// - /// The 'abvm' (above-base mark positioning) feature tag. - /// - private static readonly Tag AbvmTag = Tag.Parse("abvm"); - - /// - /// The 'blwm' (below-base mark positioning) feature tag. - /// - private static readonly Tag BlwmTag = Tag.Parse("blwm"); - /// /// Dotted circle code point (U+25CC) used as a placeholder base. /// @@ -184,11 +169,34 @@ public UniversalShaper(ScriptClass script, TextOptions textOptions, FontMetrics this.fontMetrics = fontMetrics; this.setupSyllablesAction = this.SetupSyllables; this.reorderAction = this.Reorder; + + // Every character comes apart first, even one the font already draws whole. + // This shaper divides a run into syllables and moves their pieces about, so a + // character standing for several pieces must be split before the division can + // see them. + this.NormalizationMode = NormalizationMode.ComposedDiacriticsNoShortCircuit; } + /// + protected override void PreprocessText(ShapingBuffer buffer, int index, int count) + => VowelConstraints.Insert(buffer, this.fontMetrics, this.ScriptClass, index, count); + /// protected override void PlanFeatures(ShapingBuffer buffer, int index, int count) { + // A cursive script needs a mask for each form its characters can take, so + // the pass that settles them has somewhere to record its choice. + if (ArabicJoining.Joins(this.ScriptClass)) + { + this.AddFeature(buffer, index, count, ArabicJoining.IsolTag, ShapingFeatureFlags.ManualZwj, false, null, null); + this.AddFeature(buffer, index, count, ArabicJoining.FinaTag, ShapingFeatureFlags.ManualZwj, false, null, null); + this.AddFeature(buffer, index, count, ArabicJoining.Fin2Tag, ShapingFeatureFlags.ManualZwj, false, null, null); + this.AddFeature(buffer, index, count, ArabicJoining.Fin3Tag, ShapingFeatureFlags.ManualZwj, false, null, null); + this.AddFeature(buffer, index, count, ArabicJoining.MediTag, ShapingFeatureFlags.ManualZwj, false, null, null); + this.AddFeature(buffer, index, count, ArabicJoining.Med2Tag, ShapingFeatureFlags.ManualZwj, false, null, null); + this.AddFeature(buffer, index, count, ArabicJoining.InitTag, ShapingFeatureFlags.ManualZwj, false, null, null); + } + // Default glyph pre-processing group this.EnableFeature(buffer, index, count, LoclTag, ShapingFeatureFlags.PerSyllable, this.setupSyllablesAction, null); this.EnableFeature(buffer, index, count, CcmpTag, ShapingFeatureFlags.PerSyllable); @@ -215,14 +223,20 @@ protected override void PlanFeatures(ShapingBuffer buffer, int index, int count) this.EnableFeature(buffer, index, count, BlwsTag, ShapingFeatureFlags.ManualZwj); this.EnableFeature(buffer, index, count, PresTag, ShapingFeatureFlags.ManualZwj); this.EnableFeature(buffer, index, count, PstsTag, ShapingFeatureFlags.ManualZwj); - this.EnableFeature(buffer, index, count, DistTag); - this.EnableFeature(buffer, index, count, AbvmTag); - this.EnableFeature(buffer, index, count, BlwmTag); } /// protected override void AssignFeatures(ShapingBuffer buffer, int index, int count) - => this.DecomposeSplitVowels(buffer, index, count); + { + this.DecomposeSplitVowels(buffer, index, count); + + // Several of the scripts this engine shapes are cursive, and their + // characters take their form from the ones around them. + if (ArabicJoining.Joins(this.ScriptClass)) + { + ArabicJoining.Apply(buffer, index, count, this.ScriptClass, this.Features); + } + } /// /// Decomposes split vowels into their constituent parts if supported by the font. diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/VowelConstraints.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/VowelConstraints.cs new file mode 100644 index 000000000..ae6d6393f --- /dev/null +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/VowelConstraints.cs @@ -0,0 +1,82 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Unicode; +using SixLabors.Fonts.Unicode.Resources; + +namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers; + +/// +/// Separates the character sequences that spell one vowel but read as another. +/// The shapers whose scripts define such sequences run this over the text before +/// classifying it into syllables. +/// +internal static class VowelConstraints +{ + /// + /// The dotted circle, which stands between the characters of a sequence so + /// it cannot be mistaken for the vowel it imitates. + /// + private const int DottedCircle = 0x25CC; + + /// + /// Places a dotted circle before the final character of every constrained + /// sequence in the range. A sequence is consumed once recognised, so the + /// characters it spans cannot begin another. + /// + /// The glyph shaping buffer. + /// The font metrics used to resolve the dotted circle. + /// The script the segment is written in. + /// The zero-based index of the first record. + /// The number of records. + /// The number of dotted circles placed. + public static int Insert(ShapingBuffer buffer, FontMetrics fontMetrics, ScriptClass script, int index, int count) + { + if (!buffer.HasVowelConstraintCandidates || !VowelConstraintData.IsConstrainedScript(script)) + { + return 0; + } + + // The sequence is separated whether or not the font draws the circle; + // a font without one renders the missing glyph in its place. + if (!fontMetrics.TryGetGlyphId(new CodePoint(DottedCircle), out ushort circleId)) + { + circleId = 0; + } + + int inserted = 0; + int end = index + count; + int i = index; + while (i + 1 < end) + { + int first = buffer[i].CodePoint.Value; + int second = buffer[i + 1].CodePoint.Value; + + int length = 0; + if (i + 2 < end && VowelConstraintData.IsConstrainedTriple(script, first, second, buffer[i + 2].CodePoint.Value)) + { + length = 3; + } + else if (VowelConstraintData.IsConstrainedPair(script, first, second)) + { + length = 2; + } + + if (length == 0) + { + i++; + continue; + } + + buffer.InsertDottedCircle(i + length - 1, circleId); + inserted++; + end++; + + // The sequence and the circle now placed within it are behind the + // cursor, so the search resumes past all of them. + i += length + 1; + } + + return inserted; + } +} diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/SyllableTypeMap.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/SyllableTypeMap.cs index 4a0993446..dc187f74f 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/SyllableTypeMap.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/SyllableTypeMap.cs @@ -25,6 +25,7 @@ internal static class SyllableTypeMap "broken_cluster" => SyllableType.BrokenCluster, "consonant_syllable" => SyllableType.ConsonantSyllable, "independent_cluster" => SyllableType.IndependentCluster, + "non_khmer_cluster" => SyllableType.NonIndicCluster, "number_joiner_terminated_cluster" => SyllableType.NumberJoinerTerminatedCluster, "numeral_cluster" => SyllableType.NumeralCluster, "standalone_cluster" => SyllableType.StandaloneCluster, diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/UnicodeScriptTagMap.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/UnicodeScriptTagMap.cs index 3b12b0df2..145ef5801 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/UnicodeScriptTagMap.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/UnicodeScriptTagMap.cs @@ -51,7 +51,7 @@ private static UnicodeScriptTagMap CreateMap() { ScriptClass.Bamum, new[] { Tag.Parse("bamu") } }, { ScriptClass.BassaVah, new[] { Tag.Parse("bass") } }, { ScriptClass.Batak, new[] { Tag.Parse("batk") } }, - { ScriptClass.Bengali, new[] { Tag.Parse("bng2"), Tag.Parse("beng") } }, + { ScriptClass.Bengali, new[] { Tag.Parse("bng3"), Tag.Parse("bng2"), Tag.Parse("beng") } }, { ScriptClass.Bhaiksuki, new[] { Tag.Parse("bhks") } }, { ScriptClass.Bopomofo, new[] { Tag.Parse("bopo") } }, { ScriptClass.Brahmi, new[] { Tag.Parse("brah") } }, @@ -69,7 +69,7 @@ private static UnicodeScriptTagMap CreateMap() { ScriptClass.Cypriot, new[] { Tag.Parse("cprt") } }, { ScriptClass.Cyrillic, new[] { Tag.Parse("cyrl") } }, { ScriptClass.Default, new[] { Tag.Parse("DFLT"), Tag.Parse("dflt"), Tag.Parse("latn") } }, - { ScriptClass.Devanagari, new[] { Tag.Parse("dev2"), Tag.Parse("deva") } }, + { ScriptClass.Devanagari, new[] { Tag.Parse("dev3"), Tag.Parse("dev2"), Tag.Parse("deva") } }, { ScriptClass.DivesAkuru, new[] { Tag.Parse("diak") } }, { ScriptClass.Dogra, new[] { Tag.Parse("dogr") } }, { ScriptClass.Deseret, new[] { Tag.Parse("dsrt") } }, @@ -85,8 +85,8 @@ private static UnicodeScriptTagMap CreateMap() { ScriptClass.Gothic, new[] { Tag.Parse("goth") } }, { ScriptClass.Grantha, new[] { Tag.Parse("gran") } }, { ScriptClass.Greek, new[] { Tag.Parse("grek") } }, - { ScriptClass.Gujarati, new[] { Tag.Parse("gjr2"), Tag.Parse("gujr") } }, - { ScriptClass.Gurmukhi, new[] { Tag.Parse("gur2"), Tag.Parse("guru") } }, + { ScriptClass.Gujarati, new[] { Tag.Parse("gjr3"), Tag.Parse("gjr2"), Tag.Parse("gujr") } }, + { ScriptClass.Gurmukhi, new[] { Tag.Parse("gur3"), Tag.Parse("gur2"), Tag.Parse("guru") } }, { ScriptClass.Hangul, new[] { Tag.Parse("hang") } }, { ScriptClass.Han, new[] { Tag.Parse("hani") } }, { ScriptClass.Hanunoo, new[] { Tag.Parse("hano") } }, @@ -106,7 +106,7 @@ private static UnicodeScriptTagMap CreateMap() { ScriptClass.Khmer, new[] { Tag.Parse("khmr") } }, { ScriptClass.Khojki, new[] { Tag.Parse("khoj") } }, { ScriptClass.KhitanSmallScript, new[] { Tag.Parse("kits") } }, - { ScriptClass.Kannada, new[] { Tag.Parse("knd2"), Tag.Parse("knda") } }, + { ScriptClass.Kannada, new[] { Tag.Parse("knd3"), Tag.Parse("knd2"), Tag.Parse("knda") } }, { ScriptClass.Kaithi, new[] { Tag.Parse("kthi") } }, { ScriptClass.TaiTham, new[] { Tag.Parse("lana") } }, { ScriptClass.Lao, new[] { Tag.Parse("lao ") } }, @@ -127,7 +127,7 @@ private static UnicodeScriptTagMap CreateMap() { ScriptClass.MendeKikakui, new[] { Tag.Parse("mend") } }, { ScriptClass.MeroiticCursive, new[] { Tag.Parse("merc") } }, { ScriptClass.MeroiticHieroglyphs, new[] { Tag.Parse("mero") } }, - { ScriptClass.Malayalam, new[] { Tag.Parse("mlm2"), Tag.Parse("mlym") } }, + { ScriptClass.Malayalam, new[] { Tag.Parse("mlm3"), Tag.Parse("mlm2"), Tag.Parse("mlym") } }, { ScriptClass.Modi, new[] { Tag.Parse("modi") } }, { ScriptClass.Mongolian, new[] { Tag.Parse("mong") } }, { ScriptClass.Mro, new[] { Tag.Parse("mroo") } }, @@ -143,7 +143,7 @@ private static UnicodeScriptTagMap CreateMap() { ScriptClass.Ogham, new[] { Tag.Parse("ogam") } }, { ScriptClass.OlChiki, new[] { Tag.Parse("olck") } }, { ScriptClass.OldTurkic, new[] { Tag.Parse("orkh") } }, - { ScriptClass.Oriya, new[] { Tag.Parse("ory2"), Tag.Parse("orya") } }, + { ScriptClass.Oriya, new[] { Tag.Parse("ory3"), Tag.Parse("ory2"), Tag.Parse("orya") } }, { ScriptClass.Osage, new[] { Tag.Parse("osge") } }, { ScriptClass.Osmanya, new[] { Tag.Parse("osma") } }, { ScriptClass.OldUyghur, new[] { Tag.Parse("ougr") } }, @@ -179,10 +179,10 @@ private static UnicodeScriptTagMap CreateMap() { ScriptClass.Takri, new[] { Tag.Parse("takr") } }, { ScriptClass.TaiLe, new[] { Tag.Parse("tale") } }, { ScriptClass.NewTaiLue, new[] { Tag.Parse("talu") } }, - { ScriptClass.Tamil, new[] { Tag.Parse("tml2"), Tag.Parse("taml") } }, + { ScriptClass.Tamil, new[] { Tag.Parse("tml3"), Tag.Parse("tml2"), Tag.Parse("taml") } }, { ScriptClass.Tangut, new[] { Tag.Parse("tang") } }, { ScriptClass.TaiViet, new[] { Tag.Parse("tavt") } }, - { ScriptClass.Telugu, new[] { Tag.Parse("tel2"), Tag.Parse("telu") } }, + { ScriptClass.Telugu, new[] { Tag.Parse("tel3"), Tag.Parse("tel2"), Tag.Parse("telu") } }, { ScriptClass.Tifinagh, new[] { Tag.Parse("tfng") } }, { ScriptClass.Tagalog, new[] { Tag.Parse("tglg") } }, { ScriptClass.Thaana, new[] { Tag.Parse("thaa") } }, diff --git a/src/SixLabors.Fonts/TextLine.cs b/src/SixLabors.Fonts/TextLine.cs index e2ea7f1d8..b37ba8e4b 100644 --- a/src/SixLabors.Fonts/TextLine.cs +++ b/src/SixLabors.Fonts/TextLine.cs @@ -665,8 +665,9 @@ or LineBreakClass.Ambiguous /// When , marks the line so becomes a no-op /// (used for paragraph-final lines). /// - /// - /// When , moves decomposed grapheme advances to the final visual entry. + /// + /// When , moves each decomposed grapheme's advance onto the entry + /// that ends it once reordering has settled which entry that is. /// /// /// When , keeps ordinary trailing breaking whitespace in the finalized line. @@ -674,16 +675,16 @@ or LineBreakClass.Ambiguous /// This line, for fluent chaining. public TextLine Finalize( bool skipJustification = false, - bool normalizeDecomposedAdvances = false, + bool redistributeGraphemeAdvances = false, bool preserveTrailingBreakingWhitespace = false) { this.SkipJustification = skipJustification; this.RemoveTrailingBreakingWhitespace(preserveTrailingBreakingWhitespace); this.BidiReOrder(); - if (normalizeDecomposedAdvances) + if (redistributeGraphemeAdvances) { - this.NormalizeDecomposedAdvances(); + this.RedistributeGraphemeAdvances(); } RecalculateLineMetrics(this); @@ -691,9 +692,11 @@ public TextLine Finalize( } /// - /// Moves decomposed grapheme advances when bidi reordering moved the grapheme boundary marker. + /// Gathers each decomposed grapheme's advance onto the entry that ends it, which + /// reordering may have changed. This is advance bookkeeping and has nothing to do + /// with normalizing text. /// - private void NormalizeDecomposedAdvances() + private void RedistributeGraphemeAdvances() { int start = 0; while (start < this.data.Count) diff --git a/src/SixLabors.Fonts/TextShaper.Pipeline.cs b/src/SixLabors.Fonts/TextShaper.Pipeline.cs index c8173b3b8..e5dc84376 100644 --- a/src/SixLabors.Fonts/TextShaper.Pipeline.cs +++ b/src/SixLabors.Fonts/TextShaper.Pipeline.cs @@ -197,6 +197,11 @@ private static ShapingBuffer ShapeCore( ? (bidi.ResolveEmbeddingLevel(bidiData.Types) == 1 ? BidiCharacterType.RightToLeft : BidiCharacterType.LeftToRight) : (options.TextDirection == TextDirection.RightToLeft ? BidiCharacterType.RightToLeft : BidiCharacterType.LeftToRight); + // The way the whole run reads is settled here, including when the caller + // left it to the text to say, so the shaping API can hand the run back in + // the order it is read. + scratch.RunReadsRightToLeft = overrideType == BidiCharacterType.RightToLeft; + for (int i = 0; i < bidiData.Types.Length; i++) { // Bidi override is a higher-level protocol override: real text behaves as the requested @@ -716,7 +721,7 @@ private static void PopulateAndSubstitute( // as transparent and the hide stage replaces them at the end. substitutions.TryGetGlyphId(font.FontMetrics, current, next, out ushort glyphId, out skipNextCodePoint); - substitutions.AddGlyph(glyphId, current, (TextDirection)bidiRuns[bidiRunIndex].Direction, (ushort)textRunIndex, codePointIndex); + substitutions.AddGlyph(glyphId, current, (TextDirection)bidiRuns[bidiRunIndex].Direction, (ushort)textRunIndex, codePointIndex, graphemeIndex); codePointIndex++; graphemeCodePointIndex++; diff --git a/src/SixLabors.Fonts/TextShaper.cs b/src/SixLabors.Fonts/TextShaper.cs index eeea1bbb8..9ea1112a7 100644 --- a/src/SixLabors.Fonts/TextShaper.cs +++ b/src/SixLabors.Fonts/TextShaper.cs @@ -2,102 +2,81 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using SixLabors.Fonts.Tables.AdvancedTypographic; namespace SixLabors.Fonts; /// -/// Encapsulates logic for shaping text into a positioned glyph stream. +/// Encapsulates logic for shaping one run of text into a positioned glyph stream. /// /// /// -/// Shaping runs the pipeline text layout uses: bidi analysis, font and text run -/// itemization, fallback font resolution, and the font's substitution and positioning -/// features. The result is the glyph stream in logical (source) order, before line -/// breaking, visual reordering, or scaling. Advances and offsets are expressed in font -/// design units; see for the conversion to pixel units. +/// The text of a run and the properties it is shaped under are set on a +/// , the buffer is shaped against a font, and the +/// glyphs are read back from it. Shaping applies the font's substitution and +/// positioning features; it does not divide the text, break lines, or scale. /// /// -/// External text stacks that itemize runs themselves shape one run per call: set a -/// single with no -/// so unmapped codepoints produce the -/// font's missing glyph, and pre-resolve the direction with -/// and . -/// Shaping is context sensitive, so a caller shaping a slice of a larger paragraph -/// passes the containing text and keeps the glyphs whose -/// falls inside the slice. +/// A run reads one way throughout. A caller holding text of mixed direction +/// divides it into runs itself, shapes each of them, and places them against one +/// another once it knows where its lines break. +/// +/// +/// Advances and offsets are expressed in font design units; see +/// for the conversion to pixel units. /// /// public static partial class TextShaper { - /// - public static IReadOnlyList Shape(string text, TextOptions options) - { - Guard.NotNull(text, nameof(text)); - - return Shape(text.AsSpan(), options); - } - /// - /// Shapes the text into a positioned glyph stream using the supplied options. + /// Shapes the buffer's text against the font, replacing the buffer's glyphs. /// - /// The text to shape. - /// - /// The text options. Shaping honors the font selection members - /// (, , - /// ), and - /// , , - /// , , and - /// . Layout members such as - /// , , wrapping, and - /// alignment do not affect shaping. - /// - /// The shaped glyphs in logical order. - public static IReadOnlyList Shape(ReadOnlySpan text, TextOptions options) + /// The font to shape against. + /// The buffer holding the run, which receives the glyphs. + public static void Shape(Font font, TextShapingBuffer buffer) { - Guard.NotNull(options, nameof(options)); - - if (text.IsEmpty) - { - return []; - } - - TextShapingBuffer buffer = new(); - Shape(text, options, buffer); - return buffer.Glyphs.ToArray(); - } - - /// - public static void Shape(string text, TextOptions options, TextShapingBuffer buffer) - { - Guard.NotNull(text, nameof(text)); + Guard.NotNull(font, nameof(font)); + Guard.NotNull(buffer, nameof(buffer)); - Shape(text.AsSpan(), options, buffer); + Shape(font, buffer, []); } /// - /// Shapes the text into a positioned glyph stream, replacing the contents of the - /// supplied buffer. Reusing one buffer across calls keeps steady-state shaping - /// free of allocation; see - /// for the shaping semantics and honored options. + /// Shapes the buffer's text against the font with the given features turned on, + /// replacing the buffer's glyphs. /// - /// The text to shape. - /// The text options. - /// The buffer receiving the shaped glyphs in logical order. - public static void Shape(ReadOnlySpan text, TextOptions options, TextShapingBuffer buffer) + /// The font to shape against. + /// The buffer holding the run, which receives the glyphs. + /// The feature tags to turn on for the run. + public static void Shape(Font font, TextShapingBuffer buffer, Tag[] features) { - Guard.NotNull(options, nameof(options)); + Guard.NotNull(font, nameof(font)); Guard.NotNull(buffer, nameof(buffer)); + Guard.NotNull(features, nameof(features)); - if (text.IsEmpty) + if (buffer.Text.IsEmpty) { - buffer.Clear(); + buffer.Reserve(0); + buffer.Commit(0); return; } ShapingScratch scratch = ScratchPool.Get(); try { - ShapingBuffer shaped = ShapeCore(text, options, scratch, null); + TextOptions options = scratch.GetShapingOptions(font, buffer.Direction, buffer.Language, features); + ShapingBuffer shaped = ShapeCore(buffer.Text, options, scratch, null); + + // Shaping hands the run back in the order it is read, as the callers of + // a shaping API expect. A run that reads backwards is turned around whole, + // once, after positioning: every record moves, including the characters + // carrying no direction of their own such as the joiners. Turning the + // run's parts around separately would strand those where they were + // written, because they belong to no directional run. + if (scratch.RunReadsRightToLeft) + { + shaped.ReverseRange(0, shaped.Count); + } int count = shaped.Count; Span destination = buffer.Reserve(count); @@ -115,11 +94,8 @@ public static void Shape(ReadOnlySpan text, TextOptions options, TextShapi ref ShapingBuffer.GlyphMetricsEntry entry = ref shaped.MetricsAt(i); ref GlyphShapingPosition position = ref shaped.PositionAt(i); destination[written++] = new ShapedGlyph( - entry.Font, entry.Metrics.GlyphId, - shaping.CodePoint, shaping.CodePointIndex, - shaping.CodePointCount, entry.GetAdvanceWidth(in position), entry.GetAdvanceHeight(in position), new Vector2(position.Bounds.X, position.Bounds.Y) + entry.Metrics.Offset); diff --git a/src/SixLabors.Fonts/TextShapingBuffer.cs b/src/SixLabors.Fonts/TextShapingBuffer.cs index 08081e103..ea31c16d1 100644 --- a/src/SixLabors.Fonts/TextShapingBuffer.cs +++ b/src/SixLabors.Fonts/TextShapingBuffer.cs @@ -1,16 +1,22 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Globalization; + namespace SixLabors.Fonts; /// -/// A reusable, caller-owned buffer that receives the glyph stream of a shaping call. -/// Each call replaces the contents; storage grows to its high-water mark and is +/// A reusable, caller-owned buffer holding one run of text, the properties it is +/// shaped under, and the glyphs shaping produces. Text and properties are set on +/// the buffer, the buffer is shaped, and the glyphs are read back from it. Each +/// shaping call replaces the glyphs; storage grows to its high-water mark and is /// retained, so repeated shaping through one instance does not allocate. /// /// -/// An instance is not thread safe: use one buffer per shaping thread and reuse it -/// across calls. +/// A run reads one way throughout. Text of mixed direction is divided into runs by +/// the caller, which shapes each of them and places them against one another once +/// it knows where its lines break. An instance is not thread safe: use one buffer +/// per shaping thread and reuse it across calls. /// public sealed class TextShapingBuffer { @@ -20,13 +26,41 @@ public sealed class TextShapingBuffer /// private ShapedGlyph[] glyphs = []; + /// + /// The text storage, so a buffer that is refilled from a span does not allocate. + /// + private char[] text = []; + + /// + /// The number of characters of that are live. + /// + private int textLength; + + /// + /// Gets the text of the run. + /// + public ReadOnlySpan Text => this.text.AsSpan(0, this.textLength); + + /// + /// Gets or sets the direction the run reads in. Shaping takes the text as one + /// run reading this way, whichever way it would read on its own. + /// + public TextDirection Direction { get; set; } = TextDirection.LeftToRight; + + /// + /// Gets or sets the language the run is written in, which selects the language + /// specific behaviour of the font's features. + /// + public CultureInfo Language { get; set; } = CultureInfo.InvariantCulture; + /// /// Gets the number of shaped glyphs the last shaping call produced. /// public int Count { get; private set; } /// - /// Gets the shaped glyphs in logical order. + /// Gets the shaped glyphs, in the order the run is read: a caller walks them + /// forward and adds the advances up. /// public ReadOnlySpan Glyphs => this.glyphs.AsSpan(0, this.Count); @@ -44,14 +78,42 @@ public ref readonly ShapedGlyph this[int index] } /// - /// Removes all glyphs while retaining the storage. + /// Replaces the text of the run, discarding any glyphs already shaped. /// - public void Clear() => this.Count = 0; + /// The text of the run. + public void Add(ReadOnlySpan value) + { + if (this.text.Length < value.Length) + { + this.text = new char[Math.Max(value.Length, Math.Max(64, this.text.Length * 2))]; + } + + value.CopyTo(this.text); + this.textLength = value.Length; + this.Count = 0; + } + + /// + public void Add(string value) + { + Guard.NotNull(value, nameof(value)); + + this.Add(value.AsSpan()); + } + + /// + /// Removes the text and the glyphs while retaining the storage. + /// + public void Clear() + { + this.Count = 0; + this.textLength = 0; + } /// - /// Begins replacing the contents: empties the buffer, ensures capacity for the - /// given record count, and returns the writable storage. The written records - /// become visible when publishes their count. + /// Begins replacing the glyphs: empties them, ensures capacity for the given + /// record count, and returns the writable storage. The written records become + /// visible when publishes their count. /// /// The record capacity to reserve. /// The writable storage span. diff --git a/src/SixLabors.Fonts/Unicode/ArabicJoiningClass.cs b/src/SixLabors.Fonts/Unicode/ArabicJoiningClass.cs index 4802745bb..afe27b9be 100644 --- a/src/SixLabors.Fonts/Unicode/ArabicJoiningClass.cs +++ b/src/SixLabors.Fonts/Unicode/ArabicJoiningClass.cs @@ -26,7 +26,7 @@ public ArabicJoiningClass(CodePoint codePoint) { UnicodeCategory category = CodePoint.GetGeneralCategory(codePoint); uint value = UnicodeData.GetJoiningClass((uint)codePoint.Value); - this.JoiningType = GetJoiningType(codePoint, value, category); + this.JoiningType = GetJoiningType(value, category); this.JoiningGroup = (ArabicJoiningGroup)((value >> 16) & 0xFF); } @@ -41,38 +41,18 @@ public ArabicJoiningClass(CodePoint codePoint) public ArabicJoiningGroup JoiningGroup { get; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ArabicJoiningType GetJoiningType(CodePoint codePoint, uint value, UnicodeCategory category) + private static ArabicJoiningType GetJoiningType(uint value, UnicodeCategory category) { var type = (ArabicJoiningType)(value & 0xFF); - - // All others not explicitly listed have joining type U - if (type == ArabicJoiningType.NonJoining) + if (type != ArabicJoiningType.Unlisted) { - // 200C; ZERO WIDTH NON-JOINER; U; No_Joining_Group - // 200D; ZERO WIDTH JOINER; C; No_Joining_Group - // 202F; NARROW NO-BREAK SPACE; U; No_Joining_Group - // 2066; LEFT-TO-RIGHT ISOLATE; U; No_Joining_Group - // 2067; RIGHT-TO-LEFT ISOLATE; U; No_Joining_Group - // 2068; FIRST STRONG ISOLATE; U; No_Joining_Group - // 2069; POP DIRECTIONAL ISOLATE; U; No_Joining_Group - if (codePoint.Value is 0x200C - or 0x200D - or 0x202F - or 0x2066 - or 0x2067 - or 0x2068 - or 0x2069) - { - return type; - } - - // Those that are not explicitly listed and that are of General Category Mn, Me, or Cf have joining type T. - if (category is UnicodeCategory.NonSpacingMark or UnicodeCategory.EnclosingMark or UnicodeCategory.Format) - { - type = ArabicJoiningType.Transparent; - } + return type; } - return type; + // A character the joining data does not list is transparent when it is a + // mark or a format character, and does not join otherwise. + return category is UnicodeCategory.NonSpacingMark or UnicodeCategory.EnclosingMark or UnicodeCategory.Format + ? ArabicJoiningType.Transparent + : ArabicJoiningType.NonJoining; } } diff --git a/src/SixLabors.Fonts/Unicode/ArabicJoiningType.cs b/src/SixLabors.Fonts/Unicode/ArabicJoiningType.cs index 4e10d0cee..e7d0ce844 100644 --- a/src/SixLabors.Fonts/Unicode/ArabicJoiningType.cs +++ b/src/SixLabors.Fonts/Unicode/ArabicJoiningType.cs @@ -41,5 +41,13 @@ public enum ArabicJoiningType /// /// Transparent (T): ignored when determining the joining relationship of surrounding characters. /// - Transparent + Transparent, + + /// + /// Non_Joining_Unassigned (X): the character is not listed in the joining data. + /// Its behaviour follows from its general category, resolved where the joining + /// class is read: a mark or a format character is transparent, anything else + /// does not join. + /// + Unlisted } diff --git a/src/SixLabors.Fonts/Unicode/CodePoint.Normalization.cs b/src/SixLabors.Fonts/Unicode/CodePoint.Normalization.cs new file mode 100644 index 000000000..2d39a69e4 --- /dev/null +++ b/src/SixLabors.Fonts/Unicode/CodePoint.Normalization.cs @@ -0,0 +1,292 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; +using System.Runtime.CompilerServices; +using SixLabors.Fonts.Unicode.Resources; + +namespace SixLabors.Fonts.Unicode; + +/// +/// Canonical decomposition, canonical composition, and the combining classes that +/// order the marks between the two. +/// +/// Sources. The tables both searches read are generated from the Unicode Character +/// Database; see for which files and which +/// fields. The Hangul arithmetic that stands in for those tables is the Hangul +/// syllable composition and decomposition of UAX #15, and follows +/// _hb_ucd_decompose_hangul and _hb_ucd_compose_hangul in +/// hb-ucd.cc of HarfBuzz 14.2.1, including the order the two cases are tested +/// in. +/// +/// +public readonly partial struct CodePoint +{ + /// + /// The number of bits each scalar value occupies in a packed table entry. + /// + private const int NormalizationEntryShift = 21; + + /// + /// The mask of one scalar value within a packed table entry. + /// + private const ulong NormalizationEntryMask = (1UL << NormalizationEntryShift) - 1; + + /// + /// The number of bytes one packed table entry occupies. + /// + private const int NormalizationEntrySize = sizeof(ulong); + + /// + /// The first Hangul leading consonant. + /// + private const uint HangulLeadBase = 0x1100; + + /// + /// The first Hangul vowel. + /// + private const uint HangulVowelBase = 0x1161; + + /// + /// The Hangul trailing consonant base, one below the first trailing consonant + /// so that the zero offset stands for a syllable that has none. + /// + private const uint HangulTrailBase = 0x11A7; + + /// + /// The first Hangul syllable. + /// + private const uint HangulSyllableBase = 0xAC00; + + /// + /// The number of Hangul leading consonants. + /// + private const uint HangulLeadCount = 19; + + /// + /// The number of Hangul vowels. + /// + private const uint HangulVowelCount = 21; + + /// + /// The number of Hangul trailing consonant slots, counting the empty one that + /// stands for a syllable without a trailing consonant. + /// + private const uint HangulTrailCount = 28; + + /// + /// The number of syllables that share one leading consonant. + /// + private const uint HangulVowelTrailCount = HangulVowelCount * HangulTrailCount; + + /// + /// The number of Hangul syllables. + /// + private const uint HangulSyllableCount = HangulLeadCount * HangulVowelTrailCount; + + /// + /// Gets the canonical combining class of the given code point. + /// + /// The code point to evaluate. + /// The canonical combining class. + public static int GetCanonicalCombiningClass(CodePoint codePoint) + => UnicodeData.GetCanonicalCombiningClass(codePoint.value); + + /// + /// Gets the class that orders a mark against the marks around it. + /// + /// This is the canonical combining class with the classes of several scripts + /// renumbered, so that sorting by it leaves the marks of those scripts in the + /// order they are drawn rather than the order the standard assigns. Hebrew, + /// Arabic, Syriac, Telugu, Thai, Lao and Tibetan all order differently from + /// their assigned classes. + /// + /// + /// The code point to evaluate. + /// The ordering class. + public static int GetMarkOrderingClass(CodePoint codePoint) + => UnicodeData.GetMarkOrderingClass(codePoint.value); + + /// + /// Tries to take the given code point apart into the pair of code points it is + /// canonically equivalent to. + /// + /// The code point to take apart. + /// When this method returns, contains the first code point. + /// + /// When this method returns, contains the second code point, or the default when + /// the code point stands for a single other one. + /// + /// if the code point comes apart. + public static bool TryDecompose(CodePoint codePoint, out CodePoint first, out CodePoint second) + { + uint value = codePoint.value; + + // A Hangul syllable comes apart by arithmetic rather than by table, which is + // why the tables leave the eleven thousand of them out. The syllables are + // laid out so that a syllable's index counts, from the outside in, its + // leading consonant, then its vowel, then its trailing consonant, with the + // zero trailing slot standing for a syllable that has none. + // + // Subtracting the base leaves that index. An unsigned compare against the + // count then rejects everything below the base too, because a value below it + // wraps to a very large number, so one compare does the work of two. + uint syllable = value - HangulSyllableBase; + if (syllable < HangulSyllableCount) + { + uint trail = syllable % HangulTrailCount; + if (trail != 0) + { + // The syllable carries a trailing consonant, so it parts into the + // same syllable without one, reached by clearing the trailing slot, + // and that consonant on its own. + first = new CodePoint(HangulSyllableBase + (syllable - trail)); + second = new CodePoint(HangulTrailBase + trail); + } + else + { + // The syllable is a leading consonant and a vowel only, so it parts + // into the two of them. Dividing by the number of syllables that + // share a leading consonant gives which consonant; what remains, + // divided by the trailing slots, gives which vowel. + first = new CodePoint(HangulLeadBase + (syllable / HangulVowelTrailCount)); + second = new CodePoint(HangulVowelBase + ((syllable % HangulVowelTrailCount) / HangulTrailCount)); + } + + return true; + } + + // Every other character is looked up. The table is ordered by the character + // that decomposes, which the packing puts in the high bits, so the entries + // are searched as the plain integers they are. + ReadOnlySpan entries = NormalizationData.Decompositions; + int low = 0; + int high = (entries.Length / NormalizationEntrySize) - 1; + while (low <= high) + { + // The midpoint is computed on unsigned values so that a large table + // cannot overflow the sum into a negative index. + int middle = (int)(((uint)low + (uint)high) >> 1); + ulong entry = ReadEntry(entries, middle); + uint key = (uint)(entry >> (NormalizationEntryShift * 2)); + + if (value < key) + { + high = middle - 1; + } + else if (value > key) + { + low = middle + 1; + } + else + { + // The two parts sit in the lower lanes. A singleton decomposition + // stores zero in the last lane, which the caller reads as absent. + first = new CodePoint((uint)((entry >> NormalizationEntryShift) & NormalizationEntryMask)); + second = new CodePoint((uint)(entry & NormalizationEntryMask)); + return true; + } + } + + first = default; + second = default; + return false; + } + + /// + /// Tries to join the given pair of code points into the single code point they + /// are canonically equivalent to. + /// + /// The first code point. + /// The second code point. + /// When this method returns, contains the joined code point. + /// if the pair joins. + public static bool TryCompose(CodePoint first, CodePoint second, out CodePoint composed) + { + uint a = first.value; + uint b = second.value; + + // Hangul joins by the arithmetic that takes a syllable apart, run backwards, + // and in the same two cases. Each unsigned compare against a count also + // rejects everything below the base it subtracted, because a value below it + // wraps to a very large number. + + // A leading consonant followed by a vowel builds the syllable that has no + // trailing consonant, by laying the two out at their strides. + uint lead = a - HangulLeadBase; + uint vowel = b - HangulVowelBase; + if (lead < HangulLeadCount && vowel < HangulVowelCount) + { + composed = new CodePoint(HangulSyllableBase + (lead * HangulVowelTrailCount) + (vowel * HangulTrailCount)); + return true; + } + + // A syllable followed by a trailing consonant fills that syllable's empty + // trailing slot. The syllable must have an empty one, which is what a zero + // remainder against the trailing stride means; a syllable that already + // carries a trailing consonant takes no second one. + uint syllable = a - HangulSyllableBase; + uint trail = b - HangulTrailBase; + if (syllable < HangulSyllableCount && syllable % HangulTrailCount == 0 + && trail is > 0 and < HangulTrailCount) + { + composed = new CodePoint(a + trail); + return true; + } + + // Every other pair is looked up. The table is ordered by the pair, which the + // packing puts in the high lanes, so packing the pair the same way turns the + // two-value search into one integer comparison per step. + ulong sought = ((ulong)a << NormalizationEntryShift) | b; + ReadOnlySpan entries = NormalizationData.Compositions; + int low = 0; + int high = (entries.Length / NormalizationEntrySize) - 1; + while (low <= high) + { + // The midpoint is computed on unsigned values so that a large table + // cannot overflow the sum into a negative index. + int middle = (int)(((uint)low + (uint)high) >> 1); + ulong entry = ReadEntry(entries, middle); + + // Shifting the joined character out of the low lane leaves exactly the + // packed pair, which is the key being searched for. + ulong key = entry >> NormalizationEntryShift; + + if (sought < key) + { + high = middle - 1; + } + else if (sought > key) + { + low = middle + 1; + } + else + { + // A pair the table holds always joins: a pair that must not be put + // back together is left out of the table when it is generated, so + // nothing needs to be excluded here. + composed = new CodePoint((uint)(entry & NormalizationEntryMask)); + return true; + } + } + + composed = default; + return false; + } + + /// + /// Reads one packed entry from a normalization table. + /// + /// The tables are held as bytes because only a span of a one-byte type is a blob + /// in the assembly's read-only data; a span of a wider type would be a fresh + /// array on every access, allocating once per lookup. Reading the eight bytes back + /// is a single load on a little-endian machine. + /// + /// + /// The table. + /// The zero-based index of the entry. + /// The packed entry. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong ReadEntry(ReadOnlySpan entries, int index) + => BinaryPrimitives.ReadUInt64LittleEndian(entries[(index * NormalizationEntrySize)..]); +} diff --git a/src/SixLabors.Fonts/Unicode/CodePoint.cs b/src/SixLabors.Fonts/Unicode/CodePoint.cs index ed37a4347..a7f4cecf8 100644 --- a/src/SixLabors.Fonts/Unicode/CodePoint.cs +++ b/src/SixLabors.Fonts/Unicode/CodePoint.cs @@ -15,7 +15,7 @@ namespace SixLabors.Fonts.Unicode; /// assuming that the underlying instance is well-formed. /// [DebuggerDisplay("{DebuggerDisplay,nq}")] -public readonly struct CodePoint : IComparable, IComparable, IEquatable +public readonly partial struct CodePoint : IComparable, IComparable, IEquatable { // Supplementary plane code points are encoded as 2 UTF-16 code units private const int MaxUtf16CharsPerCodePoint = 2; diff --git a/src/SixLabors.Fonts/Unicode/Resources/ArabicShapingTrie.Generated.cs b/src/SixLabors.Fonts/Unicode/Resources/ArabicShapingTrie.Generated.cs index ae5974603..d5157c23b 100644 --- a/src/SixLabors.Fonts/Unicode/Resources/ArabicShapingTrie.Generated.cs +++ b/src/SixLabors.Fonts/Unicode/Resources/ArabicShapingTrie.Generated.cs @@ -10,151 +10,159 @@ internal static class ArabicShapingTrie { public static ReadOnlySpan Data => new byte[] { - 0, 240, 1, 0, 0, 0, 0, 0, 80, 56, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 72, 2, 0, 0, 80, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, - 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 56, 2, 0, 0, - 64, 2, 0, 0, 104, 2, 0, 0, 112, 2, 0, 0, 120, 2, 0, 0, 128, 2, 0, 0, 136, 2, 0, 0, 144, 2, 0, 0, 152, 2, 0, 0, 160, 2, 0, 0, 168, 2, 0, 0, 176, 2, 0, 0, 181, 2, 0, 0, 189, 2, 0, 0, 56, 2, 0, 0, 64, 2, 0, 0, 197, 2, 0, 0, 205, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 213, 2, 0, 0, 221, 2, 0, 0, 229, 2, 0, 0, 237, 2, 0, 0, 245, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 252, 2, 0, 0, 4, 3, 0, 0, 4, 3, 0, 0, 6, 3, 0, 0, 13, 3, 0, 0, - 19, 3, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 24, 3, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 96, 9, 0, 0, 96, 9, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, - 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 224, 8, 0, 0, 160, 9, 0, 0, 224, 9, 0, 0, 32, 10, 0, 0, 96, 10, 0, 0, 160, 10, 0, 0, 212, 10, 0, 0, 224, 8, 0, 0, 20, 11, 0, 0, 71, 0, 0, 0, 94, 8, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, - 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 71, 0, 0, 0, 157, 8, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 32, 3, 0, 0, 40, 3, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 48, 3, 0, 0, 54, 3, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 62, 3, 0, 0, 70, 3, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 78, 3, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 82, 3, 0, 0, 90, 3, 0, 0, 96, 3, 0, 0, 104, 3, 0, 0, 108, 3, 0, 0, 116, 3, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 4, 3, 0, 0, 4, 3, 0, 0, 124, 3, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, - 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 56, 2, 0, 0, 55, 2, 1, 0, 55, 2, 1, 0, 55, 2, 1, 0, - 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, - 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, - 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, - 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, - 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, - 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 240, 1, 0, 0, 0, 0, 0, 96, 59, 0, 0, 70, 2, 0, 0, 78, 2, 0, 0, 86, 2, 0, 0, 94, 2, 0, 0, 70, 2, 0, 0, 78, 2, 0, 0, 70, 2, 0, 0, 78, 2, 0, 0, 70, 2, 0, 0, 78, 2, 0, 0, 70, 2, 0, 0, 78, 2, 0, 0, 70, 2, 0, 0, 78, 2, 0, 0, 70, 2, 0, 0, 78, 2, 0, 0, 70, 2, 0, 0, 78, 2, 0, 0, 70, 2, 0, 0, 78, 2, 0, 0, 70, 2, 0, 0, 78, 2, 0, 0, + 70, 2, 0, 0, 78, 2, 0, 0, 70, 2, 0, 0, 78, 2, 0, 0, 70, 2, 0, 0, 78, 2, 0, 0, 70, 2, 0, 0, 78, 2, 0, 0, 70, 2, 0, 0, 78, 2, 0, 0, 70, 2, 0, 0, 78, 2, 0, 0, 70, 2, 0, 0, 78, 2, 0, 0, 70, 2, 0, 0, 78, 2, 0, 0, 70, 2, 0, 0, 78, 2, 0, 0, 70, 2, 0, 0, 78, 2, 0, 0, 70, 2, 0, 0, 78, 2, 0, 0, 70, 2, 0, 0, 78, 2, 0, 0, 70, 2, 0, 0, + 78, 2, 0, 0, 118, 2, 0, 0, 126, 2, 0, 0, 134, 2, 0, 0, 142, 2, 0, 0, 150, 2, 0, 0, 158, 2, 0, 0, 166, 2, 0, 0, 174, 2, 0, 0, 182, 2, 0, 0, 190, 2, 0, 0, 195, 2, 0, 0, 203, 2, 0, 0, 70, 2, 0, 0, 78, 2, 0, 0, 211, 2, 0, 0, 219, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 227, 2, 0, 0, 235, 2, 0, 0, 243, 2, 0, 0, 251, 2, 0, 0, 3, 3, 0, 0, 11, 3, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 18, 3, 0, 0, 26, 3, 0, 0, 26, 3, 0, 0, 28, 3, 0, 0, 36, 3, 0, 0, + 42, 3, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 47, 3, 0, 0, 52, 3, 0, 0, 70, 2, 0, 0, 59, 3, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 26, 3, 0, 0, + 67, 3, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 152, 9, 0, 0, 152, 9, 0, 0, 24, 9, 0, 0, 24, 9, 0, 0, 24, 9, 0, 0, 24, 9, 0, 0, 24, 9, 0, 0, 24, 9, 0, 0, 24, 9, 0, 0, 24, 9, 0, 0, 24, 9, 0, 0, 24, 9, 0, 0, 24, 9, 0, 0, 24, 9, 0, 0, 24, 9, 0, 0, 24, 9, 0, 0, 24, 9, 0, 0, + 24, 9, 0, 0, 24, 9, 0, 0, 24, 9, 0, 0, 24, 9, 0, 0, 24, 9, 0, 0, 24, 9, 0, 0, 24, 9, 0, 0, 216, 9, 0, 0, 24, 10, 0, 0, 88, 10, 0, 0, 152, 10, 0, 0, 216, 10, 0, 0, 12, 11, 0, 0, 24, 9, 0, 0, 76, 11, 0, 0, 72, 0, 0, 0, 94, 8, 0, 0, 157, 8, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, + 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 213, 8, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 75, 3, 0, 0, 83, 3, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 91, 3, 0, 0, 97, 3, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 105, 3, 0, 0, 113, 3, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 121, 3, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 125, 3, 0, 0, 132, 3, 0, 0, 138, 3, 0, 0, 146, 3, 0, 0, 150, 3, 0, 0, 158, 3, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 161, 3, 0, 0, 165, 3, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 26, 3, 0, 0, 26, 3, 0, 0, 173, 3, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, + 70, 2, 0, 0, 70, 2, 0, 0, 70, 2, 0, 0, 69, 2, 1, 0, 69, 2, 1, 0, 69, 2, 1, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, + 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, + 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, + 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, + 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, + 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, - 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 2, 0, 27, 0, - 4, 0, 72, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 98, 0, 0, 0, 5, 0, 2, 0, 99, 0, 0, 0, 5, 0, 2, 0, 6, 0, 0, 0, 92, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 18, 0, 2, 0, 18, 0, 2, 0, 18, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 79, 0, 0, 0, 79, 0, 2, 0, 84, 0, 2, 0, 84, 0, 2, 0, 82, 0, 2, 0, 82, 0, 2, 0, 90, 0, 2, 0, 90, 0, 2, 0, 3, 0, - 2, 0, 3, 0, 2, 0, 16, 0, 2, 0, 16, 0, 2, 0, 12, 0, 2, 0, 12, 0, 2, 0, 12, 0, 3, 0, 72, 0, 2, 0, 14, 0, 2, 0, 77, 0, 2, 0, 25, 0, 2, 0, 30, 0, 2, 0, 70, 0, 2, 0, 73, 0, 2, 0, 22, 0, 0, 0, 98, 0, 2, 0, 99, 0, 2, 0, 99, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, - 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, - 4, 0, 72, 0, 4, 0, 72, 0, 2, 0, 6, 0, 2, 0, 77, 0, 4, 0, 72, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 4, 0, 72, 0, 0, 0, 5, 0, 0, 0, 98, 0, 0, 0, 98, 0, 2, 0, 99, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 18, 0, 2, 0, 18, 0, 2, 0, 18, 0, 2, 0, 18, 0, - 2, 0, 18, 0, 2, 0, 18, 0, 2, 0, 18, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 2, 0, 84, 0, 2, 0, 84, 0, 2, 0, 84, 0, 2, 0, 82, 0, - 2, 0, 82, 0, 2, 0, 90, 0, 2, 0, 3, 0, 2, 0, 14, 0, 2, 0, 14, 0, 2, 0, 14, 0, 2, 0, 14, 0, 2, 0, 14, 0, 2, 0, 14, 0, 2, 0, 77, 0, 2, 0, 77, 0, 2, 0, 16, 0, 2, 0, 88, 0, 2, 0, 16, 0, 2, 0, 25, 0, 2, 0, 25, 0, 2, 0, 25, 0, 2, 0, 16, 0, 2, 0, 16, 0, 2, 0, 16, 0, 2, 0, 16, 0, 2, 0, 16, 0, 2, 0, 16, 0, 2, 0, 30, 0, 2, 0, 30, 0, - 2, 0, 30, 0, 2, 0, 30, 0, 2, 0, 73, 0, 2, 0, 73, 0, 2, 0, 73, 0, 2, 0, 73, 0, 2, 0, 75, 0, 2, 0, 29, 0, 2, 0, 18, 0, 0, 0, 92, 0, 2, 0, 23, 0, 2, 0, 23, 0, 0, 0, 93, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 2, 0, 12, 0, 0, 0, 101, 0, 2, 0, 12, 0, 0, 0, 98, 0, - 2, 0, 99, 0, 2, 0, 99, 0, 0, 0, 100, 0, 0, 0, 100, 0, 4, 0, 72, 0, 0, 0, 92, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, - 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 0, 0, 9, 0, 0, 0, 79, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 2, 0, 84, 0, 2, 0, 82, 0, 2, 0, 3, 0, 4, 0, 72, 0, 4, 0, 72, 0, 2, 0, 29, 0, 4, 0, 72, 0, 4, 0, 72, 0, - 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 5, 0, 72, 0, 0, 0, 4, 0, 4, 0, 72, 0, 2, 0, 7, 0, 2, 0, 17, 0, 2, 0, 17, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 21, 0, 0, 0, 89, 0, 0, 0, 104, 0, 2, 0, 24, 0, - 2, 0, 94, 0, 2, 0, 94, 0, 2, 0, 102, 0, 0, 0, 103, 0, 2, 0, 26, 0, 2, 0, 31, 0, 2, 0, 71, 0, 2, 0, 74, 0, 2, 0, 85, 0, 2, 0, 15, 0, 2, 0, 11, 0, 2, 0, 76, 0, 2, 0, 80, 0, 0, 0, 83, 0, 2, 0, 78, 0, 0, 0, 10, 0, 2, 0, 86, 0, 0, 0, 91, 0, 2, 0, 7, 0, 2, 0, 17, 0, 0, 0, 10, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, - 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 0, 0, 105, 0, 2, 0, 28, 0, 2, 0, 13, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 18, 0, 2, 0, 18, 0, - 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 79, 0, 2, 0, 84, 0, 2, 0, 3, 0, 2, 0, 3, 0, 2, 0, 3, 0, 2, 0, 14, 0, 2, 0, 14, 0, 2, 0, 16, 0, 2, 0, 16, 0, 2, 0, 16, 0, 2, 0, 70, 0, 2, 0, 70, 0, 2, 0, 73, 0, 2, 0, 73, 0, 2, 0, 73, 0, 2, 0, 30, 0, 0, 0, 79, 0, 0, 0, 79, 0, 2, 0, 84, 0, 2, 0, 18, 0, 2, 0, 18, 0, 2, 0, 84, 0, 0, 0, 79, 0, - 2, 0, 18, 0, 0, 0, 5, 0, 0, 0, 5, 0, 2, 0, 12, 0, 2, 0, 12, 0, 2, 0, 99, 0, 0, 0, 98, 0, 0, 0, 98, 0, 2, 0, 8, 0, 2, 0, 8, 0, 2, 0, 18, 0, 2, 0, 84, 0, 2, 0, 84, 0, 2, 0, 25, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, - 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, - 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 3, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, - 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, - 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 2, 0, 36, 0, 4, 0, 33, 0, 2, 0, 39, 0, 2, 0, 42, 0, 2, 0, 37, 0, 2, 0, 38, 0, 4, 0, 32, 0, 0, 0, 40, 0, 2, 0, 34, 0, 0, 0, 35, 0, 0, 0, 41, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, - 4, 0, 72, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 3, 0, 72, 0, 3, 0, 72, 0, 3, 0, 72, 0, 2, 0, 96, 0, 4, 0, 72, 0, - 4, 0, 72, 0, 2, 0, 73, 0, 2, 0, 18, 0, 2, 0, 90, 0, 2, 0, 90, 0, 2, 0, 16, 0, 0, 0, 97, 0, 2, 0, 73, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 2, 0, 6, 0, - 2, 0, 6, 0, 2, 0, 18, 0, 2, 0, 90, 0, 2, 0, 14, 0, 2, 0, 77, 0, 2, 0, 30, 0, 2, 0, 70, 0, 2, 0, 99, 0, 2, 0, 99, 0, 0, 0, 79, 0, 0, 0, 98, 0, 0, 0, 81, 0, 4, 0, 72, 0, 0, 0, 9, 0, 2, 0, 82, 0, 2, 0, 16, 0, 0, 0, 87, 0, 0, 0, 79, 0, 2, 0, 3, 0, 2, 0, 25, 0, 2, 0, 77, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, 0, 0, 79, 0, - 2, 0, 99, 0, 2, 0, 0, 0, 2, 0, 2, 0, 2, 0, 1, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 18, 0, 2, 0, 16, 0, 2, 0, 3, 0, 2, 0, 2, 0, 2, 0, 18, 0, 2, 0, 18, 0, 2, 0, 30, 0, 2, 0, 16, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, - 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 3, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, - 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, - 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, - 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 5, 0, 72, 0, 5, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, - 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, - 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 3, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, - 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 2, 0, 43, 0, 2, 0, 45, 0, 2, 0, 45, 0, 2, 0, 49, 0, 2, 0, 49, 0, 0, 0, 46, 0, 4, 0, 72, 0, 0, 0, 67, 0, 4, 0, 72, 0, 0, 0, 69, 0, 0, 0, 69, 0, 4, 0, 72, 0, 4, 0, 72, 0, 1, 0, 50, 0, 0, 0, 64, 0, 0, 0, 68, 0, 0, 0, 52, 0, 0, 0, 52, 0, 0, 0, 52, 0, 2, 0, 53, 0, 2, 0, 47, 0, 2, 0, 65, 0, - 2, 0, 54, 0, 1, 0, 55, 0, 2, 0, 61, 0, 2, 0, 44, 0, 2, 0, 44, 0, 2, 0, 57, 0, 2, 0, 57, 0, 0, 0, 60, 0, 2, 0, 58, 0, 2, 0, 58, 0, 2, 0, 58, 0, 0, 0, 59, 0, 4, 0, 72, 0, 4, 0, 72, 0, 0, 0, 62, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 2, 0, 56, 0, 2, 0, 48, 0, 2, 0, 63, 0, 2, 0, 66, 0, - 0, 0, 51, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, - 2, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, - 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 1, 0, 72, 0, 2, 0, 72, 0, - 2, 0, 20, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 20, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 19, 0, 2, 0, 72, 0, - 2, 0, 72, 0, 2, 0, 20, 0, 2, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 2, 0, 19, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, - 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 0, 0, 9, 0, 2, 0, 90, 0, 2, 0, 25, 0, 4, 0, 72, 0, 2, 0, 95, 0, 2, 0, 99, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, - 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, - 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, - 4, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, - 2, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, - 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, - 2, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, - 1, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, - 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 5, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, - 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 72, 0, 4, 0, 72, 0, + 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 4, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 4, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, + 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 2, 0, 27, 0, 4, 0, 72, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 98, 0, 0, 0, 5, 0, 2, 0, 99, 0, 0, 0, 5, 0, 2, 0, 6, 0, 0, 0, 92, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 18, 0, 2, 0, 18, 0, 2, 0, 18, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 79, 0, 0, 0, 79, 0, 2, 0, 84, 0, + 2, 0, 84, 0, 2, 0, 82, 0, 2, 0, 82, 0, 2, 0, 90, 0, 2, 0, 90, 0, 2, 0, 3, 0, 2, 0, 3, 0, 2, 0, 16, 0, 2, 0, 16, 0, 2, 0, 12, 0, 2, 0, 12, 0, 2, 0, 12, 0, 3, 0, 72, 0, 2, 0, 14, 0, 2, 0, 77, 0, 2, 0, 25, 0, 2, 0, 30, 0, 2, 0, 70, 0, 2, 0, 73, 0, 2, 0, 22, 0, 0, 0, 98, 0, 2, 0, 99, 0, 2, 0, 99, 0, 6, 0, 72, 0, 6, 0, 72, 0, + 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, + 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 2, 0, 6, 0, 2, 0, 77, 0, 6, 0, 72, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 4, 0, 72, 0, 0, 0, 5, 0, 0, 0, 98, 0, 0, 0, 98, 0, 2, 0, 99, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, + 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 18, 0, 2, 0, 18, 0, 2, 0, 18, 0, 2, 0, 18, 0, 2, 0, 18, 0, 2, 0, 18, 0, 2, 0, 18, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, 0, 0, 79, 0, + 0, 0, 79, 0, 0, 0, 79, 0, 2, 0, 84, 0, 2, 0, 84, 0, 2, 0, 84, 0, 2, 0, 82, 0, 2, 0, 82, 0, 2, 0, 90, 0, 2, 0, 3, 0, 2, 0, 14, 0, 2, 0, 14, 0, 2, 0, 14, 0, 2, 0, 14, 0, 2, 0, 14, 0, 2, 0, 14, 0, 2, 0, 77, 0, 2, 0, 77, 0, 2, 0, 16, 0, 2, 0, 88, 0, 2, 0, 16, 0, 2, 0, 25, 0, 2, 0, 25, 0, 2, 0, 25, 0, 2, 0, 16, 0, 2, 0, 16, 0, + 2, 0, 16, 0, 2, 0, 16, 0, 2, 0, 16, 0, 2, 0, 16, 0, 2, 0, 30, 0, 2, 0, 30, 0, 2, 0, 30, 0, 2, 0, 30, 0, 2, 0, 73, 0, 2, 0, 73, 0, 2, 0, 73, 0, 2, 0, 73, 0, 2, 0, 75, 0, 2, 0, 29, 0, 2, 0, 18, 0, 0, 0, 92, 0, 2, 0, 23, 0, 2, 0, 23, 0, 0, 0, 93, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, 0, 0, 98, 0, + 0, 0, 98, 0, 0, 0, 98, 0, 2, 0, 12, 0, 0, 0, 101, 0, 2, 0, 12, 0, 0, 0, 98, 0, 2, 0, 99, 0, 2, 0, 99, 0, 0, 0, 100, 0, 0, 0, 100, 0, 6, 0, 72, 0, 0, 0, 92, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 4, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, + 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 0, 0, 9, 0, 0, 0, 79, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 2, 0, 84, 0, 2, 0, 82, 0, + 2, 0, 3, 0, 6, 0, 72, 0, 6, 0, 72, 0, 2, 0, 29, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 5, 0, 72, 0, 0, 0, 4, 0, 6, 0, 72, 0, 2, 0, 7, 0, 2, 0, 17, 0, 2, 0, 17, 0, + 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 21, 0, 0, 0, 89, 0, 0, 0, 104, 0, 2, 0, 24, 0, 2, 0, 94, 0, 2, 0, 94, 0, 2, 0, 102, 0, 0, 0, 103, 0, 2, 0, 26, 0, 2, 0, 31, 0, 2, 0, 71, 0, 2, 0, 74, 0, 2, 0, 85, 0, 2, 0, 15, 0, 2, 0, 11, 0, 2, 0, 76, 0, 2, 0, 80, 0, 0, 0, 83, 0, 2, 0, 78, 0, 0, 0, 10, 0, 2, 0, 86, 0, 0, 0, 91, 0, 2, 0, 7, 0, + 2, 0, 17, 0, 0, 0, 10, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 0, 0, 105, 0, 2, 0, 28, 0, 2, 0, 13, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, + 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 18, 0, 2, 0, 18, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 79, 0, 2, 0, 84, 0, 2, 0, 3, 0, 2, 0, 3, 0, 2, 0, 3, 0, 2, 0, 14, 0, 2, 0, 14, 0, 2, 0, 16, 0, 2, 0, 16, 0, 2, 0, 16, 0, 2, 0, 70, 0, 2, 0, 70, 0, 2, 0, 73, 0, 2, 0, 73, 0, 2, 0, 73, 0, 2, 0, 30, 0, 0, 0, 79, 0, + 0, 0, 79, 0, 2, 0, 84, 0, 2, 0, 18, 0, 2, 0, 18, 0, 2, 0, 84, 0, 0, 0, 79, 0, 2, 0, 18, 0, 0, 0, 5, 0, 0, 0, 5, 0, 2, 0, 12, 0, 2, 0, 12, 0, 2, 0, 99, 0, 0, 0, 98, 0, 0, 0, 98, 0, 2, 0, 8, 0, 2, 0, 8, 0, 2, 0, 18, 0, 2, 0, 84, 0, 2, 0, 84, 0, 2, 0, 25, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, + 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, + 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, + 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 3, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, + 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 2, 0, 36, 0, 4, 0, 33, 0, 2, 0, 39, 0, 2, 0, 42, 0, 2, 0, 37, 0, 2, 0, 38, 0, 4, 0, 32, 0, 0, 0, 40, 0, 2, 0, 34, 0, + 0, 0, 35, 0, 0, 0, 41, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, + 0, 0, 5, 0, 3, 0, 72, 0, 3, 0, 72, 0, 3, 0, 72, 0, 2, 0, 96, 0, 4, 0, 72, 0, 4, 0, 72, 0, 2, 0, 73, 0, 2, 0, 18, 0, 2, 0, 90, 0, 2, 0, 90, 0, 2, 0, 16, 0, 0, 0, 97, 0, 2, 0, 73, 0, 4, 0, 72, 0, 4, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, + 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 18, 0, 2, 0, 90, 0, 2, 0, 14, 0, 2, 0, 77, 0, 2, 0, 30, 0, 2, 0, 70, 0, 2, 0, 99, 0, 2, 0, 99, 0, 0, 0, 79, 0, 0, 0, 98, 0, 0, 0, 81, 0, 4, 0, 72, 0, 0, 0, 9, 0, 2, 0, 82, 0, 2, 0, 16, 0, 0, 0, 87, 0, 0, 0, 79, 0, 2, 0, 3, 0, + 2, 0, 25, 0, 2, 0, 77, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, 0, 0, 79, 0, 2, 0, 99, 0, 2, 0, 0, 0, 2, 0, 2, 0, 2, 0, 1, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 6, 0, 2, 0, 18, 0, 2, 0, 16, 0, 2, 0, 3, 0, 2, 0, 2, 0, 2, 0, 18, 0, 2, 0, 18, 0, 2, 0, 30, 0, 2, 0, 16, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, + 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 4, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, + 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, + 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 3, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 4, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, + 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, + 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 5, 0, 72, 0, + 5, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, + 2, 0, 72, 0, 2, 0, 72, 0, 6, 0, 72, 0, 2, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, + 4, 0, 72, 0, 3, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 4, 0, 72, 0, 6, 0, 72, 0, + 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, + 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, + 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 1, 0, 72, 0, 4, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, + 2, 0, 43, 0, 2, 0, 45, 0, 2, 0, 45, 0, 2, 0, 49, 0, 2, 0, 49, 0, 0, 0, 46, 0, 4, 0, 72, 0, 0, 0, 67, 0, 4, 0, 72, 0, 0, 0, 69, 0, 0, 0, 69, 0, 4, 0, 72, 0, 4, 0, 72, 0, 1, 0, 50, 0, 0, 0, 64, 0, 0, 0, 68, 0, 0, 0, 52, 0, 0, 0, 52, 0, 0, 0, 52, 0, 2, 0, 53, 0, 2, 0, 47, 0, 2, 0, 65, 0, 2, 0, 54, 0, 1, 0, 55, 0, 2, 0, 61, 0, + 2, 0, 44, 0, 2, 0, 44, 0, 2, 0, 57, 0, 2, 0, 57, 0, 0, 0, 60, 0, 2, 0, 58, 0, 2, 0, 58, 0, 2, 0, 58, 0, 0, 0, 59, 0, 4, 0, 72, 0, 4, 0, 72, 0, 0, 0, 62, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 2, 0, 56, 0, 2, 0, 48, 0, 2, 0, 63, 0, 2, 0, 66, 0, 0, 0, 51, 0, 6, 0, 72, 0, 6, 0, 72, 0, + 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, + 2, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, + 0, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 1, 0, 72, 0, 2, 0, 72, 0, 2, 0, 20, 0, 2, 0, 72, 0, 2, 0, 72, 0, + 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 20, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 19, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 20, 0, 2, 0, 72, 0, + 2, 0, 19, 0, 2, 0, 72, 0, 2, 0, 19, 0, 2, 0, 72, 0, 0, 0, 72, 0, 2, 0, 19, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, + 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 0, 0, 9, 0, 2, 0, 90, 0, 2, 0, 25, 0, 6, 0, 72, 0, 2, 0, 95, 0, 2, 0, 99, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, + 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, + 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 6, 0, 72, 0, + 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, + 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, + 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, + 0, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, 2, 0, 72, 0, 0, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 4, 0, 72, 0, 0, 0, 72, 0, 2, 0, 72, 0, 1, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, + 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 4, 0, 72, 0, 6, 0, 72, 0, + 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 2, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, + 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 5, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, + 6, 0, 72, 0, 6, 0, 72, 0, 6, 0, 72, 0, }; } } diff --git a/src/SixLabors.Fonts/Unicode/Resources/CanonicalCombiningClassTrie.Generated.cs b/src/SixLabors.Fonts/Unicode/Resources/CanonicalCombiningClassTrie.Generated.cs new file mode 100644 index 000000000..3929e8047 --- /dev/null +++ b/src/SixLabors.Fonts/Unicode/Resources/CanonicalCombiningClassTrie.Generated.cs @@ -0,0 +1,270 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// +using System; + +namespace SixLabors.Fonts.Unicode.Resources +{ + internal static class CanonicalCombiningClassTrie + { + public static ReadOnlySpan Data => new byte[] + { + 0, 240, 1, 0, 0, 0, 0, 0, 64, 99, 0, 0, 168, 2, 0, 0, 176, 2, 0, 0, 184, 2, 0, 0, 192, 2, 0, 0, 168, 2, 0, 0, 176, 2, 0, 0, 168, 2, 0, 0, 176, 2, 0, 0, 168, 2, 0, 0, 176, 2, 0, 0, 168, 2, 0, 0, 176, 2, 0, 0, 168, 2, 0, 0, 176, 2, 0, 0, 168, 2, 0, 0, 176, 2, 0, 0, 168, 2, 0, 0, 176, 2, 0, 0, 168, 2, 0, 0, 176, 2, 0, 0, 168, 2, 0, 0, 176, 2, 0, 0, + 168, 2, 0, 0, 176, 2, 0, 0, 216, 2, 0, 0, 224, 2, 0, 0, 232, 2, 0, 0, 240, 2, 0, 0, 168, 2, 0, 0, 176, 2, 0, 0, 168, 2, 0, 0, 176, 2, 0, 0, 168, 2, 0, 0, 176, 2, 0, 0, 168, 2, 0, 0, 176, 2, 0, 0, 248, 2, 0, 0, 0, 3, 0, 0, 168, 2, 0, 0, 176, 2, 0, 0, 168, 2, 0, 0, 176, 2, 0, 0, 168, 2, 0, 0, 176, 2, 0, 0, 4, 3, 0, 0, 12, 3, 0, 0, 20, 3, 0, 0, + 28, 3, 0, 0, 32, 3, 0, 0, 40, 3, 0, 0, 46, 3, 0, 0, 54, 3, 0, 0, 168, 2, 0, 0, 176, 2, 0, 0, 59, 3, 0, 0, 67, 3, 0, 0, 71, 3, 0, 0, 79, 3, 0, 0, 87, 3, 0, 0, 95, 3, 0, 0, 168, 2, 0, 0, 176, 2, 0, 0, 93, 3, 0, 0, 101, 3, 0, 0, 109, 3, 0, 0, 116, 3, 0, 0, 120, 3, 0, 0, 168, 2, 0, 0, 127, 3, 0, 0, 168, 2, 0, 0, 135, 3, 0, 0, 143, 3, 0, 0, + 168, 2, 0, 0, 151, 3, 0, 0, 159, 3, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 151, 3, 0, 0, 165, 3, 0, 0, 169, 3, 0, 0, 168, 2, 0, 0, 151, 3, 0, 0, 165, 3, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 151, 3, 0, 0, 165, 3, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 151, 3, 0, 0, 165, 3, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 165, 3, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 151, 3, 0, 0, 177, 3, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 151, 3, 0, 0, 165, 3, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 183, 3, 0, 0, 165, 3, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 191, 3, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 194, 3, 0, 0, 201, 3, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 204, 3, 0, 0, 211, 3, 0, 0, 168, 2, 0, 0, 214, 3, 0, 0, 221, 3, 0, 0, + 168, 2, 0, 0, 228, 3, 0, 0, 236, 3, 0, 0, 168, 2, 0, 0, 243, 3, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 246, 3, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 253, 3, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 1, 4, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 9, 4, 0, 0, 15, 4, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 21, 4, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 29, 4, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 32, 4, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 39, 4, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 47, 4, 0, 0, 168, 2, 0, 0, 55, 4, 0, 0, 63, 4, 0, 0, 71, 4, 0, 0, 168, 2, 0, 0, 153, 3, 0, 0, 78, 4, 0, 0, 84, 4, 0, 0, 168, 2, 0, 0, 90, 4, 0, 0, + 168, 2, 0, 0, 97, 4, 0, 0, 168, 2, 0, 0, 102, 4, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 108, 4, 0, 0, 116, 4, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 124, 4, 0, 0, 129, 4, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 137, 4, 0, 0, 145, 4, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 150, 4, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 155, 4, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 163, 4, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 171, 4, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 175, 4, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 182, 4, 0, 0, 190, 4, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 198, 4, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 192, 3, 0, 0, 17, 4, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 78, 4, 0, 0, 206, 4, 0, 0, 168, 2, 0, 0, 212, 4, 0, 0, 216, 4, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 103, 4, 0, 0, 224, 4, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 228, 4, 0, 0, 236, 4, 0, 0, 239, 4, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 165, 3, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 245, 4, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 253, 4, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 32, 11, 0, 0, 32, 11, 0, 0, 160, 10, 0, 0, 160, 10, 0, 0, 160, 10, 0, 0, 160, 10, 0, 0, 160, 10, 0, 0, 160, 10, 0, 0, 160, 10, 0, 0, 160, 10, 0, 0, 160, 10, 0, 0, 160, 10, 0, 0, 96, 11, 0, 0, 160, 11, 0, 0, 160, 10, 0, 0, 160, 10, 0, 0, 160, 10, 0, 0, + 160, 10, 0, 0, 224, 11, 0, 0, 160, 10, 0, 0, 160, 10, 0, 0, 160, 10, 0, 0, 16, 12, 0, 0, 80, 12, 0, 0, 128, 12, 0, 0, 184, 12, 0, 0, 160, 10, 0, 0, 236, 12, 0, 0, 28, 13, 0, 0, 92, 13, 0, 0, 160, 10, 0, 0, 116, 13, 0, 0, 94, 8, 0, 0, 142, 8, 0, 0, 204, 8, 0, 0, 11, 9, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, + 8, 1, 0, 0, 8, 1, 0, 0, 70, 9, 0, 0, 111, 9, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 175, 9, 0, 0, 8, 1, 0, 0, 8, 1, 0, 0, 228, 9, 0, 0, 8, 1, 0, 0, 36, 10, 0, 0, 94, 10, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 1, 5, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 9, 5, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 12, 5, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 19, 5, 0, 0, 23, 5, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 31, 5, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 38, 5, 0, 0, 168, 2, 0, 0, 44, 5, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 50, 5, 0, 0, 168, 2, 0, 0, 54, 5, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 62, 5, 0, 0, 168, 2, 0, 0, 70, 5, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 192, 3, 0, 0, 74, 5, 0, 0, 168, 2, 0, 0, 82, 5, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 90, 5, 0, 0, 185, 3, 0, 0, 168, 2, 0, 0, 103, 4, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 98, 5, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 101, 5, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 107, 5, 0, 0, 168, 2, 0, 0, 110, 5, 0, 0, 165, 3, 0, 0, 118, 5, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 124, 5, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 132, 5, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 140, 5, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 155, 4, 0, 0, 148, 5, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 155, 4, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 151, 5, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 79, 5, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 82, 5, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 157, 5, 0, 0, 165, 5, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 224, 4, 0, 0, 168, 2, 0, 0, 15, 4, 0, 0, 80, 5, 0, 0, 168, 2, 0, 0, 167, 5, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 155, 4, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 175, 5, 0, 0, 168, 2, 0, 0, 178, 5, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 186, 5, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 191, 5, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 195, 5, 0, 0, 168, 2, 0, 0, 201, 5, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 207, 5, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 212, 5, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 220, 5, 0, 0, 228, 5, 0, 0, 234, 5, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 242, 5, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 250, 5, 0, 0, + 2, 6, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 7, 6, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 201, 5, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 12, 6, 0, 0, 168, 2, 0, 0, 36, 5, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 17, 6, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 22, 6, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 30, 6, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 36, 6, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 43, 6, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, + 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 168, 2, 0, 0, 167, 2, 1, 0, 167, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, + 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 232, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 232, 0, 0, 0, 216, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 202, 0, 0, 0, 202, 0, 0, 0, + 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 202, 0, 0, 0, 202, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, + 220, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 240, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, + 220, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 232, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 233, 0, 0, 0, 234, 0, 0, 0, 234, 0, 0, 0, 233, 0, 0, 0, 234, 0, 0, 0, 234, 0, 0, 0, 233, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, + 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, + 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 222, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, + 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 222, 0, 0, 0, 228, 0, 0, 0, 230, 0, 0, 0, 10, 0, 0, 0, 11, 0, 0, 0, 12, 0, 0, 0, 13, 0, 0, 0, 14, 0, 0, 0, 15, 0, 0, 0, 16, 0, 0, 0, 17, 0, 0, 0, 18, 0, 0, 0, 19, 0, 0, 0, 19, 0, 0, 0, + 20, 0, 0, 0, 21, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 25, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, + 230, 0, 0, 0, 230, 0, 0, 0, 30, 0, 0, 0, 31, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 30, 0, 0, 0, 31, 0, 0, 0, + 32, 0, 0, 0, 33, 0, 0, 0, 34, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, + 220, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 220, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, + 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, + 220, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 27, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, + 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 84, 0, 0, 0, 91, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 103, 0, 0, 0, 103, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, 107, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 118, 0, 0, 0, 118, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 122, 0, 0, 0, 122, 0, 0, 0, 122, 0, 0, 0, 122, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 220, 0, 0, 0, 0, 0, 0, 0, 220, 0, 0, 0, 0, 0, 0, 0, 216, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 0, 0, 0, 130, 0, 0, 0, 0, 0, 0, 0, 132, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 130, 0, 0, 0, 130, 0, 0, 0, 130, 0, 0, 0, 130, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 130, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, + 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 220, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 220, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 228, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 222, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 220, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 0, 0, 0, 0, + 220, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, + 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 234, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, + 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 220, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, + 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 234, 0, 0, 0, 214, 0, 0, 0, 220, 0, 0, 0, 202, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, + 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 232, 0, 0, 0, 228, 0, 0, 0, 228, 0, 0, 0, 220, 0, 0, 0, 218, 0, 0, 0, 230, 0, 0, 0, 233, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 230, 0, 0, 0, + 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 9, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, + 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 218, 0, 0, 0, 228, 0, 0, 0, 232, 0, 0, 0, 222, 0, 0, 0, 224, 0, 0, 0, 224, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, + 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, + 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 26, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, + 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 220, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 220, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 220, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 1, 0, 0, 0, 220, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 0, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 9, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 7, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, + 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 7, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 216, 0, 0, 0, 216, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 226, 0, 0, 0, 216, 0, 0, 0, 216, 0, 0, 0, 216, 0, 0, 0, 216, 0, 0, 0, 216, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, + 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 232, 0, 0, 0, 232, 0, 0, 0, 220, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, + 220, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 220, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, + 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 230, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + } +} diff --git a/src/SixLabors.Fonts/Unicode/Resources/IndicShapingData.Generated.cs b/src/SixLabors.Fonts/Unicode/Resources/IndicShapingData.Generated.cs index 1de555e15..729aa5d54 100644 --- a/src/SixLabors.Fonts/Unicode/Resources/IndicShapingData.Generated.cs +++ b/src/SixLabors.Fonts/Unicode/Resources/IndicShapingData.Generated.cs @@ -9,242 +9,169 @@ namespace SixLabors.Fonts.Unicode.Resources { internal static partial class IndicShapingData { - public static int[][] StateTable => new int[234][] + public static int[][] StateTable => new int[161][] { - new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, - new int[] { 0,2,3,4,5,6,7,8,9,10,11,11,12,8,13,14,15,16,17,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,9 }, - new int[] { 0,0,0,19,20,21,22,23,24,25,0,0,26,23,0,0,27,0,0,0,0,0,0,0,0,0,28,0,0,0,0,0,0,0,0,24 }, - new int[] { 0,0,0,29,30,31,32,33,34,35,0,0,36,33,0,0,37,0,0,0,0,0,0,0,0,0,38,0,0,0,0,0,0,0,0,34 }, - new int[] { 0,0,0,39,5,7,7,8,9,10,0,0,0,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,9 }, - new int[] { 0,40,0,0,0,41,42,0,43,10,0,0,0,0,0,40,0,0,0,0,0,0,0,0,0,0,44,0,0,0,0,0,0,0,0,43 }, - new int[] { 0,0,0,0,45,46,46,8,9,0,0,0,12,8,0,0,0,0,0,0,0,0,0,0,0,0,45,0,0,0,0,0,0,0,0,9 }, - new int[] { 0,0,0,0,45,46,46,8,9,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,45,0,0,0,0,0,0,0,0,9 }, - new int[] { 0,0,0,47,48,49,50,8,9,10,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,44,0,0,0,0,0,0,0,0,9 }, - new int[] { 0,0,0,0,0,10,0,0,51,10,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,51 }, - new int[] { 0,0,0,0,0,0,0,0,0,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, - new int[] { 0,0,0,52,53,54,55,56,57,58,0,0,59,56,0,0,60,0,0,0,0,0,0,0,0,0,61,0,0,0,0,0,0,0,0,57 }, - new int[] { 0,0,0,4,5,7,7,8,9,10,0,0,0,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,9 }, - new int[] { 0,2,3,4,5,6,7,8,9,10,11,11,12,8,0,2,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,9 }, - new int[] { 0,0,0,19,62,21,22,23,24,25,0,0,26,23,0,0,27,0,0,0,0,0,0,0,0,0,28,0,0,0,0,0,0,0,0,24 }, - new int[] { 0,0,0,0,63,64,64,8,9,10,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,65,0,0,0,0,0,0,0,0,9 }, - new int[] { 0,0,0,66,0,67,67,0,68,69,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,68 }, - new int[] { 0,2,0,0,0,0,0,0,0,0,11,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, - new int[] { 0,70,71,0,0,41,42,0,43,10,0,0,0,0,0,70,0,0,0,0,0,0,0,0,0,0,44,0,0,0,0,0,0,0,0,43 }, - new int[] { 0,0,0,72,20,73,73,23,24,25,0,0,0,23,0,0,27,0,0,0,0,0,0,0,0,0,28,0,0,0,0,0,0,0,0,24 }, - new int[] { 0,74,0,0,0,75,76,0,77,25,0,0,0,0,0,74,0,0,0,0,0,0,0,0,0,0,78,0,0,0,0,0,0,0,0,77 }, - new int[] { 0,0,0,0,79,80,80,23,24,0,0,0,26,23,0,0,0,0,0,0,0,0,0,0,0,0,79,0,0,0,0,0,0,0,0,24 }, - new int[] { 0,0,0,19,20,21,73,23,24,25,0,0,26,23,0,0,27,0,0,0,0,0,0,0,0,0,28,0,0,0,0,0,0,0,0,24 }, - new int[] { 0,0,0,81,82,83,84,23,24,25,0,0,0,23,0,0,0,0,0,0,0,0,0,0,0,0,78,0,0,0,0,0,0,0,0,24 }, - new int[] { 0,0,0,0,0,25,0,0,85,25,0,0,0,23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,85 }, - new int[] { 0,0,0,0,0,0,0,0,0,25,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, - new int[] { 0,0,0,19,20,73,73,23,24,25,0,0,0,23,0,0,27,0,0,0,0,0,0,0,0,0,28,0,0,0,0,0,0,0,0,24 }, - new int[] { 0,0,0,0,86,87,87,23,24,25,0,0,0,23,0,0,0,0,0,0,0,0,0,0,0,0,88,0,0,0,0,0,0,0,0,24 }, - new int[] { 0,89,90,0,0,75,76,0,77,25,0,0,0,0,0,89,0,0,0,0,0,0,0,0,0,0,78,0,0,0,0,0,0,0,0,77 }, - new int[] { 0,0,0,91,30,92,32,33,34,35,0,0,0,33,0,0,37,0,0,0,0,0,0,0,0,0,38,0,0,0,0,0,0,0,0,34 }, - new int[] { 0,93,0,0,0,94,95,0,96,35,0,0,0,0,0,93,0,0,0,0,0,0,0,0,0,0,97,0,0,0,0,0,0,0,0,96 }, - new int[] { 0,0,0,0,98,99,99,33,34,0,0,0,36,33,0,0,0,0,0,0,0,0,0,0,0,0,98,0,0,0,0,0,0,0,0,34 }, - new int[] { 0,0,0,0,98,99,99,33,34,0,0,0,0,33,0,0,0,0,0,0,0,0,0,0,0,0,98,0,0,0,0,0,0,0,0,34 }, - new int[] { 0,0,0,100,101,102,103,33,34,35,0,0,0,33,0,0,0,0,0,0,0,0,0,0,0,0,97,0,0,0,0,0,0,0,0,34 }, - new int[] { 0,0,0,0,0,35,0,0,104,35,0,0,0,33,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,104 }, - new int[] { 0,0,0,0,0,0,0,0,0,35,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, - new int[] { 0,0,0,29,30,92,32,33,34,35,0,0,0,33,0,0,37,0,0,0,0,0,0,0,0,0,38,0,0,0,0,0,0,0,0,34 }, - new int[] { 0,0,0,0,105,106,106,33,34,35,0,0,0,33,0,0,0,0,0,0,0,0,0,0,0,0,107,0,0,0,0,0,0,0,0,34 }, - new int[] { 0,108,109,0,0,94,95,0,96,35,0,0,0,0,0,108,0,0,0,0,0,0,0,0,0,0,97,0,0,0,0,0,0,0,0,96 }, - new int[] { 0,0,0,0,5,7,7,8,9,10,0,0,0,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,9 }, - new int[] { 0,0,0,110,5,111,112,8,9,10,0,0,113,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,9 }, - new int[] { 0,0,0,0,0,114,114,0,43,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,44,0,0,0,0,0,0,0,0,43 }, - new int[] { 0,40,0,115,0,114,114,0,43,10,0,0,0,0,0,40,0,0,0,0,0,0,0,0,0,0,44,0,0,0,0,0,0,0,0,43 }, - new int[] { 0,0,0,0,0,10,0,0,51,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,51 }, - new int[] { 0,116,71,0,0,0,0,0,0,0,0,0,0,0,0,116,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, - new int[] { 0,40,0,0,0,114,42,0,43,10,0,0,0,0,0,40,0,0,0,0,0,0,0,0,0,0,44,0,0,0,0,0,0,0,0,43 }, - new int[] { 0,0,0,0,0,46,46,8,117,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,117 }, - new int[] { 0,0,0,0,48,49,50,8,9,10,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,44,0,0,0,0,0,0,0,0,9 }, - new int[] { 0,0,0,0,0,49,49,8,9,10,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,44,0,0,0,0,0,0,0,0,9 }, - new int[] { 0,0,0,0,0,46,46,8,9,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9 }, - new int[] { 0,0,0,0,118,46,46,8,9,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9 }, - new int[] { 0,0,0,0,0,10,0,0,0,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, - new int[] { 0,0,0,119,53,55,55,56,57,58,0,0,0,56,0,0,60,0,0,0,0,0,0,0,0,0,61,0,0,0,0,0,0,0,0,57 }, - new int[] { 0,120,0,0,0,121,122,0,123,58,0,0,0,0,0,120,0,0,0,0,0,0,0,0,0,0,124,0,0,0,0,0,0,0,0,123 }, - new int[] { 0,0,0,0,125,126,126,56,57,0,0,0,59,56,0,0,0,0,0,0,0,0,0,0,0,0,125,0,0,0,0,0,0,0,0,57 }, - new int[] { 0,0,0,0,125,126,126,56,57,0,0,0,0,56,0,0,0,0,0,0,0,0,0,0,0,0,125,0,0,0,0,0,0,0,0,57 }, - new int[] { 0,0,0,127,128,129,130,56,57,58,0,0,0,56,0,0,0,0,0,0,0,0,0,0,0,0,124,0,0,0,0,0,0,0,0,57 }, - new int[] { 0,0,0,0,0,58,0,0,131,58,0,0,0,56,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,131 }, - new int[] { 0,0,0,0,0,0,0,0,0,58,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, - new int[] { 0,0,0,52,53,55,55,56,57,58,0,0,0,56,0,0,60,0,0,0,0,0,0,0,0,0,61,0,0,0,0,0,0,0,0,57 }, - new int[] { 0,0,0,0,132,133,133,56,57,58,0,0,0,56,0,0,0,0,0,0,0,0,0,0,0,0,134,0,0,0,0,0,0,0,0,57 }, - new int[] { 0,135,136,0,0,121,122,0,123,58,0,0,0,0,0,135,0,0,0,0,0,0,0,0,0,0,124,0,0,0,0,0,0,0,0,123 }, - new int[] { 0,74,3,4,5,137,138,8,139,140,0,11,12,8,0,74,15,0,0,0,0,0,0,0,0,0,141,0,0,0,0,0,0,0,0,139 }, - new int[] { 0,0,0,0,0,41,142,0,43,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,44,0,0,0,0,0,0,0,0,43 }, - new int[] { 0,0,0,0,143,46,46,8,9,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,143,0,0,0,0,0,0,0,0,9 }, - new int[] { 0,116,71,0,0,41,142,0,43,10,0,0,0,0,0,116,0,0,0,0,0,0,0,0,0,0,44,0,0,0,0,0,0,0,0,43 }, - new int[] { 0,0,0,0,0,67,67,0,68,69,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,68 }, - new int[] { 0,0,0,0,0,0,0,0,68,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,68 }, - new int[] { 0,0,0,0,0,69,0,0,144,69,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,144 }, - new int[] { 0,0,0,0,0,0,0,0,0,69,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, - new int[] { 0,0,0,145,5,146,147,8,9,10,0,0,148,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,9 }, - new int[] { 0,0,0,0,0,114,114,0,43,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,43 }, - new int[] { 0,0,0,0,20,73,73,23,24,25,0,0,0,23,0,0,27,0,0,0,0,0,0,0,0,0,28,0,0,0,0,0,0,0,0,24 }, - new int[] { 0,0,0,0,79,80,80,23,24,0,0,0,0,23,0,0,0,0,0,0,0,0,0,0,0,0,79,0,0,0,0,0,0,0,0,24 }, - new int[] { 0,0,0,149,20,150,151,23,24,25,0,0,152,23,0,0,27,0,0,0,0,0,0,0,0,0,28,0,0,0,0,0,0,0,0,24 }, - new int[] { 0,0,0,0,0,153,153,0,77,25,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,78,0,0,0,0,0,0,0,0,77 }, - new int[] { 0,74,0,154,0,153,153,0,77,25,0,0,0,0,0,74,0,0,0,0,0,0,0,0,0,0,78,0,0,0,0,0,0,0,0,77 }, - new int[] { 0,0,0,0,0,25,0,0,85,25,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,85 }, - new int[] { 0,155,90,0,0,0,0,0,0,0,0,0,0,0,0,155,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, - new int[] { 0,74,0,0,0,153,76,0,77,25,0,0,0,0,0,74,0,0,0,0,0,0,0,0,0,0,78,0,0,0,0,0,0,0,0,77 }, - new int[] { 0,0,0,0,0,80,80,23,156,0,0,0,0,23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,156 }, - new int[] { 0,0,0,0,82,83,84,23,24,25,0,0,0,23,0,0,0,0,0,0,0,0,0,0,0,0,78,0,0,0,0,0,0,0,0,24 }, - new int[] { 0,0,0,0,0,83,83,23,24,25,0,0,0,23,0,0,0,0,0,0,0,0,0,0,0,0,78,0,0,0,0,0,0,0,0,24 }, - new int[] { 0,0,0,0,0,80,80,23,24,0,0,0,0,23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24 }, - new int[] { 0,0,0,0,157,80,80,23,24,0,0,0,0,23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24 }, - new int[] { 0,0,0,0,0,25,0,0,0,25,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, - new int[] { 0,0,0,0,0,75,158,0,77,25,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,78,0,0,0,0,0,0,0,0,77 }, - new int[] { 0,0,0,0,159,80,80,23,24,0,0,0,0,23,0,0,0,0,0,0,0,0,0,0,0,0,159,0,0,0,0,0,0,0,0,24 }, - new int[] { 0,155,90,0,0,75,158,0,77,25,0,0,0,0,0,155,0,0,0,0,0,0,0,0,0,0,78,0,0,0,0,0,0,0,0,77 }, - new int[] { 0,0,0,160,20,161,162,23,24,25,0,0,163,23,0,0,27,0,0,0,0,0,0,0,0,0,28,0,0,0,0,0,0,0,0,24 }, - new int[] { 0,0,0,0,0,153,153,0,77,25,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,77 }, - new int[] { 0,0,0,0,30,92,32,33,34,35,0,0,0,33,0,0,37,0,0,0,0,0,0,0,0,0,38,0,0,0,0,0,0,0,0,34 }, - new int[] { 0,0,0,0,98,99,99,33,34,0,0,0,0,33,0,0,0,0,0,0,0,0,0,0,0,0,98,0,0,0,0,0,0,0,0,34 }, - new int[] { 0,0,0,164,30,165,166,33,34,35,0,0,167,33,0,0,37,0,0,0,0,0,0,0,0,0,38,0,0,0,0,0,0,0,0,34 }, - new int[] { 0,0,0,0,0,168,168,0,96,35,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,97,0,0,0,0,0,0,0,0,96 }, - new int[] { 0,93,0,169,0,168,168,0,96,35,0,0,0,0,0,93,0,0,0,0,0,0,0,0,0,0,97,0,0,0,0,0,0,0,0,96 }, - new int[] { 0,0,0,0,0,35,0,0,104,35,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,104 }, - new int[] { 0,170,109,0,0,0,0,0,0,0,0,0,0,0,0,170,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, - new int[] { 0,93,0,0,0,168,95,0,96,35,0,0,0,0,0,93,0,0,0,0,0,0,0,0,0,0,97,0,0,0,0,0,0,0,0,96 }, - new int[] { 0,0,0,0,0,99,99,33,171,0,0,0,0,33,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,171 }, - new int[] { 0,0,0,0,101,102,103,33,34,35,0,0,0,33,0,0,0,0,0,0,0,0,0,0,0,0,97,0,0,0,0,0,0,0,0,34 }, - new int[] { 0,0,0,0,0,102,102,33,34,35,0,0,0,33,0,0,0,0,0,0,0,0,0,0,0,0,97,0,0,0,0,0,0,0,0,34 }, - new int[] { 0,0,0,0,0,99,99,33,34,0,0,0,0,33,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,34 }, - new int[] { 0,0,0,0,172,99,99,33,34,0,0,0,0,33,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,34 }, - new int[] { 0,0,0,0,0,35,0,0,0,35,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, - new int[] { 0,0,0,0,0,94,173,0,96,35,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,97,0,0,0,0,0,0,0,0,96 }, - new int[] { 0,0,0,0,174,99,99,33,34,0,0,0,0,33,0,0,0,0,0,0,0,0,0,0,0,0,174,0,0,0,0,0,0,0,0,34 }, - new int[] { 0,170,109,0,0,94,173,0,96,35,0,0,0,0,0,170,0,0,0,0,0,0,0,0,0,0,97,0,0,0,0,0,0,0,0,96 }, - new int[] { 0,0,0,175,30,176,177,33,34,35,0,0,178,33,0,0,37,0,0,0,0,0,0,0,0,0,38,0,0,0,0,0,0,0,0,34 }, - new int[] { 0,0,0,0,0,168,168,0,96,35,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,96 }, - new int[] { 0,0,0,39,5,7,7,8,9,10,0,0,0,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,9 }, - new int[] { 0,0,0,0,45,46,46,8,9,0,0,0,113,8,0,0,0,0,0,0,0,0,0,0,0,0,45,0,0,0,0,0,0,0,0,9 }, - new int[] { 0,0,0,110,5,111,7,8,9,10,0,0,113,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,9 }, - new int[] { 0,0,0,110,5,7,7,8,9,10,0,0,0,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,9 }, - new int[] { 0,0,0,0,0,0,0,0,43,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,43 }, - new int[] { 0,40,0,0,0,114,114,0,43,10,0,0,0,0,0,40,0,0,0,0,0,0,0,0,0,0,44,0,0,0,0,0,0,0,0,43 }, - new int[] { 0,0,0,179,0,180,181,0,43,10,0,0,182,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,43 }, - new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, - new int[] { 0,0,0,0,0,0,183,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, - new int[] { 0,0,0,0,53,55,55,56,57,58,0,0,0,56,0,0,60,0,0,0,0,0,0,0,0,0,61,0,0,0,0,0,0,0,0,57 }, - new int[] { 0,0,0,184,53,185,186,56,57,58,0,0,187,56,0,0,60,0,0,0,0,0,0,0,0,0,61,0,0,0,0,0,0,0,0,57 }, - new int[] { 0,0,0,0,0,188,188,0,123,58,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,124,0,0,0,0,0,0,0,0,123 }, - new int[] { 0,120,0,189,0,188,188,0,123,58,0,0,0,0,0,120,0,0,0,0,0,0,0,0,0,0,124,0,0,0,0,0,0,0,0,123 }, - new int[] { 0,0,0,0,0,58,0,0,131,58,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,131 }, - new int[] { 0,190,136,0,0,0,0,0,0,0,0,0,0,0,0,190,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, - new int[] { 0,120,0,0,0,188,122,0,123,58,0,0,0,0,0,120,0,0,0,0,0,0,0,0,0,0,124,0,0,0,0,0,0,0,0,123 }, - new int[] { 0,0,0,0,0,126,126,56,191,0,0,0,0,56,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,191 }, - new int[] { 0,0,0,0,128,129,130,56,57,58,0,0,0,56,0,0,0,0,0,0,0,0,0,0,0,0,124,0,0,0,0,0,0,0,0,57 }, - new int[] { 0,0,0,0,0,129,129,56,57,58,0,0,0,56,0,0,0,0,0,0,0,0,0,0,0,0,124,0,0,0,0,0,0,0,0,57 }, - new int[] { 0,0,0,0,0,126,126,56,57,0,0,0,0,56,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,57 }, - new int[] { 0,0,0,0,192,126,126,56,57,0,0,0,0,56,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,57 }, - new int[] { 0,0,0,0,0,58,0,0,0,58,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, - new int[] { 0,0,0,0,0,121,193,0,123,58,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,124,0,0,0,0,0,0,0,0,123 }, - new int[] { 0,0,0,0,194,126,126,56,57,0,0,0,0,56,0,0,0,0,0,0,0,0,0,0,0,0,194,0,0,0,0,0,0,0,0,57 }, - new int[] { 0,190,136,0,0,121,193,0,123,58,0,0,0,0,0,190,0,0,0,0,0,0,0,0,0,0,124,0,0,0,0,0,0,0,0,123 }, - new int[] { 0,0,0,195,53,196,197,56,57,58,0,0,198,56,0,0,60,0,0,0,0,0,0,0,0,0,61,0,0,0,0,0,0,0,0,57 }, - new int[] { 0,0,0,0,0,188,188,0,123,58,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,123 }, - new int[] { 0,0,0,0,45,199,199,8,139,25,0,0,12,8,0,0,0,0,0,0,0,0,0,0,0,0,200,0,0,0,0,0,0,0,0,139 }, - new int[] { 0,74,0,154,45,199,199,8,139,25,0,0,0,8,0,74,0,0,0,0,0,0,0,0,0,0,200,0,0,0,0,0,0,0,0,139 }, - new int[] { 0,0,0,0,0,140,0,0,201,140,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,201 }, - new int[] { 0,0,0,0,0,0,0,0,0,140,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, - new int[] { 0,202,203,0,0,41,42,0,43,10,0,0,0,0,0,202,0,0,0,0,0,0,0,0,0,0,44,0,0,0,0,0,0,0,0,43 }, - new int[] { 0,0,0,41,0,114,114,0,43,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,44,0,0,0,0,0,0,0,0,43 }, - new int[] { 0,0,0,0,0,114,142,0,43,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,44,0,0,0,0,0,0,0,0,43 }, - new int[] { 0,0,0,0,0,69,0,0,0,69,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, - new int[] { 0,0,0,39,5,7,7,8,9,10,0,0,0,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,9 }, - new int[] { 0,0,0,0,45,46,46,8,9,0,0,0,148,8,0,0,0,0,0,0,0,0,0,0,0,0,45,0,0,0,0,0,0,0,0,9 }, - new int[] { 0,0,0,145,5,146,7,8,9,10,0,0,148,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,9 }, - new int[] { 0,0,0,145,5,7,7,8,9,10,0,0,0,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,9 }, - new int[] { 0,0,0,72,20,73,73,23,24,25,0,0,0,23,0,0,27,0,0,0,0,0,0,0,0,0,28,0,0,0,0,0,0,0,0,24 }, - new int[] { 0,0,0,0,79,80,80,23,24,0,0,0,152,23,0,0,0,0,0,0,0,0,0,0,0,0,79,0,0,0,0,0,0,0,0,24 }, - new int[] { 0,0,0,149,20,150,73,23,24,25,0,0,152,23,0,0,27,0,0,0,0,0,0,0,0,0,28,0,0,0,0,0,0,0,0,24 }, - new int[] { 0,0,0,149,20,73,73,23,24,25,0,0,0,23,0,0,27,0,0,0,0,0,0,0,0,0,28,0,0,0,0,0,0,0,0,24 }, - new int[] { 0,0,0,0,0,0,0,0,77,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,77 }, - new int[] { 0,74,0,0,0,153,153,0,77,25,0,0,0,0,0,74,0,0,0,0,0,0,0,0,0,0,78,0,0,0,0,0,0,0,0,77 }, - new int[] { 0,0,0,204,0,205,206,0,77,25,0,0,207,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,77 }, - new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, - new int[] { 0,0,0,0,0,0,208,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, - new int[] { 0,0,0,75,0,153,153,0,77,25,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,78,0,0,0,0,0,0,0,0,77 }, - new int[] { 0,0,0,0,0,153,158,0,77,25,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,78,0,0,0,0,0,0,0,0,77 }, - new int[] { 0,0,0,72,20,73,73,23,24,25,0,0,0,23,0,0,27,0,0,0,0,0,0,0,0,0,28,0,0,0,0,0,0,0,0,24 }, - new int[] { 0,0,0,0,79,80,80,23,24,0,0,0,163,23,0,0,0,0,0,0,0,0,0,0,0,0,79,0,0,0,0,0,0,0,0,24 }, - new int[] { 0,0,0,160,20,161,73,23,24,25,0,0,163,23,0,0,27,0,0,0,0,0,0,0,0,0,28,0,0,0,0,0,0,0,0,24 }, - new int[] { 0,0,0,160,20,73,73,23,24,25,0,0,0,23,0,0,27,0,0,0,0,0,0,0,0,0,28,0,0,0,0,0,0,0,0,24 }, - new int[] { 0,0,0,209,30,92,92,33,34,35,0,0,0,33,0,0,37,0,0,0,0,0,0,0,0,0,38,0,0,0,0,0,0,0,0,34 }, - new int[] { 0,0,0,0,98,99,99,33,34,0,0,0,167,33,0,0,0,0,0,0,0,0,0,0,0,0,98,0,0,0,0,0,0,0,0,34 }, - new int[] { 0,0,0,164,30,165,92,33,34,35,0,0,167,33,0,0,37,0,0,0,0,0,0,0,0,0,38,0,0,0,0,0,0,0,0,34 }, - new int[] { 0,0,0,164,30,92,92,33,34,35,0,0,0,33,0,0,37,0,0,0,0,0,0,0,0,0,38,0,0,0,0,0,0,0,0,34 }, - new int[] { 0,0,0,0,0,0,0,0,96,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,96 }, - new int[] { 0,93,0,0,0,168,168,0,96,35,0,0,0,0,0,93,0,0,0,0,0,0,0,0,0,0,97,0,0,0,0,0,0,0,0,96 }, - new int[] { 0,0,0,210,0,211,212,0,96,35,0,0,213,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,96 }, - new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,33,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, - new int[] { 0,0,0,0,0,0,214,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, - new int[] { 0,0,0,94,0,168,168,0,96,35,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,97,0,0,0,0,0,0,0,0,96 }, - new int[] { 0,0,0,0,0,168,173,0,96,35,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,97,0,0,0,0,0,0,0,0,96 }, - new int[] { 0,0,0,209,30,92,92,33,34,35,0,0,0,33,0,0,37,0,0,0,0,0,0,0,0,0,38,0,0,0,0,0,0,0,0,34 }, - new int[] { 0,0,0,0,98,99,99,33,34,0,0,0,178,33,0,0,0,0,0,0,0,0,0,0,0,0,98,0,0,0,0,0,0,0,0,34 }, - new int[] { 0,0,0,175,30,176,92,33,34,35,0,0,178,33,0,0,37,0,0,0,0,0,0,0,0,0,38,0,0,0,0,0,0,0,0,34 }, - new int[] { 0,0,0,175,30,92,92,33,34,35,0,0,0,33,0,0,37,0,0,0,0,0,0,0,0,0,38,0,0,0,0,0,0,0,0,34 }, - new int[] { 0,0,0,71,0,114,114,0,43,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,43 }, - new int[] { 0,0,0,0,0,0,0,0,43,0,0,0,182,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,43 }, - new int[] { 0,0,0,179,0,180,114,0,43,10,0,0,182,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,43 }, - new int[] { 0,0,0,179,0,114,114,0,43,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,43 }, - new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,48,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, - new int[] { 0,0,0,119,53,55,55,56,57,58,0,0,0,56,0,0,60,0,0,0,0,0,0,0,0,0,61,0,0,0,0,0,0,0,0,57 }, - new int[] { 0,0,0,0,125,126,126,56,57,0,0,0,187,56,0,0,0,0,0,0,0,0,0,0,0,0,125,0,0,0,0,0,0,0,0,57 }, - new int[] { 0,0,0,184,53,185,55,56,57,58,0,0,187,56,0,0,60,0,0,0,0,0,0,0,0,0,61,0,0,0,0,0,0,0,0,57 }, - new int[] { 0,0,0,184,53,55,55,56,57,58,0,0,0,56,0,0,60,0,0,0,0,0,0,0,0,0,61,0,0,0,0,0,0,0,0,57 }, - new int[] { 0,0,0,0,0,0,0,0,123,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,123 }, - new int[] { 0,120,0,0,0,188,188,0,123,58,0,0,0,0,0,120,0,0,0,0,0,0,0,0,0,0,124,0,0,0,0,0,0,0,0,123 }, - new int[] { 0,0,0,215,0,216,217,0,123,58,0,0,218,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,123 }, - new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,56,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, - new int[] { 0,0,0,0,0,0,219,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, - new int[] { 0,0,0,121,0,188,188,0,123,58,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,124,0,0,0,0,0,0,0,0,123 }, - new int[] { 0,0,0,0,0,188,193,0,123,58,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,124,0,0,0,0,0,0,0,0,123 }, - new int[] { 0,0,0,119,53,55,55,56,57,58,0,0,0,56,0,0,60,0,0,0,0,0,0,0,0,0,61,0,0,0,0,0,0,0,0,57 }, - new int[] { 0,0,0,0,125,126,126,56,57,0,0,0,198,56,0,0,0,0,0,0,0,0,0,0,0,0,125,0,0,0,0,0,0,0,0,57 }, - new int[] { 0,0,0,195,53,196,55,56,57,58,0,0,198,56,0,0,60,0,0,0,0,0,0,0,0,0,61,0,0,0,0,0,0,0,0,57 }, - new int[] { 0,0,0,195,53,55,55,56,57,58,0,0,0,56,0,0,60,0,0,0,0,0,0,0,0,0,61,0,0,0,0,0,0,0,0,57 }, - new int[] { 0,0,0,0,0,46,46,8,220,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,220 }, - new int[] { 0,221,90,0,0,114,42,0,43,10,0,0,0,0,0,221,0,0,0,0,0,0,0,0,0,0,44,0,0,0,0,0,0,0,0,43 }, - new int[] { 0,0,0,0,0,140,0,0,0,140,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, - new int[] { 0,0,0,222,5,223,224,8,139,140,0,0,225,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,139 }, - new int[] { 0,0,0,0,0,226,226,0,227,140,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,227 }, - new int[] { 0,0,0,90,0,153,153,0,77,25,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,77 }, - new int[] { 0,0,0,0,0,0,0,0,77,0,0,0,207,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,77 }, - new int[] { 0,0,0,204,0,205,153,0,77,25,0,0,207,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,77 }, - new int[] { 0,0,0,204,0,153,153,0,77,25,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,77 }, - new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,82,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, - new int[] { 0,0,0,0,30,92,92,33,34,35,0,0,0,33,0,0,37,0,0,0,0,0,0,0,0,0,38,0,0,0,0,0,0,0,0,34 }, - new int[] { 0,0,0,109,0,168,168,0,96,35,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,96 }, - new int[] { 0,0,0,0,0,0,0,0,96,0,0,0,213,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,96 }, - new int[] { 0,0,0,210,0,211,168,0,96,35,0,0,213,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,96 }, - new int[] { 0,0,0,210,0,168,168,0,96,35,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,96 }, - new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,101,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, - new int[] { 0,0,0,136,0,188,188,0,123,58,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,123 }, - new int[] { 0,0,0,0,0,0,0,0,123,0,0,0,218,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,123 }, - new int[] { 0,0,0,215,0,216,188,0,123,58,0,0,218,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,123 }, - new int[] { 0,0,0,215,0,188,188,0,123,58,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,123 }, - new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,128,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, - new int[] { 0,0,0,0,0,25,0,0,85,25,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,85 }, - new int[] { 0,0,0,228,5,229,230,8,139,140,0,0,231,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,139 }, - new int[] { 0,0,0,232,5,233,233,8,139,140,0,0,0,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,139 }, - new int[] { 0,0,0,0,45,46,46,8,139,0,0,0,225,8,0,0,0,0,0,0,0,0,0,0,0,0,45,0,0,0,0,0,0,0,0,139 }, - new int[] { 0,0,0,222,5,223,233,8,139,140,0,0,225,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,139 }, - new int[] { 0,0,0,222,5,233,233,8,139,140,0,0,0,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,139 }, - new int[] { 0,0,0,0,0,0,0,0,227,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,227 }, - new int[] { 0,0,0,0,0,140,0,0,201,140,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,201 }, - new int[] { 0,0,0,232,5,233,233,8,139,140,0,0,0,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,139 }, - new int[] { 0,0,0,0,45,46,46,8,139,0,0,0,231,8,0,0,0,0,0,0,0,0,0,0,0,0,45,0,0,0,0,0,0,0,0,139 }, - new int[] { 0,0,0,228,5,229,233,8,139,140,0,0,231,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,139 }, - new int[] { 0,0,0,228,5,233,233,8,139,140,0,0,0,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,139 }, - new int[] { 0,0,0,0,5,233,233,8,139,140,0,0,0,8,0,0,15,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,139 }, - new int[] { 0,0,0,0,45,46,46,8,139,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,45,0,0,0,0,0,0,0,0,139 } + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,2,3,4,5,6,7,8,9,10,11,11,12,8,13,14,15,16,17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,18,19,20,21,22,23,24,0,0,25,22,0,0,26,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23 }, + new int[] { 0,0,0,27,28,29,30,31,32,33,0,0,34,31,0,0,35,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32 }, + new int[] { 0,0,0,36,5,7,7,8,9,10,0,0,0,8,0,0,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,37,0,0,0,38,39,0,40,10,0,0,0,0,0,37,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,40 }, + new int[] { 0,0,0,0,41,42,42,8,9,0,0,0,12,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,0,41,42,42,8,9,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,43,44,45,46,8,9,10,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,0,0,10,0,0,47,10,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,47 }, + new int[] { 0,0,0,0,0,0,0,0,0,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,48,49,50,51,52,53,54,0,0,55,52,0,0,56,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,53 }, + new int[] { 0,0,0,4,5,7,7,8,9,10,0,0,0,8,0,0,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,2,3,4,5,6,7,8,9,10,11,11,12,8,0,2,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,18,57,20,21,22,23,24,0,0,25,22,0,0,26,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23 }, + new int[] { 0,0,0,0,58,59,59,8,9,10,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,60,0,61,61,0,62,63,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,62 }, + new int[] { 0,2,0,0,0,0,0,0,0,0,11,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,64,19,65,65,22,23,24,0,0,0,22,0,0,26,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23 }, + new int[] { 0,66,0,0,0,67,68,0,69,24,0,0,0,0,0,66,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,69 }, + new int[] { 0,0,0,0,70,71,71,22,23,0,0,0,25,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23 }, + new int[] { 0,0,0,18,19,20,65,22,23,24,0,0,25,22,0,0,26,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23 }, + new int[] { 0,0,0,72,73,74,75,22,23,24,0,0,0,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23 }, + new int[] { 0,0,0,0,0,24,0,0,76,24,0,0,0,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,76 }, + new int[] { 0,0,0,0,0,0,0,0,0,24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,18,19,65,65,22,23,24,0,0,0,22,0,0,26,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23 }, + new int[] { 0,0,0,0,77,78,78,22,23,24,0,0,0,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23 }, + new int[] { 0,0,0,79,28,80,30,31,32,33,0,0,0,31,0,0,35,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32 }, + new int[] { 0,81,0,0,0,82,83,0,84,33,0,0,0,0,0,81,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,84 }, + new int[] { 0,0,0,0,85,86,86,31,32,0,0,0,34,31,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32 }, + new int[] { 0,0,0,0,85,86,86,31,32,0,0,0,0,31,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32 }, + new int[] { 0,0,0,87,88,89,90,31,32,33,0,0,0,31,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32 }, + new int[] { 0,0,0,0,0,33,0,0,91,33,0,0,0,31,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,91 }, + new int[] { 0,0,0,0,0,0,0,0,0,33,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,27,28,80,30,31,32,33,0,0,0,31,0,0,35,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32 }, + new int[] { 0,0,0,0,92,93,93,31,32,33,0,0,0,31,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32 }, + new int[] { 0,0,0,0,5,7,7,8,9,10,0,0,0,8,0,0,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,94,5,95,96,8,9,10,0,0,97,8,0,0,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,0,0,98,98,0,40,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,40 }, + new int[] { 0,37,0,99,0,98,98,0,40,10,0,0,0,0,0,37,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,40 }, + new int[] { 0,0,0,0,0,10,0,0,47,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,47 }, + new int[] { 0,37,0,0,0,98,39,0,40,10,0,0,0,0,0,37,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,40 }, + new int[] { 0,0,0,0,0,42,42,8,100,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,100 }, + new int[] { 0,0,0,0,44,45,46,8,9,10,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,0,0,45,45,8,9,10,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,0,0,42,42,8,9,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,0,101,42,42,8,9,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,0,0,10,0,0,0,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,102,49,51,51,52,53,54,0,0,0,52,0,0,56,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,53 }, + new int[] { 0,103,0,0,0,104,105,0,106,54,0,0,0,0,0,103,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,106 }, + new int[] { 0,0,0,0,107,108,108,52,53,0,0,0,55,52,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,53 }, + new int[] { 0,0,0,0,107,108,108,52,53,0,0,0,0,52,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,53 }, + new int[] { 0,0,0,109,110,111,112,52,53,54,0,0,0,52,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,53 }, + new int[] { 0,0,0,0,0,54,0,0,113,54,0,0,0,52,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,113 }, + new int[] { 0,0,0,0,0,0,0,0,0,54,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,48,49,51,51,52,53,54,0,0,0,52,0,0,56,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,53 }, + new int[] { 0,0,0,0,114,115,115,52,53,54,0,0,0,52,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,53 }, + new int[] { 0,66,3,4,5,116,117,8,118,119,0,11,12,8,0,66,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,118 }, + new int[] { 0,0,0,0,0,38,120,0,40,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,40 }, + new int[] { 0,0,0,0,121,42,42,8,9,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,0,0,61,61,0,62,63,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,62 }, + new int[] { 0,0,0,0,0,0,0,0,62,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,62 }, + new int[] { 0,0,0,0,0,63,0,0,122,63,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,122 }, + new int[] { 0,0,0,0,0,0,0,0,0,63,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,19,65,65,22,23,24,0,0,0,22,0,0,26,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23 }, + new int[] { 0,0,0,0,70,71,71,22,23,0,0,0,0,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23 }, + new int[] { 0,0,0,123,19,124,125,22,23,24,0,0,126,22,0,0,26,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23 }, + new int[] { 0,0,0,0,0,127,127,0,69,24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,69 }, + new int[] { 0,66,0,128,0,127,127,0,69,24,0,0,0,0,0,66,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,69 }, + new int[] { 0,0,0,0,0,24,0,0,76,24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,76 }, + new int[] { 0,66,0,0,0,127,68,0,69,24,0,0,0,0,0,66,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,69 }, + new int[] { 0,0,0,0,0,71,71,22,129,0,0,0,0,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,129 }, + new int[] { 0,0,0,0,73,74,75,22,23,24,0,0,0,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23 }, + new int[] { 0,0,0,0,0,74,74,22,23,24,0,0,0,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23 }, + new int[] { 0,0,0,0,0,71,71,22,23,0,0,0,0,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23 }, + new int[] { 0,0,0,0,130,71,71,22,23,0,0,0,0,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23 }, + new int[] { 0,0,0,0,0,24,0,0,0,24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,67,131,0,69,24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,69 }, + new int[] { 0,0,0,0,132,71,71,22,23,0,0,0,0,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23 }, + new int[] { 0,0,0,0,28,80,30,31,32,33,0,0,0,31,0,0,35,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32 }, + new int[] { 0,0,0,0,85,86,86,31,32,0,0,0,0,31,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32 }, + new int[] { 0,0,0,133,28,134,135,31,32,33,0,0,136,31,0,0,35,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32 }, + new int[] { 0,0,0,0,0,137,137,0,84,33,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,84 }, + new int[] { 0,81,0,138,0,137,137,0,84,33,0,0,0,0,0,81,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,84 }, + new int[] { 0,0,0,0,0,33,0,0,91,33,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,91 }, + new int[] { 0,81,0,0,0,137,83,0,84,33,0,0,0,0,0,81,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,84 }, + new int[] { 0,0,0,0,0,86,86,31,139,0,0,0,0,31,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,139 }, + new int[] { 0,0,0,0,88,89,90,31,32,33,0,0,0,31,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32 }, + new int[] { 0,0,0,0,0,89,89,31,32,33,0,0,0,31,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32 }, + new int[] { 0,0,0,0,0,86,86,31,32,0,0,0,0,31,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32 }, + new int[] { 0,0,0,0,140,86,86,31,32,0,0,0,0,31,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32 }, + new int[] { 0,0,0,0,0,33,0,0,0,33,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,82,141,0,84,33,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,84 }, + new int[] { 0,0,0,0,142,86,86,31,32,0,0,0,0,31,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32 }, + new int[] { 0,0,0,36,5,7,7,8,9,10,0,0,0,8,0,0,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,0,41,42,42,8,9,0,0,0,97,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,94,5,95,7,8,9,10,0,0,97,8,0,0,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,94,5,7,7,8,9,10,0,0,0,8,0,0,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9 }, + new int[] { 0,0,0,0,0,0,0,0,40,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,40 }, + new int[] { 0,37,0,0,0,98,98,0,40,10,0,0,0,0,0,37,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,40 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,143,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,49,51,51,52,53,54,0,0,0,52,0,0,56,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,53 }, + new int[] { 0,0,0,144,49,145,146,52,53,54,0,0,147,52,0,0,56,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,53 }, + new int[] { 0,0,0,0,0,148,148,0,106,54,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,106 }, + new int[] { 0,103,0,149,0,148,148,0,106,54,0,0,0,0,0,103,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,106 }, + new int[] { 0,0,0,0,0,54,0,0,113,54,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,113 }, + new int[] { 0,103,0,0,0,148,105,0,106,54,0,0,0,0,0,103,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,106 }, + new int[] { 0,0,0,0,0,108,108,52,150,0,0,0,0,52,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,150 }, + new int[] { 0,0,0,0,110,111,112,52,53,54,0,0,0,52,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,53 }, + new int[] { 0,0,0,0,0,111,111,52,53,54,0,0,0,52,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,53 }, + new int[] { 0,0,0,0,0,108,108,52,53,0,0,0,0,52,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,53 }, + new int[] { 0,0,0,0,151,108,108,52,53,0,0,0,0,52,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,53 }, + new int[] { 0,0,0,0,0,54,0,0,0,54,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,104,152,0,106,54,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,106 }, + new int[] { 0,0,0,0,153,108,108,52,53,0,0,0,0,52,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,53 }, + new int[] { 0,0,0,0,41,154,154,8,118,24,0,0,12,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,118 }, + new int[] { 0,66,0,128,41,154,154,8,118,24,0,0,0,8,0,66,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,118 }, + new int[] { 0,0,0,0,0,119,0,0,155,119,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,155 }, + new int[] { 0,0,0,0,0,0,0,0,0,119,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,38,0,98,98,0,40,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,40 }, + new int[] { 0,0,0,0,0,98,120,0,40,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,40 }, + new int[] { 0,0,0,0,0,63,0,0,0,63,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,64,19,65,65,22,23,24,0,0,0,22,0,0,26,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23 }, + new int[] { 0,0,0,0,70,71,71,22,23,0,0,0,126,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23 }, + new int[] { 0,0,0,123,19,124,65,22,23,24,0,0,126,22,0,0,26,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23 }, + new int[] { 0,0,0,123,19,65,65,22,23,24,0,0,0,22,0,0,26,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23 }, + new int[] { 0,0,0,0,0,0,0,0,69,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,69 }, + new int[] { 0,66,0,0,0,127,127,0,69,24,0,0,0,0,0,66,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,69 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,156,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,67,0,127,127,0,69,24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,69 }, + new int[] { 0,0,0,0,0,127,131,0,69,24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,69 }, + new int[] { 0,0,0,157,28,80,80,31,32,33,0,0,0,31,0,0,35,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32 }, + new int[] { 0,0,0,0,85,86,86,31,32,0,0,0,136,31,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32 }, + new int[] { 0,0,0,133,28,134,80,31,32,33,0,0,136,31,0,0,35,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32 }, + new int[] { 0,0,0,133,28,80,80,31,32,33,0,0,0,31,0,0,35,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32 }, + new int[] { 0,0,0,0,0,0,0,0,84,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,84 }, + new int[] { 0,81,0,0,0,137,137,0,84,33,0,0,0,0,0,81,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,84 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,31,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,158,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,82,0,137,137,0,84,33,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,84 }, + new int[] { 0,0,0,0,0,137,141,0,84,33,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,84 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,44,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,102,49,51,51,52,53,54,0,0,0,52,0,0,56,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,53 }, + new int[] { 0,0,0,0,107,108,108,52,53,0,0,0,147,52,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,53 }, + new int[] { 0,0,0,144,49,145,51,52,53,54,0,0,147,52,0,0,56,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,53 }, + new int[] { 0,0,0,144,49,51,51,52,53,54,0,0,0,52,0,0,56,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,53 }, + new int[] { 0,0,0,0,0,0,0,0,106,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,106 }, + new int[] { 0,103,0,0,0,148,148,0,106,54,0,0,0,0,0,103,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,106 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,52,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,159,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,104,0,148,148,0,106,54,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,106 }, + new int[] { 0,0,0,0,0,148,152,0,106,54,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,106 }, + new int[] { 0,0,0,0,0,42,42,8,160,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,160 }, + new int[] { 0,0,0,0,0,119,0,0,0,119,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,73,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,28,80,80,31,32,33,0,0,0,31,0,0,35,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,88,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,110,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,0,24,0,0,76,24,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,76 } }; public static bool[] AcceptingStates => new bool[] @@ -269,7 +196,6 @@ internal static partial class IndicShapingData false, true, true, - true, false, true, true, @@ -279,7 +205,6 @@ internal static partial class IndicShapingData true, true, true, - true, false, true, true, @@ -295,8 +220,6 @@ internal static partial class IndicShapingData true, false, true, - false, - true, true, false, false, @@ -312,22 +235,17 @@ internal static partial class IndicShapingData true, true, true, - true, false, true, - true, false, true, true, true, - true, - true, false, true, true, true, true, - false, true, false, true, @@ -338,15 +256,11 @@ internal static partial class IndicShapingData true, false, true, - true, - true, - true, false, true, true, true, true, - false, true, false, true, @@ -357,15 +271,11 @@ internal static partial class IndicShapingData true, false, true, - true, - true, - true, false, true, true, false, true, - true, false, false, true, @@ -373,7 +283,6 @@ internal static partial class IndicShapingData true, true, true, - false, true, false, true, @@ -391,25 +300,12 @@ internal static partial class IndicShapingData true, true, true, - true, - true, - true, - true, false, true, true, - true, false, true, - true, - false, - true, - true, - false, false, - true, - true, - true, false, true, true, @@ -419,73 +315,31 @@ internal static partial class IndicShapingData true, false, true, - true, false, false, true, true, - true, false, true, - true, - true, false, true, true, false, true, false, - true, - true, false, true, true, false, - false, - true, - true, - true, - false, - true, - true, - false, - true, - true, - true, - true, - true, - false, - true, - true, - false, - true, true, false, true, - true, - false, - true, - false, - true, - true, - false, - true, - true, - true, false, - true, - true, false, - true, - true, - false, - true, - true, - true, - false + true }; - public static string[][] Tags => new string[234][] + public static string[][] Tags => new string[161][] { Array.Empty(), new string[] { "broken_cluster" }, @@ -505,7 +359,6 @@ internal static partial class IndicShapingData new string[] { "broken_cluster" }, new string[] { "symbol_cluster" }, Array.Empty(), - new string[] { "broken_cluster" }, new string[] { "consonant_syllable" }, new string[] { "consonant_syllable" }, Array.Empty(), @@ -515,7 +368,6 @@ internal static partial class IndicShapingData new string[] { "consonant_syllable" }, new string[] { "consonant_syllable" }, new string[] { "consonant_syllable" }, - new string[] { "consonant_syllable" }, new string[] { "vowel_syllable" }, new string[] { "vowel_syllable" }, Array.Empty(), @@ -525,13 +377,11 @@ internal static partial class IndicShapingData new string[] { "vowel_syllable" }, new string[] { "vowel_syllable" }, new string[] { "vowel_syllable" }, - new string[] { "vowel_syllable" }, new string[] { "broken_cluster" }, new string[] { "broken_cluster" }, new string[] { "broken_cluster" }, new string[] { "broken_cluster" }, new string[] { "broken_cluster" }, - Array.Empty(), new string[] { "broken_cluster" }, Array.Empty(), new string[] { "broken_cluster" }, @@ -548,24 +398,19 @@ internal static partial class IndicShapingData new string[] { "standalone_cluster" }, new string[] { "standalone_cluster" }, new string[] { "standalone_cluster" }, - new string[] { "standalone_cluster" }, new string[] { "consonant_syllable","broken_cluster" }, new string[] { "broken_cluster" }, Array.Empty(), - new string[] { "broken_cluster" }, new string[] { "symbol_cluster" }, Array.Empty(), new string[] { "symbol_cluster" }, new string[] { "symbol_cluster" }, - new string[] { "broken_cluster" }, - new string[] { "broken_cluster" }, new string[] { "consonant_syllable" }, Array.Empty(), new string[] { "consonant_syllable" }, new string[] { "consonant_syllable" }, new string[] { "consonant_syllable" }, new string[] { "consonant_syllable" }, - Array.Empty(), new string[] { "consonant_syllable" }, Array.Empty(), new string[] { "consonant_syllable" }, @@ -575,16 +420,12 @@ internal static partial class IndicShapingData new string[] { "consonant_syllable" }, new string[] { "consonant_syllable" }, Array.Empty(), - new string[] { "consonant_syllable" }, - new string[] { "consonant_syllable" }, - new string[] { "consonant_syllable" }, new string[] { "vowel_syllable" }, Array.Empty(), new string[] { "vowel_syllable" }, new string[] { "vowel_syllable" }, new string[] { "vowel_syllable" }, new string[] { "vowel_syllable" }, - Array.Empty(), new string[] { "vowel_syllable" }, Array.Empty(), new string[] { "vowel_syllable" }, @@ -594,16 +435,12 @@ internal static partial class IndicShapingData new string[] { "vowel_syllable" }, new string[] { "vowel_syllable" }, Array.Empty(), - new string[] { "vowel_syllable" }, - new string[] { "vowel_syllable" }, - new string[] { "vowel_syllable" }, new string[] { "broken_cluster" }, Array.Empty(), new string[] { "broken_cluster" }, new string[] { "broken_cluster" }, Array.Empty(), new string[] { "broken_cluster" }, - new string[] { "broken_cluster" }, Array.Empty(), Array.Empty(), new string[] { "standalone_cluster" }, @@ -611,7 +448,6 @@ internal static partial class IndicShapingData new string[] { "standalone_cluster" }, new string[] { "standalone_cluster" }, new string[] { "standalone_cluster" }, - Array.Empty(), new string[] { "standalone_cluster" }, Array.Empty(), new string[] { "standalone_cluster" }, @@ -621,33 +457,20 @@ internal static partial class IndicShapingData new string[] { "standalone_cluster" }, new string[] { "standalone_cluster" }, Array.Empty(), - new string[] { "standalone_cluster" }, - new string[] { "standalone_cluster" }, - new string[] { "standalone_cluster" }, new string[] { "consonant_syllable" }, new string[] { "consonant_syllable" }, new string[] { "consonant_syllable","broken_cluster" }, new string[] { "consonant_syllable","broken_cluster" }, new string[] { "broken_cluster" }, new string[] { "broken_cluster" }, - new string[] { "broken_cluster" }, new string[] { "symbol_cluster" }, - new string[] { "broken_cluster" }, - Array.Empty(), - new string[] { "broken_cluster" }, - new string[] { "broken_cluster" }, new string[] { "consonant_syllable" }, Array.Empty(), new string[] { "consonant_syllable" }, new string[] { "consonant_syllable" }, Array.Empty(), new string[] { "consonant_syllable" }, - new string[] { "consonant_syllable" }, - Array.Empty(), Array.Empty(), - new string[] { "consonant_syllable" }, - new string[] { "consonant_syllable" }, - new string[] { "consonant_syllable" }, Array.Empty(), new string[] { "consonant_syllable" }, new string[] { "consonant_syllable" }, @@ -657,19 +480,10 @@ internal static partial class IndicShapingData new string[] { "vowel_syllable" }, Array.Empty(), new string[] { "vowel_syllable" }, - new string[] { "vowel_syllable" }, Array.Empty(), Array.Empty(), new string[] { "vowel_syllable" }, new string[] { "vowel_syllable" }, - new string[] { "vowel_syllable" }, - Array.Empty(), - new string[] { "vowel_syllable" }, - new string[] { "vowel_syllable" }, - new string[] { "broken_cluster" }, - Array.Empty(), - new string[] { "broken_cluster" }, - new string[] { "broken_cluster" }, Array.Empty(), new string[] { "standalone_cluster" }, Array.Empty(), @@ -677,50 +491,17 @@ internal static partial class IndicShapingData new string[] { "standalone_cluster" }, Array.Empty(), new string[] { "standalone_cluster" }, - new string[] { "standalone_cluster" }, Array.Empty(), Array.Empty(), new string[] { "standalone_cluster" }, new string[] { "standalone_cluster" }, - new string[] { "standalone_cluster" }, Array.Empty(), - new string[] { "standalone_cluster" }, - new string[] { "standalone_cluster" }, - Array.Empty(), - new string[] { "broken_cluster" }, new string[] { "consonant_syllable","broken_cluster" }, - new string[] { "consonant_syllable","broken_cluster" }, - new string[] { "consonant_syllable","broken_cluster" }, - new string[] { "consonant_syllable" }, - Array.Empty(), - new string[] { "consonant_syllable" }, - new string[] { "consonant_syllable" }, - Array.Empty(), - new string[] { "vowel_syllable" }, - new string[] { "vowel_syllable" }, Array.Empty(), new string[] { "vowel_syllable" }, - new string[] { "vowel_syllable" }, - Array.Empty(), - new string[] { "standalone_cluster" }, - Array.Empty(), - new string[] { "standalone_cluster" }, - new string[] { "standalone_cluster" }, - Array.Empty(), - new string[] { "consonant_syllable" }, - new string[] { "consonant_syllable","broken_cluster" }, - new string[] { "consonant_syllable","broken_cluster" }, Array.Empty(), - new string[] { "consonant_syllable","broken_cluster" }, - new string[] { "consonant_syllable","broken_cluster" }, Array.Empty(), - new string[] { "consonant_syllable","broken_cluster" }, - new string[] { "consonant_syllable","broken_cluster" }, - Array.Empty(), - new string[] { "consonant_syllable","broken_cluster" }, - new string[] { "consonant_syllable","broken_cluster" }, - new string[] { "consonant_syllable","broken_cluster" }, - Array.Empty() + new string[] { "consonant_syllable" } }; } } diff --git a/src/SixLabors.Fonts/Unicode/Resources/IndicShapingData.cs b/src/SixLabors.Fonts/Unicode/Resources/IndicShapingData.cs index 353aba174..8e6aebe5c 100644 --- a/src/SixLabors.Fonts/Unicode/Resources/IndicShapingData.cs +++ b/src/SixLabors.Fonts/Unicode/Resources/IndicShapingData.cs @@ -9,41 +9,15 @@ internal static partial class IndicShapingData { /// /// Script shaping category values used for Indic, Khmer, and Myanmar text - /// classification. These values correspond to the category codes used by - /// HarfBuzz in its Indic-style shaping engines, including the extended - /// categories required for Myanmar. + /// classification. /// /// The values serve as the input alphabet for the script syllable machines /// and determine script-specific parsing, reordering, and dotted circle insertion. /// - /// Categories are sourced from the OpenType Script Development Specifications - /// and HarfBuzz's generated Indic tables: - /// - /// Indic specification: - /// https://learn.microsoft.com/en-us/typography/script-development/devanagari - /// - /// General Indic shaper category model and data: - /// https://github.com/harfbuzz/harfbuzz/blob/main/src/hb-ot-shaper-indic.cc - /// https://github.com/harfbuzz/harfbuzz/blob/main/src/hb-ot-shaper-indic-table.hh - /// - /// Khmer specification: - /// https://learn.microsoft.com/en-us/typography/script-development/khmer - /// - /// Myanmar specification: - /// https://learn.microsoft.com/en-us/typography/script-development/myanmar - /// - /// Myanmar machine exports: - /// https://github.com/harfbuzz/harfbuzz/blob/main/src/hb-ot-shaper-myanmar-machine.rl - /// - /// Notes: - /// * X is the default category and always has value 0. - /// * Coeng intentionally shares the same category value as H, matching - /// HarfBuzz behavior for Khmer. - /// * Some values are shared across scripts (for example VAbv, VBlw, VPre, - /// VPst) because the OpenType model for dependent vowels is the same. - /// * Myanmar-specific medial and tone categories begin at 32 and above, - /// matching HarfBuzz's numeric category assignments. /// + /// + /// The values are transcribed from HarfBuzz 14.2.1, src/gen-indic-table.py, symbols category_map, category_overrides, and position_to_category. The shaping categories and overrides are not derivable from the Unicode Character Database. + /// public enum Categories : int { // Core Indic-style categories (shared across scripts where applicable) @@ -54,7 +28,6 @@ public enum Categories : int N = 3, // Nukta H = 4, // Halant (virama) - // Coeng = H, // Khmer Coeng, mapped to H in HarfBuzz ZWNJ = 5, // Zero width non-joiner ZWJ = 6, // Zero width joiner M = 7, // Generic matra / dependent vowel @@ -85,10 +58,9 @@ public enum Categories : int Robatic = 25, // Khmer Robatic sign Xgroup = 26, // Khmer X-group matra sequence Ygroup = 27, // Khmer Y-group matra sequence - Coeng = 28, // Remove once we no longer need it for Khmer // Myanmar-specific categories - // IV = V, // Independent vowel (shares code 2 with V in HarfBuzz) + // IV = V, // Independent vowel (shares code 2 with V) // DB = N, // Dot-below (shares code 3 with N) // GB = Placeholder, // Generic base / placeholder (shares code 10) As = 32, // Asat @@ -101,10 +73,36 @@ public enum Categories : int ML = 41 // Medial Mon La } + /// + /// The shaping categories consumed by the Khmer syllable machine. + /// + /// + /// The category set is transcribed from HarfBuzz 14.2.1, src/hb-ot-shaper-khmer-machine.rl, symbol khmer_syllable_machine. The values are not derivable from the Unicode Character Database. + /// + public enum KhmerCategories : int + { + X = Categories.X, + C = Categories.C, + V = Categories.V, + H = Categories.H, + ZWNJ = Categories.ZWNJ, + ZWJ = Categories.ZWJ, + Placeholder = Categories.Placeholder, + Dotted_Circle = Categories.Dotted_Circle, + Ra = Categories.Ra, + VAbv = Categories.VAbv, + VBlw = Categories.VBlw, + VPre = Categories.VPre, + VPst = Categories.VPst, + Robatic = Categories.Robatic, + Xgroup = Categories.Xgroup, + Ygroup = Categories.Ygroup, + } + // Categories used in the Myanmar shaping engine. // Note: // The OpenType Myanmar spec defines categories D, D0, and P. - // HarfBuzz collapses: + // The source table collapses: // D => GB // D0 => D => GB // P => GB @@ -341,16 +339,6 @@ public enum BlwfMode } }; - public static Dictionary Decompositions { get; } = new() - { - // Khmer - { 0x17BE, new int[] { 0x17C1, 0x17BE } }, - { 0x17BF, new int[] { 0x17C1, 0x17BF } }, - { 0x17C0, new int[] { 0x17C1, 0x17C0 } }, - { 0x17C4, new int[] { 0x17C1, 0x17C4 } }, - { 0x17C5, new int[] { 0x17C1, 0x17C5 } } - }; - public static uint ConsonantFlags { get; } = Flag(Categories.C) | Flag(Categories.Ra) | @@ -376,9 +364,10 @@ public enum BlwfMode Flag(Categories.ZWJ) | Flag(Categories.ZWNJ); - public static uint HalantOrCoengFlags { get; } = - Flag(Categories.H) | - Flag(Categories.Coeng); + /// + /// Gets the bit identifying a halant category. + /// + public static uint HalantFlags { get; } = Flag(Categories.H); /// /// Provides a flag value for the given category. Only valid for categories < 32. diff --git a/src/SixLabors.Fonts/Unicode/Resources/IndicShapingTrie.Generated.cs b/src/SixLabors.Fonts/Unicode/Resources/IndicShapingTrie.Generated.cs index 1e3f51ebc..e5397c9d1 100644 --- a/src/SixLabors.Fonts/Unicode/Resources/IndicShapingTrie.Generated.cs +++ b/src/SixLabors.Fonts/Unicode/Resources/IndicShapingTrie.Generated.cs @@ -283,7 +283,7 @@ internal static class IndicShapingTrie 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 14, 8, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 15, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 15, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 11, 23, 0, 0, 11, 23, 0, 0, 6, 20, 0, 0, - 6, 20, 0, 0, 8, 21, 0, 0, 8, 21, 0, 0, 3, 22, 0, 0, 14, 9, 0, 0, 6, 20, 0, 0, 6, 20, 0, 0, 6, 20, 0, 0, 14, 9, 0, 0, 8, 3, 0, 0, 14, 8, 0, 0, 15, 28, 0, 0, 6, 32, 0, 0, 11, 38, 0, 0, 15, 36, 0, 0, 8, 37, 0, 0, 8, 35, 0, 0, 4, 1, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, + 6, 20, 0, 0, 8, 21, 0, 0, 8, 21, 0, 0, 3, 22, 0, 0, 14, 9, 0, 0, 6, 20, 0, 0, 6, 20, 0, 0, 6, 20, 0, 0, 14, 9, 0, 0, 8, 3, 0, 0, 14, 8, 0, 0, 15, 4, 0, 0, 6, 32, 0, 0, 11, 38, 0, 0, 15, 36, 0, 0, 8, 37, 0, 0, 8, 35, 0, 0, 4, 1, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 1, 0, 0, 15, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 11, 23, 0, 0, 11, 23, 0, 0, 8, 21, 0, 0, 8, 21, 0, 0, 4, 15, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 8, 38, 0, 0, 8, 38, 0, 0, 8, 41, 0, 0, 4, 1, 0, 0, 11, 23, 0, 0, 11, 39, 0, 0, 11, 39, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 11, 23, 0, 0, 11, 23, 0, 0, 11, 39, 0, 0, 11, 39, 0, 0, 11, 39, 0, 0, 11, 39, 0, 0, 11, 39, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 6, 20, 0, 0, 6, 20, 0, 0, 6, 20, 0, 0, 6, 20, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 8, 37, 0, 0, 11, 23, 0, 0, 3, 22, 0, 0, 6, 20, 0, 0, 6, 20, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 4, 1, 0, 0, 14, 8, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, @@ -303,8 +303,8 @@ internal static class IndicShapingTrie 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 15, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, - 4, 2, 0, 0, 4, 2, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 12, 7, 0, 0, 12, 7, 0, 0, 6, 3, 0, 0, 14, 8, 0, 0, 12, 7, 0, 0, 6, 12, 0, 0, 6, 12, 0, 0, - 14, 8, 0, 0, 4, 16, 0, 0, 12, 7, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 12, 7, 0, 0, 15, 28, 0, 0, 14, 8, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 10, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 14, 17, 0, 0, 14, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, + 4, 2, 0, 0, 4, 2, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 11, 23, 0, 0, 6, 20, 0, 0, 6, 20, 0, 0, 6, 20, 0, 0, 6, 20, 0, 0, 8, 21, 0, 0, 8, 21, 0, 0, 8, 21, 0, 0, 6, 20, 0, 0, 11, 23, 0, 0, 11, 23, 0, 0, 3, 22, 0, 0, 3, 22, 0, 0, 3, 22, 0, 0, 11, 23, 0, 0, 11, 23, 0, 0, 6, 26, 0, 0, 11, 27, 0, 0, 11, 27, 0, 0, 6, 25, 0, 0, 6, 25, 0, 0, + 6, 26, 0, 0, 6, 25, 0, 0, 6, 26, 0, 0, 6, 26, 0, 0, 6, 26, 0, 0, 6, 26, 0, 0, 6, 26, 0, 0, 15, 4, 0, 0, 6, 27, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 10, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 14, 17, 0, 0, 6, 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -317,7 +317,7 @@ internal static class IndicShapingTrie 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 1, 0, 0, 4, 16, 0, 0, - 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 0, 0, 0, 0, 15, 28, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 14, 8, 0, 0, + 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 0, 0, 0, 0, 15, 4, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 14, 8, 0, 0, 6, 3, 0, 0, 6, 3, 0, 0, 6, 3, 0, 0, 6, 3, 0, 0, 6, 3, 0, 0, 9, 7, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 8, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, @@ -327,7 +327,7 @@ internal static class IndicShapingTrie 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 6, 0, 0, 0, 8, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 14, 8, 0, 0, 4, 16, 0, 0, 14, 8, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, - 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 15, 28, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 14, 17, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 16, 0, 0, + 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 15, 4, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 14, 17, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 6, 3, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 8, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, @@ -390,7 +390,7 @@ internal static class IndicShapingTrie 4, 1, 0, 0, 11, 39, 0, 0, 6, 3, 0, 0, 11, 3, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 6, 3, 0, 0, 15, 0, 0, 0, 6, 3, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, - 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 14, 8, 0, 0, 15, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 14, 8, 0, 0, 15, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, @@ -434,7 +434,7 @@ internal static class IndicShapingTrie 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 1, 0, 0, 5, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 9, 7, 0, 0, 5, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, - 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 3, 0, 0, 8, 3, 0, 0, 8, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 28, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 3, 0, 0, 8, 3, 0, 0, 8, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 4, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, @@ -461,7 +461,7 @@ internal static class IndicShapingTrie 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, - 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 15, 28, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, + 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 15, 4, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 8, 3, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, @@ -484,7 +484,7 @@ internal static class IndicShapingTrie 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 14, 17, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 2, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 7, 0, 0, 0, 0, 0, 0, - 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 14, 8, 0, 0, 0, 0, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 15, 28, 0, 0, 6, 14, 0, 0, 14, 8, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 14, 8, 0, 0, 0, 0, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 15, 4, 0, 0, 6, 14, 0, 0, 14, 8, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 9, 0, 0, 14, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 8, 4, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 8, 3, 0, 0, 14, 17, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, @@ -509,16 +509,16 @@ internal static class IndicShapingTrie 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 8, 4, 0, 0, 8, 3, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, - 9, 7, 0, 0, 2, 7, 0, 0, 0, 0, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 9, 7, 0, 0, 15, 28, 0, 0, 6, 0, 0, 0, 4, 16, 0, 0, 6, 14, 0, 0, 4, 16, 0, 0, 8, 3, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 9, 7, 0, 0, 2, 7, 0, 0, 0, 0, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 9, 7, 0, 0, 15, 4, 0, 0, 6, 0, 0, 0, 4, 16, 0, 0, 6, 14, 0, 0, 4, 16, 0, 0, 8, 3, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 8, 4, 0, 0, 14, 17, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 2, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 14, 8, 0, 0, 9, 7, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 15, 18, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 10, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, - 15, 0, 0, 0, 15, 0, 0, 0, 4, 10, 0, 0, 15, 0, 0, 0, 15, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 4, 10, 0, 0, 15, 0, 0, 0, 15, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 14, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, - 15, 28, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 14, 17, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 15, 4, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 14, 17, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, @@ -528,15 +528,15 @@ internal static class IndicShapingTrie 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 0, 0, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 4, 16, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, - 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 9, 7, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 8, 3, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 15, 28, 0, 0, 11, 14, 0, 0, 4, 16, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 9, 7, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 8, 3, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 15, 4, 0, 0, 11, 14, 0, 0, 4, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, - 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 15, 28, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 15, 4, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 10, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 8, 0, 0, 14, 8, 0, 0, 6, 14, 0, 0, 14, 8, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, - 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 15, 28, 0, 0, 15, 0, 0, 0, + 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 4, 1, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 7, 0, 0, 2, 7, 0, 0, 9, 7, 0, 0, 9, 7, 0, 0, 15, 4, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 4, 10, 0, 0, 6, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, diff --git a/src/SixLabors.Fonts/Unicode/Resources/KhmerShapingData.Generated.cs b/src/SixLabors.Fonts/Unicode/Resources/KhmerShapingData.Generated.cs new file mode 100644 index 000000000..cda6f5d34 --- /dev/null +++ b/src/SixLabors.Fonts/Unicode/Resources/KhmerShapingData.Generated.cs @@ -0,0 +1,156 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// +using System; +using System.Collections.Generic; + +namespace SixLabors.Fonts.Unicode.Resources +{ + internal static class KhmerShapingData + { + public static int[][] StateTable => new int[44][] + { + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 2,3,3,4,5,5,6,6,3,7,8,9,10,11,12,13 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,14,15,15,0,0,0,16,17,18,19,6,20,21 }, + new int[] { 0,22,22,0,0,0,0,0,22,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,23,23,0,0,0,7,0,0,0,0,12,0 }, + new int[] { 0,0,0,14,24,24,0,0,0,16,17,18,19,25,20,21 }, + new int[] { 0,0,0,26,27,27,0,0,0,0,0,0,10,0,7,13 }, + new int[] { 0,0,0,26,28,28,0,0,0,7,0,0,10,0,8,13 }, + new int[] { 0,0,0,26,29,29,0,0,0,7,8,0,10,0,9,13 }, + new int[] { 0,0,0,26,30,30,0,0,0,0,0,0,0,0,10,13 }, + new int[] { 0,0,0,4,5,5,0,0,0,7,8,9,10,0,12,13 }, + new int[] { 0,0,0,26,5,5,0,0,0,7,8,9,10,0,12,13 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,13 }, + new int[] { 0,31,31,0,0,0,0,0,31,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,32,32,0,0,0,16,0,0,0,6,20,0 }, + new int[] { 0,0,0,33,34,34,0,0,0,0,0,0,19,0,16,21 }, + new int[] { 0,0,0,33,35,35,0,0,0,16,0,0,19,0,17,21 }, + new int[] { 0,0,0,33,36,36,0,0,0,16,17,0,19,0,18,21 }, + new int[] { 0,0,0,33,37,37,0,0,0,0,0,0,0,0,19,21 }, + new int[] { 0,0,0,33,24,24,0,0,0,16,17,18,19,0,20,21 }, + new int[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,21 }, + new int[] { 0,0,0,4,38,38,0,0,0,7,8,9,10,11,12,13 }, + new int[] { 0,0,0,0,23,23,0,0,0,0,0,0,0,0,12,0 }, + new int[] { 0,0,0,0,32,32,0,0,0,16,0,0,0,0,20,0 }, + new int[] { 0,0,0,14,24,24,0,0,0,16,17,18,19,0,20,21 }, + new int[] { 0,13,13,0,0,0,0,0,13,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,27,27,0,0,0,0,0,0,0,0,7,0 }, + new int[] { 0,0,0,0,39,39,0,0,0,7,0,0,0,0,8,0 }, + new int[] { 0,0,0,0,40,40,0,0,0,7,0,0,0,0,9,0 }, + new int[] { 0,0,0,0,30,30,0,0,0,0,0,0,0,0,10,0 }, + new int[] { 0,0,0,14,41,41,0,0,0,16,17,18,19,25,20,21 }, + new int[] { 0,0,0,0,32,32,0,0,0,0,0,0,0,0,20,0 }, + new int[] { 0,21,21,0,0,0,0,0,21,0,0,0,0,0,0,0 }, + new int[] { 0,0,0,0,34,34,0,0,0,0,0,0,0,0,16,0 }, + new int[] { 0,0,0,0,42,42,0,0,0,16,0,0,0,0,17,0 }, + new int[] { 0,0,0,0,43,43,0,0,0,16,0,0,0,0,18,0 }, + new int[] { 0,0,0,0,37,37,0,0,0,0,0,0,0,0,19,0 }, + new int[] { 0,0,0,0,23,23,0,0,0,7,0,0,0,11,12,0 }, + new int[] { 0,0,0,0,39,39,0,0,0,0,0,0,0,0,8,0 }, + new int[] { 0,0,0,0,40,40,0,0,0,0,0,0,0,0,9,0 }, + new int[] { 0,0,0,0,32,32,0,0,0,16,0,0,0,25,20,0 }, + new int[] { 0,0,0,0,42,42,0,0,0,0,0,0,0,0,17,0 }, + new int[] { 0,0,0,0,43,43,0,0,0,0,0,0,0,0,18,0 } + }; + + public static bool[] AcceptingStates => new bool[] + { + false, + true, + true, + true, + true, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true, + false, + true, + true, + true, + true, + true, + true, + true, + false, + false, + true, + false, + false, + false, + false, + false, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + }; + + public static string[][] Tags => new string[44][] + { + Array.Empty(), + new string[] { "broken_cluster" }, + new string[] { "non_khmer_cluster" }, + new string[] { "consonant_syllable" }, + new string[] { "broken_cluster" }, + Array.Empty(), + new string[] { "consonant_syllable" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "broken_cluster" }, + new string[] { "consonant_syllable" }, + Array.Empty(), + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "consonant_syllable" }, + new string[] { "broken_cluster" }, + Array.Empty(), + Array.Empty(), + new string[] { "consonant_syllable" }, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new string[] { "consonant_syllable" }, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty() + }; + } +} diff --git a/src/SixLabors.Fonts/Unicode/Resources/MarkOrderingData.Generated.cs b/src/SixLabors.Fonts/Unicode/Resources/MarkOrderingData.Generated.cs new file mode 100644 index 000000000..c25e37dab --- /dev/null +++ b/src/SixLabors.Fonts/Unicode/Resources/MarkOrderingData.Generated.cs @@ -0,0 +1,165 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// +using System; + +namespace SixLabors.Fonts.Unicode.Resources +{ + /// + /// The classes that order a mark against the marks around it. + /// + internal static class MarkOrderingData + { + /// + /// Gets the order given to the canonical combining class at each index. Every + /// class orders as itself except where a script draws its marks in an order its + /// assigned classes do not give, and those are renumbered so that one sort + /// leaves the marks of every script in the order they are drawn. + /// + public static ReadOnlySpan Classes => new byte[] + { + 0, // ccc 0, Not_Reordered + 1, // ccc 1, Overlay + 2, 3, 4, 5, 6, // ccc 2..6 + 7, // ccc 7, Nukta + 8, // ccc 8, Kana_Voicing + 9, // ccc 9, Virama + + // Hebrew. + 22, // ccc 10, sheva + 15, // ccc 11, hataf segol + 16, // ccc 12, hataf patah + 17, // ccc 13, hataf qamats + 23, // ccc 14, hiriq + 18, // ccc 15, tsere + 19, // ccc 16, segol + 20, // ccc 17, patah + 21, // ccc 18, qamats & qamats qatan + 14, // ccc 19, holam & holam haser for vav + 24, // ccc 20, qubuts + 12, // ccc 21, dagesh + 25, // ccc 22, meteg + 13, // ccc 23, rafe + 10, // ccc 24, shin dot + 11, // ccc 25, sin dot + 26, // ccc 26, point varika + + // Arabic. + 28, // ccc 27, fathatan + 29, // ccc 28, dammatan + 30, // ccc 29, kasratan + 31, // ccc 30, fatha + 32, // ccc 31, damma + 33, // ccc 32, kasra + 27, // ccc 33, shadda + 34, // ccc 34, sukun + 35, // ccc 35, superscript alef + + // Syriac. + 36, // ccc 36, superscript alaph + 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, // ccc 37..46 + 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, // ccc 47..56 + 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, // ccc 57..66 + 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, // ccc 67..76 + 77, 78, 79, 80, 81, 82, 83, // ccc 77..83 + + // Telugu. + 4, // ccc 84, length mark + 85, 86, 87, 88, 89, 90, // ccc 85..90 + 5, // ccc 91, ai length mark + 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, // ccc 92..101 + 102, // ccc 102..102 + + // Thai. + 3, // ccc 103, sara u / sara uu + 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, // ccc 104..113 + 114, 115, 116, 117, // ccc 114..117 + + // Lao. + 118, // ccc 118, sign u / sign uu + 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, // ccc 119..128 + + // Tibetan. + 129, // ccc 129, sign aa + 132, // ccc 130, sign i + 131, // ccc 131..131 + 131, // ccc 132, sign u + 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, // ccc 133..142 + 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, // ccc 143..152 + 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, // ccc 153..162 + 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, // ccc 163..172 + 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, // ccc 173..182 + 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, // ccc 183..192 + 193, 194, 195, 196, 197, 198, 199, // ccc 193..199 + 200, // ccc 200, Attached_Below_Left + 201, // ccc 201..201 + 202, // ccc 202, Attached_Below + 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, // ccc 203..212 + 213, // ccc 213..213 + 214, // ccc 214, Attached_Above + 215, // ccc 215..215 + 216, // ccc 216, Attached_Above_Right + 217, // ccc 217..217 + 218, // ccc 218, Below_Left + 219, // ccc 219..219 + 220, // ccc 220, Below + 221, // ccc 221..221 + 222, // ccc 222, Below_Right + 223, // ccc 223..223 + 224, // ccc 224, Left + 225, // ccc 225..225 + 226, // ccc 226, Right + 227, // ccc 227..227 + 228, // ccc 228, Above_Left + 229, // ccc 229..229 + 230, // ccc 230, Above + 231, // ccc 231..231 + 232, // ccc 232, Above_Right + 233, // ccc 233, Double_Below + 234, // ccc 234, Double_Above + 235, 236, 237, 238, 239, // ccc 235..239 + 240, // ccc 240, Iota_Subscript + 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, // ccc 241..250 + 251, 252, 253, 254, // ccc 251..254 + 255, // ccc 255, Invalid + }; + + /// + /// Tries to get the order of a character that orders by where it is drawn rather + /// than by its class. + /// + /// + /// Written as a switch rather than a table because there are only a handful, and + /// because a span of anything wider than a byte is a fresh array on every access: + /// a table here would allocate once per character shaped. + /// + /// The code point to look up. + /// When this method returns, contains the order. + /// when the character orders by where it is drawn. + public static bool TryGetOverride(uint codePoint, out byte order) + { + switch (codePoint) + { + // TIBETAN MARK TSA -PHRU + case 0x0F39: + order = 127; + return true; + + // TIBETAN SYMBOL PADMA GDAN + case 0x0FC6: + order = 254; + return true; + + // TAI THAM SIGN SAKOT + case 0x1A60: + order = 254; + return true; + + default: + order = 0; + return false; + } + } + } +} diff --git a/src/SixLabors.Fonts/Unicode/Resources/NormalizationData.Generated.cs b/src/SixLabors.Fonts/Unicode/Resources/NormalizationData.Generated.cs new file mode 100644 index 000000000..9916bd4af --- /dev/null +++ b/src/SixLabors.Fonts/Unicode/Resources/NormalizationData.Generated.cs @@ -0,0 +1,3097 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// +using System; + +namespace SixLabors.Fonts.Unicode.Resources +{ + /// + /// The canonical decomposition and composition tables used when normalizing + /// text before it is shaped. + /// + internal static class NormalizationData + { + /// + /// Gets the canonical decompositions, ordered by the character that decomposes so that the table can be searched by it. + /// + /// Each entry packs three scalar values into 21 bits each, most significant first: the character that decomposes, + /// the first character it decomposes to, and the second. A singleton decomposition leaves the second character zero. + /// + /// + /// Hangul syllables are absent: they decompose by arithmetic. A compatibility decomposition is absent as well, + /// because normalization does not use one. + /// + /// + /// Each entry occupies eight bytes, least significant first, and is read with + /// . + /// + /// + public static ReadOnlySpan Decompositions => new byte[] + { + 0x00, 0x03, 0x20, 0x08, 0x00, 0x00, 0x03, 0x00, // 0x3000008200300 + 0x01, 0x03, 0x20, 0x08, 0x00, 0x04, 0x03, 0x00, // 0x3040008200301 + 0x02, 0x03, 0x20, 0x08, 0x00, 0x08, 0x03, 0x00, // 0x3080008200302 + 0x03, 0x03, 0x20, 0x08, 0x00, 0x0C, 0x03, 0x00, // 0x30C0008200303 + 0x08, 0x03, 0x20, 0x08, 0x00, 0x10, 0x03, 0x00, // 0x3100008200308 + 0x0A, 0x03, 0x20, 0x08, 0x00, 0x14, 0x03, 0x00, // 0x314000820030A + 0x27, 0x03, 0x60, 0x08, 0x00, 0x1C, 0x03, 0x00, // 0x31C0008600327 + 0x00, 0x03, 0xA0, 0x08, 0x00, 0x20, 0x03, 0x00, // 0x3200008A00300 + 0x01, 0x03, 0xA0, 0x08, 0x00, 0x24, 0x03, 0x00, // 0x3240008A00301 + 0x02, 0x03, 0xA0, 0x08, 0x00, 0x28, 0x03, 0x00, // 0x3280008A00302 + 0x08, 0x03, 0xA0, 0x08, 0x00, 0x2C, 0x03, 0x00, // 0x32C0008A00308 + 0x00, 0x03, 0x20, 0x09, 0x00, 0x30, 0x03, 0x00, // 0x3300009200300 + 0x01, 0x03, 0x20, 0x09, 0x00, 0x34, 0x03, 0x00, // 0x3340009200301 + 0x02, 0x03, 0x20, 0x09, 0x00, 0x38, 0x03, 0x00, // 0x3380009200302 + 0x08, 0x03, 0x20, 0x09, 0x00, 0x3C, 0x03, 0x00, // 0x33C0009200308 + 0x03, 0x03, 0xC0, 0x09, 0x00, 0x44, 0x03, 0x00, // 0x3440009C00303 + 0x00, 0x03, 0xE0, 0x09, 0x00, 0x48, 0x03, 0x00, // 0x3480009E00300 + 0x01, 0x03, 0xE0, 0x09, 0x00, 0x4C, 0x03, 0x00, // 0x34C0009E00301 + 0x02, 0x03, 0xE0, 0x09, 0x00, 0x50, 0x03, 0x00, // 0x3500009E00302 + 0x03, 0x03, 0xE0, 0x09, 0x00, 0x54, 0x03, 0x00, // 0x3540009E00303 + 0x08, 0x03, 0xE0, 0x09, 0x00, 0x58, 0x03, 0x00, // 0x3580009E00308 + 0x00, 0x03, 0xA0, 0x0A, 0x00, 0x64, 0x03, 0x00, // 0x364000AA00300 + 0x01, 0x03, 0xA0, 0x0A, 0x00, 0x68, 0x03, 0x00, // 0x368000AA00301 + 0x02, 0x03, 0xA0, 0x0A, 0x00, 0x6C, 0x03, 0x00, // 0x36C000AA00302 + 0x08, 0x03, 0xA0, 0x0A, 0x00, 0x70, 0x03, 0x00, // 0x370000AA00308 + 0x01, 0x03, 0x20, 0x0B, 0x00, 0x74, 0x03, 0x00, // 0x374000B200301 + 0x00, 0x03, 0x20, 0x0C, 0x00, 0x80, 0x03, 0x00, // 0x380000C200300 + 0x01, 0x03, 0x20, 0x0C, 0x00, 0x84, 0x03, 0x00, // 0x384000C200301 + 0x02, 0x03, 0x20, 0x0C, 0x00, 0x88, 0x03, 0x00, // 0x388000C200302 + 0x03, 0x03, 0x20, 0x0C, 0x00, 0x8C, 0x03, 0x00, // 0x38C000C200303 + 0x08, 0x03, 0x20, 0x0C, 0x00, 0x90, 0x03, 0x00, // 0x390000C200308 + 0x0A, 0x03, 0x20, 0x0C, 0x00, 0x94, 0x03, 0x00, // 0x394000C20030A + 0x27, 0x03, 0x60, 0x0C, 0x00, 0x9C, 0x03, 0x00, // 0x39C000C600327 + 0x00, 0x03, 0xA0, 0x0C, 0x00, 0xA0, 0x03, 0x00, // 0x3A0000CA00300 + 0x01, 0x03, 0xA0, 0x0C, 0x00, 0xA4, 0x03, 0x00, // 0x3A4000CA00301 + 0x02, 0x03, 0xA0, 0x0C, 0x00, 0xA8, 0x03, 0x00, // 0x3A8000CA00302 + 0x08, 0x03, 0xA0, 0x0C, 0x00, 0xAC, 0x03, 0x00, // 0x3AC000CA00308 + 0x00, 0x03, 0x20, 0x0D, 0x00, 0xB0, 0x03, 0x00, // 0x3B0000D200300 + 0x01, 0x03, 0x20, 0x0D, 0x00, 0xB4, 0x03, 0x00, // 0x3B4000D200301 + 0x02, 0x03, 0x20, 0x0D, 0x00, 0xB8, 0x03, 0x00, // 0x3B8000D200302 + 0x08, 0x03, 0x20, 0x0D, 0x00, 0xBC, 0x03, 0x00, // 0x3BC000D200308 + 0x03, 0x03, 0xC0, 0x0D, 0x00, 0xC4, 0x03, 0x00, // 0x3C4000DC00303 + 0x00, 0x03, 0xE0, 0x0D, 0x00, 0xC8, 0x03, 0x00, // 0x3C8000DE00300 + 0x01, 0x03, 0xE0, 0x0D, 0x00, 0xCC, 0x03, 0x00, // 0x3CC000DE00301 + 0x02, 0x03, 0xE0, 0x0D, 0x00, 0xD0, 0x03, 0x00, // 0x3D0000DE00302 + 0x03, 0x03, 0xE0, 0x0D, 0x00, 0xD4, 0x03, 0x00, // 0x3D4000DE00303 + 0x08, 0x03, 0xE0, 0x0D, 0x00, 0xD8, 0x03, 0x00, // 0x3D8000DE00308 + 0x00, 0x03, 0xA0, 0x0E, 0x00, 0xE4, 0x03, 0x00, // 0x3E4000EA00300 + 0x01, 0x03, 0xA0, 0x0E, 0x00, 0xE8, 0x03, 0x00, // 0x3E8000EA00301 + 0x02, 0x03, 0xA0, 0x0E, 0x00, 0xEC, 0x03, 0x00, // 0x3EC000EA00302 + 0x08, 0x03, 0xA0, 0x0E, 0x00, 0xF0, 0x03, 0x00, // 0x3F0000EA00308 + 0x01, 0x03, 0x20, 0x0F, 0x00, 0xF4, 0x03, 0x00, // 0x3F4000F200301 + 0x08, 0x03, 0x20, 0x0F, 0x00, 0xFC, 0x03, 0x00, // 0x3FC000F200308 + 0x04, 0x03, 0x20, 0x08, 0x00, 0x00, 0x04, 0x00, // 0x4000008200304 + 0x04, 0x03, 0x20, 0x0C, 0x00, 0x04, 0x04, 0x00, // 0x404000C200304 + 0x06, 0x03, 0x20, 0x08, 0x00, 0x08, 0x04, 0x00, // 0x4080008200306 + 0x06, 0x03, 0x20, 0x0C, 0x00, 0x0C, 0x04, 0x00, // 0x40C000C200306 + 0x28, 0x03, 0x20, 0x08, 0x00, 0x10, 0x04, 0x00, // 0x4100008200328 + 0x28, 0x03, 0x20, 0x0C, 0x00, 0x14, 0x04, 0x00, // 0x414000C200328 + 0x01, 0x03, 0x60, 0x08, 0x00, 0x18, 0x04, 0x00, // 0x4180008600301 + 0x01, 0x03, 0x60, 0x0C, 0x00, 0x1C, 0x04, 0x00, // 0x41C000C600301 + 0x02, 0x03, 0x60, 0x08, 0x00, 0x20, 0x04, 0x00, // 0x4200008600302 + 0x02, 0x03, 0x60, 0x0C, 0x00, 0x24, 0x04, 0x00, // 0x424000C600302 + 0x07, 0x03, 0x60, 0x08, 0x00, 0x28, 0x04, 0x00, // 0x4280008600307 + 0x07, 0x03, 0x60, 0x0C, 0x00, 0x2C, 0x04, 0x00, // 0x42C000C600307 + 0x0C, 0x03, 0x60, 0x08, 0x00, 0x30, 0x04, 0x00, // 0x430000860030C + 0x0C, 0x03, 0x60, 0x0C, 0x00, 0x34, 0x04, 0x00, // 0x434000C60030C + 0x0C, 0x03, 0x80, 0x08, 0x00, 0x38, 0x04, 0x00, // 0x438000880030C + 0x0C, 0x03, 0x80, 0x0C, 0x00, 0x3C, 0x04, 0x00, // 0x43C000C80030C + 0x04, 0x03, 0xA0, 0x08, 0x00, 0x48, 0x04, 0x00, // 0x4480008A00304 + 0x04, 0x03, 0xA0, 0x0C, 0x00, 0x4C, 0x04, 0x00, // 0x44C000CA00304 + 0x06, 0x03, 0xA0, 0x08, 0x00, 0x50, 0x04, 0x00, // 0x4500008A00306 + 0x06, 0x03, 0xA0, 0x0C, 0x00, 0x54, 0x04, 0x00, // 0x454000CA00306 + 0x07, 0x03, 0xA0, 0x08, 0x00, 0x58, 0x04, 0x00, // 0x4580008A00307 + 0x07, 0x03, 0xA0, 0x0C, 0x00, 0x5C, 0x04, 0x00, // 0x45C000CA00307 + 0x28, 0x03, 0xA0, 0x08, 0x00, 0x60, 0x04, 0x00, // 0x4600008A00328 + 0x28, 0x03, 0xA0, 0x0C, 0x00, 0x64, 0x04, 0x00, // 0x464000CA00328 + 0x0C, 0x03, 0xA0, 0x08, 0x00, 0x68, 0x04, 0x00, // 0x4680008A0030C + 0x0C, 0x03, 0xA0, 0x0C, 0x00, 0x6C, 0x04, 0x00, // 0x46C000CA0030C + 0x02, 0x03, 0xE0, 0x08, 0x00, 0x70, 0x04, 0x00, // 0x4700008E00302 + 0x02, 0x03, 0xE0, 0x0C, 0x00, 0x74, 0x04, 0x00, // 0x474000CE00302 + 0x06, 0x03, 0xE0, 0x08, 0x00, 0x78, 0x04, 0x00, // 0x4780008E00306 + 0x06, 0x03, 0xE0, 0x0C, 0x00, 0x7C, 0x04, 0x00, // 0x47C000CE00306 + 0x07, 0x03, 0xE0, 0x08, 0x00, 0x80, 0x04, 0x00, // 0x4800008E00307 + 0x07, 0x03, 0xE0, 0x0C, 0x00, 0x84, 0x04, 0x00, // 0x484000CE00307 + 0x27, 0x03, 0xE0, 0x08, 0x00, 0x88, 0x04, 0x00, // 0x4880008E00327 + 0x27, 0x03, 0xE0, 0x0C, 0x00, 0x8C, 0x04, 0x00, // 0x48C000CE00327 + 0x02, 0x03, 0x00, 0x09, 0x00, 0x90, 0x04, 0x00, // 0x4900009000302 + 0x02, 0x03, 0x00, 0x0D, 0x00, 0x94, 0x04, 0x00, // 0x494000D000302 + 0x03, 0x03, 0x20, 0x09, 0x00, 0xA0, 0x04, 0x00, // 0x4A00009200303 + 0x03, 0x03, 0x20, 0x0D, 0x00, 0xA4, 0x04, 0x00, // 0x4A4000D200303 + 0x04, 0x03, 0x20, 0x09, 0x00, 0xA8, 0x04, 0x00, // 0x4A80009200304 + 0x04, 0x03, 0x20, 0x0D, 0x00, 0xAC, 0x04, 0x00, // 0x4AC000D200304 + 0x06, 0x03, 0x20, 0x09, 0x00, 0xB0, 0x04, 0x00, // 0x4B00009200306 + 0x06, 0x03, 0x20, 0x0D, 0x00, 0xB4, 0x04, 0x00, // 0x4B4000D200306 + 0x28, 0x03, 0x20, 0x09, 0x00, 0xB8, 0x04, 0x00, // 0x4B80009200328 + 0x28, 0x03, 0x20, 0x0D, 0x00, 0xBC, 0x04, 0x00, // 0x4BC000D200328 + 0x07, 0x03, 0x20, 0x09, 0x00, 0xC0, 0x04, 0x00, // 0x4C00009200307 + 0x02, 0x03, 0x40, 0x09, 0x00, 0xD0, 0x04, 0x00, // 0x4D00009400302 + 0x02, 0x03, 0x40, 0x0D, 0x00, 0xD4, 0x04, 0x00, // 0x4D4000D400302 + 0x27, 0x03, 0x60, 0x09, 0x00, 0xD8, 0x04, 0x00, // 0x4D80009600327 + 0x27, 0x03, 0x60, 0x0D, 0x00, 0xDC, 0x04, 0x00, // 0x4DC000D600327 + 0x01, 0x03, 0x80, 0x09, 0x00, 0xE4, 0x04, 0x00, // 0x4E40009800301 + 0x01, 0x03, 0x80, 0x0D, 0x00, 0xE8, 0x04, 0x00, // 0x4E8000D800301 + 0x27, 0x03, 0x80, 0x09, 0x00, 0xEC, 0x04, 0x00, // 0x4EC0009800327 + 0x27, 0x03, 0x80, 0x0D, 0x00, 0xF0, 0x04, 0x00, // 0x4F0000D800327 + 0x0C, 0x03, 0x80, 0x09, 0x00, 0xF4, 0x04, 0x00, // 0x4F4000980030C + 0x0C, 0x03, 0x80, 0x0D, 0x00, 0xF8, 0x04, 0x00, // 0x4F8000D80030C + 0x01, 0x03, 0xC0, 0x09, 0x00, 0x0C, 0x05, 0x00, // 0x50C0009C00301 + 0x01, 0x03, 0xC0, 0x0D, 0x00, 0x10, 0x05, 0x00, // 0x510000DC00301 + 0x27, 0x03, 0xC0, 0x09, 0x00, 0x14, 0x05, 0x00, // 0x5140009C00327 + 0x27, 0x03, 0xC0, 0x0D, 0x00, 0x18, 0x05, 0x00, // 0x518000DC00327 + 0x0C, 0x03, 0xC0, 0x09, 0x00, 0x1C, 0x05, 0x00, // 0x51C0009C0030C + 0x0C, 0x03, 0xC0, 0x0D, 0x00, 0x20, 0x05, 0x00, // 0x520000DC0030C + 0x04, 0x03, 0xE0, 0x09, 0x00, 0x30, 0x05, 0x00, // 0x5300009E00304 + 0x04, 0x03, 0xE0, 0x0D, 0x00, 0x34, 0x05, 0x00, // 0x534000DE00304 + 0x06, 0x03, 0xE0, 0x09, 0x00, 0x38, 0x05, 0x00, // 0x5380009E00306 + 0x06, 0x03, 0xE0, 0x0D, 0x00, 0x3C, 0x05, 0x00, // 0x53C000DE00306 + 0x0B, 0x03, 0xE0, 0x09, 0x00, 0x40, 0x05, 0x00, // 0x5400009E0030B + 0x0B, 0x03, 0xE0, 0x0D, 0x00, 0x44, 0x05, 0x00, // 0x544000DE0030B + 0x01, 0x03, 0x40, 0x0A, 0x00, 0x50, 0x05, 0x00, // 0x550000A400301 + 0x01, 0x03, 0x40, 0x0E, 0x00, 0x54, 0x05, 0x00, // 0x554000E400301 + 0x27, 0x03, 0x40, 0x0A, 0x00, 0x58, 0x05, 0x00, // 0x558000A400327 + 0x27, 0x03, 0x40, 0x0E, 0x00, 0x5C, 0x05, 0x00, // 0x55C000E400327 + 0x0C, 0x03, 0x40, 0x0A, 0x00, 0x60, 0x05, 0x00, // 0x560000A40030C + 0x0C, 0x03, 0x40, 0x0E, 0x00, 0x64, 0x05, 0x00, // 0x564000E40030C + 0x01, 0x03, 0x60, 0x0A, 0x00, 0x68, 0x05, 0x00, // 0x568000A600301 + 0x01, 0x03, 0x60, 0x0E, 0x00, 0x6C, 0x05, 0x00, // 0x56C000E600301 + 0x02, 0x03, 0x60, 0x0A, 0x00, 0x70, 0x05, 0x00, // 0x570000A600302 + 0x02, 0x03, 0x60, 0x0E, 0x00, 0x74, 0x05, 0x00, // 0x574000E600302 + 0x27, 0x03, 0x60, 0x0A, 0x00, 0x78, 0x05, 0x00, // 0x578000A600327 + 0x27, 0x03, 0x60, 0x0E, 0x00, 0x7C, 0x05, 0x00, // 0x57C000E600327 + 0x0C, 0x03, 0x60, 0x0A, 0x00, 0x80, 0x05, 0x00, // 0x580000A60030C + 0x0C, 0x03, 0x60, 0x0E, 0x00, 0x84, 0x05, 0x00, // 0x584000E60030C + 0x27, 0x03, 0x80, 0x0A, 0x00, 0x88, 0x05, 0x00, // 0x588000A800327 + 0x27, 0x03, 0x80, 0x0E, 0x00, 0x8C, 0x05, 0x00, // 0x58C000E800327 + 0x0C, 0x03, 0x80, 0x0A, 0x00, 0x90, 0x05, 0x00, // 0x590000A80030C + 0x0C, 0x03, 0x80, 0x0E, 0x00, 0x94, 0x05, 0x00, // 0x594000E80030C + 0x03, 0x03, 0xA0, 0x0A, 0x00, 0xA0, 0x05, 0x00, // 0x5A0000AA00303 + 0x03, 0x03, 0xA0, 0x0E, 0x00, 0xA4, 0x05, 0x00, // 0x5A4000EA00303 + 0x04, 0x03, 0xA0, 0x0A, 0x00, 0xA8, 0x05, 0x00, // 0x5A8000AA00304 + 0x04, 0x03, 0xA0, 0x0E, 0x00, 0xAC, 0x05, 0x00, // 0x5AC000EA00304 + 0x06, 0x03, 0xA0, 0x0A, 0x00, 0xB0, 0x05, 0x00, // 0x5B0000AA00306 + 0x06, 0x03, 0xA0, 0x0E, 0x00, 0xB4, 0x05, 0x00, // 0x5B4000EA00306 + 0x0A, 0x03, 0xA0, 0x0A, 0x00, 0xB8, 0x05, 0x00, // 0x5B8000AA0030A + 0x0A, 0x03, 0xA0, 0x0E, 0x00, 0xBC, 0x05, 0x00, // 0x5BC000EA0030A + 0x0B, 0x03, 0xA0, 0x0A, 0x00, 0xC0, 0x05, 0x00, // 0x5C0000AA0030B + 0x0B, 0x03, 0xA0, 0x0E, 0x00, 0xC4, 0x05, 0x00, // 0x5C4000EA0030B + 0x28, 0x03, 0xA0, 0x0A, 0x00, 0xC8, 0x05, 0x00, // 0x5C8000AA00328 + 0x28, 0x03, 0xA0, 0x0E, 0x00, 0xCC, 0x05, 0x00, // 0x5CC000EA00328 + 0x02, 0x03, 0xE0, 0x0A, 0x00, 0xD0, 0x05, 0x00, // 0x5D0000AE00302 + 0x02, 0x03, 0xE0, 0x0E, 0x00, 0xD4, 0x05, 0x00, // 0x5D4000EE00302 + 0x02, 0x03, 0x20, 0x0B, 0x00, 0xD8, 0x05, 0x00, // 0x5D8000B200302 + 0x02, 0x03, 0x20, 0x0F, 0x00, 0xDC, 0x05, 0x00, // 0x5DC000F200302 + 0x08, 0x03, 0x20, 0x0B, 0x00, 0xE0, 0x05, 0x00, // 0x5E0000B200308 + 0x01, 0x03, 0x40, 0x0B, 0x00, 0xE4, 0x05, 0x00, // 0x5E4000B400301 + 0x01, 0x03, 0x40, 0x0F, 0x00, 0xE8, 0x05, 0x00, // 0x5E8000F400301 + 0x07, 0x03, 0x40, 0x0B, 0x00, 0xEC, 0x05, 0x00, // 0x5EC000B400307 + 0x07, 0x03, 0x40, 0x0F, 0x00, 0xF0, 0x05, 0x00, // 0x5F0000F400307 + 0x0C, 0x03, 0x40, 0x0B, 0x00, 0xF4, 0x05, 0x00, // 0x5F4000B40030C + 0x0C, 0x03, 0x40, 0x0F, 0x00, 0xF8, 0x05, 0x00, // 0x5F8000F40030C + 0x1B, 0x03, 0xE0, 0x09, 0x00, 0x80, 0x06, 0x00, // 0x6800009E0031B + 0x1B, 0x03, 0xE0, 0x0D, 0x00, 0x84, 0x06, 0x00, // 0x684000DE0031B + 0x1B, 0x03, 0xA0, 0x0A, 0x00, 0xBC, 0x06, 0x00, // 0x6BC000AA0031B + 0x1B, 0x03, 0xA0, 0x0E, 0x00, 0xC0, 0x06, 0x00, // 0x6C0000EA0031B + 0x0C, 0x03, 0x20, 0x08, 0x00, 0x34, 0x07, 0x00, // 0x734000820030C + 0x0C, 0x03, 0x20, 0x0C, 0x00, 0x38, 0x07, 0x00, // 0x738000C20030C + 0x0C, 0x03, 0x20, 0x09, 0x00, 0x3C, 0x07, 0x00, // 0x73C000920030C + 0x0C, 0x03, 0x20, 0x0D, 0x00, 0x40, 0x07, 0x00, // 0x740000D20030C + 0x0C, 0x03, 0xE0, 0x09, 0x00, 0x44, 0x07, 0x00, // 0x7440009E0030C + 0x0C, 0x03, 0xE0, 0x0D, 0x00, 0x48, 0x07, 0x00, // 0x748000DE0030C + 0x0C, 0x03, 0xA0, 0x0A, 0x00, 0x4C, 0x07, 0x00, // 0x74C000AA0030C + 0x0C, 0x03, 0xA0, 0x0E, 0x00, 0x50, 0x07, 0x00, // 0x750000EA0030C + 0x04, 0x03, 0x80, 0x1B, 0x00, 0x54, 0x07, 0x00, // 0x754001B800304 + 0x04, 0x03, 0x80, 0x1F, 0x00, 0x58, 0x07, 0x00, // 0x758001F800304 + 0x01, 0x03, 0x80, 0x1B, 0x00, 0x5C, 0x07, 0x00, // 0x75C001B800301 + 0x01, 0x03, 0x80, 0x1F, 0x00, 0x60, 0x07, 0x00, // 0x760001F800301 + 0x0C, 0x03, 0x80, 0x1B, 0x00, 0x64, 0x07, 0x00, // 0x764001B80030C + 0x0C, 0x03, 0x80, 0x1F, 0x00, 0x68, 0x07, 0x00, // 0x768001F80030C + 0x00, 0x03, 0x80, 0x1B, 0x00, 0x6C, 0x07, 0x00, // 0x76C001B800300 + 0x00, 0x03, 0x80, 0x1F, 0x00, 0x70, 0x07, 0x00, // 0x770001F800300 + 0x04, 0x03, 0x80, 0x18, 0x00, 0x78, 0x07, 0x00, // 0x7780018800304 + 0x04, 0x03, 0x80, 0x1C, 0x00, 0x7C, 0x07, 0x00, // 0x77C001C800304 + 0x04, 0x03, 0xC0, 0x44, 0x00, 0x80, 0x07, 0x00, // 0x7800044C00304 + 0x04, 0x03, 0xE0, 0x44, 0x00, 0x84, 0x07, 0x00, // 0x7840044E00304 + 0x04, 0x03, 0xC0, 0x18, 0x00, 0x88, 0x07, 0x00, // 0x7880018C00304 + 0x04, 0x03, 0xC0, 0x1C, 0x00, 0x8C, 0x07, 0x00, // 0x78C001CC00304 + 0x0C, 0x03, 0xE0, 0x08, 0x00, 0x98, 0x07, 0x00, // 0x7980008E0030C + 0x0C, 0x03, 0xE0, 0x0C, 0x00, 0x9C, 0x07, 0x00, // 0x79C000CE0030C + 0x0C, 0x03, 0x60, 0x09, 0x00, 0xA0, 0x07, 0x00, // 0x7A0000960030C + 0x0C, 0x03, 0x60, 0x0D, 0x00, 0xA4, 0x07, 0x00, // 0x7A4000D60030C + 0x28, 0x03, 0xE0, 0x09, 0x00, 0xA8, 0x07, 0x00, // 0x7A80009E00328 + 0x28, 0x03, 0xE0, 0x0D, 0x00, 0xAC, 0x07, 0x00, // 0x7AC000DE00328 + 0x04, 0x03, 0x40, 0x3D, 0x00, 0xB0, 0x07, 0x00, // 0x7B0003D400304 + 0x04, 0x03, 0x60, 0x3D, 0x00, 0xB4, 0x07, 0x00, // 0x7B4003D600304 + 0x0C, 0x03, 0xE0, 0x36, 0x00, 0xB8, 0x07, 0x00, // 0x7B80036E0030C + 0x0C, 0x03, 0x40, 0x52, 0x00, 0xBC, 0x07, 0x00, // 0x7BC005240030C + 0x0C, 0x03, 0x40, 0x0D, 0x00, 0xC0, 0x07, 0x00, // 0x7C0000D40030C + 0x01, 0x03, 0xE0, 0x08, 0x00, 0xD0, 0x07, 0x00, // 0x7D00008E00301 + 0x01, 0x03, 0xE0, 0x0C, 0x00, 0xD4, 0x07, 0x00, // 0x7D4000CE00301 + 0x00, 0x03, 0xC0, 0x09, 0x00, 0xE0, 0x07, 0x00, // 0x7E00009C00300 + 0x00, 0x03, 0xC0, 0x0D, 0x00, 0xE4, 0x07, 0x00, // 0x7E4000DC00300 + 0x01, 0x03, 0xA0, 0x18, 0x00, 0xE8, 0x07, 0x00, // 0x7E80018A00301 + 0x01, 0x03, 0xA0, 0x1C, 0x00, 0xEC, 0x07, 0x00, // 0x7EC001CA00301 + 0x01, 0x03, 0xC0, 0x18, 0x00, 0xF0, 0x07, 0x00, // 0x7F00018C00301 + 0x01, 0x03, 0xC0, 0x1C, 0x00, 0xF4, 0x07, 0x00, // 0x7F4001CC00301 + 0x01, 0x03, 0x00, 0x1B, 0x00, 0xF8, 0x07, 0x00, // 0x7F8001B000301 + 0x01, 0x03, 0x00, 0x1F, 0x00, 0xFC, 0x07, 0x00, // 0x7FC001F000301 + 0x0F, 0x03, 0x20, 0x08, 0x00, 0x00, 0x08, 0x00, // 0x800000820030F + 0x0F, 0x03, 0x20, 0x0C, 0x00, 0x04, 0x08, 0x00, // 0x804000C20030F + 0x11, 0x03, 0x20, 0x08, 0x00, 0x08, 0x08, 0x00, // 0x8080008200311 + 0x11, 0x03, 0x20, 0x0C, 0x00, 0x0C, 0x08, 0x00, // 0x80C000C200311 + 0x0F, 0x03, 0xA0, 0x08, 0x00, 0x10, 0x08, 0x00, // 0x8100008A0030F + 0x0F, 0x03, 0xA0, 0x0C, 0x00, 0x14, 0x08, 0x00, // 0x814000CA0030F + 0x11, 0x03, 0xA0, 0x08, 0x00, 0x18, 0x08, 0x00, // 0x8180008A00311 + 0x11, 0x03, 0xA0, 0x0C, 0x00, 0x1C, 0x08, 0x00, // 0x81C000CA00311 + 0x0F, 0x03, 0x20, 0x09, 0x00, 0x20, 0x08, 0x00, // 0x820000920030F + 0x0F, 0x03, 0x20, 0x0D, 0x00, 0x24, 0x08, 0x00, // 0x824000D20030F + 0x11, 0x03, 0x20, 0x09, 0x00, 0x28, 0x08, 0x00, // 0x8280009200311 + 0x11, 0x03, 0x20, 0x0D, 0x00, 0x2C, 0x08, 0x00, // 0x82C000D200311 + 0x0F, 0x03, 0xE0, 0x09, 0x00, 0x30, 0x08, 0x00, // 0x8300009E0030F + 0x0F, 0x03, 0xE0, 0x0D, 0x00, 0x34, 0x08, 0x00, // 0x834000DE0030F + 0x11, 0x03, 0xE0, 0x09, 0x00, 0x38, 0x08, 0x00, // 0x8380009E00311 + 0x11, 0x03, 0xE0, 0x0D, 0x00, 0x3C, 0x08, 0x00, // 0x83C000DE00311 + 0x0F, 0x03, 0x40, 0x0A, 0x00, 0x40, 0x08, 0x00, // 0x840000A40030F + 0x0F, 0x03, 0x40, 0x0E, 0x00, 0x44, 0x08, 0x00, // 0x844000E40030F + 0x11, 0x03, 0x40, 0x0A, 0x00, 0x48, 0x08, 0x00, // 0x848000A400311 + 0x11, 0x03, 0x40, 0x0E, 0x00, 0x4C, 0x08, 0x00, // 0x84C000E400311 + 0x0F, 0x03, 0xA0, 0x0A, 0x00, 0x50, 0x08, 0x00, // 0x850000AA0030F + 0x0F, 0x03, 0xA0, 0x0E, 0x00, 0x54, 0x08, 0x00, // 0x854000EA0030F + 0x11, 0x03, 0xA0, 0x0A, 0x00, 0x58, 0x08, 0x00, // 0x858000AA00311 + 0x11, 0x03, 0xA0, 0x0E, 0x00, 0x5C, 0x08, 0x00, // 0x85C000EA00311 + 0x26, 0x03, 0x60, 0x0A, 0x00, 0x60, 0x08, 0x00, // 0x860000A600326 + 0x26, 0x03, 0x60, 0x0E, 0x00, 0x64, 0x08, 0x00, // 0x864000E600326 + 0x26, 0x03, 0x80, 0x0A, 0x00, 0x68, 0x08, 0x00, // 0x868000A800326 + 0x26, 0x03, 0x80, 0x0E, 0x00, 0x6C, 0x08, 0x00, // 0x86C000E800326 + 0x0C, 0x03, 0x00, 0x09, 0x00, 0x78, 0x08, 0x00, // 0x878000900030C + 0x0C, 0x03, 0x00, 0x0D, 0x00, 0x7C, 0x08, 0x00, // 0x87C000D00030C + 0x07, 0x03, 0x20, 0x08, 0x00, 0x98, 0x08, 0x00, // 0x8980008200307 + 0x07, 0x03, 0x20, 0x0C, 0x00, 0x9C, 0x08, 0x00, // 0x89C000C200307 + 0x27, 0x03, 0xA0, 0x08, 0x00, 0xA0, 0x08, 0x00, // 0x8A00008A00327 + 0x27, 0x03, 0xA0, 0x0C, 0x00, 0xA4, 0x08, 0x00, // 0x8A4000CA00327 + 0x04, 0x03, 0xC0, 0x1A, 0x00, 0xA8, 0x08, 0x00, // 0x8A8001AC00304 + 0x04, 0x03, 0xC0, 0x1E, 0x00, 0xAC, 0x08, 0x00, // 0x8AC001EC00304 + 0x04, 0x03, 0xA0, 0x1A, 0x00, 0xB0, 0x08, 0x00, // 0x8B0001AA00304 + 0x04, 0x03, 0xA0, 0x1E, 0x00, 0xB4, 0x08, 0x00, // 0x8B4001EA00304 + 0x07, 0x03, 0xE0, 0x09, 0x00, 0xB8, 0x08, 0x00, // 0x8B80009E00307 + 0x07, 0x03, 0xE0, 0x0D, 0x00, 0xBC, 0x08, 0x00, // 0x8BC000DE00307 + 0x04, 0x03, 0xC0, 0x45, 0x00, 0xC0, 0x08, 0x00, // 0x8C00045C00304 + 0x04, 0x03, 0xE0, 0x45, 0x00, 0xC4, 0x08, 0x00, // 0x8C40045E00304 + 0x04, 0x03, 0x20, 0x0B, 0x00, 0xC8, 0x08, 0x00, // 0x8C8000B200304 + 0x04, 0x03, 0x20, 0x0F, 0x00, 0xCC, 0x08, 0x00, // 0x8CC000F200304 + 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x0D, 0x00, // 0xD000060000000 + 0x00, 0x00, 0x20, 0x60, 0x00, 0x04, 0x0D, 0x00, // 0xD040060200000 + 0x00, 0x00, 0x60, 0x62, 0x00, 0x0C, 0x0D, 0x00, // 0xD0C0062600000 + 0x01, 0x03, 0x00, 0x61, 0x00, 0x10, 0x0D, 0x00, // 0xD100061000301 + 0x00, 0x00, 0x20, 0x57, 0x00, 0xD0, 0x0D, 0x00, // 0xDD00057200000 + 0x00, 0x00, 0x60, 0x07, 0x00, 0xF8, 0x0D, 0x00, // 0xDF80007600000 + 0x01, 0x03, 0x00, 0x15, 0x00, 0x14, 0x0E, 0x00, // 0xE140015000301 + 0x01, 0x03, 0x20, 0x72, 0x00, 0x18, 0x0E, 0x00, // 0xE180072200301 + 0x00, 0x00, 0xE0, 0x16, 0x00, 0x1C, 0x0E, 0x00, // 0xE1C0016E00000 + 0x01, 0x03, 0xA0, 0x72, 0x00, 0x20, 0x0E, 0x00, // 0xE200072A00301 + 0x01, 0x03, 0xE0, 0x72, 0x00, 0x24, 0x0E, 0x00, // 0xE240072E00301 + 0x01, 0x03, 0x20, 0x73, 0x00, 0x28, 0x0E, 0x00, // 0xE280073200301 + 0x01, 0x03, 0xE0, 0x73, 0x00, 0x30, 0x0E, 0x00, // 0xE300073E00301 + 0x01, 0x03, 0xA0, 0x74, 0x00, 0x38, 0x0E, 0x00, // 0xE380074A00301 + 0x01, 0x03, 0x20, 0x75, 0x00, 0x3C, 0x0E, 0x00, // 0xE3C0075200301 + 0x01, 0x03, 0x40, 0x79, 0x00, 0x40, 0x0E, 0x00, // 0xE400079400301 + 0x08, 0x03, 0x20, 0x73, 0x00, 0xA8, 0x0E, 0x00, // 0xEA80073200308 + 0x08, 0x03, 0xA0, 0x74, 0x00, 0xAC, 0x0E, 0x00, // 0xEAC0074A00308 + 0x01, 0x03, 0x20, 0x76, 0x00, 0xB0, 0x0E, 0x00, // 0xEB00076200301 + 0x01, 0x03, 0xA0, 0x76, 0x00, 0xB4, 0x0E, 0x00, // 0xEB40076A00301 + 0x01, 0x03, 0xE0, 0x76, 0x00, 0xB8, 0x0E, 0x00, // 0xEB80076E00301 + 0x01, 0x03, 0x20, 0x77, 0x00, 0xBC, 0x0E, 0x00, // 0xEBC0077200301 + 0x01, 0x03, 0x60, 0x79, 0x00, 0xC0, 0x0E, 0x00, // 0xEC00079600301 + 0x08, 0x03, 0x20, 0x77, 0x00, 0x28, 0x0F, 0x00, // 0xF280077200308 + 0x08, 0x03, 0xA0, 0x78, 0x00, 0x2C, 0x0F, 0x00, // 0xF2C0078A00308 + 0x01, 0x03, 0xE0, 0x77, 0x00, 0x30, 0x0F, 0x00, // 0xF300077E00301 + 0x01, 0x03, 0xA0, 0x78, 0x00, 0x34, 0x0F, 0x00, // 0xF340078A00301 + 0x01, 0x03, 0x20, 0x79, 0x00, 0x38, 0x0F, 0x00, // 0xF380079200301 + 0x01, 0x03, 0x40, 0x7A, 0x00, 0x4C, 0x0F, 0x00, // 0xF4C007A400301 + 0x08, 0x03, 0x40, 0x7A, 0x00, 0x50, 0x0F, 0x00, // 0xF50007A400308 + 0x00, 0x03, 0xA0, 0x82, 0x00, 0x00, 0x10, 0x00, // 0x10000082A00300 + 0x08, 0x03, 0xA0, 0x82, 0x00, 0x04, 0x10, 0x00, // 0x10040082A00308 + 0x01, 0x03, 0x60, 0x82, 0x00, 0x0C, 0x10, 0x00, // 0x100C0082600301 + 0x08, 0x03, 0xC0, 0x80, 0x00, 0x1C, 0x10, 0x00, // 0x101C0080C00308 + 0x01, 0x03, 0x40, 0x83, 0x00, 0x30, 0x10, 0x00, // 0x10300083400301 + 0x00, 0x03, 0x00, 0x83, 0x00, 0x34, 0x10, 0x00, // 0x10340083000300 + 0x06, 0x03, 0x60, 0x84, 0x00, 0x38, 0x10, 0x00, // 0x10380084600306 + 0x06, 0x03, 0x00, 0x83, 0x00, 0x64, 0x10, 0x00, // 0x10640083000306 + 0x06, 0x03, 0x00, 0x87, 0x00, 0xE4, 0x10, 0x00, // 0x10E40087000306 + 0x00, 0x03, 0xA0, 0x86, 0x00, 0x40, 0x11, 0x00, // 0x11400086A00300 + 0x08, 0x03, 0xA0, 0x86, 0x00, 0x44, 0x11, 0x00, // 0x11440086A00308 + 0x01, 0x03, 0x60, 0x86, 0x00, 0x4C, 0x11, 0x00, // 0x114C0086600301 + 0x08, 0x03, 0xC0, 0x8A, 0x00, 0x5C, 0x11, 0x00, // 0x115C008AC00308 + 0x01, 0x03, 0x40, 0x87, 0x00, 0x70, 0x11, 0x00, // 0x11700087400301 + 0x00, 0x03, 0x00, 0x87, 0x00, 0x74, 0x11, 0x00, // 0x11740087000300 + 0x06, 0x03, 0x60, 0x88, 0x00, 0x78, 0x11, 0x00, // 0x11780088600306 + 0x0F, 0x03, 0x80, 0x8E, 0x00, 0xD8, 0x11, 0x00, // 0x11D8008E80030F + 0x0F, 0x03, 0xA0, 0x8E, 0x00, 0xDC, 0x11, 0x00, // 0x11DC008EA0030F + 0x06, 0x03, 0xC0, 0x82, 0x00, 0x04, 0x13, 0x00, // 0x13040082C00306 + 0x06, 0x03, 0xC0, 0x86, 0x00, 0x08, 0x13, 0x00, // 0x13080086C00306 + 0x06, 0x03, 0x00, 0x82, 0x00, 0x40, 0x13, 0x00, // 0x13400082000306 + 0x06, 0x03, 0x00, 0x86, 0x00, 0x44, 0x13, 0x00, // 0x13440086000306 + 0x08, 0x03, 0x00, 0x82, 0x00, 0x48, 0x13, 0x00, // 0x13480082000308 + 0x08, 0x03, 0x00, 0x86, 0x00, 0x4C, 0x13, 0x00, // 0x134C0086000308 + 0x06, 0x03, 0xA0, 0x82, 0x00, 0x58, 0x13, 0x00, // 0x13580082A00306 + 0x06, 0x03, 0xA0, 0x86, 0x00, 0x5C, 0x13, 0x00, // 0x135C0086A00306 + 0x08, 0x03, 0x00, 0x9B, 0x00, 0x68, 0x13, 0x00, // 0x1368009B000308 + 0x08, 0x03, 0x20, 0x9B, 0x00, 0x6C, 0x13, 0x00, // 0x136C009B200308 + 0x08, 0x03, 0xC0, 0x82, 0x00, 0x70, 0x13, 0x00, // 0x13700082C00308 + 0x08, 0x03, 0xC0, 0x86, 0x00, 0x74, 0x13, 0x00, // 0x13740086C00308 + 0x08, 0x03, 0xE0, 0x82, 0x00, 0x78, 0x13, 0x00, // 0x13780082E00308 + 0x08, 0x03, 0xE0, 0x86, 0x00, 0x7C, 0x13, 0x00, // 0x137C0086E00308 + 0x04, 0x03, 0x00, 0x83, 0x00, 0x88, 0x13, 0x00, // 0x13880083000304 + 0x04, 0x03, 0x00, 0x87, 0x00, 0x8C, 0x13, 0x00, // 0x138C0087000304 + 0x08, 0x03, 0x00, 0x83, 0x00, 0x90, 0x13, 0x00, // 0x13900083000308 + 0x08, 0x03, 0x00, 0x87, 0x00, 0x94, 0x13, 0x00, // 0x13940087000308 + 0x08, 0x03, 0xC0, 0x83, 0x00, 0x98, 0x13, 0x00, // 0x13980083C00308 + 0x08, 0x03, 0xC0, 0x87, 0x00, 0x9C, 0x13, 0x00, // 0x139C0087C00308 + 0x08, 0x03, 0x00, 0x9D, 0x00, 0xA8, 0x13, 0x00, // 0x13A8009D000308 + 0x08, 0x03, 0x20, 0x9D, 0x00, 0xAC, 0x13, 0x00, // 0x13AC009D200308 + 0x08, 0x03, 0xA0, 0x85, 0x00, 0xB0, 0x13, 0x00, // 0x13B00085A00308 + 0x08, 0x03, 0xA0, 0x89, 0x00, 0xB4, 0x13, 0x00, // 0x13B40089A00308 + 0x04, 0x03, 0x60, 0x84, 0x00, 0xB8, 0x13, 0x00, // 0x13B80084600304 + 0x04, 0x03, 0x60, 0x88, 0x00, 0xBC, 0x13, 0x00, // 0x13BC0088600304 + 0x08, 0x03, 0x60, 0x84, 0x00, 0xC0, 0x13, 0x00, // 0x13C00084600308 + 0x08, 0x03, 0x60, 0x88, 0x00, 0xC4, 0x13, 0x00, // 0x13C40088600308 + 0x0B, 0x03, 0x60, 0x84, 0x00, 0xC8, 0x13, 0x00, // 0x13C8008460030B + 0x0B, 0x03, 0x60, 0x88, 0x00, 0xCC, 0x13, 0x00, // 0x13CC008860030B + 0x08, 0x03, 0xE0, 0x84, 0x00, 0xD0, 0x13, 0x00, // 0x13D00084E00308 + 0x08, 0x03, 0xE0, 0x88, 0x00, 0xD4, 0x13, 0x00, // 0x13D40088E00308 + 0x08, 0x03, 0x60, 0x85, 0x00, 0xE0, 0x13, 0x00, // 0x13E00085600308 + 0x08, 0x03, 0x60, 0x89, 0x00, 0xE4, 0x13, 0x00, // 0x13E40089600308 + 0x53, 0x06, 0xE0, 0xC4, 0x00, 0x88, 0x18, 0x00, // 0x188800C4E00653 + 0x54, 0x06, 0xE0, 0xC4, 0x00, 0x8C, 0x18, 0x00, // 0x188C00C4E00654 + 0x54, 0x06, 0x00, 0xC9, 0x00, 0x90, 0x18, 0x00, // 0x189000C9000654 + 0x55, 0x06, 0xE0, 0xC4, 0x00, 0x94, 0x18, 0x00, // 0x189400C4E00655 + 0x54, 0x06, 0x40, 0xC9, 0x00, 0x98, 0x18, 0x00, // 0x189800C9400654 + 0x54, 0x06, 0xA0, 0xDA, 0x00, 0x00, 0x1B, 0x00, // 0x1B0000DAA00654 + 0x54, 0x06, 0x20, 0xD8, 0x00, 0x08, 0x1B, 0x00, // 0x1B0800D8200654 + 0x54, 0x06, 0x40, 0xDA, 0x00, 0x4C, 0x1B, 0x00, // 0x1B4C00DA400654 + 0x3C, 0x09, 0x00, 0x25, 0x01, 0xA4, 0x24, 0x00, // 0x24A4012500093C + 0x3C, 0x09, 0x00, 0x26, 0x01, 0xC4, 0x24, 0x00, // 0x24C4012600093C + 0x3C, 0x09, 0x60, 0x26, 0x01, 0xD0, 0x24, 0x00, // 0x24D0012660093C + 0x3C, 0x09, 0xA0, 0x22, 0x01, 0x60, 0x25, 0x00, // 0x25600122A0093C + 0x3C, 0x09, 0xC0, 0x22, 0x01, 0x64, 0x25, 0x00, // 0x25640122C0093C + 0x3C, 0x09, 0xE0, 0x22, 0x01, 0x68, 0x25, 0x00, // 0x25680122E0093C + 0x3C, 0x09, 0x80, 0x23, 0x01, 0x6C, 0x25, 0x00, // 0x256C012380093C + 0x3C, 0x09, 0x20, 0x24, 0x01, 0x70, 0x25, 0x00, // 0x2570012420093C + 0x3C, 0x09, 0x40, 0x24, 0x01, 0x74, 0x25, 0x00, // 0x2574012440093C + 0x3C, 0x09, 0x60, 0x25, 0x01, 0x78, 0x25, 0x00, // 0x2578012560093C + 0x3C, 0x09, 0xE0, 0x25, 0x01, 0x7C, 0x25, 0x00, // 0x257C0125E0093C + 0xBE, 0x09, 0xE0, 0x38, 0x01, 0x2C, 0x27, 0x00, // 0x272C0138E009BE + 0xD7, 0x09, 0xE0, 0x38, 0x01, 0x30, 0x27, 0x00, // 0x27300138E009D7 + 0xBC, 0x09, 0x20, 0x34, 0x01, 0x70, 0x27, 0x00, // 0x277001342009BC + 0xBC, 0x09, 0x40, 0x34, 0x01, 0x74, 0x27, 0x00, // 0x277401344009BC + 0xBC, 0x09, 0xE0, 0x35, 0x01, 0x7C, 0x27, 0x00, // 0x277C0135E009BC + 0x3C, 0x0A, 0x40, 0x46, 0x01, 0xCC, 0x28, 0x00, // 0x28CC0146400A3C + 0x3C, 0x0A, 0x00, 0x47, 0x01, 0xD8, 0x28, 0x00, // 0x28D80147000A3C + 0x3C, 0x0A, 0xC0, 0x42, 0x01, 0x64, 0x29, 0x00, // 0x29640142C00A3C + 0x3C, 0x0A, 0xE0, 0x42, 0x01, 0x68, 0x29, 0x00, // 0x29680142E00A3C + 0x3C, 0x0A, 0x80, 0x43, 0x01, 0x6C, 0x29, 0x00, // 0x296C0143800A3C + 0x3C, 0x0A, 0x60, 0x45, 0x01, 0x78, 0x29, 0x00, // 0x29780145600A3C + 0x56, 0x0B, 0xE0, 0x68, 0x01, 0x20, 0x2D, 0x00, // 0x2D200168E00B56 + 0x3E, 0x0B, 0xE0, 0x68, 0x01, 0x2C, 0x2D, 0x00, // 0x2D2C0168E00B3E + 0x57, 0x0B, 0xE0, 0x68, 0x01, 0x30, 0x2D, 0x00, // 0x2D300168E00B57 + 0x3C, 0x0B, 0x20, 0x64, 0x01, 0x70, 0x2D, 0x00, // 0x2D700164200B3C + 0x3C, 0x0B, 0x40, 0x64, 0x01, 0x74, 0x2D, 0x00, // 0x2D740164400B3C + 0xD7, 0x0B, 0x40, 0x72, 0x01, 0x50, 0x2E, 0x00, // 0x2E500172400BD7 + 0xBE, 0x0B, 0xC0, 0x78, 0x01, 0x28, 0x2F, 0x00, // 0x2F280178C00BBE + 0xBE, 0x0B, 0xE0, 0x78, 0x01, 0x2C, 0x2F, 0x00, // 0x2F2C0178E00BBE + 0xD7, 0x0B, 0xC0, 0x78, 0x01, 0x30, 0x2F, 0x00, // 0x2F300178C00BD7 + 0x56, 0x0C, 0xC0, 0x88, 0x01, 0x20, 0x31, 0x00, // 0x31200188C00C56 + 0xD5, 0x0C, 0xE0, 0x97, 0x01, 0x00, 0x33, 0x00, // 0x33000197E00CD5 + 0xD5, 0x0C, 0xC0, 0x98, 0x01, 0x1C, 0x33, 0x00, // 0x331C0198C00CD5 + 0xD6, 0x0C, 0xC0, 0x98, 0x01, 0x20, 0x33, 0x00, // 0x33200198C00CD6 + 0xC2, 0x0C, 0xC0, 0x98, 0x01, 0x28, 0x33, 0x00, // 0x33280198C00CC2 + 0xD5, 0x0C, 0x40, 0x99, 0x01, 0x2C, 0x33, 0x00, // 0x332C0199400CD5 + 0x3E, 0x0D, 0xC0, 0xA8, 0x01, 0x28, 0x35, 0x00, // 0x352801A8C00D3E + 0x3E, 0x0D, 0xE0, 0xA8, 0x01, 0x2C, 0x35, 0x00, // 0x352C01A8E00D3E + 0x57, 0x0D, 0xC0, 0xA8, 0x01, 0x30, 0x35, 0x00, // 0x353001A8C00D57 + 0xCA, 0x0D, 0x20, 0xBB, 0x01, 0x68, 0x37, 0x00, // 0x376801BB200DCA + 0xCF, 0x0D, 0x20, 0xBB, 0x01, 0x70, 0x37, 0x00, // 0x377001BB200DCF + 0xCA, 0x0D, 0x80, 0xBB, 0x01, 0x74, 0x37, 0x00, // 0x377401BB800DCA + 0xDF, 0x0D, 0x20, 0xBB, 0x01, 0x78, 0x37, 0x00, // 0x377801BB200DDF + 0xB7, 0x0F, 0x40, 0xE8, 0x01, 0x0C, 0x3D, 0x00, // 0x3D0C01E8400FB7 + 0xB7, 0x0F, 0x80, 0xE9, 0x01, 0x34, 0x3D, 0x00, // 0x3D3401E9800FB7 + 0xB7, 0x0F, 0x20, 0xEA, 0x01, 0x48, 0x3D, 0x00, // 0x3D4801EA200FB7 + 0xB7, 0x0F, 0xC0, 0xEA, 0x01, 0x5C, 0x3D, 0x00, // 0x3D5C01EAC00FB7 + 0xB7, 0x0F, 0x60, 0xEB, 0x01, 0x70, 0x3D, 0x00, // 0x3D7001EB600FB7 + 0xB5, 0x0F, 0x00, 0xE8, 0x01, 0xA4, 0x3D, 0x00, // 0x3DA401E8000FB5 + 0x72, 0x0F, 0x20, 0xEE, 0x01, 0xCC, 0x3D, 0x00, // 0x3DCC01EE200F72 + 0x74, 0x0F, 0x20, 0xEE, 0x01, 0xD4, 0x3D, 0x00, // 0x3DD401EE200F74 + 0x80, 0x0F, 0x40, 0xF6, 0x01, 0xD8, 0x3D, 0x00, // 0x3DD801F6400F80 + 0x80, 0x0F, 0x60, 0xF6, 0x01, 0xE0, 0x3D, 0x00, // 0x3DE001F6600F80 + 0x80, 0x0F, 0x20, 0xEE, 0x01, 0x04, 0x3E, 0x00, // 0x3E0401EE200F80 + 0xB7, 0x0F, 0x40, 0xF2, 0x01, 0x4C, 0x3E, 0x00, // 0x3E4C01F2400FB7 + 0xB7, 0x0F, 0x80, 0xF3, 0x01, 0x74, 0x3E, 0x00, // 0x3E7401F3800FB7 + 0xB7, 0x0F, 0x20, 0xF4, 0x01, 0x88, 0x3E, 0x00, // 0x3E8801F4200FB7 + 0xB7, 0x0F, 0xC0, 0xF4, 0x01, 0x9C, 0x3E, 0x00, // 0x3E9C01F4C00FB7 + 0xB7, 0x0F, 0x60, 0xF5, 0x01, 0xB0, 0x3E, 0x00, // 0x3EB001F5600FB7 + 0xB5, 0x0F, 0x00, 0xF2, 0x01, 0xE4, 0x3E, 0x00, // 0x3EE401F2000FB5 + 0x2E, 0x10, 0xA0, 0x04, 0x02, 0x98, 0x40, 0x00, // 0x40980204A0102E + 0x35, 0x1B, 0xA0, 0x60, 0x03, 0x18, 0x6C, 0x00, // 0x6C180360A01B35 + 0x35, 0x1B, 0xE0, 0x60, 0x03, 0x20, 0x6C, 0x00, // 0x6C200360E01B35 + 0x35, 0x1B, 0x20, 0x61, 0x03, 0x28, 0x6C, 0x00, // 0x6C280361201B35 + 0x35, 0x1B, 0x60, 0x61, 0x03, 0x30, 0x6C, 0x00, // 0x6C300361601B35 + 0x35, 0x1B, 0xA0, 0x61, 0x03, 0x38, 0x6C, 0x00, // 0x6C380361A01B35 + 0x35, 0x1B, 0x20, 0x62, 0x03, 0x48, 0x6C, 0x00, // 0x6C480362201B35 + 0x35, 0x1B, 0x40, 0x67, 0x03, 0xEC, 0x6C, 0x00, // 0x6CEC0367401B35 + 0x35, 0x1B, 0x80, 0x67, 0x03, 0xF4, 0x6C, 0x00, // 0x6CF40367801B35 + 0x35, 0x1B, 0xC0, 0x67, 0x03, 0x00, 0x6D, 0x00, // 0x6D000367C01B35 + 0x35, 0x1B, 0xE0, 0x67, 0x03, 0x04, 0x6D, 0x00, // 0x6D040367E01B35 + 0x35, 0x1B, 0x40, 0x68, 0x03, 0x0C, 0x6D, 0x00, // 0x6D0C0368401B35 + 0x25, 0x03, 0x20, 0x08, 0x00, 0x00, 0x78, 0x00, // 0x78000008200325 + 0x25, 0x03, 0x20, 0x0C, 0x00, 0x04, 0x78, 0x00, // 0x7804000C200325 + 0x07, 0x03, 0x40, 0x08, 0x00, 0x08, 0x78, 0x00, // 0x78080008400307 + 0x07, 0x03, 0x40, 0x0C, 0x00, 0x0C, 0x78, 0x00, // 0x780C000C400307 + 0x23, 0x03, 0x40, 0x08, 0x00, 0x10, 0x78, 0x00, // 0x78100008400323 + 0x23, 0x03, 0x40, 0x0C, 0x00, 0x14, 0x78, 0x00, // 0x7814000C400323 + 0x31, 0x03, 0x40, 0x08, 0x00, 0x18, 0x78, 0x00, // 0x78180008400331 + 0x31, 0x03, 0x40, 0x0C, 0x00, 0x1C, 0x78, 0x00, // 0x781C000C400331 + 0x01, 0x03, 0xE0, 0x18, 0x00, 0x20, 0x78, 0x00, // 0x78200018E00301 + 0x01, 0x03, 0xE0, 0x1C, 0x00, 0x24, 0x78, 0x00, // 0x7824001CE00301 + 0x07, 0x03, 0x80, 0x08, 0x00, 0x28, 0x78, 0x00, // 0x78280008800307 + 0x07, 0x03, 0x80, 0x0C, 0x00, 0x2C, 0x78, 0x00, // 0x782C000C800307 + 0x23, 0x03, 0x80, 0x08, 0x00, 0x30, 0x78, 0x00, // 0x78300008800323 + 0x23, 0x03, 0x80, 0x0C, 0x00, 0x34, 0x78, 0x00, // 0x7834000C800323 + 0x31, 0x03, 0x80, 0x08, 0x00, 0x38, 0x78, 0x00, // 0x78380008800331 + 0x31, 0x03, 0x80, 0x0C, 0x00, 0x3C, 0x78, 0x00, // 0x783C000C800331 + 0x27, 0x03, 0x80, 0x08, 0x00, 0x40, 0x78, 0x00, // 0x78400008800327 + 0x27, 0x03, 0x80, 0x0C, 0x00, 0x44, 0x78, 0x00, // 0x7844000C800327 + 0x2D, 0x03, 0x80, 0x08, 0x00, 0x48, 0x78, 0x00, // 0x7848000880032D + 0x2D, 0x03, 0x80, 0x0C, 0x00, 0x4C, 0x78, 0x00, // 0x784C000C80032D + 0x00, 0x03, 0x40, 0x22, 0x00, 0x50, 0x78, 0x00, // 0x78500022400300 + 0x00, 0x03, 0x60, 0x22, 0x00, 0x54, 0x78, 0x00, // 0x78540022600300 + 0x01, 0x03, 0x40, 0x22, 0x00, 0x58, 0x78, 0x00, // 0x78580022400301 + 0x01, 0x03, 0x60, 0x22, 0x00, 0x5C, 0x78, 0x00, // 0x785C0022600301 + 0x2D, 0x03, 0xA0, 0x08, 0x00, 0x60, 0x78, 0x00, // 0x78600008A0032D + 0x2D, 0x03, 0xA0, 0x0C, 0x00, 0x64, 0x78, 0x00, // 0x7864000CA0032D + 0x30, 0x03, 0xA0, 0x08, 0x00, 0x68, 0x78, 0x00, // 0x78680008A00330 + 0x30, 0x03, 0xA0, 0x0C, 0x00, 0x6C, 0x78, 0x00, // 0x786C000CA00330 + 0x06, 0x03, 0x00, 0x45, 0x00, 0x70, 0x78, 0x00, // 0x78700045000306 + 0x06, 0x03, 0x20, 0x45, 0x00, 0x74, 0x78, 0x00, // 0x78740045200306 + 0x07, 0x03, 0xC0, 0x08, 0x00, 0x78, 0x78, 0x00, // 0x78780008C00307 + 0x07, 0x03, 0xC0, 0x0C, 0x00, 0x7C, 0x78, 0x00, // 0x787C000CC00307 + 0x04, 0x03, 0xE0, 0x08, 0x00, 0x80, 0x78, 0x00, // 0x78800008E00304 + 0x04, 0x03, 0xE0, 0x0C, 0x00, 0x84, 0x78, 0x00, // 0x7884000CE00304 + 0x07, 0x03, 0x00, 0x09, 0x00, 0x88, 0x78, 0x00, // 0x78880009000307 + 0x07, 0x03, 0x00, 0x0D, 0x00, 0x8C, 0x78, 0x00, // 0x788C000D000307 + 0x23, 0x03, 0x00, 0x09, 0x00, 0x90, 0x78, 0x00, // 0x78900009000323 + 0x23, 0x03, 0x00, 0x0D, 0x00, 0x94, 0x78, 0x00, // 0x7894000D000323 + 0x08, 0x03, 0x00, 0x09, 0x00, 0x98, 0x78, 0x00, // 0x78980009000308 + 0x08, 0x03, 0x00, 0x0D, 0x00, 0x9C, 0x78, 0x00, // 0x789C000D000308 + 0x27, 0x03, 0x00, 0x09, 0x00, 0xA0, 0x78, 0x00, // 0x78A00009000327 + 0x27, 0x03, 0x00, 0x0D, 0x00, 0xA4, 0x78, 0x00, // 0x78A4000D000327 + 0x2E, 0x03, 0x00, 0x09, 0x00, 0xA8, 0x78, 0x00, // 0x78A8000900032E + 0x2E, 0x03, 0x00, 0x0D, 0x00, 0xAC, 0x78, 0x00, // 0x78AC000D00032E + 0x30, 0x03, 0x20, 0x09, 0x00, 0xB0, 0x78, 0x00, // 0x78B00009200330 + 0x30, 0x03, 0x20, 0x0D, 0x00, 0xB4, 0x78, 0x00, // 0x78B4000D200330 + 0x01, 0x03, 0xE0, 0x19, 0x00, 0xB8, 0x78, 0x00, // 0x78B80019E00301 + 0x01, 0x03, 0xE0, 0x1D, 0x00, 0xBC, 0x78, 0x00, // 0x78BC001DE00301 + 0x01, 0x03, 0x60, 0x09, 0x00, 0xC0, 0x78, 0x00, // 0x78C00009600301 + 0x01, 0x03, 0x60, 0x0D, 0x00, 0xC4, 0x78, 0x00, // 0x78C4000D600301 + 0x23, 0x03, 0x60, 0x09, 0x00, 0xC8, 0x78, 0x00, // 0x78C80009600323 + 0x23, 0x03, 0x60, 0x0D, 0x00, 0xCC, 0x78, 0x00, // 0x78CC000D600323 + 0x31, 0x03, 0x60, 0x09, 0x00, 0xD0, 0x78, 0x00, // 0x78D00009600331 + 0x31, 0x03, 0x60, 0x0D, 0x00, 0xD4, 0x78, 0x00, // 0x78D4000D600331 + 0x23, 0x03, 0x80, 0x09, 0x00, 0xD8, 0x78, 0x00, // 0x78D80009800323 + 0x23, 0x03, 0x80, 0x0D, 0x00, 0xDC, 0x78, 0x00, // 0x78DC000D800323 + 0x04, 0x03, 0xC0, 0xC6, 0x03, 0xE0, 0x78, 0x00, // 0x78E003C6C00304 + 0x04, 0x03, 0xE0, 0xC6, 0x03, 0xE4, 0x78, 0x00, // 0x78E403C6E00304 + 0x31, 0x03, 0x80, 0x09, 0x00, 0xE8, 0x78, 0x00, // 0x78E80009800331 + 0x31, 0x03, 0x80, 0x0D, 0x00, 0xEC, 0x78, 0x00, // 0x78EC000D800331 + 0x2D, 0x03, 0x80, 0x09, 0x00, 0xF0, 0x78, 0x00, // 0x78F0000980032D + 0x2D, 0x03, 0x80, 0x0D, 0x00, 0xF4, 0x78, 0x00, // 0x78F4000D80032D + 0x01, 0x03, 0xA0, 0x09, 0x00, 0xF8, 0x78, 0x00, // 0x78F80009A00301 + 0x01, 0x03, 0xA0, 0x0D, 0x00, 0xFC, 0x78, 0x00, // 0x78FC000DA00301 + 0x07, 0x03, 0xA0, 0x09, 0x00, 0x00, 0x79, 0x00, // 0x79000009A00307 + 0x07, 0x03, 0xA0, 0x0D, 0x00, 0x04, 0x79, 0x00, // 0x7904000DA00307 + 0x23, 0x03, 0xA0, 0x09, 0x00, 0x08, 0x79, 0x00, // 0x79080009A00323 + 0x23, 0x03, 0xA0, 0x0D, 0x00, 0x0C, 0x79, 0x00, // 0x790C000DA00323 + 0x07, 0x03, 0xC0, 0x09, 0x00, 0x10, 0x79, 0x00, // 0x79100009C00307 + 0x07, 0x03, 0xC0, 0x0D, 0x00, 0x14, 0x79, 0x00, // 0x7914000DC00307 + 0x23, 0x03, 0xC0, 0x09, 0x00, 0x18, 0x79, 0x00, // 0x79180009C00323 + 0x23, 0x03, 0xC0, 0x0D, 0x00, 0x1C, 0x79, 0x00, // 0x791C000DC00323 + 0x31, 0x03, 0xC0, 0x09, 0x00, 0x20, 0x79, 0x00, // 0x79200009C00331 + 0x31, 0x03, 0xC0, 0x0D, 0x00, 0x24, 0x79, 0x00, // 0x7924000DC00331 + 0x2D, 0x03, 0xC0, 0x09, 0x00, 0x28, 0x79, 0x00, // 0x79280009C0032D + 0x2D, 0x03, 0xC0, 0x0D, 0x00, 0x2C, 0x79, 0x00, // 0x792C000DC0032D + 0x01, 0x03, 0xA0, 0x1A, 0x00, 0x30, 0x79, 0x00, // 0x7930001AA00301 + 0x01, 0x03, 0xA0, 0x1E, 0x00, 0x34, 0x79, 0x00, // 0x7934001EA00301 + 0x08, 0x03, 0xA0, 0x1A, 0x00, 0x38, 0x79, 0x00, // 0x7938001AA00308 + 0x08, 0x03, 0xA0, 0x1E, 0x00, 0x3C, 0x79, 0x00, // 0x793C001EA00308 + 0x00, 0x03, 0x80, 0x29, 0x00, 0x40, 0x79, 0x00, // 0x79400029800300 + 0x00, 0x03, 0xA0, 0x29, 0x00, 0x44, 0x79, 0x00, // 0x79440029A00300 + 0x01, 0x03, 0x80, 0x29, 0x00, 0x48, 0x79, 0x00, // 0x79480029800301 + 0x01, 0x03, 0xA0, 0x29, 0x00, 0x4C, 0x79, 0x00, // 0x794C0029A00301 + 0x01, 0x03, 0x00, 0x0A, 0x00, 0x50, 0x79, 0x00, // 0x7950000A000301 + 0x01, 0x03, 0x00, 0x0E, 0x00, 0x54, 0x79, 0x00, // 0x7954000E000301 + 0x07, 0x03, 0x00, 0x0A, 0x00, 0x58, 0x79, 0x00, // 0x7958000A000307 + 0x07, 0x03, 0x00, 0x0E, 0x00, 0x5C, 0x79, 0x00, // 0x795C000E000307 + 0x07, 0x03, 0x40, 0x0A, 0x00, 0x60, 0x79, 0x00, // 0x7960000A400307 + 0x07, 0x03, 0x40, 0x0E, 0x00, 0x64, 0x79, 0x00, // 0x7964000E400307 + 0x23, 0x03, 0x40, 0x0A, 0x00, 0x68, 0x79, 0x00, // 0x7968000A400323 + 0x23, 0x03, 0x40, 0x0E, 0x00, 0x6C, 0x79, 0x00, // 0x796C000E400323 + 0x04, 0x03, 0x40, 0xCB, 0x03, 0x70, 0x79, 0x00, // 0x797003CB400304 + 0x04, 0x03, 0x60, 0xCB, 0x03, 0x74, 0x79, 0x00, // 0x797403CB600304 + 0x31, 0x03, 0x40, 0x0A, 0x00, 0x78, 0x79, 0x00, // 0x7978000A400331 + 0x31, 0x03, 0x40, 0x0E, 0x00, 0x7C, 0x79, 0x00, // 0x797C000E400331 + 0x07, 0x03, 0x60, 0x0A, 0x00, 0x80, 0x79, 0x00, // 0x7980000A600307 + 0x07, 0x03, 0x60, 0x0E, 0x00, 0x84, 0x79, 0x00, // 0x7984000E600307 + 0x23, 0x03, 0x60, 0x0A, 0x00, 0x88, 0x79, 0x00, // 0x7988000A600323 + 0x23, 0x03, 0x60, 0x0E, 0x00, 0x8C, 0x79, 0x00, // 0x798C000E600323 + 0x07, 0x03, 0x40, 0x2B, 0x00, 0x90, 0x79, 0x00, // 0x7990002B400307 + 0x07, 0x03, 0x60, 0x2B, 0x00, 0x94, 0x79, 0x00, // 0x7994002B600307 + 0x07, 0x03, 0x00, 0x2C, 0x00, 0x98, 0x79, 0x00, // 0x7998002C000307 + 0x07, 0x03, 0x20, 0x2C, 0x00, 0x9C, 0x79, 0x00, // 0x799C002C200307 + 0x07, 0x03, 0x40, 0xCC, 0x03, 0xA0, 0x79, 0x00, // 0x79A003CC400307 + 0x07, 0x03, 0x60, 0xCC, 0x03, 0xA4, 0x79, 0x00, // 0x79A403CC600307 + 0x07, 0x03, 0x80, 0x0A, 0x00, 0xA8, 0x79, 0x00, // 0x79A8000A800307 + 0x07, 0x03, 0x80, 0x0E, 0x00, 0xAC, 0x79, 0x00, // 0x79AC000E800307 + 0x23, 0x03, 0x80, 0x0A, 0x00, 0xB0, 0x79, 0x00, // 0x79B0000A800323 + 0x23, 0x03, 0x80, 0x0E, 0x00, 0xB4, 0x79, 0x00, // 0x79B4000E800323 + 0x31, 0x03, 0x80, 0x0A, 0x00, 0xB8, 0x79, 0x00, // 0x79B8000A800331 + 0x31, 0x03, 0x80, 0x0E, 0x00, 0xBC, 0x79, 0x00, // 0x79BC000E800331 + 0x2D, 0x03, 0x80, 0x0A, 0x00, 0xC0, 0x79, 0x00, // 0x79C0000A80032D + 0x2D, 0x03, 0x80, 0x0E, 0x00, 0xC4, 0x79, 0x00, // 0x79C4000E80032D + 0x24, 0x03, 0xA0, 0x0A, 0x00, 0xC8, 0x79, 0x00, // 0x79C8000AA00324 + 0x24, 0x03, 0xA0, 0x0E, 0x00, 0xCC, 0x79, 0x00, // 0x79CC000EA00324 + 0x30, 0x03, 0xA0, 0x0A, 0x00, 0xD0, 0x79, 0x00, // 0x79D0000AA00330 + 0x30, 0x03, 0xA0, 0x0E, 0x00, 0xD4, 0x79, 0x00, // 0x79D4000EA00330 + 0x2D, 0x03, 0xA0, 0x0A, 0x00, 0xD8, 0x79, 0x00, // 0x79D8000AA0032D + 0x2D, 0x03, 0xA0, 0x0E, 0x00, 0xDC, 0x79, 0x00, // 0x79DC000EA0032D + 0x01, 0x03, 0x00, 0x2D, 0x00, 0xE0, 0x79, 0x00, // 0x79E0002D000301 + 0x01, 0x03, 0x20, 0x2D, 0x00, 0xE4, 0x79, 0x00, // 0x79E4002D200301 + 0x08, 0x03, 0x40, 0x2D, 0x00, 0xE8, 0x79, 0x00, // 0x79E8002D400308 + 0x08, 0x03, 0x60, 0x2D, 0x00, 0xEC, 0x79, 0x00, // 0x79EC002D600308 + 0x03, 0x03, 0xC0, 0x0A, 0x00, 0xF0, 0x79, 0x00, // 0x79F0000AC00303 + 0x03, 0x03, 0xC0, 0x0E, 0x00, 0xF4, 0x79, 0x00, // 0x79F4000EC00303 + 0x23, 0x03, 0xC0, 0x0A, 0x00, 0xF8, 0x79, 0x00, // 0x79F8000AC00323 + 0x23, 0x03, 0xC0, 0x0E, 0x00, 0xFC, 0x79, 0x00, // 0x79FC000EC00323 + 0x00, 0x03, 0xE0, 0x0A, 0x00, 0x00, 0x7A, 0x00, // 0x7A00000AE00300 + 0x00, 0x03, 0xE0, 0x0E, 0x00, 0x04, 0x7A, 0x00, // 0x7A04000EE00300 + 0x01, 0x03, 0xE0, 0x0A, 0x00, 0x08, 0x7A, 0x00, // 0x7A08000AE00301 + 0x01, 0x03, 0xE0, 0x0E, 0x00, 0x0C, 0x7A, 0x00, // 0x7A0C000EE00301 + 0x08, 0x03, 0xE0, 0x0A, 0x00, 0x10, 0x7A, 0x00, // 0x7A10000AE00308 + 0x08, 0x03, 0xE0, 0x0E, 0x00, 0x14, 0x7A, 0x00, // 0x7A14000EE00308 + 0x07, 0x03, 0xE0, 0x0A, 0x00, 0x18, 0x7A, 0x00, // 0x7A18000AE00307 + 0x07, 0x03, 0xE0, 0x0E, 0x00, 0x1C, 0x7A, 0x00, // 0x7A1C000EE00307 + 0x23, 0x03, 0xE0, 0x0A, 0x00, 0x20, 0x7A, 0x00, // 0x7A20000AE00323 + 0x23, 0x03, 0xE0, 0x0E, 0x00, 0x24, 0x7A, 0x00, // 0x7A24000EE00323 + 0x07, 0x03, 0x00, 0x0B, 0x00, 0x28, 0x7A, 0x00, // 0x7A28000B000307 + 0x07, 0x03, 0x00, 0x0F, 0x00, 0x2C, 0x7A, 0x00, // 0x7A2C000F000307 + 0x08, 0x03, 0x00, 0x0B, 0x00, 0x30, 0x7A, 0x00, // 0x7A30000B000308 + 0x08, 0x03, 0x00, 0x0F, 0x00, 0x34, 0x7A, 0x00, // 0x7A34000F000308 + 0x07, 0x03, 0x20, 0x0B, 0x00, 0x38, 0x7A, 0x00, // 0x7A38000B200307 + 0x07, 0x03, 0x20, 0x0F, 0x00, 0x3C, 0x7A, 0x00, // 0x7A3C000F200307 + 0x02, 0x03, 0x40, 0x0B, 0x00, 0x40, 0x7A, 0x00, // 0x7A40000B400302 + 0x02, 0x03, 0x40, 0x0F, 0x00, 0x44, 0x7A, 0x00, // 0x7A44000F400302 + 0x23, 0x03, 0x40, 0x0B, 0x00, 0x48, 0x7A, 0x00, // 0x7A48000B400323 + 0x23, 0x03, 0x40, 0x0F, 0x00, 0x4C, 0x7A, 0x00, // 0x7A4C000F400323 + 0x31, 0x03, 0x40, 0x0B, 0x00, 0x50, 0x7A, 0x00, // 0x7A50000B400331 + 0x31, 0x03, 0x40, 0x0F, 0x00, 0x54, 0x7A, 0x00, // 0x7A54000F400331 + 0x31, 0x03, 0x00, 0x0D, 0x00, 0x58, 0x7A, 0x00, // 0x7A58000D000331 + 0x08, 0x03, 0x80, 0x0E, 0x00, 0x5C, 0x7A, 0x00, // 0x7A5C000E800308 + 0x0A, 0x03, 0xE0, 0x0E, 0x00, 0x60, 0x7A, 0x00, // 0x7A60000EE0030A + 0x0A, 0x03, 0x20, 0x0F, 0x00, 0x64, 0x7A, 0x00, // 0x7A64000F20030A + 0x07, 0x03, 0xE0, 0x2F, 0x00, 0x6C, 0x7A, 0x00, // 0x7A6C002FE00307 + 0x23, 0x03, 0x20, 0x08, 0x00, 0x80, 0x7A, 0x00, // 0x7A800008200323 + 0x23, 0x03, 0x20, 0x0C, 0x00, 0x84, 0x7A, 0x00, // 0x7A84000C200323 + 0x09, 0x03, 0x20, 0x08, 0x00, 0x88, 0x7A, 0x00, // 0x7A880008200309 + 0x09, 0x03, 0x20, 0x0C, 0x00, 0x8C, 0x7A, 0x00, // 0x7A8C000C200309 + 0x01, 0x03, 0x40, 0x18, 0x00, 0x90, 0x7A, 0x00, // 0x7A900018400301 + 0x01, 0x03, 0x40, 0x1C, 0x00, 0x94, 0x7A, 0x00, // 0x7A94001C400301 + 0x00, 0x03, 0x40, 0x18, 0x00, 0x98, 0x7A, 0x00, // 0x7A980018400300 + 0x00, 0x03, 0x40, 0x1C, 0x00, 0x9C, 0x7A, 0x00, // 0x7A9C001C400300 + 0x09, 0x03, 0x40, 0x18, 0x00, 0xA0, 0x7A, 0x00, // 0x7AA00018400309 + 0x09, 0x03, 0x40, 0x1C, 0x00, 0xA4, 0x7A, 0x00, // 0x7AA4001C400309 + 0x03, 0x03, 0x40, 0x18, 0x00, 0xA8, 0x7A, 0x00, // 0x7AA80018400303 + 0x03, 0x03, 0x40, 0x1C, 0x00, 0xAC, 0x7A, 0x00, // 0x7AAC001C400303 + 0x02, 0x03, 0x00, 0xD4, 0x03, 0xB0, 0x7A, 0x00, // 0x7AB003D4000302 + 0x02, 0x03, 0x20, 0xD4, 0x03, 0xB4, 0x7A, 0x00, // 0x7AB403D4200302 + 0x01, 0x03, 0x40, 0x20, 0x00, 0xB8, 0x7A, 0x00, // 0x7AB80020400301 + 0x01, 0x03, 0x60, 0x20, 0x00, 0xBC, 0x7A, 0x00, // 0x7ABC0020600301 + 0x00, 0x03, 0x40, 0x20, 0x00, 0xC0, 0x7A, 0x00, // 0x7AC00020400300 + 0x00, 0x03, 0x60, 0x20, 0x00, 0xC4, 0x7A, 0x00, // 0x7AC40020600300 + 0x09, 0x03, 0x40, 0x20, 0x00, 0xC8, 0x7A, 0x00, // 0x7AC80020400309 + 0x09, 0x03, 0x60, 0x20, 0x00, 0xCC, 0x7A, 0x00, // 0x7ACC0020600309 + 0x03, 0x03, 0x40, 0x20, 0x00, 0xD0, 0x7A, 0x00, // 0x7AD00020400303 + 0x03, 0x03, 0x60, 0x20, 0x00, 0xD4, 0x7A, 0x00, // 0x7AD40020600303 + 0x06, 0x03, 0x00, 0xD4, 0x03, 0xD8, 0x7A, 0x00, // 0x7AD803D4000306 + 0x06, 0x03, 0x20, 0xD4, 0x03, 0xDC, 0x7A, 0x00, // 0x7ADC03D4200306 + 0x23, 0x03, 0xA0, 0x08, 0x00, 0xE0, 0x7A, 0x00, // 0x7AE00008A00323 + 0x23, 0x03, 0xA0, 0x0C, 0x00, 0xE4, 0x7A, 0x00, // 0x7AE4000CA00323 + 0x09, 0x03, 0xA0, 0x08, 0x00, 0xE8, 0x7A, 0x00, // 0x7AE80008A00309 + 0x09, 0x03, 0xA0, 0x0C, 0x00, 0xEC, 0x7A, 0x00, // 0x7AEC000CA00309 + 0x03, 0x03, 0xA0, 0x08, 0x00, 0xF0, 0x7A, 0x00, // 0x7AF00008A00303 + 0x03, 0x03, 0xA0, 0x0C, 0x00, 0xF4, 0x7A, 0x00, // 0x7AF4000CA00303 + 0x01, 0x03, 0x40, 0x19, 0x00, 0xF8, 0x7A, 0x00, // 0x7AF80019400301 + 0x01, 0x03, 0x40, 0x1D, 0x00, 0xFC, 0x7A, 0x00, // 0x7AFC001D400301 + 0x00, 0x03, 0x40, 0x19, 0x00, 0x00, 0x7B, 0x00, // 0x7B000019400300 + 0x00, 0x03, 0x40, 0x1D, 0x00, 0x04, 0x7B, 0x00, // 0x7B04001D400300 + 0x09, 0x03, 0x40, 0x19, 0x00, 0x08, 0x7B, 0x00, // 0x7B080019400309 + 0x09, 0x03, 0x40, 0x1D, 0x00, 0x0C, 0x7B, 0x00, // 0x7B0C001D400309 + 0x03, 0x03, 0x40, 0x19, 0x00, 0x10, 0x7B, 0x00, // 0x7B100019400303 + 0x03, 0x03, 0x40, 0x1D, 0x00, 0x14, 0x7B, 0x00, // 0x7B14001D400303 + 0x02, 0x03, 0x00, 0xD7, 0x03, 0x18, 0x7B, 0x00, // 0x7B1803D7000302 + 0x02, 0x03, 0x20, 0xD7, 0x03, 0x1C, 0x7B, 0x00, // 0x7B1C03D7200302 + 0x09, 0x03, 0x20, 0x09, 0x00, 0x20, 0x7B, 0x00, // 0x7B200009200309 + 0x09, 0x03, 0x20, 0x0D, 0x00, 0x24, 0x7B, 0x00, // 0x7B24000D200309 + 0x23, 0x03, 0x20, 0x09, 0x00, 0x28, 0x7B, 0x00, // 0x7B280009200323 + 0x23, 0x03, 0x20, 0x0D, 0x00, 0x2C, 0x7B, 0x00, // 0x7B2C000D200323 + 0x23, 0x03, 0xE0, 0x09, 0x00, 0x30, 0x7B, 0x00, // 0x7B300009E00323 + 0x23, 0x03, 0xE0, 0x0D, 0x00, 0x34, 0x7B, 0x00, // 0x7B34000DE00323 + 0x09, 0x03, 0xE0, 0x09, 0x00, 0x38, 0x7B, 0x00, // 0x7B380009E00309 + 0x09, 0x03, 0xE0, 0x0D, 0x00, 0x3C, 0x7B, 0x00, // 0x7B3C000DE00309 + 0x01, 0x03, 0x80, 0x1A, 0x00, 0x40, 0x7B, 0x00, // 0x7B40001A800301 + 0x01, 0x03, 0x80, 0x1E, 0x00, 0x44, 0x7B, 0x00, // 0x7B44001E800301 + 0x00, 0x03, 0x80, 0x1A, 0x00, 0x48, 0x7B, 0x00, // 0x7B48001A800300 + 0x00, 0x03, 0x80, 0x1E, 0x00, 0x4C, 0x7B, 0x00, // 0x7B4C001E800300 + 0x09, 0x03, 0x80, 0x1A, 0x00, 0x50, 0x7B, 0x00, // 0x7B50001A800309 + 0x09, 0x03, 0x80, 0x1E, 0x00, 0x54, 0x7B, 0x00, // 0x7B54001E800309 + 0x03, 0x03, 0x80, 0x1A, 0x00, 0x58, 0x7B, 0x00, // 0x7B58001A800303 + 0x03, 0x03, 0x80, 0x1E, 0x00, 0x5C, 0x7B, 0x00, // 0x7B5C001E800303 + 0x02, 0x03, 0x80, 0xD9, 0x03, 0x60, 0x7B, 0x00, // 0x7B6003D9800302 + 0x02, 0x03, 0xA0, 0xD9, 0x03, 0x64, 0x7B, 0x00, // 0x7B6403D9A00302 + 0x01, 0x03, 0x00, 0x34, 0x00, 0x68, 0x7B, 0x00, // 0x7B680034000301 + 0x01, 0x03, 0x20, 0x34, 0x00, 0x6C, 0x7B, 0x00, // 0x7B6C0034200301 + 0x00, 0x03, 0x00, 0x34, 0x00, 0x70, 0x7B, 0x00, // 0x7B700034000300 + 0x00, 0x03, 0x20, 0x34, 0x00, 0x74, 0x7B, 0x00, // 0x7B740034200300 + 0x09, 0x03, 0x00, 0x34, 0x00, 0x78, 0x7B, 0x00, // 0x7B780034000309 + 0x09, 0x03, 0x20, 0x34, 0x00, 0x7C, 0x7B, 0x00, // 0x7B7C0034200309 + 0x03, 0x03, 0x00, 0x34, 0x00, 0x80, 0x7B, 0x00, // 0x7B800034000303 + 0x03, 0x03, 0x20, 0x34, 0x00, 0x84, 0x7B, 0x00, // 0x7B840034200303 + 0x23, 0x03, 0x00, 0x34, 0x00, 0x88, 0x7B, 0x00, // 0x7B880034000323 + 0x23, 0x03, 0x20, 0x34, 0x00, 0x8C, 0x7B, 0x00, // 0x7B8C0034200323 + 0x23, 0x03, 0xA0, 0x0A, 0x00, 0x90, 0x7B, 0x00, // 0x7B90000AA00323 + 0x23, 0x03, 0xA0, 0x0E, 0x00, 0x94, 0x7B, 0x00, // 0x7B94000EA00323 + 0x09, 0x03, 0xA0, 0x0A, 0x00, 0x98, 0x7B, 0x00, // 0x7B98000AA00309 + 0x09, 0x03, 0xA0, 0x0E, 0x00, 0x9C, 0x7B, 0x00, // 0x7B9C000EA00309 + 0x01, 0x03, 0xE0, 0x35, 0x00, 0xA0, 0x7B, 0x00, // 0x7BA00035E00301 + 0x01, 0x03, 0x00, 0x36, 0x00, 0xA4, 0x7B, 0x00, // 0x7BA40036000301 + 0x00, 0x03, 0xE0, 0x35, 0x00, 0xA8, 0x7B, 0x00, // 0x7BA80035E00300 + 0x00, 0x03, 0x00, 0x36, 0x00, 0xAC, 0x7B, 0x00, // 0x7BAC0036000300 + 0x09, 0x03, 0xE0, 0x35, 0x00, 0xB0, 0x7B, 0x00, // 0x7BB00035E00309 + 0x09, 0x03, 0x00, 0x36, 0x00, 0xB4, 0x7B, 0x00, // 0x7BB40036000309 + 0x03, 0x03, 0xE0, 0x35, 0x00, 0xB8, 0x7B, 0x00, // 0x7BB80035E00303 + 0x03, 0x03, 0x00, 0x36, 0x00, 0xBC, 0x7B, 0x00, // 0x7BBC0036000303 + 0x23, 0x03, 0xE0, 0x35, 0x00, 0xC0, 0x7B, 0x00, // 0x7BC00035E00323 + 0x23, 0x03, 0x00, 0x36, 0x00, 0xC4, 0x7B, 0x00, // 0x7BC40036000323 + 0x00, 0x03, 0x20, 0x0B, 0x00, 0xC8, 0x7B, 0x00, // 0x7BC8000B200300 + 0x00, 0x03, 0x20, 0x0F, 0x00, 0xCC, 0x7B, 0x00, // 0x7BCC000F200300 + 0x23, 0x03, 0x20, 0x0B, 0x00, 0xD0, 0x7B, 0x00, // 0x7BD0000B200323 + 0x23, 0x03, 0x20, 0x0F, 0x00, 0xD4, 0x7B, 0x00, // 0x7BD4000F200323 + 0x09, 0x03, 0x20, 0x0B, 0x00, 0xD8, 0x7B, 0x00, // 0x7BD8000B200309 + 0x09, 0x03, 0x20, 0x0F, 0x00, 0xDC, 0x7B, 0x00, // 0x7BDC000F200309 + 0x03, 0x03, 0x20, 0x0B, 0x00, 0xE0, 0x7B, 0x00, // 0x7BE0000B200303 + 0x03, 0x03, 0x20, 0x0F, 0x00, 0xE4, 0x7B, 0x00, // 0x7BE4000F200303 + 0x13, 0x03, 0x20, 0x76, 0x00, 0x00, 0x7C, 0x00, // 0x7C000076200313 + 0x14, 0x03, 0x20, 0x76, 0x00, 0x04, 0x7C, 0x00, // 0x7C040076200314 + 0x00, 0x03, 0x00, 0xE0, 0x03, 0x08, 0x7C, 0x00, // 0x7C0803E0000300 + 0x00, 0x03, 0x20, 0xE0, 0x03, 0x0C, 0x7C, 0x00, // 0x7C0C03E0200300 + 0x01, 0x03, 0x00, 0xE0, 0x03, 0x10, 0x7C, 0x00, // 0x7C1003E0000301 + 0x01, 0x03, 0x20, 0xE0, 0x03, 0x14, 0x7C, 0x00, // 0x7C1403E0200301 + 0x42, 0x03, 0x00, 0xE0, 0x03, 0x18, 0x7C, 0x00, // 0x7C1803E0000342 + 0x42, 0x03, 0x20, 0xE0, 0x03, 0x1C, 0x7C, 0x00, // 0x7C1C03E0200342 + 0x13, 0x03, 0x20, 0x72, 0x00, 0x20, 0x7C, 0x00, // 0x7C200072200313 + 0x14, 0x03, 0x20, 0x72, 0x00, 0x24, 0x7C, 0x00, // 0x7C240072200314 + 0x00, 0x03, 0x00, 0xE1, 0x03, 0x28, 0x7C, 0x00, // 0x7C2803E1000300 + 0x00, 0x03, 0x20, 0xE1, 0x03, 0x2C, 0x7C, 0x00, // 0x7C2C03E1200300 + 0x01, 0x03, 0x00, 0xE1, 0x03, 0x30, 0x7C, 0x00, // 0x7C3003E1000301 + 0x01, 0x03, 0x20, 0xE1, 0x03, 0x34, 0x7C, 0x00, // 0x7C3403E1200301 + 0x42, 0x03, 0x00, 0xE1, 0x03, 0x38, 0x7C, 0x00, // 0x7C3803E1000342 + 0x42, 0x03, 0x20, 0xE1, 0x03, 0x3C, 0x7C, 0x00, // 0x7C3C03E1200342 + 0x13, 0x03, 0xA0, 0x76, 0x00, 0x40, 0x7C, 0x00, // 0x7C400076A00313 + 0x14, 0x03, 0xA0, 0x76, 0x00, 0x44, 0x7C, 0x00, // 0x7C440076A00314 + 0x00, 0x03, 0x00, 0xE2, 0x03, 0x48, 0x7C, 0x00, // 0x7C4803E2000300 + 0x00, 0x03, 0x20, 0xE2, 0x03, 0x4C, 0x7C, 0x00, // 0x7C4C03E2200300 + 0x01, 0x03, 0x00, 0xE2, 0x03, 0x50, 0x7C, 0x00, // 0x7C5003E2000301 + 0x01, 0x03, 0x20, 0xE2, 0x03, 0x54, 0x7C, 0x00, // 0x7C5403E2200301 + 0x13, 0x03, 0xA0, 0x72, 0x00, 0x60, 0x7C, 0x00, // 0x7C600072A00313 + 0x14, 0x03, 0xA0, 0x72, 0x00, 0x64, 0x7C, 0x00, // 0x7C640072A00314 + 0x00, 0x03, 0x00, 0xE3, 0x03, 0x68, 0x7C, 0x00, // 0x7C6803E3000300 + 0x00, 0x03, 0x20, 0xE3, 0x03, 0x6C, 0x7C, 0x00, // 0x7C6C03E3200300 + 0x01, 0x03, 0x00, 0xE3, 0x03, 0x70, 0x7C, 0x00, // 0x7C7003E3000301 + 0x01, 0x03, 0x20, 0xE3, 0x03, 0x74, 0x7C, 0x00, // 0x7C7403E3200301 + 0x13, 0x03, 0xE0, 0x76, 0x00, 0x80, 0x7C, 0x00, // 0x7C800076E00313 + 0x14, 0x03, 0xE0, 0x76, 0x00, 0x84, 0x7C, 0x00, // 0x7C840076E00314 + 0x00, 0x03, 0x00, 0xE4, 0x03, 0x88, 0x7C, 0x00, // 0x7C8803E4000300 + 0x00, 0x03, 0x20, 0xE4, 0x03, 0x8C, 0x7C, 0x00, // 0x7C8C03E4200300 + 0x01, 0x03, 0x00, 0xE4, 0x03, 0x90, 0x7C, 0x00, // 0x7C9003E4000301 + 0x01, 0x03, 0x20, 0xE4, 0x03, 0x94, 0x7C, 0x00, // 0x7C9403E4200301 + 0x42, 0x03, 0x00, 0xE4, 0x03, 0x98, 0x7C, 0x00, // 0x7C9803E4000342 + 0x42, 0x03, 0x20, 0xE4, 0x03, 0x9C, 0x7C, 0x00, // 0x7C9C03E4200342 + 0x13, 0x03, 0xE0, 0x72, 0x00, 0xA0, 0x7C, 0x00, // 0x7CA00072E00313 + 0x14, 0x03, 0xE0, 0x72, 0x00, 0xA4, 0x7C, 0x00, // 0x7CA40072E00314 + 0x00, 0x03, 0x00, 0xE5, 0x03, 0xA8, 0x7C, 0x00, // 0x7CA803E5000300 + 0x00, 0x03, 0x20, 0xE5, 0x03, 0xAC, 0x7C, 0x00, // 0x7CAC03E5200300 + 0x01, 0x03, 0x00, 0xE5, 0x03, 0xB0, 0x7C, 0x00, // 0x7CB003E5000301 + 0x01, 0x03, 0x20, 0xE5, 0x03, 0xB4, 0x7C, 0x00, // 0x7CB403E5200301 + 0x42, 0x03, 0x00, 0xE5, 0x03, 0xB8, 0x7C, 0x00, // 0x7CB803E5000342 + 0x42, 0x03, 0x20, 0xE5, 0x03, 0xBC, 0x7C, 0x00, // 0x7CBC03E5200342 + 0x13, 0x03, 0x20, 0x77, 0x00, 0xC0, 0x7C, 0x00, // 0x7CC00077200313 + 0x14, 0x03, 0x20, 0x77, 0x00, 0xC4, 0x7C, 0x00, // 0x7CC40077200314 + 0x00, 0x03, 0x00, 0xE6, 0x03, 0xC8, 0x7C, 0x00, // 0x7CC803E6000300 + 0x00, 0x03, 0x20, 0xE6, 0x03, 0xCC, 0x7C, 0x00, // 0x7CCC03E6200300 + 0x01, 0x03, 0x00, 0xE6, 0x03, 0xD0, 0x7C, 0x00, // 0x7CD003E6000301 + 0x01, 0x03, 0x20, 0xE6, 0x03, 0xD4, 0x7C, 0x00, // 0x7CD403E6200301 + 0x42, 0x03, 0x00, 0xE6, 0x03, 0xD8, 0x7C, 0x00, // 0x7CD803E6000342 + 0x42, 0x03, 0x20, 0xE6, 0x03, 0xDC, 0x7C, 0x00, // 0x7CDC03E6200342 + 0x13, 0x03, 0x20, 0x73, 0x00, 0xE0, 0x7C, 0x00, // 0x7CE00073200313 + 0x14, 0x03, 0x20, 0x73, 0x00, 0xE4, 0x7C, 0x00, // 0x7CE40073200314 + 0x00, 0x03, 0x00, 0xE7, 0x03, 0xE8, 0x7C, 0x00, // 0x7CE803E7000300 + 0x00, 0x03, 0x20, 0xE7, 0x03, 0xEC, 0x7C, 0x00, // 0x7CEC03E7200300 + 0x01, 0x03, 0x00, 0xE7, 0x03, 0xF0, 0x7C, 0x00, // 0x7CF003E7000301 + 0x01, 0x03, 0x20, 0xE7, 0x03, 0xF4, 0x7C, 0x00, // 0x7CF403E7200301 + 0x42, 0x03, 0x00, 0xE7, 0x03, 0xF8, 0x7C, 0x00, // 0x7CF803E7000342 + 0x42, 0x03, 0x20, 0xE7, 0x03, 0xFC, 0x7C, 0x00, // 0x7CFC03E7200342 + 0x13, 0x03, 0xE0, 0x77, 0x00, 0x00, 0x7D, 0x00, // 0x7D000077E00313 + 0x14, 0x03, 0xE0, 0x77, 0x00, 0x04, 0x7D, 0x00, // 0x7D040077E00314 + 0x00, 0x03, 0x00, 0xE8, 0x03, 0x08, 0x7D, 0x00, // 0x7D0803E8000300 + 0x00, 0x03, 0x20, 0xE8, 0x03, 0x0C, 0x7D, 0x00, // 0x7D0C03E8200300 + 0x01, 0x03, 0x00, 0xE8, 0x03, 0x10, 0x7D, 0x00, // 0x7D1003E8000301 + 0x01, 0x03, 0x20, 0xE8, 0x03, 0x14, 0x7D, 0x00, // 0x7D1403E8200301 + 0x13, 0x03, 0xE0, 0x73, 0x00, 0x20, 0x7D, 0x00, // 0x7D200073E00313 + 0x14, 0x03, 0xE0, 0x73, 0x00, 0x24, 0x7D, 0x00, // 0x7D240073E00314 + 0x00, 0x03, 0x00, 0xE9, 0x03, 0x28, 0x7D, 0x00, // 0x7D2803E9000300 + 0x00, 0x03, 0x20, 0xE9, 0x03, 0x2C, 0x7D, 0x00, // 0x7D2C03E9200300 + 0x01, 0x03, 0x00, 0xE9, 0x03, 0x30, 0x7D, 0x00, // 0x7D3003E9000301 + 0x01, 0x03, 0x20, 0xE9, 0x03, 0x34, 0x7D, 0x00, // 0x7D3403E9200301 + 0x13, 0x03, 0xA0, 0x78, 0x00, 0x40, 0x7D, 0x00, // 0x7D400078A00313 + 0x14, 0x03, 0xA0, 0x78, 0x00, 0x44, 0x7D, 0x00, // 0x7D440078A00314 + 0x00, 0x03, 0x00, 0xEA, 0x03, 0x48, 0x7D, 0x00, // 0x7D4803EA000300 + 0x00, 0x03, 0x20, 0xEA, 0x03, 0x4C, 0x7D, 0x00, // 0x7D4C03EA200300 + 0x01, 0x03, 0x00, 0xEA, 0x03, 0x50, 0x7D, 0x00, // 0x7D5003EA000301 + 0x01, 0x03, 0x20, 0xEA, 0x03, 0x54, 0x7D, 0x00, // 0x7D5403EA200301 + 0x42, 0x03, 0x00, 0xEA, 0x03, 0x58, 0x7D, 0x00, // 0x7D5803EA000342 + 0x42, 0x03, 0x20, 0xEA, 0x03, 0x5C, 0x7D, 0x00, // 0x7D5C03EA200342 + 0x14, 0x03, 0xA0, 0x74, 0x00, 0x64, 0x7D, 0x00, // 0x7D640074A00314 + 0x00, 0x03, 0x20, 0xEB, 0x03, 0x6C, 0x7D, 0x00, // 0x7D6C03EB200300 + 0x01, 0x03, 0x20, 0xEB, 0x03, 0x74, 0x7D, 0x00, // 0x7D7403EB200301 + 0x42, 0x03, 0x20, 0xEB, 0x03, 0x7C, 0x7D, 0x00, // 0x7D7C03EB200342 + 0x13, 0x03, 0x20, 0x79, 0x00, 0x80, 0x7D, 0x00, // 0x7D800079200313 + 0x14, 0x03, 0x20, 0x79, 0x00, 0x84, 0x7D, 0x00, // 0x7D840079200314 + 0x00, 0x03, 0x00, 0xEC, 0x03, 0x88, 0x7D, 0x00, // 0x7D8803EC000300 + 0x00, 0x03, 0x20, 0xEC, 0x03, 0x8C, 0x7D, 0x00, // 0x7D8C03EC200300 + 0x01, 0x03, 0x00, 0xEC, 0x03, 0x90, 0x7D, 0x00, // 0x7D9003EC000301 + 0x01, 0x03, 0x20, 0xEC, 0x03, 0x94, 0x7D, 0x00, // 0x7D9403EC200301 + 0x42, 0x03, 0x00, 0xEC, 0x03, 0x98, 0x7D, 0x00, // 0x7D9803EC000342 + 0x42, 0x03, 0x20, 0xEC, 0x03, 0x9C, 0x7D, 0x00, // 0x7D9C03EC200342 + 0x13, 0x03, 0x20, 0x75, 0x00, 0xA0, 0x7D, 0x00, // 0x7DA00075200313 + 0x14, 0x03, 0x20, 0x75, 0x00, 0xA4, 0x7D, 0x00, // 0x7DA40075200314 + 0x00, 0x03, 0x00, 0xED, 0x03, 0xA8, 0x7D, 0x00, // 0x7DA803ED000300 + 0x00, 0x03, 0x20, 0xED, 0x03, 0xAC, 0x7D, 0x00, // 0x7DAC03ED200300 + 0x01, 0x03, 0x00, 0xED, 0x03, 0xB0, 0x7D, 0x00, // 0x7DB003ED000301 + 0x01, 0x03, 0x20, 0xED, 0x03, 0xB4, 0x7D, 0x00, // 0x7DB403ED200301 + 0x42, 0x03, 0x00, 0xED, 0x03, 0xB8, 0x7D, 0x00, // 0x7DB803ED000342 + 0x42, 0x03, 0x20, 0xED, 0x03, 0xBC, 0x7D, 0x00, // 0x7DBC03ED200342 + 0x00, 0x03, 0x20, 0x76, 0x00, 0xC0, 0x7D, 0x00, // 0x7DC00076200300 + 0x00, 0x00, 0x80, 0x75, 0x00, 0xC4, 0x7D, 0x00, // 0x7DC40075800000 + 0x00, 0x03, 0xA0, 0x76, 0x00, 0xC8, 0x7D, 0x00, // 0x7DC80076A00300 + 0x00, 0x00, 0xA0, 0x75, 0x00, 0xCC, 0x7D, 0x00, // 0x7DCC0075A00000 + 0x00, 0x03, 0xE0, 0x76, 0x00, 0xD0, 0x7D, 0x00, // 0x7DD00076E00300 + 0x00, 0x00, 0xC0, 0x75, 0x00, 0xD4, 0x7D, 0x00, // 0x7DD40075C00000 + 0x00, 0x03, 0x20, 0x77, 0x00, 0xD8, 0x7D, 0x00, // 0x7DD80077200300 + 0x00, 0x00, 0xE0, 0x75, 0x00, 0xDC, 0x7D, 0x00, // 0x7DDC0075E00000 + 0x00, 0x03, 0xE0, 0x77, 0x00, 0xE0, 0x7D, 0x00, // 0x7DE00077E00300 + 0x00, 0x00, 0x80, 0x79, 0x00, 0xE4, 0x7D, 0x00, // 0x7DE40079800000 + 0x00, 0x03, 0xA0, 0x78, 0x00, 0xE8, 0x7D, 0x00, // 0x7DE80078A00300 + 0x00, 0x00, 0xA0, 0x79, 0x00, 0xEC, 0x7D, 0x00, // 0x7DEC0079A00000 + 0x00, 0x03, 0x20, 0x79, 0x00, 0xF0, 0x7D, 0x00, // 0x7DF00079200300 + 0x00, 0x00, 0xC0, 0x79, 0x00, 0xF4, 0x7D, 0x00, // 0x7DF40079C00000 + 0x45, 0x03, 0x00, 0xE0, 0x03, 0x00, 0x7E, 0x00, // 0x7E0003E0000345 + 0x45, 0x03, 0x20, 0xE0, 0x03, 0x04, 0x7E, 0x00, // 0x7E0403E0200345 + 0x45, 0x03, 0x40, 0xE0, 0x03, 0x08, 0x7E, 0x00, // 0x7E0803E0400345 + 0x45, 0x03, 0x60, 0xE0, 0x03, 0x0C, 0x7E, 0x00, // 0x7E0C03E0600345 + 0x45, 0x03, 0x80, 0xE0, 0x03, 0x10, 0x7E, 0x00, // 0x7E1003E0800345 + 0x45, 0x03, 0xA0, 0xE0, 0x03, 0x14, 0x7E, 0x00, // 0x7E1403E0A00345 + 0x45, 0x03, 0xC0, 0xE0, 0x03, 0x18, 0x7E, 0x00, // 0x7E1803E0C00345 + 0x45, 0x03, 0xE0, 0xE0, 0x03, 0x1C, 0x7E, 0x00, // 0x7E1C03E0E00345 + 0x45, 0x03, 0x00, 0xE1, 0x03, 0x20, 0x7E, 0x00, // 0x7E2003E1000345 + 0x45, 0x03, 0x20, 0xE1, 0x03, 0x24, 0x7E, 0x00, // 0x7E2403E1200345 + 0x45, 0x03, 0x40, 0xE1, 0x03, 0x28, 0x7E, 0x00, // 0x7E2803E1400345 + 0x45, 0x03, 0x60, 0xE1, 0x03, 0x2C, 0x7E, 0x00, // 0x7E2C03E1600345 + 0x45, 0x03, 0x80, 0xE1, 0x03, 0x30, 0x7E, 0x00, // 0x7E3003E1800345 + 0x45, 0x03, 0xA0, 0xE1, 0x03, 0x34, 0x7E, 0x00, // 0x7E3403E1A00345 + 0x45, 0x03, 0xC0, 0xE1, 0x03, 0x38, 0x7E, 0x00, // 0x7E3803E1C00345 + 0x45, 0x03, 0xE0, 0xE1, 0x03, 0x3C, 0x7E, 0x00, // 0x7E3C03E1E00345 + 0x45, 0x03, 0x00, 0xE4, 0x03, 0x40, 0x7E, 0x00, // 0x7E4003E4000345 + 0x45, 0x03, 0x20, 0xE4, 0x03, 0x44, 0x7E, 0x00, // 0x7E4403E4200345 + 0x45, 0x03, 0x40, 0xE4, 0x03, 0x48, 0x7E, 0x00, // 0x7E4803E4400345 + 0x45, 0x03, 0x60, 0xE4, 0x03, 0x4C, 0x7E, 0x00, // 0x7E4C03E4600345 + 0x45, 0x03, 0x80, 0xE4, 0x03, 0x50, 0x7E, 0x00, // 0x7E5003E4800345 + 0x45, 0x03, 0xA0, 0xE4, 0x03, 0x54, 0x7E, 0x00, // 0x7E5403E4A00345 + 0x45, 0x03, 0xC0, 0xE4, 0x03, 0x58, 0x7E, 0x00, // 0x7E5803E4C00345 + 0x45, 0x03, 0xE0, 0xE4, 0x03, 0x5C, 0x7E, 0x00, // 0x7E5C03E4E00345 + 0x45, 0x03, 0x00, 0xE5, 0x03, 0x60, 0x7E, 0x00, // 0x7E6003E5000345 + 0x45, 0x03, 0x20, 0xE5, 0x03, 0x64, 0x7E, 0x00, // 0x7E6403E5200345 + 0x45, 0x03, 0x40, 0xE5, 0x03, 0x68, 0x7E, 0x00, // 0x7E6803E5400345 + 0x45, 0x03, 0x60, 0xE5, 0x03, 0x6C, 0x7E, 0x00, // 0x7E6C03E5600345 + 0x45, 0x03, 0x80, 0xE5, 0x03, 0x70, 0x7E, 0x00, // 0x7E7003E5800345 + 0x45, 0x03, 0xA0, 0xE5, 0x03, 0x74, 0x7E, 0x00, // 0x7E7403E5A00345 + 0x45, 0x03, 0xC0, 0xE5, 0x03, 0x78, 0x7E, 0x00, // 0x7E7803E5C00345 + 0x45, 0x03, 0xE0, 0xE5, 0x03, 0x7C, 0x7E, 0x00, // 0x7E7C03E5E00345 + 0x45, 0x03, 0x00, 0xEC, 0x03, 0x80, 0x7E, 0x00, // 0x7E8003EC000345 + 0x45, 0x03, 0x20, 0xEC, 0x03, 0x84, 0x7E, 0x00, // 0x7E8403EC200345 + 0x45, 0x03, 0x40, 0xEC, 0x03, 0x88, 0x7E, 0x00, // 0x7E8803EC400345 + 0x45, 0x03, 0x60, 0xEC, 0x03, 0x8C, 0x7E, 0x00, // 0x7E8C03EC600345 + 0x45, 0x03, 0x80, 0xEC, 0x03, 0x90, 0x7E, 0x00, // 0x7E9003EC800345 + 0x45, 0x03, 0xA0, 0xEC, 0x03, 0x94, 0x7E, 0x00, // 0x7E9403ECA00345 + 0x45, 0x03, 0xC0, 0xEC, 0x03, 0x98, 0x7E, 0x00, // 0x7E9803ECC00345 + 0x45, 0x03, 0xE0, 0xEC, 0x03, 0x9C, 0x7E, 0x00, // 0x7E9C03ECE00345 + 0x45, 0x03, 0x00, 0xED, 0x03, 0xA0, 0x7E, 0x00, // 0x7EA003ED000345 + 0x45, 0x03, 0x20, 0xED, 0x03, 0xA4, 0x7E, 0x00, // 0x7EA403ED200345 + 0x45, 0x03, 0x40, 0xED, 0x03, 0xA8, 0x7E, 0x00, // 0x7EA803ED400345 + 0x45, 0x03, 0x60, 0xED, 0x03, 0xAC, 0x7E, 0x00, // 0x7EAC03ED600345 + 0x45, 0x03, 0x80, 0xED, 0x03, 0xB0, 0x7E, 0x00, // 0x7EB003ED800345 + 0x45, 0x03, 0xA0, 0xED, 0x03, 0xB4, 0x7E, 0x00, // 0x7EB403EDA00345 + 0x45, 0x03, 0xC0, 0xED, 0x03, 0xB8, 0x7E, 0x00, // 0x7EB803EDC00345 + 0x45, 0x03, 0xE0, 0xED, 0x03, 0xBC, 0x7E, 0x00, // 0x7EBC03EDE00345 + 0x06, 0x03, 0x20, 0x76, 0x00, 0xC0, 0x7E, 0x00, // 0x7EC00076200306 + 0x04, 0x03, 0x20, 0x76, 0x00, 0xC4, 0x7E, 0x00, // 0x7EC40076200304 + 0x45, 0x03, 0x00, 0xEE, 0x03, 0xC8, 0x7E, 0x00, // 0x7EC803EE000345 + 0x45, 0x03, 0x20, 0x76, 0x00, 0xCC, 0x7E, 0x00, // 0x7ECC0076200345 + 0x45, 0x03, 0x80, 0x75, 0x00, 0xD0, 0x7E, 0x00, // 0x7ED00075800345 + 0x42, 0x03, 0x20, 0x76, 0x00, 0xD8, 0x7E, 0x00, // 0x7ED80076200342 + 0x45, 0x03, 0xC0, 0xF6, 0x03, 0xDC, 0x7E, 0x00, // 0x7EDC03F6C00345 + 0x06, 0x03, 0x20, 0x72, 0x00, 0xE0, 0x7E, 0x00, // 0x7EE00072200306 + 0x04, 0x03, 0x20, 0x72, 0x00, 0xE4, 0x7E, 0x00, // 0x7EE40072200304 + 0x00, 0x03, 0x20, 0x72, 0x00, 0xE8, 0x7E, 0x00, // 0x7EE80072200300 + 0x00, 0x00, 0xC0, 0x70, 0x00, 0xEC, 0x7E, 0x00, // 0x7EEC0070C00000 + 0x45, 0x03, 0x20, 0x72, 0x00, 0xF0, 0x7E, 0x00, // 0x7EF00072200345 + 0x00, 0x00, 0x20, 0x77, 0x00, 0xF8, 0x7E, 0x00, // 0x7EF80077200000 + 0x42, 0x03, 0x00, 0x15, 0x00, 0x04, 0x7F, 0x00, // 0x7F040015000342 + 0x45, 0x03, 0x80, 0xEE, 0x03, 0x08, 0x7F, 0x00, // 0x7F0803EE800345 + 0x45, 0x03, 0xE0, 0x76, 0x00, 0x0C, 0x7F, 0x00, // 0x7F0C0076E00345 + 0x45, 0x03, 0xC0, 0x75, 0x00, 0x10, 0x7F, 0x00, // 0x7F100075C00345 + 0x42, 0x03, 0xE0, 0x76, 0x00, 0x18, 0x7F, 0x00, // 0x7F180076E00342 + 0x45, 0x03, 0xC0, 0xF8, 0x03, 0x1C, 0x7F, 0x00, // 0x7F1C03F8C00345 + 0x00, 0x03, 0xA0, 0x72, 0x00, 0x20, 0x7F, 0x00, // 0x7F200072A00300 + 0x00, 0x00, 0x00, 0x71, 0x00, 0x24, 0x7F, 0x00, // 0x7F240071000000 + 0x00, 0x03, 0xE0, 0x72, 0x00, 0x28, 0x7F, 0x00, // 0x7F280072E00300 + 0x00, 0x00, 0x20, 0x71, 0x00, 0x2C, 0x7F, 0x00, // 0x7F2C0071200000 + 0x45, 0x03, 0xE0, 0x72, 0x00, 0x30, 0x7F, 0x00, // 0x7F300072E00345 + 0x00, 0x03, 0xE0, 0xF7, 0x03, 0x34, 0x7F, 0x00, // 0x7F3403F7E00300 + 0x01, 0x03, 0xE0, 0xF7, 0x03, 0x38, 0x7F, 0x00, // 0x7F3803F7E00301 + 0x42, 0x03, 0xE0, 0xF7, 0x03, 0x3C, 0x7F, 0x00, // 0x7F3C03F7E00342 + 0x06, 0x03, 0x20, 0x77, 0x00, 0x40, 0x7F, 0x00, // 0x7F400077200306 + 0x04, 0x03, 0x20, 0x77, 0x00, 0x44, 0x7F, 0x00, // 0x7F440077200304 + 0x00, 0x03, 0x40, 0x79, 0x00, 0x48, 0x7F, 0x00, // 0x7F480079400300 + 0x00, 0x00, 0x00, 0x72, 0x00, 0x4C, 0x7F, 0x00, // 0x7F4C0072000000 + 0x42, 0x03, 0x20, 0x77, 0x00, 0x58, 0x7F, 0x00, // 0x7F580077200342 + 0x42, 0x03, 0x40, 0x79, 0x00, 0x5C, 0x7F, 0x00, // 0x7F5C0079400342 + 0x06, 0x03, 0x20, 0x73, 0x00, 0x60, 0x7F, 0x00, // 0x7F600073200306 + 0x04, 0x03, 0x20, 0x73, 0x00, 0x64, 0x7F, 0x00, // 0x7F640073200304 + 0x00, 0x03, 0x20, 0x73, 0x00, 0x68, 0x7F, 0x00, // 0x7F680073200300 + 0x00, 0x00, 0x40, 0x71, 0x00, 0x6C, 0x7F, 0x00, // 0x7F6C0071400000 + 0x00, 0x03, 0xC0, 0xFF, 0x03, 0x74, 0x7F, 0x00, // 0x7F7403FFC00300 + 0x01, 0x03, 0xC0, 0xFF, 0x03, 0x78, 0x7F, 0x00, // 0x7F7803FFC00301 + 0x42, 0x03, 0xC0, 0xFF, 0x03, 0x7C, 0x7F, 0x00, // 0x7F7C03FFC00342 + 0x06, 0x03, 0xA0, 0x78, 0x00, 0x80, 0x7F, 0x00, // 0x7F800078A00306 + 0x04, 0x03, 0xA0, 0x78, 0x00, 0x84, 0x7F, 0x00, // 0x7F840078A00304 + 0x00, 0x03, 0x60, 0x79, 0x00, 0x88, 0x7F, 0x00, // 0x7F880079600300 + 0x00, 0x00, 0x00, 0x76, 0x00, 0x8C, 0x7F, 0x00, // 0x7F8C0076000000 + 0x13, 0x03, 0x20, 0x78, 0x00, 0x90, 0x7F, 0x00, // 0x7F900078200313 + 0x14, 0x03, 0x20, 0x78, 0x00, 0x94, 0x7F, 0x00, // 0x7F940078200314 + 0x42, 0x03, 0xA0, 0x78, 0x00, 0x98, 0x7F, 0x00, // 0x7F980078A00342 + 0x42, 0x03, 0x60, 0x79, 0x00, 0x9C, 0x7F, 0x00, // 0x7F9C0079600342 + 0x06, 0x03, 0xA0, 0x74, 0x00, 0xA0, 0x7F, 0x00, // 0x7FA00074A00306 + 0x04, 0x03, 0xA0, 0x74, 0x00, 0xA4, 0x7F, 0x00, // 0x7FA40074A00304 + 0x00, 0x03, 0xA0, 0x74, 0x00, 0xA8, 0x7F, 0x00, // 0x7FA80074A00300 + 0x00, 0x00, 0xC0, 0x71, 0x00, 0xAC, 0x7F, 0x00, // 0x7FAC0071C00000 + 0x14, 0x03, 0x20, 0x74, 0x00, 0xB0, 0x7F, 0x00, // 0x7FB00074200314 + 0x00, 0x03, 0x00, 0x15, 0x00, 0xB4, 0x7F, 0x00, // 0x7FB40015000300 + 0x00, 0x00, 0xA0, 0x70, 0x00, 0xB8, 0x7F, 0x00, // 0x7FB80070A00000 + 0x00, 0x00, 0x00, 0x0C, 0x00, 0xBC, 0x7F, 0x00, // 0x7FBC000C000000 + 0x45, 0x03, 0x80, 0xEF, 0x03, 0xC8, 0x7F, 0x00, // 0x7FC803EF800345 + 0x45, 0x03, 0x20, 0x79, 0x00, 0xCC, 0x7F, 0x00, // 0x7FCC0079200345 + 0x45, 0x03, 0xC0, 0x79, 0x00, 0xD0, 0x7F, 0x00, // 0x7FD00079C00345 + 0x42, 0x03, 0x20, 0x79, 0x00, 0xD8, 0x7F, 0x00, // 0x7FD80079200342 + 0x45, 0x03, 0xC0, 0xFE, 0x03, 0xDC, 0x7F, 0x00, // 0x7FDC03FEC00345 + 0x00, 0x03, 0xE0, 0x73, 0x00, 0xE0, 0x7F, 0x00, // 0x7FE00073E00300 + 0x00, 0x00, 0x80, 0x71, 0x00, 0xE4, 0x7F, 0x00, // 0x7FE40071800000 + 0x00, 0x03, 0x20, 0x75, 0x00, 0xE8, 0x7F, 0x00, // 0x7FE80075200300 + 0x00, 0x00, 0xE0, 0x71, 0x00, 0xEC, 0x7F, 0x00, // 0x7FEC0071E00000 + 0x45, 0x03, 0x20, 0x75, 0x00, 0xF0, 0x7F, 0x00, // 0x7FF00075200345 + 0x00, 0x00, 0x80, 0x16, 0x00, 0xF4, 0x7F, 0x00, // 0x7FF40016800000 + 0x00, 0x00, 0x40, 0x00, 0x04, 0x00, 0x80, 0x00, // 0x80000400400000 + 0x00, 0x00, 0x60, 0x00, 0x04, 0x04, 0x80, 0x00, // 0x80040400600000 + 0x00, 0x00, 0x20, 0x75, 0x00, 0x98, 0x84, 0x00, // 0x84980075200000 + 0x00, 0x00, 0x60, 0x09, 0x00, 0xA8, 0x84, 0x00, // 0x84A80009600000 + 0x00, 0x00, 0xA0, 0x18, 0x00, 0xAC, 0x84, 0x00, // 0x84AC0018A00000 + 0x38, 0x03, 0x00, 0x32, 0x04, 0x68, 0x86, 0x00, // 0x86680432000338 + 0x38, 0x03, 0x40, 0x32, 0x04, 0x6C, 0x86, 0x00, // 0x866C0432400338 + 0x38, 0x03, 0x80, 0x32, 0x04, 0xB8, 0x86, 0x00, // 0x86B80432800338 + 0x38, 0x03, 0x00, 0x3A, 0x04, 0x34, 0x87, 0x00, // 0x8734043A000338 + 0x38, 0x03, 0x80, 0x3A, 0x04, 0x38, 0x87, 0x00, // 0x8738043A800338 + 0x38, 0x03, 0x40, 0x3A, 0x04, 0x3C, 0x87, 0x00, // 0x873C043A400338 + 0x38, 0x03, 0x60, 0x40, 0x04, 0x10, 0x88, 0x00, // 0x88100440600338 + 0x38, 0x03, 0x00, 0x41, 0x04, 0x24, 0x88, 0x00, // 0x88240441000338 + 0x38, 0x03, 0x60, 0x41, 0x04, 0x30, 0x88, 0x00, // 0x88300441600338 + 0x38, 0x03, 0x60, 0x44, 0x04, 0x90, 0x88, 0x00, // 0x88900444600338 + 0x38, 0x03, 0xA0, 0x44, 0x04, 0x98, 0x88, 0x00, // 0x88980444A00338 + 0x38, 0x03, 0x80, 0x47, 0x04, 0x04, 0x89, 0x00, // 0x89040447800338 + 0x38, 0x03, 0x60, 0x48, 0x04, 0x10, 0x89, 0x00, // 0x89100448600338 + 0x38, 0x03, 0xA0, 0x48, 0x04, 0x1C, 0x89, 0x00, // 0x891C0448A00338 + 0x38, 0x03, 0x00, 0x49, 0x04, 0x24, 0x89, 0x00, // 0x89240449000338 + 0x38, 0x03, 0xA0, 0x07, 0x00, 0x80, 0x89, 0x00, // 0x89800007A00338 + 0x38, 0x03, 0x20, 0x4C, 0x04, 0x88, 0x89, 0x00, // 0x8988044C200338 + 0x38, 0x03, 0xA0, 0x49, 0x04, 0xB4, 0x89, 0x00, // 0x89B40449A00338 + 0x38, 0x03, 0x80, 0x07, 0x00, 0xB8, 0x89, 0x00, // 0x89B80007800338 + 0x38, 0x03, 0xC0, 0x07, 0x00, 0xBC, 0x89, 0x00, // 0x89BC0007C00338 + 0x38, 0x03, 0x80, 0x4C, 0x04, 0xC0, 0x89, 0x00, // 0x89C0044C800338 + 0x38, 0x03, 0xA0, 0x4C, 0x04, 0xC4, 0x89, 0x00, // 0x89C4044CA00338 + 0x38, 0x03, 0x40, 0x4E, 0x04, 0xD0, 0x89, 0x00, // 0x89D0044E400338 + 0x38, 0x03, 0x60, 0x4E, 0x04, 0xD4, 0x89, 0x00, // 0x89D4044E600338 + 0x38, 0x03, 0xC0, 0x4E, 0x04, 0xE0, 0x89, 0x00, // 0x89E0044EC00338 + 0x38, 0x03, 0xE0, 0x4E, 0x04, 0xE4, 0x89, 0x00, // 0x89E4044EE00338 + 0x38, 0x03, 0x40, 0x4F, 0x04, 0x00, 0x8A, 0x00, // 0x8A00044F400338 + 0x38, 0x03, 0x60, 0x4F, 0x04, 0x04, 0x8A, 0x00, // 0x8A04044F600338 + 0x38, 0x03, 0x40, 0x50, 0x04, 0x10, 0x8A, 0x00, // 0x8A100450400338 + 0x38, 0x03, 0x60, 0x50, 0x04, 0x14, 0x8A, 0x00, // 0x8A140450600338 + 0x38, 0x03, 0xC0, 0x50, 0x04, 0x20, 0x8A, 0x00, // 0x8A200450C00338 + 0x38, 0x03, 0xE0, 0x50, 0x04, 0x24, 0x8A, 0x00, // 0x8A240450E00338 + 0x38, 0x03, 0x40, 0x54, 0x04, 0xB0, 0x8A, 0x00, // 0x8AB00454400338 + 0x38, 0x03, 0x00, 0x55, 0x04, 0xB4, 0x8A, 0x00, // 0x8AB40455000338 + 0x38, 0x03, 0x20, 0x55, 0x04, 0xB8, 0x8A, 0x00, // 0x8AB80455200338 + 0x38, 0x03, 0x60, 0x55, 0x04, 0xBC, 0x8A, 0x00, // 0x8ABC0455600338 + 0x38, 0x03, 0x80, 0x4F, 0x04, 0x80, 0x8B, 0x00, // 0x8B80044F800338 + 0x38, 0x03, 0xA0, 0x4F, 0x04, 0x84, 0x8B, 0x00, // 0x8B84044FA00338 + 0x38, 0x03, 0x20, 0x52, 0x04, 0x88, 0x8B, 0x00, // 0x8B880452200338 + 0x38, 0x03, 0x40, 0x52, 0x04, 0x8C, 0x8B, 0x00, // 0x8B8C0452400338 + 0x38, 0x03, 0x40, 0x56, 0x04, 0xA8, 0x8B, 0x00, // 0x8BA80456400338 + 0x38, 0x03, 0x60, 0x56, 0x04, 0xAC, 0x8B, 0x00, // 0x8BAC0456600338 + 0x38, 0x03, 0x80, 0x56, 0x04, 0xB0, 0x8B, 0x00, // 0x8BB00456800338 + 0x38, 0x03, 0xA0, 0x56, 0x04, 0xB4, 0x8B, 0x00, // 0x8BB40456A00338 + 0x00, 0x00, 0x00, 0x01, 0x06, 0xA4, 0x8C, 0x00, // 0x8CA40601000000 + 0x00, 0x00, 0x20, 0x01, 0x06, 0xA8, 0x8C, 0x00, // 0x8CA80601200000 + 0x38, 0x03, 0xA0, 0x5B, 0x05, 0x70, 0xAB, 0x00, // 0xAB70055BA00338 + 0x99, 0x30, 0x60, 0x09, 0x06, 0x30, 0xC1, 0x00, // 0xC1300609603099 + 0x99, 0x30, 0xA0, 0x09, 0x06, 0x38, 0xC1, 0x00, // 0xC1380609A03099 + 0x99, 0x30, 0xE0, 0x09, 0x06, 0x40, 0xC1, 0x00, // 0xC1400609E03099 + 0x99, 0x30, 0x20, 0x0A, 0x06, 0x48, 0xC1, 0x00, // 0xC148060A203099 + 0x99, 0x30, 0x60, 0x0A, 0x06, 0x50, 0xC1, 0x00, // 0xC150060A603099 + 0x99, 0x30, 0xA0, 0x0A, 0x06, 0x58, 0xC1, 0x00, // 0xC158060AA03099 + 0x99, 0x30, 0xE0, 0x0A, 0x06, 0x60, 0xC1, 0x00, // 0xC160060AE03099 + 0x99, 0x30, 0x20, 0x0B, 0x06, 0x68, 0xC1, 0x00, // 0xC168060B203099 + 0x99, 0x30, 0x60, 0x0B, 0x06, 0x70, 0xC1, 0x00, // 0xC170060B603099 + 0x99, 0x30, 0xA0, 0x0B, 0x06, 0x78, 0xC1, 0x00, // 0xC178060BA03099 + 0x99, 0x30, 0xE0, 0x0B, 0x06, 0x80, 0xC1, 0x00, // 0xC180060BE03099 + 0x99, 0x30, 0x20, 0x0C, 0x06, 0x88, 0xC1, 0x00, // 0xC188060C203099 + 0x99, 0x30, 0x80, 0x0C, 0x06, 0x94, 0xC1, 0x00, // 0xC194060C803099 + 0x99, 0x30, 0xC0, 0x0C, 0x06, 0x9C, 0xC1, 0x00, // 0xC19C060CC03099 + 0x99, 0x30, 0x00, 0x0D, 0x06, 0xA4, 0xC1, 0x00, // 0xC1A4060D003099 + 0x99, 0x30, 0xE0, 0x0D, 0x06, 0xC0, 0xC1, 0x00, // 0xC1C0060DE03099 + 0x9A, 0x30, 0xE0, 0x0D, 0x06, 0xC4, 0xC1, 0x00, // 0xC1C4060DE0309A + 0x99, 0x30, 0x40, 0x0E, 0x06, 0xCC, 0xC1, 0x00, // 0xC1CC060E403099 + 0x9A, 0x30, 0x40, 0x0E, 0x06, 0xD0, 0xC1, 0x00, // 0xC1D0060E40309A + 0x99, 0x30, 0xA0, 0x0E, 0x06, 0xD8, 0xC1, 0x00, // 0xC1D8060EA03099 + 0x9A, 0x30, 0xA0, 0x0E, 0x06, 0xDC, 0xC1, 0x00, // 0xC1DC060EA0309A + 0x99, 0x30, 0x00, 0x0F, 0x06, 0xE4, 0xC1, 0x00, // 0xC1E4060F003099 + 0x9A, 0x30, 0x00, 0x0F, 0x06, 0xE8, 0xC1, 0x00, // 0xC1E8060F00309A + 0x99, 0x30, 0x60, 0x0F, 0x06, 0xF0, 0xC1, 0x00, // 0xC1F0060F603099 + 0x9A, 0x30, 0x60, 0x0F, 0x06, 0xF4, 0xC1, 0x00, // 0xC1F4060F60309A + 0x99, 0x30, 0xC0, 0x08, 0x06, 0x50, 0xC2, 0x00, // 0xC2500608C03099 + 0x99, 0x30, 0xA0, 0x13, 0x06, 0x78, 0xC2, 0x00, // 0xC2780613A03099 + 0x99, 0x30, 0x60, 0x15, 0x06, 0xB0, 0xC2, 0x00, // 0xC2B00615603099 + 0x99, 0x30, 0xA0, 0x15, 0x06, 0xB8, 0xC2, 0x00, // 0xC2B80615A03099 + 0x99, 0x30, 0xE0, 0x15, 0x06, 0xC0, 0xC2, 0x00, // 0xC2C00615E03099 + 0x99, 0x30, 0x20, 0x16, 0x06, 0xC8, 0xC2, 0x00, // 0xC2C80616203099 + 0x99, 0x30, 0x60, 0x16, 0x06, 0xD0, 0xC2, 0x00, // 0xC2D00616603099 + 0x99, 0x30, 0xA0, 0x16, 0x06, 0xD8, 0xC2, 0x00, // 0xC2D80616A03099 + 0x99, 0x30, 0xE0, 0x16, 0x06, 0xE0, 0xC2, 0x00, // 0xC2E00616E03099 + 0x99, 0x30, 0x20, 0x17, 0x06, 0xE8, 0xC2, 0x00, // 0xC2E80617203099 + 0x99, 0x30, 0x60, 0x17, 0x06, 0xF0, 0xC2, 0x00, // 0xC2F00617603099 + 0x99, 0x30, 0xA0, 0x17, 0x06, 0xF8, 0xC2, 0x00, // 0xC2F80617A03099 + 0x99, 0x30, 0xE0, 0x17, 0x06, 0x00, 0xC3, 0x00, // 0xC3000617E03099 + 0x99, 0x30, 0x20, 0x18, 0x06, 0x08, 0xC3, 0x00, // 0xC3080618203099 + 0x99, 0x30, 0x80, 0x18, 0x06, 0x14, 0xC3, 0x00, // 0xC3140618803099 + 0x99, 0x30, 0xC0, 0x18, 0x06, 0x1C, 0xC3, 0x00, // 0xC31C0618C03099 + 0x99, 0x30, 0x00, 0x19, 0x06, 0x24, 0xC3, 0x00, // 0xC3240619003099 + 0x99, 0x30, 0xE0, 0x19, 0x06, 0x40, 0xC3, 0x00, // 0xC3400619E03099 + 0x9A, 0x30, 0xE0, 0x19, 0x06, 0x44, 0xC3, 0x00, // 0xC3440619E0309A + 0x99, 0x30, 0x40, 0x1A, 0x06, 0x4C, 0xC3, 0x00, // 0xC34C061A403099 + 0x9A, 0x30, 0x40, 0x1A, 0x06, 0x50, 0xC3, 0x00, // 0xC350061A40309A + 0x99, 0x30, 0xA0, 0x1A, 0x06, 0x58, 0xC3, 0x00, // 0xC358061AA03099 + 0x9A, 0x30, 0xA0, 0x1A, 0x06, 0x5C, 0xC3, 0x00, // 0xC35C061AA0309A + 0x99, 0x30, 0x00, 0x1B, 0x06, 0x64, 0xC3, 0x00, // 0xC364061B003099 + 0x9A, 0x30, 0x00, 0x1B, 0x06, 0x68, 0xC3, 0x00, // 0xC368061B00309A + 0x99, 0x30, 0x60, 0x1B, 0x06, 0x70, 0xC3, 0x00, // 0xC370061B603099 + 0x9A, 0x30, 0x60, 0x1B, 0x06, 0x74, 0xC3, 0x00, // 0xC374061B60309A + 0x99, 0x30, 0xC0, 0x14, 0x06, 0xD0, 0xC3, 0x00, // 0xC3D00614C03099 + 0x99, 0x30, 0xE0, 0x1D, 0x06, 0xDC, 0xC3, 0x00, // 0xC3DC061DE03099 + 0x99, 0x30, 0x00, 0x1E, 0x06, 0xE0, 0xC3, 0x00, // 0xC3E0061E003099 + 0x99, 0x30, 0x20, 0x1E, 0x06, 0xE4, 0xC3, 0x00, // 0xC3E4061E203099 + 0x99, 0x30, 0x40, 0x1E, 0x06, 0xE8, 0xC3, 0x00, // 0xC3E8061E403099 + 0x99, 0x30, 0xA0, 0x1F, 0x06, 0xF8, 0xC3, 0x00, // 0xC3F8061FA03099 + 0x00, 0x00, 0x00, 0x89, 0x11, 0x00, 0xE4, 0x03, // 0x3E4001189000000 + 0x00, 0x00, 0x80, 0xDE, 0x0C, 0x04, 0xE4, 0x03, // 0x3E4040CDE800000 + 0x00, 0x00, 0x40, 0xD9, 0x11, 0x08, 0xE4, 0x03, // 0x3E40811D9400000 + 0x00, 0x00, 0x00, 0x99, 0x11, 0x0C, 0xE4, 0x03, // 0x3E40C1199000000 + 0x00, 0x00, 0x20, 0xDA, 0x0D, 0x10, 0xE4, 0x03, // 0x3E4100DDA200000 + 0x00, 0x00, 0x40, 0xC6, 0x09, 0x14, 0xE4, 0x03, // 0x3E41409C6400000 + 0x00, 0x00, 0xA0, 0x7C, 0x0A, 0x18, 0xE4, 0x03, // 0x3E4180A7CA00000 + 0x00, 0x00, 0x80, 0xF3, 0x13, 0x1C, 0xE4, 0x03, // 0x3E41C13F3800000 + 0x00, 0x00, 0x80, 0xF3, 0x13, 0x20, 0xE4, 0x03, // 0x3E42013F3800000 + 0x00, 0x00, 0x20, 0x2A, 0x0B, 0x24, 0xE4, 0x03, // 0x3E4240B2A200000 + 0x00, 0x00, 0x20, 0x3A, 0x12, 0x28, 0xE4, 0x03, // 0x3E428123A200000 + 0x00, 0x00, 0xE0, 0xB0, 0x0A, 0x2C, 0xE4, 0x03, // 0x3E42C0AB0E00000 + 0x00, 0x00, 0x00, 0x29, 0x0B, 0x30, 0xE4, 0x03, // 0x3E4300B29000000 + 0x00, 0x00, 0xC0, 0x3E, 0x0C, 0x34, 0xE4, 0x03, // 0x3E4340C3EC00000 + 0x00, 0x00, 0x20, 0xCD, 0x0E, 0x38, 0xE4, 0x03, // 0x3E4380ECD200000 + 0x00, 0x00, 0xA0, 0xF0, 0x0F, 0x3C, 0xE4, 0x03, // 0x3E43C0FF0A00000 + 0x00, 0x00, 0xE0, 0xC7, 0x10, 0x40, 0xE4, 0x03, // 0x3E44010C7E00000 + 0x00, 0x00, 0x40, 0xF7, 0x10, 0x44, 0xE4, 0x03, // 0x3E44410F7400000 + 0x00, 0x00, 0x00, 0x1F, 0x11, 0x48, 0xE4, 0x03, // 0x3E448111F000000 + 0x00, 0x00, 0xE0, 0x11, 0x12, 0x4C, 0xE4, 0x03, // 0x3E44C1211E00000 + 0x00, 0x00, 0x40, 0x40, 0x0D, 0x50, 0xE4, 0x03, // 0x3E4500D40400000 + 0x00, 0x00, 0x60, 0xA3, 0x0D, 0x54, 0xE4, 0x03, // 0x3E4540DA3600000 + 0x00, 0x00, 0x20, 0x1B, 0x0E, 0x58, 0xE4, 0x03, // 0x3E4580E1B200000 + 0x00, 0x00, 0xC0, 0x7B, 0x0E, 0x5C, 0xE4, 0x03, // 0x3E45C0E7BC00000 + 0x00, 0x00, 0xA0, 0x87, 0x10, 0x60, 0xE4, 0x03, // 0x3E4601087A00000 + 0x00, 0x00, 0x40, 0x2D, 0x12, 0x64, 0xE4, 0x03, // 0x3E464122D400000 + 0x00, 0x00, 0x20, 0x3E, 0x13, 0x68, 0xE4, 0x03, // 0x3E468133E200000 + 0x00, 0x00, 0x40, 0xD0, 0x09, 0x6C, 0xE4, 0x03, // 0x3E46C09D0400000 + 0x00, 0x00, 0xA0, 0x6E, 0x0A, 0x70, 0xE4, 0x03, // 0x3E4700A6EA00000 + 0x00, 0x00, 0x80, 0x60, 0x0D, 0x74, 0xE4, 0x03, // 0x3E4740D60800000 + 0x00, 0x00, 0x60, 0x43, 0x0E, 0x78, 0xE4, 0x03, // 0x3E4780E43600000 + 0x00, 0x00, 0xA0, 0xC5, 0x10, 0x7C, 0xE4, 0x03, // 0x3E47C10C5A00000 + 0x00, 0x00, 0xC0, 0xC3, 0x13, 0x80, 0xE4, 0x03, // 0x3E48013C3C00000 + 0x00, 0x00, 0x00, 0xAA, 0x0B, 0x84, 0xE4, 0x03, // 0x3E4840BAA000000 + 0x00, 0x00, 0x60, 0xFD, 0x0D, 0x88, 0xE4, 0x03, // 0x3E4880DFD600000 + 0x00, 0x00, 0xA0, 0xB9, 0x10, 0x8C, 0xE4, 0x03, // 0x3E48C10B9A00000 + 0x00, 0x00, 0x80, 0x2C, 0x11, 0x90, 0xE4, 0x03, // 0x3E490112C800000 + 0x00, 0x00, 0x20, 0x59, 0x0C, 0x94, 0xE4, 0x03, // 0x3E4940C59200000 + 0x00, 0x00, 0x00, 0x3B, 0x10, 0x98, 0xE4, 0x03, // 0x3E498103B000000 + 0x00, 0x00, 0xE0, 0x03, 0x11, 0x9C, 0xE4, 0x03, // 0x3E49C1103E00000 + 0x00, 0x00, 0x40, 0xD9, 0x0B, 0xA0, 0xE4, 0x03, // 0x3E4A00BD9400000 + 0x00, 0x00, 0xE0, 0xE2, 0x0C, 0xA4, 0xE4, 0x03, // 0x3E4A40CE2E00000 + 0x00, 0x00, 0x40, 0xAD, 0x0D, 0xA8, 0xE4, 0x03, // 0x3E4A80DAD400000 + 0x00, 0x00, 0x80, 0x5F, 0x0E, 0xAC, 0xE4, 0x03, // 0x3E4AC0E5F800000 + 0x00, 0x00, 0xC0, 0x19, 0x12, 0xB0, 0xE4, 0x03, // 0x3E4B01219C00000 + 0x00, 0x00, 0xC0, 0xF0, 0x09, 0xB4, 0xE4, 0x03, // 0x3E4B409F0C00000 + 0x00, 0x00, 0xE0, 0x36, 0x0A, 0xB8, 0xE4, 0x03, // 0x3E4B80A36E00000 + 0x00, 0x00, 0xC0, 0x5B, 0x0A, 0xBC, 0xE4, 0x03, // 0x3E4BC0A5BC00000 + 0x00, 0x00, 0x80, 0x98, 0x0C, 0xC0, 0xE4, 0x03, // 0x3E4C00C98800000 + 0x00, 0x00, 0x60, 0x5A, 0x0D, 0xC4, 0xE4, 0x03, // 0x3E4C40D5A600000 + 0x00, 0x00, 0x00, 0x42, 0x0E, 0xC8, 0xE4, 0x03, // 0x3E4C80E42000000 + 0x00, 0x00, 0xE0, 0xDC, 0x0E, 0xCC, 0xE4, 0x03, // 0x3E4CC0EDCE00000 + 0x00, 0x00, 0x20, 0x00, 0x10, 0xD0, 0xE4, 0x03, // 0x3E4D01000200000 + 0x00, 0x00, 0xC0, 0xC0, 0x10, 0xD4, 0xE4, 0x03, // 0x3E4D410C0C00000 + 0x00, 0x00, 0x80, 0xCB, 0x10, 0xD8, 0xE4, 0x03, // 0x3E4D810CB800000 + 0x00, 0x00, 0xE0, 0xBD, 0x11, 0xDC, 0xE4, 0x03, // 0x3E4DC11BDE00000 + 0x00, 0x00, 0x40, 0xE6, 0x12, 0xE0, 0xE4, 0x03, // 0x3E4E012E6400000 + 0x00, 0x00, 0xE0, 0x6D, 0x13, 0xE4, 0xE4, 0x03, // 0x3E4E4136DE00000 + 0x00, 0x00, 0x40, 0xBF, 0x13, 0xE8, 0xE4, 0x03, // 0x3E4E813BF400000 + 0x00, 0x00, 0x80, 0x11, 0x0F, 0xEC, 0xE4, 0x03, // 0x3E4EC0F11800000 + 0x00, 0x00, 0xE0, 0x2F, 0x0F, 0xF0, 0xE4, 0x03, // 0x3E4F00F2FE00000 + 0x00, 0x00, 0x00, 0xB4, 0x0F, 0xF4, 0xE4, 0x03, // 0x3E4F40FB4000000 + 0x00, 0x00, 0x20, 0x79, 0x10, 0xF8, 0xE4, 0x03, // 0x3E4F81079200000 + 0x00, 0x00, 0x80, 0x60, 0x12, 0xFC, 0xE4, 0x03, // 0x3E4FC1260800000 + 0x00, 0x00, 0xE0, 0xCF, 0x13, 0x00, 0xE5, 0x03, // 0x3E50013CFE00000 + 0x00, 0x00, 0xC0, 0x5A, 0x11, 0x04, 0xE5, 0x03, // 0x3E504115AC00000 + 0x00, 0x00, 0xE0, 0x1B, 0x0B, 0x08, 0xE5, 0x03, // 0x3E5080B1BE00000 + 0x00, 0x00, 0x80, 0xE0, 0x0B, 0x0C, 0xE5, 0x03, // 0x3E50C0BE0800000 + 0x00, 0x00, 0x00, 0x8C, 0x0F, 0x10, 0xE5, 0x03, // 0x3E5100F8C000000 + 0x00, 0x00, 0xC0, 0x0F, 0x10, 0x14, 0xE5, 0x03, // 0x3E514100FC00000 + 0x00, 0x00, 0x40, 0x4C, 0x0E, 0x18, 0xE5, 0x03, // 0x3E5180E4C400000 + 0x00, 0x00, 0x40, 0x19, 0x0F, 0x1C, 0xE5, 0x03, // 0x3E51C0F19400000 + 0x00, 0x00, 0x40, 0x98, 0x11, 0x20, 0xE5, 0x03, // 0x3E5201198400000 + 0x00, 0x00, 0xE0, 0xDE, 0x12, 0x24, 0xE5, 0x03, // 0x3E52412DEE00000 + 0x00, 0x00, 0x00, 0x1B, 0x0B, 0x28, 0xE5, 0x03, // 0x3E5280B1B000000 + 0x00, 0x00, 0x40, 0x8C, 0x0B, 0x2C, 0xE5, 0x03, // 0x3E52C0B8C400000 + 0x00, 0x00, 0x60, 0x42, 0x0D, 0x30, 0xE5, 0x03, // 0x3E5300D42600000 + 0x00, 0x00, 0x40, 0xBB, 0x0D, 0x34, 0xE5, 0x03, // 0x3E5340DBB400000 + 0x00, 0x00, 0xE0, 0xE1, 0x0D, 0x38, 0xE5, 0x03, // 0x3E5380DE1E00000 + 0x00, 0x00, 0xE0, 0xA5, 0x0F, 0x3C, 0xE5, 0x03, // 0x3E53C0FA5E00000 + 0x00, 0x00, 0xE0, 0xC6, 0x0F, 0x40, 0xE5, 0x03, // 0x3E5400FC6E00000 + 0x00, 0x00, 0x60, 0xC9, 0x12, 0x44, 0xE5, 0x03, // 0x3E54412C9600000 + 0x00, 0x00, 0x40, 0x5A, 0x0A, 0x48, 0xE5, 0x03, // 0x3E5480A5A400000 + 0x00, 0x00, 0x60, 0x11, 0x10, 0x4C, 0xE5, 0x03, // 0x3E54C1011600000 + 0x00, 0x00, 0x80, 0x3B, 0x0A, 0x50, 0xE5, 0x03, // 0x3E5500A3B800000 + 0x00, 0x00, 0x80, 0x39, 0x0A, 0x54, 0xE5, 0x03, // 0x3E5540A39800000 + 0x00, 0x00, 0x80, 0x43, 0x0F, 0x58, 0xE5, 0x03, // 0x3E5580F43800000 + 0x00, 0x00, 0xC0, 0xB7, 0x0F, 0x5C, 0xE5, 0x03, // 0x3E55C0FB7C00000 + 0x00, 0x00, 0x20, 0x7E, 0x10, 0x60, 0xE5, 0x03, // 0x3E560107E200000 + 0x00, 0x00, 0xA0, 0xCE, 0x12, 0x64, 0xE5, 0x03, // 0x3E56412CEA00000 + 0x00, 0x00, 0x00, 0x70, 0x11, 0x68, 0xE5, 0x03, // 0x3E5681170000000 + 0x00, 0x00, 0xE0, 0x59, 0x0C, 0x6C, 0xE5, 0x03, // 0x3E56C0C59E00000 + 0x00, 0x00, 0x40, 0x40, 0x0D, 0x70, 0xE5, 0x03, // 0x3E5700D40400000 + 0x00, 0x00, 0xC0, 0x5F, 0x11, 0x74, 0xE5, 0x03, // 0x3E574115FC00000 + 0x00, 0x00, 0x20, 0xC7, 0x09, 0x78, 0xE5, 0x03, // 0x3E57809C7200000 + 0x00, 0x00, 0xE0, 0x7C, 0x0B, 0x7C, 0xE5, 0x03, // 0x3E57C0B7CE00000 + 0x00, 0x00, 0x40, 0x02, 0x0C, 0x80, 0xE5, 0x03, // 0x3E5800C02400000 + 0x00, 0x00, 0xE0, 0x70, 0x0E, 0x84, 0xE5, 0x03, // 0x3E5840E70E00000 + 0x00, 0x00, 0x00, 0xAE, 0x0E, 0x88, 0xE5, 0x03, // 0x3E5880EAE000000 + 0x00, 0x00, 0xE0, 0x62, 0x0A, 0x8C, 0xE5, 0x03, // 0x3E58C0A62E00000 + 0x00, 0x00, 0x60, 0x1F, 0x0F, 0x90, 0xE5, 0x03, // 0x3E5900F1F600000 + 0x00, 0x00, 0xE0, 0xF7, 0x09, 0x94, 0xE5, 0x03, // 0x3E59409F7E00000 + 0x00, 0x00, 0x20, 0xF5, 0x0B, 0x98, 0xE5, 0x03, // 0x3E5980BF5200000 + 0x00, 0x00, 0xA0, 0xC1, 0x09, 0x9C, 0xE5, 0x03, // 0x3E59C09C1A00000 + 0x00, 0x00, 0x80, 0x99, 0x0D, 0xA0, 0xE5, 0x03, // 0x3E5A00D99800000 + 0x00, 0x00, 0x00, 0xAF, 0x0C, 0xA4, 0xE5, 0x03, // 0x3E5A40CAF000000 + 0x00, 0x00, 0x40, 0xA4, 0x0F, 0xA8, 0xE5, 0x03, // 0x3E5A80FA4400000 + 0x00, 0x00, 0x60, 0x78, 0x0A, 0xAC, 0xE5, 0x03, // 0x3E5AC0A78600000 + 0x00, 0x00, 0xC0, 0x0B, 0x0B, 0xB0, 0xE5, 0x03, // 0x3E5B00B0BC00000 + 0x00, 0x00, 0x20, 0xE0, 0x0E, 0xB4, 0xE5, 0x03, // 0x3E5B40EE0200000 + 0x00, 0x00, 0x20, 0x89, 0x10, 0xB8, 0xE5, 0x03, // 0x3E5B81089200000 + 0x00, 0x00, 0x40, 0x55, 0x11, 0xBC, 0xE5, 0x03, // 0x3E5BC1155400000 + 0x00, 0x00, 0x40, 0x77, 0x0D, 0xC0, 0xE5, 0x03, // 0x3E5C00D77400000 + 0x00, 0x00, 0x00, 0xF6, 0x11, 0xC4, 0xE5, 0x03, // 0x3E5C411F6000000 + 0x00, 0x00, 0x00, 0x91, 0x0D, 0xC8, 0xE5, 0x03, // 0x3E5C80D91000000 + 0x00, 0x00, 0xC0, 0x5F, 0x0C, 0xCC, 0xE5, 0x03, // 0x3E5CC0C5FC00000 + 0x00, 0x00, 0xA0, 0x5C, 0x10, 0xD0, 0xE5, 0x03, // 0x3E5D0105CA00000 + 0x00, 0x00, 0x00, 0x74, 0x0C, 0xD4, 0xE5, 0x03, // 0x3E5D40C74000000 + 0x00, 0x00, 0xA0, 0xAC, 0x0E, 0xD8, 0xE5, 0x03, // 0x3E5D80EACA00000 + 0x00, 0x00, 0xC0, 0xD5, 0x09, 0xDC, 0xE5, 0x03, // 0x3E5DC09D5C00000 + 0x00, 0x00, 0x20, 0x2D, 0x0A, 0xE0, 0xE5, 0x03, // 0x3E5E00A2D200000 + 0x00, 0x00, 0x20, 0x39, 0x0A, 0xE4, 0xE5, 0x03, // 0x3E5E40A39200000 + 0x00, 0x00, 0x20, 0x10, 0x0D, 0xE8, 0xE5, 0x03, // 0x3E5E80D10200000 + 0x00, 0x00, 0xE0, 0x9C, 0x0F, 0xEC, 0xE5, 0x03, // 0x3E5EC0F9CE00000 + 0x00, 0x00, 0xE0, 0x4D, 0x10, 0xF0, 0xE5, 0x03, // 0x3E5F0104DE00000 + 0x00, 0x00, 0x40, 0x5A, 0x11, 0xF4, 0xE5, 0x03, // 0x3E5F4115A400000 + 0x00, 0x00, 0xE0, 0x39, 0x12, 0xF8, 0xE5, 0x03, // 0x3E5F81239E00000 + 0x00, 0x00, 0xA0, 0x5E, 0x0A, 0xFC, 0xE5, 0x03, // 0x3E5FC0A5EA00000 + 0x00, 0x00, 0x40, 0x88, 0x0A, 0x00, 0xE6, 0x03, // 0x3E6000A88400000 + 0x00, 0x00, 0x60, 0x2E, 0x0B, 0x04, 0xE6, 0x03, // 0x3E6040B2E600000 + 0x00, 0x00, 0x80, 0xDD, 0x0B, 0x08, 0xE6, 0x03, // 0x3E6080BDD800000 + 0x00, 0x00, 0xA0, 0xB8, 0x0C, 0x0C, 0xE6, 0x03, // 0x3E60C0CB8A00000 + 0x00, 0x00, 0xC0, 0xFF, 0x0D, 0x10, 0xE6, 0x03, // 0x3E6100DFFC00000 + 0x00, 0x00, 0x40, 0x25, 0x0F, 0x14, 0xE6, 0x03, // 0x3E6140F25400000 + 0x00, 0x00, 0xA0, 0xB5, 0x12, 0x18, 0xE6, 0x03, // 0x3E61812B5A00000 + 0x00, 0x00, 0x40, 0x4D, 0x13, 0x1C, 0xE6, 0x03, // 0x3E61C134D400000 + 0x00, 0x00, 0xE0, 0xD2, 0x13, 0x20, 0xE6, 0x03, // 0x3E62013D2E00000 + 0x00, 0x00, 0xC0, 0xD9, 0x13, 0x24, 0xE6, 0x03, // 0x3E62413D9C00000 + 0x00, 0x00, 0x60, 0x53, 0x0A, 0x28, 0xE6, 0x03, // 0x3E6280A53600000 + 0x00, 0x00, 0xC0, 0xD8, 0x0C, 0x2C, 0xE6, 0x03, // 0x3E62C0CD8C00000 + 0x00, 0x00, 0xE0, 0x6E, 0x0D, 0x30, 0xE6, 0x03, // 0x3E6300D6EE00000 + 0x00, 0x00, 0x40, 0xEC, 0x11, 0x34, 0xE6, 0x03, // 0x3E63411EC400000 + 0x00, 0x00, 0x80, 0xCE, 0x0B, 0x38, 0xE6, 0x03, // 0x3E6380BCE800000 + 0x00, 0x00, 0x00, 0x32, 0x0C, 0x3C, 0xE6, 0x03, // 0x3E63C0C32000000 + 0x00, 0x00, 0x00, 0x40, 0x0C, 0x40, 0xE6, 0x03, // 0x3E6400C40000000 + 0x00, 0x00, 0x40, 0x93, 0x0C, 0x44, 0xE6, 0x03, // 0x3E6440C93400000 + 0x00, 0x00, 0x60, 0xE4, 0x0D, 0x48, 0xE6, 0x03, // 0x3E6480DE4600000 + 0x00, 0x00, 0x20, 0x29, 0x0E, 0x4C, 0xE6, 0x03, // 0x3E64C0E29200000 + 0x00, 0x00, 0x20, 0x91, 0x0E, 0x50, 0xE6, 0x03, // 0x3E6500E91200000 + 0x00, 0x00, 0x40, 0x39, 0x0F, 0x54, 0xE6, 0x03, // 0x3E6540F39400000 + 0x00, 0x00, 0x80, 0xBE, 0x0F, 0x58, 0xE6, 0x03, // 0x3E6580FBE800000 + 0x00, 0x00, 0xE0, 0x0D, 0x10, 0x5C, 0xE6, 0x03, // 0x3E65C100DE00000 + 0x00, 0x00, 0xC0, 0xE4, 0x11, 0x60, 0xE6, 0x03, // 0x3E66011E4C00000 + 0x00, 0x00, 0xC0, 0x9D, 0x10, 0x64, 0xE6, 0x03, // 0x3E664109DC00000 + 0x00, 0x00, 0x60, 0x04, 0x12, 0x68, 0xE6, 0x03, // 0x3E6681204600000 + 0x00, 0x00, 0x40, 0x69, 0x12, 0x6C, 0xE6, 0x03, // 0x3E66C1269400000 + 0x00, 0x00, 0xE0, 0x42, 0x0A, 0x70, 0xE6, 0x03, // 0x3E6700A42E00000 + 0x00, 0x00, 0x60, 0x54, 0x0A, 0x74, 0xE6, 0x03, // 0x3E6740A54600000 + 0x00, 0x00, 0xA0, 0x97, 0x0A, 0x78, 0xE6, 0x03, // 0x3E6780A97A00000 + 0x00, 0x00, 0x00, 0x19, 0x0E, 0x7C, 0xE6, 0x03, // 0x3E67C0E19000000 + 0x00, 0x00, 0x40, 0x18, 0x11, 0x80, 0xE6, 0x03, // 0x3E6801118400000 + 0x00, 0x00, 0x40, 0x55, 0x11, 0x84, 0xE6, 0x03, // 0x3E6841155400000 + 0x00, 0x00, 0x20, 0xD9, 0x0B, 0x88, 0xE6, 0x03, // 0x3E6880BD9200000 + 0x00, 0x00, 0xA0, 0xFE, 0x0B, 0x8C, 0xE6, 0x03, // 0x3E68C0BFEA00000 + 0x00, 0x00, 0x60, 0x6F, 0x0C, 0x90, 0xE6, 0x03, // 0x3E6900C6F600000 + 0x00, 0x00, 0xC0, 0x75, 0x0D, 0x94, 0xE6, 0x03, // 0x3E6940D75C00000 + 0x00, 0x00, 0xC0, 0x87, 0x0F, 0x98, 0xE6, 0x03, // 0x3E6980F87C00000 + 0x00, 0x00, 0xA0, 0x6E, 0x0E, 0x9C, 0xE6, 0x03, // 0x3E69C0E6EA00000 + 0x00, 0x00, 0x80, 0xDC, 0x09, 0xA0, 0xE6, 0x03, // 0x3E6A009DC800000 + 0x00, 0x00, 0x20, 0xDF, 0x0A, 0xA4, 0xE6, 0x03, // 0x3E6A40ADF200000 + 0x00, 0x00, 0xE0, 0x7C, 0x0B, 0xA8, 0xE6, 0x03, // 0x3E6A80B7CE00000 + 0x00, 0x00, 0x40, 0xB7, 0x0B, 0xAC, 0xE6, 0x03, // 0x3E6AC0BB7400000 + 0x00, 0x00, 0x80, 0x03, 0x0C, 0xB0, 0xE6, 0x03, // 0x3E6B00C03800000 + 0x00, 0x00, 0x40, 0x76, 0x0E, 0xB4, 0xE6, 0x03, // 0x3E6B40E76400000 + 0x00, 0x00, 0x20, 0x8D, 0x0E, 0xB8, 0xE6, 0x03, // 0x3E6B80E8D200000 + 0x00, 0x00, 0x40, 0xF3, 0x0F, 0xBC, 0xE6, 0x03, // 0x3E6BC0FF3400000 + 0x00, 0x00, 0xC0, 0x08, 0x10, 0xC0, 0xE6, 0x03, // 0x3E6C01008C00000 + 0x00, 0x00, 0x80, 0x46, 0x12, 0xC4, 0xE6, 0x03, // 0x3E6C41246800000 + 0x00, 0x00, 0xC0, 0xDE, 0x12, 0xC8, 0xE6, 0x03, // 0x3E6C812DEC00000 + 0x00, 0x00, 0x00, 0xE9, 0x12, 0xCC, 0xE6, 0x03, // 0x3E6CC12E9000000 + 0x00, 0x00, 0x00, 0x03, 0x13, 0xD0, 0xE6, 0x03, // 0x3E6D01303000000 + 0x00, 0x00, 0x60, 0xF1, 0x09, 0xD4, 0xE6, 0x03, // 0x3E6D409F1600000 + 0x00, 0x00, 0xC0, 0x35, 0x0F, 0xD8, 0xE6, 0x03, // 0x3E6D80F35C00000 + 0x00, 0x00, 0x80, 0x36, 0x12, 0xDC, 0xE6, 0x03, // 0x3E6DC1236800000 + 0x00, 0x00, 0x00, 0xD7, 0x12, 0xE0, 0xE6, 0x03, // 0x3E6E012D7000000 + 0x00, 0x00, 0x20, 0x1C, 0x0C, 0xE4, 0xE6, 0x03, // 0x3E6E40C1C200000 + 0x00, 0x00, 0xC0, 0xD0, 0x09, 0xE8, 0xE6, 0x03, // 0x3E6E809D0C00000 + 0x00, 0x00, 0x40, 0x1B, 0x0A, 0xEC, 0xE6, 0x03, // 0x3E6EC0A1B400000 + 0x00, 0x00, 0xC0, 0x7D, 0x0B, 0xF0, 0xE6, 0x03, // 0x3E6F00B7DC00000 + 0x00, 0x00, 0xE0, 0x87, 0x0B, 0xF4, 0xE6, 0x03, // 0x3E6F40B87E00000 + 0x00, 0x00, 0x20, 0xB3, 0x0C, 0xF8, 0xE6, 0x03, // 0x3E6F80CB3200000 + 0x00, 0x00, 0x40, 0x40, 0x0D, 0xFC, 0xE6, 0x03, // 0x3E6FC0D40400000 + 0x00, 0x00, 0xC0, 0x39, 0x0E, 0x00, 0xE7, 0x03, // 0x3E7000E39C00000 + 0x00, 0x00, 0x40, 0xC8, 0x0E, 0x04, 0xE7, 0x03, // 0x3E7040EC8400000 + 0x00, 0x00, 0x80, 0x9F, 0x10, 0x08, 0xE7, 0x03, // 0x3E708109F800000 + 0x00, 0x00, 0x80, 0x0F, 0x12, 0x0C, 0xE7, 0x03, // 0x3E70C120F800000 + 0x00, 0x00, 0xA0, 0xF1, 0x13, 0x10, 0xE7, 0x03, // 0x3E71013F1A00000 + 0x00, 0x00, 0x00, 0xD1, 0x0C, 0x14, 0xE7, 0x03, // 0x3E7140CD1000000 + 0x00, 0x00, 0xC0, 0xC5, 0x12, 0x18, 0xE7, 0x03, // 0x3E71812C5C00000 + 0x00, 0x00, 0x20, 0x51, 0x0A, 0x1C, 0xE7, 0x03, // 0x3E71C0A51200000 + 0x00, 0x00, 0x60, 0xEF, 0x0C, 0x20, 0xE7, 0x03, // 0x3E7200CEF600000 + 0x00, 0x00, 0x60, 0xFE, 0x0C, 0x24, 0xE7, 0x03, // 0x3E7240CFE600000 + 0x00, 0x00, 0x20, 0xA8, 0x0D, 0x28, 0xE7, 0x03, // 0x3E7280DA8200000 + 0x00, 0x00, 0x80, 0xD3, 0x0D, 0x2C, 0xE7, 0x03, // 0x3E72C0DD3800000 + 0x00, 0x00, 0x20, 0x81, 0x0E, 0x30, 0xE7, 0x03, // 0x3E7300E81200000 + 0x00, 0x00, 0x20, 0xAB, 0x0E, 0x34, 0xE7, 0x03, // 0x3E7340EAB200000 + 0x00, 0x00, 0x60, 0x0D, 0x0F, 0x38, 0xE7, 0x03, // 0x3E7380F0D600000 + 0x00, 0x00, 0x00, 0xA2, 0x0F, 0x3C, 0xE7, 0x03, // 0x3E73C0FA2000000 + 0x00, 0x00, 0xC0, 0x0B, 0x13, 0x40, 0xE7, 0x03, // 0x3E740130BC00000 + 0x00, 0x00, 0xA0, 0x2D, 0x0A, 0x44, 0xE7, 0x03, // 0x3E7440A2DA00000 + 0x00, 0x00, 0xC0, 0x45, 0x0C, 0x48, 0xE7, 0x03, // 0x3E7480C45C00000 + 0x00, 0x00, 0x00, 0xCF, 0x12, 0x4C, 0xE7, 0x03, // 0x3E74C12CF000000 + 0x00, 0x00, 0x60, 0x05, 0x0A, 0x50, 0xE7, 0x03, // 0x3E7500A05600000 + 0x00, 0x00, 0x20, 0xA3, 0x0B, 0x54, 0xE7, 0x03, // 0x3E7540BA3200000 + 0x00, 0x00, 0x40, 0xBD, 0x0D, 0x58, 0xE7, 0x03, // 0x3E7580DBD400000 + 0x00, 0x00, 0x40, 0xE5, 0x11, 0x5C, 0xE7, 0x03, // 0x3E75C11E5400000 + 0x00, 0x00, 0x60, 0xF1, 0x0B, 0x60, 0xE7, 0x03, // 0x3E7600BF1600000 + 0x00, 0x00, 0x80, 0x28, 0x0C, 0x64, 0xE7, 0x03, // 0x3E7640C28800000 + 0x00, 0x00, 0xE0, 0x02, 0x0D, 0x68, 0xE7, 0x03, // 0x3E7680D02E00000 + 0x00, 0x00, 0xE0, 0x70, 0x0E, 0x6C, 0xE7, 0x03, // 0x3E76C0E70E00000 + 0x00, 0x00, 0xC0, 0xD0, 0x12, 0x70, 0xE7, 0x03, // 0x3E77012D0C00000 + 0x00, 0x00, 0x20, 0x45, 0x0A, 0x74, 0xE7, 0x03, // 0x3E7740A45200000 + 0x00, 0x00, 0xE0, 0x81, 0x0A, 0x78, 0xE7, 0x03, // 0x3E7780A81E00000 + 0x00, 0x00, 0xA0, 0x8C, 0x0B, 0x7C, 0xE7, 0x03, // 0x3E77C0B8CA00000 + 0x00, 0x00, 0x60, 0xC2, 0x0C, 0x80, 0xE7, 0x03, // 0x3E7800CC2600000 + 0x00, 0x00, 0xC0, 0xE9, 0x0C, 0x84, 0xE7, 0x03, // 0x3E7840CE9C00000 + 0x00, 0x00, 0x00, 0x15, 0x0D, 0x88, 0xE7, 0x03, // 0x3E7880D15000000 + 0x00, 0x00, 0xA0, 0x9C, 0x0D, 0x8C, 0xE7, 0x03, // 0x3E78C0D9CA00000 + 0x00, 0x00, 0xC0, 0x80, 0x0E, 0x90, 0xE7, 0x03, // 0x3E7900E80C00000 + 0x00, 0x00, 0x40, 0xBC, 0x0E, 0x94, 0xE7, 0x03, // 0x3E7940EBC400000 + 0x00, 0x00, 0x20, 0xEF, 0x0F, 0x98, 0xE7, 0x03, // 0x3E7980FEF200000 + 0x00, 0x00, 0xE0, 0x19, 0x11, 0x9C, 0xE7, 0x03, // 0x3E79C1119E00000 + 0x00, 0x00, 0x20, 0x1C, 0x11, 0xA0, 0xE7, 0x03, // 0x3E7A0111C200000 + 0x00, 0x00, 0x80, 0x39, 0x12, 0xA4, 0xE7, 0x03, // 0x3E7A41239800000 + 0x00, 0x00, 0x40, 0xDC, 0x12, 0xA8, 0xE7, 0x03, // 0x3E7A812DC400000 + 0x00, 0x00, 0xE0, 0x67, 0x0A, 0xAC, 0xE7, 0x03, // 0x3E7AC0A67E00000 + 0x00, 0x00, 0x40, 0xD7, 0x0D, 0xB0, 0xE7, 0x03, // 0x3E7B00DD7400000 + 0x00, 0x00, 0xA0, 0x83, 0x0A, 0xB4, 0xE7, 0x03, // 0x3E7B40A83A00000 + 0x00, 0x00, 0x00, 0x3A, 0x0E, 0xB8, 0xE7, 0x03, // 0x3E7B80E3A000000 + 0x00, 0x00, 0x00, 0x93, 0x0E, 0xBC, 0xE7, 0x03, // 0x3E7BC0E93000000 + 0x00, 0x00, 0x40, 0xBF, 0x10, 0xC0, 0xE7, 0x03, // 0x3E7C010BF400000 + 0x00, 0x00, 0x60, 0xD4, 0x12, 0xC4, 0xE7, 0x03, // 0x3E7C412D4600000 + 0x00, 0x00, 0xE0, 0x8A, 0x13, 0xC8, 0xE7, 0x03, // 0x3E7C8138AE00000 + 0x00, 0x00, 0xE0, 0xD3, 0x13, 0xCC, 0xE7, 0x03, // 0x3E7CC13D3E00000 + 0x00, 0x00, 0xE0, 0xF2, 0x0C, 0xD0, 0xE7, 0x03, // 0x3E7D00CF2E00000 + 0x00, 0x00, 0x60, 0xB9, 0x0D, 0xD4, 0xE7, 0x03, // 0x3E7D40DB9600000 + 0x00, 0x00, 0x00, 0x3D, 0x10, 0xD8, 0xE7, 0x03, // 0x3E7D8103D000000 + 0x00, 0x00, 0x60, 0x59, 0x0F, 0xDC, 0xE7, 0x03, // 0x3E7DC0F59600000 + 0x00, 0x00, 0x00, 0x64, 0x0F, 0xE0, 0xE7, 0x03, // 0x3E7E00F64000000 + 0x00, 0x00, 0x40, 0x92, 0x0F, 0xE4, 0xE7, 0x03, // 0x3E7E40F92400000 + 0x00, 0x00, 0x00, 0x58, 0x0E, 0xE8, 0xE7, 0x03, // 0x3E7E80E58000000 + 0x00, 0x00, 0x20, 0x13, 0x0E, 0xEC, 0xE7, 0x03, // 0x3E7EC0E13200000 + 0x00, 0x00, 0x00, 0x6B, 0x11, 0xF0, 0xE7, 0x03, // 0x3E7F0116B000000 + 0x00, 0x00, 0x00, 0xD8, 0x09, 0xF4, 0xE7, 0x03, // 0x3E7F409D8000000 + 0x00, 0x00, 0xC0, 0x66, 0x10, 0xF8, 0xE7, 0x03, // 0x3E7F81066C00000 + 0x00, 0x00, 0x40, 0x47, 0x0A, 0xFC, 0xE7, 0x03, // 0x3E7FC0A47400000 + 0x00, 0x00, 0xE0, 0x40, 0x0A, 0x00, 0xE8, 0x03, // 0x3E8000A40E00000 + 0x00, 0x00, 0xC0, 0xD4, 0x0B, 0x04, 0xE8, 0x03, // 0x3E8040BD4C00000 + 0x00, 0x00, 0x60, 0x5A, 0x0C, 0x08, 0xE8, 0x03, // 0x3E8080C5A600000 + 0x00, 0x00, 0xC0, 0x9A, 0x0F, 0x0C, 0xE8, 0x03, // 0x3E80C0F9AC00000 + 0x00, 0x00, 0xA0, 0x70, 0x0B, 0x10, 0xE8, 0x03, // 0x3E8100B70A00000 + 0x00, 0x00, 0xC0, 0xA3, 0x0D, 0x14, 0xE8, 0x03, // 0x3E8140DA3C00000 + 0x00, 0x00, 0x80, 0xD6, 0x0C, 0x18, 0xE8, 0x03, // 0x3E8180CD6800000 + 0x00, 0x00, 0x60, 0xE7, 0x11, 0x1C, 0xE8, 0x03, // 0x3E81C11E7600000 + 0x00, 0x00, 0x80, 0x09, 0x11, 0x20, 0xE8, 0x03, // 0x3E8201109800000 + 0x00, 0x00, 0xA0, 0xC9, 0x12, 0x24, 0xE8, 0x03, // 0x3E82412C9A00000 + 0x00, 0x00, 0x60, 0x31, 0x11, 0x28, 0xE8, 0x03, // 0x3E8281131600000 + 0x00, 0x00, 0x60, 0xDA, 0x0B, 0x2C, 0xE8, 0x03, // 0x3E82C0BDA600000 + 0x00, 0x00, 0x00, 0x28, 0x0A, 0x30, 0xE8, 0x03, // 0x3E8300A28000000 + 0x00, 0x00, 0x00, 0xB8, 0x0A, 0x34, 0xE8, 0x03, // 0x3E8340AB8000000 + 0x00, 0x00, 0x40, 0x0B, 0x0B, 0x40, 0xE8, 0x03, // 0x3E8400B0B400000 + 0x00, 0x00, 0x80, 0xCE, 0x0C, 0x48, 0xE8, 0x03, // 0x3E8480CCE800000 + 0x00, 0x00, 0xC0, 0x3B, 0x0A, 0x54, 0xE8, 0x03, // 0x3E8540A3BC00000 + 0x00, 0x00, 0x40, 0x65, 0x0E, 0x58, 0xE8, 0x03, // 0x3E8580E65400000 + 0x00, 0x00, 0x40, 0xD9, 0x0E, 0x5C, 0xE8, 0x03, // 0x3E85C0ED9400000 + 0x00, 0x00, 0x80, 0x27, 0x0F, 0x60, 0xE8, 0x03, // 0x3E8600F27800000 + 0x00, 0x00, 0xC0, 0x2B, 0x0F, 0x64, 0xE8, 0x03, // 0x3E8640F2BC00000 + 0x00, 0x00, 0xA0, 0x2C, 0x0F, 0x68, 0xE8, 0x03, // 0x3E8680F2CA00000 + 0x00, 0x00, 0xE0, 0x31, 0x0F, 0x6C, 0xE8, 0x03, // 0x3E86C0F31E00000 + 0x00, 0x00, 0xC0, 0xEA, 0x12, 0x70, 0xE8, 0x03, // 0x3E87012EAC00000 + 0x00, 0x00, 0xC0, 0x97, 0x0F, 0x74, 0xE8, 0x03, // 0x3E8740F97C00000 + 0x00, 0x00, 0xA0, 0xF7, 0x0F, 0x78, 0xE8, 0x03, // 0x3E8780FF7A00000 + 0x00, 0x00, 0x40, 0xC2, 0x10, 0x80, 0xE8, 0x03, // 0x3E88010C2400000 + 0x00, 0x00, 0x00, 0x5F, 0x11, 0x88, 0xE8, 0x03, // 0x3E888115F000000 + 0x00, 0x00, 0x00, 0x07, 0x12, 0x94, 0xE8, 0x03, // 0x3E8941207000000 + 0x00, 0x00, 0xA0, 0x1F, 0x12, 0x98, 0xE8, 0x03, // 0x3E898121FA00000 + 0x00, 0x00, 0xE0, 0x1D, 0x13, 0xA8, 0xE8, 0x03, // 0x3E8A8131DE00000 + 0x00, 0x00, 0x80, 0x1F, 0x13, 0xAC, 0xE8, 0x03, // 0x3E8AC131F800000 + 0x00, 0x00, 0x00, 0x25, 0x13, 0xB0, 0xE8, 0x03, // 0x3E8B01325000000 + 0x00, 0x00, 0x80, 0xB6, 0x13, 0xB4, 0xE8, 0x03, // 0x3E8B413B6800000 + 0x00, 0x00, 0xC0, 0x1B, 0x12, 0xB8, 0xE8, 0x03, // 0x3E8B8121BC00000 + 0x00, 0x00, 0xE0, 0xD6, 0x12, 0xBC, 0xE8, 0x03, // 0x3E8BC12D6E00000 + 0x00, 0x00, 0xC0, 0xF5, 0x09, 0xC0, 0xE8, 0x03, // 0x3E8C009F5C00000 + 0x00, 0x00, 0xE0, 0x1C, 0x0A, 0xC4, 0xE8, 0x03, // 0x3E8C40A1CE00000 + 0x00, 0x00, 0xA0, 0x29, 0x0A, 0xC8, 0xE8, 0x03, // 0x3E8C80A29A00000 + 0x00, 0x00, 0x20, 0x59, 0x0A, 0xCC, 0xE8, 0x03, // 0x3E8CC0A59200000 + 0x00, 0x00, 0x80, 0x5C, 0x0A, 0xD0, 0xE8, 0x03, // 0x3E8D00A5C800000 + 0x00, 0x00, 0x20, 0x6A, 0x0A, 0xD4, 0xE8, 0x03, // 0x3E8D40A6A200000 + 0x00, 0x00, 0xA0, 0xB3, 0x0A, 0xD8, 0xE8, 0x03, // 0x3E8D80AB3A00000 + 0x00, 0x00, 0xC0, 0xC0, 0x0A, 0xDC, 0xE8, 0x03, // 0x3E8DC0AC0C00000 + 0x00, 0x00, 0x00, 0xCD, 0x0A, 0xE0, 0xE8, 0x03, // 0x3E8E00ACD000000 + 0x00, 0x00, 0x00, 0x08, 0x0B, 0xE4, 0xE8, 0x03, // 0x3E8E40B08000000 + 0x00, 0x00, 0x00, 0x15, 0x0B, 0xE8, 0xE8, 0x03, // 0x3E8E80B15000000 + 0x00, 0x00, 0x80, 0x8C, 0x0B, 0xEC, 0xE8, 0x03, // 0x3E8EC0B8C800000 + 0x00, 0x00, 0xC0, 0x8D, 0x0B, 0xF0, 0xE8, 0x03, // 0x3E8F00B8DC00000 + 0x00, 0x00, 0x80, 0x12, 0x0C, 0xF4, 0xE8, 0x03, // 0x3E8F40C12800000 + 0x00, 0x00, 0x00, 0x2D, 0x0C, 0xF8, 0xE8, 0x03, // 0x3E8F80C2D000000 + 0x00, 0x00, 0xC0, 0x31, 0x0C, 0xFC, 0xE8, 0x03, // 0x3E8FC0C31C00000 + 0x00, 0x00, 0x40, 0x3E, 0x0C, 0x00, 0xE9, 0x03, // 0x3E9000C3E400000 + 0x00, 0x00, 0xE0, 0xA9, 0x0C, 0x04, 0xE9, 0x03, // 0x3E9040CA9E00000 + 0x00, 0x00, 0x40, 0xBC, 0x0C, 0x08, 0xE9, 0x03, // 0x3E9080CBC400000 + 0x00, 0x00, 0x20, 0xD2, 0x0C, 0x0C, 0xE9, 0x03, // 0x3E90C0CD2200000 + 0x00, 0x00, 0xA0, 0x10, 0x0D, 0x10, 0xE9, 0x03, // 0x3E9100D10A00000 + 0x00, 0x00, 0xE0, 0xAE, 0x0D, 0x14, 0xE9, 0x03, // 0x3E9140DAEE00000 + 0x00, 0x00, 0x40, 0xC3, 0x0D, 0x18, 0xE9, 0x03, // 0x3E9180DC3400000 + 0x00, 0x00, 0x40, 0xE4, 0x0D, 0x1C, 0xE9, 0x03, // 0x3E91C0DE4400000 + 0x00, 0x00, 0xC0, 0x2D, 0x0E, 0x20, 0xE9, 0x03, // 0x3E9200E2DC00000 + 0x00, 0x00, 0x60, 0x45, 0x0E, 0x24, 0xE9, 0x03, // 0x3E9240E45600000 + 0x00, 0x00, 0x40, 0x84, 0x0E, 0x28, 0xE9, 0x03, // 0x3E9280E84400000 + 0x00, 0x00, 0x20, 0x12, 0x0F, 0x2C, 0xE9, 0x03, // 0x3E92C0F12200000 + 0x00, 0x00, 0xC0, 0x27, 0x0F, 0x30, 0xE9, 0x03, // 0x3E9300F27C00000 + 0x00, 0x00, 0x20, 0x29, 0x0F, 0x34, 0xE9, 0x03, // 0x3E9340F29200000 + 0x00, 0x00, 0x00, 0x29, 0x0F, 0x38, 0xE9, 0x03, // 0x3E9380F29000000 + 0x00, 0x00, 0x00, 0x2A, 0x0F, 0x3C, 0xE9, 0x03, // 0x3E93C0F2A000000 + 0x00, 0x00, 0xC0, 0x2A, 0x0F, 0x40, 0xE9, 0x03, // 0x3E9400F2AC00000 + 0x00, 0x00, 0xA0, 0x2B, 0x0F, 0x44, 0xE9, 0x03, // 0x3E9440F2BA00000 + 0x00, 0x00, 0xA0, 0x31, 0x0F, 0x48, 0xE9, 0x03, // 0x3E9480F31A00000 + 0x00, 0x00, 0xC0, 0x31, 0x0F, 0x4C, 0xE9, 0x03, // 0x3E94C0F31C00000 + 0x00, 0x00, 0x00, 0x48, 0x0F, 0x50, 0xE9, 0x03, // 0x3E9500F48000000 + 0x00, 0x00, 0x20, 0x50, 0x0F, 0x54, 0xE9, 0x03, // 0x3E9540F50200000 + 0x00, 0x00, 0x00, 0x78, 0x0F, 0x58, 0xE9, 0x03, // 0x3E9580F78000000 + 0x00, 0x00, 0x80, 0xBE, 0x0F, 0x5C, 0xE9, 0x03, // 0x3E95C0FBE800000 + 0x00, 0x00, 0x20, 0xC1, 0x0F, 0x60, 0xE9, 0x03, // 0x3E9600FC1200000 + 0x00, 0x00, 0x20, 0xC8, 0x0F, 0x64, 0xE9, 0x03, // 0x3E9640FC8200000 + 0x00, 0x00, 0x40, 0xEE, 0x0F, 0x68, 0xE9, 0x03, // 0x3E9680FEE400000 + 0x00, 0x00, 0xA0, 0x00, 0x10, 0x6C, 0xE9, 0x03, // 0x3E96C1000A00000 + 0x00, 0x00, 0xA0, 0x3D, 0x10, 0x70, 0xE9, 0x03, // 0x3E970103DA00000 + 0x00, 0x00, 0x20, 0x4F, 0x10, 0x74, 0xE9, 0x03, // 0x3E974104F200000 + 0x00, 0x00, 0x20, 0x4F, 0x10, 0x78, 0xE9, 0x03, // 0x3E978104F200000 + 0x00, 0x00, 0xE0, 0x8A, 0x10, 0x7C, 0xE9, 0x03, // 0x3E97C108AE00000 + 0x00, 0x00, 0x00, 0x22, 0x11, 0x80, 0xE9, 0x03, // 0x3E9801122000000 + 0x00, 0x00, 0xC0, 0x32, 0x11, 0x84, 0xE9, 0x03, // 0x3E9841132C00000 + 0x00, 0x00, 0x20, 0x60, 0x11, 0x88, 0xE9, 0x03, // 0x3E9881160200000 + 0x00, 0x00, 0x20, 0x67, 0x11, 0x8C, 0xE9, 0x03, // 0x3E98C1167200000 + 0x00, 0x00, 0x60, 0x9A, 0x11, 0x90, 0xE9, 0x03, // 0x3E990119A600000 + 0x00, 0x00, 0x00, 0xA1, 0x11, 0x94, 0xE9, 0x03, // 0x3E99411A1000000 + 0x00, 0x00, 0xC0, 0xF6, 0x11, 0x98, 0xE9, 0x03, // 0x3E99811F6C00000 + 0x00, 0x00, 0x00, 0x07, 0x12, 0x9C, 0xE9, 0x03, // 0x3E99C1207000000 + 0x00, 0x00, 0x60, 0xDC, 0x12, 0xA0, 0xE9, 0x03, // 0x3E9A012DC600000 + 0x00, 0x00, 0xE0, 0xFF, 0x12, 0xA4, 0xE9, 0x03, // 0x3E9A412FFE00000 + 0x00, 0x00, 0x60, 0x07, 0x13, 0xA8, 0xE9, 0x03, // 0x3E9A81307600000 + 0x00, 0x00, 0xA0, 0x0E, 0x0C, 0xAC, 0xE9, 0x03, // 0x3E9AC0C0EA00000 + 0x00, 0x00, 0xC0, 0x5D, 0x48, 0xB0, 0xE9, 0x03, // 0x3E9B0485DC00000 + 0x00, 0x00, 0x00, 0x43, 0x10, 0xB4, 0xE9, 0x03, // 0x3E9B41043000000 + 0x00, 0x00, 0xC0, 0xC4, 0x09, 0xC0, 0xE9, 0x03, // 0x3E9C009C4C00000 + 0x00, 0x00, 0xA0, 0x36, 0x0A, 0xC4, 0xE9, 0x03, // 0x3E9C40A36A00000 + 0x00, 0x00, 0x00, 0x2D, 0x0A, 0xC8, 0xE9, 0x03, // 0x3E9C80A2D000000 + 0x00, 0x00, 0x00, 0xF0, 0x09, 0xCC, 0xE9, 0x03, // 0x3E9CC09F0000000 + 0x00, 0x00, 0xA0, 0x28, 0x0A, 0xD0, 0xE9, 0x03, // 0x3E9D00A28A00000 + 0x00, 0x00, 0x00, 0x30, 0x0A, 0xD4, 0xE9, 0x03, // 0x3E9D40A30000000 + 0x00, 0x00, 0xE0, 0x58, 0x0A, 0xD8, 0xE9, 0x03, // 0x3E9D80A58E00000 + 0x00, 0x00, 0x40, 0x5F, 0x0A, 0xDC, 0xE9, 0x03, // 0x3E9DC0A5F400000 + 0x00, 0x00, 0xA0, 0xB3, 0x0A, 0xE0, 0xE9, 0x03, // 0x3E9E00AB3A00000 + 0x00, 0x00, 0xA0, 0xAA, 0x0A, 0xE4, 0xE9, 0x03, // 0x3E9E40AAAA00000 + 0x00, 0x00, 0x20, 0xB3, 0x0A, 0xE8, 0xE9, 0x03, // 0x3E9E80AB3200000 + 0x00, 0x00, 0x40, 0xBC, 0x0A, 0xEC, 0xE9, 0x03, // 0x3E9EC0ABC400000 + 0x00, 0x00, 0x40, 0x0B, 0x0B, 0xF0, 0xE9, 0x03, // 0x3E9F00B0B400000 + 0x00, 0x00, 0x60, 0x16, 0x0B, 0xF4, 0xE9, 0x03, // 0x3E9F40B16600000 + 0x00, 0x00, 0x80, 0x28, 0x0B, 0xF8, 0xE9, 0x03, // 0x3E9F80B28800000 + 0x00, 0x00, 0x80, 0x2A, 0x0B, 0xFC, 0xE9, 0x03, // 0x3E9FC0B2A800000 + 0x00, 0x00, 0x40, 0x4C, 0x0B, 0x00, 0xEA, 0x03, // 0x3EA000B4C400000 + 0x00, 0x00, 0x00, 0x65, 0x0B, 0x04, 0xEA, 0x03, // 0x3EA040B65000000 + 0x00, 0x00, 0x40, 0xDA, 0x0B, 0x08, 0xEA, 0x03, // 0x3EA080BDA400000 + 0x00, 0x00, 0x20, 0xDB, 0x0B, 0x0C, 0xEA, 0x03, // 0x3EA0C0BDB200000 + 0x00, 0x00, 0x20, 0xED, 0x0B, 0x10, 0xEA, 0x03, // 0x3EA100BED200000 + 0x00, 0x00, 0xA0, 0xF5, 0x0B, 0x14, 0xEA, 0x03, // 0x3EA140BF5A00000 + 0x00, 0x00, 0x00, 0x1B, 0x0C, 0x18, 0xEA, 0x03, // 0x3EA180C1B000000 + 0x00, 0x00, 0xC0, 0x29, 0x0C, 0x1C, 0xEA, 0x03, // 0x3EA1C0C29C00000 + 0x00, 0x00, 0x00, 0x21, 0x0C, 0x20, 0xEA, 0x03, // 0x3EA200C21000000 + 0x00, 0x00, 0xC0, 0x31, 0x0C, 0x24, 0xEA, 0x03, // 0x3EA240C31C00000 + 0x00, 0x00, 0x00, 0x2C, 0x0C, 0x28, 0xEA, 0x03, // 0x3EA280C2C000000 + 0x00, 0x00, 0x40, 0x3E, 0x0C, 0x2C, 0xEA, 0x03, // 0x3EA2C0C3E400000 + 0x00, 0x00, 0x80, 0x46, 0x0C, 0x30, 0xEA, 0x03, // 0x3EA300C46800000 + 0x00, 0x00, 0x80, 0x78, 0x0C, 0x34, 0xEA, 0x03, // 0x3EA340C78800000 + 0x00, 0x00, 0x80, 0x83, 0x0C, 0x38, 0xEA, 0x03, // 0x3EA380C83800000 + 0x00, 0x00, 0x40, 0x8A, 0x0C, 0x3C, 0xEA, 0x03, // 0x3EA3C0C8A400000 + 0x00, 0x00, 0xC0, 0xAA, 0x0C, 0x40, 0xEA, 0x03, // 0x3EA400CAAC00000 + 0x00, 0x00, 0x80, 0xCE, 0x0C, 0x44, 0xEA, 0x03, // 0x3EA440CCE800000 + 0x00, 0x00, 0xE0, 0xE2, 0x0C, 0x48, 0xEA, 0x03, // 0x3EA480CE2E00000 + 0x00, 0x00, 0x60, 0xE3, 0x0C, 0x4C, 0xEA, 0x03, // 0x3EA4C0CE3600000 + 0x00, 0x00, 0xC0, 0xEA, 0x0C, 0x50, 0xEA, 0x03, // 0x3EA500CEAC00000 + 0x00, 0x00, 0x20, 0x6F, 0x0D, 0x54, 0xEA, 0x03, // 0x3EA540D6F200000 + 0x00, 0x00, 0x40, 0x77, 0x0D, 0x58, 0xEA, 0x03, // 0x3EA580D77400000 + 0x00, 0x00, 0x20, 0xA8, 0x0D, 0x5C, 0xEA, 0x03, // 0x3EA5C0DA8200000 + 0x00, 0x00, 0x60, 0xDB, 0x0D, 0x60, 0xEA, 0x03, // 0x3EA600DDB600000 + 0x00, 0x00, 0x60, 0xD9, 0x0D, 0x64, 0xEA, 0x03, // 0x3EA640DD9600000 + 0x00, 0x00, 0x40, 0xE4, 0x0D, 0x68, 0xEA, 0x03, // 0x3EA680DE4400000 + 0x00, 0x00, 0xC0, 0x03, 0x0E, 0x6C, 0xEA, 0x03, // 0x3EA6C0E03C00000 + 0x00, 0x00, 0xC0, 0x2D, 0x0E, 0x70, 0xEA, 0x03, // 0x3EA700E2DC00000 + 0x00, 0x00, 0xE0, 0xF4, 0x0E, 0x74, 0xEA, 0x03, // 0x3EA740EF4E00000 + 0x00, 0x00, 0xA0, 0x46, 0x0E, 0x78, 0xEA, 0x03, // 0x3EA780E46A00000 + 0x00, 0x00, 0xE0, 0x55, 0x0E, 0x7C, 0xEA, 0x03, // 0x3EA7C0E55E00000 + 0x00, 0x00, 0x40, 0x65, 0x0E, 0x80, 0xEA, 0x03, // 0x3EA800E65400000 + 0x00, 0x00, 0x20, 0x8E, 0x0E, 0x84, 0xEA, 0x03, // 0x3EA840E8E200000 + 0x00, 0x00, 0xC0, 0xA0, 0x0E, 0x88, 0xEA, 0x03, // 0x3EA880EA0C00000 + 0x00, 0x00, 0x60, 0xA7, 0x0E, 0x8C, 0xEA, 0x03, // 0x3EA8C0EA7600000 + 0x00, 0x00, 0xA0, 0xC3, 0x0E, 0x90, 0xEA, 0x03, // 0x3EA900EC3A00000 + 0x00, 0x00, 0xE0, 0xC3, 0x0E, 0x94, 0xEA, 0x03, // 0x3EA940EC3E00000 + 0x00, 0x00, 0x40, 0xD9, 0x0E, 0x98, 0xEA, 0x03, // 0x3EA980ED9400000 + 0x00, 0x00, 0x60, 0xDB, 0x0E, 0x9C, 0xEA, 0x03, // 0x3EA9C0EDB600000 + 0x00, 0x00, 0x80, 0xDE, 0x0E, 0xA0, 0xEA, 0x03, // 0x3EAA00EDE800000 + 0x00, 0x00, 0x40, 0xE9, 0x0E, 0xA4, 0xEA, 0x03, // 0x3EAA40EE9400000 + 0x00, 0x00, 0x00, 0xE8, 0x0E, 0xA8, 0xEA, 0x03, // 0x3EAA80EE8000000 + 0x00, 0x00, 0x80, 0x19, 0x0F, 0xAC, 0xEA, 0x03, // 0x3EAAC0F19800000 + 0x00, 0x00, 0x20, 0x56, 0x0F, 0xB0, 0xEA, 0x03, // 0x3EAB00F56200000 + 0x00, 0x00, 0x00, 0x78, 0x0F, 0xB4, 0xEA, 0x03, // 0x3EAB40F78000000 + 0x00, 0x00, 0x60, 0x8F, 0x0F, 0xB8, 0xEA, 0x03, // 0x3EAB80F8F600000 + 0x00, 0x00, 0x60, 0xAB, 0x0F, 0xBC, 0xEA, 0x03, // 0x3EABC0FAB600000 + 0x00, 0x00, 0x80, 0xBE, 0x0F, 0xC0, 0xEA, 0x03, // 0x3EAC00FBE800000 + 0x00, 0x00, 0xC0, 0xE7, 0x0F, 0xC4, 0xEA, 0x03, // 0x3EAC40FE7C00000 + 0x00, 0x00, 0xA0, 0x00, 0x10, 0xC8, 0xEA, 0x03, // 0x3EAC81000A00000 + 0x00, 0x00, 0x40, 0x6A, 0x10, 0xCC, 0xEA, 0x03, // 0x3EACC106A400000 + 0x00, 0x00, 0xE0, 0x7D, 0x10, 0xD0, 0xEA, 0x03, // 0x3EAD0107DE00000 + 0x00, 0x00, 0x20, 0xEF, 0x10, 0xD4, 0xEA, 0x03, // 0x3EAD410EF200000 + 0x00, 0x00, 0x20, 0x28, 0x11, 0xD8, 0xEA, 0x03, // 0x3EAD81128200000 + 0x00, 0x00, 0xC0, 0x30, 0x11, 0xDC, 0xEA, 0x03, // 0x3EADC1130C00000 + 0x00, 0x00, 0xC0, 0x32, 0x11, 0xE0, 0xEA, 0x03, // 0x3EAE01132C00000 + 0x00, 0x00, 0xE0, 0x57, 0x11, 0xE4, 0xEA, 0x03, // 0x3EAE41157E00000 + 0x00, 0x00, 0x00, 0x5F, 0x11, 0xE8, 0xEA, 0x03, // 0x3EAE8115F000000 + 0x00, 0x00, 0x60, 0x59, 0x11, 0xEC, 0xEA, 0x03, // 0x3EAEC1159600000 + 0x00, 0x00, 0x20, 0x60, 0x11, 0xF0, 0xEA, 0x03, // 0x3EAF01160200000 + 0x00, 0x00, 0xC0, 0x5F, 0x11, 0xF4, 0xEA, 0x03, // 0x3EAF4115FC00000 + 0x00, 0x00, 0xA0, 0x5D, 0x11, 0xF8, 0xEA, 0x03, // 0x3EAF8115DA00000 + 0x00, 0x00, 0x20, 0x67, 0x11, 0xFC, 0xEA, 0x03, // 0x3EAFC1167200000 + 0x00, 0x00, 0x40, 0x71, 0x11, 0x00, 0xEB, 0x03, // 0x3EB001171400000 + 0x00, 0x00, 0x00, 0xA1, 0x11, 0x04, 0xEB, 0x03, // 0x3EB0411A1000000 + 0x00, 0x00, 0x00, 0xE7, 0x11, 0x08, 0xEB, 0x03, // 0x3EB0811E7000000 + 0x00, 0x00, 0x40, 0x0E, 0x12, 0x0C, 0xEB, 0x03, // 0x3EB0C120E400000 + 0x00, 0x00, 0x20, 0x33, 0x12, 0x10, 0xEB, 0x03, // 0x3EB101233200000 + 0x00, 0x00, 0xC0, 0x4E, 0x12, 0x14, 0xEB, 0x03, // 0x3EB14124EC00000 + 0x00, 0x00, 0x80, 0xCF, 0x12, 0x18, 0xEB, 0x03, // 0x3EB1812CF800000 + 0x00, 0x00, 0x60, 0xDC, 0x12, 0x1C, 0xEB, 0x03, // 0x3EB1C12DC600000 + 0x00, 0x00, 0xC0, 0xEA, 0x12, 0x20, 0xEB, 0x03, // 0x3EB2012EAC00000 + 0x00, 0x00, 0x60, 0xFB, 0x12, 0x24, 0xEB, 0x03, // 0x3EB2412FB600000 + 0x00, 0x00, 0xE0, 0xFF, 0x12, 0x28, 0xEB, 0x03, // 0x3EB2812FFE00000 + 0x00, 0x00, 0x60, 0x01, 0x13, 0x2C, 0xEB, 0x03, // 0x3EB2C1301600000 + 0x00, 0x00, 0x60, 0x07, 0x13, 0x30, 0xEB, 0x03, // 0x3EB301307600000 + 0x00, 0x00, 0x40, 0x62, 0x13, 0x34, 0xEB, 0x03, // 0x3EB341362400000 + 0x00, 0x00, 0x80, 0xF3, 0x13, 0x38, 0xEB, 0x03, // 0x3EB3813F3800000 + 0x00, 0x00, 0x40, 0x09, 0x45, 0x3C, 0xEB, 0x03, // 0x3EB3C4509400000 + 0x00, 0x00, 0x80, 0x08, 0x45, 0x40, 0xEB, 0x03, // 0x3EB404508800000 + 0x00, 0x00, 0xA0, 0x7A, 0x46, 0x44, 0xEB, 0x03, // 0x3EB44467AA00000 + 0x00, 0x00, 0xA0, 0x73, 0x07, 0x48, 0xEB, 0x03, // 0x3EB480773A00000 + 0x00, 0x00, 0x00, 0x03, 0x08, 0x4C, 0xEB, 0x03, // 0x3EB4C0803000000 + 0x00, 0x00, 0x20, 0x07, 0x08, 0x50, 0xEB, 0x03, // 0x3EB500807200000 + 0x00, 0x00, 0x20, 0x49, 0x4A, 0x54, 0xEB, 0x03, // 0x3EB544A49200000 + 0x00, 0x00, 0x00, 0x9A, 0x4B, 0x58, 0xEB, 0x03, // 0x3EB584B9A000000 + 0x00, 0x00, 0x60, 0xDA, 0x4F, 0x5C, 0xEB, 0x03, // 0x3EB5C4FDA600000 + 0x00, 0x00, 0x60, 0xE8, 0x13, 0x60, 0xEB, 0x03, // 0x3EB6013E8600000 + 0x00, 0x00, 0xC0, 0xF1, 0x13, 0x64, 0xEB, 0x03, // 0x3EB6413F1C00000 + 0xB4, 0x05, 0x20, 0xBB, 0x00, 0x74, 0xEC, 0x03, // 0x3EC7400BB2005B4 + 0xB7, 0x05, 0x40, 0xBE, 0x00, 0x7C, 0xEC, 0x03, // 0x3EC7C00BE4005B7 + 0xC1, 0x05, 0x20, 0xBD, 0x00, 0xA8, 0xEC, 0x03, // 0x3ECA800BD2005C1 + 0xC2, 0x05, 0x20, 0xBD, 0x00, 0xAC, 0xEC, 0x03, // 0x3ECAC00BD2005C2 + 0xC1, 0x05, 0x20, 0x69, 0x1F, 0xB0, 0xEC, 0x03, // 0x3ECB01F692005C1 + 0xC2, 0x05, 0x20, 0x69, 0x1F, 0xB4, 0xEC, 0x03, // 0x3ECB41F692005C2 + 0xB7, 0x05, 0x00, 0xBA, 0x00, 0xB8, 0xEC, 0x03, // 0x3ECB800BA0005B7 + 0xB8, 0x05, 0x00, 0xBA, 0x00, 0xBC, 0xEC, 0x03, // 0x3ECBC00BA0005B8 + 0xBC, 0x05, 0x00, 0xBA, 0x00, 0xC0, 0xEC, 0x03, // 0x3ECC000BA0005BC + 0xBC, 0x05, 0x20, 0xBA, 0x00, 0xC4, 0xEC, 0x03, // 0x3ECC400BA2005BC + 0xBC, 0x05, 0x40, 0xBA, 0x00, 0xC8, 0xEC, 0x03, // 0x3ECC800BA4005BC + 0xBC, 0x05, 0x60, 0xBA, 0x00, 0xCC, 0xEC, 0x03, // 0x3ECCC00BA6005BC + 0xBC, 0x05, 0x80, 0xBA, 0x00, 0xD0, 0xEC, 0x03, // 0x3ECD000BA8005BC + 0xBC, 0x05, 0xA0, 0xBA, 0x00, 0xD4, 0xEC, 0x03, // 0x3ECD400BAA005BC + 0xBC, 0x05, 0xC0, 0xBA, 0x00, 0xD8, 0xEC, 0x03, // 0x3ECD800BAC005BC + 0xBC, 0x05, 0x00, 0xBB, 0x00, 0xE0, 0xEC, 0x03, // 0x3ECE000BB0005BC + 0xBC, 0x05, 0x20, 0xBB, 0x00, 0xE4, 0xEC, 0x03, // 0x3ECE400BB2005BC + 0xBC, 0x05, 0x40, 0xBB, 0x00, 0xE8, 0xEC, 0x03, // 0x3ECE800BB4005BC + 0xBC, 0x05, 0x60, 0xBB, 0x00, 0xEC, 0xEC, 0x03, // 0x3ECEC00BB6005BC + 0xBC, 0x05, 0x80, 0xBB, 0x00, 0xF0, 0xEC, 0x03, // 0x3ECF000BB8005BC + 0xBC, 0x05, 0xC0, 0xBB, 0x00, 0xF8, 0xEC, 0x03, // 0x3ECF800BBC005BC + 0xBC, 0x05, 0x00, 0xBC, 0x00, 0x00, 0xED, 0x03, // 0x3ED0000BC0005BC + 0xBC, 0x05, 0x20, 0xBC, 0x00, 0x04, 0xED, 0x03, // 0x3ED0400BC2005BC + 0xBC, 0x05, 0x60, 0xBC, 0x00, 0x0C, 0xED, 0x03, // 0x3ED0C00BC6005BC + 0xBC, 0x05, 0x80, 0xBC, 0x00, 0x10, 0xED, 0x03, // 0x3ED1000BC8005BC + 0xBC, 0x05, 0xC0, 0xBC, 0x00, 0x18, 0xED, 0x03, // 0x3ED1800BCC005BC + 0xBC, 0x05, 0xE0, 0xBC, 0x00, 0x1C, 0xED, 0x03, // 0x3ED1C00BCE005BC + 0xBC, 0x05, 0x00, 0xBD, 0x00, 0x20, 0xED, 0x03, // 0x3ED2000BD0005BC + 0xBC, 0x05, 0x20, 0xBD, 0x00, 0x24, 0xED, 0x03, // 0x3ED2400BD2005BC + 0xBC, 0x05, 0x40, 0xBD, 0x00, 0x28, 0xED, 0x03, // 0x3ED2800BD4005BC + 0xB9, 0x05, 0xA0, 0xBA, 0x00, 0x2C, 0xED, 0x03, // 0x3ED2C00BAA005B9 + 0xBF, 0x05, 0x20, 0xBA, 0x00, 0x30, 0xED, 0x03, // 0x3ED3000BA2005BF + 0xBF, 0x05, 0x60, 0xBB, 0x00, 0x34, 0xED, 0x03, // 0x3ED3400BB6005BF + 0xBF, 0x05, 0x80, 0xBC, 0x00, 0x38, 0xED, 0x03, // 0x3ED3800BC8005BF + 0x07, 0x03, 0x40, 0xBA, 0x20, 0x24, 0x17, 0x04, // 0x4172420BA400307 + 0x07, 0x03, 0x40, 0xBB, 0x20, 0x90, 0x17, 0x04, // 0x4179020BB400307 + 0xBA, 0x10, 0x21, 0x13, 0x22, 0x68, 0x42, 0x04, // 0x4426822132110BA + 0xBA, 0x10, 0x61, 0x13, 0x22, 0x70, 0x42, 0x04, // 0x4427022136110BA + 0xBA, 0x10, 0xA1, 0x14, 0x22, 0xAC, 0x42, 0x04, // 0x442AC2214A110BA + 0x27, 0x11, 0x21, 0x26, 0x22, 0xB8, 0x44, 0x04, // 0x444B82226211127 + 0x27, 0x11, 0x41, 0x26, 0x22, 0xBC, 0x44, 0x04, // 0x444BC2226411127 + 0x3E, 0x13, 0xE1, 0x68, 0x22, 0x2C, 0x4D, 0x04, // 0x44D2C2268E1133E + 0x57, 0x13, 0xE1, 0x68, 0x22, 0x30, 0x4D, 0x04, // 0x44D302268E11357 + 0xC9, 0x13, 0x41, 0x70, 0x22, 0x0C, 0x4E, 0x04, // 0x44E0C22704113C9 + 0xBB, 0x13, 0x81, 0x70, 0x22, 0x14, 0x4E, 0x04, // 0x44E1422708113BB + 0xC2, 0x13, 0x61, 0x71, 0x22, 0x38, 0x4E, 0x04, // 0x44E3822716113C2 + 0xC9, 0x13, 0x01, 0x72, 0x22, 0x44, 0x4E, 0x04, // 0x44E4422720113C9 + 0xC2, 0x13, 0x41, 0x78, 0x22, 0x14, 0x4F, 0x04, // 0x44F1422784113C2 + 0xB8, 0x13, 0x41, 0x78, 0x22, 0x1C, 0x4F, 0x04, // 0x44F1C22784113B8 + 0xC9, 0x13, 0x41, 0x78, 0x22, 0x20, 0x4F, 0x04, // 0x44F2022784113C9 + 0xBA, 0x14, 0x21, 0x97, 0x22, 0xEC, 0x52, 0x04, // 0x452EC22972114BA + 0xB0, 0x14, 0x21, 0x97, 0x22, 0xF0, 0x52, 0x04, // 0x452F022972114B0 + 0xBD, 0x14, 0x21, 0x97, 0x22, 0xF8, 0x52, 0x04, // 0x452F822972114BD + 0xAF, 0x15, 0x01, 0xB7, 0x22, 0xE8, 0x56, 0x04, // 0x456E822B70115AF + 0xAF, 0x15, 0x21, 0xB7, 0x22, 0xEC, 0x56, 0x04, // 0x456EC22B72115AF + 0x30, 0x19, 0xA1, 0x26, 0x23, 0xE0, 0x64, 0x04, // 0x464E02326A11930 + 0x1E, 0x61, 0xC1, 0x23, 0x2C, 0x84, 0x84, 0x05, // 0x584842C23C1611E + 0x29, 0x61, 0xC1, 0x23, 0x2C, 0x88, 0x84, 0x05, // 0x584882C23C16129 + 0x1F, 0x61, 0xC1, 0x23, 0x2C, 0x8C, 0x84, 0x05, // 0x5848C2C23C1611F + 0x1F, 0x61, 0x21, 0x25, 0x2C, 0x90, 0x84, 0x05, // 0x584902C2521611F + 0x20, 0x61, 0xC1, 0x23, 0x2C, 0x94, 0x84, 0x05, // 0x584942C23C16120 + 0x1F, 0x61, 0x21, 0x24, 0x2C, 0x98, 0x84, 0x05, // 0x584982C2421611F + 0x1F, 0x61, 0x41, 0x24, 0x2C, 0x9C, 0x84, 0x05, // 0x5849C2C2441611F + 0x20, 0x61, 0x21, 0x24, 0x2C, 0xA0, 0x84, 0x05, // 0x584A02C24216120 + 0x67, 0x6D, 0xE1, 0xAC, 0x2D, 0xA0, 0xB5, 0x05, // 0x5B5A02DACE16D67 + 0x67, 0x6D, 0x61, 0xAC, 0x2D, 0xA4, 0xB5, 0x05, // 0x5B5A42DAC616D67 + 0x67, 0x6D, 0x21, 0xAD, 0x2D, 0xA8, 0xB5, 0x05, // 0x5B5A82DAD216D67 + 0x65, 0xD1, 0xE1, 0x2A, 0x3A, 0x78, 0x45, 0x07, // 0x745783A2AE1D165 + 0x65, 0xD1, 0x01, 0x2B, 0x3A, 0x7C, 0x45, 0x07, // 0x7457C3A2B01D165 + 0x6E, 0xD1, 0xE1, 0x2B, 0x3A, 0x80, 0x45, 0x07, // 0x745803A2BE1D16E + 0x6F, 0xD1, 0xE1, 0x2B, 0x3A, 0x84, 0x45, 0x07, // 0x745843A2BE1D16F + 0x70, 0xD1, 0xE1, 0x2B, 0x3A, 0x88, 0x45, 0x07, // 0x745883A2BE1D170 + 0x71, 0xD1, 0xE1, 0x2B, 0x3A, 0x8C, 0x45, 0x07, // 0x7458C3A2BE1D171 + 0x72, 0xD1, 0xE1, 0x2B, 0x3A, 0x90, 0x45, 0x07, // 0x745903A2BE1D172 + 0x65, 0xD1, 0x21, 0x37, 0x3A, 0xEC, 0x46, 0x07, // 0x746EC3A3721D165 + 0x65, 0xD1, 0x41, 0x37, 0x3A, 0xF0, 0x46, 0x07, // 0x746F03A3741D165 + 0x6E, 0xD1, 0x61, 0x37, 0x3A, 0xF4, 0x46, 0x07, // 0x746F43A3761D16E + 0x6E, 0xD1, 0x81, 0x37, 0x3A, 0xF8, 0x46, 0x07, // 0x746F83A3781D16E + 0x6F, 0xD1, 0x61, 0x37, 0x3A, 0xFC, 0x46, 0x07, // 0x746FC3A3761D16F + 0x6F, 0xD1, 0x81, 0x37, 0x3A, 0x00, 0x47, 0x07, // 0x747003A3781D16F + 0x00, 0x00, 0xA0, 0xC7, 0x09, 0x00, 0xE0, 0x0B, // 0xBE00009C7A00000 + 0x00, 0x00, 0x00, 0xC7, 0x09, 0x04, 0xE0, 0x0B, // 0xBE00409C7000000 + 0x00, 0x00, 0x20, 0xC8, 0x09, 0x08, 0xE0, 0x0B, // 0xBE00809C8200000 + 0x00, 0x00, 0x40, 0x24, 0x40, 0x0C, 0xE0, 0x0B, // 0xBE00C4024400000 + 0x00, 0x00, 0x00, 0xEC, 0x09, 0x10, 0xE0, 0x0B, // 0xBE01009EC000000 + 0x00, 0x00, 0xC0, 0xF5, 0x09, 0x14, 0xE0, 0x0B, // 0xBE01409F5C00000 + 0x00, 0x00, 0x60, 0xF7, 0x09, 0x18, 0xE0, 0x0B, // 0xBE01809F7600000 + 0x00, 0x00, 0x40, 0x00, 0x0A, 0x1C, 0xE0, 0x0B, // 0xBE01C0A00400000 + 0x00, 0x00, 0x40, 0x0F, 0x0A, 0x20, 0xE0, 0x0B, // 0xBE0200A0F400000 + 0x00, 0x00, 0x20, 0x13, 0x0A, 0x24, 0xE0, 0x0B, // 0xBE0240A13200000 + 0x00, 0x00, 0xE0, 0x1C, 0x0A, 0x28, 0xE0, 0x0B, // 0xBE0280A1CE00000 + 0x00, 0x00, 0xE0, 0x19, 0x0A, 0x2C, 0xE0, 0x0B, // 0xBE02C0A19E00000 + 0x00, 0x00, 0xC0, 0x93, 0x06, 0x30, 0xE0, 0x0B, // 0xBE0300693C00000 + 0x00, 0x00, 0x40, 0xC7, 0x40, 0x34, 0xE0, 0x0B, // 0xBE03440C7400000 + 0x00, 0x00, 0xA0, 0x29, 0x0A, 0x38, 0xE0, 0x0B, // 0xBE0380A29A00000 + 0x00, 0x00, 0x80, 0x2A, 0x0A, 0x3C, 0xE0, 0x0B, // 0xBE03C0A2A800000 + 0x00, 0x00, 0x80, 0x2C, 0x0A, 0x40, 0xE0, 0x0B, // 0xBE0400A2C800000 + 0x00, 0x00, 0xE0, 0x2E, 0x0A, 0x44, 0xE0, 0x0B, // 0xBE0440A2EE00000 + 0x00, 0x00, 0x80, 0xA3, 0x40, 0x48, 0xE0, 0x0B, // 0xBE04840A3800000 + 0x00, 0x00, 0x20, 0x97, 0x06, 0x4C, 0xE0, 0x0B, // 0xBE04C0697200000 + 0x00, 0x00, 0xE0, 0x2C, 0x0A, 0x50, 0xE0, 0x0B, // 0xBE0500A2CE00000 + 0x00, 0x00, 0xA0, 0x31, 0x0A, 0x54, 0xE0, 0x0B, // 0xBE0540A31A00000 + 0x00, 0x00, 0x60, 0xA9, 0x40, 0x58, 0xE0, 0x0B, // 0xBE05840A9600000 + 0x00, 0x00, 0xE0, 0x32, 0x0A, 0x5C, 0xE0, 0x0B, // 0xBE05C0A32E00000 + 0x00, 0x00, 0x80, 0x34, 0x0A, 0x60, 0xE0, 0x0B, // 0xBE0600A34800000 + 0x00, 0x00, 0x80, 0xD9, 0x09, 0x64, 0xE0, 0x0B, // 0xBE06409D9800000 + 0x00, 0x00, 0x80, 0x35, 0x0A, 0x68, 0xE0, 0x0B, // 0xBE0680A35800000 + 0x00, 0x00, 0xA0, 0x36, 0x0A, 0x6C, 0xE0, 0x0B, // 0xBE06C0A36A00000 + 0x00, 0x00, 0xE0, 0x3B, 0x52, 0x70, 0xE0, 0x0B, // 0xBE070523BE00000 + 0x00, 0x00, 0xA0, 0x3E, 0x0A, 0x74, 0xE0, 0x0B, // 0xBE0740A3EA00000 + 0x00, 0x00, 0x60, 0x40, 0x0A, 0x78, 0xE0, 0x0B, // 0xBE0780A40600000 + 0x00, 0x00, 0xE0, 0x9B, 0x06, 0x7C, 0xE0, 0x0B, // 0xBE07C069BE00000 + 0x00, 0x00, 0x60, 0x47, 0x0A, 0x80, 0xE0, 0x0B, // 0xBE0800A47600000 + 0x00, 0x00, 0xC0, 0x48, 0x0A, 0x84, 0xE0, 0x0B, // 0xBE0840A48C00000 + 0x00, 0x00, 0x40, 0x4E, 0x0A, 0x88, 0xE0, 0x0B, // 0xBE0880A4E400000 + 0x00, 0x00, 0xE0, 0x4E, 0x0A, 0x8C, 0xE0, 0x0B, // 0xBE08C0A4EE00000 + 0x00, 0x00, 0xA0, 0xA2, 0x06, 0x90, 0xE0, 0x0B, // 0xBE09006A2A00000 + 0x00, 0x00, 0xE0, 0x58, 0x0A, 0x94, 0xE0, 0x0B, // 0xBE0940A58E00000 + 0x00, 0x00, 0x20, 0x59, 0x0A, 0x98, 0xE0, 0x0B, // 0xBE0980A59200000 + 0x00, 0x00, 0x80, 0x5C, 0x0A, 0x9C, 0xE0, 0x0B, // 0xBE09C0A5C800000 + 0x00, 0x00, 0x40, 0x5F, 0x0A, 0xA0, 0xE0, 0x0B, // 0xBE0A00A5F400000 + 0x00, 0x00, 0xA0, 0x60, 0x0A, 0xA4, 0xE0, 0x0B, // 0xBE0A40A60A00000 + 0x00, 0x00, 0xC0, 0x60, 0x0A, 0xA8, 0xE0, 0x0B, // 0xBE0A80A60C00000 + 0x00, 0x00, 0xE0, 0x62, 0x0A, 0xAC, 0xE0, 0x0B, // 0xBE0AC0A62E00000 + 0x00, 0x00, 0x20, 0x69, 0x0A, 0xB0, 0xE0, 0x0B, // 0xBE0B00A69200000 + 0x00, 0x00, 0x20, 0x6A, 0x0A, 0xB4, 0xE0, 0x0B, // 0xBE0B40A6A200000 + 0x00, 0x00, 0x40, 0x6B, 0x0A, 0xB8, 0xE0, 0x0B, // 0xBE0B80A6B400000 + 0x00, 0x00, 0x60, 0x6E, 0x0A, 0xBC, 0xE0, 0x0B, // 0xBE0BC0A6E600000 + 0x00, 0x00, 0xA0, 0x6F, 0x0A, 0xC0, 0xE0, 0x0B, // 0xBE0C00A6FA00000 + 0x00, 0x00, 0xE0, 0x6F, 0x0A, 0xC4, 0xE0, 0x0B, // 0xBE0C40A6FE00000 + 0x00, 0x00, 0xE0, 0x6F, 0x0A, 0xC8, 0xE0, 0x0B, // 0xBE0C80A6FE00000 + 0x00, 0x00, 0xE0, 0x6F, 0x0A, 0xCC, 0xE0, 0x0B, // 0xBE0CC0A6FE00000 + 0x00, 0x00, 0x80, 0x45, 0x41, 0xD0, 0xE0, 0x0B, // 0xBE0D04145800000 + 0x00, 0x00, 0x00, 0x0E, 0x0E, 0xD4, 0xE0, 0x0B, // 0xBE0D40E0E000000 + 0x00, 0x00, 0x40, 0x79, 0x0A, 0xD8, 0xE0, 0x0B, // 0xBE0D80A79400000 + 0x00, 0x00, 0xE0, 0x7B, 0x0A, 0xDC, 0xE0, 0x0B, // 0xBE0DC0A7BE00000 + 0x00, 0x00, 0x60, 0x6C, 0x41, 0xE0, 0xE0, 0x0B, // 0xBE0E0416C600000 + 0x00, 0x00, 0x60, 0x7D, 0x0A, 0xE4, 0xE0, 0x0B, // 0xBE0E40A7D600000 + 0x00, 0x00, 0x20, 0x7E, 0x0A, 0xE8, 0xE0, 0x0B, // 0xBE0E80A7E200000 + 0x00, 0x00, 0xC0, 0x80, 0x0A, 0xEC, 0xE0, 0x0B, // 0xBE0EC0A80C00000 + 0x00, 0x00, 0xC0, 0x93, 0x0A, 0xF0, 0xE0, 0x0B, // 0xBE0F00A93C00000 + 0x00, 0x00, 0x00, 0x87, 0x0A, 0xF4, 0xE0, 0x0B, // 0xBE0F40A87000000 + 0x00, 0x00, 0x00, 0x89, 0x0A, 0xF8, 0xE0, 0x0B, // 0xBE0F80A89000000 + 0x00, 0x00, 0x00, 0x8D, 0x0A, 0xFC, 0xE0, 0x0B, // 0xBE0FC0A8D000000 + 0x00, 0x00, 0x40, 0x94, 0x0A, 0x00, 0xE1, 0x0B, // 0xBE1000A94400000 + 0x00, 0x00, 0xC0, 0x9E, 0x0A, 0x04, 0xE1, 0x0B, // 0xBE1040A9EC00000 + 0x00, 0x00, 0x00, 0xA2, 0x0A, 0x08, 0xE1, 0x0B, // 0xBE1080AA2000000 + 0x00, 0x00, 0x60, 0xAA, 0x0A, 0x0C, 0xE1, 0x0B, // 0xBE10C0AAA600000 + 0x00, 0x00, 0x60, 0xAC, 0x0A, 0x10, 0xE1, 0x0B, // 0xBE1100AAC600000 + 0x00, 0x00, 0x80, 0xB0, 0x0A, 0x14, 0xE1, 0x0B, // 0xBE1140AB0800000 + 0x00, 0x00, 0x80, 0xB0, 0x0A, 0x18, 0xE1, 0x0B, // 0xBE1180AB0800000 + 0x00, 0x00, 0x20, 0xB3, 0x0A, 0x1C, 0xE1, 0x0B, // 0xBE11C0AB3200000 + 0x00, 0x00, 0x60, 0xB5, 0x0A, 0x20, 0xE1, 0x0B, // 0xBE1200AB5600000 + 0x00, 0x00, 0x60, 0xB6, 0x0A, 0x24, 0xE1, 0x0B, // 0xBE1240AB6600000 + 0x00, 0x00, 0x40, 0xB8, 0x0A, 0x28, 0xE1, 0x0B, // 0xBE1280AB8400000 + 0x00, 0x00, 0xC0, 0xE2, 0x0A, 0x2C, 0xE1, 0x0B, // 0xBE12C0AE2C00000 + 0x00, 0x00, 0xC0, 0xC0, 0x0A, 0x30, 0xE1, 0x0B, // 0xBE1300AC0C00000 + 0x00, 0x00, 0xE0, 0xE2, 0x0A, 0x34, 0xE1, 0x0B, // 0xBE1340AE2E00000 + 0x00, 0x00, 0x20, 0xCA, 0x0A, 0x38, 0xE1, 0x0B, // 0xBE1380ACA200000 + 0x00, 0x00, 0x80, 0xCE, 0x0A, 0x3C, 0xE1, 0x0B, // 0xBE13C0ACE800000 + 0x00, 0x00, 0xE0, 0x40, 0x0A, 0x40, 0xE1, 0x0B, // 0xBE1400A40E00000 + 0x00, 0x00, 0xC0, 0x1D, 0x0B, 0x44, 0xE1, 0x0B, // 0xBE1440B1DC00000 + 0x00, 0x00, 0xC0, 0xF9, 0x0A, 0x48, 0xE1, 0x0B, // 0xBE1480AF9C00000 + 0x00, 0x00, 0x80, 0xFE, 0x0A, 0x4C, 0xE1, 0x0B, // 0xBE14C0AFE800000 + 0x00, 0x00, 0xA0, 0x01, 0x0B, 0x50, 0xE1, 0x0B, // 0xBE1500B01A00000 + 0x00, 0x00, 0x60, 0xF1, 0x0A, 0x54, 0xE1, 0x0B, // 0xBE1540AF1600000 + 0x00, 0x00, 0x40, 0x06, 0x0B, 0x58, 0xE1, 0x0B, // 0xBE1580B06400000 + 0x00, 0x00, 0x20, 0x06, 0x0B, 0x5C, 0xE1, 0x0B, // 0xBE15C0B06200000 + 0x00, 0x00, 0x80, 0x15, 0x0B, 0x60, 0xE1, 0x0B, // 0xBE1600B15800000 + 0x00, 0x00, 0x80, 0x9C, 0x42, 0x64, 0xE1, 0x0B, // 0xBE164429C800000 + 0x00, 0x00, 0x40, 0x1E, 0x0B, 0x68, 0xE1, 0x0B, // 0xBE1680B1E400000 + 0x00, 0x00, 0xE0, 0x1E, 0x0B, 0x6C, 0xE1, 0x0B, // 0xBE16C0B1EE00000 + 0x00, 0x00, 0xC0, 0x20, 0x0B, 0x70, 0xE1, 0x0B, // 0xBE1700B20C00000 + 0x00, 0x00, 0x40, 0x23, 0x0B, 0x74, 0xE1, 0x0B, // 0xBE1740B23400000 + 0x00, 0x00, 0x40, 0x24, 0x0B, 0x78, 0xE1, 0x0B, // 0xBE1780B24400000 + 0x00, 0x00, 0x40, 0x2C, 0x0B, 0x7C, 0xE1, 0x0B, // 0xBE17C0B2C400000 + 0x00, 0x00, 0x00, 0xD5, 0x42, 0x80, 0xE1, 0x0B, // 0xBE18042D5000000 + 0x00, 0x00, 0x40, 0xDD, 0x42, 0x84, 0xE1, 0x0B, // 0xBE18442DD400000 + 0x00, 0x00, 0x80, 0x3D, 0x0B, 0x88, 0xE1, 0x0B, // 0xBE1880B3D800000 + 0x00, 0x00, 0x60, 0x43, 0x0B, 0x8C, 0xE1, 0x0B, // 0xBE18C0B43600000 + 0x00, 0x00, 0xE0, 0x44, 0x0B, 0x90, 0xE1, 0x0B, // 0xBE1900B44E00000 + 0x00, 0x00, 0x00, 0x3B, 0x0B, 0x94, 0xE1, 0x0B, // 0xBE1940B3B000000 + 0x00, 0x00, 0xC0, 0x4C, 0x0B, 0x98, 0xE1, 0x0B, // 0xBE1980B4CC00000 + 0x00, 0x00, 0xC0, 0xDD, 0x06, 0x9C, 0xE1, 0x0B, // 0xBE19C06DDC00000 + 0x00, 0x00, 0x80, 0xDF, 0x06, 0xA0, 0xE1, 0x0B, // 0xBE1A006DF800000 + 0x00, 0x00, 0x00, 0x61, 0x0B, 0xA4, 0xE1, 0x0B, // 0xBE1A40B61000000 + 0x00, 0x00, 0xC0, 0x67, 0x0B, 0xA8, 0xE1, 0x0B, // 0xBE1A80B67C00000 + 0x00, 0x00, 0xC0, 0x67, 0x0B, 0xAC, 0xE1, 0x0B, // 0xBE1AC0B67C00000 + 0x00, 0x00, 0x00, 0x39, 0x43, 0xB0, 0xE1, 0x0B, // 0xBE1B04339000000 + 0x00, 0x00, 0x60, 0x78, 0x0B, 0xB4, 0xE1, 0x0B, // 0xBE1B40B78600000 + 0x00, 0x00, 0x00, 0x7B, 0x0B, 0xB8, 0xE1, 0x0B, // 0xBE1B80B7B000000 + 0x00, 0x00, 0xE0, 0x7C, 0x0B, 0xBC, 0xE1, 0x0B, // 0xBE1BC0B7CE00000 + 0x00, 0x00, 0x60, 0x7E, 0x0B, 0xC0, 0xE1, 0x0B, // 0xBE1C00B7E600000 + 0x00, 0x00, 0x00, 0x63, 0x43, 0xC4, 0xE1, 0x0B, // 0xBE1C44363000000 + 0x00, 0x00, 0xE0, 0x7F, 0x0B, 0xC8, 0xE1, 0x0B, // 0xBE1C80B7FE00000 + 0x00, 0x00, 0xC0, 0x80, 0x0B, 0xCC, 0xE1, 0x0B, // 0xBE1CC0B80C00000 + 0x00, 0x00, 0x60, 0xEA, 0x0B, 0xD0, 0xE1, 0x0B, // 0xBE1D00BEA600000 + 0x00, 0x00, 0x40, 0x84, 0x0B, 0xD4, 0xE1, 0x0B, // 0xBE1D40B84400000 + 0x00, 0x00, 0x20, 0xF0, 0x06, 0xD8, 0xE1, 0x0B, // 0xBE1D806F0200000 + 0x00, 0x00, 0x00, 0x8C, 0x0B, 0xDC, 0xE1, 0x0B, // 0xBE1DC0B8C000000 + 0x00, 0x00, 0xC0, 0x8D, 0x0B, 0xE0, 0xE1, 0x0B, // 0xBE1E00B8DC00000 + 0x00, 0x00, 0x00, 0x98, 0x0B, 0xE4, 0xE1, 0x0B, // 0xBE1E40B98000000 + 0x00, 0x00, 0xA0, 0x91, 0x0B, 0xE8, 0xE1, 0x0B, // 0xBE1E80B91A00000 + 0x00, 0x00, 0x80, 0xBC, 0x43, 0xEC, 0xE1, 0x0B, // 0xBE1EC43BC800000 + 0x00, 0x00, 0x60, 0xA8, 0x0B, 0xF0, 0xE1, 0x0B, // 0xBE1F00BA8600000 + 0x00, 0x00, 0xC0, 0xBC, 0x43, 0xF4, 0xE1, 0x0B, // 0xBE1F443BCC00000 + 0x00, 0x00, 0xC0, 0xAD, 0x0B, 0xF8, 0xE1, 0x0B, // 0xBE1F80BADC00000 + 0x00, 0x00, 0x60, 0xAD, 0x0B, 0xFC, 0xE1, 0x0B, // 0xBE1FC0BAD600000 + 0x00, 0x00, 0x80, 0xAF, 0x0B, 0x00, 0xE2, 0x0B, // 0xBE2000BAF800000 + 0x00, 0x00, 0x20, 0xBC, 0x0B, 0x04, 0xE2, 0x0B, // 0xBE2040BBC200000 + 0x00, 0x00, 0x40, 0xBC, 0x0B, 0x08, 0xE2, 0x0B, // 0xBE2080BBC400000 + 0x00, 0x00, 0xE0, 0x05, 0x07, 0x0C, 0xE2, 0x0B, // 0xBE20C0705E00000 + 0x00, 0x00, 0xA0, 0xBF, 0x0B, 0x10, 0xE2, 0x0B, // 0xBE2100BBFA00000 + 0x00, 0x00, 0x00, 0xC5, 0x0B, 0x14, 0xE2, 0x0B, // 0xBE2140BC5000000 + 0x00, 0x00, 0xA0, 0xC7, 0x0B, 0x18, 0xE2, 0x0B, // 0xBE2180BC7A00000 + 0x00, 0x00, 0x20, 0xCD, 0x0B, 0x1C, 0xE2, 0x0B, // 0xBE21C0BCD200000 + 0x00, 0x00, 0x40, 0x0C, 0x07, 0x20, 0xE2, 0x0B, // 0xBE220070C400000 + 0x00, 0x00, 0x60, 0x30, 0x44, 0x24, 0xE2, 0x0B, // 0xBE2244430600000 + 0x00, 0x00, 0x80, 0x0F, 0x07, 0x28, 0xE2, 0x0B, // 0xBE228070F800000 + 0x00, 0x00, 0x00, 0xD6, 0x0B, 0x2C, 0xE2, 0x0B, // 0xBE22C0BD6000000 + 0x00, 0x00, 0x60, 0xD6, 0x0B, 0x30, 0xE2, 0x0B, // 0xBE2300BD6600000 + 0x00, 0x00, 0xC0, 0xD6, 0x0B, 0x34, 0xE2, 0x0B, // 0xBE2340BD6C00000 + 0x00, 0x00, 0x40, 0xD9, 0x0B, 0x38, 0xE2, 0x0B, // 0xBE2380BD9400000 + 0x00, 0x00, 0x40, 0x72, 0x54, 0x3C, 0xE2, 0x0B, // 0xBE23C5472400000 + 0x00, 0x00, 0xC0, 0xDF, 0x0B, 0x40, 0xE2, 0x0B, // 0xBE2400BDFC00000 + 0x00, 0x00, 0x20, 0x66, 0x44, 0x44, 0xE2, 0x0B, // 0xBE2444466200000 + 0x00, 0x00, 0x20, 0x66, 0x44, 0x48, 0xE2, 0x0B, // 0xBE2484466200000 + 0x00, 0x00, 0x20, 0x40, 0x10, 0x4C, 0xE2, 0x0B, // 0xBE24C1040200000 + 0x00, 0x00, 0x40, 0xE4, 0x0B, 0x50, 0xE2, 0x0B, // 0xBE2500BE4400000 + 0x00, 0x00, 0x40, 0xE4, 0x0B, 0x54, 0xE2, 0x0B, // 0xBE2540BE4400000 + 0x00, 0x00, 0xE0, 0x18, 0x07, 0x58, 0xE2, 0x0B, // 0xBE2580718E00000 + 0x00, 0x00, 0x00, 0x57, 0x46, 0x5C, 0xE2, 0x0B, // 0xBE25C4657000000 + 0x00, 0x00, 0x40, 0x3B, 0x4C, 0x60, 0xE2, 0x0B, // 0xBE2604C3B400000 + 0x00, 0x00, 0x40, 0xEC, 0x0B, 0x64, 0xE2, 0x0B, // 0xBE2640BEC400000 + 0x00, 0x00, 0x60, 0xED, 0x0B, 0x68, 0xE2, 0x0B, // 0xBE2680BED600000 + 0x00, 0x00, 0x60, 0x1C, 0x07, 0x6C, 0xE2, 0x0B, // 0xBE26C071C600000 + 0x00, 0x00, 0x40, 0xF3, 0x0B, 0x70, 0xE2, 0x0B, // 0xBE2700BF3400000 + 0x00, 0x00, 0xA0, 0xF9, 0x0B, 0x74, 0xE2, 0x0B, // 0xBE2740BF9A00000 + 0x00, 0x00, 0xE0, 0xFA, 0x0B, 0x78, 0xE2, 0x0B, // 0xBE2780BFAE00000 + 0x00, 0x00, 0x20, 0xFF, 0x0B, 0x7C, 0xE2, 0x0B, // 0xBE27C0BFF200000 + 0x00, 0x00, 0x20, 0x10, 0x0C, 0x80, 0xE2, 0x0B, // 0xBE2800C10200000 + 0x00, 0x00, 0x40, 0x27, 0x07, 0x84, 0xE2, 0x0B, // 0xBE2840727400000 + 0x00, 0x00, 0x80, 0x23, 0x07, 0x88, 0xE2, 0x0B, // 0xBE2880723800000 + 0x00, 0x00, 0x80, 0x12, 0x0C, 0x8C, 0xE2, 0x0B, // 0xBE28C0C12800000 + 0x00, 0x00, 0x80, 0xDA, 0x44, 0x90, 0xE2, 0x0B, // 0xBE29044DA800000 + 0x00, 0x00, 0xE0, 0x18, 0x0C, 0x94, 0xE2, 0x0B, // 0xBE2940C18E00000 + 0x00, 0x00, 0x00, 0x29, 0x0C, 0x98, 0xE2, 0x0B, // 0xBE2980C29000000 + 0x00, 0x00, 0x80, 0x29, 0x0C, 0x9C, 0xE2, 0x0B, // 0xBE29C0C29800000 + 0x00, 0x00, 0xC0, 0x29, 0x0C, 0xA0, 0xE2, 0x0B, // 0xBE2A00C29C00000 + 0x00, 0x00, 0x80, 0x29, 0x0C, 0xA4, 0xE2, 0x0B, // 0xBE2A40C29800000 + 0x00, 0x00, 0x40, 0x2F, 0x0C, 0xA8, 0xE2, 0x0B, // 0xBE2A80C2F400000 + 0x00, 0x00, 0xC0, 0x31, 0x0C, 0xAC, 0xE2, 0x0B, // 0xBE2AC0C31C00000 + 0x00, 0x00, 0x40, 0x36, 0x0C, 0xB0, 0xE2, 0x0B, // 0xBE2B00C36400000 + 0x00, 0x00, 0x80, 0x34, 0x0C, 0xB4, 0xE2, 0x0B, // 0xBE2B40C34800000 + 0x00, 0x00, 0xE0, 0x35, 0x0C, 0xB8, 0xE2, 0x0B, // 0xBE2B80C35E00000 + 0x00, 0x00, 0xC0, 0x3B, 0x0C, 0xBC, 0xE2, 0x0B, // 0xBE2BC0C3BC00000 + 0x00, 0x00, 0x40, 0x3E, 0x0C, 0xC0, 0xE2, 0x0B, // 0xBE2C00C3E400000 + 0x00, 0x00, 0xC0, 0x3E, 0x0C, 0xC4, 0xE2, 0x0B, // 0xBE2C40C3EC00000 + 0x00, 0x00, 0x00, 0x42, 0x0C, 0xC8, 0xE2, 0x0B, // 0xBE2C80C42000000 + 0x00, 0x00, 0x60, 0x43, 0x0C, 0xCC, 0xE2, 0x0B, // 0xBE2CC0C43600000 + 0x00, 0x00, 0xA0, 0x4B, 0x0C, 0xD0, 0xE2, 0x0B, // 0xBE2D00C4BA00000 + 0x00, 0x00, 0x20, 0x56, 0x0C, 0xD4, 0xE2, 0x0B, // 0xBE2D40C56200000 + 0x00, 0x00, 0x80, 0x5A, 0x0C, 0xD8, 0xE2, 0x0B, // 0xBE2D80C5A800000 + 0x00, 0x00, 0x00, 0x6A, 0x0C, 0xDC, 0xE2, 0x0B, // 0xBE2DC0C6A000000 + 0x00, 0x00, 0x80, 0x61, 0x45, 0xE0, 0xE2, 0x0B, // 0xBE2E04561800000 + 0x00, 0x00, 0xA0, 0x67, 0x0C, 0xE4, 0xE2, 0x0B, // 0xBE2E40C67A00000 + 0x00, 0x00, 0x80, 0x5F, 0x0C, 0xE8, 0xE2, 0x0B, // 0xBE2E80C5F800000 + 0x00, 0x00, 0x00, 0x6D, 0x0C, 0xEC, 0xE2, 0x0B, // 0xBE2EC0C6D000000 + 0x00, 0x00, 0x60, 0x70, 0x0C, 0xF0, 0xE2, 0x0B, // 0xBE2F00C70600000 + 0x00, 0x00, 0x80, 0x7C, 0x0C, 0xF4, 0xE2, 0x0B, // 0xBE2F40C7C800000 + 0x00, 0x00, 0x20, 0x7E, 0x45, 0xF8, 0xE2, 0x0B, // 0xBE2F8457E200000 + 0x00, 0x00, 0x40, 0x84, 0x0C, 0xFC, 0xE2, 0x0B, // 0xBE2FC0C84400000 + 0x00, 0x00, 0xA0, 0x78, 0x0C, 0x00, 0xE3, 0x0B, // 0xBE3000C78A00000 + 0x00, 0x00, 0x20, 0x75, 0x0C, 0x04, 0xE3, 0x0B, // 0xBE3040C75200000 + 0x00, 0x00, 0xC0, 0x45, 0x07, 0x08, 0xE3, 0x0B, // 0xBE3080745C00000 + 0x00, 0x00, 0x20, 0x8D, 0x0C, 0x0C, 0xE3, 0x0B, // 0xBE30C0C8D200000 + 0x00, 0x00, 0xC0, 0x8F, 0x0C, 0x10, 0xE3, 0x0B, // 0xBE3100C8FC00000 + 0x00, 0x00, 0xA0, 0x93, 0x0C, 0x14, 0xE3, 0x0B, // 0xBE3140C93A00000 + 0x00, 0x00, 0xE0, 0x8E, 0x0C, 0x18, 0xE3, 0x0B, // 0xBE3180C8EE00000 + 0x00, 0x00, 0x80, 0x4D, 0x07, 0x1C, 0xE3, 0x0B, // 0xBE31C074D800000 + 0x00, 0x00, 0xE0, 0xA9, 0x0C, 0x20, 0xE3, 0x0B, // 0xBE3200CA9E00000 + 0x00, 0x00, 0x80, 0xAD, 0x0C, 0x24, 0xE3, 0x0B, // 0xBE3240CAD800000 + 0x00, 0x00, 0x40, 0x01, 0x46, 0x28, 0xE3, 0x0B, // 0xBE3284601400000 + 0x00, 0x00, 0x60, 0xBC, 0x0C, 0x2C, 0xE3, 0x0B, // 0xBE32C0CBC600000 + 0x00, 0x00, 0x00, 0xDF, 0x0C, 0x30, 0xE3, 0x0B, // 0xBE3300CDF000000 + 0x00, 0x00, 0x20, 0xC9, 0x0C, 0x34, 0xE3, 0x0B, // 0xBE3340CC9200000 + 0x00, 0x00, 0x20, 0x63, 0x07, 0x38, 0xE3, 0x0B, // 0xBE3380763200000 + 0x00, 0x00, 0x20, 0xD2, 0x0C, 0x3C, 0xE3, 0x0B, // 0xBE33C0CD2200000 + 0x00, 0x00, 0x00, 0x61, 0x07, 0x40, 0xE3, 0x0B, // 0xBE3400761000000 + 0x00, 0x00, 0x80, 0x5C, 0x07, 0x44, 0xE3, 0x0B, // 0xBE344075C800000 + 0x00, 0x00, 0x40, 0x32, 0x0A, 0x48, 0xE3, 0x0B, // 0xBE3480A32400000 + 0x00, 0x00, 0xA0, 0x32, 0x0A, 0x4C, 0xE3, 0x0B, // 0xBE34C0A32A00000 + 0x00, 0x00, 0x00, 0xE0, 0x0C, 0x50, 0xE3, 0x0B, // 0xBE3500CE0000000 + 0x00, 0x00, 0x80, 0xD3, 0x0C, 0x54, 0xE3, 0x0B, // 0xBE3540CD3800000 + 0x00, 0x00, 0xA0, 0x15, 0x10, 0x58, 0xE3, 0x0B, // 0xBE3581015A00000 + 0x00, 0x00, 0x20, 0x7B, 0x08, 0x5C, 0xE3, 0x0B, // 0xBE35C087B200000 + 0x00, 0x00, 0xE0, 0xE2, 0x0C, 0x60, 0xE3, 0x0B, // 0xBE3600CE2E00000 + 0x00, 0x00, 0x60, 0xE3, 0x0C, 0x64, 0xE3, 0x0B, // 0xBE3640CE3600000 + 0x00, 0x00, 0x20, 0xE4, 0x0C, 0x68, 0xE3, 0x0B, // 0xBE3680CE4200000 + 0x00, 0x00, 0xC0, 0xEB, 0x0C, 0x6C, 0xE3, 0x0B, // 0xBE36C0CEBC00000 + 0x00, 0x00, 0x60, 0xEA, 0x0C, 0x70, 0xE3, 0x0B, // 0xBE3700CEA600000 + 0x00, 0x00, 0x60, 0x78, 0x46, 0x74, 0xE3, 0x0B, // 0xBE3744678600000 + 0x00, 0x00, 0x20, 0x69, 0x07, 0x78, 0xE3, 0x0B, // 0xBE3780769200000 + 0x00, 0x00, 0x40, 0xFF, 0x0C, 0x7C, 0xE3, 0x0B, // 0xBE37C0CFF400000 + 0x00, 0x00, 0xA0, 0xF0, 0x0C, 0x80, 0xE3, 0x0B, // 0xBE3800CF0A00000 + 0x00, 0x00, 0x40, 0x0A, 0x0D, 0x84, 0xE3, 0x0B, // 0xBE3840D0A400000 + 0x00, 0x00, 0xA0, 0x10, 0x0D, 0x88, 0xE3, 0x0B, // 0xBE3880D10A00000 + 0x00, 0x00, 0xA0, 0x8D, 0x46, 0x8C, 0xE3, 0x0B, // 0xBE38C468DA00000 + 0x00, 0x00, 0xC0, 0x11, 0x0D, 0x90, 0xE3, 0x0B, // 0xBE3900D11C00000 + 0x00, 0x00, 0xE0, 0x03, 0x0D, 0x94, 0xE3, 0x0B, // 0xBE3940D03E00000 + 0x00, 0x00, 0x80, 0x22, 0x0D, 0x98, 0xE3, 0x0B, // 0xBE3980D22800000 + 0x00, 0x00, 0xA0, 0x73, 0x07, 0x9C, 0xE3, 0x0B, // 0xBE39C0773A00000 + 0x00, 0x00, 0x40, 0x28, 0x0D, 0xA0, 0xE3, 0x0B, // 0xBE3A00D28400000 + 0x00, 0x00, 0x60, 0x34, 0x0D, 0xA4, 0xE3, 0x0B, // 0xBE3A40D34600000 + 0x00, 0x00, 0x40, 0x3D, 0x0D, 0xA8, 0xE3, 0x0B, // 0xBE3A80D3D400000 + 0x00, 0x00, 0x00, 0x55, 0x0D, 0xAC, 0xE3, 0x0B, // 0xBE3AC0D55000000 + 0x00, 0x00, 0x60, 0xD4, 0x46, 0xB0, 0xE3, 0x0B, // 0xBE3B046D4600000 + 0x00, 0x00, 0x60, 0x5B, 0x0D, 0xB4, 0xE3, 0x0B, // 0xBE3B40D5B600000 + 0x00, 0x00, 0x00, 0x83, 0x07, 0xB8, 0xE3, 0x0B, // 0xBE3B80783000000 + 0x00, 0x00, 0x20, 0x64, 0x0D, 0xBC, 0xE3, 0x0B, // 0xBE3BC0D64200000 + 0x00, 0x00, 0xE0, 0x14, 0x47, 0xC0, 0xE3, 0x0B, // 0xBE3C04714E00000 + 0x00, 0x00, 0x80, 0x6A, 0x0D, 0xC4, 0xE3, 0x0B, // 0xBE3C40D6A800000 + 0x00, 0x00, 0xC0, 0x89, 0x07, 0xC8, 0xE3, 0x0B, // 0xBE3C80789C00000 + 0x00, 0x00, 0x40, 0x6E, 0x0D, 0xCC, 0xE3, 0x0B, // 0xBE3CC0D6E400000 + 0x00, 0x00, 0xE0, 0x73, 0x0D, 0xD0, 0xE3, 0x0B, // 0xBE3D00D73E00000 + 0x00, 0x00, 0x40, 0x77, 0x0D, 0xD4, 0xE3, 0x0B, // 0xBE3D40D77400000 + 0x00, 0x00, 0x60, 0x77, 0x0D, 0xD8, 0xE3, 0x0B, // 0xBE3D80D77600000 + 0x00, 0x00, 0xA0, 0x51, 0x47, 0xDC, 0xE3, 0x0B, // 0xBE3DC4751A00000 + 0x00, 0x00, 0x60, 0xA1, 0x43, 0xE0, 0xE3, 0x0B, // 0xBE3E043A1600000 + 0x00, 0x00, 0x40, 0x5F, 0x47, 0xE4, 0xE3, 0x0B, // 0xBE3E4475F400000 + 0x00, 0x00, 0xC0, 0x89, 0x0D, 0xE8, 0xE3, 0x0B, // 0xBE3E80D89C00000 + 0x00, 0x00, 0x80, 0x97, 0x47, 0xEC, 0xE3, 0x0B, // 0xBE3EC4797800000 + 0x00, 0x00, 0xE0, 0x97, 0x0D, 0xF0, 0xE3, 0x0B, // 0xBE3F00D97E00000 + 0x00, 0x00, 0xA0, 0x99, 0x0D, 0xF4, 0xE3, 0x0B, // 0xBE3F40D99A00000 + 0x00, 0x00, 0xE0, 0x8C, 0x0D, 0xF8, 0xE3, 0x0B, // 0xBE3F80D8CE00000 + 0x00, 0x00, 0xC0, 0xA2, 0x0D, 0xFC, 0xE3, 0x0B, // 0xBE3FC0DA2C00000 + 0x00, 0x00, 0xC0, 0xA7, 0x0D, 0x00, 0xE4, 0x0B, // 0xBE4000DA7C00000 + 0x00, 0x00, 0xE0, 0xAE, 0x0D, 0x04, 0xE4, 0x0B, // 0xBE4040DAEE00000 + 0x00, 0x00, 0x20, 0xA8, 0x0D, 0x08, 0xE4, 0x0B, // 0xBE4080DA8200000 + 0x00, 0x00, 0x20, 0xAD, 0x0D, 0x0C, 0xE4, 0x0B, // 0xBE40C0DAD200000 + 0x00, 0x00, 0x00, 0xAF, 0x0D, 0x10, 0xE4, 0x0B, // 0xBE4100DAF000000 + 0x00, 0x00, 0xA0, 0xB0, 0x0D, 0x14, 0xE4, 0x0B, // 0xBE4140DB0A00000 + 0x00, 0x00, 0xC0, 0xA3, 0x47, 0x18, 0xE4, 0x0B, // 0xBE41847A3C00000 + 0x00, 0x00, 0x80, 0xA6, 0x0D, 0x1C, 0xE4, 0x0B, // 0xBE41C0DA6800000 + 0x00, 0x00, 0xE0, 0xC5, 0x0D, 0x20, 0xE4, 0x0B, // 0xBE4200DC5E00000 + 0x00, 0x00, 0xC0, 0xCD, 0x0D, 0x24, 0xE4, 0x0B, // 0xBE4240DCDC00000 + 0x00, 0x00, 0x60, 0xA6, 0x07, 0x28, 0xE4, 0x0B, // 0xBE42807A6600000 + 0x00, 0x00, 0x60, 0xD9, 0x0D, 0x2C, 0xE4, 0x0B, // 0xBE42C0DD9600000 + 0x00, 0x00, 0xE0, 0xD8, 0x0D, 0x30, 0xE4, 0x0B, // 0xBE4300DD8E00000 + 0x00, 0x00, 0x20, 0xDA, 0x47, 0x34, 0xE4, 0x0B, // 0xBE43447DA200000 + 0x00, 0x00, 0x20, 0xBF, 0x0D, 0x38, 0xE4, 0x0B, // 0xBE4380DBF200000 + 0x00, 0x00, 0xC0, 0xED, 0x0D, 0x3C, 0xE4, 0x0B, // 0xBE43C0DEDC00000 + 0x00, 0x00, 0xC0, 0xEB, 0x47, 0x40, 0xE4, 0x0B, // 0xBE44047EBC00000 + 0x00, 0x00, 0xC0, 0xF1, 0x47, 0x44, 0xE4, 0x0B, // 0xBE44447F1C00000 + 0x00, 0x00, 0xC0, 0xF8, 0x0D, 0x48, 0xE4, 0x0B, // 0xBE4480DF8C00000 + 0x00, 0x00, 0x20, 0x07, 0x0E, 0x4C, 0xE4, 0x0B, // 0xBE44C0E07200000 + 0x00, 0x00, 0xC0, 0x03, 0x0E, 0x50, 0xE4, 0x0B, // 0xBE4500E03C00000 + 0x00, 0x00, 0x60, 0x03, 0x0E, 0x54, 0xE4, 0x0B, // 0xBE4540E03600000 + 0x00, 0x00, 0xC0, 0xB2, 0x07, 0x58, 0xE4, 0x0B, // 0xBE45807B2C00000 + 0x00, 0x00, 0x40, 0x09, 0x0E, 0x5C, 0xE4, 0x0B, // 0xBE45C0E09400000 + 0x00, 0x00, 0xA0, 0x0F, 0x0E, 0x60, 0xE4, 0x0B, // 0xBE4600E0FA00000 + 0x00, 0x00, 0xE0, 0x0E, 0x0E, 0x64, 0xE4, 0x0B, // 0xBE4640E0EE00000 + 0x00, 0x00, 0xA0, 0x15, 0x0E, 0x68, 0xE4, 0x0B, // 0xBE4680E15A00000 + 0x00, 0x00, 0xA0, 0xA4, 0x40, 0x6C, 0xE4, 0x0B, // 0xBE46C40A4A00000 + 0x00, 0x00, 0xA0, 0x28, 0x0E, 0x70, 0xE4, 0x0B, // 0xBE4700E28A00000 + 0x00, 0x00, 0x60, 0x4C, 0x48, 0x74, 0xE4, 0x0B, // 0xBE474484C600000 + 0x00, 0x00, 0x80, 0x33, 0x0E, 0x78, 0xE4, 0x0B, // 0xBE4780E33800000 + 0x00, 0x00, 0x60, 0x75, 0x48, 0x7C, 0xE4, 0x0B, // 0xBE47C4875600000 + 0x00, 0x00, 0x00, 0x45, 0x0E, 0x80, 0xE4, 0x0B, // 0xBE4800E45000000 + 0x00, 0x00, 0xA0, 0x46, 0x0E, 0x84, 0xE4, 0x0B, // 0xBE4840E46A00000 + 0x00, 0x00, 0x00, 0x4A, 0x0E, 0x88, 0xE4, 0x0B, // 0xBE4880E4A000000 + 0x00, 0x00, 0x00, 0xC1, 0x48, 0x8C, 0xE4, 0x0B, // 0xBE48C48C1000000 + 0x00, 0x00, 0x00, 0x50, 0x0E, 0x90, 0xE4, 0x0B, // 0xBE4900E50000000 + 0x00, 0x00, 0xA0, 0x52, 0x0E, 0x94, 0xE4, 0x0B, // 0xBE4940E52A00000 + 0x00, 0x00, 0xA0, 0xE6, 0x48, 0x98, 0xE4, 0x0B, // 0xBE49848E6A00000 + 0x00, 0x00, 0x80, 0x02, 0x49, 0x9C, 0xE4, 0x0B, // 0xBE49C4902800000 + 0x00, 0x00, 0x40, 0x6F, 0x0E, 0xA0, 0xE4, 0x0B, // 0xBE4A00E6F400000 + 0x00, 0x00, 0x60, 0x71, 0x0E, 0xA4, 0xE4, 0x0B, // 0xBE4A40E71600000 + 0x00, 0x00, 0x80, 0xD5, 0x07, 0xA8, 0xE4, 0x0B, // 0xBE4A807D5800000 + 0x00, 0x00, 0xA0, 0x74, 0x0E, 0xAC, 0xE4, 0x0B, // 0xBE4AC0E74A00000 + 0x00, 0x00, 0x00, 0xD7, 0x07, 0xB0, 0xE4, 0x0B, // 0xBE4B007D7000000 + 0x00, 0x00, 0x00, 0xD7, 0x07, 0xB4, 0xE4, 0x0B, // 0xBE4B407D7000000 + 0x00, 0x00, 0xE0, 0x88, 0x0E, 0xB8, 0xE4, 0x0B, // 0xBE4B80E88E00000 + 0x00, 0x00, 0x80, 0x8B, 0x0E, 0xBC, 0xE4, 0x0B, // 0xBE4BC0E8B800000 + 0x00, 0x00, 0x20, 0x8E, 0x0E, 0xC0, 0xE4, 0x0B, // 0xBE4C00E8E200000 + 0x00, 0x00, 0xA0, 0x90, 0x0E, 0xC4, 0xE4, 0x0B, // 0xBE4C40E90A00000 + 0x00, 0x00, 0x40, 0x99, 0x0E, 0xC8, 0xE4, 0x0B, // 0xBE4C80E99400000 + 0x00, 0x00, 0x60, 0xE3, 0x07, 0xCC, 0xE4, 0x0B, // 0xBE4CC07E3600000 + 0x00, 0x00, 0x80, 0xA4, 0x0E, 0xD0, 0xE4, 0x0B, // 0xBE4D00EA4800000 + 0x00, 0x00, 0xC0, 0x86, 0x49, 0xD4, 0xE4, 0x0B, // 0xBE4D44986C00000 + 0x00, 0x00, 0xC0, 0xA7, 0x0E, 0xD8, 0xE4, 0x0B, // 0xBE4D80EA7C00000 + 0x00, 0x00, 0x40, 0x92, 0x49, 0xDC, 0xE4, 0x0B, // 0xBE4DC4992400000 + 0x00, 0x00, 0x00, 0xAE, 0x0E, 0xE0, 0xE4, 0x0B, // 0xBE4E00EAE000000 + 0x00, 0x00, 0xE0, 0x33, 0x44, 0xE4, 0xE4, 0x0B, // 0xBE4E44433E00000 + 0x00, 0x00, 0x00, 0xC2, 0x0E, 0xE8, 0xE4, 0x0B, // 0xBE4E80EC2000000 + 0x00, 0x00, 0x20, 0xF4, 0x49, 0xEC, 0xE4, 0x0B, // 0xBE4EC49F4200000 + 0x00, 0x00, 0x00, 0xF7, 0x49, 0xF0, 0xE4, 0x0B, // 0xBE4F049F7000000 + 0x00, 0x00, 0x80, 0x08, 0x4A, 0xF4, 0xE4, 0x0B, // 0xBE4F44A08800000 + 0x00, 0x00, 0x80, 0xFF, 0x07, 0xF8, 0xE4, 0x0B, // 0xBE4F807FF800000 + 0x00, 0x00, 0x00, 0x01, 0x08, 0xFC, 0xE4, 0x0B, // 0xBE4FC0801000000 + 0x00, 0x00, 0x80, 0xDE, 0x0E, 0x00, 0xE5, 0x0B, // 0xBE5000EDE800000 + 0x00, 0x00, 0x60, 0x1E, 0x4A, 0x04, 0xE5, 0x0B, // 0xBE5044A1E600000 + 0x00, 0x00, 0x40, 0x1E, 0x4A, 0x08, 0xE5, 0x0B, // 0xBE5084A1E400000 + 0x00, 0x00, 0x20, 0x23, 0x4A, 0x0C, 0xE5, 0x0B, // 0xBE50C4A23200000 + 0x00, 0x00, 0x60, 0x26, 0x4A, 0x10, 0xE5, 0x0B, // 0xBE5104A26600000 + 0x00, 0x00, 0xC0, 0xE3, 0x0E, 0x14, 0xE5, 0x0B, // 0xBE5140EE3C00000 + 0x00, 0x00, 0xE0, 0xE3, 0x0E, 0x18, 0xE5, 0x0B, // 0xBE5180EE3E00000 + 0x00, 0x00, 0xE0, 0xE3, 0x0E, 0x1C, 0xE5, 0x0B, // 0xBE51C0EE3E00000 + 0x00, 0x00, 0x40, 0xE9, 0x0E, 0x20, 0xE5, 0x0B, // 0xBE5200EE9400000 + 0x00, 0x00, 0x20, 0x07, 0x08, 0x24, 0xE5, 0x0B, // 0xBE5240807200000 + 0x00, 0x00, 0x60, 0xF1, 0x0E, 0x28, 0xE5, 0x0B, // 0xBE5280EF1600000 + 0x00, 0x00, 0xC0, 0x08, 0x08, 0x2C, 0xE5, 0x0B, // 0xBE52C0808C00000 + 0x00, 0x00, 0xC0, 0x12, 0x08, 0x30, 0xE5, 0x0B, // 0xBE5300812C00000 + 0x00, 0x00, 0xA0, 0x83, 0x4A, 0x34, 0xE5, 0x0B, // 0xBE5344A83A00000 + 0x00, 0x00, 0xC0, 0x09, 0x0F, 0x38, 0xE5, 0x0B, // 0xBE5380F09C00000 + 0x00, 0x00, 0x80, 0x11, 0x0F, 0x3C, 0xE5, 0x0B, // 0xBE53C0F11800000 + 0x00, 0x00, 0x80, 0x19, 0x0F, 0x40, 0xE5, 0x0B, // 0xBE5400F19800000 + 0x00, 0x00, 0x60, 0x1C, 0x08, 0x44, 0xE5, 0x0B, // 0xBE544081C600000 + 0x00, 0x00, 0xC0, 0xC4, 0x4A, 0x48, 0xE5, 0x0B, // 0xBE5484AC4C00000 + 0x00, 0x00, 0xC0, 0x2A, 0x0F, 0x4C, 0xE5, 0x0B, // 0xBE54C0F2AC00000 + 0x00, 0x00, 0x40, 0xD3, 0x4A, 0x50, 0xE5, 0x0B, // 0xBE5504AD3400000 + 0x00, 0x00, 0xA0, 0xD8, 0x4A, 0x54, 0xE5, 0x0B, // 0xBE5544AD8A00000 + 0x00, 0x00, 0xE0, 0x31, 0x0F, 0x58, 0xE5, 0x0B, // 0xBE5580F31E00000 + 0x00, 0x00, 0x60, 0x3D, 0x0F, 0x5C, 0xE5, 0x0B, // 0xBE55C0F3D600000 + 0x00, 0x00, 0xE0, 0x25, 0x08, 0x60, 0xE5, 0x0B, // 0xBE5600825E00000 + 0x00, 0x00, 0x00, 0x48, 0x0F, 0x64, 0xE5, 0x0B, // 0xBE5640F48000000 + 0x00, 0x00, 0x40, 0x49, 0x0F, 0x68, 0xE5, 0x0B, // 0xBE5680F49400000 + 0x00, 0x00, 0xE0, 0x49, 0x0F, 0x6C, 0xE5, 0x0B, // 0xBE56C0F49E00000 + 0x00, 0x00, 0x80, 0x2F, 0x4B, 0x70, 0xE5, 0x0B, // 0xBE5704B2F800000 + 0x00, 0x00, 0xE0, 0x54, 0x4B, 0x74, 0xE5, 0x0B, // 0xBE5744B54E00000 + 0x00, 0x00, 0xE0, 0x54, 0x4B, 0x78, 0xE5, 0x0B, // 0xBE5784B54E00000 + 0x00, 0x00, 0xC0, 0x5D, 0x0F, 0x7C, 0xE5, 0x0B, // 0xBE57C0F5DC00000 + 0x00, 0x00, 0x40, 0x40, 0x08, 0x80, 0xE5, 0x0B, // 0xBE5800840400000 + 0x00, 0x00, 0x60, 0x75, 0x4B, 0x84, 0xE5, 0x0B, // 0xBE5844B75600000 + 0x00, 0x00, 0xC0, 0x78, 0x0F, 0x88, 0xE5, 0x0B, // 0xBE5880F78C00000 + 0x00, 0x00, 0x20, 0x79, 0x0F, 0x8C, 0xE5, 0x0B, // 0xBE58C0F79200000 + 0x00, 0x00, 0xE0, 0x44, 0x08, 0x90, 0xE5, 0x0B, // 0xBE5900844E00000 + 0x00, 0x00, 0x00, 0x90, 0x4B, 0x94, 0xE5, 0x0B, // 0xBE5944B90000000 + 0x00, 0x00, 0x40, 0x9A, 0x0F, 0x98, 0xE5, 0x0B, // 0xBE5980F9A400000 + 0x00, 0x00, 0x00, 0x54, 0x08, 0x9C, 0xE5, 0x0B, // 0xBE59C0854000000 + 0x00, 0x00, 0x00, 0x9D, 0x0F, 0xA0, 0xE5, 0x0B, // 0xBE5A00F9D000000 + 0x00, 0x00, 0x60, 0x9C, 0x0F, 0xA4, 0xE5, 0x0B, // 0xBE5A40F9C600000 + 0x00, 0x00, 0x00, 0xA0, 0x0F, 0xA8, 0xE5, 0x0B, // 0xBE5A80FA0000000 + 0x00, 0x00, 0xC0, 0xF0, 0x4B, 0xAC, 0xE5, 0x0B, // 0xBE5AC4BF0C00000 + 0x00, 0x00, 0x60, 0xAC, 0x0F, 0xB0, 0xE5, 0x0B, // 0xBE5B00FAC600000 + 0x00, 0x00, 0x20, 0x60, 0x08, 0xB4, 0xE5, 0x0B, // 0xBE5B40860200000 + 0x00, 0x00, 0xE0, 0xB8, 0x0F, 0xB8, 0xE5, 0x0B, // 0xBE5B80FB8E00000 + 0x00, 0x00, 0x40, 0xC0, 0x0F, 0xBC, 0xE5, 0x0B, // 0xBE5BC0FC0400000 + 0x00, 0x00, 0xA0, 0xC8, 0x0F, 0xC0, 0xE5, 0x0B, // 0xBE5C00FC8A00000 + 0x00, 0x00, 0x80, 0x66, 0x08, 0xC4, 0xE5, 0x0B, // 0xBE5C40866800000 + 0x00, 0x00, 0x00, 0x45, 0x4C, 0xC8, 0xE5, 0x0B, // 0xBE5C84C45000000 + 0x00, 0x00, 0xE0, 0x48, 0x4C, 0xCC, 0xE5, 0x0B, // 0xBE5CC4C48E00000 + 0x00, 0x00, 0x20, 0x6B, 0x08, 0xD0, 0xE5, 0x0B, // 0xBE5D0086B200000 + 0x00, 0x00, 0x20, 0x5B, 0x4C, 0xD4, 0xE5, 0x0B, // 0xBE5D44C5B200000 + 0x00, 0x00, 0x40, 0xEF, 0x0F, 0xD8, 0xE5, 0x0B, // 0xBE5D80FEF400000 + 0x00, 0x00, 0xC0, 0x67, 0x4C, 0xDC, 0xE5, 0x0B, // 0xBE5DC4C67C00000 + 0x00, 0x00, 0xA0, 0xF2, 0x0F, 0xE0, 0xE5, 0x0B, // 0xBE5E00FF2A00000 + 0x00, 0x00, 0x40, 0xFF, 0x0F, 0xE4, 0xE5, 0x0B, // 0xBE5E40FFF400000 + 0x00, 0x00, 0xA0, 0x00, 0x10, 0xE8, 0xE5, 0x0B, // 0xBE5E81000A00000 + 0x00, 0x00, 0x40, 0x9B, 0x4C, 0xEC, 0xE5, 0x0B, // 0xBE5EC4C9B400000 + 0x00, 0x00, 0x60, 0xA4, 0x4C, 0xF0, 0xE5, 0x0B, // 0xBE5F04CA4600000 + 0x00, 0x00, 0x00, 0x0C, 0x10, 0xF4, 0xE5, 0x0B, // 0xBE5F4100C000000 + 0x00, 0x00, 0x00, 0xB5, 0x4C, 0xF8, 0xE5, 0x0B, // 0xBE5F84CB5000000 + 0x00, 0x00, 0x00, 0x0E, 0x10, 0xFC, 0xE5, 0x0B, // 0xBE5FC100E000000 + 0x00, 0x00, 0xE0, 0x6B, 0x46, 0x00, 0xE6, 0x0B, // 0xBE600466BE00000 + 0x00, 0x00, 0xA0, 0x7A, 0x08, 0x04, 0xE6, 0x0B, // 0xBE604087AA00000 + 0x00, 0x00, 0x40, 0x16, 0x10, 0x08, 0xE6, 0x0B, // 0xBE6081016400000 + 0x00, 0x00, 0x60, 0x20, 0x10, 0x0C, 0xE6, 0x0B, // 0xBE60C1020600000 + 0x00, 0x00, 0x60, 0x81, 0x08, 0x10, 0xE6, 0x0B, // 0xBE6100881600000 + 0x00, 0x00, 0xC0, 0x27, 0x10, 0x14, 0xE6, 0x0B, // 0xBE6141027C00000 + 0x00, 0x00, 0xA0, 0x56, 0x0B, 0x18, 0xE6, 0x0B, // 0xBE6180B56A00000 + 0x00, 0x00, 0xE0, 0xF4, 0x4C, 0x1C, 0xE6, 0x0B, // 0xBE61C4CF4E00000 + 0x00, 0x00, 0xA0, 0xF6, 0x4C, 0x20, 0xE6, 0x0B, // 0xBE6204CF6A00000 + 0x00, 0x00, 0x60, 0x72, 0x46, 0x24, 0xE6, 0x0B, // 0xBE6244672600000 + 0x00, 0x00, 0x80, 0x73, 0x46, 0x28, 0xE6, 0x0B, // 0xBE6284673800000 + 0x00, 0x00, 0x20, 0x40, 0x10, 0x2C, 0xE6, 0x0B, // 0xBE62C1040200000 + 0x00, 0x00, 0x80, 0x40, 0x10, 0x30, 0xE6, 0x0B, // 0xBE6301040800000 + 0x00, 0x00, 0xC0, 0xF3, 0x11, 0x34, 0xE6, 0x0B, // 0xBE63411F3C00000 + 0x00, 0x00, 0x60, 0x8D, 0x08, 0x38, 0xE6, 0x0B, // 0xBE638088D600000 + 0x00, 0x00, 0x20, 0x52, 0x10, 0x3C, 0xE6, 0x0B, // 0xBE63C1052200000 + 0x00, 0x00, 0x60, 0x51, 0x10, 0x40, 0xE6, 0x0B, // 0xBE6401051600000 + 0x00, 0x00, 0xA0, 0x53, 0x10, 0x44, 0xE6, 0x0B, // 0xBE6441053A00000 + 0x00, 0x00, 0x60, 0x56, 0x0A, 0x48, 0xE6, 0x0B, // 0xBE6480A56600000 + 0x00, 0x00, 0x20, 0x56, 0x10, 0x4C, 0xE6, 0x0B, // 0xBE64C1056200000 + 0x00, 0x00, 0x60, 0x56, 0x10, 0x50, 0xE6, 0x0B, // 0xBE6501056600000 + 0x00, 0x00, 0xA0, 0x57, 0x10, 0x54, 0xE6, 0x0B, // 0xBE6541057A00000 + 0x00, 0x00, 0xC0, 0x5C, 0x10, 0x58, 0xE6, 0x0B, // 0xBE658105CC00000 + 0x00, 0x00, 0x80, 0x67, 0x4D, 0x5C, 0xE6, 0x0B, // 0xBE65C4D67800000 + 0x00, 0x00, 0xA0, 0x5C, 0x10, 0x60, 0xE6, 0x0B, // 0xBE660105CA00000 + 0x00, 0x00, 0xA0, 0x63, 0x10, 0x64, 0xE6, 0x0B, // 0xBE6641063A00000 + 0x00, 0x00, 0x60, 0x6C, 0x10, 0x68, 0xE6, 0x0B, // 0xBE668106C600000 + 0x00, 0x00, 0xA0, 0x75, 0x10, 0x6C, 0xE6, 0x0B, // 0xBE66C1075A00000 + 0x00, 0x00, 0x60, 0x64, 0x10, 0x70, 0xE6, 0x0B, // 0xBE6701064600000 + 0x00, 0x00, 0xA0, 0x77, 0x10, 0x74, 0xE6, 0x0B, // 0xBE6741077A00000 + 0x00, 0x00, 0xE0, 0x7C, 0x10, 0x78, 0xE6, 0x0B, // 0xBE678107CE00000 + 0x00, 0x00, 0xE0, 0x8A, 0x10, 0x7C, 0xE6, 0x0B, // 0xBE67C108AE00000 + 0x00, 0x00, 0x60, 0x6A, 0x10, 0x80, 0xE6, 0x0B, // 0xBE680106A600000 + 0x00, 0x00, 0x40, 0x79, 0x10, 0x84, 0xE6, 0x0B, // 0xBE6841079400000 + 0x00, 0x00, 0x80, 0x79, 0x10, 0x88, 0xE6, 0x0B, // 0xBE6881079800000 + 0x00, 0x00, 0x80, 0x7B, 0x10, 0x8C, 0xE6, 0x0B, // 0xBE68C107B800000 + 0x00, 0x00, 0xC0, 0x86, 0x4D, 0x90, 0xE6, 0x0B, // 0xBE6904D86C00000 + 0x00, 0x00, 0x60, 0xAD, 0x4D, 0x94, 0xE6, 0x0B, // 0xBE6944DAD600000 + 0x00, 0x00, 0xA0, 0x9A, 0x4D, 0x98, 0xE6, 0x0B, // 0xBE6984D9AA00000 + 0x00, 0x00, 0x60, 0xA5, 0x08, 0x9C, 0xE6, 0x0B, // 0xBE69C08A5600000 + 0x00, 0x00, 0x20, 0x9E, 0x10, 0xA0, 0xE6, 0x0B, // 0xBE6A0109E200000 + 0x00, 0x00, 0x60, 0x9E, 0x10, 0xA4, 0xE6, 0x0B, // 0xBE6A4109E600000 + 0x00, 0x00, 0xC0, 0xA2, 0x10, 0xA8, 0xE6, 0x0B, // 0xBE6A810A2C00000 + 0x00, 0x00, 0x40, 0x79, 0x4E, 0xAC, 0xE6, 0x0B, // 0xBE6AC4E79400000 + 0x00, 0x00, 0x80, 0xAC, 0x10, 0xB0, 0xE6, 0x0B, // 0xBE6B010AC800000 + 0x00, 0x00, 0x80, 0xE5, 0x4D, 0xB4, 0xE6, 0x0B, // 0xBE6B44DE5800000 + 0x00, 0x00, 0xA0, 0xAB, 0x08, 0xB8, 0xE6, 0x0B, // 0xBE6B808ABA00000 + 0x00, 0x00, 0x20, 0xAC, 0x08, 0xBC, 0xE6, 0x0B, // 0xBE6BC08AC200000 + 0x00, 0x00, 0x20, 0xF6, 0x4D, 0xC0, 0xE6, 0x0B, // 0xBE6C04DF6200000 + 0x00, 0x00, 0x40, 0x1A, 0x4E, 0xC4, 0xE6, 0x0B, // 0xBE6C44E1A400000 + 0x00, 0x00, 0x60, 0xAD, 0x08, 0xC8, 0xE6, 0x0B, // 0xBE6C808AD600000 + 0x00, 0x00, 0x00, 0xCA, 0x10, 0xCC, 0xE6, 0x0B, // 0xBE6CC10CA000000 + 0x00, 0x00, 0x80, 0xCB, 0x10, 0xD0, 0xE6, 0x0B, // 0xBE6D010CB800000 + 0x00, 0x00, 0xE0, 0xCC, 0x10, 0xD4, 0xE6, 0x0B, // 0xBE6D410CCE00000 + 0x00, 0x00, 0x20, 0xCD, 0x10, 0xD8, 0xE6, 0x0B, // 0xBE6D810CD200000 + 0x00, 0x00, 0x20, 0xD5, 0x10, 0xDC, 0xE6, 0x0B, // 0xBE6DC10D5200000 + 0x00, 0x00, 0x00, 0xD1, 0x10, 0xE0, 0xE6, 0x0B, // 0xBE6E010D1000000 + 0x00, 0x00, 0xC0, 0xE1, 0x10, 0xE4, 0xE6, 0x0B, // 0xBE6E410E1C00000 + 0x00, 0x00, 0x40, 0xDC, 0x10, 0xE8, 0xE6, 0x0B, // 0xBE6E810DC400000 + 0x00, 0x00, 0x20, 0xEF, 0x10, 0xEC, 0xE6, 0x0B, // 0xBE6EC10EF200000 + 0x00, 0x00, 0x00, 0xE5, 0x10, 0xF0, 0xE6, 0x0B, // 0xBE6F010E5000000 + 0x00, 0x00, 0x60, 0xED, 0x10, 0xF4, 0xE6, 0x0B, // 0xBE6F410ED600000 + 0x00, 0x00, 0xC0, 0xF0, 0x10, 0xF8, 0xE6, 0x0B, // 0xBE6F810F0C00000 + 0x00, 0x00, 0xE0, 0xBA, 0x08, 0xFC, 0xE6, 0x0B, // 0xBE6FC08BAE00000 + 0x00, 0x00, 0x20, 0xFC, 0x10, 0x00, 0xE7, 0x0B, // 0xBE70010FC200000 + 0x00, 0x00, 0x20, 0x00, 0x11, 0x04, 0xE7, 0x0B, // 0xBE7041100200000 + 0x00, 0x00, 0x20, 0xBF, 0x08, 0x08, 0xE7, 0x0B, // 0xBE70808BF200000 + 0x00, 0x00, 0x00, 0x0C, 0x11, 0x0C, 0xE7, 0x0B, // 0xBE70C110C000000 + 0x00, 0x00, 0x60, 0x0C, 0x11, 0x10, 0xE7, 0x0B, // 0xBE710110C600000 + 0x00, 0x00, 0xE0, 0xCC, 0x4E, 0x14, 0xE7, 0x0B, // 0xBE7144ECCE00000 + 0x00, 0x00, 0xE0, 0x1A, 0x11, 0x18, 0xE7, 0x0B, // 0xBE718111AE00000 + 0x00, 0x00, 0xC0, 0x1B, 0x11, 0x1C, 0xE7, 0x0B, // 0xBE71C111BC00000 + 0x00, 0x00, 0xA0, 0xC6, 0x08, 0x20, 0xE7, 0x0B, // 0xBE72008C6A00000 + 0x00, 0x00, 0x40, 0x1F, 0x11, 0x24, 0xE7, 0x0B, // 0xBE724111F400000 + 0x00, 0x00, 0x60, 0x97, 0x06, 0x28, 0xE7, 0x0B, // 0xBE7280697600000 + 0x00, 0x00, 0xC0, 0x15, 0x4F, 0x2C, 0xE7, 0x0B, // 0xBE72C4F15C00000 + 0x00, 0x00, 0xC0, 0x2C, 0x4F, 0x30, 0xE7, 0x0B, // 0xBE7304F2CC00000 + 0x00, 0x00, 0xC0, 0xD7, 0x08, 0x34, 0xE7, 0x0B, // 0xBE73408D7C00000 + 0x00, 0x00, 0xE0, 0xD8, 0x08, 0x38, 0xE7, 0x0B, // 0xBE73808D8E00000 + 0x00, 0x00, 0x00, 0x54, 0x11, 0x3C, 0xE7, 0x0B, // 0xBE73C1154000000 + 0x00, 0x00, 0xA0, 0x5D, 0x11, 0x40, 0xE7, 0x0B, // 0xBE740115DA00000 + 0x00, 0x00, 0x40, 0x71, 0x11, 0x44, 0xE7, 0x0B, // 0xBE7441171400000 + 0x00, 0x00, 0xA0, 0x8A, 0x11, 0x48, 0xE7, 0x0B, // 0xBE748118AA00000 + 0x00, 0x00, 0x00, 0x95, 0x4F, 0x4C, 0xE7, 0x0B, // 0xBE74C4F95000000 + 0x00, 0x00, 0x60, 0x95, 0x11, 0x50, 0xE7, 0x0B, // 0xBE7501195600000 + 0x00, 0x00, 0x20, 0x98, 0x11, 0x54, 0xE7, 0x0B, // 0xBE7541198200000 + 0x00, 0x00, 0x60, 0xA3, 0x11, 0x58, 0xE7, 0x0B, // 0xBE75811A3600000 + 0x00, 0x00, 0xE0, 0xAE, 0x11, 0x5C, 0xE7, 0x0B, // 0xBE75C11AEE00000 + 0x00, 0x00, 0xE0, 0xE5, 0x4F, 0x60, 0xE7, 0x0B, // 0xBE7604FE5E00000 + 0x00, 0x00, 0x80, 0x00, 0x41, 0x64, 0xE7, 0x0B, // 0xBE7644100800000 + 0x00, 0x00, 0x60, 0xB9, 0x11, 0x68, 0xE7, 0x0B, // 0xBE76811B9600000 + 0x00, 0x00, 0x80, 0xB7, 0x11, 0x6C, 0xE7, 0x0B, // 0xBE76C11B7800000 + 0x00, 0x00, 0x00, 0xBE, 0x11, 0x70, 0xE7, 0x0B, // 0xBE77011BE000000 + 0x00, 0x00, 0xC0, 0x1B, 0x41, 0x74, 0xE7, 0x0B, // 0xBE774411BC00000 + 0x00, 0x00, 0x80, 0xDA, 0x11, 0x78, 0xE7, 0x0B, // 0xBE77811DA800000 + 0x00, 0x00, 0x00, 0xE7, 0x11, 0x7C, 0xE7, 0x0B, // 0xBE77C11E7000000 + 0x00, 0x00, 0x40, 0xBA, 0x50, 0x80, 0xE7, 0x0B, // 0xBE78050BA400000 + 0x00, 0x00, 0xA0, 0xBD, 0x50, 0x84, 0xE7, 0x0B, // 0xBE78450BDA00000 + 0x00, 0x00, 0x80, 0x12, 0x12, 0x88, 0xE7, 0x0B, // 0xBE7881212800000 + 0x00, 0x00, 0x20, 0x1E, 0x12, 0x8C, 0xE7, 0x0B, // 0xBE78C121E200000 + 0x00, 0x00, 0x20, 0x22, 0x12, 0x90, 0xE7, 0x0B, // 0xBE7901222200000 + 0x00, 0x00, 0xC0, 0xE5, 0x50, 0x94, 0xE7, 0x0B, // 0xBE79450E5C00000 + 0x00, 0x00, 0x60, 0x23, 0x12, 0x98, 0xE7, 0x0B, // 0xBE7981223600000 + 0x00, 0x00, 0x00, 0x47, 0x12, 0x9C, 0xE7, 0x0B, // 0xBE79C1247000000 + 0x00, 0x00, 0xE0, 0x5A, 0x12, 0xA0, 0xE7, 0x0B, // 0xBE7A0125AE00000 + 0x00, 0x00, 0x00, 0x5B, 0x12, 0xA4, 0xE7, 0x0B, // 0xBE7A4125B000000 + 0x00, 0x00, 0x80, 0x4F, 0x12, 0xA8, 0xE7, 0x0B, // 0xBE7A8124F800000 + 0x00, 0x00, 0x20, 0x7F, 0x12, 0xAC, 0xE7, 0x0B, // 0xBE7AC127F200000 + 0x00, 0x00, 0xA0, 0x82, 0x12, 0xB0, 0xE7, 0x0B, // 0xBE7B01282A00000 + 0x00, 0x00, 0x40, 0x7F, 0x51, 0xB4, 0xE7, 0x0B, // 0xBE7B4517F400000 + 0x00, 0x00, 0x60, 0xB1, 0x12, 0xB8, 0xE7, 0x0B, // 0xBE7B812B1600000 + 0x00, 0x00, 0xA0, 0x32, 0x09, 0xBC, 0xE7, 0x0B, // 0xBE7BC0932A00000 + 0x00, 0x00, 0xE0, 0xB6, 0x12, 0xC0, 0xE7, 0x0B, // 0xBE7C012B6E00000 + 0x00, 0x00, 0xE0, 0xAE, 0x51, 0xC4, 0xE7, 0x0B, // 0xBE7C451AEE00000 + 0x00, 0x00, 0xC0, 0x3C, 0x09, 0xC8, 0xE7, 0x0B, // 0xBE7C8093CC00000 + 0x00, 0x00, 0x60, 0xD8, 0x12, 0xCC, 0xE7, 0x0B, // 0xBE7CC12D8600000 + 0x00, 0x00, 0x40, 0xB6, 0x0B, 0xD0, 0xE7, 0x0B, // 0xBE7D00BB6400000 + 0x00, 0x00, 0x60, 0xE4, 0x12, 0xD4, 0xE7, 0x0B, // 0xBE7D412E4600000 + 0x00, 0x00, 0xA0, 0x28, 0x52, 0xD8, 0xE7, 0x0B, // 0xBE7D85228A00000 + 0x00, 0x00, 0x40, 0x43, 0x52, 0xDC, 0xE7, 0x0B, // 0xBE7DC5243400000 + 0x00, 0x00, 0xC0, 0x4D, 0x09, 0xE0, 0xE7, 0x0B, // 0xBE7E0094DC00000 + 0x00, 0x00, 0xC0, 0x4E, 0x09, 0xE4, 0xE7, 0x0B, // 0xBE7E4094EC00000 + 0x00, 0x00, 0x00, 0xFC, 0x12, 0xE8, 0xE7, 0x0B, // 0xBE7E812FC000000 + 0x00, 0x00, 0x40, 0x81, 0x52, 0xEC, 0xE7, 0x0B, // 0xBE7EC5281400000 + 0x00, 0x00, 0x40, 0x56, 0x09, 0xF0, 0xE7, 0x0B, // 0xBE7F00956400000 + 0x00, 0x00, 0xC0, 0x92, 0x52, 0xF4, 0xE7, 0x0B, // 0xBE7F45292C00000 + 0x00, 0x00, 0x60, 0x01, 0x13, 0xF8, 0xE7, 0x0B, // 0xBE7F81301600000 + 0x00, 0x00, 0x60, 0x01, 0x13, 0xFC, 0xE7, 0x0B, // 0xBE7FC1301600000 + 0x00, 0x00, 0x20, 0x05, 0x13, 0x00, 0xE8, 0x0B, // 0xBE8001305200000 + 0x00, 0x00, 0xC0, 0xB6, 0x52, 0x04, 0xE8, 0x0B, // 0xBE80452B6C00000 + 0x00, 0x00, 0x40, 0x1C, 0x13, 0x08, 0xE8, 0x0B, // 0xBE808131C400000 + 0x00, 0x00, 0x60, 0x66, 0x09, 0x0C, 0xE8, 0x0B, // 0xBE80C0966600000 + 0x00, 0x00, 0x20, 0x25, 0x13, 0x10, 0xE8, 0x0B, // 0xBE8101325200000 + 0x00, 0x00, 0xE0, 0x34, 0x13, 0x14, 0xE8, 0x0B, // 0xBE8141334E00000 + 0x00, 0x00, 0x40, 0x38, 0x13, 0x18, 0xE8, 0x0B, // 0xBE8181338400000 + 0x00, 0x00, 0xC0, 0x3F, 0x13, 0x1C, 0xE8, 0x0B, // 0xBE81C133FC00000 + 0x00, 0x00, 0xC0, 0x79, 0x09, 0x20, 0xE8, 0x0B, // 0xBE8200979C00000 + 0x00, 0x00, 0x00, 0x66, 0x53, 0x24, 0xE8, 0x0B, // 0xBE8245366000000 + 0x00, 0x00, 0x40, 0x62, 0x13, 0x28, 0xE8, 0x0B, // 0xBE8281362400000 + 0x00, 0x00, 0x00, 0x88, 0x13, 0x2C, 0xE8, 0x0B, // 0xBE82C1388000000 + 0x00, 0x00, 0xA0, 0x9F, 0x13, 0x30, 0xE8, 0x0B, // 0xBE830139FA00000 + 0x00, 0x00, 0xC0, 0x99, 0x09, 0x34, 0xE8, 0x0B, // 0xBE8340999C00000 + 0x00, 0x00, 0xA0, 0x9D, 0x09, 0x38, 0xE8, 0x0B, // 0xBE838099DA00000 + 0x00, 0x00, 0xE0, 0xAC, 0x13, 0x3C, 0xE8, 0x0B, // 0xBE83C13ACE00000 + 0x00, 0x00, 0xC0, 0x19, 0x54, 0x40, 0xE8, 0x0B, // 0xBE8405419C00000 + 0x00, 0x00, 0x00, 0x9F, 0x09, 0x44, 0xE8, 0x0B, // 0xBE844099F000000 + 0x00, 0x00, 0xA0, 0x20, 0x54, 0x48, 0xE8, 0x0B, // 0xBE8485420A00000 + 0x00, 0x00, 0xC0, 0x41, 0x54, 0x4C, 0xE8, 0x0B, // 0xBE84C5441C00000 + 0x00, 0x00, 0x20, 0x52, 0x54, 0x50, 0xE8, 0x0B, // 0xBE8505452200000 + 0x00, 0x00, 0x60, 0xD7, 0x13, 0x54, 0xE8, 0x0B, // 0xBE85413D7600000 + 0x00, 0x00, 0xC0, 0xAA, 0x09, 0x58, 0xE8, 0x0B, // 0xBE85809AAC00000 + 0x00, 0x00, 0x20, 0xDF, 0x13, 0x5C, 0xE8, 0x0B, // 0xBE85C13DF200000 + 0x00, 0x00, 0xC0, 0xDF, 0x13, 0x60, 0xE8, 0x0B, // 0xBE86013DFC00000 + 0x00, 0x00, 0xA0, 0xE0, 0x13, 0x64, 0xE8, 0x0B, // 0xBE86413E0A00000 + 0x00, 0x00, 0xE0, 0xE1, 0x13, 0x68, 0xE8, 0x0B, // 0xBE86813E1E00000 + 0x00, 0x00, 0xC0, 0xE2, 0x13, 0x6C, 0xE8, 0x0B, // 0xBE86C13E2C00000 + 0x00, 0x00, 0x60, 0xE7, 0x13, 0x70, 0xE8, 0x0B, // 0xBE87013E7600000 + 0x00, 0x00, 0x00, 0xC0, 0x54, 0x74, 0xE8, 0x0B, // 0xBE87454C0000000 + }; + + /// + /// Gets the canonical compositions, ordered by the pair of characters that join so that the table can be searched by the pair. + /// + /// Each entry packs three scalar values into 21 bits each, most significant first: the first character of the pair, + /// the second, and the character they compose to. + /// + /// + /// A pair appears only when it may be recomposed after being taken apart. A character excluded from composition, + /// one whose own combining class is non-zero, and one whose decomposition begins with a combining mark are all left + /// out, so composing never contradicts the decomposition it reverses. Hangul syllables compose by arithmetic and are + /// absent for that reason. + /// + /// + /// Each entry occupies eight bytes, least significant first, and is read with + /// . + /// + /// + public static ReadOnlySpan Compositions => new byte[] + { + 0x6E, 0x22, 0x00, 0x67, 0x00, 0xF0, 0x00, 0x00, // 0xF0006700226E + 0x60, 0x22, 0x00, 0x67, 0x00, 0xF4, 0x00, 0x00, // 0xF40067002260 + 0x6F, 0x22, 0x00, 0x67, 0x00, 0xF8, 0x00, 0x00, // 0xF8006700226F + 0xC0, 0x00, 0x00, 0x60, 0x00, 0x04, 0x01, 0x00, // 0x10400600000C0 + 0xC1, 0x00, 0x20, 0x60, 0x00, 0x04, 0x01, 0x00, // 0x10400602000C1 + 0xC2, 0x00, 0x40, 0x60, 0x00, 0x04, 0x01, 0x00, // 0x10400604000C2 + 0xC3, 0x00, 0x60, 0x60, 0x00, 0x04, 0x01, 0x00, // 0x10400606000C3 + 0x00, 0x01, 0x80, 0x60, 0x00, 0x04, 0x01, 0x00, // 0x1040060800100 + 0x02, 0x01, 0xC0, 0x60, 0x00, 0x04, 0x01, 0x00, // 0x1040060C00102 + 0x26, 0x02, 0xE0, 0x60, 0x00, 0x04, 0x01, 0x00, // 0x1040060E00226 + 0xC4, 0x00, 0x00, 0x61, 0x00, 0x04, 0x01, 0x00, // 0x10400610000C4 + 0xA2, 0x1E, 0x20, 0x61, 0x00, 0x04, 0x01, 0x00, // 0x1040061201EA2 + 0xC5, 0x00, 0x40, 0x61, 0x00, 0x04, 0x01, 0x00, // 0x10400614000C5 + 0xCD, 0x01, 0x80, 0x61, 0x00, 0x04, 0x01, 0x00, // 0x10400618001CD + 0x00, 0x02, 0xE0, 0x61, 0x00, 0x04, 0x01, 0x00, // 0x1040061E00200 + 0x02, 0x02, 0x20, 0x62, 0x00, 0x04, 0x01, 0x00, // 0x1040062200202 + 0xA0, 0x1E, 0x60, 0x64, 0x00, 0x04, 0x01, 0x00, // 0x1040064601EA0 + 0x00, 0x1E, 0xA0, 0x64, 0x00, 0x04, 0x01, 0x00, // 0x1040064A01E00 + 0x04, 0x01, 0x00, 0x65, 0x00, 0x04, 0x01, 0x00, // 0x1040065000104 + 0x02, 0x1E, 0xE0, 0x60, 0x00, 0x08, 0x01, 0x00, // 0x1080060E01E02 + 0x04, 0x1E, 0x60, 0x64, 0x00, 0x08, 0x01, 0x00, // 0x1080064601E04 + 0x06, 0x1E, 0x20, 0x66, 0x00, 0x08, 0x01, 0x00, // 0x1080066201E06 + 0x06, 0x01, 0x20, 0x60, 0x00, 0x0C, 0x01, 0x00, // 0x10C0060200106 + 0x08, 0x01, 0x40, 0x60, 0x00, 0x0C, 0x01, 0x00, // 0x10C0060400108 + 0x0A, 0x01, 0xE0, 0x60, 0x00, 0x0C, 0x01, 0x00, // 0x10C0060E0010A + 0x0C, 0x01, 0x80, 0x61, 0x00, 0x0C, 0x01, 0x00, // 0x10C006180010C + 0xC7, 0x00, 0xE0, 0x64, 0x00, 0x0C, 0x01, 0x00, // 0x10C0064E000C7 + 0x0A, 0x1E, 0xE0, 0x60, 0x00, 0x10, 0x01, 0x00, // 0x1100060E01E0A + 0x0E, 0x01, 0x80, 0x61, 0x00, 0x10, 0x01, 0x00, // 0x110006180010E + 0x0C, 0x1E, 0x60, 0x64, 0x00, 0x10, 0x01, 0x00, // 0x1100064601E0C + 0x10, 0x1E, 0xE0, 0x64, 0x00, 0x10, 0x01, 0x00, // 0x1100064E01E10 + 0x12, 0x1E, 0xA0, 0x65, 0x00, 0x10, 0x01, 0x00, // 0x1100065A01E12 + 0x0E, 0x1E, 0x20, 0x66, 0x00, 0x10, 0x01, 0x00, // 0x1100066201E0E + 0xC8, 0x00, 0x00, 0x60, 0x00, 0x14, 0x01, 0x00, // 0x11400600000C8 + 0xC9, 0x00, 0x20, 0x60, 0x00, 0x14, 0x01, 0x00, // 0x11400602000C9 + 0xCA, 0x00, 0x40, 0x60, 0x00, 0x14, 0x01, 0x00, // 0x11400604000CA + 0xBC, 0x1E, 0x60, 0x60, 0x00, 0x14, 0x01, 0x00, // 0x1140060601EBC + 0x12, 0x01, 0x80, 0x60, 0x00, 0x14, 0x01, 0x00, // 0x1140060800112 + 0x14, 0x01, 0xC0, 0x60, 0x00, 0x14, 0x01, 0x00, // 0x1140060C00114 + 0x16, 0x01, 0xE0, 0x60, 0x00, 0x14, 0x01, 0x00, // 0x1140060E00116 + 0xCB, 0x00, 0x00, 0x61, 0x00, 0x14, 0x01, 0x00, // 0x11400610000CB + 0xBA, 0x1E, 0x20, 0x61, 0x00, 0x14, 0x01, 0x00, // 0x1140061201EBA + 0x1A, 0x01, 0x80, 0x61, 0x00, 0x14, 0x01, 0x00, // 0x114006180011A + 0x04, 0x02, 0xE0, 0x61, 0x00, 0x14, 0x01, 0x00, // 0x1140061E00204 + 0x06, 0x02, 0x20, 0x62, 0x00, 0x14, 0x01, 0x00, // 0x1140062200206 + 0xB8, 0x1E, 0x60, 0x64, 0x00, 0x14, 0x01, 0x00, // 0x1140064601EB8 + 0x28, 0x02, 0xE0, 0x64, 0x00, 0x14, 0x01, 0x00, // 0x1140064E00228 + 0x18, 0x01, 0x00, 0x65, 0x00, 0x14, 0x01, 0x00, // 0x1140065000118 + 0x18, 0x1E, 0xA0, 0x65, 0x00, 0x14, 0x01, 0x00, // 0x1140065A01E18 + 0x1A, 0x1E, 0x00, 0x66, 0x00, 0x14, 0x01, 0x00, // 0x1140066001E1A + 0x1E, 0x1E, 0xE0, 0x60, 0x00, 0x18, 0x01, 0x00, // 0x1180060E01E1E + 0xF4, 0x01, 0x20, 0x60, 0x00, 0x1C, 0x01, 0x00, // 0x11C00602001F4 + 0x1C, 0x01, 0x40, 0x60, 0x00, 0x1C, 0x01, 0x00, // 0x11C006040011C + 0x20, 0x1E, 0x80, 0x60, 0x00, 0x1C, 0x01, 0x00, // 0x11C0060801E20 + 0x1E, 0x01, 0xC0, 0x60, 0x00, 0x1C, 0x01, 0x00, // 0x11C0060C0011E + 0x20, 0x01, 0xE0, 0x60, 0x00, 0x1C, 0x01, 0x00, // 0x11C0060E00120 + 0xE6, 0x01, 0x80, 0x61, 0x00, 0x1C, 0x01, 0x00, // 0x11C00618001E6 + 0x22, 0x01, 0xE0, 0x64, 0x00, 0x1C, 0x01, 0x00, // 0x11C0064E00122 + 0x24, 0x01, 0x40, 0x60, 0x00, 0x20, 0x01, 0x00, // 0x1200060400124 + 0x22, 0x1E, 0xE0, 0x60, 0x00, 0x20, 0x01, 0x00, // 0x1200060E01E22 + 0x26, 0x1E, 0x00, 0x61, 0x00, 0x20, 0x01, 0x00, // 0x1200061001E26 + 0x1E, 0x02, 0x80, 0x61, 0x00, 0x20, 0x01, 0x00, // 0x120006180021E + 0x24, 0x1E, 0x60, 0x64, 0x00, 0x20, 0x01, 0x00, // 0x1200064601E24 + 0x28, 0x1E, 0xE0, 0x64, 0x00, 0x20, 0x01, 0x00, // 0x1200064E01E28 + 0x2A, 0x1E, 0xC0, 0x65, 0x00, 0x20, 0x01, 0x00, // 0x1200065C01E2A + 0xCC, 0x00, 0x00, 0x60, 0x00, 0x24, 0x01, 0x00, // 0x12400600000CC + 0xCD, 0x00, 0x20, 0x60, 0x00, 0x24, 0x01, 0x00, // 0x12400602000CD + 0xCE, 0x00, 0x40, 0x60, 0x00, 0x24, 0x01, 0x00, // 0x12400604000CE + 0x28, 0x01, 0x60, 0x60, 0x00, 0x24, 0x01, 0x00, // 0x1240060600128 + 0x2A, 0x01, 0x80, 0x60, 0x00, 0x24, 0x01, 0x00, // 0x124006080012A + 0x2C, 0x01, 0xC0, 0x60, 0x00, 0x24, 0x01, 0x00, // 0x1240060C0012C + 0x30, 0x01, 0xE0, 0x60, 0x00, 0x24, 0x01, 0x00, // 0x1240060E00130 + 0xCF, 0x00, 0x00, 0x61, 0x00, 0x24, 0x01, 0x00, // 0x12400610000CF + 0xC8, 0x1E, 0x20, 0x61, 0x00, 0x24, 0x01, 0x00, // 0x1240061201EC8 + 0xCF, 0x01, 0x80, 0x61, 0x00, 0x24, 0x01, 0x00, // 0x12400618001CF + 0x08, 0x02, 0xE0, 0x61, 0x00, 0x24, 0x01, 0x00, // 0x1240061E00208 + 0x0A, 0x02, 0x20, 0x62, 0x00, 0x24, 0x01, 0x00, // 0x124006220020A + 0xCA, 0x1E, 0x60, 0x64, 0x00, 0x24, 0x01, 0x00, // 0x1240064601ECA + 0x2E, 0x01, 0x00, 0x65, 0x00, 0x24, 0x01, 0x00, // 0x124006500012E + 0x2C, 0x1E, 0x00, 0x66, 0x00, 0x24, 0x01, 0x00, // 0x1240066001E2C + 0x34, 0x01, 0x40, 0x60, 0x00, 0x28, 0x01, 0x00, // 0x1280060400134 + 0x30, 0x1E, 0x20, 0x60, 0x00, 0x2C, 0x01, 0x00, // 0x12C0060201E30 + 0xE8, 0x01, 0x80, 0x61, 0x00, 0x2C, 0x01, 0x00, // 0x12C00618001E8 + 0x32, 0x1E, 0x60, 0x64, 0x00, 0x2C, 0x01, 0x00, // 0x12C0064601E32 + 0x36, 0x01, 0xE0, 0x64, 0x00, 0x2C, 0x01, 0x00, // 0x12C0064E00136 + 0x34, 0x1E, 0x20, 0x66, 0x00, 0x2C, 0x01, 0x00, // 0x12C0066201E34 + 0x39, 0x01, 0x20, 0x60, 0x00, 0x30, 0x01, 0x00, // 0x1300060200139 + 0x3D, 0x01, 0x80, 0x61, 0x00, 0x30, 0x01, 0x00, // 0x130006180013D + 0x36, 0x1E, 0x60, 0x64, 0x00, 0x30, 0x01, 0x00, // 0x1300064601E36 + 0x3B, 0x01, 0xE0, 0x64, 0x00, 0x30, 0x01, 0x00, // 0x1300064E0013B + 0x3C, 0x1E, 0xA0, 0x65, 0x00, 0x30, 0x01, 0x00, // 0x1300065A01E3C + 0x3A, 0x1E, 0x20, 0x66, 0x00, 0x30, 0x01, 0x00, // 0x1300066201E3A + 0x3E, 0x1E, 0x20, 0x60, 0x00, 0x34, 0x01, 0x00, // 0x1340060201E3E + 0x40, 0x1E, 0xE0, 0x60, 0x00, 0x34, 0x01, 0x00, // 0x1340060E01E40 + 0x42, 0x1E, 0x60, 0x64, 0x00, 0x34, 0x01, 0x00, // 0x1340064601E42 + 0xF8, 0x01, 0x00, 0x60, 0x00, 0x38, 0x01, 0x00, // 0x13800600001F8 + 0x43, 0x01, 0x20, 0x60, 0x00, 0x38, 0x01, 0x00, // 0x1380060200143 + 0xD1, 0x00, 0x60, 0x60, 0x00, 0x38, 0x01, 0x00, // 0x13800606000D1 + 0x44, 0x1E, 0xE0, 0x60, 0x00, 0x38, 0x01, 0x00, // 0x1380060E01E44 + 0x47, 0x01, 0x80, 0x61, 0x00, 0x38, 0x01, 0x00, // 0x1380061800147 + 0x46, 0x1E, 0x60, 0x64, 0x00, 0x38, 0x01, 0x00, // 0x1380064601E46 + 0x45, 0x01, 0xE0, 0x64, 0x00, 0x38, 0x01, 0x00, // 0x1380064E00145 + 0x4A, 0x1E, 0xA0, 0x65, 0x00, 0x38, 0x01, 0x00, // 0x1380065A01E4A + 0x48, 0x1E, 0x20, 0x66, 0x00, 0x38, 0x01, 0x00, // 0x1380066201E48 + 0xD2, 0x00, 0x00, 0x60, 0x00, 0x3C, 0x01, 0x00, // 0x13C00600000D2 + 0xD3, 0x00, 0x20, 0x60, 0x00, 0x3C, 0x01, 0x00, // 0x13C00602000D3 + 0xD4, 0x00, 0x40, 0x60, 0x00, 0x3C, 0x01, 0x00, // 0x13C00604000D4 + 0xD5, 0x00, 0x60, 0x60, 0x00, 0x3C, 0x01, 0x00, // 0x13C00606000D5 + 0x4C, 0x01, 0x80, 0x60, 0x00, 0x3C, 0x01, 0x00, // 0x13C006080014C + 0x4E, 0x01, 0xC0, 0x60, 0x00, 0x3C, 0x01, 0x00, // 0x13C0060C0014E + 0x2E, 0x02, 0xE0, 0x60, 0x00, 0x3C, 0x01, 0x00, // 0x13C0060E0022E + 0xD6, 0x00, 0x00, 0x61, 0x00, 0x3C, 0x01, 0x00, // 0x13C00610000D6 + 0xCE, 0x1E, 0x20, 0x61, 0x00, 0x3C, 0x01, 0x00, // 0x13C0061201ECE + 0x50, 0x01, 0x60, 0x61, 0x00, 0x3C, 0x01, 0x00, // 0x13C0061600150 + 0xD1, 0x01, 0x80, 0x61, 0x00, 0x3C, 0x01, 0x00, // 0x13C00618001D1 + 0x0C, 0x02, 0xE0, 0x61, 0x00, 0x3C, 0x01, 0x00, // 0x13C0061E0020C + 0x0E, 0x02, 0x20, 0x62, 0x00, 0x3C, 0x01, 0x00, // 0x13C006220020E + 0xA0, 0x01, 0x60, 0x63, 0x00, 0x3C, 0x01, 0x00, // 0x13C00636001A0 + 0xCC, 0x1E, 0x60, 0x64, 0x00, 0x3C, 0x01, 0x00, // 0x13C0064601ECC + 0xEA, 0x01, 0x00, 0x65, 0x00, 0x3C, 0x01, 0x00, // 0x13C00650001EA + 0x54, 0x1E, 0x20, 0x60, 0x00, 0x40, 0x01, 0x00, // 0x1400060201E54 + 0x56, 0x1E, 0xE0, 0x60, 0x00, 0x40, 0x01, 0x00, // 0x1400060E01E56 + 0x54, 0x01, 0x20, 0x60, 0x00, 0x48, 0x01, 0x00, // 0x1480060200154 + 0x58, 0x1E, 0xE0, 0x60, 0x00, 0x48, 0x01, 0x00, // 0x1480060E01E58 + 0x58, 0x01, 0x80, 0x61, 0x00, 0x48, 0x01, 0x00, // 0x1480061800158 + 0x10, 0x02, 0xE0, 0x61, 0x00, 0x48, 0x01, 0x00, // 0x1480061E00210 + 0x12, 0x02, 0x20, 0x62, 0x00, 0x48, 0x01, 0x00, // 0x1480062200212 + 0x5A, 0x1E, 0x60, 0x64, 0x00, 0x48, 0x01, 0x00, // 0x1480064601E5A + 0x56, 0x01, 0xE0, 0x64, 0x00, 0x48, 0x01, 0x00, // 0x1480064E00156 + 0x5E, 0x1E, 0x20, 0x66, 0x00, 0x48, 0x01, 0x00, // 0x1480066201E5E + 0x5A, 0x01, 0x20, 0x60, 0x00, 0x4C, 0x01, 0x00, // 0x14C006020015A + 0x5C, 0x01, 0x40, 0x60, 0x00, 0x4C, 0x01, 0x00, // 0x14C006040015C + 0x60, 0x1E, 0xE0, 0x60, 0x00, 0x4C, 0x01, 0x00, // 0x14C0060E01E60 + 0x60, 0x01, 0x80, 0x61, 0x00, 0x4C, 0x01, 0x00, // 0x14C0061800160 + 0x62, 0x1E, 0x60, 0x64, 0x00, 0x4C, 0x01, 0x00, // 0x14C0064601E62 + 0x18, 0x02, 0xC0, 0x64, 0x00, 0x4C, 0x01, 0x00, // 0x14C0064C00218 + 0x5E, 0x01, 0xE0, 0x64, 0x00, 0x4C, 0x01, 0x00, // 0x14C0064E0015E + 0x6A, 0x1E, 0xE0, 0x60, 0x00, 0x50, 0x01, 0x00, // 0x1500060E01E6A + 0x64, 0x01, 0x80, 0x61, 0x00, 0x50, 0x01, 0x00, // 0x1500061800164 + 0x6C, 0x1E, 0x60, 0x64, 0x00, 0x50, 0x01, 0x00, // 0x1500064601E6C + 0x1A, 0x02, 0xC0, 0x64, 0x00, 0x50, 0x01, 0x00, // 0x1500064C0021A + 0x62, 0x01, 0xE0, 0x64, 0x00, 0x50, 0x01, 0x00, // 0x1500064E00162 + 0x70, 0x1E, 0xA0, 0x65, 0x00, 0x50, 0x01, 0x00, // 0x1500065A01E70 + 0x6E, 0x1E, 0x20, 0x66, 0x00, 0x50, 0x01, 0x00, // 0x1500066201E6E + 0xD9, 0x00, 0x00, 0x60, 0x00, 0x54, 0x01, 0x00, // 0x15400600000D9 + 0xDA, 0x00, 0x20, 0x60, 0x00, 0x54, 0x01, 0x00, // 0x15400602000DA + 0xDB, 0x00, 0x40, 0x60, 0x00, 0x54, 0x01, 0x00, // 0x15400604000DB + 0x68, 0x01, 0x60, 0x60, 0x00, 0x54, 0x01, 0x00, // 0x1540060600168 + 0x6A, 0x01, 0x80, 0x60, 0x00, 0x54, 0x01, 0x00, // 0x154006080016A + 0x6C, 0x01, 0xC0, 0x60, 0x00, 0x54, 0x01, 0x00, // 0x1540060C0016C + 0xDC, 0x00, 0x00, 0x61, 0x00, 0x54, 0x01, 0x00, // 0x15400610000DC + 0xE6, 0x1E, 0x20, 0x61, 0x00, 0x54, 0x01, 0x00, // 0x1540061201EE6 + 0x6E, 0x01, 0x40, 0x61, 0x00, 0x54, 0x01, 0x00, // 0x154006140016E + 0x70, 0x01, 0x60, 0x61, 0x00, 0x54, 0x01, 0x00, // 0x1540061600170 + 0xD3, 0x01, 0x80, 0x61, 0x00, 0x54, 0x01, 0x00, // 0x15400618001D3 + 0x14, 0x02, 0xE0, 0x61, 0x00, 0x54, 0x01, 0x00, // 0x1540061E00214 + 0x16, 0x02, 0x20, 0x62, 0x00, 0x54, 0x01, 0x00, // 0x1540062200216 + 0xAF, 0x01, 0x60, 0x63, 0x00, 0x54, 0x01, 0x00, // 0x15400636001AF + 0xE4, 0x1E, 0x60, 0x64, 0x00, 0x54, 0x01, 0x00, // 0x1540064601EE4 + 0x72, 0x1E, 0x80, 0x64, 0x00, 0x54, 0x01, 0x00, // 0x1540064801E72 + 0x72, 0x01, 0x00, 0x65, 0x00, 0x54, 0x01, 0x00, // 0x1540065000172 + 0x76, 0x1E, 0xA0, 0x65, 0x00, 0x54, 0x01, 0x00, // 0x1540065A01E76 + 0x74, 0x1E, 0x00, 0x66, 0x00, 0x54, 0x01, 0x00, // 0x1540066001E74 + 0x7C, 0x1E, 0x60, 0x60, 0x00, 0x58, 0x01, 0x00, // 0x1580060601E7C + 0x7E, 0x1E, 0x60, 0x64, 0x00, 0x58, 0x01, 0x00, // 0x1580064601E7E + 0x80, 0x1E, 0x00, 0x60, 0x00, 0x5C, 0x01, 0x00, // 0x15C0060001E80 + 0x82, 0x1E, 0x20, 0x60, 0x00, 0x5C, 0x01, 0x00, // 0x15C0060201E82 + 0x74, 0x01, 0x40, 0x60, 0x00, 0x5C, 0x01, 0x00, // 0x15C0060400174 + 0x86, 0x1E, 0xE0, 0x60, 0x00, 0x5C, 0x01, 0x00, // 0x15C0060E01E86 + 0x84, 0x1E, 0x00, 0x61, 0x00, 0x5C, 0x01, 0x00, // 0x15C0061001E84 + 0x88, 0x1E, 0x60, 0x64, 0x00, 0x5C, 0x01, 0x00, // 0x15C0064601E88 + 0x8A, 0x1E, 0xE0, 0x60, 0x00, 0x60, 0x01, 0x00, // 0x1600060E01E8A + 0x8C, 0x1E, 0x00, 0x61, 0x00, 0x60, 0x01, 0x00, // 0x1600061001E8C + 0xF2, 0x1E, 0x00, 0x60, 0x00, 0x64, 0x01, 0x00, // 0x1640060001EF2 + 0xDD, 0x00, 0x20, 0x60, 0x00, 0x64, 0x01, 0x00, // 0x16400602000DD + 0x76, 0x01, 0x40, 0x60, 0x00, 0x64, 0x01, 0x00, // 0x1640060400176 + 0xF8, 0x1E, 0x60, 0x60, 0x00, 0x64, 0x01, 0x00, // 0x1640060601EF8 + 0x32, 0x02, 0x80, 0x60, 0x00, 0x64, 0x01, 0x00, // 0x1640060800232 + 0x8E, 0x1E, 0xE0, 0x60, 0x00, 0x64, 0x01, 0x00, // 0x1640060E01E8E + 0x78, 0x01, 0x00, 0x61, 0x00, 0x64, 0x01, 0x00, // 0x1640061000178 + 0xF6, 0x1E, 0x20, 0x61, 0x00, 0x64, 0x01, 0x00, // 0x1640061201EF6 + 0xF4, 0x1E, 0x60, 0x64, 0x00, 0x64, 0x01, 0x00, // 0x1640064601EF4 + 0x79, 0x01, 0x20, 0x60, 0x00, 0x68, 0x01, 0x00, // 0x1680060200179 + 0x90, 0x1E, 0x40, 0x60, 0x00, 0x68, 0x01, 0x00, // 0x1680060401E90 + 0x7B, 0x01, 0xE0, 0x60, 0x00, 0x68, 0x01, 0x00, // 0x1680060E0017B + 0x7D, 0x01, 0x80, 0x61, 0x00, 0x68, 0x01, 0x00, // 0x168006180017D + 0x92, 0x1E, 0x60, 0x64, 0x00, 0x68, 0x01, 0x00, // 0x1680064601E92 + 0x94, 0x1E, 0x20, 0x66, 0x00, 0x68, 0x01, 0x00, // 0x1680066201E94 + 0xE0, 0x00, 0x00, 0x60, 0x00, 0x84, 0x01, 0x00, // 0x18400600000E0 + 0xE1, 0x00, 0x20, 0x60, 0x00, 0x84, 0x01, 0x00, // 0x18400602000E1 + 0xE2, 0x00, 0x40, 0x60, 0x00, 0x84, 0x01, 0x00, // 0x18400604000E2 + 0xE3, 0x00, 0x60, 0x60, 0x00, 0x84, 0x01, 0x00, // 0x18400606000E3 + 0x01, 0x01, 0x80, 0x60, 0x00, 0x84, 0x01, 0x00, // 0x1840060800101 + 0x03, 0x01, 0xC0, 0x60, 0x00, 0x84, 0x01, 0x00, // 0x1840060C00103 + 0x27, 0x02, 0xE0, 0x60, 0x00, 0x84, 0x01, 0x00, // 0x1840060E00227 + 0xE4, 0x00, 0x00, 0x61, 0x00, 0x84, 0x01, 0x00, // 0x18400610000E4 + 0xA3, 0x1E, 0x20, 0x61, 0x00, 0x84, 0x01, 0x00, // 0x1840061201EA3 + 0xE5, 0x00, 0x40, 0x61, 0x00, 0x84, 0x01, 0x00, // 0x18400614000E5 + 0xCE, 0x01, 0x80, 0x61, 0x00, 0x84, 0x01, 0x00, // 0x18400618001CE + 0x01, 0x02, 0xE0, 0x61, 0x00, 0x84, 0x01, 0x00, // 0x1840061E00201 + 0x03, 0x02, 0x20, 0x62, 0x00, 0x84, 0x01, 0x00, // 0x1840062200203 + 0xA1, 0x1E, 0x60, 0x64, 0x00, 0x84, 0x01, 0x00, // 0x1840064601EA1 + 0x01, 0x1E, 0xA0, 0x64, 0x00, 0x84, 0x01, 0x00, // 0x1840064A01E01 + 0x05, 0x01, 0x00, 0x65, 0x00, 0x84, 0x01, 0x00, // 0x1840065000105 + 0x03, 0x1E, 0xE0, 0x60, 0x00, 0x88, 0x01, 0x00, // 0x1880060E01E03 + 0x05, 0x1E, 0x60, 0x64, 0x00, 0x88, 0x01, 0x00, // 0x1880064601E05 + 0x07, 0x1E, 0x20, 0x66, 0x00, 0x88, 0x01, 0x00, // 0x1880066201E07 + 0x07, 0x01, 0x20, 0x60, 0x00, 0x8C, 0x01, 0x00, // 0x18C0060200107 + 0x09, 0x01, 0x40, 0x60, 0x00, 0x8C, 0x01, 0x00, // 0x18C0060400109 + 0x0B, 0x01, 0xE0, 0x60, 0x00, 0x8C, 0x01, 0x00, // 0x18C0060E0010B + 0x0D, 0x01, 0x80, 0x61, 0x00, 0x8C, 0x01, 0x00, // 0x18C006180010D + 0xE7, 0x00, 0xE0, 0x64, 0x00, 0x8C, 0x01, 0x00, // 0x18C0064E000E7 + 0x0B, 0x1E, 0xE0, 0x60, 0x00, 0x90, 0x01, 0x00, // 0x1900060E01E0B + 0x0F, 0x01, 0x80, 0x61, 0x00, 0x90, 0x01, 0x00, // 0x190006180010F + 0x0D, 0x1E, 0x60, 0x64, 0x00, 0x90, 0x01, 0x00, // 0x1900064601E0D + 0x11, 0x1E, 0xE0, 0x64, 0x00, 0x90, 0x01, 0x00, // 0x1900064E01E11 + 0x13, 0x1E, 0xA0, 0x65, 0x00, 0x90, 0x01, 0x00, // 0x1900065A01E13 + 0x0F, 0x1E, 0x20, 0x66, 0x00, 0x90, 0x01, 0x00, // 0x1900066201E0F + 0xE8, 0x00, 0x00, 0x60, 0x00, 0x94, 0x01, 0x00, // 0x19400600000E8 + 0xE9, 0x00, 0x20, 0x60, 0x00, 0x94, 0x01, 0x00, // 0x19400602000E9 + 0xEA, 0x00, 0x40, 0x60, 0x00, 0x94, 0x01, 0x00, // 0x19400604000EA + 0xBD, 0x1E, 0x60, 0x60, 0x00, 0x94, 0x01, 0x00, // 0x1940060601EBD + 0x13, 0x01, 0x80, 0x60, 0x00, 0x94, 0x01, 0x00, // 0x1940060800113 + 0x15, 0x01, 0xC0, 0x60, 0x00, 0x94, 0x01, 0x00, // 0x1940060C00115 + 0x17, 0x01, 0xE0, 0x60, 0x00, 0x94, 0x01, 0x00, // 0x1940060E00117 + 0xEB, 0x00, 0x00, 0x61, 0x00, 0x94, 0x01, 0x00, // 0x19400610000EB + 0xBB, 0x1E, 0x20, 0x61, 0x00, 0x94, 0x01, 0x00, // 0x1940061201EBB + 0x1B, 0x01, 0x80, 0x61, 0x00, 0x94, 0x01, 0x00, // 0x194006180011B + 0x05, 0x02, 0xE0, 0x61, 0x00, 0x94, 0x01, 0x00, // 0x1940061E00205 + 0x07, 0x02, 0x20, 0x62, 0x00, 0x94, 0x01, 0x00, // 0x1940062200207 + 0xB9, 0x1E, 0x60, 0x64, 0x00, 0x94, 0x01, 0x00, // 0x1940064601EB9 + 0x29, 0x02, 0xE0, 0x64, 0x00, 0x94, 0x01, 0x00, // 0x1940064E00229 + 0x19, 0x01, 0x00, 0x65, 0x00, 0x94, 0x01, 0x00, // 0x1940065000119 + 0x19, 0x1E, 0xA0, 0x65, 0x00, 0x94, 0x01, 0x00, // 0x1940065A01E19 + 0x1B, 0x1E, 0x00, 0x66, 0x00, 0x94, 0x01, 0x00, // 0x1940066001E1B + 0x1F, 0x1E, 0xE0, 0x60, 0x00, 0x98, 0x01, 0x00, // 0x1980060E01E1F + 0xF5, 0x01, 0x20, 0x60, 0x00, 0x9C, 0x01, 0x00, // 0x19C00602001F5 + 0x1D, 0x01, 0x40, 0x60, 0x00, 0x9C, 0x01, 0x00, // 0x19C006040011D + 0x21, 0x1E, 0x80, 0x60, 0x00, 0x9C, 0x01, 0x00, // 0x19C0060801E21 + 0x1F, 0x01, 0xC0, 0x60, 0x00, 0x9C, 0x01, 0x00, // 0x19C0060C0011F + 0x21, 0x01, 0xE0, 0x60, 0x00, 0x9C, 0x01, 0x00, // 0x19C0060E00121 + 0xE7, 0x01, 0x80, 0x61, 0x00, 0x9C, 0x01, 0x00, // 0x19C00618001E7 + 0x23, 0x01, 0xE0, 0x64, 0x00, 0x9C, 0x01, 0x00, // 0x19C0064E00123 + 0x25, 0x01, 0x40, 0x60, 0x00, 0xA0, 0x01, 0x00, // 0x1A00060400125 + 0x23, 0x1E, 0xE0, 0x60, 0x00, 0xA0, 0x01, 0x00, // 0x1A00060E01E23 + 0x27, 0x1E, 0x00, 0x61, 0x00, 0xA0, 0x01, 0x00, // 0x1A00061001E27 + 0x1F, 0x02, 0x80, 0x61, 0x00, 0xA0, 0x01, 0x00, // 0x1A0006180021F + 0x25, 0x1E, 0x60, 0x64, 0x00, 0xA0, 0x01, 0x00, // 0x1A00064601E25 + 0x29, 0x1E, 0xE0, 0x64, 0x00, 0xA0, 0x01, 0x00, // 0x1A00064E01E29 + 0x2B, 0x1E, 0xC0, 0x65, 0x00, 0xA0, 0x01, 0x00, // 0x1A00065C01E2B + 0x96, 0x1E, 0x20, 0x66, 0x00, 0xA0, 0x01, 0x00, // 0x1A00066201E96 + 0xEC, 0x00, 0x00, 0x60, 0x00, 0xA4, 0x01, 0x00, // 0x1A400600000EC + 0xED, 0x00, 0x20, 0x60, 0x00, 0xA4, 0x01, 0x00, // 0x1A400602000ED + 0xEE, 0x00, 0x40, 0x60, 0x00, 0xA4, 0x01, 0x00, // 0x1A400604000EE + 0x29, 0x01, 0x60, 0x60, 0x00, 0xA4, 0x01, 0x00, // 0x1A40060600129 + 0x2B, 0x01, 0x80, 0x60, 0x00, 0xA4, 0x01, 0x00, // 0x1A4006080012B + 0x2D, 0x01, 0xC0, 0x60, 0x00, 0xA4, 0x01, 0x00, // 0x1A40060C0012D + 0xEF, 0x00, 0x00, 0x61, 0x00, 0xA4, 0x01, 0x00, // 0x1A400610000EF + 0xC9, 0x1E, 0x20, 0x61, 0x00, 0xA4, 0x01, 0x00, // 0x1A40061201EC9 + 0xD0, 0x01, 0x80, 0x61, 0x00, 0xA4, 0x01, 0x00, // 0x1A400618001D0 + 0x09, 0x02, 0xE0, 0x61, 0x00, 0xA4, 0x01, 0x00, // 0x1A40061E00209 + 0x0B, 0x02, 0x20, 0x62, 0x00, 0xA4, 0x01, 0x00, // 0x1A4006220020B + 0xCB, 0x1E, 0x60, 0x64, 0x00, 0xA4, 0x01, 0x00, // 0x1A40064601ECB + 0x2F, 0x01, 0x00, 0x65, 0x00, 0xA4, 0x01, 0x00, // 0x1A4006500012F + 0x2D, 0x1E, 0x00, 0x66, 0x00, 0xA4, 0x01, 0x00, // 0x1A40066001E2D + 0x35, 0x01, 0x40, 0x60, 0x00, 0xA8, 0x01, 0x00, // 0x1A80060400135 + 0xF0, 0x01, 0x80, 0x61, 0x00, 0xA8, 0x01, 0x00, // 0x1A800618001F0 + 0x31, 0x1E, 0x20, 0x60, 0x00, 0xAC, 0x01, 0x00, // 0x1AC0060201E31 + 0xE9, 0x01, 0x80, 0x61, 0x00, 0xAC, 0x01, 0x00, // 0x1AC00618001E9 + 0x33, 0x1E, 0x60, 0x64, 0x00, 0xAC, 0x01, 0x00, // 0x1AC0064601E33 + 0x37, 0x01, 0xE0, 0x64, 0x00, 0xAC, 0x01, 0x00, // 0x1AC0064E00137 + 0x35, 0x1E, 0x20, 0x66, 0x00, 0xAC, 0x01, 0x00, // 0x1AC0066201E35 + 0x3A, 0x01, 0x20, 0x60, 0x00, 0xB0, 0x01, 0x00, // 0x1B0006020013A + 0x3E, 0x01, 0x80, 0x61, 0x00, 0xB0, 0x01, 0x00, // 0x1B0006180013E + 0x37, 0x1E, 0x60, 0x64, 0x00, 0xB0, 0x01, 0x00, // 0x1B00064601E37 + 0x3C, 0x01, 0xE0, 0x64, 0x00, 0xB0, 0x01, 0x00, // 0x1B00064E0013C + 0x3D, 0x1E, 0xA0, 0x65, 0x00, 0xB0, 0x01, 0x00, // 0x1B00065A01E3D + 0x3B, 0x1E, 0x20, 0x66, 0x00, 0xB0, 0x01, 0x00, // 0x1B00066201E3B + 0x3F, 0x1E, 0x20, 0x60, 0x00, 0xB4, 0x01, 0x00, // 0x1B40060201E3F + 0x41, 0x1E, 0xE0, 0x60, 0x00, 0xB4, 0x01, 0x00, // 0x1B40060E01E41 + 0x43, 0x1E, 0x60, 0x64, 0x00, 0xB4, 0x01, 0x00, // 0x1B40064601E43 + 0xF9, 0x01, 0x00, 0x60, 0x00, 0xB8, 0x01, 0x00, // 0x1B800600001F9 + 0x44, 0x01, 0x20, 0x60, 0x00, 0xB8, 0x01, 0x00, // 0x1B80060200144 + 0xF1, 0x00, 0x60, 0x60, 0x00, 0xB8, 0x01, 0x00, // 0x1B800606000F1 + 0x45, 0x1E, 0xE0, 0x60, 0x00, 0xB8, 0x01, 0x00, // 0x1B80060E01E45 + 0x48, 0x01, 0x80, 0x61, 0x00, 0xB8, 0x01, 0x00, // 0x1B80061800148 + 0x47, 0x1E, 0x60, 0x64, 0x00, 0xB8, 0x01, 0x00, // 0x1B80064601E47 + 0x46, 0x01, 0xE0, 0x64, 0x00, 0xB8, 0x01, 0x00, // 0x1B80064E00146 + 0x4B, 0x1E, 0xA0, 0x65, 0x00, 0xB8, 0x01, 0x00, // 0x1B80065A01E4B + 0x49, 0x1E, 0x20, 0x66, 0x00, 0xB8, 0x01, 0x00, // 0x1B80066201E49 + 0xF2, 0x00, 0x00, 0x60, 0x00, 0xBC, 0x01, 0x00, // 0x1BC00600000F2 + 0xF3, 0x00, 0x20, 0x60, 0x00, 0xBC, 0x01, 0x00, // 0x1BC00602000F3 + 0xF4, 0x00, 0x40, 0x60, 0x00, 0xBC, 0x01, 0x00, // 0x1BC00604000F4 + 0xF5, 0x00, 0x60, 0x60, 0x00, 0xBC, 0x01, 0x00, // 0x1BC00606000F5 + 0x4D, 0x01, 0x80, 0x60, 0x00, 0xBC, 0x01, 0x00, // 0x1BC006080014D + 0x4F, 0x01, 0xC0, 0x60, 0x00, 0xBC, 0x01, 0x00, // 0x1BC0060C0014F + 0x2F, 0x02, 0xE0, 0x60, 0x00, 0xBC, 0x01, 0x00, // 0x1BC0060E0022F + 0xF6, 0x00, 0x00, 0x61, 0x00, 0xBC, 0x01, 0x00, // 0x1BC00610000F6 + 0xCF, 0x1E, 0x20, 0x61, 0x00, 0xBC, 0x01, 0x00, // 0x1BC0061201ECF + 0x51, 0x01, 0x60, 0x61, 0x00, 0xBC, 0x01, 0x00, // 0x1BC0061600151 + 0xD2, 0x01, 0x80, 0x61, 0x00, 0xBC, 0x01, 0x00, // 0x1BC00618001D2 + 0x0D, 0x02, 0xE0, 0x61, 0x00, 0xBC, 0x01, 0x00, // 0x1BC0061E0020D + 0x0F, 0x02, 0x20, 0x62, 0x00, 0xBC, 0x01, 0x00, // 0x1BC006220020F + 0xA1, 0x01, 0x60, 0x63, 0x00, 0xBC, 0x01, 0x00, // 0x1BC00636001A1 + 0xCD, 0x1E, 0x60, 0x64, 0x00, 0xBC, 0x01, 0x00, // 0x1BC0064601ECD + 0xEB, 0x01, 0x00, 0x65, 0x00, 0xBC, 0x01, 0x00, // 0x1BC00650001EB + 0x55, 0x1E, 0x20, 0x60, 0x00, 0xC0, 0x01, 0x00, // 0x1C00060201E55 + 0x57, 0x1E, 0xE0, 0x60, 0x00, 0xC0, 0x01, 0x00, // 0x1C00060E01E57 + 0x55, 0x01, 0x20, 0x60, 0x00, 0xC8, 0x01, 0x00, // 0x1C80060200155 + 0x59, 0x1E, 0xE0, 0x60, 0x00, 0xC8, 0x01, 0x00, // 0x1C80060E01E59 + 0x59, 0x01, 0x80, 0x61, 0x00, 0xC8, 0x01, 0x00, // 0x1C80061800159 + 0x11, 0x02, 0xE0, 0x61, 0x00, 0xC8, 0x01, 0x00, // 0x1C80061E00211 + 0x13, 0x02, 0x20, 0x62, 0x00, 0xC8, 0x01, 0x00, // 0x1C80062200213 + 0x5B, 0x1E, 0x60, 0x64, 0x00, 0xC8, 0x01, 0x00, // 0x1C80064601E5B + 0x57, 0x01, 0xE0, 0x64, 0x00, 0xC8, 0x01, 0x00, // 0x1C80064E00157 + 0x5F, 0x1E, 0x20, 0x66, 0x00, 0xC8, 0x01, 0x00, // 0x1C80066201E5F + 0x5B, 0x01, 0x20, 0x60, 0x00, 0xCC, 0x01, 0x00, // 0x1CC006020015B + 0x5D, 0x01, 0x40, 0x60, 0x00, 0xCC, 0x01, 0x00, // 0x1CC006040015D + 0x61, 0x1E, 0xE0, 0x60, 0x00, 0xCC, 0x01, 0x00, // 0x1CC0060E01E61 + 0x61, 0x01, 0x80, 0x61, 0x00, 0xCC, 0x01, 0x00, // 0x1CC0061800161 + 0x63, 0x1E, 0x60, 0x64, 0x00, 0xCC, 0x01, 0x00, // 0x1CC0064601E63 + 0x19, 0x02, 0xC0, 0x64, 0x00, 0xCC, 0x01, 0x00, // 0x1CC0064C00219 + 0x5F, 0x01, 0xE0, 0x64, 0x00, 0xCC, 0x01, 0x00, // 0x1CC0064E0015F + 0x6B, 0x1E, 0xE0, 0x60, 0x00, 0xD0, 0x01, 0x00, // 0x1D00060E01E6B + 0x97, 0x1E, 0x00, 0x61, 0x00, 0xD0, 0x01, 0x00, // 0x1D00061001E97 + 0x65, 0x01, 0x80, 0x61, 0x00, 0xD0, 0x01, 0x00, // 0x1D00061800165 + 0x6D, 0x1E, 0x60, 0x64, 0x00, 0xD0, 0x01, 0x00, // 0x1D00064601E6D + 0x1B, 0x02, 0xC0, 0x64, 0x00, 0xD0, 0x01, 0x00, // 0x1D00064C0021B + 0x63, 0x01, 0xE0, 0x64, 0x00, 0xD0, 0x01, 0x00, // 0x1D00064E00163 + 0x71, 0x1E, 0xA0, 0x65, 0x00, 0xD0, 0x01, 0x00, // 0x1D00065A01E71 + 0x6F, 0x1E, 0x20, 0x66, 0x00, 0xD0, 0x01, 0x00, // 0x1D00066201E6F + 0xF9, 0x00, 0x00, 0x60, 0x00, 0xD4, 0x01, 0x00, // 0x1D400600000F9 + 0xFA, 0x00, 0x20, 0x60, 0x00, 0xD4, 0x01, 0x00, // 0x1D400602000FA + 0xFB, 0x00, 0x40, 0x60, 0x00, 0xD4, 0x01, 0x00, // 0x1D400604000FB + 0x69, 0x01, 0x60, 0x60, 0x00, 0xD4, 0x01, 0x00, // 0x1D40060600169 + 0x6B, 0x01, 0x80, 0x60, 0x00, 0xD4, 0x01, 0x00, // 0x1D4006080016B + 0x6D, 0x01, 0xC0, 0x60, 0x00, 0xD4, 0x01, 0x00, // 0x1D40060C0016D + 0xFC, 0x00, 0x00, 0x61, 0x00, 0xD4, 0x01, 0x00, // 0x1D400610000FC + 0xE7, 0x1E, 0x20, 0x61, 0x00, 0xD4, 0x01, 0x00, // 0x1D40061201EE7 + 0x6F, 0x01, 0x40, 0x61, 0x00, 0xD4, 0x01, 0x00, // 0x1D4006140016F + 0x71, 0x01, 0x60, 0x61, 0x00, 0xD4, 0x01, 0x00, // 0x1D40061600171 + 0xD4, 0x01, 0x80, 0x61, 0x00, 0xD4, 0x01, 0x00, // 0x1D400618001D4 + 0x15, 0x02, 0xE0, 0x61, 0x00, 0xD4, 0x01, 0x00, // 0x1D40061E00215 + 0x17, 0x02, 0x20, 0x62, 0x00, 0xD4, 0x01, 0x00, // 0x1D40062200217 + 0xB0, 0x01, 0x60, 0x63, 0x00, 0xD4, 0x01, 0x00, // 0x1D400636001B0 + 0xE5, 0x1E, 0x60, 0x64, 0x00, 0xD4, 0x01, 0x00, // 0x1D40064601EE5 + 0x73, 0x1E, 0x80, 0x64, 0x00, 0xD4, 0x01, 0x00, // 0x1D40064801E73 + 0x73, 0x01, 0x00, 0x65, 0x00, 0xD4, 0x01, 0x00, // 0x1D40065000173 + 0x77, 0x1E, 0xA0, 0x65, 0x00, 0xD4, 0x01, 0x00, // 0x1D40065A01E77 + 0x75, 0x1E, 0x00, 0x66, 0x00, 0xD4, 0x01, 0x00, // 0x1D40066001E75 + 0x7D, 0x1E, 0x60, 0x60, 0x00, 0xD8, 0x01, 0x00, // 0x1D80060601E7D + 0x7F, 0x1E, 0x60, 0x64, 0x00, 0xD8, 0x01, 0x00, // 0x1D80064601E7F + 0x81, 0x1E, 0x00, 0x60, 0x00, 0xDC, 0x01, 0x00, // 0x1DC0060001E81 + 0x83, 0x1E, 0x20, 0x60, 0x00, 0xDC, 0x01, 0x00, // 0x1DC0060201E83 + 0x75, 0x01, 0x40, 0x60, 0x00, 0xDC, 0x01, 0x00, // 0x1DC0060400175 + 0x87, 0x1E, 0xE0, 0x60, 0x00, 0xDC, 0x01, 0x00, // 0x1DC0060E01E87 + 0x85, 0x1E, 0x00, 0x61, 0x00, 0xDC, 0x01, 0x00, // 0x1DC0061001E85 + 0x98, 0x1E, 0x40, 0x61, 0x00, 0xDC, 0x01, 0x00, // 0x1DC0061401E98 + 0x89, 0x1E, 0x60, 0x64, 0x00, 0xDC, 0x01, 0x00, // 0x1DC0064601E89 + 0x8B, 0x1E, 0xE0, 0x60, 0x00, 0xE0, 0x01, 0x00, // 0x1E00060E01E8B + 0x8D, 0x1E, 0x00, 0x61, 0x00, 0xE0, 0x01, 0x00, // 0x1E00061001E8D + 0xF3, 0x1E, 0x00, 0x60, 0x00, 0xE4, 0x01, 0x00, // 0x1E40060001EF3 + 0xFD, 0x00, 0x20, 0x60, 0x00, 0xE4, 0x01, 0x00, // 0x1E400602000FD + 0x77, 0x01, 0x40, 0x60, 0x00, 0xE4, 0x01, 0x00, // 0x1E40060400177 + 0xF9, 0x1E, 0x60, 0x60, 0x00, 0xE4, 0x01, 0x00, // 0x1E40060601EF9 + 0x33, 0x02, 0x80, 0x60, 0x00, 0xE4, 0x01, 0x00, // 0x1E40060800233 + 0x8F, 0x1E, 0xE0, 0x60, 0x00, 0xE4, 0x01, 0x00, // 0x1E40060E01E8F + 0xFF, 0x00, 0x00, 0x61, 0x00, 0xE4, 0x01, 0x00, // 0x1E400610000FF + 0xF7, 0x1E, 0x20, 0x61, 0x00, 0xE4, 0x01, 0x00, // 0x1E40061201EF7 + 0x99, 0x1E, 0x40, 0x61, 0x00, 0xE4, 0x01, 0x00, // 0x1E40061401E99 + 0xF5, 0x1E, 0x60, 0x64, 0x00, 0xE4, 0x01, 0x00, // 0x1E40064601EF5 + 0x7A, 0x01, 0x20, 0x60, 0x00, 0xE8, 0x01, 0x00, // 0x1E8006020017A + 0x91, 0x1E, 0x40, 0x60, 0x00, 0xE8, 0x01, 0x00, // 0x1E80060401E91 + 0x7C, 0x01, 0xE0, 0x60, 0x00, 0xE8, 0x01, 0x00, // 0x1E80060E0017C + 0x7E, 0x01, 0x80, 0x61, 0x00, 0xE8, 0x01, 0x00, // 0x1E8006180017E + 0x93, 0x1E, 0x60, 0x64, 0x00, 0xE8, 0x01, 0x00, // 0x1E80064601E93 + 0x95, 0x1E, 0x20, 0x66, 0x00, 0xE8, 0x01, 0x00, // 0x1E80066201E95 + 0xED, 0x1F, 0x00, 0x60, 0x00, 0xA0, 0x02, 0x00, // 0x2A00060001FED + 0x85, 0x03, 0x20, 0x60, 0x00, 0xA0, 0x02, 0x00, // 0x2A00060200385 + 0xC1, 0x1F, 0x40, 0x68, 0x00, 0xA0, 0x02, 0x00, // 0x2A00068401FC1 + 0xA6, 0x1E, 0x00, 0x60, 0x00, 0x08, 0x03, 0x00, // 0x3080060001EA6 + 0xA4, 0x1E, 0x20, 0x60, 0x00, 0x08, 0x03, 0x00, // 0x3080060201EA4 + 0xAA, 0x1E, 0x60, 0x60, 0x00, 0x08, 0x03, 0x00, // 0x3080060601EAA + 0xA8, 0x1E, 0x20, 0x61, 0x00, 0x08, 0x03, 0x00, // 0x3080061201EA8 + 0xDE, 0x01, 0x80, 0x60, 0x00, 0x10, 0x03, 0x00, // 0x31000608001DE + 0xFA, 0x01, 0x20, 0x60, 0x00, 0x14, 0x03, 0x00, // 0x31400602001FA + 0xFC, 0x01, 0x20, 0x60, 0x00, 0x18, 0x03, 0x00, // 0x31800602001FC + 0xE2, 0x01, 0x80, 0x60, 0x00, 0x18, 0x03, 0x00, // 0x31800608001E2 + 0x08, 0x1E, 0x20, 0x60, 0x00, 0x1C, 0x03, 0x00, // 0x31C0060201E08 + 0xC0, 0x1E, 0x00, 0x60, 0x00, 0x28, 0x03, 0x00, // 0x3280060001EC0 + 0xBE, 0x1E, 0x20, 0x60, 0x00, 0x28, 0x03, 0x00, // 0x3280060201EBE + 0xC4, 0x1E, 0x60, 0x60, 0x00, 0x28, 0x03, 0x00, // 0x3280060601EC4 + 0xC2, 0x1E, 0x20, 0x61, 0x00, 0x28, 0x03, 0x00, // 0x3280061201EC2 + 0x2E, 0x1E, 0x20, 0x60, 0x00, 0x3C, 0x03, 0x00, // 0x33C0060201E2E + 0xD2, 0x1E, 0x00, 0x60, 0x00, 0x50, 0x03, 0x00, // 0x3500060001ED2 + 0xD0, 0x1E, 0x20, 0x60, 0x00, 0x50, 0x03, 0x00, // 0x3500060201ED0 + 0xD6, 0x1E, 0x60, 0x60, 0x00, 0x50, 0x03, 0x00, // 0x3500060601ED6 + 0xD4, 0x1E, 0x20, 0x61, 0x00, 0x50, 0x03, 0x00, // 0x3500061201ED4 + 0x4C, 0x1E, 0x20, 0x60, 0x00, 0x54, 0x03, 0x00, // 0x3540060201E4C + 0x2C, 0x02, 0x80, 0x60, 0x00, 0x54, 0x03, 0x00, // 0x354006080022C + 0x4E, 0x1E, 0x00, 0x61, 0x00, 0x54, 0x03, 0x00, // 0x3540061001E4E + 0x2A, 0x02, 0x80, 0x60, 0x00, 0x58, 0x03, 0x00, // 0x358006080022A + 0xFE, 0x01, 0x20, 0x60, 0x00, 0x60, 0x03, 0x00, // 0x36000602001FE + 0xDB, 0x01, 0x00, 0x60, 0x00, 0x70, 0x03, 0x00, // 0x37000600001DB + 0xD7, 0x01, 0x20, 0x60, 0x00, 0x70, 0x03, 0x00, // 0x37000602001D7 + 0xD5, 0x01, 0x80, 0x60, 0x00, 0x70, 0x03, 0x00, // 0x37000608001D5 + 0xD9, 0x01, 0x80, 0x61, 0x00, 0x70, 0x03, 0x00, // 0x37000618001D9 + 0xA7, 0x1E, 0x00, 0x60, 0x00, 0x88, 0x03, 0x00, // 0x3880060001EA7 + 0xA5, 0x1E, 0x20, 0x60, 0x00, 0x88, 0x03, 0x00, // 0x3880060201EA5 + 0xAB, 0x1E, 0x60, 0x60, 0x00, 0x88, 0x03, 0x00, // 0x3880060601EAB + 0xA9, 0x1E, 0x20, 0x61, 0x00, 0x88, 0x03, 0x00, // 0x3880061201EA9 + 0xDF, 0x01, 0x80, 0x60, 0x00, 0x90, 0x03, 0x00, // 0x39000608001DF + 0xFB, 0x01, 0x20, 0x60, 0x00, 0x94, 0x03, 0x00, // 0x39400602001FB + 0xFD, 0x01, 0x20, 0x60, 0x00, 0x98, 0x03, 0x00, // 0x39800602001FD + 0xE3, 0x01, 0x80, 0x60, 0x00, 0x98, 0x03, 0x00, // 0x39800608001E3 + 0x09, 0x1E, 0x20, 0x60, 0x00, 0x9C, 0x03, 0x00, // 0x39C0060201E09 + 0xC1, 0x1E, 0x00, 0x60, 0x00, 0xA8, 0x03, 0x00, // 0x3A80060001EC1 + 0xBF, 0x1E, 0x20, 0x60, 0x00, 0xA8, 0x03, 0x00, // 0x3A80060201EBF + 0xC5, 0x1E, 0x60, 0x60, 0x00, 0xA8, 0x03, 0x00, // 0x3A80060601EC5 + 0xC3, 0x1E, 0x20, 0x61, 0x00, 0xA8, 0x03, 0x00, // 0x3A80061201EC3 + 0x2F, 0x1E, 0x20, 0x60, 0x00, 0xBC, 0x03, 0x00, // 0x3BC0060201E2F + 0xD3, 0x1E, 0x00, 0x60, 0x00, 0xD0, 0x03, 0x00, // 0x3D00060001ED3 + 0xD1, 0x1E, 0x20, 0x60, 0x00, 0xD0, 0x03, 0x00, // 0x3D00060201ED1 + 0xD7, 0x1E, 0x60, 0x60, 0x00, 0xD0, 0x03, 0x00, // 0x3D00060601ED7 + 0xD5, 0x1E, 0x20, 0x61, 0x00, 0xD0, 0x03, 0x00, // 0x3D00061201ED5 + 0x4D, 0x1E, 0x20, 0x60, 0x00, 0xD4, 0x03, 0x00, // 0x3D40060201E4D + 0x2D, 0x02, 0x80, 0x60, 0x00, 0xD4, 0x03, 0x00, // 0x3D4006080022D + 0x4F, 0x1E, 0x00, 0x61, 0x00, 0xD4, 0x03, 0x00, // 0x3D40061001E4F + 0x2B, 0x02, 0x80, 0x60, 0x00, 0xD8, 0x03, 0x00, // 0x3D8006080022B + 0xFF, 0x01, 0x20, 0x60, 0x00, 0xE0, 0x03, 0x00, // 0x3E000602001FF + 0xDC, 0x01, 0x00, 0x60, 0x00, 0xF0, 0x03, 0x00, // 0x3F000600001DC + 0xD8, 0x01, 0x20, 0x60, 0x00, 0xF0, 0x03, 0x00, // 0x3F000602001D8 + 0xD6, 0x01, 0x80, 0x60, 0x00, 0xF0, 0x03, 0x00, // 0x3F000608001D6 + 0xDA, 0x01, 0x80, 0x61, 0x00, 0xF0, 0x03, 0x00, // 0x3F000618001DA + 0xB0, 0x1E, 0x00, 0x60, 0x00, 0x08, 0x04, 0x00, // 0x4080060001EB0 + 0xAE, 0x1E, 0x20, 0x60, 0x00, 0x08, 0x04, 0x00, // 0x4080060201EAE + 0xB4, 0x1E, 0x60, 0x60, 0x00, 0x08, 0x04, 0x00, // 0x4080060601EB4 + 0xB2, 0x1E, 0x20, 0x61, 0x00, 0x08, 0x04, 0x00, // 0x4080061201EB2 + 0xB1, 0x1E, 0x00, 0x60, 0x00, 0x0C, 0x04, 0x00, // 0x40C0060001EB1 + 0xAF, 0x1E, 0x20, 0x60, 0x00, 0x0C, 0x04, 0x00, // 0x40C0060201EAF + 0xB5, 0x1E, 0x60, 0x60, 0x00, 0x0C, 0x04, 0x00, // 0x40C0060601EB5 + 0xB3, 0x1E, 0x20, 0x61, 0x00, 0x0C, 0x04, 0x00, // 0x40C0061201EB3 + 0x14, 0x1E, 0x00, 0x60, 0x00, 0x48, 0x04, 0x00, // 0x4480060001E14 + 0x16, 0x1E, 0x20, 0x60, 0x00, 0x48, 0x04, 0x00, // 0x4480060201E16 + 0x15, 0x1E, 0x00, 0x60, 0x00, 0x4C, 0x04, 0x00, // 0x44C0060001E15 + 0x17, 0x1E, 0x20, 0x60, 0x00, 0x4C, 0x04, 0x00, // 0x44C0060201E17 + 0x50, 0x1E, 0x00, 0x60, 0x00, 0x30, 0x05, 0x00, // 0x5300060001E50 + 0x52, 0x1E, 0x20, 0x60, 0x00, 0x30, 0x05, 0x00, // 0x5300060201E52 + 0x51, 0x1E, 0x00, 0x60, 0x00, 0x34, 0x05, 0x00, // 0x5340060001E51 + 0x53, 0x1E, 0x20, 0x60, 0x00, 0x34, 0x05, 0x00, // 0x5340060201E53 + 0x64, 0x1E, 0xE0, 0x60, 0x00, 0x68, 0x05, 0x00, // 0x5680060E01E64 + 0x65, 0x1E, 0xE0, 0x60, 0x00, 0x6C, 0x05, 0x00, // 0x56C0060E01E65 + 0x66, 0x1E, 0xE0, 0x60, 0x00, 0x80, 0x05, 0x00, // 0x5800060E01E66 + 0x67, 0x1E, 0xE0, 0x60, 0x00, 0x84, 0x05, 0x00, // 0x5840060E01E67 + 0x78, 0x1E, 0x20, 0x60, 0x00, 0xA0, 0x05, 0x00, // 0x5A00060201E78 + 0x79, 0x1E, 0x20, 0x60, 0x00, 0xA4, 0x05, 0x00, // 0x5A40060201E79 + 0x7A, 0x1E, 0x00, 0x61, 0x00, 0xA8, 0x05, 0x00, // 0x5A80061001E7A + 0x7B, 0x1E, 0x00, 0x61, 0x00, 0xAC, 0x05, 0x00, // 0x5AC0061001E7B + 0x9B, 0x1E, 0xE0, 0x60, 0x00, 0xFC, 0x05, 0x00, // 0x5FC0060E01E9B + 0xDC, 0x1E, 0x00, 0x60, 0x00, 0x80, 0x06, 0x00, // 0x6800060001EDC + 0xDA, 0x1E, 0x20, 0x60, 0x00, 0x80, 0x06, 0x00, // 0x6800060201EDA + 0xE0, 0x1E, 0x60, 0x60, 0x00, 0x80, 0x06, 0x00, // 0x6800060601EE0 + 0xDE, 0x1E, 0x20, 0x61, 0x00, 0x80, 0x06, 0x00, // 0x6800061201EDE + 0xE2, 0x1E, 0x60, 0x64, 0x00, 0x80, 0x06, 0x00, // 0x6800064601EE2 + 0xDD, 0x1E, 0x00, 0x60, 0x00, 0x84, 0x06, 0x00, // 0x6840060001EDD + 0xDB, 0x1E, 0x20, 0x60, 0x00, 0x84, 0x06, 0x00, // 0x6840060201EDB + 0xE1, 0x1E, 0x60, 0x60, 0x00, 0x84, 0x06, 0x00, // 0x6840060601EE1 + 0xDF, 0x1E, 0x20, 0x61, 0x00, 0x84, 0x06, 0x00, // 0x6840061201EDF + 0xE3, 0x1E, 0x60, 0x64, 0x00, 0x84, 0x06, 0x00, // 0x6840064601EE3 + 0xEA, 0x1E, 0x00, 0x60, 0x00, 0xBC, 0x06, 0x00, // 0x6BC0060001EEA + 0xE8, 0x1E, 0x20, 0x60, 0x00, 0xBC, 0x06, 0x00, // 0x6BC0060201EE8 + 0xEE, 0x1E, 0x60, 0x60, 0x00, 0xBC, 0x06, 0x00, // 0x6BC0060601EEE + 0xEC, 0x1E, 0x20, 0x61, 0x00, 0xBC, 0x06, 0x00, // 0x6BC0061201EEC + 0xF0, 0x1E, 0x60, 0x64, 0x00, 0xBC, 0x06, 0x00, // 0x6BC0064601EF0 + 0xEB, 0x1E, 0x00, 0x60, 0x00, 0xC0, 0x06, 0x00, // 0x6C00060001EEB + 0xE9, 0x1E, 0x20, 0x60, 0x00, 0xC0, 0x06, 0x00, // 0x6C00060201EE9 + 0xEF, 0x1E, 0x60, 0x60, 0x00, 0xC0, 0x06, 0x00, // 0x6C00060601EEF + 0xED, 0x1E, 0x20, 0x61, 0x00, 0xC0, 0x06, 0x00, // 0x6C00061201EED + 0xF1, 0x1E, 0x60, 0x64, 0x00, 0xC0, 0x06, 0x00, // 0x6C00064601EF1 + 0xEE, 0x01, 0x80, 0x61, 0x00, 0xDC, 0x06, 0x00, // 0x6DC00618001EE + 0xEC, 0x01, 0x80, 0x60, 0x00, 0xA8, 0x07, 0x00, // 0x7A800608001EC + 0xED, 0x01, 0x80, 0x60, 0x00, 0xAC, 0x07, 0x00, // 0x7AC00608001ED + 0xE0, 0x01, 0x80, 0x60, 0x00, 0x98, 0x08, 0x00, // 0x89800608001E0 + 0xE1, 0x01, 0x80, 0x60, 0x00, 0x9C, 0x08, 0x00, // 0x89C00608001E1 + 0x1C, 0x1E, 0xC0, 0x60, 0x00, 0xA0, 0x08, 0x00, // 0x8A00060C01E1C + 0x1D, 0x1E, 0xC0, 0x60, 0x00, 0xA4, 0x08, 0x00, // 0x8A40060C01E1D + 0x30, 0x02, 0x80, 0x60, 0x00, 0xB8, 0x08, 0x00, // 0x8B80060800230 + 0x31, 0x02, 0x80, 0x60, 0x00, 0xBC, 0x08, 0x00, // 0x8BC0060800231 + 0xEF, 0x01, 0x80, 0x61, 0x00, 0x48, 0x0A, 0x00, // 0xA4800618001EF + 0xBA, 0x1F, 0x00, 0x60, 0x00, 0x44, 0x0E, 0x00, // 0xE440060001FBA + 0x86, 0x03, 0x20, 0x60, 0x00, 0x44, 0x0E, 0x00, // 0xE440060200386 + 0xB9, 0x1F, 0x80, 0x60, 0x00, 0x44, 0x0E, 0x00, // 0xE440060801FB9 + 0xB8, 0x1F, 0xC0, 0x60, 0x00, 0x44, 0x0E, 0x00, // 0xE440060C01FB8 + 0x08, 0x1F, 0x60, 0x62, 0x00, 0x44, 0x0E, 0x00, // 0xE440062601F08 + 0x09, 0x1F, 0x80, 0x62, 0x00, 0x44, 0x0E, 0x00, // 0xE440062801F09 + 0xBC, 0x1F, 0xA0, 0x68, 0x00, 0x44, 0x0E, 0x00, // 0xE440068A01FBC + 0xC8, 0x1F, 0x00, 0x60, 0x00, 0x54, 0x0E, 0x00, // 0xE540060001FC8 + 0x88, 0x03, 0x20, 0x60, 0x00, 0x54, 0x0E, 0x00, // 0xE540060200388 + 0x18, 0x1F, 0x60, 0x62, 0x00, 0x54, 0x0E, 0x00, // 0xE540062601F18 + 0x19, 0x1F, 0x80, 0x62, 0x00, 0x54, 0x0E, 0x00, // 0xE540062801F19 + 0xCA, 0x1F, 0x00, 0x60, 0x00, 0x5C, 0x0E, 0x00, // 0xE5C0060001FCA + 0x89, 0x03, 0x20, 0x60, 0x00, 0x5C, 0x0E, 0x00, // 0xE5C0060200389 + 0x28, 0x1F, 0x60, 0x62, 0x00, 0x5C, 0x0E, 0x00, // 0xE5C0062601F28 + 0x29, 0x1F, 0x80, 0x62, 0x00, 0x5C, 0x0E, 0x00, // 0xE5C0062801F29 + 0xCC, 0x1F, 0xA0, 0x68, 0x00, 0x5C, 0x0E, 0x00, // 0xE5C0068A01FCC + 0xDA, 0x1F, 0x00, 0x60, 0x00, 0x64, 0x0E, 0x00, // 0xE640060001FDA + 0x8A, 0x03, 0x20, 0x60, 0x00, 0x64, 0x0E, 0x00, // 0xE64006020038A + 0xD9, 0x1F, 0x80, 0x60, 0x00, 0x64, 0x0E, 0x00, // 0xE640060801FD9 + 0xD8, 0x1F, 0xC0, 0x60, 0x00, 0x64, 0x0E, 0x00, // 0xE640060C01FD8 + 0xAA, 0x03, 0x00, 0x61, 0x00, 0x64, 0x0E, 0x00, // 0xE6400610003AA + 0x38, 0x1F, 0x60, 0x62, 0x00, 0x64, 0x0E, 0x00, // 0xE640062601F38 + 0x39, 0x1F, 0x80, 0x62, 0x00, 0x64, 0x0E, 0x00, // 0xE640062801F39 + 0xF8, 0x1F, 0x00, 0x60, 0x00, 0x7C, 0x0E, 0x00, // 0xE7C0060001FF8 + 0x8C, 0x03, 0x20, 0x60, 0x00, 0x7C, 0x0E, 0x00, // 0xE7C006020038C + 0x48, 0x1F, 0x60, 0x62, 0x00, 0x7C, 0x0E, 0x00, // 0xE7C0062601F48 + 0x49, 0x1F, 0x80, 0x62, 0x00, 0x7C, 0x0E, 0x00, // 0xE7C0062801F49 + 0xEC, 0x1F, 0x80, 0x62, 0x00, 0x84, 0x0E, 0x00, // 0xE840062801FEC + 0xEA, 0x1F, 0x00, 0x60, 0x00, 0x94, 0x0E, 0x00, // 0xE940060001FEA + 0x8E, 0x03, 0x20, 0x60, 0x00, 0x94, 0x0E, 0x00, // 0xE94006020038E + 0xE9, 0x1F, 0x80, 0x60, 0x00, 0x94, 0x0E, 0x00, // 0xE940060801FE9 + 0xE8, 0x1F, 0xC0, 0x60, 0x00, 0x94, 0x0E, 0x00, // 0xE940060C01FE8 + 0xAB, 0x03, 0x00, 0x61, 0x00, 0x94, 0x0E, 0x00, // 0xE9400610003AB + 0x59, 0x1F, 0x80, 0x62, 0x00, 0x94, 0x0E, 0x00, // 0xE940062801F59 + 0xFA, 0x1F, 0x00, 0x60, 0x00, 0xA4, 0x0E, 0x00, // 0xEA40060001FFA + 0x8F, 0x03, 0x20, 0x60, 0x00, 0xA4, 0x0E, 0x00, // 0xEA4006020038F + 0x68, 0x1F, 0x60, 0x62, 0x00, 0xA4, 0x0E, 0x00, // 0xEA40062601F68 + 0x69, 0x1F, 0x80, 0x62, 0x00, 0xA4, 0x0E, 0x00, // 0xEA40062801F69 + 0xFC, 0x1F, 0xA0, 0x68, 0x00, 0xA4, 0x0E, 0x00, // 0xEA40068A01FFC + 0xB4, 0x1F, 0xA0, 0x68, 0x00, 0xB0, 0x0E, 0x00, // 0xEB00068A01FB4 + 0xC4, 0x1F, 0xA0, 0x68, 0x00, 0xB8, 0x0E, 0x00, // 0xEB80068A01FC4 + 0x70, 0x1F, 0x00, 0x60, 0x00, 0xC4, 0x0E, 0x00, // 0xEC40060001F70 + 0xAC, 0x03, 0x20, 0x60, 0x00, 0xC4, 0x0E, 0x00, // 0xEC400602003AC + 0xB1, 0x1F, 0x80, 0x60, 0x00, 0xC4, 0x0E, 0x00, // 0xEC40060801FB1 + 0xB0, 0x1F, 0xC0, 0x60, 0x00, 0xC4, 0x0E, 0x00, // 0xEC40060C01FB0 + 0x00, 0x1F, 0x60, 0x62, 0x00, 0xC4, 0x0E, 0x00, // 0xEC40062601F00 + 0x01, 0x1F, 0x80, 0x62, 0x00, 0xC4, 0x0E, 0x00, // 0xEC40062801F01 + 0xB6, 0x1F, 0x40, 0x68, 0x00, 0xC4, 0x0E, 0x00, // 0xEC40068401FB6 + 0xB3, 0x1F, 0xA0, 0x68, 0x00, 0xC4, 0x0E, 0x00, // 0xEC40068A01FB3 + 0x72, 0x1F, 0x00, 0x60, 0x00, 0xD4, 0x0E, 0x00, // 0xED40060001F72 + 0xAD, 0x03, 0x20, 0x60, 0x00, 0xD4, 0x0E, 0x00, // 0xED400602003AD + 0x10, 0x1F, 0x60, 0x62, 0x00, 0xD4, 0x0E, 0x00, // 0xED40062601F10 + 0x11, 0x1F, 0x80, 0x62, 0x00, 0xD4, 0x0E, 0x00, // 0xED40062801F11 + 0x74, 0x1F, 0x00, 0x60, 0x00, 0xDC, 0x0E, 0x00, // 0xEDC0060001F74 + 0xAE, 0x03, 0x20, 0x60, 0x00, 0xDC, 0x0E, 0x00, // 0xEDC00602003AE + 0x20, 0x1F, 0x60, 0x62, 0x00, 0xDC, 0x0E, 0x00, // 0xEDC0062601F20 + 0x21, 0x1F, 0x80, 0x62, 0x00, 0xDC, 0x0E, 0x00, // 0xEDC0062801F21 + 0xC6, 0x1F, 0x40, 0x68, 0x00, 0xDC, 0x0E, 0x00, // 0xEDC0068401FC6 + 0xC3, 0x1F, 0xA0, 0x68, 0x00, 0xDC, 0x0E, 0x00, // 0xEDC0068A01FC3 + 0x76, 0x1F, 0x00, 0x60, 0x00, 0xE4, 0x0E, 0x00, // 0xEE40060001F76 + 0xAF, 0x03, 0x20, 0x60, 0x00, 0xE4, 0x0E, 0x00, // 0xEE400602003AF + 0xD1, 0x1F, 0x80, 0x60, 0x00, 0xE4, 0x0E, 0x00, // 0xEE40060801FD1 + 0xD0, 0x1F, 0xC0, 0x60, 0x00, 0xE4, 0x0E, 0x00, // 0xEE40060C01FD0 + 0xCA, 0x03, 0x00, 0x61, 0x00, 0xE4, 0x0E, 0x00, // 0xEE400610003CA + 0x30, 0x1F, 0x60, 0x62, 0x00, 0xE4, 0x0E, 0x00, // 0xEE40062601F30 + 0x31, 0x1F, 0x80, 0x62, 0x00, 0xE4, 0x0E, 0x00, // 0xEE40062801F31 + 0xD6, 0x1F, 0x40, 0x68, 0x00, 0xE4, 0x0E, 0x00, // 0xEE40068401FD6 + 0x78, 0x1F, 0x00, 0x60, 0x00, 0xFC, 0x0E, 0x00, // 0xEFC0060001F78 + 0xCC, 0x03, 0x20, 0x60, 0x00, 0xFC, 0x0E, 0x00, // 0xEFC00602003CC + 0x40, 0x1F, 0x60, 0x62, 0x00, 0xFC, 0x0E, 0x00, // 0xEFC0062601F40 + 0x41, 0x1F, 0x80, 0x62, 0x00, 0xFC, 0x0E, 0x00, // 0xEFC0062801F41 + 0xE4, 0x1F, 0x60, 0x62, 0x00, 0x04, 0x0F, 0x00, // 0xF040062601FE4 + 0xE5, 0x1F, 0x80, 0x62, 0x00, 0x04, 0x0F, 0x00, // 0xF040062801FE5 + 0x7A, 0x1F, 0x00, 0x60, 0x00, 0x14, 0x0F, 0x00, // 0xF140060001F7A + 0xCD, 0x03, 0x20, 0x60, 0x00, 0x14, 0x0F, 0x00, // 0xF1400602003CD + 0xE1, 0x1F, 0x80, 0x60, 0x00, 0x14, 0x0F, 0x00, // 0xF140060801FE1 + 0xE0, 0x1F, 0xC0, 0x60, 0x00, 0x14, 0x0F, 0x00, // 0xF140060C01FE0 + 0xCB, 0x03, 0x00, 0x61, 0x00, 0x14, 0x0F, 0x00, // 0xF1400610003CB + 0x50, 0x1F, 0x60, 0x62, 0x00, 0x14, 0x0F, 0x00, // 0xF140062601F50 + 0x51, 0x1F, 0x80, 0x62, 0x00, 0x14, 0x0F, 0x00, // 0xF140062801F51 + 0xE6, 0x1F, 0x40, 0x68, 0x00, 0x14, 0x0F, 0x00, // 0xF140068401FE6 + 0x7C, 0x1F, 0x00, 0x60, 0x00, 0x24, 0x0F, 0x00, // 0xF240060001F7C + 0xCE, 0x03, 0x20, 0x60, 0x00, 0x24, 0x0F, 0x00, // 0xF2400602003CE + 0x60, 0x1F, 0x60, 0x62, 0x00, 0x24, 0x0F, 0x00, // 0xF240062601F60 + 0x61, 0x1F, 0x80, 0x62, 0x00, 0x24, 0x0F, 0x00, // 0xF240062801F61 + 0xF6, 0x1F, 0x40, 0x68, 0x00, 0x24, 0x0F, 0x00, // 0xF240068401FF6 + 0xF3, 0x1F, 0xA0, 0x68, 0x00, 0x24, 0x0F, 0x00, // 0xF240068A01FF3 + 0xD2, 0x1F, 0x00, 0x60, 0x00, 0x28, 0x0F, 0x00, // 0xF280060001FD2 + 0x90, 0x03, 0x20, 0x60, 0x00, 0x28, 0x0F, 0x00, // 0xF280060200390 + 0xD7, 0x1F, 0x40, 0x68, 0x00, 0x28, 0x0F, 0x00, // 0xF280068401FD7 + 0xE2, 0x1F, 0x00, 0x60, 0x00, 0x2C, 0x0F, 0x00, // 0xF2C0060001FE2 + 0xB0, 0x03, 0x20, 0x60, 0x00, 0x2C, 0x0F, 0x00, // 0xF2C00602003B0 + 0xE7, 0x1F, 0x40, 0x68, 0x00, 0x2C, 0x0F, 0x00, // 0xF2C0068401FE7 + 0xF4, 0x1F, 0xA0, 0x68, 0x00, 0x38, 0x0F, 0x00, // 0xF380068A01FF4 + 0xD3, 0x03, 0x20, 0x60, 0x00, 0x48, 0x0F, 0x00, // 0xF4800602003D3 + 0xD4, 0x03, 0x00, 0x61, 0x00, 0x48, 0x0F, 0x00, // 0xF4800610003D4 + 0x07, 0x04, 0x00, 0x61, 0x00, 0x18, 0x10, 0x00, // 0x10180061000407 + 0xD0, 0x04, 0xC0, 0x60, 0x00, 0x40, 0x10, 0x00, // 0x10400060C004D0 + 0xD2, 0x04, 0x00, 0x61, 0x00, 0x40, 0x10, 0x00, // 0x104000610004D2 + 0x03, 0x04, 0x20, 0x60, 0x00, 0x4C, 0x10, 0x00, // 0x104C0060200403 + 0x00, 0x04, 0x00, 0x60, 0x00, 0x54, 0x10, 0x00, // 0x10540060000400 + 0xD6, 0x04, 0xC0, 0x60, 0x00, 0x54, 0x10, 0x00, // 0x10540060C004D6 + 0x01, 0x04, 0x00, 0x61, 0x00, 0x54, 0x10, 0x00, // 0x10540061000401 + 0xC1, 0x04, 0xC0, 0x60, 0x00, 0x58, 0x10, 0x00, // 0x10580060C004C1 + 0xDC, 0x04, 0x00, 0x61, 0x00, 0x58, 0x10, 0x00, // 0x105800610004DC + 0xDE, 0x04, 0x00, 0x61, 0x00, 0x5C, 0x10, 0x00, // 0x105C00610004DE + 0x0D, 0x04, 0x00, 0x60, 0x00, 0x60, 0x10, 0x00, // 0x1060006000040D + 0xE2, 0x04, 0x80, 0x60, 0x00, 0x60, 0x10, 0x00, // 0x106000608004E2 + 0x19, 0x04, 0xC0, 0x60, 0x00, 0x60, 0x10, 0x00, // 0x10600060C00419 + 0xE4, 0x04, 0x00, 0x61, 0x00, 0x60, 0x10, 0x00, // 0x106000610004E4 + 0x0C, 0x04, 0x20, 0x60, 0x00, 0x68, 0x10, 0x00, // 0x1068006020040C + 0xE6, 0x04, 0x00, 0x61, 0x00, 0x78, 0x10, 0x00, // 0x107800610004E6 + 0xEE, 0x04, 0x80, 0x60, 0x00, 0x8C, 0x10, 0x00, // 0x108C00608004EE + 0x0E, 0x04, 0xC0, 0x60, 0x00, 0x8C, 0x10, 0x00, // 0x108C0060C0040E + 0xF0, 0x04, 0x00, 0x61, 0x00, 0x8C, 0x10, 0x00, // 0x108C00610004F0 + 0xF2, 0x04, 0x60, 0x61, 0x00, 0x8C, 0x10, 0x00, // 0x108C00616004F2 + 0xF4, 0x04, 0x00, 0x61, 0x00, 0x9C, 0x10, 0x00, // 0x109C00610004F4 + 0xF8, 0x04, 0x00, 0x61, 0x00, 0xAC, 0x10, 0x00, // 0x10AC00610004F8 + 0xEC, 0x04, 0x00, 0x61, 0x00, 0xB4, 0x10, 0x00, // 0x10B400610004EC + 0xD1, 0x04, 0xC0, 0x60, 0x00, 0xC0, 0x10, 0x00, // 0x10C00060C004D1 + 0xD3, 0x04, 0x00, 0x61, 0x00, 0xC0, 0x10, 0x00, // 0x10C000610004D3 + 0x53, 0x04, 0x20, 0x60, 0x00, 0xCC, 0x10, 0x00, // 0x10CC0060200453 + 0x50, 0x04, 0x00, 0x60, 0x00, 0xD4, 0x10, 0x00, // 0x10D40060000450 + 0xD7, 0x04, 0xC0, 0x60, 0x00, 0xD4, 0x10, 0x00, // 0x10D40060C004D7 + 0x51, 0x04, 0x00, 0x61, 0x00, 0xD4, 0x10, 0x00, // 0x10D40061000451 + 0xC2, 0x04, 0xC0, 0x60, 0x00, 0xD8, 0x10, 0x00, // 0x10D80060C004C2 + 0xDD, 0x04, 0x00, 0x61, 0x00, 0xD8, 0x10, 0x00, // 0x10D800610004DD + 0xDF, 0x04, 0x00, 0x61, 0x00, 0xDC, 0x10, 0x00, // 0x10DC00610004DF + 0x5D, 0x04, 0x00, 0x60, 0x00, 0xE0, 0x10, 0x00, // 0x10E0006000045D + 0xE3, 0x04, 0x80, 0x60, 0x00, 0xE0, 0x10, 0x00, // 0x10E000608004E3 + 0x39, 0x04, 0xC0, 0x60, 0x00, 0xE0, 0x10, 0x00, // 0x10E00060C00439 + 0xE5, 0x04, 0x00, 0x61, 0x00, 0xE0, 0x10, 0x00, // 0x10E000610004E5 + 0x5C, 0x04, 0x20, 0x60, 0x00, 0xE8, 0x10, 0x00, // 0x10E8006020045C + 0xE7, 0x04, 0x00, 0x61, 0x00, 0xF8, 0x10, 0x00, // 0x10F800610004E7 + 0xEF, 0x04, 0x80, 0x60, 0x00, 0x0C, 0x11, 0x00, // 0x110C00608004EF + 0x5E, 0x04, 0xC0, 0x60, 0x00, 0x0C, 0x11, 0x00, // 0x110C0060C0045E + 0xF1, 0x04, 0x00, 0x61, 0x00, 0x0C, 0x11, 0x00, // 0x110C00610004F1 + 0xF3, 0x04, 0x60, 0x61, 0x00, 0x0C, 0x11, 0x00, // 0x110C00616004F3 + 0xF5, 0x04, 0x00, 0x61, 0x00, 0x1C, 0x11, 0x00, // 0x111C00610004F5 + 0xF9, 0x04, 0x00, 0x61, 0x00, 0x2C, 0x11, 0x00, // 0x112C00610004F9 + 0xED, 0x04, 0x00, 0x61, 0x00, 0x34, 0x11, 0x00, // 0x113400610004ED + 0x57, 0x04, 0x00, 0x61, 0x00, 0x58, 0x11, 0x00, // 0x11580061000457 + 0x76, 0x04, 0xE0, 0x61, 0x00, 0xD0, 0x11, 0x00, // 0x11D00061E00476 + 0x77, 0x04, 0xE0, 0x61, 0x00, 0xD4, 0x11, 0x00, // 0x11D40061E00477 + 0xDA, 0x04, 0x00, 0x61, 0x00, 0x60, 0x13, 0x00, // 0x136000610004DA + 0xDB, 0x04, 0x00, 0x61, 0x00, 0x64, 0x13, 0x00, // 0x136400610004DB + 0xEA, 0x04, 0x00, 0x61, 0x00, 0xA0, 0x13, 0x00, // 0x13A000610004EA + 0xEB, 0x04, 0x00, 0x61, 0x00, 0xA4, 0x13, 0x00, // 0x13A400610004EB + 0x22, 0x06, 0x60, 0xCA, 0x00, 0x9C, 0x18, 0x00, // 0x189C00CA600622 + 0x23, 0x06, 0x80, 0xCA, 0x00, 0x9C, 0x18, 0x00, // 0x189C00CA800623 + 0x25, 0x06, 0xA0, 0xCA, 0x00, 0x9C, 0x18, 0x00, // 0x189C00CAA00625 + 0x24, 0x06, 0x80, 0xCA, 0x00, 0x20, 0x19, 0x00, // 0x192000CA800624 + 0x26, 0x06, 0x80, 0xCA, 0x00, 0x28, 0x19, 0x00, // 0x192800CA800626 + 0xC2, 0x06, 0x80, 0xCA, 0x00, 0x04, 0x1B, 0x00, // 0x1B0400CA8006C2 + 0xD3, 0x06, 0x80, 0xCA, 0x00, 0x48, 0x1B, 0x00, // 0x1B4800CA8006D3 + 0xC0, 0x06, 0x80, 0xCA, 0x00, 0x54, 0x1B, 0x00, // 0x1B5400CA8006C0 + 0x29, 0x09, 0x80, 0x27, 0x01, 0xA0, 0x24, 0x00, // 0x24A00127800929 + 0x31, 0x09, 0x80, 0x27, 0x01, 0xC0, 0x24, 0x00, // 0x24C00127800931 + 0x34, 0x09, 0x80, 0x27, 0x01, 0xCC, 0x24, 0x00, // 0x24CC0127800934 + 0xCB, 0x09, 0xC0, 0x37, 0x01, 0x1C, 0x27, 0x00, // 0x271C0137C009CB + 0xCC, 0x09, 0xE0, 0x3A, 0x01, 0x1C, 0x27, 0x00, // 0x271C013AE009CC + 0x4B, 0x0B, 0xC0, 0x67, 0x01, 0x1C, 0x2D, 0x00, // 0x2D1C0167C00B4B + 0x48, 0x0B, 0xC0, 0x6A, 0x01, 0x1C, 0x2D, 0x00, // 0x2D1C016AC00B48 + 0x4C, 0x0B, 0xE0, 0x6A, 0x01, 0x1C, 0x2D, 0x00, // 0x2D1C016AE00B4C + 0x94, 0x0B, 0xE0, 0x7A, 0x01, 0x48, 0x2E, 0x00, // 0x2E48017AE00B94 + 0xCA, 0x0B, 0xC0, 0x77, 0x01, 0x18, 0x2F, 0x00, // 0x2F180177C00BCA + 0xCC, 0x0B, 0xE0, 0x7A, 0x01, 0x18, 0x2F, 0x00, // 0x2F18017AE00BCC + 0xCB, 0x0B, 0xC0, 0x77, 0x01, 0x1C, 0x2F, 0x00, // 0x2F1C0177C00BCB + 0x48, 0x0C, 0xC0, 0x8A, 0x01, 0x18, 0x31, 0x00, // 0x3118018AC00C48 + 0xC0, 0x0C, 0xA0, 0x9A, 0x01, 0xFC, 0x32, 0x00, // 0x32FC019AA00CC0 + 0xCA, 0x0C, 0x40, 0x98, 0x01, 0x18, 0x33, 0x00, // 0x33180198400CCA + 0xC7, 0x0C, 0xA0, 0x9A, 0x01, 0x18, 0x33, 0x00, // 0x3318019AA00CC7 + 0xC8, 0x0C, 0xC0, 0x9A, 0x01, 0x18, 0x33, 0x00, // 0x3318019AC00CC8 + 0xCB, 0x0C, 0xA0, 0x9A, 0x01, 0x28, 0x33, 0x00, // 0x3328019AA00CCB + 0x4A, 0x0D, 0xC0, 0xA7, 0x01, 0x18, 0x35, 0x00, // 0x351801A7C00D4A + 0x4C, 0x0D, 0xE0, 0xAA, 0x01, 0x18, 0x35, 0x00, // 0x351801AAE00D4C + 0x4B, 0x0D, 0xC0, 0xA7, 0x01, 0x1C, 0x35, 0x00, // 0x351C01A7C00D4B + 0xDA, 0x0D, 0x40, 0xB9, 0x01, 0x64, 0x37, 0x00, // 0x376401B9400DDA + 0xDC, 0x0D, 0xE0, 0xB9, 0x01, 0x64, 0x37, 0x00, // 0x376401B9E00DDC + 0xDE, 0x0D, 0xE0, 0xBB, 0x01, 0x64, 0x37, 0x00, // 0x376401BBE00DDE + 0xDD, 0x0D, 0x40, 0xB9, 0x01, 0x70, 0x37, 0x00, // 0x377001B9400DDD + 0x26, 0x10, 0xC0, 0x05, 0x02, 0x94, 0x40, 0x00, // 0x40940205C01026 + 0x06, 0x1B, 0xA0, 0x66, 0x03, 0x14, 0x6C, 0x00, // 0x6C140366A01B06 + 0x08, 0x1B, 0xA0, 0x66, 0x03, 0x1C, 0x6C, 0x00, // 0x6C1C0366A01B08 + 0x0A, 0x1B, 0xA0, 0x66, 0x03, 0x24, 0x6C, 0x00, // 0x6C240366A01B0A + 0x0C, 0x1B, 0xA0, 0x66, 0x03, 0x2C, 0x6C, 0x00, // 0x6C2C0366A01B0C + 0x0E, 0x1B, 0xA0, 0x66, 0x03, 0x34, 0x6C, 0x00, // 0x6C340366A01B0E + 0x12, 0x1B, 0xA0, 0x66, 0x03, 0x44, 0x6C, 0x00, // 0x6C440366A01B12 + 0x3B, 0x1B, 0xA0, 0x66, 0x03, 0xE8, 0x6C, 0x00, // 0x6CE80366A01B3B + 0x3D, 0x1B, 0xA0, 0x66, 0x03, 0xF0, 0x6C, 0x00, // 0x6CF00366A01B3D + 0x40, 0x1B, 0xA0, 0x66, 0x03, 0xF8, 0x6C, 0x00, // 0x6CF80366A01B40 + 0x41, 0x1B, 0xA0, 0x66, 0x03, 0xFC, 0x6C, 0x00, // 0x6CFC0366A01B41 + 0x43, 0x1B, 0xA0, 0x66, 0x03, 0x08, 0x6D, 0x00, // 0x6D080366A01B43 + 0x38, 0x1E, 0x80, 0x60, 0x00, 0xD8, 0x78, 0x00, // 0x78D80060801E38 + 0x39, 0x1E, 0x80, 0x60, 0x00, 0xDC, 0x78, 0x00, // 0x78DC0060801E39 + 0x5C, 0x1E, 0x80, 0x60, 0x00, 0x68, 0x79, 0x00, // 0x79680060801E5C + 0x5D, 0x1E, 0x80, 0x60, 0x00, 0x6C, 0x79, 0x00, // 0x796C0060801E5D + 0x68, 0x1E, 0xE0, 0x60, 0x00, 0x88, 0x79, 0x00, // 0x79880060E01E68 + 0x69, 0x1E, 0xE0, 0x60, 0x00, 0x8C, 0x79, 0x00, // 0x798C0060E01E69 + 0xAC, 0x1E, 0x40, 0x60, 0x00, 0x80, 0x7A, 0x00, // 0x7A800060401EAC + 0xB6, 0x1E, 0xC0, 0x60, 0x00, 0x80, 0x7A, 0x00, // 0x7A800060C01EB6 + 0xAD, 0x1E, 0x40, 0x60, 0x00, 0x84, 0x7A, 0x00, // 0x7A840060401EAD + 0xB7, 0x1E, 0xC0, 0x60, 0x00, 0x84, 0x7A, 0x00, // 0x7A840060C01EB7 + 0xC6, 0x1E, 0x40, 0x60, 0x00, 0xE0, 0x7A, 0x00, // 0x7AE00060401EC6 + 0xC7, 0x1E, 0x40, 0x60, 0x00, 0xE4, 0x7A, 0x00, // 0x7AE40060401EC7 + 0xD8, 0x1E, 0x40, 0x60, 0x00, 0x30, 0x7B, 0x00, // 0x7B300060401ED8 + 0xD9, 0x1E, 0x40, 0x60, 0x00, 0x34, 0x7B, 0x00, // 0x7B340060401ED9 + 0x02, 0x1F, 0x00, 0x60, 0x00, 0x00, 0x7C, 0x00, // 0x7C000060001F02 + 0x04, 0x1F, 0x20, 0x60, 0x00, 0x00, 0x7C, 0x00, // 0x7C000060201F04 + 0x06, 0x1F, 0x40, 0x68, 0x00, 0x00, 0x7C, 0x00, // 0x7C000068401F06 + 0x80, 0x1F, 0xA0, 0x68, 0x00, 0x00, 0x7C, 0x00, // 0x7C000068A01F80 + 0x03, 0x1F, 0x00, 0x60, 0x00, 0x04, 0x7C, 0x00, // 0x7C040060001F03 + 0x05, 0x1F, 0x20, 0x60, 0x00, 0x04, 0x7C, 0x00, // 0x7C040060201F05 + 0x07, 0x1F, 0x40, 0x68, 0x00, 0x04, 0x7C, 0x00, // 0x7C040068401F07 + 0x81, 0x1F, 0xA0, 0x68, 0x00, 0x04, 0x7C, 0x00, // 0x7C040068A01F81 + 0x82, 0x1F, 0xA0, 0x68, 0x00, 0x08, 0x7C, 0x00, // 0x7C080068A01F82 + 0x83, 0x1F, 0xA0, 0x68, 0x00, 0x0C, 0x7C, 0x00, // 0x7C0C0068A01F83 + 0x84, 0x1F, 0xA0, 0x68, 0x00, 0x10, 0x7C, 0x00, // 0x7C100068A01F84 + 0x85, 0x1F, 0xA0, 0x68, 0x00, 0x14, 0x7C, 0x00, // 0x7C140068A01F85 + 0x86, 0x1F, 0xA0, 0x68, 0x00, 0x18, 0x7C, 0x00, // 0x7C180068A01F86 + 0x87, 0x1F, 0xA0, 0x68, 0x00, 0x1C, 0x7C, 0x00, // 0x7C1C0068A01F87 + 0x0A, 0x1F, 0x00, 0x60, 0x00, 0x20, 0x7C, 0x00, // 0x7C200060001F0A + 0x0C, 0x1F, 0x20, 0x60, 0x00, 0x20, 0x7C, 0x00, // 0x7C200060201F0C + 0x0E, 0x1F, 0x40, 0x68, 0x00, 0x20, 0x7C, 0x00, // 0x7C200068401F0E + 0x88, 0x1F, 0xA0, 0x68, 0x00, 0x20, 0x7C, 0x00, // 0x7C200068A01F88 + 0x0B, 0x1F, 0x00, 0x60, 0x00, 0x24, 0x7C, 0x00, // 0x7C240060001F0B + 0x0D, 0x1F, 0x20, 0x60, 0x00, 0x24, 0x7C, 0x00, // 0x7C240060201F0D + 0x0F, 0x1F, 0x40, 0x68, 0x00, 0x24, 0x7C, 0x00, // 0x7C240068401F0F + 0x89, 0x1F, 0xA0, 0x68, 0x00, 0x24, 0x7C, 0x00, // 0x7C240068A01F89 + 0x8A, 0x1F, 0xA0, 0x68, 0x00, 0x28, 0x7C, 0x00, // 0x7C280068A01F8A + 0x8B, 0x1F, 0xA0, 0x68, 0x00, 0x2C, 0x7C, 0x00, // 0x7C2C0068A01F8B + 0x8C, 0x1F, 0xA0, 0x68, 0x00, 0x30, 0x7C, 0x00, // 0x7C300068A01F8C + 0x8D, 0x1F, 0xA0, 0x68, 0x00, 0x34, 0x7C, 0x00, // 0x7C340068A01F8D + 0x8E, 0x1F, 0xA0, 0x68, 0x00, 0x38, 0x7C, 0x00, // 0x7C380068A01F8E + 0x8F, 0x1F, 0xA0, 0x68, 0x00, 0x3C, 0x7C, 0x00, // 0x7C3C0068A01F8F + 0x12, 0x1F, 0x00, 0x60, 0x00, 0x40, 0x7C, 0x00, // 0x7C400060001F12 + 0x14, 0x1F, 0x20, 0x60, 0x00, 0x40, 0x7C, 0x00, // 0x7C400060201F14 + 0x13, 0x1F, 0x00, 0x60, 0x00, 0x44, 0x7C, 0x00, // 0x7C440060001F13 + 0x15, 0x1F, 0x20, 0x60, 0x00, 0x44, 0x7C, 0x00, // 0x7C440060201F15 + 0x1A, 0x1F, 0x00, 0x60, 0x00, 0x60, 0x7C, 0x00, // 0x7C600060001F1A + 0x1C, 0x1F, 0x20, 0x60, 0x00, 0x60, 0x7C, 0x00, // 0x7C600060201F1C + 0x1B, 0x1F, 0x00, 0x60, 0x00, 0x64, 0x7C, 0x00, // 0x7C640060001F1B + 0x1D, 0x1F, 0x20, 0x60, 0x00, 0x64, 0x7C, 0x00, // 0x7C640060201F1D + 0x22, 0x1F, 0x00, 0x60, 0x00, 0x80, 0x7C, 0x00, // 0x7C800060001F22 + 0x24, 0x1F, 0x20, 0x60, 0x00, 0x80, 0x7C, 0x00, // 0x7C800060201F24 + 0x26, 0x1F, 0x40, 0x68, 0x00, 0x80, 0x7C, 0x00, // 0x7C800068401F26 + 0x90, 0x1F, 0xA0, 0x68, 0x00, 0x80, 0x7C, 0x00, // 0x7C800068A01F90 + 0x23, 0x1F, 0x00, 0x60, 0x00, 0x84, 0x7C, 0x00, // 0x7C840060001F23 + 0x25, 0x1F, 0x20, 0x60, 0x00, 0x84, 0x7C, 0x00, // 0x7C840060201F25 + 0x27, 0x1F, 0x40, 0x68, 0x00, 0x84, 0x7C, 0x00, // 0x7C840068401F27 + 0x91, 0x1F, 0xA0, 0x68, 0x00, 0x84, 0x7C, 0x00, // 0x7C840068A01F91 + 0x92, 0x1F, 0xA0, 0x68, 0x00, 0x88, 0x7C, 0x00, // 0x7C880068A01F92 + 0x93, 0x1F, 0xA0, 0x68, 0x00, 0x8C, 0x7C, 0x00, // 0x7C8C0068A01F93 + 0x94, 0x1F, 0xA0, 0x68, 0x00, 0x90, 0x7C, 0x00, // 0x7C900068A01F94 + 0x95, 0x1F, 0xA0, 0x68, 0x00, 0x94, 0x7C, 0x00, // 0x7C940068A01F95 + 0x96, 0x1F, 0xA0, 0x68, 0x00, 0x98, 0x7C, 0x00, // 0x7C980068A01F96 + 0x97, 0x1F, 0xA0, 0x68, 0x00, 0x9C, 0x7C, 0x00, // 0x7C9C0068A01F97 + 0x2A, 0x1F, 0x00, 0x60, 0x00, 0xA0, 0x7C, 0x00, // 0x7CA00060001F2A + 0x2C, 0x1F, 0x20, 0x60, 0x00, 0xA0, 0x7C, 0x00, // 0x7CA00060201F2C + 0x2E, 0x1F, 0x40, 0x68, 0x00, 0xA0, 0x7C, 0x00, // 0x7CA00068401F2E + 0x98, 0x1F, 0xA0, 0x68, 0x00, 0xA0, 0x7C, 0x00, // 0x7CA00068A01F98 + 0x2B, 0x1F, 0x00, 0x60, 0x00, 0xA4, 0x7C, 0x00, // 0x7CA40060001F2B + 0x2D, 0x1F, 0x20, 0x60, 0x00, 0xA4, 0x7C, 0x00, // 0x7CA40060201F2D + 0x2F, 0x1F, 0x40, 0x68, 0x00, 0xA4, 0x7C, 0x00, // 0x7CA40068401F2F + 0x99, 0x1F, 0xA0, 0x68, 0x00, 0xA4, 0x7C, 0x00, // 0x7CA40068A01F99 + 0x9A, 0x1F, 0xA0, 0x68, 0x00, 0xA8, 0x7C, 0x00, // 0x7CA80068A01F9A + 0x9B, 0x1F, 0xA0, 0x68, 0x00, 0xAC, 0x7C, 0x00, // 0x7CAC0068A01F9B + 0x9C, 0x1F, 0xA0, 0x68, 0x00, 0xB0, 0x7C, 0x00, // 0x7CB00068A01F9C + 0x9D, 0x1F, 0xA0, 0x68, 0x00, 0xB4, 0x7C, 0x00, // 0x7CB40068A01F9D + 0x9E, 0x1F, 0xA0, 0x68, 0x00, 0xB8, 0x7C, 0x00, // 0x7CB80068A01F9E + 0x9F, 0x1F, 0xA0, 0x68, 0x00, 0xBC, 0x7C, 0x00, // 0x7CBC0068A01F9F + 0x32, 0x1F, 0x00, 0x60, 0x00, 0xC0, 0x7C, 0x00, // 0x7CC00060001F32 + 0x34, 0x1F, 0x20, 0x60, 0x00, 0xC0, 0x7C, 0x00, // 0x7CC00060201F34 + 0x36, 0x1F, 0x40, 0x68, 0x00, 0xC0, 0x7C, 0x00, // 0x7CC00068401F36 + 0x33, 0x1F, 0x00, 0x60, 0x00, 0xC4, 0x7C, 0x00, // 0x7CC40060001F33 + 0x35, 0x1F, 0x20, 0x60, 0x00, 0xC4, 0x7C, 0x00, // 0x7CC40060201F35 + 0x37, 0x1F, 0x40, 0x68, 0x00, 0xC4, 0x7C, 0x00, // 0x7CC40068401F37 + 0x3A, 0x1F, 0x00, 0x60, 0x00, 0xE0, 0x7C, 0x00, // 0x7CE00060001F3A + 0x3C, 0x1F, 0x20, 0x60, 0x00, 0xE0, 0x7C, 0x00, // 0x7CE00060201F3C + 0x3E, 0x1F, 0x40, 0x68, 0x00, 0xE0, 0x7C, 0x00, // 0x7CE00068401F3E + 0x3B, 0x1F, 0x00, 0x60, 0x00, 0xE4, 0x7C, 0x00, // 0x7CE40060001F3B + 0x3D, 0x1F, 0x20, 0x60, 0x00, 0xE4, 0x7C, 0x00, // 0x7CE40060201F3D + 0x3F, 0x1F, 0x40, 0x68, 0x00, 0xE4, 0x7C, 0x00, // 0x7CE40068401F3F + 0x42, 0x1F, 0x00, 0x60, 0x00, 0x00, 0x7D, 0x00, // 0x7D000060001F42 + 0x44, 0x1F, 0x20, 0x60, 0x00, 0x00, 0x7D, 0x00, // 0x7D000060201F44 + 0x43, 0x1F, 0x00, 0x60, 0x00, 0x04, 0x7D, 0x00, // 0x7D040060001F43 + 0x45, 0x1F, 0x20, 0x60, 0x00, 0x04, 0x7D, 0x00, // 0x7D040060201F45 + 0x4A, 0x1F, 0x00, 0x60, 0x00, 0x20, 0x7D, 0x00, // 0x7D200060001F4A + 0x4C, 0x1F, 0x20, 0x60, 0x00, 0x20, 0x7D, 0x00, // 0x7D200060201F4C + 0x4B, 0x1F, 0x00, 0x60, 0x00, 0x24, 0x7D, 0x00, // 0x7D240060001F4B + 0x4D, 0x1F, 0x20, 0x60, 0x00, 0x24, 0x7D, 0x00, // 0x7D240060201F4D + 0x52, 0x1F, 0x00, 0x60, 0x00, 0x40, 0x7D, 0x00, // 0x7D400060001F52 + 0x54, 0x1F, 0x20, 0x60, 0x00, 0x40, 0x7D, 0x00, // 0x7D400060201F54 + 0x56, 0x1F, 0x40, 0x68, 0x00, 0x40, 0x7D, 0x00, // 0x7D400068401F56 + 0x53, 0x1F, 0x00, 0x60, 0x00, 0x44, 0x7D, 0x00, // 0x7D440060001F53 + 0x55, 0x1F, 0x20, 0x60, 0x00, 0x44, 0x7D, 0x00, // 0x7D440060201F55 + 0x57, 0x1F, 0x40, 0x68, 0x00, 0x44, 0x7D, 0x00, // 0x7D440068401F57 + 0x5B, 0x1F, 0x00, 0x60, 0x00, 0x64, 0x7D, 0x00, // 0x7D640060001F5B + 0x5D, 0x1F, 0x20, 0x60, 0x00, 0x64, 0x7D, 0x00, // 0x7D640060201F5D + 0x5F, 0x1F, 0x40, 0x68, 0x00, 0x64, 0x7D, 0x00, // 0x7D640068401F5F + 0x62, 0x1F, 0x00, 0x60, 0x00, 0x80, 0x7D, 0x00, // 0x7D800060001F62 + 0x64, 0x1F, 0x20, 0x60, 0x00, 0x80, 0x7D, 0x00, // 0x7D800060201F64 + 0x66, 0x1F, 0x40, 0x68, 0x00, 0x80, 0x7D, 0x00, // 0x7D800068401F66 + 0xA0, 0x1F, 0xA0, 0x68, 0x00, 0x80, 0x7D, 0x00, // 0x7D800068A01FA0 + 0x63, 0x1F, 0x00, 0x60, 0x00, 0x84, 0x7D, 0x00, // 0x7D840060001F63 + 0x65, 0x1F, 0x20, 0x60, 0x00, 0x84, 0x7D, 0x00, // 0x7D840060201F65 + 0x67, 0x1F, 0x40, 0x68, 0x00, 0x84, 0x7D, 0x00, // 0x7D840068401F67 + 0xA1, 0x1F, 0xA0, 0x68, 0x00, 0x84, 0x7D, 0x00, // 0x7D840068A01FA1 + 0xA2, 0x1F, 0xA0, 0x68, 0x00, 0x88, 0x7D, 0x00, // 0x7D880068A01FA2 + 0xA3, 0x1F, 0xA0, 0x68, 0x00, 0x8C, 0x7D, 0x00, // 0x7D8C0068A01FA3 + 0xA4, 0x1F, 0xA0, 0x68, 0x00, 0x90, 0x7D, 0x00, // 0x7D900068A01FA4 + 0xA5, 0x1F, 0xA0, 0x68, 0x00, 0x94, 0x7D, 0x00, // 0x7D940068A01FA5 + 0xA6, 0x1F, 0xA0, 0x68, 0x00, 0x98, 0x7D, 0x00, // 0x7D980068A01FA6 + 0xA7, 0x1F, 0xA0, 0x68, 0x00, 0x9C, 0x7D, 0x00, // 0x7D9C0068A01FA7 + 0x6A, 0x1F, 0x00, 0x60, 0x00, 0xA0, 0x7D, 0x00, // 0x7DA00060001F6A + 0x6C, 0x1F, 0x20, 0x60, 0x00, 0xA0, 0x7D, 0x00, // 0x7DA00060201F6C + 0x6E, 0x1F, 0x40, 0x68, 0x00, 0xA0, 0x7D, 0x00, // 0x7DA00068401F6E + 0xA8, 0x1F, 0xA0, 0x68, 0x00, 0xA0, 0x7D, 0x00, // 0x7DA00068A01FA8 + 0x6B, 0x1F, 0x00, 0x60, 0x00, 0xA4, 0x7D, 0x00, // 0x7DA40060001F6B + 0x6D, 0x1F, 0x20, 0x60, 0x00, 0xA4, 0x7D, 0x00, // 0x7DA40060201F6D + 0x6F, 0x1F, 0x40, 0x68, 0x00, 0xA4, 0x7D, 0x00, // 0x7DA40068401F6F + 0xA9, 0x1F, 0xA0, 0x68, 0x00, 0xA4, 0x7D, 0x00, // 0x7DA40068A01FA9 + 0xAA, 0x1F, 0xA0, 0x68, 0x00, 0xA8, 0x7D, 0x00, // 0x7DA80068A01FAA + 0xAB, 0x1F, 0xA0, 0x68, 0x00, 0xAC, 0x7D, 0x00, // 0x7DAC0068A01FAB + 0xAC, 0x1F, 0xA0, 0x68, 0x00, 0xB0, 0x7D, 0x00, // 0x7DB00068A01FAC + 0xAD, 0x1F, 0xA0, 0x68, 0x00, 0xB4, 0x7D, 0x00, // 0x7DB40068A01FAD + 0xAE, 0x1F, 0xA0, 0x68, 0x00, 0xB8, 0x7D, 0x00, // 0x7DB80068A01FAE + 0xAF, 0x1F, 0xA0, 0x68, 0x00, 0xBC, 0x7D, 0x00, // 0x7DBC0068A01FAF + 0xB2, 0x1F, 0xA0, 0x68, 0x00, 0xC0, 0x7D, 0x00, // 0x7DC00068A01FB2 + 0xC2, 0x1F, 0xA0, 0x68, 0x00, 0xD0, 0x7D, 0x00, // 0x7DD00068A01FC2 + 0xF2, 0x1F, 0xA0, 0x68, 0x00, 0xF0, 0x7D, 0x00, // 0x7DF00068A01FF2 + 0xB7, 0x1F, 0xA0, 0x68, 0x00, 0xD8, 0x7E, 0x00, // 0x7ED80068A01FB7 + 0xCD, 0x1F, 0x00, 0x60, 0x00, 0xFC, 0x7E, 0x00, // 0x7EFC0060001FCD + 0xCE, 0x1F, 0x20, 0x60, 0x00, 0xFC, 0x7E, 0x00, // 0x7EFC0060201FCE + 0xCF, 0x1F, 0x40, 0x68, 0x00, 0xFC, 0x7E, 0x00, // 0x7EFC0068401FCF + 0xC7, 0x1F, 0xA0, 0x68, 0x00, 0x18, 0x7F, 0x00, // 0x7F180068A01FC7 + 0xF7, 0x1F, 0xA0, 0x68, 0x00, 0xD8, 0x7F, 0x00, // 0x7FD80068A01FF7 + 0xDD, 0x1F, 0x00, 0x60, 0x00, 0xF8, 0x7F, 0x00, // 0x7FF80060001FDD + 0xDE, 0x1F, 0x20, 0x60, 0x00, 0xF8, 0x7F, 0x00, // 0x7FF80060201FDE + 0xDF, 0x1F, 0x40, 0x68, 0x00, 0xF8, 0x7F, 0x00, // 0x7FF80068401FDF + 0x9A, 0x21, 0x00, 0x67, 0x00, 0x40, 0x86, 0x00, // 0x8640006700219A + 0x9B, 0x21, 0x00, 0x67, 0x00, 0x48, 0x86, 0x00, // 0x8648006700219B + 0xAE, 0x21, 0x00, 0x67, 0x00, 0x50, 0x86, 0x00, // 0x865000670021AE + 0xCD, 0x21, 0x00, 0x67, 0x00, 0x40, 0x87, 0x00, // 0x874000670021CD + 0xCF, 0x21, 0x00, 0x67, 0x00, 0x48, 0x87, 0x00, // 0x874800670021CF + 0xCE, 0x21, 0x00, 0x67, 0x00, 0x50, 0x87, 0x00, // 0x875000670021CE + 0x04, 0x22, 0x00, 0x67, 0x00, 0x0C, 0x88, 0x00, // 0x880C0067002204 + 0x09, 0x22, 0x00, 0x67, 0x00, 0x20, 0x88, 0x00, // 0x88200067002209 + 0x0C, 0x22, 0x00, 0x67, 0x00, 0x2C, 0x88, 0x00, // 0x882C006700220C + 0x24, 0x22, 0x00, 0x67, 0x00, 0x8C, 0x88, 0x00, // 0x888C0067002224 + 0x26, 0x22, 0x00, 0x67, 0x00, 0x94, 0x88, 0x00, // 0x88940067002226 + 0x41, 0x22, 0x00, 0x67, 0x00, 0xF0, 0x88, 0x00, // 0x88F00067002241 + 0x44, 0x22, 0x00, 0x67, 0x00, 0x0C, 0x89, 0x00, // 0x890C0067002244 + 0x47, 0x22, 0x00, 0x67, 0x00, 0x14, 0x89, 0x00, // 0x89140067002247 + 0x49, 0x22, 0x00, 0x67, 0x00, 0x20, 0x89, 0x00, // 0x89200067002249 + 0x6D, 0x22, 0x00, 0x67, 0x00, 0x34, 0x89, 0x00, // 0x8934006700226D + 0x62, 0x22, 0x00, 0x67, 0x00, 0x84, 0x89, 0x00, // 0x89840067002262 + 0x70, 0x22, 0x00, 0x67, 0x00, 0x90, 0x89, 0x00, // 0x89900067002270 + 0x71, 0x22, 0x00, 0x67, 0x00, 0x94, 0x89, 0x00, // 0x89940067002271 + 0x74, 0x22, 0x00, 0x67, 0x00, 0xC8, 0x89, 0x00, // 0x89C80067002274 + 0x75, 0x22, 0x00, 0x67, 0x00, 0xCC, 0x89, 0x00, // 0x89CC0067002275 + 0x78, 0x22, 0x00, 0x67, 0x00, 0xD8, 0x89, 0x00, // 0x89D80067002278 + 0x79, 0x22, 0x00, 0x67, 0x00, 0xDC, 0x89, 0x00, // 0x89DC0067002279 + 0x80, 0x22, 0x00, 0x67, 0x00, 0xE8, 0x89, 0x00, // 0x89E80067002280 + 0x81, 0x22, 0x00, 0x67, 0x00, 0xEC, 0x89, 0x00, // 0x89EC0067002281 + 0xE0, 0x22, 0x00, 0x67, 0x00, 0xF0, 0x89, 0x00, // 0x89F000670022E0 + 0xE1, 0x22, 0x00, 0x67, 0x00, 0xF4, 0x89, 0x00, // 0x89F400670022E1 + 0x84, 0x22, 0x00, 0x67, 0x00, 0x08, 0x8A, 0x00, // 0x8A080067002284 + 0x85, 0x22, 0x00, 0x67, 0x00, 0x0C, 0x8A, 0x00, // 0x8A0C0067002285 + 0x88, 0x22, 0x00, 0x67, 0x00, 0x18, 0x8A, 0x00, // 0x8A180067002288 + 0x89, 0x22, 0x00, 0x67, 0x00, 0x1C, 0x8A, 0x00, // 0x8A1C0067002289 + 0xE2, 0x22, 0x00, 0x67, 0x00, 0x44, 0x8A, 0x00, // 0x8A4400670022E2 + 0xE3, 0x22, 0x00, 0x67, 0x00, 0x48, 0x8A, 0x00, // 0x8A4800670022E3 + 0xAC, 0x22, 0x00, 0x67, 0x00, 0x88, 0x8A, 0x00, // 0x8A8800670022AC + 0xAD, 0x22, 0x00, 0x67, 0x00, 0xA0, 0x8A, 0x00, // 0x8AA000670022AD + 0xAE, 0x22, 0x00, 0x67, 0x00, 0xA4, 0x8A, 0x00, // 0x8AA400670022AE + 0xAF, 0x22, 0x00, 0x67, 0x00, 0xAC, 0x8A, 0x00, // 0x8AAC00670022AF + 0xEA, 0x22, 0x00, 0x67, 0x00, 0xC8, 0x8A, 0x00, // 0x8AC800670022EA + 0xEB, 0x22, 0x00, 0x67, 0x00, 0xCC, 0x8A, 0x00, // 0x8ACC00670022EB + 0xEC, 0x22, 0x00, 0x67, 0x00, 0xD0, 0x8A, 0x00, // 0x8AD000670022EC + 0xED, 0x22, 0x00, 0x67, 0x00, 0xD4, 0x8A, 0x00, // 0x8AD400670022ED + 0x94, 0x30, 0x20, 0x13, 0x06, 0x18, 0xC1, 0x00, // 0xC1180613203094 + 0x4C, 0x30, 0x20, 0x13, 0x06, 0x2C, 0xC1, 0x00, // 0xC12C061320304C + 0x4E, 0x30, 0x20, 0x13, 0x06, 0x34, 0xC1, 0x00, // 0xC134061320304E + 0x50, 0x30, 0x20, 0x13, 0x06, 0x3C, 0xC1, 0x00, // 0xC13C0613203050 + 0x52, 0x30, 0x20, 0x13, 0x06, 0x44, 0xC1, 0x00, // 0xC1440613203052 + 0x54, 0x30, 0x20, 0x13, 0x06, 0x4C, 0xC1, 0x00, // 0xC14C0613203054 + 0x56, 0x30, 0x20, 0x13, 0x06, 0x54, 0xC1, 0x00, // 0xC1540613203056 + 0x58, 0x30, 0x20, 0x13, 0x06, 0x5C, 0xC1, 0x00, // 0xC15C0613203058 + 0x5A, 0x30, 0x20, 0x13, 0x06, 0x64, 0xC1, 0x00, // 0xC164061320305A + 0x5C, 0x30, 0x20, 0x13, 0x06, 0x6C, 0xC1, 0x00, // 0xC16C061320305C + 0x5E, 0x30, 0x20, 0x13, 0x06, 0x74, 0xC1, 0x00, // 0xC174061320305E + 0x60, 0x30, 0x20, 0x13, 0x06, 0x7C, 0xC1, 0x00, // 0xC17C0613203060 + 0x62, 0x30, 0x20, 0x13, 0x06, 0x84, 0xC1, 0x00, // 0xC1840613203062 + 0x65, 0x30, 0x20, 0x13, 0x06, 0x90, 0xC1, 0x00, // 0xC1900613203065 + 0x67, 0x30, 0x20, 0x13, 0x06, 0x98, 0xC1, 0x00, // 0xC1980613203067 + 0x69, 0x30, 0x20, 0x13, 0x06, 0xA0, 0xC1, 0x00, // 0xC1A00613203069 + 0x70, 0x30, 0x20, 0x13, 0x06, 0xBC, 0xC1, 0x00, // 0xC1BC0613203070 + 0x71, 0x30, 0x40, 0x13, 0x06, 0xBC, 0xC1, 0x00, // 0xC1BC0613403071 + 0x73, 0x30, 0x20, 0x13, 0x06, 0xC8, 0xC1, 0x00, // 0xC1C80613203073 + 0x74, 0x30, 0x40, 0x13, 0x06, 0xC8, 0xC1, 0x00, // 0xC1C80613403074 + 0x76, 0x30, 0x20, 0x13, 0x06, 0xD4, 0xC1, 0x00, // 0xC1D40613203076 + 0x77, 0x30, 0x40, 0x13, 0x06, 0xD4, 0xC1, 0x00, // 0xC1D40613403077 + 0x79, 0x30, 0x20, 0x13, 0x06, 0xE0, 0xC1, 0x00, // 0xC1E00613203079 + 0x7A, 0x30, 0x40, 0x13, 0x06, 0xE0, 0xC1, 0x00, // 0xC1E0061340307A + 0x7C, 0x30, 0x20, 0x13, 0x06, 0xEC, 0xC1, 0x00, // 0xC1EC061320307C + 0x7D, 0x30, 0x40, 0x13, 0x06, 0xEC, 0xC1, 0x00, // 0xC1EC061340307D + 0x9E, 0x30, 0x20, 0x13, 0x06, 0x74, 0xC2, 0x00, // 0xC274061320309E + 0xF4, 0x30, 0x20, 0x13, 0x06, 0x98, 0xC2, 0x00, // 0xC29806132030F4 + 0xAC, 0x30, 0x20, 0x13, 0x06, 0xAC, 0xC2, 0x00, // 0xC2AC06132030AC + 0xAE, 0x30, 0x20, 0x13, 0x06, 0xB4, 0xC2, 0x00, // 0xC2B406132030AE + 0xB0, 0x30, 0x20, 0x13, 0x06, 0xBC, 0xC2, 0x00, // 0xC2BC06132030B0 + 0xB2, 0x30, 0x20, 0x13, 0x06, 0xC4, 0xC2, 0x00, // 0xC2C406132030B2 + 0xB4, 0x30, 0x20, 0x13, 0x06, 0xCC, 0xC2, 0x00, // 0xC2CC06132030B4 + 0xB6, 0x30, 0x20, 0x13, 0x06, 0xD4, 0xC2, 0x00, // 0xC2D406132030B6 + 0xB8, 0x30, 0x20, 0x13, 0x06, 0xDC, 0xC2, 0x00, // 0xC2DC06132030B8 + 0xBA, 0x30, 0x20, 0x13, 0x06, 0xE4, 0xC2, 0x00, // 0xC2E406132030BA + 0xBC, 0x30, 0x20, 0x13, 0x06, 0xEC, 0xC2, 0x00, // 0xC2EC06132030BC + 0xBE, 0x30, 0x20, 0x13, 0x06, 0xF4, 0xC2, 0x00, // 0xC2F406132030BE + 0xC0, 0x30, 0x20, 0x13, 0x06, 0xFC, 0xC2, 0x00, // 0xC2FC06132030C0 + 0xC2, 0x30, 0x20, 0x13, 0x06, 0x04, 0xC3, 0x00, // 0xC30406132030C2 + 0xC5, 0x30, 0x20, 0x13, 0x06, 0x10, 0xC3, 0x00, // 0xC31006132030C5 + 0xC7, 0x30, 0x20, 0x13, 0x06, 0x18, 0xC3, 0x00, // 0xC31806132030C7 + 0xC9, 0x30, 0x20, 0x13, 0x06, 0x20, 0xC3, 0x00, // 0xC32006132030C9 + 0xD0, 0x30, 0x20, 0x13, 0x06, 0x3C, 0xC3, 0x00, // 0xC33C06132030D0 + 0xD1, 0x30, 0x40, 0x13, 0x06, 0x3C, 0xC3, 0x00, // 0xC33C06134030D1 + 0xD3, 0x30, 0x20, 0x13, 0x06, 0x48, 0xC3, 0x00, // 0xC34806132030D3 + 0xD4, 0x30, 0x40, 0x13, 0x06, 0x48, 0xC3, 0x00, // 0xC34806134030D4 + 0xD6, 0x30, 0x20, 0x13, 0x06, 0x54, 0xC3, 0x00, // 0xC35406132030D6 + 0xD7, 0x30, 0x40, 0x13, 0x06, 0x54, 0xC3, 0x00, // 0xC35406134030D7 + 0xD9, 0x30, 0x20, 0x13, 0x06, 0x60, 0xC3, 0x00, // 0xC36006132030D9 + 0xDA, 0x30, 0x40, 0x13, 0x06, 0x60, 0xC3, 0x00, // 0xC36006134030DA + 0xDC, 0x30, 0x20, 0x13, 0x06, 0x6C, 0xC3, 0x00, // 0xC36C06132030DC + 0xDD, 0x30, 0x40, 0x13, 0x06, 0x6C, 0xC3, 0x00, // 0xC36C06134030DD + 0xF7, 0x30, 0x20, 0x13, 0x06, 0xBC, 0xC3, 0x00, // 0xC3BC06132030F7 + 0xF8, 0x30, 0x20, 0x13, 0x06, 0xC0, 0xC3, 0x00, // 0xC3C006132030F8 + 0xF9, 0x30, 0x20, 0x13, 0x06, 0xC4, 0xC3, 0x00, // 0xC3C406132030F9 + 0xFA, 0x30, 0x20, 0x13, 0x06, 0xC8, 0xC3, 0x00, // 0xC3C806132030FA + 0xFE, 0x30, 0x20, 0x13, 0x06, 0xF4, 0xC3, 0x00, // 0xC3F406132030FE + 0xC9, 0x05, 0xE1, 0x60, 0x00, 0x48, 0x17, 0x04, // 0x417480060E105C9 + 0xE4, 0x05, 0xE1, 0x60, 0x00, 0x68, 0x17, 0x04, // 0x417680060E105E4 + 0x9A, 0x10, 0x41, 0x17, 0x22, 0x64, 0x42, 0x04, // 0x44264221741109A + 0x9C, 0x10, 0x41, 0x17, 0x22, 0x6C, 0x42, 0x04, // 0x4426C221741109C + 0xAB, 0x10, 0x41, 0x17, 0x22, 0x94, 0x42, 0x04, // 0x4429422174110AB + 0x2E, 0x11, 0xE1, 0x24, 0x22, 0xC4, 0x44, 0x04, // 0x444C42224E1112E + 0x2F, 0x11, 0xE1, 0x24, 0x22, 0xC8, 0x44, 0x04, // 0x444C82224E1112F + 0x4B, 0x13, 0xC1, 0x67, 0x22, 0x1C, 0x4D, 0x04, // 0x44D1C2267C1134B + 0x4C, 0x13, 0xE1, 0x6A, 0x22, 0x1C, 0x4D, 0x04, // 0x44D1C226AE1134C + 0x83, 0x13, 0x21, 0x79, 0x22, 0x08, 0x4E, 0x04, // 0x44E082279211383 + 0x85, 0x13, 0x61, 0x77, 0x22, 0x10, 0x4E, 0x04, // 0x44E102277611385 + 0x8E, 0x13, 0x41, 0x78, 0x22, 0x2C, 0x4E, 0x04, // 0x44E2C227841138E + 0x91, 0x13, 0x21, 0x79, 0x22, 0x40, 0x4E, 0x04, // 0x44E402279211391 + 0xC7, 0x13, 0x01, 0x77, 0x22, 0x08, 0x4F, 0x04, // 0x44F0822770113C7 + 0xC5, 0x13, 0x41, 0x78, 0x22, 0x08, 0x4F, 0x04, // 0x44F0822784113C5 + 0xC8, 0x13, 0x21, 0x79, 0x22, 0x08, 0x4F, 0x04, // 0x44F0822792113C8 + 0xBC, 0x14, 0x01, 0x96, 0x22, 0xE4, 0x52, 0x04, // 0x452E422960114BC + 0xBB, 0x14, 0x41, 0x97, 0x22, 0xE4, 0x52, 0x04, // 0x452E422974114BB + 0xBE, 0x14, 0xA1, 0x97, 0x22, 0xE4, 0x52, 0x04, // 0x452E42297A114BE + 0xBA, 0x15, 0xE1, 0xB5, 0x22, 0xE0, 0x56, 0x04, // 0x456E022B5E115BA + 0xBB, 0x15, 0xE1, 0xB5, 0x22, 0xE4, 0x56, 0x04, // 0x456E422B5E115BB + 0x38, 0x19, 0x01, 0x26, 0x23, 0xD4, 0x64, 0x04, // 0x464D42326011938 + 0x21, 0x61, 0xC1, 0x23, 0x2C, 0x78, 0x84, 0x05, // 0x584782C23C16121 + 0x23, 0x61, 0xE1, 0x23, 0x2C, 0x78, 0x84, 0x05, // 0x584782C23E16123 + 0x25, 0x61, 0x01, 0x24, 0x2C, 0x78, 0x84, 0x05, // 0x584782C24016125 + 0x22, 0x61, 0x21, 0x25, 0x2C, 0x78, 0x84, 0x05, // 0x584782C25216122 + 0x26, 0x61, 0xE1, 0x23, 0x2C, 0x84, 0x84, 0x05, // 0x584842C23E16126 + 0x28, 0x61, 0x01, 0x24, 0x2C, 0x84, 0x84, 0x05, // 0x584842C24016128 + 0x27, 0x61, 0xE1, 0x23, 0x2C, 0x88, 0x84, 0x05, // 0x584882C23E16127 + 0x24, 0x61, 0xE1, 0x23, 0x2C, 0xA4, 0x84, 0x05, // 0x584A42C23E16124 + 0x69, 0x6D, 0xE1, 0xAC, 0x2D, 0x8C, 0xB5, 0x05, // 0x5B58C2DACE16D69 + 0x68, 0x6D, 0xE1, 0xAC, 0x2D, 0x9C, 0xB5, 0x05, // 0x5B59C2DACE16D68 + 0x6A, 0x6D, 0xE1, 0xAC, 0x2D, 0xA4, 0xB5, 0x05, // 0x5B5A42DACE16D6A + }; + } +} diff --git a/src/SixLabors.Fonts/Unicode/Resources/ScriptDirectionData.Generated.cs b/src/SixLabors.Fonts/Unicode/Resources/ScriptDirectionData.Generated.cs new file mode 100644 index 000000000..b6be5b0f7 --- /dev/null +++ b/src/SixLabors.Fonts/Unicode/Resources/ScriptDirectionData.Generated.cs @@ -0,0 +1,71 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// +namespace SixLabors.Fonts.Unicode.Resources +{ + /// + /// The direction each script is written in when it is set horizontally. + /// + internal static class ScriptDirectionData + { + /// + /// Gets the direction the given script is written in. A script the standard writes + /// left to right is not listed here and answers with that direction. + /// + /// The script to look up. + /// The direction the script is written in. + public static ScriptHorizontalDirection GetDirection(ScriptClass script) + { + switch (script) + { + case ScriptClass.Adlam: + case ScriptClass.Arabic: + case ScriptClass.Avestan: + case ScriptClass.Chorasmian: + case ScriptClass.Cypriot: + case ScriptClass.Elymaic: + case ScriptClass.Garay: + case ScriptClass.HanifiRohingya: + case ScriptClass.Hatran: + case ScriptClass.Hebrew: + case ScriptClass.ImperialAramaic: + case ScriptClass.InscriptionalPahlavi: + case ScriptClass.InscriptionalParthian: + case ScriptClass.Kharoshthi: + case ScriptClass.Lydian: + case ScriptClass.Mandaic: + case ScriptClass.Manichaean: + case ScriptClass.MendeKikakui: + case ScriptClass.MeroiticCursive: + case ScriptClass.MeroiticHieroglyphs: + case ScriptClass.Nabataean: + case ScriptClass.Nko: + case ScriptClass.OldNorthArabian: + case ScriptClass.OldSogdian: + case ScriptClass.OldSouthArabian: + case ScriptClass.OldTurkic: + case ScriptClass.OldUyghur: + case ScriptClass.Palmyrene: + case ScriptClass.Phoenician: + case ScriptClass.PsalterPahlavi: + case ScriptClass.Samaritan: + case ScriptClass.Sidetic: + case ScriptClass.Sogdian: + case ScriptClass.Syriac: + case ScriptClass.Thaana: + case ScriptClass.Yezidi: + return ScriptHorizontalDirection.RightToLeft; + + case ScriptClass.OldHungarian: + case ScriptClass.OldItalic: + case ScriptClass.Runic: + case ScriptClass.Tifinagh: + return ScriptHorizontalDirection.Either; + + default: + return ScriptHorizontalDirection.LeftToRight; + } + } + } +} diff --git a/src/SixLabors.Fonts/Unicode/Resources/VowelConstraintData.Generated.cs b/src/SixLabors.Fonts/Unicode/Resources/VowelConstraintData.Generated.cs new file mode 100644 index 000000000..a744efe93 --- /dev/null +++ b/src/SixLabors.Fonts/Unicode/Resources/VowelConstraintData.Generated.cs @@ -0,0 +1,224 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Unicode.Resources; + +/// +/// The character sequences that spell one vowel but read as another. A font is +/// not asked to render such a sequence: a dotted circle is placed before its +/// final character so the sequence cannot be mistaken for the vowel it +/// imitates. Generated from IndicShapingInvalidCluster.txt. +/// +internal static class VowelConstraintData +{ + /// + /// The two character sequences as packed keys, in ascending order so a + /// lookup is a binary search. + /// + private static readonly ulong[] PairData = + [ + 0x380130A009BEUL, + 0x3801316009C3UL, + 0x3801318009E2UL, + 0x442200A11038UL, + 0x44220161103EUL, + 0x442201E11042UL, + 0x7C0120A0093AUL, + 0x7C0120A0093BUL, + 0x7C0120A0093EUL, + 0x7C0120A00945UL, + 0x7C0120A00946UL, + 0x7C0120A00949UL, + 0x7C0120A0094AUL, + 0x7C0120A0094BUL, + 0x7C0120A0094CUL, + 0x7C0120A0094FUL, + 0x7C0120A00956UL, + 0x7C0120A00957UL, + 0x7C0120C0093AUL, + 0x7C0120C00945UL, + 0x7C0120C00946UL, + 0x7C0120C00947UL, + 0x7C0120C00948UL, + 0x7C0121200941UL, + 0x7C0121E00945UL, + 0x7C0121E00946UL, + 0x7C0121E00947UL, + 0xBC0150A00ABEUL, + 0xBC0150A00AC5UL, + 0xBC0150A00AC7UL, + 0xBC0150A00AC8UL, + 0xBC0150A00AC9UL, + 0xBC0150A00ACBUL, + 0xBC0150A00ACCUL, + 0xBC0158A00ABEUL, + 0xC00140A00A3EUL, + 0xC00140A00A48UL, + 0xC00140A00A4CUL, + 0xC0014E400A3FUL, + 0xC0014E400A40UL, + 0xC0014E400A47UL, + 0xC0014E600A41UL, + 0xC0014E600A42UL, + 0xC0014E600A4BUL, + 0x108224001122CUL, + 0x1082240011231UL, + 0x1082240011233UL, + 0x1082240C1122CUL, + 0x1082245811230UL, + 0x1082245811231UL, + 0x108224801122EUL, + 0x1100191200CBEUL, + 0x1100191600CBEUL, + 0x1100192400CCCUL, + 0x16401A0E00D57UL, + 0x16401A1200D57UL, + 0x16401A1C00D46UL, + 0x16401A2400D3EUL, + 0x16401A2400D57UL, + 0x16822C0011639UL, + 0x16822C001163AUL, + 0x16822C0211639UL, + 0x16822C021163AUL, + 0x1A40160A00B3EUL, + 0x1A40161E00B57UL, + 0x1A40162600B57UL, + 0x20022560112E0UL, + 0x20022560112E5UL, + 0x20022560112E6UL, + 0x20022560112E7UL, + 0x20022560112E8UL, + 0x20401B0A00DCFUL, + 0x20401B0A00DD0UL, + 0x20401B0A00DD1UL, + 0x20401B1600DDFUL, + 0x20401B1A00DD8UL, + 0x20401B1E00DDFUL, + 0x20401B2200DCAUL, + 0x20401B2200DD9UL, + 0x20401B2200DDAUL, + 0x20401B2200DDCUL, + 0x20401B2200DDDUL, + 0x20401B2200DDEUL, + 0x20401B2800DDFUL, + 0x22822D00116ADUL, + 0x22822D00116B4UL, + 0x22822D00116B5UL, + 0x22822D0C116B2UL, + 0x2340170A00BC2UL, + 0x2400182400C4CUL, + 0x2400182400C55UL, + 0x2400187E00C55UL, + 0x2400188C00C55UL, + 0x2400189400C55UL, + 0x25822902114B0UL, + 0x25822916114BAUL, + 0x2582291A114BAUL, + 0x25822954114B5UL, + 0x25822954114B6UL, + ]; + + /// + /// The first two characters of each three character sequence, packed and + /// ordered as the pairs are. + /// + private static readonly ulong[] TripleData = + [ + 0x7C012600094DUL, + ]; + + /// + /// The final character of each three character sequence, positioned as its + /// packed key is. + /// + private static readonly int[] TripleFinalData = + [ + 0x0907, + ]; + + /// + /// Determines whether any sequence is written in the given script. Text in + /// any other script carries no constrained sequence and is left alone. + /// + /// The script the text is written in. + /// when the script carries sequences. + public static bool IsConstrainedScript(ScriptClass script) + => script switch + { + ScriptClass.Bengali => true, + ScriptClass.Brahmi => true, + ScriptClass.Devanagari => true, + ScriptClass.Gujarati => true, + ScriptClass.Gurmukhi => true, + ScriptClass.Kannada => true, + ScriptClass.Khojki => true, + ScriptClass.Khudawadi => true, + ScriptClass.Malayalam => true, + ScriptClass.Modi => true, + ScriptClass.Oriya => true, + ScriptClass.Sinhala => true, + ScriptClass.Takri => true, + ScriptClass.Tamil => true, + ScriptClass.Telugu => true, + ScriptClass.Tirhuta => true, + _ => false, + }; + + /// + /// Determines whether the two characters spell a constrained sequence. + /// + /// The script the text is written in. + /// The character that begins the sequence. + /// The character that follows it. + /// when the two are constrained. + public static bool IsConstrainedPair(ScriptClass script, int first, int second) + => Array.BinarySearch(PairData, Key(script, first, second)) >= 0; + + /// + /// Determines whether the three characters spell a constrained sequence. + /// + /// The script the text is written in. + /// The character that begins the sequence. + /// The character that follows it. + /// The character that ends it. + /// when the three are constrained. + public static bool IsConstrainedTriple(ScriptClass script, int first, int second, int third) + { + ulong key = Key(script, first, second); + int index = Array.BinarySearch(TripleData, key); + if (index < 0) + { + return false; + } + + // Sequences sharing their first two characters sit together, so the + // run around the found position holds every candidate final character. + ReadOnlySpan keys = TripleData; + ReadOnlySpan finals = TripleFinalData; + int start = index; + while (start > 0 && keys[start - 1] == key) + { + start--; + } + + for (int i = start; i < keys.Length && keys[i] == key; i++) + { + if (finals[i] == third) + { + return true; + } + } + + return false; + } + + /// + /// Packs a script and two characters into one ordered key. + /// + /// The script the text is written in. + /// The character that begins the sequence. + /// The character that follows it. + /// The packed key. + private static ulong Key(ScriptClass script, int first, int second) + => ((ulong)script << 42) | ((ulong)(uint)first << 21) | (uint)second; +} diff --git a/src/SixLabors.Fonts/Unicode/ScriptHorizontalDirection.cs b/src/SixLabors.Fonts/Unicode/ScriptHorizontalDirection.cs new file mode 100644 index 000000000..22e6cb900 --- /dev/null +++ b/src/SixLabors.Fonts/Unicode/ScriptHorizontalDirection.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Unicode; + +/// +/// The direction a script is written in when it is set horizontally. +/// +internal enum ScriptHorizontalDirection +{ + /// + /// The script is written from left to right. + /// + LeftToRight, + + /// + /// The script is written from right to left. + /// + RightToLeft, + + /// + /// The script is written either way, so a run of it is left in the order it + /// arrived rather than being turned around. + /// + Either +} diff --git a/src/SixLabors.Fonts/Unicode/UnicodeData.cs b/src/SixLabors.Fonts/Unicode/UnicodeData.cs index 3d42765b7..840ac045e 100644 --- a/src/SixLabors.Fonts/Unicode/UnicodeData.cs +++ b/src/SixLabors.Fonts/Unicode/UnicodeData.cs @@ -9,22 +9,23 @@ namespace SixLabors.Fonts.Unicode; internal static class UnicodeData { - private static readonly Lazy LazyBidiTrie = new(() => GetBidiTrie(), true); - private static readonly Lazy LazyBidiMirrorTrie = new(() => GetBidiMirrorTrie(), true); - private static readonly Lazy LazyEastAsianWidthTrie = new(() => GetEastAsianWidthTrie(), true); - private static readonly Lazy LazyEmojiTrie = new(() => GetEmojiTrie(), true); - private static readonly Lazy LazyGraphemeTrie = new(() => GetGraphemeTrie(), true); - private static readonly Lazy LazyLineBreakTrie = new(() => GetLineBreakTrie(), true); - private static readonly Lazy LazyWordBreakTrie = new(() => GetWordBreakTrie(), true); - private static readonly Lazy LazyScriptTrie = new(() => GetScriptTrie(), true); - private static readonly Lazy LazyCategoryTrie = new(() => GetCategoryTrie(), true); - private static readonly Lazy LazyArabicShapingTrie = new(() => GetArabicShapingTrie(), true); - private static readonly Lazy LazyIndicConjunctBreakTrie = new(() => GetIndicConjunctBreakTrie(), true); - private static readonly Lazy LazyIndicSyllabicCategoryTrie = new(() => GetIndicSyllabicCategoryTrie(), true); - private static readonly Lazy LazyIndicPositionalCategoryTrie = new(() => GetIndicPositionalCategoryTrie(), true); - private static readonly Lazy LazyVerticalOrientationTrie = new(() => GetVerticalOrientationTrie(), true); - private static readonly Lazy LazyUniversalShapingTrie = new(() => GetUniversalShapingTrie(), true); - private static readonly Lazy LazyIndicShapingTrie = new(() => GetIndicShapingTrie(), true); + private static readonly Lazy LazyBidiTrie = new(GetBidiTrie, true); + private static readonly Lazy LazyBidiMirrorTrie = new(GetBidiMirrorTrie, true); + private static readonly Lazy LazyEastAsianWidthTrie = new(GetEastAsianWidthTrie, true); + private static readonly Lazy LazyEmojiTrie = new(GetEmojiTrie, true); + private static readonly Lazy LazyGraphemeTrie = new(GetGraphemeTrie, true); + private static readonly Lazy LazyLineBreakTrie = new(GetLineBreakTrie, true); + private static readonly Lazy LazyWordBreakTrie = new(GetWordBreakTrie, true); + private static readonly Lazy LazyScriptTrie = new(GetScriptTrie, true); + private static readonly Lazy LazyCategoryTrie = new(GetCategoryTrie, true); + private static readonly Lazy LazyArabicShapingTrie = new(GetArabicShapingTrie, true); + private static readonly Lazy LazyIndicConjunctBreakTrie = new(GetIndicConjunctBreakTrie, true); + private static readonly Lazy LazyIndicSyllabicCategoryTrie = new(GetIndicSyllabicCategoryTrie, true); + private static readonly Lazy LazyIndicPositionalCategoryTrie = new(GetIndicPositionalCategoryTrie, true); + private static readonly Lazy LazyVerticalOrientationTrie = new(GetVerticalOrientationTrie, true); + private static readonly Lazy LazyUniversalShapingTrie = new(GetUniversalShapingTrie, true); + private static readonly Lazy LazyIndicShapingTrie = new(GetIndicShapingTrie, true); + private static readonly Lazy LazyCanonicalCombiningClassTrie = new(GetCanonicalCombiningClassTrie, true); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint GetBidiData(uint codePoint) => LazyBidiTrie.Value.Get(codePoint); @@ -74,6 +75,28 @@ internal static class UnicodeData [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetIndicShapingProperties(uint codePoint) => (int)LazyIndicShapingTrie.Value.Get(codePoint); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetCanonicalCombiningClass(uint codePoint) => (int)LazyCanonicalCombiningClassTrie.Value.Get(codePoint); + + /// + /// Gets the class that orders a mark against the marks around it: the + /// canonical combining class with the classes of several scripts renumbered so + /// that sorting by it leaves their marks in the order they are drawn. + /// + /// The code point to evaluate. + /// The ordering class. + public static int GetMarkOrderingClass(uint codePoint) + { + // A few characters order by where they are drawn rather than by their class, + // so they are resolved before the table is consulted. + if (MarkOrderingData.TryGetOverride(codePoint, out byte order)) + { + return order; + } + + return MarkOrderingData.Classes[GetCanonicalCombiningClass(codePoint)]; + } + private static UnicodeTrie GetBidiTrie() => new(BidiTrie.Data); private static UnicodeTrie GetBidiMirrorTrie() => new(BidiMirrorTrie.Data); @@ -94,6 +117,8 @@ internal static class UnicodeData private static UnicodeTrie GetArabicShapingTrie() => new(ArabicShapingTrie.Data); + private static UnicodeTrie GetCanonicalCombiningClassTrie() => new(CanonicalCombiningClassTrie.Data); + private static UnicodeTrie GetIndicConjunctBreakTrie() => new(IndicConjunctBreakTrie.Data); private static UnicodeTrie GetIndicSyllabicCategoryTrie() => new(IndicSyllabicCategoryTrie.Data); diff --git a/src/UnicodeTrieGenerator/Generator.IndicShapingEngine.cs b/src/UnicodeTrieGenerator/Generator.IndicShapingEngine.cs index f2e936fe9..441101431 100644 --- a/src/UnicodeTrieGenerator/Generator.IndicShapingEngine.cs +++ b/src/UnicodeTrieGenerator/Generator.IndicShapingEngine.cs @@ -37,7 +37,7 @@ public static partial class Generator { ISC.ConsonantSucceedingRepha, Categories.CM }, { ISC.ConsonantWithStacker, Categories.CS }, { ISC.GeminationMark, Categories.SM }, // https://github.com/harfbuzz/harfbuzz/issues/552 - { ISC.InvisibleStacker, Categories.Coeng }, // TODO: Use H once we add explicit Khmer shaper + { ISC.InvisibleStacker, Categories.H }, { ISC.Joiner, Categories.ZWJ }, { ISC.ModifyingLetter, Categories.X }, { ISC.NonJoiner, Categories.ZWNJ }, @@ -56,9 +56,12 @@ public static partial class Generator { ISC.VowelIndependent, Categories.V } }; - // Per-codepoint category overrides for Indic-style shaping. - // These values augment the base Unicode/Indic tables to match - // HarfBuzz behavior for Indic, Khmer, and Myanmar scripts. + /// + /// Gets the per-character category overrides applied to the shared shaping trie. + /// + /// + /// These values are transcribed from HarfBuzz 14.2.1, src/gen-indic-table.py, symbol category_overrides. They are not derivable from the Unicode Character Database. + /// private static readonly Dictionary IndicShapingOverrides = new() { // -------------------------------------------------------------------- @@ -190,23 +193,23 @@ public static partial class Generator // -------------------------------------------------------------------- // Khmer overrides // -------------------------------------------------------------------- - { 0x179A, Categories.Ra }, // Khmer Ra - { 0x17C6, Categories.N }, // TODO: Replace with Xgroup as per below once we support it. - - // { 0x17CC, Categories.Robatic }, - // { 0x17C9, Categories.Robatic }, - // { 0x17CA, Categories.Robatic }, - // { 0x17C6, Categories.Xgroup }, - // { 0x17CB, Categories.Xgroup }, - // { 0x17CD, Categories.Xgroup }, - // { 0x17CE, Categories.Xgroup }, - // { 0x17CF, Categories.Xgroup }, - // { 0x17D0, Categories.Xgroup }, - // { 0x17D1, Categories.Xgroup }, - // { 0x17C7, Categories.Ygroup }, - // { 0x17C8, Categories.Ygroup }, - // { 0x17DD, Categories.Ygroup }, - // { 0x17D3, Categories.Ygroup }, // Just guessing. Uniscribe does not categorize it. + { 0x179A, Categories.Ra }, + { 0x17CC, Categories.Robatic }, + { 0x17C9, Categories.Robatic }, + { 0x17CA, Categories.Robatic }, + { 0x17C6, Categories.Xgroup }, + { 0x17CB, Categories.Xgroup }, + { 0x17CD, Categories.Xgroup }, + { 0x17CE, Categories.Xgroup }, + { 0x17CF, Categories.Xgroup }, + { 0x17D0, Categories.Xgroup }, + { 0x17D1, Categories.Xgroup }, + { 0x17C7, Categories.Ygroup }, + { 0x17C8, Categories.Ygroup }, + { 0x17DD, Categories.Ygroup }, + + // The reference source labels this compatibility category as a guess based on Uniscribe behavior. + { 0x17D3, Categories.Ygroup }, // https://github.com/harfbuzz/harfbuzz/issues/2384 { 0x17D9, Categories.Placeholder }, @@ -353,7 +356,10 @@ private static int GetPosition(Codepoint codepoint, Categories category) position = Positions.Before_Sub; } - return (int)Math.Log((int)position, 2); + // The trie stores the position zero-based. Zero is the unassigned + // sentinel of the enum, so every named position sits one ahead of the + // value the shaper reads back out. + return (int)position - 1; } private static void GenerateIndicShapingDataTrie(Codepoint[] codePoints) @@ -420,6 +426,15 @@ private static void GenerateIndicShapingDataTrie(Codepoint[] codePoints) GenerateDataClass("IndicShaping", null, null, machine, true); } + /// + /// Converts dependent vowels in the Khmer and Myanmar blocks to the positional category their syllable machines consume. + /// + /// + /// This conversion is transcribed from HarfBuzz 14.2.1, src/gen-indic-table.py, symbols matra_categories and position_to_category. The conversion is not derivable from the Unicode Character Database. + /// + /// The character whose block and position determine the category. + /// The category mapped from its syllabic property. + /// The category stored in the shaping trie. private static Categories NormalizeCategoryForBlock(Codepoint codepoint, Categories category) { // HarfBuzz: matra_categories = ('M', 'MPst') @@ -430,10 +445,7 @@ private static Categories NormalizeCategoryForBlock(Codepoint codepoint, Categor { string block = codepoint.Block; - // TODO: Once we implement the Khmer shaper, enable Khmer here too. - // if (block.StartsWith("Khmer", StringComparison.Ordinal) || - // block.StartsWith("Myanmar", StringComparison.Ordinal)) - if (block.StartsWith("Myanmar", StringComparison.Ordinal)) + if (block.StartsWith("Khmer", StringComparison.Ordinal) || block.StartsWith("Myanmar", StringComparison.Ordinal)) { // Base positional category from IndicPositionalCategory.txt Positions basePos = PositionMap.GetValueOrDefault( @@ -456,6 +468,15 @@ private static Categories NormalizeCategoryForBlock(Codepoint codepoint, Categor private static void SetBlocks(Codepoint[] codePoints) { + // Blocks span the whole of Unicode, so every character of every range is + // looked up. Index the characters by code once and the whole file costs + // one lookup per character rather than a scan. + Dictionary byCode = new(codePoints.Length); + foreach (Codepoint codePoint in codePoints) + { + byCode.TryAdd(codePoint.Code, codePoint); + } + Regex regex = IndicPropertyRowRegex(); using StreamReader sr = GetStreamReader("Blocks.txt"); @@ -464,29 +485,27 @@ private static void SetBlocks(Codepoint[] codePoints) { Match match = regex.Match(line); - if (match.Success) + if (!match.Success) { - string start = match.Groups[1].Value; - string end = match.Groups[2].Value; - string block = match.Groups[3].Value; + continue; + } - if (string.IsNullOrEmpty(end)) - { - end = start; - } + string start = match.Groups[1].Value; + string end = match.Groups[2].Value; + string block = match.Groups[3].Value; - int min = ParseHexInt(start); - int max = ParseHexInt(end); + if (string.IsNullOrEmpty(end)) + { + end = start; + } - for (int i = min; i <= max; i++) - { - // TODO: Make an enum of block values and create a trie. This is painfully slow. - Codepoint? codePoint = Array.Find(codePoints, x => x.Code == i); - if (codePoint is null) - { - continue; - } + int min = ParseHexInt(start); + int max = ParseHexInt(end); + for (int i = min; i <= max; i++) + { + if (byCode.TryGetValue(i, out Codepoint? codePoint)) + { codePoint.Block = block; } } diff --git a/src/UnicodeTrieGenerator/Generator.KhmerShapingEngine.cs b/src/UnicodeTrieGenerator/Generator.KhmerShapingEngine.cs new file mode 100644 index 000000000..a9297d425 --- /dev/null +++ b/src/UnicodeTrieGenerator/Generator.KhmerShapingEngine.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using UnicodeTrieGenerator.StateAutomation; +using KhmerCategories = SixLabors.Fonts.Unicode.Resources.IndicShapingData.KhmerCategories; + +namespace UnicodeTrieGenerator; + +/// +/// Contains code to generate the Khmer syllable state machine. +/// +public static partial class Generator +{ + /// + /// Generates the Khmer syllable state machine from its grammar. + /// + /// + /// The grammar and category alphabet are transcribed from HarfBuzz 14.2.1, src/hb-ot-shaper-khmer-machine.rl, symbol khmer_syllable_machine. They are not derivable from the Unicode Character Database. + /// + private static void GenerateKhmerShapingData() + { + KhmerCategories[] categories = Enum.GetValues(); + Dictionary symbols = new(categories.Length); + int id = 0; + + foreach (KhmerCategories category in categories) + { + symbols[category.ToString()] = id++; + } + + StateMachine machine = GetStateMachine("khmer", symbols); + + GenerateDataClass("KhmerShaping", null, null, machine, false); + } +} diff --git a/src/UnicodeTrieGenerator/Generator.MarkOrdering.cs b/src/UnicodeTrieGenerator/Generator.MarkOrdering.cs new file mode 100644 index 000000000..1a7c3c781 --- /dev/null +++ b/src/UnicodeTrieGenerator/Generator.MarkOrdering.cs @@ -0,0 +1,449 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using System.Globalization; +using System.Text.RegularExpressions; + +namespace UnicodeTrieGenerator; + +/// +/// Generates the classes that order a mark against the marks around it. +/// +/// +/// +/// These are the canonical combining classes with the classes of several scripts +/// renumbered, so that sorting by them leaves the marks of those scripts in the +/// order they are drawn. No Unicode data file derives the renumbering, so it is +/// read out of the reference implementation instead: the table +/// _hb_modified_combining_class in hb-unicode.cc, whose entries are +/// either plain numbers or the HB_MODIFIED_COMBINING_CLASS_CCC* macros +/// defined in hb-unicode.hh, together with the handful of characters that +/// modified_combining_class resolves ahead of the table. +/// +/// +/// Reading it rather than transcribing it means the pin is the single statement of +/// which reference version this library matches, and moving the pin regenerates the +/// table. +/// +/// +public static partial class Generator +{ + /// + /// The path of the pinned reference implementation, relative to the solution. + /// + private const string ReferenceSubmoduleRelativePath = @"tests\harfbuzz"; + + /// + /// The number of canonical combining classes, which the standard bounds to one + /// byte. + /// + private const int CombiningClassCount = 256; + + /// + /// Matches one HB_MODIFIED_COMBINING_CLASS_CCC* definition, including the + /// label the reference attaches to it. + /// + [GeneratedRegex(@"#define\s+HB_MODIFIED_COMBINING_CLASS_CCC(?\d+)\s+(?\d+)(?:[ \t]*/\*\s*(?