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