// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; 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 : 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 = []; /// /// Initializes a new instance of the class. /// /// The text options. public GlyphPositioningCollection(TextOptions textOptions) => this.TextOptions = textOptions; /// public int Count => this.glyphs.Count; /// public TextOptions TextOptions { get; } /// public GlyphShapingData this[int index] { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => this.glyphs[index].Data; } /// public void AddShapingFeature(int index, TagEntry feature) { GlyphShapingData data = this.glyphs[index].Data; data.Features.Add(feature); if (feature.Enabled) { data.EnabledFeatureTags.Add(feature.Tag); } } /// public void EnableShapingFeature(int index, Tag feature) { 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; } } } /// public void DisableShapingFeature(int index, Tag feature) { 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; } } } /// /// 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; Tag vert = KnownFeatureTags.VerticalAlternates; Tag vrt2 = KnownFeatureTags.VerticalAlternatesAndRotation; Tag vrtr = KnownFeatureTags.VerticalAlternatesForRotation; 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; foreach (Tag feature in glyph.Data.AppliedFeatures) { isVerticalSubstitution |= feature == vert; isVerticalSubstitution |= feature == vrt2; isVerticalSubstitution |= feature == vrtr; } 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. /// /// 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 = []; Tag vert = KnownFeatureTags.VerticalAlternates; Tag vrt2 = KnownFeatureTags.VerticalAlternatesAndRotation; Tag vrtr = KnownFeatureTags.VerticalAlternatesForRotation; 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; // 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; bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(codePoint, layoutMode); foreach (Tag feature in shape.AppliedFeatures) { isVertical |= feature == vert; isVertical |= feature == vrt2; isVertical |= feature == vrtr; } 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. 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.glyphs.Insert(i += replacementCount, new(offset, new(shape, true) { Bounds = bounds }, font, pointSize, metrics.CloneForRendering(shape.TextRun))); 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; Tag vert = KnownFeatureTags.VerticalAlternates; Tag vrt2 = KnownFeatureTags.VerticalAlternatesAndRotation; Tag vrtr = KnownFeatureTags.VerticalAlternatesForRotation; 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. 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); GlyphShapingBounds placeholderBounds = layoutMode.IsVertical() ? new(0, 0, 0, placeholderMetrics.AdvanceHeight) : new(0, 0, placeholderMetrics.AdvanceWidth, 0); GlyphShapingData placeholderData = new(data, true) { Bounds = placeholderBounds, IsPositioned = true }; this.glyphs.Add(new(offset, placeholderData, font, font.Size, placeholderMetrics)); 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; bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(codePoint, layoutMode); foreach (Tag feature in data.AppliedFeatures) { isVertical |= feature == vert; isVertical |= feature == vrt2; isVertical |= feature == vrtr; } 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. GlyphShapingBounds bounds = isVertical ? new(0, 0, 0, metrics.AdvanceHeight) : new(0, 0, metrics.AdvanceWidth, 0); this.glyphs.Add(new(offset, new(data, true) { Bounds = bounds }, font, font.Size, metrics.CloneForRendering(data.TextRun))); } return !hasFallBacks; } /// /// Updates the position of the glyph at the specified index. /// /// 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; } } } /// /// Updates the advanced metrics of the glyphs at the given index and id, /// adding dx and dy to the current advance. /// /// 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; 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; } m.ApplyAdvance(dx, isVertical ? dy : (short)0); } } /// /// 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; } [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; } private string DebuggerDisplay => FormattableString.Invariant($"Offset: {this.Offset}, Data: {this.Data.ToDebuggerDisplay()}"); } } }