Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
b360105
Start implementing CFF2 support
brianpopow Jun 23, 2022
a1917ff
Fix mistake loading VariationAxisRecord
brianpopow Jun 27, 2022
29317e4
- Use Epsilon defined in fontkit rather the float.Epsilon
brianpopow Jun 27, 2022
c541f52
Cache calculated blend vectors
brianpopow Jun 27, 2022
dfc08fc
Make ItemVariationStore a property of ICffTable
brianpopow Jun 28, 2022
359d270
Make gvar, avar and fvar optional
brianpopow Jun 28, 2022
646ddba
Start parsing gvar table
brianpopow Jun 29, 2022
5f6030b
Merge remote-tracking branch 'origin/main' into bp/cff2
brianpopow Jul 3, 2022
0d7e743
Add FDSelect format 4
brianpopow Jul 3, 2022
71cc71a
Move ReadFdArray to base class
brianpopow Jul 3, 2022
a06eeef
Move ReadFdSelect to base class
brianpopow Jul 3, 2022
7c408d6
Better ascii art
brianpopow Jul 4, 2022
d2fa25d
Add flag descriptions
brianpopow Jul 4, 2022
3064aa4
Parse hvar table
brianpopow Jul 5, 2022
6840b84
Fix issues parsing ItemvariationStore
brianpopow Jul 5, 2022
f43a240
Use font name from NameTable for CFF2 font
brianpopow Jul 5, 2022
3d7b12c
Implement loading DeltaSetIndexMap
brianpopow Jul 6, 2022
fad1a0b
Parse deltaSets
brianpopow Jul 9, 2022
7805dca
Merge branch 'main' into bp/cff2
JimBobSquarePants Jul 11, 2023
dedd968
Add AdvanceAdjustment() to VariationProcessor
brianpopow Jul 12, 2023
e2d1416
Add Variations table properties to TrueTypeFontTables
brianpopow Jul 13, 2023
9b6a92c
Add TryGetVariationAxes method to FontMetrics
brianpopow Jul 13, 2023
31870b3
Fix stylecop warnings
brianpopow Jul 16, 2023
63192f9
Merge branch 'main' into bp/cff2
JimBobSquarePants Aug 9, 2023
b361fc6
Fix longwords mask check.
JimBobSquarePants Aug 9, 2023
8a60922
Make long delta int instead of uint and short delta short instead of …
brianpopow Aug 10, 2023
5c4e888
Use ReadSByte() instead of ReadByte()
brianpopow Aug 10, 2023
34e2c4a
Rename longDeltas to regionDeltas
brianpopow Aug 10, 2023
5e2b1e3
ItemVariationData has now a DeltaSet array with longDeltas and region…
brianpopow Aug 10, 2023
7f0e2c5
Fix delta concat order
JimBobSquarePants Aug 11, 2023
3c6cf40
Merge branch 'main' into bp/cff2
JimBobSquarePants Sep 5, 2023
a18e567
Fix build
JimBobSquarePants Sep 5, 2023
d3d5910
Update StreamFontMetrics.TrueType.cs
JimBobSquarePants Sep 5, 2023
cf17d52
Update GlyphVariationProcessor.cs
JimBobSquarePants Sep 5, 2023
05eaf59
Start figuring out missing implementation.
JimBobSquarePants Sep 12, 2023
4538e20
Merge branch 'main' into bp/cff2
JimBobSquarePants Sep 15, 2023
efd34aa
Merge branch 'main' into bp/cff2
JimBobSquarePants Oct 3, 2023
4720953
Merge branch 'main' into bp/cff2
JimBobSquarePants Dec 1, 2023
606f16b
Merge branch 'main' into bp/cff2
JimBobSquarePants Dec 6, 2023
a22a3b0
Merge branch 'main' into bp/cff2
JimBobSquarePants Feb 9, 2024
ff606eb
Merge branch 'main' into bp/cff2
JimBobSquarePants Feb 15, 2024
f4a74e7
Merge branch 'main' into bp/cff2
JimBobSquarePants Apr 23, 2025
2c6c8f3
Merge branch 'main' into bp/cff2
JimBobSquarePants Sep 15, 2025
9f95cd7
Merge branch 'main' into bp/cff2
JimBobSquarePants Nov 17, 2025
899bd45
Fix build
JimBobSquarePants Nov 17, 2025
d0cfe54
Merge branch 'main' into bp/cff2
JimBobSquarePants Nov 27, 2025
2ee34ff
Merge branch 'main' into bp/cff2
JimBobSquarePants Jan 27, 2026
a8859b2
Fix gvar loading
JimBobSquarePants Jan 27, 2026
d6aeefe
Wire up variations table loading
JimBobSquarePants Jan 27, 2026
f8ab016
Fix build
JimBobSquarePants Jan 27, 2026
7f066a3
Pass varations processor to TT streamFontMetrics
JimBobSquarePants Feb 13, 2026
20d2d7f
Assert variation table content
brianpopow Mar 8, 2026
a70b77d
Add test for gettings variations tables for adobe font
brianpopow Mar 8, 2026
f58429f
Merge branch 'main' into bp/cff2
brianpopow Mar 8, 2026
b49ad7d
Merge branch 'main' into bp/cff2
JimBobSquarePants Mar 10, 2026
3b107d7
Add glyph-variation (gvar/hvar/vvar) support
JimBobSquarePants Mar 11, 2026
e72ef8c
Apply gvar deltas to composite glyphs
JimBobSquarePants Mar 11, 2026
8903120
Support CFF2 vsindex for glyph variations
JimBobSquarePants Mar 11, 2026
f60c892
Support ItemVariationStore in GlyphDefinitionTable
JimBobSquarePants Mar 11, 2026
38a48df
Add MVAR support and refactor variation blending
JimBobSquarePants Mar 11, 2026
0ea3e79
Support MVAR metric deltas for variable fonts
JimBobSquarePants Mar 11, 2026
50a332a
Add cvar (CVT variations) table support
JimBobSquarePants Mar 11, 2026
7f99292
Support CVT variations (cvar) and caching
JimBobSquarePants Mar 11, 2026
18a7439
Apply GDEF VariationIndex deltas in GPOS/GSUB
JimBobSquarePants Mar 11, 2026
4e64db2
Add FeatureVariations support and normalized coords
JimBobSquarePants Mar 11, 2026
abf1e66
Add COLR variation delta resolution
JimBobSquarePants Mar 11, 2026
ebe78b4
Add variable font support and instance caches
JimBobSquarePants Mar 11, 2026
e2726c7
Improve CFF parsing, font-matrix & variations
JimBobSquarePants Mar 11, 2026
333f7c9
Remove dead comment
JimBobSquarePants Mar 11, 2026
8b0de97
Create KnownVariationAxes.cs
JimBobSquarePants Mar 11, 2026
286fa03
Support CFF2 index parsing & add variation tests
JimBobSquarePants Mar 11, 2026
162f4a1
Delete GlyphNameMap.cs
JimBobSquarePants Mar 11, 2026
59b5173
Add VotoSerif cvar fonts, reference images and tests
JimBobSquarePants Mar 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 115 additions & 38 deletions src/SixLabors.Fonts/BigEndianBinaryReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,52 +9,86 @@
namespace SixLabors.Fonts;

