-
-
Notifications
You must be signed in to change notification settings - Fork 73
Expand file tree
/
Copy pathGlyphPositioningCollection.cs
More file actions
400 lines (345 loc) · 15.6 KB
/
GlyphPositioningCollection.cs
File metadata and controls
400 lines (345 loc) · 15.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using SixLabors.Fonts.Tables.AdvancedTypographic;
using SixLabors.Fonts.Unicode;
namespace SixLabors.Fonts;
/// <summary>
/// Represents a collection of glyph metrics that are mapped to input codepoints.
/// </summary>
internal sealed class GlyphPositioningCollection : IGlyphShapingCollection
{
/// <summary>
/// Contains a map the index of a map within the collection, non-sequential codepoint offsets, and their glyph ids, point size, and mtrics.
/// </summary>
private readonly List<GlyphPositioningData> glyphs = [];
/// <summary>
/// Initializes a new instance of the <see cref="GlyphPositioningCollection"/> class.
/// </summary>
/// <param name="textOptions">The text options.</param>
public GlyphPositioningCollection(TextOptions textOptions) => this.TextOptions = textOptions;
/// <inheritdoc />
public int Count => this.glyphs.Count;
/// <inheritdoc />
public TextOptions TextOptions { get; }
/// <inheritdoc />
public GlyphShapingData this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.glyphs[index].Data;
}
/// <inheritdoc />
public void AddShapingFeature(int index, TagEntry feature)
=> this.glyphs[index].Data.Features.Add(feature);
/// <inheritdoc />
public void EnableShapingFeature(int index, Tag feature)
{
List<TagEntry> 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;
}
}
}
/// <inheritdoc />
public void DisableShapingFeature(int index, Tag feature)
{
List<TagEntry> 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;
}
}
}
/// <summary>
/// Gets the glyph metrics at the given codepoint offset.
/// </summary>
/// <param name="offset">The zero-based index within the input codepoint collection.</param>
/// <param name="pointSize">The font size in PT units of the font containing this glyph.</param>
/// <param name="isSubstituted">Whether the glyph is the result of a substitution.</param>
/// <param name="isVerticalSubstitution">Whether the glyph is the result of a vertical substitution.</param>
/// <param name="isDecomposed">Whether the glyph is the result of a decomposition substitution.</param>
/// <param name="metrics">
/// 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.
/// </param>
/// <returns>The metrics.</returns>
public bool TryGetGlyphMetricsAtOffset(
int offset,
out float pointSize,
out bool isSubstituted,
out bool isVerticalSubstitution,
out bool isDecomposed,
[NotNullWhen(true)] out IReadOnlyList<GlyphMetrics>? metrics)
{
List<GlyphMetrics> match = [];
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.Add(glyph.Metrics);
}
else if (match.Count > 0)
{
// Offsets, though non-sequential, are sorted, so we can stop searching.
break;
}
}
metrics = match;
return match.Count > 0;
}
/// <summary>
/// Updates the collection of glyph ids to the metrics collection to overwrite any glyphs that have been previously
/// identified as fallbacks.
/// </summary>
/// <param name="font">The font face with metrics.</param>
/// <param name="collection">The glyph substitution collection.</param>
/// <returns><see langword="true"/> if the metrics collection does not contain any fallbacks; otherwise <see langword="false"/>.</returns>
public bool TryUpdate(Font font, GlyphSubstitutionCollection collection)
{
FontMetrics fontMetrics = font.FontMetrics;
LayoutMode layoutMode = this.TextOptions.LayoutMode;
ColorFontSupport colorFontSupport = this.TextOptions.ColorFontSupport;
bool hasFallBacks = false;
List<int> orphans = [];
Tag vert = FeatureTags.VerticalAlternates;
Tag vrt2 = FeatureTags.VerticalAlternatesAndRotation;
Tag vrtr = FeatureTags.VerticalAlternatesForRotation;
for (int i = 0; i < this.glyphs.Count; i++)
{
GlyphPositioningData current = this.glyphs[i];
if (current.Metrics.GlyphType != GlyphType.Fallback)
{
// We've already got the correct glyph.
continue;
}
int offset = current.Offset;
float pointSize = current.PointSize;
if (collection.TryGetGlyphShapingDataAtOffset(offset, out IReadOnlyList<GlyphShapingData>? data))
{
int replacementCount = 0;
for (int j = 0; j < data.Count; j++)
{
GlyphShapingData shape = data[j];
ushort id = shape.GlyphId;
CodePoint codePoint = shape.CodePoint;
// Perform a semi-deep clone (FontMetrics is not cloned) so we can continue to
// cache the original in the font metrics and only update our collection.
TextAttributes textAttributes = shape.TextRun.TextAttributes;
TextDecorations textDecorations = shape.TextRun.TextDecorations;
bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(codePoint, layoutMode);
foreach (Tag feature in shape.AppliedFeatures)
{
isVertical |= feature == vert;
isVertical |= feature == vrt2;
isVertical |= feature == vrtr;
}
GlyphMetrics metrics = fontMetrics.GetGlyphMetrics(codePoint, id, textAttributes, textDecorations, layoutMode, colorFontSupport);
{
// If the glyphs are fallbacks we don't want them as
// we've already captured them on the first run.
if (metrics.GlyphType == GlyphType.Fallback && !CodePoint.IsControl(codePoint))
{
hasFallBacks = true;
}
}
if (metrics.GlyphType != GlyphType.Fallback)
{
if (replacementCount == 0)
{
// There should only be a single fallback glyph at this position from the previous collection.
this.glyphs.RemoveAt(i);
}
// We only want a single dimensional advance for positioning.
GlyphShapingBounds bounds = isVertical
? new(0, 0, 0, metrics.AdvanceHeight)
: new(0, 0, metrics.AdvanceWidth, 0);
// Track the number of inserted glyphs at the offset so we can correctly increment our position.
this.glyphs.Insert(i += replacementCount, new(offset, new(shape, true) { Bounds = bounds }, pointSize, metrics.CloneForRendering(shape.TextRun)));
replacementCount++;
}
}
}
else
{
// If a font had glyphs but a follow up font also has them and can substitute. e.g ligatures
// then we end up with orphaned fallbacks. We need to remove them.
orphans.Add(i);
}
}
// Remove any orphans.
for (int i = orphans.Count - 1; i >= 0; i--)
{
this.glyphs.RemoveAt(orphans[i]);
}
return !hasFallBacks;
}
/// <summary>
/// Adds the collection of glyph ids to the metrics collection.
/// identified as fallbacks.
/// </summary>
/// <param name="font">The font face with metrics.</param>
/// <param name="collection">The glyph substitution collection.</param>
/// <returns><see langword="true"/> if the metrics collection does not contain any fallbacks; otherwise <see langword="false"/>.</returns>
public bool TryAdd(Font font, GlyphSubstitutionCollection collection)
{
bool hasFallBacks = false;
FontMetrics fontMetrics = font.FontMetrics;
LayoutMode layoutMode = this.TextOptions.LayoutMode;
ColorFontSupport colorFontSupport = this.TextOptions.ColorFontSupport;
Tag vert = 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);
CodePoint codePoint = data.CodePoint;
ushort id = data.GlyphId;
// Perform a semi-deep clone (FontMetrics is not cloned) so we can continue to
// cache the original in the font metrics and only update our collection.
TextAttributes textAttributes = data.TextRun.TextAttributes;
TextDecorations textDecorations = data.TextRun.TextDecorations;
bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(codePoint, layoutMode);
foreach (Tag feature in data.AppliedFeatures)
{
isVertical |= feature == vert;
isVertical |= feature == vrt2;
isVertical |= feature == vrtr;
}
GlyphMetrics metrics = fontMetrics.GetGlyphMetrics(codePoint, id, textAttributes, textDecorations, layoutMode, colorFontSupport);
if (metrics.GlyphType == GlyphType.Fallback && !CodePoint.IsControl(codePoint))
{
hasFallBacks = true;
}
// We only want a single dimensional advance for positioning.
GlyphShapingBounds bounds = isVertical
? new(0, 0, 0, metrics.AdvanceHeight)
: new(0, 0, metrics.AdvanceWidth, 0);
this.glyphs.Add(new(offset, new(data, true) { Bounds = bounds }, font.Size, metrics.CloneForRendering(data.TextRun)));
}
return !hasFallBacks;
}
/// <summary>
/// Updates the position of the glyph at the specified index.
/// </summary>
/// <param name="fontMetrics">The font metrics.</param>
/// <param name="index">The zero-based index of the element.</param>
public void UpdatePosition(FontMetrics fontMetrics, int index)
{
GlyphShapingData data = this[index];
bool isDirtyXY = data.Bounds.IsDirtyXY;
bool isDirtyWH = data.Bounds.IsDirtyWH;
if (!isDirtyXY && !isDirtyWH)
{
// No change required but the glyph has been processed.
data.IsPositioned = true;
return;
}
ushort glyphId = data.GlyphId;
GlyphMetrics m = this.glyphs[index].Metrics;
if (m.GlyphId == glyphId && fontMetrics == m.FontMetrics)
{
if (isDirtyXY)
{
m.ApplyOffset((short)data.Bounds.X, (short)data.Bounds.Y);
data.IsPositioned = true;
}
if (isDirtyWH)
{
m.SetAdvanceWidth((ushort)data.Bounds.Width);
m.SetAdvanceHeight((ushort)data.Bounds.Height);
data.IsPositioned = true;
}
}
}
/// <summary>
/// Updates the advanced metrics of the glyphs at the given index and id,
/// adding dx and dy to the current advance.
/// </summary>
/// <param name="fontMetrics">The font face with metrics.</param>
/// <param name="index">The zero-based index of the element.</param>
/// <param name="glyphId">The id of the glyph to offset.</param>
/// <param name="dx">The delta x-advance.</param>
/// <param name="dy">The delta y-advance.</param>
public void Advance(FontMetrics fontMetrics, int index, ushort glyphId, short dx, short dy)
{
LayoutMode layoutMode = this.TextOptions.LayoutMode;
Tag vert = FeatureTags.VerticalAlternates;
Tag vrt2 = FeatureTags.VerticalAlternatesAndRotation;
Tag vrtr = FeatureTags.VerticalAlternatesForRotation;
GlyphPositioningData glyph = this.glyphs[index];
GlyphMetrics m = glyph.Metrics;
if (m.GlyphId == glyphId && fontMetrics == m.FontMetrics)
{
bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(m.CodePoint, layoutMode);
foreach (Tag feature in glyph.Data.AppliedFeatures)
{
isVertical |= feature == vert;
isVertical |= feature == vrt2;
isVertical |= feature == vrtr;
}
m.ApplyAdvance(dx, isVertical ? dy : (short)0);
}
}
/// <summary>
/// Returns a value indicating whether the element at the given index should be processed.
/// </summary>
/// <param name="fontMetrics">The font face with metrics.</param>
/// <param name="index">The zero-based index of the elements to position.</param>
/// <returns><see langword="true"/> if the element should be processed; otherwise, <see langword="false"/>.</returns>
public bool ShouldProcess(FontMetrics fontMetrics, int index)
{
GlyphPositioningData data = this.glyphs[index];
if (data.Data.IsPositioned)
{
return false;
}
return data.Metrics.FontMetrics == fontMetrics;
}
[DebuggerDisplay("{DebuggerDisplay,nq}")]
private class GlyphPositioningData
{
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; }
private string DebuggerDisplay => FormattableString.Invariant($"Offset: {this.Offset}, Data: {this.Data.ToDebuggerDisplay()}");
}
}