From 3a603a698950f3246c09c11ef8b5708159d4e9fa Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 13 Dec 2024 16:32:56 +1000 Subject: [PATCH 1/4] Fix vertical glyph layout --- .../DrawWithImageSharp.csproj | 4 +- src/SixLabors.Fonts/FileFontMetrics.cs | 4 + src/SixLabors.Fonts/FontMetrics.cs | 13 ++ .../GlyphPositioningCollection.cs | 61 ++++++- src/SixLabors.Fonts/GlyphShapingData.cs | 12 +- .../GlyphSubstitutionCollection.cs | 19 +- src/SixLabors.Fonts/StreamFontMetrics.cs | 23 +++ .../GSub/LookupType1SubTable.cs | 4 +- .../GSub/LookupType2SubTable.cs | 2 +- .../GSub/LookupType3SubTable.cs | 2 +- .../GSub/LookupType4SubTable.cs | 2 +- .../GSub/LookupType8SubTable.cs | 2 +- .../Shapers/DefaultShaper.cs | 3 - .../Shapers/HangulShaper.cs | 8 +- .../Shapers/IndicShaper.cs | 4 +- .../Shapers/UniversalShaper.cs | 4 +- .../Tables/General/CMap/CMapSubTable.cs | 2 + .../Tables/General/CMap/Format0SubTable.cs | 15 ++ .../Tables/General/CMap/Format12SubTable.cs | 19 ++ .../Tables/General/CMap/Format14SubTable.cs | 6 + .../Tables/General/CMap/Format4SubTable.cs | 51 +++++- .../Tables/General/CMapTable.cs | 21 ++- src/SixLabors.Fonts/TextLayout.cs | 55 ++++-- src/SixLabors.Fonts/Unicode/UnicodeUtility.cs | 167 +++++++++++++++--- .../Fakes/FakeCmapSubtable.cs | 15 ++ .../Unicode/UnicodeUtilityTests.cs | 16 ++ 26 files changed, 454 insertions(+), 80 deletions(-) diff --git a/samples/DrawWithImageSharp/DrawWithImageSharp.csproj b/samples/DrawWithImageSharp/DrawWithImageSharp.csproj index 94e5f44e..58f71516 100644 --- a/samples/DrawWithImageSharp/DrawWithImageSharp.csproj +++ b/samples/DrawWithImageSharp/DrawWithImageSharp.csproj @@ -1,4 +1,4 @@ - + portable @@ -46,7 +46,7 @@ - + diff --git a/src/SixLabors.Fonts/FileFontMetrics.cs b/src/SixLabors.Fonts/FileFontMetrics.cs index eadd93ca..4a30c357 100644 --- a/src/SixLabors.Fonts/FileFontMetrics.cs +++ b/src/SixLabors.Fonts/FileFontMetrics.cs @@ -107,6 +107,10 @@ internal override bool TryGetGlyphId( out bool skipNextCodePoint) => this.fontMetrics.Value.TryGetGlyphId(codePoint, nextCodePoint, out glyphId, out skipNextCodePoint); + /// + internal override bool TryGetCodePoint(ushort glyphId, out CodePoint codePoint) + => this.fontMetrics.Value.TryGetCodePoint(glyphId, out codePoint); + /// internal override bool TryGetGlyphClass(ushort glyphId, [NotNullWhen(true)] out GlyphClassDef? glyphClass) => this.fontMetrics.Value.TryGetGlyphClass(glyphId, out glyphClass); diff --git a/src/SixLabors.Fonts/FontMetrics.cs b/src/SixLabors.Fonts/FontMetrics.cs index 0c727b0f..d108f1fa 100644 --- a/src/SixLabors.Fonts/FontMetrics.cs +++ b/src/SixLabors.Fonts/FontMetrics.cs @@ -141,6 +141,19 @@ internal FontMetrics() /// internal abstract bool TryGetGlyphId(CodePoint codePoint, CodePoint? nextCodePoint, out ushort glyphId, out bool skipNextCodePoint); + /// + /// Gets the specified glyph id matching the codepoint. + /// + /// The glyph identifier. + /// + /// When this method returns, contains the codepoint associated with the specified glyph id, + /// if the glyph id is found; otherwise, default. + /// + /// + /// if the face contains a codepoint for the specified glyph id; otherwise, . + /// + internal abstract bool TryGetCodePoint(ushort glyphId, out CodePoint codePoint); + /// /// Tries to get the glyph class for a given glyph id. /// The font needs to have a GDEF table defined. diff --git a/src/SixLabors.Fonts/GlyphPositioningCollection.cs b/src/SixLabors.Fonts/GlyphPositioningCollection.cs index 6da0d880..ffb61a9f 100644 --- a/src/SixLabors.Fonts/GlyphPositioningCollection.cs +++ b/src/SixLabors.Fonts/GlyphPositioningCollection.cs @@ -79,6 +79,8 @@ public void DisableShapingFeature(int index, Tag feature) /// /// The zero-based index within the input codepoint collection. /// The font size in PT units of the font containing this glyph. + /// Whether the glyph is the result of a 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, @@ -86,17 +88,39 @@ public void DisableShapingFeature(int index, Tag feature) /// This parameter is passed uninitialized. /// /// The metrics. - public bool TryGetGlyphMetricsAtOffset(int offset, out float pointSize, out bool isDecomposed, [NotNullWhen(true)] out IReadOnlyList? metrics) + public bool TryGetGlyphMetricsAtOffset( + int offset, + out float pointSize, + out bool isSubstituted, + out bool isVerticalSubstitution, + out bool isDecomposed, + [NotNullWhen(true)] out IReadOnlyList? metrics) { List match = new(); pointSize = 0; + isSubstituted = false; + isVerticalSubstitution = false; isDecomposed = false; + + Tag vert = FeatureTags.VerticalAlternates; + Tag vrt2 = FeatureTags.VerticalAlternatesAndRotation; + Tag vrtr = FeatureTags.VerticalAlternatesForRotation; + for (int i = 0; i < this.glyphs.Count; i++) { if (this.glyphs[i].Offset == offset) { GlyphPositioningData glyph = this.glyphs[i]; + 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.AddRange(glyph.Metrics); } @@ -147,7 +171,7 @@ public bool TryUpdate(Font font, GlyphSubstitutionCollection collection) // 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); + List metrics = new(data.Count); TextAttributes textAttributes = shape.TextRun.TextAttributes; TextDecorations textDecorations = shape.TextRun.TextDecorations; foreach (GlyphMetrics gm in fontMetrics.GetGlyphMetrics(codePoint, id, textAttributes, textDecorations, layoutMode, colorFontSupport)) @@ -216,6 +240,10 @@ public bool TryAdd(Font font, GlyphSubstitutionCollection collection) LayoutMode layoutMode = this.TextOptions.LayoutMode; ColorFontSupport colorFontSupport = this.TextOptions.ColorFontSupport; + Tag vert = FeatureTags.VerticalAlternates; + Tag vrt2 = FeatureTags.VerticalAlternatesAndRotation; + Tag vrtr = FeatureTags.VerticalAlternatesForRotation; + for (int i = 0; i < collection.Count; i++) { GlyphShapingData data = collection.GetGlyphShapingData(i, out int offset); @@ -227,7 +255,14 @@ public bool TryAdd(Font font, GlyphSubstitutionCollection collection) // cache the original in the font metrics and only update our collection. TextAttributes textAttributes = data.TextRun.TextAttributes; TextDecorations textDecorations = data.TextRun.TextDecorations; - bool isVerticalLayout = AdvancedTypographicUtils.IsVerticalGlyph(codePoint, layoutMode); + + bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(codePoint, layoutMode); + foreach (Tag feature in data.AppliedFeatures) + { + isVertical |= feature == vert; + isVertical |= feature == vrt2; + isVertical |= feature == vrtr; + } foreach (GlyphMetrics gm in fontMetrics.GetGlyphMetrics(codePoint, id, textAttributes, textDecorations, layoutMode, colorFontSupport)) { @@ -242,7 +277,7 @@ public bool TryAdd(Font font, GlyphSubstitutionCollection collection) if (metrics.Count > 0) { GlyphMetrics[] gm = metrics.ToArray(); - if (isVerticalLayout) + if (isVertical) { this.glyphs.Add(new(offset, new(data, true) { Bounds = new(0, 0, 0, gm[0].AdvanceHeight) }, font.Size, gm)); } @@ -302,11 +337,25 @@ public void UpdatePosition(FontMetrics fontMetrics, int index) public void Advance(FontMetrics fontMetrics, int index, ushort glyphId, short dx, short dy) { LayoutMode layoutMode = this.TextOptions.LayoutMode; - foreach (GlyphMetrics m in this.glyphs[index].Metrics) + Tag vert = FeatureTags.VerticalAlternates; + Tag vrt2 = FeatureTags.VerticalAlternatesAndRotation; + Tag vrtr = FeatureTags.VerticalAlternatesForRotation; + + GlyphPositioningData glyph = this.glyphs[index]; + foreach (GlyphMetrics m in glyph.Metrics) { if (m.GlyphId == glyphId && fontMetrics == m.FontMetrics) { - m.ApplyAdvance(dx, AdvancedTypographicUtils.IsVerticalGlyph(m.CodePoint, layoutMode) ? dy : (short)0); + 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); } } } diff --git a/src/SixLabors.Fonts/GlyphShapingData.cs b/src/SixLabors.Fonts/GlyphShapingData.cs index f5b9f2f2..a69ae3f1 100644 --- a/src/SixLabors.Fonts/GlyphShapingData.cs +++ b/src/SixLabors.Fonts/GlyphShapingData.cs @@ -37,6 +37,7 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) this.LigatureComponent = data.LigatureComponent; this.MarkAttachment = data.MarkAttachment; this.CursiveAttachment = data.CursiveAttachment; + this.IsSubstituted = data.IsSubstituted; this.IsDecomposed = data.IsDecomposed; if (data.UniversalShapingEngineInfo != null) { @@ -57,9 +58,11 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) if (!clearFeatures) { - this.Features = new(data.Features); + this.Features.AddRange(data.Features); } + this.AppliedFeatures.AddRange(data.AppliedFeatures); + this.Bounds = data.Bounds; } @@ -116,7 +119,12 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) /// /// Gets or sets the collection of features. /// - public List Features { get; set; } = new List(); + public List Features { get; set; } = new(); + + /// + /// Gets or sets the collection of applied features. + /// + public List AppliedFeatures { get; set; } = new(); /// /// Gets or sets the shaping bounds. diff --git a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs index a9b597ff..f213b681 100644 --- a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs +++ b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs @@ -225,7 +225,8 @@ public bool TryGetGlyphShapingDataAtOffset(int offset, [NotNullWhen(true)] out I /// /// The zero-based index of the element to replace. /// The replacement glyph id. - public void Replace(int index, ushort glyphId) + /// The feature to apply to the glyph at the specified index. + public void Replace(int index, ushort glyphId, Tag feature) { GlyphShapingData current = this.glyphs[index].Data; current.GlyphId = glyphId; @@ -234,6 +235,7 @@ public void Replace(int index, ushort glyphId) current.MarkAttachment = -1; current.CursiveAttachment = -1; current.IsSubstituted = true; + current.AppliedFeatures.Add(feature); } /// @@ -243,7 +245,8 @@ public void Replace(int index, ushort glyphId) /// The indices at which to remove elements. /// The replacement glyph id. /// The ligature id. - public void Replace(int index, ReadOnlySpan removalIndices, ushort glyphId, int ligatureId) + /// The feature to apply to the glyph at the specified index. + public void Replace(int index, ReadOnlySpan removalIndices, ushort glyphId, int ligatureId, Tag feature) { // Remove the glyphs at each index. int codePointCount = 0; @@ -279,6 +282,7 @@ public void Replace(int index, ReadOnlySpan removalIndices, ushort glyphId, current.MarkAttachment = -1; current.CursiveAttachment = -1; current.IsSubstituted = true; + current.AppliedFeatures.Add(feature); } /// @@ -287,7 +291,8 @@ public void Replace(int index, ReadOnlySpan removalIndices, ushort glyphId, /// 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) + /// The feature to apply to the glyph at the specified index. + public void Replace(int index, int count, ushort glyphId, Tag feature) { // Remove the glyphs at each index. int codePointCount = 0; @@ -322,6 +327,7 @@ public void Replace(int index, int count, ushort glyphId) current.MarkAttachment = -1; current.CursiveAttachment = -1; current.IsSubstituted = true; + current.AppliedFeatures.Add(feature); } /// @@ -329,7 +335,8 @@ public void Replace(int index, int count, ushort glyphId) /// /// The zero-based index of the element to replace. /// The collection of replacement glyph ids. - public void Replace(int index, ReadOnlySpan glyphIds) + /// The feature to apply to the glyph at the specified index. + public void Replace(int index, ReadOnlySpan glyphIds, Tag feature) { if (glyphIds.Length > 0) { @@ -345,7 +352,7 @@ public void Replace(int index, ReadOnlySpan glyphIds) // Add additional glyphs from the rest of the sequence. if (glyphIds.Length > 1) { - glyphIds = glyphIds.Slice(1); + glyphIds = glyphIds[1..]; for (int i = 0; i < glyphIds.Length; i++) { GlyphShapingData data = new(current, false) @@ -354,6 +361,8 @@ public void Replace(int index, ReadOnlySpan glyphIds) LigatureComponent = i + 1 }; + data.AppliedFeatures.Add(feature); + this.glyphs.Insert(++index, new(pair.Offset, data)); } } diff --git a/src/SixLabors.Fonts/StreamFontMetrics.cs b/src/SixLabors.Fonts/StreamFontMetrics.cs index afb1b9f1..aa579337 100644 --- a/src/SixLabors.Fonts/StreamFontMetrics.cs +++ b/src/SixLabors.Fonts/StreamFontMetrics.cs @@ -32,6 +32,7 @@ internal partial class StreamFontMetrics : FontMetrics private readonly ConcurrentDictionary<(int CodePoint, ushort Id, TextAttributes Attributes, bool IsVerticalLayout), GlyphMetrics[]> glyphCache; private readonly ConcurrentDictionary<(int CodePoint, ushort Id, TextAttributes Attributes, bool IsVerticalLayout), GlyphMetrics[]>? colorGlyphCache; private readonly ConcurrentDictionary<(int CodePoint, int NextCodePoint), (bool Success, ushort GlyphId, bool SkipNextCodePoint)> glyphIdCache; + private readonly ConcurrentDictionary codePointCache; private readonly FontDescription description; private readonly HorizontalMetrics horizontalMetrics; private readonly VerticalMetrics verticalMetrics; @@ -61,6 +62,7 @@ internal StreamFontMetrics(TrueTypeFontTables tables) this.outlineType = OutlineType.TrueType; this.description = new FontDescription(tables.Name, tables.Os2, tables.Head); this.glyphIdCache = new(); + this.codePointCache = new(); this.glyphCache = new(); if (tables.Colr is not null) { @@ -82,6 +84,7 @@ internal StreamFontMetrics(CompactFontTables tables) this.outlineType = OutlineType.CFF; this.description = new FontDescription(tables.Name, tables.Os2, tables.Head); this.glyphIdCache = new(); + this.codePointCache = new(); this.glyphCache = new(); if (tables.Colr is not null) { @@ -174,6 +177,26 @@ internal override bool TryGetGlyphId(CodePoint codePoint, CodePoint? nextCodePoi return success; } + /// + internal override bool TryGetCodePoint(ushort glyphId, out CodePoint codePoint) + { + CMapTable cmap = this.outlineType == OutlineType.TrueType + ? this.trueTypeFontTables!.Cmap + : this.compactFontTables!.Cmap; + + (bool success, CodePoint value) = this.codePointCache.GetOrAdd( + glyphId, + static (glyphId, arg) => + { + bool success = arg.TryGetCodePoint(glyphId, out CodePoint codePoint); + return (success, codePoint); + }, + cmap); + + codePoint = value; + return success; + } + /// internal override bool TryGetGlyphClass(ushort glyphId, [NotNullWhen(true)] out GlyphClassDef? glyphClass) { diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs index 28a82f1d..81ebdf26 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs @@ -74,7 +74,7 @@ public override bool TrySubstitution( if (this.coverageTable.CoverageIndexOf(glyphId) > -1) { - collection.Replace(index, (ushort)(glyphId + this.deltaGlyphId)); + collection.Replace(index, (ushort)(glyphId + this.deltaGlyphId), feature); return true; } @@ -135,7 +135,7 @@ public override bool TrySubstitution( if (offset > -1) { - collection.Replace(index, this.substituteGlyphs[offset]); + collection.Replace(index, this.substituteGlyphs[offset], feature); return true; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs index 581dc8cb..d01b076a 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs @@ -98,7 +98,7 @@ public override bool TrySubstitution( if (offset > -1) { - collection.Replace(index, this.sequenceTables[offset].SubstituteGlyphs); + collection.Replace(index, this.sequenceTables[offset].SubstituteGlyphs, feature); return true; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs index 3698106f..60c79d99 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs @@ -100,7 +100,7 @@ public override bool TrySubstitution( // TODO: We're just choosing the first alternative here. // It looks like the choice is arbitrary and should be determined by // the client. - collection.Replace(index, this.alternateSetTables[offset].AlternateGlyphs[0]); + collection.Replace(index, this.alternateSetTables[offset].AlternateGlyphs[0], feature); return true; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs index dd7716cd..896c74eb 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs @@ -249,7 +249,7 @@ public override bool TrySubstitution( } // Delete the matched glyphs, and replace the current glyph with the ligature glyph - collection.Replace(index, matches, ligatureTable.GlyphId, ligatureId); + collection.Replace(index, matches, ligatureTable.GlyphId, ligatureId, feature); return true; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType8SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType8SubTable.cs index 9b2ae829..ec009404 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType8SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType8SubTable.cs @@ -138,7 +138,7 @@ public override bool TrySubstitution( bool hasChanged = false; for (int i = 0; i < this.substituteGlyphIds.Length; i++) { - collection.Replace(index + i, this.substituteGlyphIds[i]); + collection.Replace(index + i, this.substituteGlyphIds[i], feature); hasChanged = true; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs index ee608464..2bc857cf 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs @@ -86,12 +86,9 @@ protected override void PlanPreprocessingFeatures(IGlyphShapingCollection collec this.AddFeature(collection, index, count, RvnrTag); // Add directional features. - LayoutMode layoutMode = collection.TextOptions.LayoutMode; - bool isVerticalLayout = false; for (int i = index; i < count; i++) { GlyphShapingData shapingData = collection[i]; - isVerticalLayout |= AdvancedTypographicUtils.IsVerticalGlyph(shapingData.CodePoint, layoutMode); if (shapingData.Direction == TextDirection.LeftToRight) { diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs index 82a347ca..5888e94a 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs @@ -234,7 +234,7 @@ private int DecomposeGlyph(GlyphSubstitutionCollection collection, GlyphShapingD ii[0] = ljmo; ii[1] = vjmo; - collection.Replace(index, ii); + collection.Replace(index, ii, FeatureTags.GlyphCompositionDecomposition); collection.EnableShapingFeature(index, LjmoTag); collection.EnableShapingFeature(index + 1, VjmoTag); return index + 1; @@ -245,7 +245,7 @@ private int DecomposeGlyph(GlyphSubstitutionCollection collection, GlyphShapingD iii[1] = vjmo; iii[0] = ljmo; - collection.Replace(index, iii); + collection.Replace(index, iii, FeatureTags.GlyphCompositionDecomposition); collection.EnableShapingFeature(index, LjmoTag); collection.EnableShapingFeature(index + 1, VjmoTag); collection.EnableShapingFeature(index + 2, TjmoTag); @@ -311,7 +311,7 @@ private int ComposeGlyph(GlyphSubstitutionCollection collection, GlyphShapingDat { int del = prevType == V ? 3 : 2; int idx = index - del + 1; - collection.Replace(idx, del - 1, id); + collection.Replace(idx, del - 1, id, FeatureTags.GlyphCompositionDecomposition); collection[idx].CodePoint = s; return idx; } @@ -412,7 +412,7 @@ private int InsertDottedCircle(GlyphSubstitutionCollection collection, GlyphShap glyphs[0] = id; } - collection.Replace(index, glyphs); + collection.Replace(index, glyphs, FeatureTags.GlyphCompositionDecomposition); return index + 1; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs index b77d1a66..6fe5900e 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs @@ -121,7 +121,7 @@ protected override void AssignFeatures(IGlyphShapingCollection collection, int i ids[j] = id; } - substitutionCollection.Replace(i, ids); + substitutionCollection.Replace(i, ids, FeatureTags.GlyphCompositionDecomposition); for (int j = 0; j < decompositions.Length; j++) { substitutionCollection[i + j].CodePoint = new(decompositions[j]); @@ -274,7 +274,7 @@ private void InitialReorder(IGlyphShapingCollection collection, int index, int c glyphs[0] = current.GlyphId; glyphs[1] = id; - substitutionCollection.Replace(i, glyphs); + substitutionCollection.Replace(i, glyphs, FeatureTags.GlyphCompositionDecomposition); // Update shaping info for newly inserted data. GlyphShapingData dotted = substitutionCollection[i + 1]; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs index d2575439..05ee92f8 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs @@ -104,7 +104,7 @@ private static void DecomposeSplitVowels(IGlyphShapingCollection collection, int ids[j] = id; } - substitutionCollection.Replace(i, ids); + substitutionCollection.Replace(i, ids, FeatureTags.GlyphCompositionDecomposition); for (int j = 0; j < decompositions.Length; j++) { substitutionCollection[i + j].CodePoint = new(decompositions[j]); @@ -256,7 +256,7 @@ private static void Reorder(IGlyphShapingCollection collection, int index, int c glyphs[0] = current.GlyphId; glyphs[1] = id; - substitutionCollection.Replace(i, glyphs); + substitutionCollection.Replace(i, glyphs, FeatureTags.GlyphCompositionDecomposition); end++; max++; } diff --git a/src/SixLabors.Fonts/Tables/General/CMap/CMapSubTable.cs b/src/SixLabors.Fonts/Tables/General/CMap/CMapSubTable.cs index 30d1a39b..934c4009 100644 --- a/src/SixLabors.Fonts/Tables/General/CMap/CMapSubTable.cs +++ b/src/SixLabors.Fonts/Tables/General/CMap/CMapSubTable.cs @@ -27,5 +27,7 @@ public CMapSubTable(PlatformIDs platform, ushort encoding, ushort format) public abstract bool TryGetGlyphId(CodePoint codePoint, out ushort glyphId); + public abstract bool TryGetCodePoint(ushort glyphId, out CodePoint codePoint); + public abstract IEnumerable GetAvailableCodePoints(); } diff --git a/src/SixLabors.Fonts/Tables/General/CMap/Format0SubTable.cs b/src/SixLabors.Fonts/Tables/General/CMap/Format0SubTable.cs index 2d686da8..19a04897 100644 --- a/src/SixLabors.Fonts/Tables/General/CMap/Format0SubTable.cs +++ b/src/SixLabors.Fonts/Tables/General/CMap/Format0SubTable.cs @@ -32,6 +32,21 @@ public override bool TryGetGlyphId(CodePoint codePoint, out ushort glyphId) return true; } + public override bool TryGetCodePoint(ushort glyphId, out CodePoint codePoint) + { + for (int i = 0; i < this.GlyphIds.Length; i++) + { + if (this.GlyphIds[i] == glyphId) + { + codePoint = new CodePoint(i); + return true; + } + } + + codePoint = default; + return false; + } + public override IEnumerable GetAvailableCodePoints() => Enumerable.Range(0, this.GlyphIds.Length); diff --git a/src/SixLabors.Fonts/Tables/General/CMap/Format12SubTable.cs b/src/SixLabors.Fonts/Tables/General/CMap/Format12SubTable.cs index bd9607a4..2a30b6a9 100644 --- a/src/SixLabors.Fonts/Tables/General/CMap/Format12SubTable.cs +++ b/src/SixLabors.Fonts/Tables/General/CMap/Format12SubTable.cs @@ -38,6 +38,25 @@ public override bool TryGetGlyphId(CodePoint codePoint, out ushort glyphId) return false; } + public override bool TryGetCodePoint(ushort glyphId, out CodePoint codePoint) + { + for (int i = 0; i < this.SequentialMapGroups.Length; i++) + { + ref SequentialMapGroup seg = ref this.SequentialMapGroups[i]; + if (glyphId >= seg.StartGlyphId && glyphId <= seg.StartGlyphId + seg.EndCodePoint - seg.StartCodePoint) + { + // Reverse the calculation: + // Forward: glyphId = (codePoint - StartCodePoint) + StartGlyphId + // Reverse: codePoint = (glyphId - StartGlyphId) + StartCodePoint + codePoint = new CodePoint(glyphId - seg.StartGlyphId + seg.StartCodePoint); + return true; + } + } + + codePoint = default; + return false; + } + public override IEnumerable GetAvailableCodePoints() => this.SequentialMapGroups.SelectMany(segment => { diff --git a/src/SixLabors.Fonts/Tables/General/CMap/Format14SubTable.cs b/src/SixLabors.Fonts/Tables/General/CMap/Format14SubTable.cs index 8fb300a0..bf4f2617 100644 --- a/src/SixLabors.Fonts/Tables/General/CMap/Format14SubTable.cs +++ b/src/SixLabors.Fonts/Tables/General/CMap/Format14SubTable.cs @@ -136,6 +136,12 @@ public override bool TryGetGlyphId(CodePoint codePoint, out ushort glyphId) return false; } + public override bool TryGetCodePoint(ushort glyphId, out CodePoint codePoint) + { + codePoint = default; + return false; + } + public override IEnumerable GetAvailableCodePoints() => Array.Empty(); diff --git a/src/SixLabors.Fonts/Tables/General/CMap/Format4SubTable.cs b/src/SixLabors.Fonts/Tables/General/CMap/Format4SubTable.cs index 95a8060e..8dc307ca 100644 --- a/src/SixLabors.Fonts/Tables/General/CMap/Format4SubTable.cs +++ b/src/SixLabors.Fonts/Tables/General/CMap/Format4SubTable.cs @@ -37,19 +37,62 @@ public override bool TryGetGlyphId(CodePoint codePoint, out ushort glyphId) glyphId = (ushort)((charAsInt + seg.Delta) & ushort.MaxValue); return true; } - else + + long offset = (seg.Offset / 2) + (charAsInt - seg.Start); + glyphId = this.GlyphIds[offset - this.Segments.Length + seg.Index]; + + return true; + } + } + + glyphId = 0; + return false; + } + + public override bool TryGetCodePoint(ushort glyphId, out CodePoint codePoint) + { + for (int i = 0; i < this.Segments.Length; i++) + { + ref Segment seg = ref this.Segments[i]; + + if (seg.Offset == 0) + { + // Reverse the delta-based calculation + // Forward was: glyphId = (charAsInt + seg.Delta) & 0xFFFF + // Reverse should apply the inverse logic with the same wrap: + int candidate = (glyphId - seg.Delta) & ushort.MaxValue; + + if (candidate >= seg.Start && candidate <= seg.End) { - long offset = (seg.Offset / 2) + (charAsInt - seg.Start); - glyphId = this.GlyphIds[offset - this.Segments.Length + seg.Index]; + codePoint = new CodePoint(candidate); return true; } } + else + { + // Reverse the offset-based calculation: + // Forward logic: + // offset = (seg.Offset / 2) + (charAsInt - seg.Start) + // glyphId = GlyphIds[offset - Segments.Length + seg.Index] + + // To reverse, iterate over possible codepoints in the segment and find the matching glyphId. + for (long j = 0; j <= (seg.End - seg.Start); j++) + { + long offset = (seg.Offset / 2) + j; + if (this.GlyphIds[offset - this.Segments.Length + seg.Index] == glyphId) + { + codePoint = new CodePoint((int)(seg.Start + j)); + return true; + } + } + } } - glyphId = 0; + codePoint = default; return false; } + public override IEnumerable GetAvailableCodePoints() => this.Segments.SelectMany(segment => Enumerable.Range(segment.Start, segment.End - segment.Start + 1)); diff --git a/src/SixLabors.Fonts/Tables/General/CMapTable.cs b/src/SixLabors.Fonts/Tables/General/CMapTable.cs index 30c39a75..8bca9c38 100644 --- a/src/SixLabors.Fonts/Tables/General/CMapTable.cs +++ b/src/SixLabors.Fonts/Tables/General/CMapTable.cs @@ -67,12 +67,9 @@ private bool TryGetGlyphId(CodePoint codePoint, out ushort glyphId) // Regardless of the encoding scheme, character codes that do // not correspond to any glyph in the font should be mapped to glyph index 0. // The glyph at this location must be a special glyph representing a missing character, commonly known as .notdef. - if (t.TryGetGlyphId(codePoint, out glyphId)) + if (t.TryGetGlyphId(codePoint, out glyphId) && glyphId > 0) { - if (glyphId > 0) - { - return true; - } + return true; } } @@ -80,6 +77,20 @@ private bool TryGetGlyphId(CodePoint codePoint, out ushort glyphId) return false; } + public bool TryGetCodePoint(ushort glyphId, out CodePoint codePoint) + { + foreach (CMapSubTable t in this.Tables) + { + if (t.TryGetCodePoint(glyphId, out codePoint)) + { + return true; + } + } + + codePoint = default; + return false; + } + /// /// Gets the unicode codepoints for which a glyph exists in the font. /// diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index 0f521d1c..195bde94 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Numerics; +using SixLabors.Fonts.Tables.AdvancedTypographic; using SixLabors.Fonts.Unicode; namespace SixLabors.Fonts; @@ -534,10 +535,9 @@ private static IEnumerable LayoutLineVertical( int j = 0; foreach (GlyphMetrics metric in data.Metrics) { - // Align the glyph horizontally and vertically centering horizontally around the baseline. + // Align the glyph horizontally and vertically centering vertically around the baseline. Vector2 scale = new Vector2(data.PointSize) / metric.ScaleFactor; - float oX = (data.ScaledLineHeight - (metric.Bounds.Size().X * scale.X)) * .5F; - Vector2 offset = new(oX, (metric.Bounds.Max.Y + metric.TopSideBearing) * scale.Y); + Vector2 offset = new(0, (metric.Bounds.Max.Y + metric.TopSideBearing) * scale.Y); glyphs.Add(new GlyphLayout( new Glyph(metric, data.PointSize), @@ -678,7 +678,7 @@ private static IEnumerable LayoutLineVerticalMixed( new Glyph(metric, data.PointSize), boxLocation, penLocation + new Vector2(((scaledMaxLineHeight - data.ScaledLineHeight) * .5F) + data.ScaledDescender, 0), - Vector2.Zero, + Vector2.Zero, // TODO: We need to shift so the baseline is moved right. advanceX, data.ScaledAdvance, GlyphLayoutMode.VerticalRotated, @@ -694,10 +694,9 @@ private static IEnumerable LayoutLineVerticalMixed( int j = 0; foreach (GlyphMetrics metric in data.Metrics) { - // Align the glyph horizontally and vertically centering horizontally around the baseline. + // Align the glyph horizontally and vertically centering vertically around the baseline. Vector2 scale = new Vector2(data.PointSize) / metric.ScaleFactor; - float oX = (data.ScaledLineHeight - (metric.Bounds.Size().X * scale.X)) * .5F; - Vector2 offset = new(oX, (metric.Bounds.Max.Y + metric.TopSideBearing) * scale.Y); + Vector2 offset = new(0, (metric.Bounds.Max.Y + metric.TopSideBearing) * scale.Y); glyphs.Add(new GlyphLayout( new Glyph(metric, data.PointSize), @@ -828,7 +827,7 @@ private static void SubstituteBidiMirrors(FontMetrics fontMetrics, GlyphSubstitu if (fontMetrics.TryGetGlyphId(mirror, out ushort glyphId)) { - collection.Replace(i, glyphId); + collection.Replace(i, glyphId, FeatureTags.RightToLeftMirroredForms); } } @@ -854,7 +853,7 @@ private static void SubstituteBidiMirrors(FontMetrics fontMetrics, GlyphSubstitu if (fontMetrics.TryGetGlyphId(mirror, out ushort glyphId)) { - collection.Replace(i, glyphId); + collection.Replace(i, glyphId, FeatureTags.VerticalAlternates); } } } @@ -907,7 +906,13 @@ private static TextBox BreakLines( SpanCodePointEnumerator codePointEnumerator = new(graphemeEnumerator.Current); while (codePointEnumerator.MoveNext()) { - if (!positionings.TryGetGlyphMetricsAtOffset(codePointIndex, out float pointSize, out bool isDecomposed, out IReadOnlyList? metrics)) + if (!positionings.TryGetGlyphMetricsAtOffset( + codePointIndex, + out float pointSize, + out bool isSubstituted, + out bool isVerticalSubstitution, + out bool isDecomposed, + out IReadOnlyList? metrics)) { // Codepoint was skipped during original enumeration. codePointIndex++; @@ -915,11 +920,31 @@ private static TextBox BreakLines( continue; } - // Determine whether the glyph advance should be calculated using vertical or horizontal metrics - // For vertical mixed layout we will be rotating glyphs with the vertical orientation type R or TR. + GlyphMetrics glyph = metrics[0]; + + // Retrieve the current codepoint from the enumerator. + // If the glyph represents a substituted codepoint and the substitution is a single codepoint substitution, + // or composite glyph, then the codepoint should be updated to the substitution value so we can read its properties. + // Substitutions that are decomposed glyphs will have multiple metrics and any layout should be based on the + // original codepoint. + // + // Note: Not all glyphs in a font will have a codepoint associated with them. e.g. most compositions, ligatures, etc. CodePoint codePoint = codePointEnumerator.Current; - VerticalOrientationType verticalOrientationType = CodePoint.GetVerticalOrientationType(codePoint); - bool isRotated = isVerticalMixedLayout && verticalOrientationType is VerticalOrientationType.Rotate or VerticalOrientationType.TransformRotate; + if (isSubstituted && + metrics.Count == 1 && + glyph.FontMetrics.TryGetCodePoint(glyph.GlyphId, out CodePoint substitution)) + { + codePoint = substitution; + } + + // Determine whether the glyph advance should be calculated using vertical or horizontal metrics + // For vertical mixed layout we will rotate glyphs with the vertical orientation type R or TR + // which do not already have a vertical substitution. + bool isRotated = isVerticalMixedLayout && + !isVerticalSubstitution && + CodePoint.GetVerticalOrientationType(codePoint) is + VerticalOrientationType.Rotate or + VerticalOrientationType.TransformRotate; if (CodePoint.IsVariationSelector(codePoint)) { @@ -929,8 +954,6 @@ private static TextBox BreakLines( } // Calculate the advance for the current codepoint. - GlyphMetrics glyph = metrics[0]; - float glyphAdvance; // This should never happen, but we need to ensure that the buffer is large enough diff --git a/src/SixLabors.Fonts/Unicode/UnicodeUtility.cs b/src/SixLabors.Fonts/Unicode/UnicodeUtility.cs index 2c2447d7..fa177e30 100644 --- a/src/SixLabors.Fonts/Unicode/UnicodeUtility.cs +++ b/src/SixLabors.Fonts/Unicode/UnicodeUtility.cs @@ -15,6 +15,7 @@ internal static class UnicodeUtility /// /// Per http://www.unicode.org/glossary/#ASCII, ASCII is only U+0000..U+007F. /// + /// The codepoint to test. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsAsciiCodePoint(uint value) => value <= 0x7Fu; @@ -22,6 +23,7 @@ internal static class UnicodeUtility /// Returns if is in the /// Basic Multilingual Plane (BMP). /// + /// The codepoint to test. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsBmpCodePoint(uint value) => value <= 0xFFFFu; @@ -32,6 +34,7 @@ internal static class UnicodeUtility ///
/// ///
+ /// The codepoint to test. /// /// The representing the mirror or 0u if not found. /// @@ -140,32 +143,14 @@ public static uint GetVerticalMirror(uint value) /// /// /// - /// - /// - /// - /// + /// /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] + /// The codepoint to test. + /// + /// if is a CJK code point. + /// public static bool IsCJKCodePoint(uint value) { - // Hiragana - if (IsInRangeInclusive(value, 0x3040u, 0x309Fu)) - { - return true; - } - - // Katakana - if (IsInRangeInclusive(value, 0x30A0u, 0x30FFu)) - { - return true; - } - - // Hangul Syllables - if (IsInRangeInclusive(value, 0xAC00u, 0xD7A3u)) - { - return true; - } - // CJK Unified Ideographs if (IsInRangeInclusive(value, 0x4E00u, 0x9FFFu)) { @@ -214,6 +199,12 @@ public static bool IsCJKCodePoint(uint value) return true; } + // CJK Unified Ideographs Extension H + if (IsInRangeInclusive(value, 0x31350u, 0x323AFu)) + { + return true; + } + // CJK Compatibility Ideographs if (IsInRangeInclusive(value, 0xF900u, 0xFAFFu)) { @@ -226,6 +217,120 @@ public static bool IsCJKCodePoint(uint value) return true; } + // CJK Compatibility + if (IsInRangeInclusive(value, 0x3300u, 0x33FFu)) + { + return true; + } + + // CJK Radicals Supplement + if (IsInRangeInclusive(value, 0x2E80u, 0x2EFFu)) + { + return true; + } + + // Kangxi Radicals + if (IsInRangeInclusive(value, 0x2F00u, 0x2FDFu)) + { + return true; + } + + // Ideographic Description Characters + if (IsInRangeInclusive(value, 0x2FF0u, 0x2FFFu)) + { + return true; + } + + // CJK Strokes + if (IsInRangeInclusive(value, 0x31C0u, 0x31EFu)) + { + return true; + } + + // CJK Symbols and Punctuation + if (IsInRangeInclusive(value, 0x3000u, 0x303Fu)) + { + return true; + } + + // Hiragana + if (IsInRangeInclusive(value, 0x3040u, 0x309Fu)) + { + return true; + } + + // Katakana + if (IsInRangeInclusive(value, 0x30A0u, 0x30FFu)) + { + return true; + } + + // Katakana Phonetic Extensions + if (IsInRangeInclusive(value, 0x31F0u, 0x31FFu)) + { + return true; + } + + // Hangul Syllables + if (IsInRangeInclusive(value, 0xAC00u, 0xD7AFu)) + { + return true; + } + + // Hangul Jamo + if (IsInRangeInclusive(value, 0x1100u, 0x11FFu)) + { + return true; + } + + // Hangul Jamo Extended-A + if (IsInRangeInclusive(value, 0xA960u, 0xA97Fu)) + { + return true; + } + + // Hangul Jamo Extended-B + if (IsInRangeInclusive(value, 0xD7B0u, 0xD7FFu)) + { + return true; + } + + // Bopomofo + if (IsInRangeInclusive(value, 0x3100u, 0x312Fu)) + { + return true; + } + + // Bopomofo Extended + if (IsInRangeInclusive(value, 0x31A0u, 0x31BFu)) + { + return true; + } + + // Enclosed CJK Letters and Months + if (IsInRangeInclusive(value, 0x3200u, 0x32FFu)) + { + return true; + } + + // Enclosed Ideographic Supplement + if (IsInRangeInclusive(value, 0x1F200u, 0x1F2FFu)) + { + return true; + } + + // Vertical Forms + if (IsInRangeInclusive(value, 0xFE10u, 0xFE1Fu)) + { + return true; + } + + // Halfwidth and Fullwidth Forms + if (IsInRangeInclusive(value, 0xFF00u, 0xFFEFu)) + { + return true; + } + return false; } @@ -439,6 +544,7 @@ public static bool ShouldRenderWhiteSpaceOnly(CodePoint codePoint) /// /// Returns the Unicode plane (0 through 16, inclusive) which contains this code point. /// + /// The code point. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetPlane(uint codePoint) { @@ -450,6 +556,7 @@ public static int GetPlane(uint codePoint) /// /// Given a Unicode scalar value, gets the number of UTF-16 code units required to represent this value. /// + /// The code point. public static int GetUtf16SequenceLength(uint codePoint) { DebugAssertIsValidCodePoint(codePoint); @@ -463,6 +570,7 @@ public static int GetUtf16SequenceLength(uint codePoint) /// /// Given a Unicode scalar value, gets the number of UTF-8 code units required to represent this value. /// + /// The code point. public static int GetUtf8SequenceLength(uint codePoint) { DebugAssertIsValidCodePoint(codePoint); @@ -506,6 +614,7 @@ public static int GetUtf8SequenceLength(uint codePoint) /// Returns if is a valid Unicode code /// point, i.e., is in [ U+0000..U+10FFFF ], inclusive. /// + /// The code point. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsValidCodePoint(uint codePoint) => codePoint <= 0x10FFFFu; @@ -513,6 +622,7 @@ public static int GetUtf8SequenceLength(uint codePoint) /// Returns if is a UTF-16 high surrogate code point, /// i.e., is in [ U+D800..U+DBFF ], inclusive. /// + /// The value to test. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsHighSurrogateCodePoint(uint value) => IsInRangeInclusive(value, 0xD800u, 0xDBFFu); @@ -521,6 +631,7 @@ public static bool IsHighSurrogateCodePoint(uint value) /// Returns if is a UTF-16 low surrogate code point, /// i.e., is in [ U+DC00..U+DFFF ], inclusive. /// + /// The value to test. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsLowSurrogateCodePoint(uint value) => IsInRangeInclusive(value, 0xDC00u, 0xDFFFu); @@ -529,6 +640,7 @@ public static bool IsLowSurrogateCodePoint(uint value) /// Returns if is a UTF-16 surrogate code point, /// i.e., is in [ U+D800..U+DFFF ], inclusive. /// + /// The value to test. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsSurrogateCodePoint(uint value) => IsInRangeInclusive(value, 0xD800u, 0xDFFFu); @@ -537,6 +649,9 @@ public static bool IsSurrogateCodePoint(uint value) /// Returns if is between /// and , inclusive. /// + /// The value to test. + /// The lower bound. + /// The upper bound. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRangeInclusive(uint value, uint lowerBound, uint upperBound) => (value - lowerBound) <= (upperBound - lowerBound); @@ -544,6 +659,8 @@ public static bool IsInRangeInclusive(uint value, uint lowerBound, uint upperBou /// /// Returns a Unicode scalar value from two code points representing a UTF-16 surrogate pair. /// + /// The high surrogate code point. + /// The low surrogate code point. public static uint GetScalarFromUtf16SurrogatePair(uint highSurrogateCodePoint, uint lowSurrogateCodePoint) { DebugAssertIsHighSurrogateCodePoint(highSurrogateCodePoint); @@ -559,6 +676,9 @@ public static uint GetScalarFromUtf16SurrogatePair(uint highSurrogateCodePoint, /// /// Decomposes an astral Unicode code point into UTF-16 high and low surrogate code units. /// + /// The Unicode code point. + /// The high surrogate code point. + /// The low surrogate code point. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void GetUtf16SurrogatesFromSupplementaryPlaneCodePoint(uint value, out char highSurrogateCodePoint, out char lowSurrogateCodePoint) { @@ -611,5 +731,6 @@ internal static void DebugAssertIsValidSupplementaryPlaneCodePoint(uint codePoin /// /// The input value doesn't have to be a real code point in the Unicode codespace. It can be any integer. /// + /// The code point. internal static string ToHexString(uint codePoint) => FormattableString.Invariant($"U+{codePoint:X4}"); } diff --git a/tests/SixLabors.Fonts.Tests/Fakes/FakeCmapSubtable.cs b/tests/SixLabors.Fonts.Tests/Fakes/FakeCmapSubtable.cs index e74ebc61..26155471 100644 --- a/tests/SixLabors.Fonts.Tests/Fakes/FakeCmapSubtable.cs +++ b/tests/SixLabors.Fonts.Tests/Fakes/FakeCmapSubtable.cs @@ -28,6 +28,21 @@ public override bool TryGetGlyphId(CodePoint codePoint, out ushort glyphId) return false; } + public override bool TryGetCodePoint(ushort glyphId, out CodePoint codePoint) + { + foreach (FakeGlyphSource c in this.glyphs) + { + if (c.Index == glyphId) + { + codePoint = c.CodePoint; + return true; + } + } + + codePoint = default; + return false; + } + public override IEnumerable GetAvailableCodePoints() => this.glyphs.Select(x => x.CodePoint.Value); } diff --git a/tests/SixLabors.Fonts.Tests/Unicode/UnicodeUtilityTests.cs b/tests/SixLabors.Fonts.Tests/Unicode/UnicodeUtilityTests.cs index f87fe633..e326f50a 100644 --- a/tests/SixLabors.Fonts.Tests/Unicode/UnicodeUtilityTests.cs +++ b/tests/SixLabors.Fonts.Tests/Unicode/UnicodeUtilityTests.cs @@ -10,7 +10,13 @@ public class UnicodeUtilityTests [Theory] [InlineData(0x3040u, 0x309Fu)] // Hiragana [InlineData(0x30A0u, 0x30FFu)] // Katakana + [InlineData(0x31F0u, 0x31FFu)] // Katakana Phonetic Extensions [InlineData(0xAC00u, 0xD7A3u)] // Hangul Syllables + [InlineData(0x1100u, 0x11FFu)] // Hangul Jamo + [InlineData(0xA960u, 0xA97Fu)] // Hangul Jamo Extended-A + [InlineData(0xD7B0u, 0xD7FFu)] // Hangul Jamo Extended-B + [InlineData(0x3100u, 0x312Fu)] // Bopomofo + [InlineData(0x31A0u, 0x31BFu)] // Bopomofo Extended [InlineData(0x4E00u, 0x9FFFu)] // CJK Unified Ideographs [InlineData(0x3400u, 0x4DBFu)] // CJK Unified Ideographs Extension A [InlineData(0x20000u, 0x2A6DFu)] // CJK Unified Ideographs Extension B @@ -21,6 +27,16 @@ public class UnicodeUtilityTests [InlineData(0x30000u, 0x3134Fu)] // CJK Unified Ideographs Extension G [InlineData(0xF900u, 0xFAFFu)] // CJK Compatibility Ideographs [InlineData(0x2F800u, 0x2FA1Fu)] // CJK Compatibility Ideographs Supplement + [InlineData(0x2E80u, 0x2EFFu)] // CJK Radicals Supplement + [InlineData(0x2F00u, 0x2FDFu)] // Kangxi Radicals + [InlineData(0x2FF0u, 0x2FFFu)] // Ideographic Description Characters + [InlineData(0x31C0u, 0x31EFu)] // CJK Strokes + [InlineData(0x3000u, 0x303Fu)] // CJK Symbols and Punctuation + [InlineData(0x3200u, 0x32FFu)] // Enclosed CJK Letters and Months + [InlineData(0x1F200u, 0x1F2FFu)] // Enclosed Ideographic Supplement + [InlineData(0x3300u, 0x33FFu)] // CJK Compatibility + [InlineData(0xFE10u, 0xFE1Fu)] // Vertical Forms + [InlineData(0xFF00u, 0xFFEFu)] // Halfwidth and Fullwidth Forms public void CanDetectCJKCodePoints(uint min, uint max) { for (uint i = min; i <= max; i++) From 7f4b7daa9a5f34b64a48f560fa50f4dd18b9d86c Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 13 Dec 2024 23:40:07 +1000 Subject: [PATCH 2/4] Align rotated glyphs --- src/SixLabors.Fonts/TextLayout.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index 195bde94..bf00d084 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -673,12 +673,27 @@ private static IEnumerable LayoutLineVerticalMixed( int j = 0; foreach (GlyphMetrics metric in data.Metrics) { + // Align the glyphs horizontally so the baseline is centered. Vector2 scale = new Vector2(data.PointSize) / metric.ScaleFactor; + + // Calculate the initial horizontal offset to center the glyph baseline: + // - Take half the difference between the max line height (scaledMaxLineHeight) + // and the current glyph's line height (data.ScaledLineHeight). + // - The line height includes both ascender and descender metrics. + float baselineDelta = (scaledMaxLineHeight - data.ScaledLineHeight) * .5F; + + // Adjust the horizontal offset further by considering the descender differences: + // - Subtract the current glyph's descender (data.ScaledDescender) to align it properly. + float descenderDelta = (Math.Abs(textLine.ScaledMaxDescender) - Math.Abs(data.ScaledDescender)) * .5F; + + // Final horizontal center offset combines the baseline and descender adjustments. + float centerOffsetX = (baselineDelta - data.ScaledDescender) + descenderDelta; + glyphs.Add(new GlyphLayout( new Glyph(metric, data.PointSize), boxLocation, - penLocation + new Vector2(((scaledMaxLineHeight - data.ScaledLineHeight) * .5F) + data.ScaledDescender, 0), - Vector2.Zero, // TODO: We need to shift so the baseline is moved right. + penLocation + new Vector2(centerOffsetX, 0), + Vector2.Zero, advanceX, data.ScaledAdvance, GlyphLayoutMode.VerticalRotated, From a32540da7319272f21b5f0cbcaf7f5b2455dd42d Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 16 Dec 2024 12:42:36 +1000 Subject: [PATCH 3/4] Add tests --- .../Issues/Issues_429.cs | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 tests/SixLabors.Fonts.Tests/Issues/Issues_429.cs diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_429.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_429.cs new file mode 100644 index 00000000..369a6775 --- /dev/null +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_429.cs @@ -0,0 +1,67 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tests.Issues; + +public class Issues_429 +{ + private static readonly ApproximateFloatComparer Comparer = new(.1F); + + [Fact] + public void VerticalMixedLayout_ExpectedRotation() + { + if (SystemFonts.TryGet("Yu Gothic", out FontFamily family)) + { + const string text = "あいうえお、「こんにちはー」。もしもし。ABCDEFG 日本語"; + Font font = family.CreateFont(30.0F); + + TextOptions options = new(font) + { + LayoutMode = LayoutMode.VerticalMixedRightLeft + }; + + IReadOnlyList glyphs = TextLayout.GenerateLayout(text.AsSpan(), options); + + // Only the Latin glyph + space should be rotated. + // Any other glyphs that appear rotated have actually been substituted by the font. + int[] rotatedGlyphs = new int[] { 20, 21, 22, 23, 24, 25, 26, 27 }; + + for (int i = 0; i < glyphs.Count; i++) + { + GlyphLayout glyph = glyphs[i]; + + if (rotatedGlyphs.Contains(i)) + { + Assert.Equal(GlyphLayoutMode.VerticalRotated, glyph.LayoutMode); + } + else + { + Assert.Equal(GlyphLayoutMode.Vertical, glyph.LayoutMode); + } + } + } + } + + [Fact] + public void VerticalMixedLayout_ExpectedBounds() + { + if (SystemFonts.TryGet("Yu Gothic", out FontFamily family)) + { + const string text = "あいうえお、「こんにちはー」。もしもし。ABCDEFG 日本語"; + Font font = family.CreateFont(30.0F); + + TextOptions options = new(font) + { + LayoutMode = LayoutMode.VerticalMixedRightLeft + }; + + FontRectangle bounds = TextMeasurer.MeasureBounds(text, options); + FontRectangle size = TextMeasurer.MeasureSize(text, options); + FontRectangle advance = TextMeasurer.MeasureAdvance(text, options); + + Assert.Equal(new FontRectangle(0.83496094F, 2.8417969F, 28.31543F, 834.9464F), bounds, Comparer); + Assert.Equal(new FontRectangle(0, 0, 28.31543F, 834.9464F), size, Comparer); + Assert.Equal(new FontRectangle(0, 0, 32.98462F, 839.3556F), advance, Comparer); + } + } +} From 7d2da819b756a4facc3c9898f0ad69e554464ce2 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 16 Dec 2024 14:56:04 +1000 Subject: [PATCH 4/4] Fix warnings --- src/SixLabors.Fonts/Tables/General/CMap/Format4SubTable.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/SixLabors.Fonts/Tables/General/CMap/Format4SubTable.cs b/src/SixLabors.Fonts/Tables/General/CMap/Format4SubTable.cs index 8dc307ca..edbf0934 100644 --- a/src/SixLabors.Fonts/Tables/General/CMap/Format4SubTable.cs +++ b/src/SixLabors.Fonts/Tables/General/CMap/Format4SubTable.cs @@ -92,7 +92,6 @@ public override bool TryGetCodePoint(ushort glyphId, out CodePoint codePoint) return false; } - public override IEnumerable GetAvailableCodePoints() => this.Segments.SelectMany(segment => Enumerable.Range(segment.Start, segment.End - segment.Start + 1));