/// <summary>
/// <para>
/// A binary reader that reads in big-endian format.
/// </para>
/// <para>
/// This reader captures the stream position at construction time as <c>startOfStream</c>.
/// All offset values read from OpenType tables (via <see cref="ReadOffset16"/>,
/// <see cref="ReadOffset32"/>, etc.) are raw values relative to wherever the spec says
/// they originate (typically the start of the containing table).
/// </para>
/// <para>
/// When seeking with <see cref="Seek"/> using <see cref="SeekOrigin.Begin"/>, the
/// <c>startOfStream</c> is automatically added to the supplied offset. This means
/// table-relative offsets can be passed directly to <see cref="Seek"/> without manually
/// adding the table's absolute position. Do <strong>not</strong> add the table start
/// yourself — that would double-count and land at the wrong position.
/// </para>
/// <para>
/// In contrast, <see cref="BaseStream"/>.<see cref="Stream.Position"/> always returns the
/// <strong>absolute</strong> position within the underlying stream and is unaffected by
/// <c>startOfStream</c>.
/// </para>
/// </summary>
[DebuggerDisplay("Start: {StartOfStream}, Position: {BaseStream.Position}")]
internal sealed class BigEndianBinaryReader : IDisposable
{
/// <summary>
/// Buffer used for temporary storage before conversion into primitives
/// Buffer used for temporary storage before conversion into primitives.
/// </summary>
private readonly byte[] buffer = new byte[16];

private readonly long startOfStream;
private readonly bool leaveOpen;

/// <summary>
/// Initializes a new instance of the <see cref="BigEndianBinaryReader" /> class.
/// Constructs a new binary reader with the given bit converter, reading
/// to the given stream, using the given encoding.
/// The current position of <paramref name="stream"/> is captured as <c>startOfStream</c>
/// and used as the origin for all subsequent <see cref="Seek"/> calls with
/// <see cref="SeekOrigin.Begin"/>.
/// </summary>
/// <param name="stream">Stream to read data from</param>
/// <param name="leaveOpen">if set to <c>true</c> [leave open].</param>
/// <param name="stream">Stream to read data from.</param>
/// <param name="leaveOpen">If <see langword="true"/>, the stream is not disposed when this reader is disposed.</param>
public BigEndianBinaryReader(Stream stream, bool leaveOpen)
{
this.BaseStream = stream;
this.startOfStream = stream.Position;
this.StartOfStream = stream.Position;
this.leaveOpen = leaveOpen;
}

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

/// <summary>
/// Gets the absolute stream position captured at construction time.
/// This is the origin for all <see cref="SeekOrigin.Begin"/> seeks.
/// </summary>
public long StartOfStream { get; }

/// <summary>
/// Seeks within the stream.
/// When <paramref name="origin"/> is <see cref="SeekOrigin.Begin"/>, <c>startOfStream</c>
/// is automatically added to <paramref name="offset"/>, so callers should pass
/// table-relative offsets directly (e.g. values read from <see cref="ReadOffset16"/>
/// or <see cref="ReadOffset32"/>). Do <strong>not</strong> add the table's absolute
/// position — that would double-count.
/// </summary>
/// <param name="offset">Offset to seek to.</param>
/// <param name="origin">Origin of seek operation. If SeekOrigin.Begin, the offset will be set to the start of stream position.</param>
/// <param name="offset">Offset to seek to, relative to <paramref name="origin"/>.</param>
/// <param name="origin">Origin of seek operation.</param>
public void Seek(long offset, SeekOrigin origin)
{
// If SeekOrigin.Begin, the offset will be set to the start of stream position.
if (origin == SeekOrigin.Begin)
{
offset += this.startOfStream;
offset += this.StartOfStream;
}

this.BaseStream.Seek(offset, origin);
_ = this.BaseStream.Seek(offset, origin);
}

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

/// <summary>
/// Reads a single byte from the stream and reinterprets it as the specified enum type.
/// </summary>
/// <typeparam name="TEnum">The enum type whose underlying type must be a single byte.</typeparam>
/// <returns>The enum value.</returns>
public TEnum ReadByte<TEnum>()
where TEnum : struct, Enum
{
TryConvert(this.ReadByte(), out TEnum value);
_ = TryConvert(this.ReadByte(), out TEnum value);
return value;
}

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

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

/// <summary>
/// Reads a 16-bit integer from the stream and reinterprets it as the specified enum type.
/// </summary>
/// <typeparam name="TEnum">The enum type whose underlying type must be 16 bits.</typeparam>
/// <returns>The enum value.</returns>
public TEnum ReadInt16<TEnum>()
where TEnum : struct, Enum
{
TryConvert(this.ReadUInt16(), out TEnum value);
_ = TryConvert(this.ReadUInt16(), out TEnum value);
return value;
}

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

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

/// <summary>
Expand Down Expand Up @@ -171,25 +225,31 @@ public ushort ReadUInt16()

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

/// <summary>
/// Reads a 16-bit unsigned integer from the stream and reinterprets it as the specified enum type.
/// </summary>
/// <typeparam name="TEnum">The enum type whose underlying type must be 16 bits.</typeparam>
/// <returns>The enum value.</returns>
public TEnum ReadUInt16<TEnum>()
where TEnum : struct, Enum
{
TryConvert(this.ReadUInt16(), out TEnum value);
_ = TryConvert(this.ReadUInt16(), out TEnum value);
return value;
}

/// <summary>
/// Reads array of 16-bit unsigned integers from the stream.
/// Reads an array of 16-bit unsigned integers from the stream.
/// </summary>
/// <param name="length">The length.</param>
/// <returns>
/// The 16-bit unsigned integer read.
/// </returns>
/// <param name="length">The number of values to read.</param>
/// <returns>An array of 16-bit unsigned integers.</returns>
public ushort[] ReadUInt16Array(int length)
{
ushort[] data = new ushort[length];
Expand All @@ -214,12 +274,10 @@ public void ReadUInt16Array(Span<ushort> buffer)
}

/// <summary>
/// Reads array or 32-bit unsigned integers from the stream.
/// Reads an array of 32-bit unsigned integers from the stream.
/// </summary>
/// <param name="length">The length.</param>
/// <returns>
/// The 32-bit unsigned integer read.
/// </returns>
/// <param name="length">The number of values to read.</param>
/// <returns>An array of 32-bit unsigned integers.</returns>
public uint[] ReadUInt32Array(int length)
{
uint[] data = new uint[length];
Expand All @@ -231,6 +289,11 @@ public uint[] ReadUInt32Array(int length)
return data;
}

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

/// <summary>
/// Reads array of 16-bit unsigned integers from the stream.
/// Reads an array of 16-bit signed integers from the stream.
/// </summary>
/// <param name="length">The length.</param>
/// <returns>
/// The 16-bit signed integer read.
/// </returns>
/// <param name="length">The number of values to read.</param>
/// <returns>An array of 16-bit signed integers.</returns>
public short[] ReadInt16Array(int length)
{
short[] data = new short[length];
Expand Down Expand Up @@ -292,6 +353,14 @@ public uint ReadUInt24()
return (uint)((highByte << 16) | this.ReadUInt16());
}

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

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

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

/// <summary>
/// Reads the uint32 string.
/// Reads a 4-byte OpenType tag from the stream as a UTF-8 string.
/// </summary>
/// <returns>a 4 character long UTF8 encoded string.</returns>
/// <returns>A 4-character string representing the tag (e.g. "glyf", "GPOS").</returns>
public string ReadTag()
{
this.ReadInternal(this.buffer, 4);
Expand All @@ -371,11 +443,15 @@ public string ReadTag()
}

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

/// <inheritdoc />
public void Dispose()
{
if (!this.leaveOpen)
Expand Down
10 changes: 10 additions & 0 deletions src/SixLabors.Fonts/Buffer{T}.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ internal ref struct Buffer<T>
private bool isDisposed;

public Buffer(int length)
: this(length, clear: false)
{
}

public Buffer(int length, bool clear)
{
Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length));
int itemSizeBytes = Unsafe.SizeOf<T>();
Expand All @@ -30,6 +35,11 @@ public Buffer(int length)
this.Memory = manager.Memory[..this.length];
this.span = this.Memory.Span;

if (clear)
{
this.span.Clear();
}

this.isDisposed = false;
}

