Skip to content

Commit 0be32b2

Browse files
Merge pull request #342 from SixLabors/bp/cff2
Add CFF2 and TrueType Variation Font Support
2 parents 1ac46b2 + 59b5173 commit 0be32b2

File tree

126 files changed

+6986
-781
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

126 files changed

+6986
-781
lines changed

src/SixLabors.Fonts/BigEndianBinaryReader.cs

Lines changed: 115 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -9,52 +9,86 @@
99
namespace SixLabors.Fonts;
1010

1111
/// <summary>
12+
/// <para>
1213
/// A binary reader that reads in big-endian format.
14+
/// </para>
15+
/// <para>
16+
/// This reader captures the stream position at construction time as <c>startOfStream</c>.
17+
/// All offset values read from OpenType tables (via <see cref="ReadOffset16"/>,
18+
/// <see cref="ReadOffset32"/>, etc.) are raw values relative to wherever the spec says
19+
/// they originate (typically the start of the containing table).
20+
/// </para>
21+
/// <para>
22+
/// When seeking with <see cref="Seek"/> using <see cref="SeekOrigin.Begin"/>, the
23+
/// <c>startOfStream</c> is automatically added to the supplied offset. This means
24+
/// table-relative offsets can be passed directly to <see cref="Seek"/> without manually
25+
/// adding the table's absolute position. Do <strong>not</strong> add the table start
26+
/// yourself — that would double-count and land at the wrong position.
27+
/// </para>
28+
/// <para>
29+
/// In contrast, <see cref="BaseStream"/>.<see cref="Stream.Position"/> always returns the
30+
/// <strong>absolute</strong> position within the underlying stream and is unaffected by
31+
/// <c>startOfStream</c>.
32+
/// </para>
1333
/// </summary>
1434
[DebuggerDisplay("Start: {StartOfStream}, Position: {BaseStream.Position}")]
1535
internal sealed class BigEndianBinaryReader : IDisposable
1636
{
1737
/// <summary>
18-
/// Buffer used for temporary storage before conversion into primitives
38+
/// Buffer used for temporary storage before conversion into primitives.
1939
/// </summary>
2040
private readonly byte[] buffer = new byte[16];
21-
22-
private readonly long startOfStream;
2341
private readonly bool leaveOpen;
2442

2543
/// <summary>
2644
/// Initializes a new instance of the <see cref="BigEndianBinaryReader" /> class.
27-
/// Constructs a new binary reader with the given bit converter, reading
28-
/// to the given stream, using the given encoding.
45+
/// The current position of <paramref name="stream"/> is captured as <c>startOfStream</c>
46+
/// and used as the origin for all subsequent <see cref="Seek"/> calls with
47+
/// <see cref="SeekOrigin.Begin"/>.
2948
/// </summary>
30-
/// <param name="stream">Stream to read data from</param>
31-
/// <param name="leaveOpen">if set to <c>true</c> [leave open].</param>
49+
/// <param name="stream">Stream to read data from.</param>
50+
/// <param name="leaveOpen">If <see langword="true"/>, the stream is not disposed when this reader is disposed.</param>
3251
public BigEndianBinaryReader(Stream stream, bool leaveOpen)
3352
{
3453
this.BaseStream = stream;
35-
this.startOfStream = stream.Position;
54+
this.StartOfStream = stream.Position;
3655
this.leaveOpen = leaveOpen;
3756
}
3857

3958
/// <summary>
4059
/// Gets the underlying stream of the EndianBinaryReader.
60+
/// Note that <see cref="Stream.Position"/> on this stream is always the
61+
/// <strong>absolute</strong> position and is <strong>not</strong> adjusted by
62+
/// <c>startOfStream</c>. Avoid using <c>BaseStream.Position</c> to compute
63+
/// offsets for <see cref="Seek"/> — use raw offsets from <see cref="ReadOffset16"/>,
64+
/// <see cref="ReadOffset32"/>, etc. instead.
4165
/// </summary>
4266
public Stream BaseStream { get; }
4367

68+
/// <summary>
69+
/// Gets the absolute stream position captured at construction time.
70+
/// This is the origin for all <see cref="SeekOrigin.Begin"/> seeks.
71+
/// </summary>
72+
public long StartOfStream { get; }
73+
4474
/// <summary>
4575
/// Seeks within the stream.
76+
/// When <paramref name="origin"/> is <see cref="SeekOrigin.Begin"/>, <c>startOfStream</c>
77+
/// is automatically added to <paramref name="offset"/>, so callers should pass
78+
/// table-relative offsets directly (e.g. values read from <see cref="ReadOffset16"/>
79+
/// or <see cref="ReadOffset32"/>). Do <strong>not</strong> add the table's absolute
80+
/// position — that would double-count.
4681
/// </summary>
47-
/// <param name="offset">Offset to seek to.</param>
48-
/// <param name="origin">Origin of seek operation. If SeekOrigin.Begin, the offset will be set to the start of stream position.</param>
82+
/// <param name="offset">Offset to seek to, relative to <paramref name="origin"/>.</param>
83+
/// <param name="origin">Origin of seek operation.</param>
4984
public void Seek(long offset, SeekOrigin origin)
5085
{
51-
// If SeekOrigin.Begin, the offset will be set to the start of stream position.
5286
if (origin == SeekOrigin.Begin)
5387
{
54-
offset += this.startOfStream;
88+
offset += this.StartOfStream;
5589
}
5690

57-
this.BaseStream.Seek(offset, origin);
91+
_ = this.BaseStream.Seek(offset, origin);
5892
}
5993

6094
/// <summary>
@@ -67,10 +101,15 @@ public byte ReadByte()
67101
return this.buffer[0];
68102
}
69103

