@@ -15,14 +15,18 @@ Date Author Change
1515using EPPlus . Fonts . OpenType . Tables . Gsub . Data . Lookups ;
1616using OfficeOpenXml . Interfaces . Drawing . Text ;
1717using System . Collections . Generic ;
18+ using System . Linq ;
1819
1920namespace EPPlus . Fonts . OpenType . TextShaping . Ligatures
2021{
2122 internal class LigatureProcessor
2223 {
24+ private readonly List < LookupTable > _ligaLookups ;
25+
2326 public LigatureProcessor ( OpenTypeFont font )
2427 {
2528 _font = font ;
29+ _ligaLookups = FindLookupsForFeature ( font . GsubTable , "liga" ) ;
2630 }
2731
2832 private readonly OpenTypeFont _font ;
@@ -45,162 +49,137 @@ internal List<ShapedGlyph> ApplyLigatures(List<ShapedGlyph> glyphs)
4549 // Apply each lookup in order
4650 foreach ( var lookup in ligaLookups )
4751 {
48- glyphs = ApplyLigatureLookup ( glyphs , lookup ) ;
52+ ApplyLigaturesInPlace ( glyphs ) ;
4953 }
5054
5155 return glyphs ;
5256 }
5357
54- /// <summary>
55- /// Finds all lookups associated with a feature tag.
56- /// </summary>
57- private List < LookupTable > FindLookupsForFeature ( GsubTable gsub , string featureTag )
58+ internal void ApplyLigaturesInPlace ( List < ShapedGlyph > glyphs )
5859 {
59- var lookups = new List < LookupTable > ( ) ;
60+ if ( _ligaLookups . Count == 0 ) return ;
6061
61- foreach ( var featureRecord in gsub . FeatureList . FeatureRecords )
62+ foreach ( var lookup in _ligaLookups )
6263 {
63- if ( featureRecord . FeatureTag . Value == featureTag )
64+ if ( lookup . LookupType != 4 ) continue ;
65+
66+ int i = 0 ;
67+ while ( i < glyphs . Count )
6468 {
65- var feature = featureRecord . FeatureTable ;
69+ bool substituted = false ;
6670
67- foreach ( var lookupIndex in feature . LookupListIndices )
71+ foreach ( var subtableObj in lookup . SubTables )
6872 {
69- if ( lookupIndex < gsub . LookupList . Lookups . Count )
73+ if ( subtableObj is not LigatureSubstSubTable subtable ) continue ;
74+
75+ if ( TryApplyLigatureInPlace ( glyphs , i , subtable , out int consumed ) )
7076 {
71- lookups . Add ( gsub . LookupList . Lookups [ lookupIndex ] ) ;
77+ substituted = true ;
78+ i += consumed ; // Oftast 1 efter ersättning
79+ break ; // Första match vinner – hoppa ur
7280 }
7381 }
82+
83+ if ( ! substituted ) i ++ ;
7484 }
7585 }
76-
77- return lookups ;
7886 }
7987
80- /// <summary>
81- /// Applies a single ligature lookup to the glyph sequence.
82- /// Processes left-to-right, replacing matching sequences with ligatures.
83- /// </summary>
84- private List < ShapedGlyph > ApplyLigatureLookup ( List < ShapedGlyph > glyphs , LookupTable lookup )
88+ private bool TryApplyLigatureInPlace (
89+ List < ShapedGlyph > glyphs ,
90+ int startIndex ,
91+ LigatureSubstSubTable subtable ,
92+ out int componentsConsumed )
8593 {
86- if ( lookup . LookupType != 4 ) // Must be Ligature Substitution
87- return glyphs ;
94+ componentsConsumed = 0 ;
95+
96+ if ( startIndex >= glyphs . Count ) return false ;
8897
89- var result = new List < ShapedGlyph > ( ) ;
90- int i = 0 ;
98+ ushort first = glyphs [ startIndex ] . GlyphId ;
99+ int covIdx = subtable . Coverage . GetGlyphIndex ( first ) ;
100+ if ( covIdx < 0 ) return false ;
91101
92- while ( i < glyphs . Count )
102+ if ( ! subtable . LigatureSets . TryGetValue ( first , out var ligSet ) || ligSet ? . Ligatures . Count == 0 )
103+ return false ;
104+
105+ // Försök längre ligaturer först (rekommenderas av OpenType-spec)
106+ var sortedLigs = ligSet . Ligatures
107+ . OrderByDescending ( l => 1 + ( l . Components ? . Length ?? 0 ) )
108+ . ToList ( ) ;
109+
110+ foreach ( var lig in sortedLigs )
93111 {
94- bool substituted = false ;
112+ int compCount = 1 + ( lig . Components ? . Length ?? 0 ) ;
113+ if ( startIndex + compCount > glyphs . Count ) continue ;
95114
96- // Try each subtable
97- foreach ( var subtable in lookup . SubTables )
115+ bool match = true ;
116+ for ( int j = 0 ; j < lig . Components ? . Length ; j ++ )
98117 {
99- if ( subtable is LigatureSubstSubTable ligSubtable )
118+ if ( glyphs [ startIndex + 1 + j ] . GlyphId != lig . Components [ j ] )
100119 {
101- // Try to match ligature starting at position i
102- if ( TryApplyLigature ( glyphs , i , ligSubtable , out var ligatureGlyph , out int componentsConsumed ) )
103- {
104- result . Add ( ligatureGlyph ) ;
105- i += componentsConsumed ;
106- substituted = true ;
107- break ; // Found a match, move to next position
108- }
120+ match = false ;
121+ break ;
109122 }
110123 }
111124
112- if ( ! substituted )
125+ if ( match )
113126 {
114- // No ligature found, keep original glyph
115- result . Add ( glyphs [ i ] ) ;
116- i ++ ;
127+ var ligGlyph = CreateLigatureGlyph ( glyphs , startIndex , ( byte ) compCount , lig . LigatureGlyph ) ;
128+
129+ // MUTERA DIREKT
130+ glyphs . RemoveRange ( startIndex , compCount ) ;
131+ glyphs . Insert ( startIndex , ligGlyph ) ;
132+
133+ componentsConsumed = 1 ; // ligatur tar platsen → nästa steg flyttar förbi den
134+ return true ;
117135 }
118136 }
119137
120- return result ;
138+ return false ;
121139 }
122140
141+
123142 /// <summary>
124- /// Attempts to find and apply a ligature substitution starting at the given position .
143+ /// Finds all lookups associated with a feature tag .
125144 /// </summary>
126- private bool TryApplyLigature (
127- List < ShapedGlyph > glyphs ,
128- int startIndex ,
129- LigatureSubstSubTable subtable ,
130- out ShapedGlyph ligatureGlyph ,
131- out int componentsConsumed )
145+ private List < LookupTable > FindLookupsForFeature ( GsubTable gsub , string featureTag )
132146 {
133- ligatureGlyph = null ;
134- componentsConsumed = 0 ;
135-
136- if ( startIndex >= glyphs . Count )
137- return false ;
138-
139- ushort firstGlyph = glyphs [ startIndex ] . GlyphId ;
140-
141- // Check if first glyph is in coverage
142- int coverageIndex = subtable . Coverage . GetGlyphIndex ( firstGlyph ) ;
143- if ( coverageIndex < 0 )
144- return false ;
145-
146- // LigatureSets is a Dictionary<ushort, LigatureSet>
147- // Key is the GLYPH ID, not coverage index!
148- if ( ! subtable . LigatureSets . TryGetValue ( firstGlyph , out var ligatureSet ) )
149- return false ;
150-
151- if ( ligatureSet ? . Ligatures == null )
152- return false ;
147+ var lookups = new List < LookupTable > ( ) ;
153148
154- // Try each ligature in the set
155- foreach ( var ligature in ligatureSet . Ligatures )
149+ foreach ( var featureRecord in gsub . FeatureList . FeatureRecords )
156150 {
157- int componentCount = 1 + ( ligature . Components ? . Length ?? 0 ) ;
158-
159- // Check if we have enough glyphs remaining
160- if ( startIndex + componentCount > glyphs . Count )
161- continue ;
162-
163- // Check if all component glyphs match
164- bool matches = true ;
165-
166- if ( ligature . Components != null )
151+ if ( featureRecord . FeatureTag . Value == featureTag )
167152 {
168- for ( int j = 0 ; j < ligature . Components . Length ; j ++ )
153+ var feature = featureRecord . FeatureTable ;
154+
155+ foreach ( var lookupIndex in feature . LookupListIndices )
169156 {
170- if ( glyphs [ startIndex + 1 + j ] . GlyphId != ligature . Components [ j ] )
157+ if ( lookupIndex < gsub . LookupList . Lookups . Count )
171158 {
172- matches = false ;
173- break ;
159+ lookups . Add ( gsub . LookupList . Lookups [ lookupIndex ] ) ;
174160 }
175161 }
176162 }
177-
178- if ( matches )
179- {
180- // Found a match! Create ligature glyph
181- ligatureGlyph = CreateLigatureGlyph ( glyphs , startIndex , componentCount , ligature . LigatureGlyph ) ;
182- componentsConsumed = componentCount ;
183- return true ;
184- }
185163 }
186164
187- return false ;
165+ return lookups ;
188166 }
189167
168+
190169 /// <summary>
191170 /// Creates a new shaped glyph for a ligature, combining metrics from components.
192171 /// </summary>
193172 private ShapedGlyph CreateLigatureGlyph (
194173 List < ShapedGlyph > glyphs ,
195174 int startIndex ,
196- int componentCount ,
175+ byte componentCount ,
197176 ushort ligatureGlyphId )
198177 {
199178 // Get advance width for ligature glyph
200- int advanceWidth = _font . HmtxTable . GetAdvanceWidth ( ligatureGlyphId ) ;
179+ var advanceWidth = ( short ) _font . HmtxTable . GetAdvanceWidth ( ligatureGlyphId ) ;
201180
202181 // Preserve cluster index from first component
203- int clusterIndex = glyphs [ startIndex ] . ClusterIndex ;
182+ var clusterIndex = glyphs [ startIndex ] . ClusterIndex ;
204183
205184 return new ShapedGlyph
206185 {
0 commit comments