Expand Down
18 changes: 18 additions & 0 deletions src/SixLabors.Fonts/FileFontMetrics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Numerics;
using SixLabors.Fonts.Tables;
using SixLabors.Fonts.Tables.AdvancedTypographic;
using SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
using SixLabors.Fonts.Unicode;

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

/// <summary>
/// Gets the underlying <see cref="StreamFontMetrics"/> that this file-backed instance delegates to.
/// </summary>
internal StreamFontMetrics StreamFontMetrics => this.fontMetrics.Value;

/// <inheritdoc />
public override ushort UnitsPerEm => this.fontMetrics.Value.UnitsPerEm;

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

/// <inheritdoc/>
public override bool TryGetVariationAxes(out VariationAxis[]? variationAxes)
=> this.fontMetrics.Value.TryGetVariationAxes(out variationAxes);

/// <inheritdoc/>
internal override bool IsInMarkFilteringSet(ushort markGlyphSetIndex, ushort glyphId)
=> this.fontMetrics.Value.IsInMarkFilteringSet(markGlyphSetIndex, glyphId);
Expand Down Expand Up @@ -163,6 +173,14 @@ internal override bool TryGetKerningOffset(ushort currentId, ushort nextId, out
internal override void UpdatePositions(GlyphPositioningCollection collection)
=> this.fontMetrics.Value.UpdatePositions(collection);

/// <inheritdoc/>
internal override float GetGDefVariationDelta(uint packedVariationIndex)
=> this.fontMetrics.Value.GetGDefVariationDelta(packedVariationIndex);

/// <inheritdoc/>
internal override ReadOnlySpan<float> GetNormalizedCoordinates()
=> this.fontMetrics.Value.GetNormalizedCoordinates();

/// <summary>
/// Reads a <see cref="StreamFontMetrics"/> from the specified stream.
/// </summary>
Expand Down
Loading
Loading