104+
/// <summary>
105+
/// Reads a single byte from the stream and reinterprets it as the specified enum type.
106+
/// </summary>
107+
/// <typeparam name="TEnum">The enum type whose underlying type must be a single byte.</typeparam>
108+
/// <returns>The enum value.</returns>
70109
public TEnum ReadByte<TEnum>()
71110
where TEnum : struct, Enum
72111
{
73-
TryConvert(this.ReadByte(), out TEnum value);
112+
_ = TryConvert(this.ReadByte(), out TEnum value);
74113
return value;
75114
}
76115

@@ -84,6 +123,11 @@ public sbyte ReadSByte()
84123
return unchecked((sbyte)this.buffer[0]);
85124
}
86125

126+
/// <summary>
127+
/// Reads a 2.14 fixed-point number from the stream.
128+
/// 2 bytes are read and divided by 16384 to produce a value in the range [-2, +2).
129+
/// </summary>
130+
/// <returns>The fixed-point value as a <see cref="float"/>.</returns>
87131
public float ReadF2Dot14()
88132
{
89133
const float f2Dot14ToFloat = 16384F;
@@ -102,10 +146,15 @@ public short ReadInt16()
102146
return BinaryPrimitives.ReadInt16BigEndian(this.buffer);
103147
}
104148

149+
/// <summary>
150+
/// Reads a 16-bit integer from the stream and reinterprets it as the specified enum type.
151+
/// </summary>
152+
/// <typeparam name="TEnum">The enum type whose underlying type must be 16 bits.</typeparam>
153+
/// <returns>The enum value.</returns>
105154
public TEnum ReadInt16<TEnum>()
106155
where TEnum : struct, Enum
107156
{
108-
TryConvert(this.ReadUInt16(), out TEnum value);
157+
_ = TryConvert(this.ReadUInt16(), out TEnum value);
109158
return value;
110159
}
111160

@@ -115,6 +164,11 @@ public TEnum ReadInt16<TEnum>()
115164
/// <returns>A 16-bit signed integer read from the stream, interpreted as an FWORD value.</returns>
116165
public short ReadFWORD() => this.ReadInt16();
117166

167+
/// <summary>
168+
/// Reads an array of FWORD (signed 16-bit) values from the stream.
169+
/// </summary>
170+
/// <param name="length">The number of values to read.</param>
171+
/// <returns>An array of 16-bit signed integers.</returns>
118172
public short[] ReadFWORDArray(int length) => this.ReadInt16Array(length);
119173

120174
/// <summary>
@@ -171,25 +225,31 @@ public ushort ReadUInt16()
171225

172226
/// <summary>
173227
/// Reads a 16-bit unsigned integer from the stream representing an offset position.
174-
/// 2 bytes are read.
228+
/// 2 bytes are read. The returned value is the raw offset as stored in the font file
229+
/// (typically relative to the start of the containing table). Pass it directly to
230+
/// <see cref="Seek"/> with <see cref="SeekOrigin.Begin"/> — do not add the table's
231+
/// absolute position.
175232
/// </summary>
176233
/// <returns>The 16-bit unsigned integer read.</returns>
177234
public ushort ReadOffset16() => this.ReadUInt16();
178235

236+
/// <summary>
237+
/// Reads a 16-bit unsigned integer from the stream and reinterprets it as the specified enum type.
238+
/// </summary>
239+
/// <typeparam name="TEnum">The enum type whose underlying type must be 16 bits.</typeparam>
240+
/// <returns>The enum value.</returns>
179241
public TEnum ReadUInt16<TEnum>()
180242
where TEnum : struct, Enum
181243
{
182-
TryConvert(this.ReadUInt16(), out TEnum value);
244+
_ = TryConvert(this.ReadUInt16(), out TEnum value);
183245
return value;
184246
}
185247

186248
/// <summary>
187-
/// Reads array of 16-bit unsigned integers from the stream.
249+
/// Reads an array of 16-bit unsigned integers from the stream.
188250
/// </summary>
189-
/// <param name="length">The length.</param>
190-
/// <returns>
191-
/// The 16-bit unsigned integer read.
192-
/// </returns>
251+
/// <param name="length">The number of values to read.</param>
252+
/// <returns>An array of 16-bit unsigned integers.</returns>
193253
public ushort[] ReadUInt16Array(int length)
194254
{
195255
ushort[] data = new ushort[length];
@@ -214,12 +274,10 @@ public void ReadUInt16Array(Span<ushort> buffer)
214274
}
215275

216276
/// <summary>
217-
/// Reads array or 32-bit unsigned integers from the stream.
277+
/// Reads an array of 32-bit unsigned integers from the stream.
218278
/// </summary>
219-
/// <param name="length">The length.</param>
220-
/// <returns>
221-
/// The 32-bit unsigned integer read.
222-
/// </returns>
279+
/// <param name="length">The number of values to read.</param>
280+
/// <returns>An array of 32-bit unsigned integers.</returns>
223281
public uint[] ReadUInt32Array(int length)
224282
{
225283
uint[] data = new uint[length];
@@ -231,6 +289,11 @@ public uint[] ReadUInt32Array(int length)
231289
return data;
232290
}
233291

292+
/// <summary>
293+
/// Reads an array of 8-bit unsigned integers (bytes) from the stream.
294+
/// </summary>
295+
/// <param name="length">The number of bytes to read.</param>
296+
/// <returns>A byte array of the requested length.</returns>
234297
public byte[] ReadUInt8Array(int length)
235298
{
236299
byte[] data = new byte[length];
@@ -241,12 +304,10 @@ public byte[] ReadUInt8Array(int length)
241304
}
242305

243306
/// <summary>
244-
/// Reads array of 16-bit unsigned integers from the stream.
307+
/// Reads an array of 16-bit signed integers from the stream.
245308
/// </summary>
246-
/// <param name="length">The length.</param>
247-
/// <returns>
248-
/// The 16-bit signed integer read.
249-
/// </returns>
309+
/// <param name="length">The number of values to read.</param>
310+
/// <returns>An array of 16-bit signed integers.</returns>
250311
public short[] ReadInt16Array(int length)
251312
{
252313
short[] data = new short[length];
@@ -292,6 +353,14 @@ public uint ReadUInt24()
292353
return (uint)((highByte << 16) | this.ReadUInt16());
293354
}
294355

356+
/// <summary>
357+
/// Reads a 24-bit unsigned integer from the stream representing an offset position.
358+
/// 3 bytes are read. The returned value is the raw offset as stored in the font file
359+
/// (typically relative to the start of the containing table). Pass it directly to
360+
/// <see cref="Seek"/> with <see cref="SeekOrigin.Begin"/> — do not add the table's
361+
/// absolute position.
362+
/// </summary>
363+
/// <returns>The 24-bit unsigned integer read.</returns>
295364
public uint ReadOffset24() => this.ReadUInt24();
296365

297366
/// <summary>
@@ -308,7 +377,10 @@ public uint ReadUInt32()
308377

309378
/// <summary>
310379
/// Reads a 32-bit unsigned integer from the stream representing an offset position.
311-
/// 4 bytes are read.
380+
/// 4 bytes are read. The returned value is the raw offset as stored in the font file
381+
/// (typically relative to the start of the containing table). Pass it directly to
382+
/// <see cref="Seek"/> with <see cref="SeekOrigin.Begin"/> — do not add the table's
383+
/// absolute position.
312384
/// </summary>
313385
/// <returns>The 32-bit unsigned integer read.</returns>
314386
public uint ReadOffset32() => this.ReadUInt32();
@@ -360,9 +432,9 @@ public string ReadString(int bytesToRead, Encoding encoding)
360432
}
361433

362434
/// <summary>
363-
/// Reads the uint32 string.
435+
/// Reads a 4-byte OpenType tag from the stream as a UTF-8 string.
364436
/// </summary>
365-
/// <returns>a 4 character long UTF8 encoded string.</returns>
437+
/// <returns>A 4-character string representing the tag (e.g. "glyf", "GPOS").</returns>
366438
public string ReadTag()
367439
{
368440
this.ReadInternal(this.buffer, 4);
@@ -371,11 +443,15 @@ public string ReadTag()
371443
}
372444

373445
/// <summary>
374-
/// Reads an offset consuming the given nuber of bytes.
446+
/// Reads an offset consuming the given number of bytes (1–4).
447+
/// The returned value is the raw offset as stored in the font file
448+
/// (typically relative to the start of the containing table). Pass it directly to
449+
/// <see cref="Seek"/> with <see cref="SeekOrigin.Begin"/> — do not add the table's
450+
/// absolute position.
375451
/// </summary>
376-
/// <param name="size">The offset size in bytes.</param>
452+
/// <param name="size">The offset size in bytes (1, 2, 3, or 4).</param>
377453
/// <returns>The 32-bit signed integer representing the offset.</returns>
378-
/// <exception cref="InvalidOperationException">Size is not in range.</exception>
454+
/// <exception cref="InvalidOperationException">Thrown when <paramref name="size"/> is not 1–4.</exception>
379455
public int ReadOffset(int size)
380456
=> size switch
381457
{
@@ -409,6 +485,7 @@ private void ReadInternal(byte[] data, int size)
409485
}
410486
}
411487

488+
/// <inheritdoc />
412489
public void Dispose()
413490
{
414491
if (!this.leaveOpen)

src/SixLabors.Fonts/Buffer{T}.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ internal ref struct Buffer<T>
1919
private bool isDisposed;
2020

2121
public Buffer(int length)
22+
: this(length, clear: false)
23+
{
24+
}
25+
26+
public Buffer(int length, bool clear)
2227
{
2328
Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length));
2429
int itemSizeBytes = Unsafe.SizeOf<T>();
@@ -30,6 +35,11 @@ public Buffer(int length)
3035
this.Memory = manager.Memory[..this.length];
3136
this.span = this.Memory.Span;
3237

38+
if (clear)
39+
{
40+
this.span.Clear();
41+
}
42+
3343
this.isDisposed = false;
3444
}
3545

src/SixLabors.Fonts/FileFontMetrics.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Numerics;
66
using SixLabors.Fonts.Tables;
77
using SixLabors.Fonts.Tables.AdvancedTypographic;
8+
using SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
89
using SixLabors.Fonts.Unicode;
910

1011
namespace SixLabors.Fonts;
@@ -44,6 +45,11 @@ internal FileFontMetrics(FontDescription description, string path, long offset)
4445
/// </summary>
4546
public string Path { get; }
4647

48+
/// <summary>
49+
/// Gets the underlying <see cref="StreamFontMetrics"/> that this file-backed instance delegates to.
50+
/// </summary>
51+
internal StreamFontMetrics StreamFontMetrics => this.fontMetrics.Value;
52+
4753
/// <inheritdoc />
4854
public override ushort UnitsPerEm => this.fontMetrics.Value.UnitsPerEm;
4955

@@ -119,6 +125,10 @@ internal override bool TryGetGlyphClass(ushort glyphId, [NotNullWhen(true)] out
119125
internal override bool TryGetMarkAttachmentClass(ushort glyphId, [NotNullWhen(true)] out GlyphClassDef? markAttachmentClass)
120126
=> this.fontMetrics.Value.TryGetMarkAttachmentClass(glyphId, out markAttachmentClass);
121127

128+
/// <inheritdoc/>
129+
public override bool TryGetVariationAxes(out VariationAxis[]? variationAxes)
130+
=> this.fontMetrics.Value.TryGetVariationAxes(out variationAxes);
131+
122132
/// <inheritdoc/>
123133
internal override bool IsInMarkFilteringSet(ushort markGlyphSetIndex, ushort glyphId)
124134
=> this.fontMetrics.Value.IsInMarkFilteringSet(markGlyphSetIndex, glyphId);
@@ -163,6 +173,14 @@ internal override bool TryGetKerningOffset(ushort currentId, ushort nextId, out
163173
internal override void UpdatePositions(GlyphPositioningCollection collection)
164174
=> this.fontMetrics.Value.UpdatePositions(collection);
165175

176+
/// <inheritdoc/>
177+
internal override float GetGDefVariationDelta(uint packedVariationIndex)
178+
=> this.fontMetrics.Value.GetGDefVariationDelta(packedVariationIndex);
179+
180+
/// <inheritdoc/>
181+
internal override ReadOnlySpan<float> GetNormalizedCoordinates()
182+
=> this.fontMetrics.Value.GetNormalizedCoordinates();
183+
166184
/// <summary>
167185
/// Reads a <see cref="StreamFontMetrics"/> from the specified stream.
168186
/// </summary>

0 commit comments

Comments
 (0)