diff --git a/src/SixLabors.Fonts/BigEndianBinaryReader.cs b/src/SixLabors.Fonts/BigEndianBinaryReader.cs
index fee870b5..70c78541 100644
--- a/src/SixLabors.Fonts/BigEndianBinaryReader.cs
+++ b/src/SixLabors.Fonts/BigEndianBinaryReader.cs
@@ -356,7 +356,7 @@ public string ReadTag()
}
///
- /// Reads an offset consuming the given nuber of bytes.
+ /// Reads an offset consuming the given number of bytes.
///
/// The offset size in bytes.
/// The 32-bit signed integer representing the offset.
@@ -393,6 +393,7 @@ private void ReadInternal(byte[] data, int size)
}
}
+ ///
public void Dispose()
{
if (!this.leaveOpen)
diff --git a/src/SixLabors.Fonts/FileFontMetrics.cs b/src/SixLabors.Fonts/FileFontMetrics.cs
index e5f8c76f..42c2c5ad 100644
--- a/src/SixLabors.Fonts/FileFontMetrics.cs
+++ b/src/SixLabors.Fonts/FileFontMetrics.cs
@@ -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;
@@ -115,6 +116,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);
+ ///
+ public override bool TryGetVariationAxes(out VariationAxis[]? variationAxes)
+ => this.fontMetrics.Value.TryGetVariationAxes(out variationAxes);
+
///
public override bool TryGetGlyphMetrics(
CodePoint codePoint,
diff --git a/src/SixLabors.Fonts/FontMetrics.cs b/src/SixLabors.Fonts/FontMetrics.cs
index ba332e16..8f83fdc5 100644
--- a/src/SixLabors.Fonts/FontMetrics.cs
+++ b/src/SixLabors.Fonts/FontMetrics.cs
@@ -4,6 +4,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using SixLabors.Fonts.Tables.AdvancedTypographic;
+using SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
using SixLabors.Fonts.Unicode;
namespace SixLabors.Fonts;
@@ -159,6 +160,14 @@ internal FontMetrics()
/// true, if the mark attachment class could be retrieved.
internal abstract bool TryGetMarkAttachmentClass(ushort glyphId, [NotNullWhen(true)] out GlyphClassDef? markAttachmentClass);
+ ///
+ /// Tries to get the variation axes that this font supports.
+ /// The font needs to have a fvar table.
+ ///
+ /// An array with Variation axes.
+ /// True, if fvar table is present.
+ public abstract bool TryGetVariationAxes(out VariationAxis[]? variationAxes);
+
///
/// Gets the glyph metrics for a given code point.
///
diff --git a/src/SixLabors.Fonts/FontReader.cs b/src/SixLabors.Fonts/FontReader.cs
index 37efd0e0..ad1ff033 100644
--- a/src/SixLabors.Fonts/FontReader.cs
+++ b/src/SixLabors.Fonts/FontReader.cs
@@ -138,18 +138,16 @@ public FontReader(Stream stream)
{
return (TTableType)table;
}
- else
+
+ TTableType? loadedTable = this.loader.Load(this);
+ if (loadedTable is null)
{
- TTableType? loadedTable = this.loader.Load(this);
- if (loadedTable is null)
- {
- return null;
- }
-
- table = loadedTable;
- this.loadedTables.Add(typeof(TTableType), loadedTable);
+ return null;
}
+ table = loadedTable;
+ this.loadedTables.Add(typeof(TTableType), loadedTable);
+
return (TTableType)table;
}
diff --git a/src/SixLabors.Fonts/StreamFontMetrics.Cff.cs b/src/SixLabors.Fonts/StreamFontMetrics.Cff.cs
index 42dfc9b0..bf56da5a 100644
--- a/src/SixLabors.Fonts/StreamFontMetrics.Cff.cs
+++ b/src/SixLabors.Fonts/StreamFontMetrics.Cff.cs
@@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using SixLabors.Fonts.Tables.AdvancedTypographic;
+using SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
using SixLabors.Fonts.Tables.Cff;
using SixLabors.Fonts.Tables.General;
using SixLabors.Fonts.Tables.General.Colr;
@@ -21,7 +22,7 @@ private static StreamFontMetrics LoadCompactFont(FontReader reader)
{
// Load using recommended order for best performance.
// https://www.microsoft.com/typography/otspec/recom.htm#TableOrdering
- // 'head', 'hhea', 'maxp', OS/2, 'name', 'cmap', 'post', 'CFF '
+ // 'head', 'hhea', 'maxp', OS/2, 'name', 'cmap', 'post', 'CFF ' / 'CFF2'
HeadTable head = reader.GetTable();
HorizontalHeadTable hhea = reader.GetTable();
MaximumProfileTable maxp = reader.GetTable();
@@ -29,7 +30,6 @@ private static StreamFontMetrics LoadCompactFont(FontReader reader)
NameTable name = reader.GetTable();
CMapTable cmap = reader.GetTable();
PostTable post = reader.GetTable();
-
ICffTable? cff = reader.TryGetTable() ?? (ICffTable?)reader.TryGetTable();
// TODO: VORG
@@ -50,6 +50,21 @@ private static StreamFontMetrics LoadCompactFont(FontReader reader)
ColrTable? colr = reader.TryGetTable();
CpalTable? cpal = reader.TryGetTable();
+ // Variations related tables.
+ FVarTable? fVar = reader.TryGetTable();
+ AVarTable? aVar = reader.TryGetTable();
+ GVarTable? gVar = reader.TryGetTable();
+ GlyphVariationProcessor? glyphVariationProcessor = null;
+ if (cff?.ItemVariationStore != null)
+ {
+ if (fVar is null)
+ {
+ throw new InvalidFontFileException("missing fvar table required for glyph variations processing");
+ }
+
+ glyphVariationProcessor = new GlyphVariationProcessor(cff.ItemVariationStore, fVar, aVar, gVar);
+ }
+
CompactFontTables tables = new(cmap, head, hhea, htmx, maxp, name, os2, post, cff!)
{
Kern = kern,
@@ -60,9 +75,12 @@ private static StreamFontMetrics LoadCompactFont(FontReader reader)
GPos = gPos,
Colr = colr,
Cpal = cpal,
+ FVar = fVar,
+ AVar = aVar,
+ GVar = gVar,
};
- return new StreamFontMetrics(tables);
+ return new StreamFontMetrics(tables, glyphVariationProcessor);
}
private GlyphMetrics CreateCffGlyphMetrics(
@@ -78,8 +96,14 @@ private GlyphMetrics CreateCffGlyphMetrics(
ICffTable cff = tables.Cff;
HorizontalMetricsTable htmx = tables.Htmx;
VerticalMetricsTable? vtmx = tables.Vmtx;
+ FVarTable? fVar = tables.FVar;
+ AVarTable? aVar = tables.AVar;
+ GVarTable? gVar = tables.GVar;
CffGlyphData vector = cff.GetGlyph(glyphId);
+ vector.FVar = fVar;
+ vector.AVar = aVar;
+ vector.GVar = gVar;
Bounds bounds = vector.GetBounds();
ushort advanceWidth = htmx.GetAdvancedWidth(glyphId);
short lsb = htmx.GetLeftSideBearing(glyphId);
diff --git a/src/SixLabors.Fonts/StreamFontMetrics.TrueType.cs b/src/SixLabors.Fonts/StreamFontMetrics.TrueType.cs
index 416e5af6..cb95319f 100644
--- a/src/SixLabors.Fonts/StreamFontMetrics.TrueType.cs
+++ b/src/SixLabors.Fonts/StreamFontMetrics.TrueType.cs
@@ -3,6 +3,7 @@
using System.Numerics;
using SixLabors.Fonts.Tables.AdvancedTypographic;
+using SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
using SixLabors.Fonts.Tables.General;
using SixLabors.Fonts.Tables.General.Colr;
using SixLabors.Fonts.Tables.General.Kern;
@@ -64,6 +65,12 @@ internal void ApplyTrueTypeHinting(HintingMode hintingMode, GlyphMetrics metrics
private static StreamFontMetrics LoadTrueTypeFont(FontReader reader)
{
+ // Load glyph variations related tables first, because glyph table needs them.
+ FVarTable? fvar = reader.TryGetTable();
+ AVarTable? avar = reader.TryGetTable();
+ GVarTable? gvar = reader.TryGetTable();
+ HVarTable? hvar = reader.TryGetTable();
+
// Load using recommended order for best performance.
// https://www.microsoft.com/typography/otspec/recom.htm#TableOrdering
// 'head', 'hhea', 'maxp', OS/2, 'hmtx', LTSH, VDMX, 'hdmx', 'cmap', 'fpgm', 'prep', 'cvt ', 'loca', 'glyf', 'kern', 'name', 'post', 'gasp', PCLT, DSIG
@@ -109,6 +116,10 @@ private static StreamFontMetrics LoadTrueTypeFont(FontReader reader)
GPos = gPos,
Colr = colr,
Cpal = cpal,
+ Fvar = fvar,
+ Gvar = gvar,
+ Hvar = hvar,
+ Avar = avar
};
return new StreamFontMetrics(tables);
diff --git a/src/SixLabors.Fonts/StreamFontMetrics.cs b/src/SixLabors.Fonts/StreamFontMetrics.cs
index d9fed0f7..6b06a1e7 100644
--- a/src/SixLabors.Fonts/StreamFontMetrics.cs
+++ b/src/SixLabors.Fonts/StreamFontMetrics.cs
@@ -3,9 +3,11 @@
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
using System.Numerics;
using SixLabors.Fonts.Tables;
using SixLabors.Fonts.Tables.AdvancedTypographic;
+using SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
using SixLabors.Fonts.Tables.Cff;
using SixLabors.Fonts.Tables.General;
using SixLabors.Fonts.Tables.General.Colr;
@@ -76,11 +78,13 @@ internal StreamFontMetrics(TrueTypeFontTables tables)
/// Initializes a new instance of the class.
///
/// The Compact Font tables.
- internal StreamFontMetrics(CompactFontTables tables)
+ /// Processor which handles glyph variations.
+ internal StreamFontMetrics(CompactFontTables tables, GlyphVariationProcessor? glyphVariationProcessor = null)
{
this.compactFontTables = tables;
this.outlineType = OutlineType.CFF;
this.description = new FontDescription(tables.Name, tables.Os2, tables.Head);
+ this.GlyphVariationProcessor = glyphVariationProcessor;
this.glyphIdCache = new();
this.glyphCache = new();
if (tables.Colr is not null)
@@ -95,6 +99,8 @@ internal StreamFontMetrics(CompactFontTables tables)
public HeadTable.HeadFlags HeadFlags { get; private set; }
+ public GlyphVariationProcessor? GlyphVariationProcessor { get; private set; }
+
///
public override FontDescription Description => this.description;
@@ -196,6 +202,35 @@ internal override bool TryGetMarkAttachmentClass(ushort glyphId, [NotNullWhen(tr
return gdef is not null && gdef.TryGetMarkAttachmentClass(glyphId, out markAttachmentClass);
}
+ ///
+ public override bool TryGetVariationAxes(out VariationAxis[]? variationAxes)
+ {
+ if (this.trueTypeFontTables?.Fvar == null)
+ {
+ variationAxes = Array.Empty();
+ return false;
+ }
+
+ FVarTable? fvar = this.trueTypeFontTables?.Fvar;
+ Tables.General.Name.NameTable? names = this.trueTypeFontTables?.Name;
+ variationAxes = new VariationAxis[fvar!.Axes.Length];
+ for (int i = 0; i < fvar.Axes.Length; i++)
+ {
+ VariationAxisRecord axis = fvar.Axes[i];
+ string name = names != null ? names.GetNameById(CultureInfo.InvariantCulture, axis.AxisNameId) : string.Empty;
+ variationAxes[i] = new VariationAxis()
+ {
+ Tag = axis.Tag,
+ Min = axis.MinValue,
+ Max = axis.MaxValue,
+ Default = axis.DefaultValue,
+ Name = name
+ };
+ }
+
+ return true;
+ }
+
///
public override bool TryGetGlyphMetrics(
CodePoint codePoint,
@@ -367,10 +402,8 @@ internal static StreamFontMetrics LoadFont(FontReader reader)
{
return LoadTrueTypeFont(reader);
}
- else
- {
- return LoadCompactFont(reader);
- }
+
+ return LoadCompactFont(reader);
}
private (HorizontalMetrics HorizontalMetrics, VerticalMetrics VerticalMetrics) Initialize(T tables)
diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/AVarTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/AVarTable.cs
new file mode 100644
index 00000000..6d08fd70
--- /dev/null
+++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/AVarTable.cs
@@ -0,0 +1,73 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
+
+///
+/// Implements reading the Font Variations Table `avar`.
+///
+///
+internal class AVarTable : Table
+{
+ internal const string TableName = "avar";
+
+ public AVarTable(uint axisCount, SegmentMapRecord[] segmentMaps)
+ {
+ this.AxisCount = axisCount;
+ this.SegmentMaps = segmentMaps;
+ }
+
+ public uint AxisCount { get; }
+
+ public SegmentMapRecord[] SegmentMaps { get; }
+
+ public static AVarTable? Load(FontReader reader)
+ {
+ if (!reader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader))
+ {
+ return null;
+ }
+
+ using (binaryReader)
+ {
+ return Load(binaryReader);
+ }
+ }
+
+ public static AVarTable Load(BigEndianBinaryReader reader)
+ {
+ // VariationsTable `avar`
+ // +-----------------+----------------------------------------+-------------------------------------------------------------------------+
+ // | Type | Name | Description |
+ // +=================+========================================+=========================================================================+
+ // | uint16 | majorVersion | Major version number of the font variations table — set to 1. |
+ // +-----------------+----------------------------------------+-------------------------------------------------------------------------+
+ // | uint16 | minorVersion | Minor version number of the font variations table — set to 0. |
+ // +-----------------+----------------------------------------+-------------------------------------------------------------------------+
+ // | uint16 | (reserved) | This field is permanently reserved. Set to zero. |
+ // +-----------------+----------------------------------------+-------------------------------------------------------------------------+
+ // | uint16 | axisCount | The number of variation axes in the font |
+ // | | | (the number of records in the axes array). |
+ // +-----------------+----------------------------------------+-------------------------------------------------------------------------+
+ // | SegmentMaps | axisSegmentMaps[axisCount] | The segment maps array — one segment map for each axis, in the order of |
+ // | | | axes specified in the 'fvar' table. |
+ // +-----------------+----------------------------------------+-------------------------------------------------------------------------+
+ ushort major = reader.ReadUInt16();
+ ushort minor = reader.ReadUInt16();
+ ushort reserved = reader.ReadUInt16();
+ ushort axisCount = reader.ReadUInt16();
+
+ if (major != 1)
+ {
+ throw new NotSupportedException("Only version 1 of avar table is supported");
+ }
+
+ var segmentMaps = new SegmentMapRecord[axisCount];
+ for (int i = 0; i < axisCount; i++)
+ {
+ segmentMaps[i] = SegmentMapRecord.Load(reader);
+ }
+
+ return new AVarTable(axisCount, segmentMaps);
+ }
+}
diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/AxisValueMapRecord.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/AxisValueMapRecord.cs
new file mode 100644
index 00000000..32c0706e
--- /dev/null
+++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/AxisValueMapRecord.cs
@@ -0,0 +1,33 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
+
+internal class AxisValueMapRecord
+{
+ public AxisValueMapRecord(float fromCoordinate, float toCoordinate)
+ {
+ this.FromCoordinate = fromCoordinate;
+ this.ToCoordinate = toCoordinate;
+ }
+
+ public float FromCoordinate { get; }
+
+ public float ToCoordinate { get; }
+
+ public static AxisValueMapRecord Load(BigEndianBinaryReader reader)
+ {
+ // AxisValueMapRecord
+ // +-----------------+----------------------------------------+-------------------------------------------------------------------------+
+ // | Type | Name | Description |
+ // +=================+========================================+=========================================================================+
+ // | F2DOT14 | fromCoordinate | A normalized coordinate value obtained using default normalization. |
+ // +-----------------+----------------------------------------+-------------------------------------------------------------------------+
+ // | F2DOT14 | toCoordinate | The modified, normalized coordinate value. |
+ // +-----------------+----------------------------------------+-------------------------------------------------------------------------+
+ float fromCoordinate = reader.ReadF2dot14();
+ float toCoordinate = reader.ReadF2dot14();
+
+ return new AxisValueMapRecord(fromCoordinate, toCoordinate);
+ }
+}
diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/DeltaSet.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/DeltaSet.cs
new file mode 100644
index 00000000..6529b3d8
--- /dev/null
+++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/DeltaSet.cs
@@ -0,0 +1,42 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
+
+internal class DeltaSet
+{
+ public DeltaSet(BigEndianBinaryReader reader, int wordDeltas, bool longWords, ushort regionIndexCount)
+ {
+ this.ShortDeltas = new int[wordDeltas];
+ for (int i = 0; i < wordDeltas; i++)
+ {
+ this.ShortDeltas[i] = longWords ? reader.ReadInt32() : reader.ReadInt16();
+ }
+
+ int remaining = regionIndexCount - wordDeltas;
+ this.RegionDeltas = new short[remaining];
+ for (int i = 0; i < remaining; i++)
+ {
+ this.RegionDeltas[i] = longWords ? reader.ReadInt16() : reader.ReadSByte();
+ }
+
+ this.Deltas = new int[this.RegionDeltas.Length + this.ShortDeltas.Length];
+ int offset = 0;
+
+ for (int i = 0; i < this.ShortDeltas.Length; i++)
+ {
+ this.Deltas[offset++] = this.ShortDeltas[i];
+ }
+
+ for (int i = 0; i < this.RegionDeltas.Length; i++)
+ {
+ this.Deltas[offset++] = this.RegionDeltas[i];
+ }
+ }
+
+ public short[] RegionDeltas { get; }
+
+ public int[] ShortDeltas { get; }
+
+ public int[] Deltas { get; }
+}
diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/DeltaSetIndexMap.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/DeltaSetIndexMap.cs
new file mode 100644
index 00000000..6d42157f
--- /dev/null
+++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/DeltaSetIndexMap.cs
@@ -0,0 +1,80 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System;
+using System.IO;
+
+namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
+
+internal class DeltaSetIndexMap
+{
+ private const int InnerIndexBitCountMask = 0x0F;
+
+ private const int MapEntrySizeMask = 0x30;
+
+ public DeltaSetIndexMap(int outerIndex, int innerIndex)
+ {
+ this.OuterIndex = outerIndex;
+ this.InnerIndex = innerIndex;
+ }
+
+ public int OuterIndex { get; }
+
+ public int InnerIndex { get; }
+
+ public static DeltaSetIndexMap[] Load(BigEndianBinaryReader reader, long offset)
+ {
+ // DeltaSetIndexMap.
+ // +-----------------+----------------------------------------+-----------------------------------------------------------------------------------+
+ // | Type | Name | Description |
+ // +=================+========================================+===================================================================================+
+ // | uint8 | format | DeltaSetIndexMap format. Either 0 or 1 |
+ // +-----------------+----------------------------------------+-----------------------------------------------------------------------------------+
+ // | uint8 | entryFormat | A packed field that describes the compressed representation of delta-set indices. |
+ // +-----------------+----------------------------------------+-----------------------------------------------------------------------------------+
+ // | uint16 or uin32 | mapCount | The number of mapping entries. uint16 for format0, uint32 for format 1 |
+ // +-----------------+----------------------------------------+-----------------------------------------------------------------------------------+
+ // | uint8 | mapData[variable] | The delta-set index mapping data. |
+ // +-----------------+----------------------------------------+-----------------------------------------------------------------------------------+
+ reader.Seek(offset, SeekOrigin.Begin);
+ byte format = reader.ReadUInt8();
+ byte entryFormat = reader.ReadUInt8();
+ ushort mapCount = reader.ReadUInt16();
+
+ if (format is not 0 or 1)
+ {
+ throw new NotSupportedException("Only format 0 or 1 of DeltaSetIndexMap is supported");
+ }
+
+ int entrySize = ((entryFormat & MapEntrySizeMask) >> 4) + 1;
+ int outerIndex = entrySize >> ((entryFormat & InnerIndexBitCountMask) + 1);
+ int innerIndex = entrySize & ((1 << ((entryFormat & InnerIndexBitCountMask) + 1)) - 1);
+
+ var deltaSetIndexMaps = new DeltaSetIndexMap[mapCount];
+ for (int i = 0; i < mapCount; i++)
+ {
+ int entry;
+ switch (entrySize)
+ {
+ case 1:
+ entry = reader.ReadByte();
+ break;
+ case 2:
+ entry = (reader.ReadByte() << 8) | reader.ReadByte();
+ break;
+ case 3:
+ entry = (reader.ReadByte() << 16) | (reader.ReadByte() << 8) | reader.ReadByte();
+ break;
+ case 4:
+ entry = (reader.ReadByte() << 24) | (reader.ReadByte() << 16) | (reader.ReadByte() << 8) | reader.ReadByte();
+ break;
+ default:
+ throw new NotSupportedException("unsupported delta set index map");
+ }
+
+ deltaSetIndexMaps[i] = new DeltaSetIndexMap((ushort)(entry & innerIndex), (ushort)(entry >> outerIndex));
+ }
+
+ return deltaSetIndexMaps;
+ }
+}
diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/FVarTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/FVarTable.cs
new file mode 100644
index 00000000..7352768e
--- /dev/null
+++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/FVarTable.cs
@@ -0,0 +1,100 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System;
+
+namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
+
+///
+/// Implements reading the Font Variations Table `fvar`.
+///
+///
+internal class FVarTable : Table
+{
+ internal const string TableName = "fvar";
+
+ public FVarTable(ushort axisCount, VariationAxisRecord[] axes, InstanceRecord[] instances)
+ {
+ this.AxisCount = axisCount;
+ this.Axes = axes;
+ this.Instances = instances;
+ }
+
+ public ushort AxisCount { get; }
+
+ public VariationAxisRecord[] Axes { get; }
+
+ public InstanceRecord[] Instances { get; }
+
+ public static FVarTable? Load(FontReader reader)
+ {
+ if (!reader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader))
+ {
+ return null;
+ }
+
+ using (binaryReader)
+ {
+ return Load(binaryReader);
+ }
+ }
+
+ public static FVarTable Load(BigEndianBinaryReader reader)
+ {
+ // VariationsTable `fvar`
+ // +-----------------+----------------------------------------+----------------------------------------------------------------+
+ // | Type | Name | Description |
+ // +=================+========================================+================================================================+
+ // | uint16 | majorVersion | Major version number of the font variations table — set to 1. |
+ // +-----------------+----------------------------------------+----------------------------------------------------------------+
+ // | uint16 | minorVersion | Minor version number of the font variations table — set to 0. |
+ // +-----------------+----------------------------------------+----------------------------------------------------------------+
+ // | Offset16 | axesArrayOffset | Offset in bytes from the beginning of the table to the start |
+ // | | | of the VariationAxisRecord array. |
+ // +-----------------+----------------------------------------+----------------------------------------------------------------+
+ // | uint16 | (reserved) | This field is permanently reserved. Set to 2. |
+ // +-----------------+----------------------------------------+----------------------------------------------------------------+
+ // | uint16 | axisCount | The number of variation axes in the font |
+ // | | | (the number of records in the axes array). |
+ // +-----------------+----------------------------------------+----------------------------------------------------------------+
+ // | uint16 | axisSize | The size in bytes of each VariationAxisRecord |
+ // | | | — set to 20 (0x0014) for this version. |
+ // +-----------------+----------------------------------------+----------------------------------------------------------------+
+ // | uint16 | instanceCount | The number of named instances defined in the font |
+ // | | | (the number of records in the instances array). |
+ // +-----------------+----------------------------------------+----------------------------------------------------------------+
+ // | uint16 | instanceSize | The size in bytes of each InstanceRecord |
+ // | | | — set to either axisCount * sizeof(Fixed) + 4, |
+ // | | | or to axisCount * sizeof(Fixed) + 6. |
+ // +-----------------+----------------------------------------+----------------------------------------------------------------+
+ long startOffset = reader.BaseStream.Position;
+ ushort major = reader.ReadUInt16();
+ ushort minor = reader.ReadUInt16();
+ ushort axesArrayOffset = reader.ReadOffset16();
+ ushort reserved = reader.ReadUInt16();
+ ushort axisCount = reader.ReadUInt16();
+ ushort axisSize = reader.ReadUInt16();
+ ushort instanceCount = reader.ReadUInt16();
+ ushort instanceSize = reader.ReadUInt16();
+
+ if (major != 1)
+ {
+ throw new NotSupportedException("Only version 1 of fvar table is supported");
+ }
+
+ var axesArray = new VariationAxisRecord[axisCount];
+ for (int i = 0; i < axisCount; i++)
+ {
+ axesArray[i] = VariationAxisRecord.Load(reader, axesArrayOffset + (axisSize * i));
+ }
+
+ var instances = new InstanceRecord[instanceCount];
+ long instancesOffset = reader.BaseStream.Position - startOffset;
+ for (int i = 0; i < instanceCount; i++)
+ {
+ instances[i] = InstanceRecord.Load(reader, instancesOffset + (i * instanceSize), axisCount);
+ }
+
+ return new FVarTable(axisCount, axesArray, instances);
+ }
+}
diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/GVarTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/GVarTable.cs
new file mode 100644
index 00000000..4be1abc5
--- /dev/null
+++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/GVarTable.cs
@@ -0,0 +1,111 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
+
+///
+/// Implements reading the Font Variations Table `gvar`.
+///
+///
+internal class GVarTable : Table
+{
+ internal const string TableName = "gvar";
+
+ public GVarTable(ushort axisCount, ushort glyphCount, float[,] sharedTuples, GlyphVariationData[] glyphVariations)
+ {
+ this.AxisCount = axisCount;
+ this.GlyphCount = glyphCount;
+ this.SharedTuples = sharedTuples;
+ this.GlyphVariations = glyphVariations;
+ }
+
+ public ushort AxisCount { get; }
+
+ public ushort GlyphCount { get; }
+
+ public float[,] SharedTuples { get; }
+
+ public GlyphVariationData[] GlyphVariations { get; }
+
+ public static GVarTable? Load(FontReader reader)
+ {
+ if (!reader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader))
+ {
+ return null;
+ }
+
+ using (binaryReader)
+ {
+ return Load(binaryReader);
+ }
+ }
+
+ public static GVarTable Load(BigEndianBinaryReader reader)
+ {
+ // VariationsTable `gvar`
+ // +-----------------+----------------------------------------+-------------------------------------------------------------------------+
+ // | Type | Name | Description |
+ // +=================+========================================+=========================================================================+
+ // | uint16 | majorVersion | Major version number of the font variations table — set to 1. |
+ // +-----------------+----------------------------------------+-------------------------------------------------------------------------+
+ // | uint16 | minorVersion | Minor version number of the font variations table — set to 0. |
+ // +-----------------+----------------------------------------+-------------------------------------------------------------------------+
+ // | uint16 | axisCount | The number of variation axes in the font |
+ // | | | (the number of records in the axes array). |
+ // +-----------------+----------------------------------------+-------------------------------------------------------------------------+
+ // | uint16 | sharedTupleCount | The number of shared tuple records. Shared tuple records can |
+ // | | | be referenced within glyph variation data tables for multiple glyphs, |
+ // | | | as opposed to other tuple records stored directly within a glyph |
+ // | | | variation data table. |
+ // +-----------------+----------------------------------------+-------------------------------------------------------------------------+
+ // | Offset32 | sharedTuplesOffset | Offset from the start of this table to the shared tuple records. |
+ // +-----------------+----------------------------------------+-------------------------------------------------------------------------+
+ // | uint16 | glyphCount | The number of glyphs in this font. This must match the number of glyphs |
+ // | | | stored elsewhere in the font. |
+ // +-----------------+----------------------------------------+-------------------------------------------------------------------------+
+ // | uint16 | flags | Bit-field that gives the format of the offset array that follows. |
+ // | | | If bit 0 is clear, the offsets are uint16; if bit 0 is set, |
+ // | | | the offsets are uint32. |
+ // +-----------------+----------------------------------------+-------------------------------------------------------------------------+
+ // | Offset32 | glyphVariationDataArrayOffset | Offset from the start of this table to the array of GlyphVariationData |
+ // | | | tables. |
+ // +-----------------+----------------------------------------+-------------------------------------------------------------------------+
+ // | Offset16 or | glyphVariationDataOffsets[glyphCount+1]| Offsets from the start of the GlyphVariationData array to each |
+ // | Offset32 | | GlyphVariationData table. |
+ // +-----------------+----------------------------------------+-------------------------------------------------------------------------+
+ long startOffset = reader.BaseStream.Position;
+ ushort major = reader.ReadUInt16();
+ ushort minor = reader.ReadUInt16();
+ ushort axisCount = reader.ReadUInt16();
+ ushort sharedTupleCount = reader.ReadUInt16();
+ ushort sharedTuplesOffset = reader.ReadOffset16();
+ ushort glyphCount = reader.ReadUInt16();
+ ushort flags = reader.ReadUInt16();
+ bool is32BitOffset = (flags & 1) == 1;
+ ushort glyphVariationDataArrayOffset = reader.ReadOffset16();
+
+ if (major != 1)
+ {
+ throw new NotSupportedException("Only version 1 of gvar table is supported");
+ }
+
+ reader.Seek(startOffset + sharedTuplesOffset, SeekOrigin.Begin);
+ float[,] sharedTuples = new float[sharedTupleCount, axisCount];
+ for (int i = 0; i < sharedTupleCount; i++)
+ {
+ for (int j = 0; j < axisCount; j++)
+ {
+ sharedTuples[i, j] = reader.ReadF2dot14();
+ }
+ }
+
+ int glyphVariationsCount = glyphCount + 1;
+ GlyphVariationData[] glyphVariations = new GlyphVariationData[glyphVariationsCount];
+ for (int i = 0; i < glyphVariationsCount; i++)
+ {
+ glyphVariations[i] = GlyphVariationData.Load(reader, glyphVariationDataArrayOffset, is32BitOffset, axisCount);
+ }
+
+ return new GVarTable(axisCount, glyphCount, sharedTuples, glyphVariations);
+ }
+}
diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/GlyphVariationData.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/GlyphVariationData.cs
new file mode 100644
index 00000000..955ecd5b
--- /dev/null
+++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/GlyphVariationData.cs
@@ -0,0 +1,55 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.IO;
+
+namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
+
+///
+/// Implements loading glyph variation data structure.
+///
+///
+internal class GlyphVariationData
+{
+ ///
+ /// Mask for the low bits to give the number of tuple variation tables.
+ ///
+ internal const int CountMask = 0x0FFF;
+
+ ///
+ /// Flag indicating that some or all tuple variation tables reference a shared set of “point” numbers.
+ /// These shared numbers are represented as packed point number data at the start of the serialized data.
+ ///
+ internal const int SharedPointNumbersMask = 0x8000;
+
+ public static GlyphVariationData Load(BigEndianBinaryReader reader, long offset, bool is32BitOffset, int axisCount)
+ {
+ // GlyphVariationData
+ // +----------------------+-------------------------------------------+------------------------------------------------------------------------------+
+ // | Type | Name | Description |
+ // +======================+===========================================+==============================================================================+
+ // | uint16 | tupleVariationCount | A packed field. The high 4 bits are flags, |
+ // | | | and the low 12 bits are the number of tuple variation tables for this glyph. |
+ // | | | The count can be any number between 1 and 4095. |
+ // +----------------------+-------------------------------------------+------------------------------------------------------------------------------+
+ // | Offset16 | dataOffset | Offset from the start of the GlyphVariationData table to the serialized data.|
+ // +----------------------+-------------------------------------------+------------------------------------------------------------------------------+
+ // | TupleVariation | tupleVariationHeaders[tupleVariationCount]| Array of tuple variation headers. |
+ // +----------------------+-------------------------------------------+------------------------------------------------------------------------------+
+ reader.Seek(offset, SeekOrigin.Begin);
+ ushort tupleVariationCount = reader.ReadUInt16();
+ bool sharedPointNumbers = (tupleVariationCount & SharedPointNumbersMask) == SharedPointNumbersMask;
+
+ int tupleVariationTables = tupleVariationCount & CountMask;
+ TupleVariation[] variationHeaders = new TupleVariation[tupleVariationTables];
+ for (int i = 0; i < tupleVariationTables; i++)
+ {
+ variationHeaders[i] = TupleVariation.Load(reader, axisCount);
+ }
+
+ // TODO: parse serialized data
+ int serializedDataOffset = is32BitOffset ? reader.ReadInt32() : reader.ReadOffset16();
+ reader.Seek(offset + serializedDataOffset, SeekOrigin.Begin);
+ return new GlyphVariationData();
+ }
+}
diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/GlyphVariationProcessor.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/GlyphVariationProcessor.cs
new file mode 100644
index 00000000..b74e2bf2
--- /dev/null
+++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/GlyphVariationProcessor.cs
@@ -0,0 +1,245 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.Fonts.Tables.TrueType.Glyphs;
+
+namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
+
+///
+///
+/// This class is transforms TrueType glyphs according to the data from
+/// the Apple Advanced Typography variation tables(fvar, gvar, and avar).
+/// These tables allow infinite adjustments to glyph weight, width, slant,
+/// and optical size without the designer needing to specify every exact style.
+///
+/// Implementation is based on fontkit:
+/// Docs for the item variations:
+///
+internal class GlyphVariationProcessor
+{
+ private readonly ItemVariationStore itemStore;
+
+ private readonly FVarTable fvar;
+
+ private readonly AVarTable? avar;
+
+ private readonly GVarTable? gVar;
+
+ private readonly HVarTable? hVar;
+
+ private readonly float[] normalizedCoords;
+
+ private readonly Dictionary blendVectors;
+
+ ///
+ /// Epsilon as used in fontkit reference implementation.
+ /// TODO: This could be float.Epsilon, but we need to check if it works.
+ ///
+ private const float Epsilon = 2.2204460492503130808472633361816E-16F;
+
+ public GlyphVariationProcessor(ItemVariationStore itemStore, FVarTable fVar, AVarTable? aVar = null, GVarTable? gVar = null, HVarTable? hVar = null)
+ {
+ DebugGuard.NotNull(itemStore, nameof(itemStore));
+ DebugGuard.NotNull(fVar, nameof(fVar));
+
+ this.itemStore = itemStore;
+ this.fvar = fVar;
+ this.avar = aVar;
+ this.gVar = gVar;
+ this.hVar = hVar;
+ this.normalizedCoords = this.NormalizeDefaultCoords();
+ this.blendVectors = new Dictionary();
+ }
+
+ public void TransformPoints(ushort glyphId, ref GlyphVector glyphPoints)
+ {
+ if (this.fvar is null || this.gVar is null)
+ {
+ return;
+ }
+
+ if (glyphId > this.gVar.GlyphCount)
+ {
+ return;
+ }
+
+ // TODO: It looks like more work is required in TupleVariation.Load to ensure
+ // we have all the information we require to transform the points.
+ GlyphVector originPoints = GlyphVector.DeepClone(glyphPoints);
+ }
+
+ public float AdvanceAdjustment(int glyphId)
+ {
+ if (this.hVar is null)
+ {
+ throw new InvalidFontFileException("Missing HVAR table");
+ }
+
+ int outerIndex;
+ int innerIndex;
+ if (this.hVar?.AdvanceWidthMapping != null && this.hVar?.AdvanceWidthMapping.Length > 0)
+ {
+ DeltaSetIndexMap[]? advanceWidthMapping = this.hVar?.AdvanceWidthMapping;
+ int idx = glyphId;
+ if (idx >= advanceWidthMapping?.Length)
+ {
+ idx = advanceWidthMapping.Length - 1;
+ }
+
+ outerIndex = advanceWidthMapping![idx].OuterIndex;
+ innerIndex = advanceWidthMapping[idx].InnerIndex;
+ }
+ else
+ {
+ outerIndex = 0;
+ innerIndex = glyphId;
+ }
+
+ return this.Delta(outerIndex, innerIndex);
+ }
+
+ public float[] BlendVector(int outerIndex)
+ {
+ ItemVariationData variationData = this.itemStore.ItemVariations[outerIndex];
+ if (this.blendVectors.TryGetValue(variationData, out float[]? blendVector))
+ {
+ return blendVector;
+ }
+
+ blendVector = new float[variationData.RegionIndexes.Length];
+
+ // Outer loop steps through master designs to be blended.
+ for (int i = 0; i < variationData.RegionIndexes.Length; i++)
+ {
+ float scalar = 1.0f;
+ ushort regionIndex = variationData.RegionIndexes[i];
+ RegionAxisCoordinates[] axes = this.itemStore.VariationRegionList.VariationRegions[regionIndex];
+
+ // Inner loop steps through axes in this region.
+ for (int j = 0; j < axes.Length; j++)
+ {
+ RegionAxisCoordinates axis = axes[j];
+
+ // Compute the scalar contribution of this axis, ignore invalid ranges.
+ float axisScalar;
+ if (axis.StartCoord > axis.PeakCoord || axis.PeakCoord > axis.EndCoord)
+ {
+ axisScalar = 1;
+ }
+ else if (axis.StartCoord < 0 && axis.EndCoord > 0 && axis.PeakCoord != 0)
+ {
+ axisScalar = 1;
+ }
+ else if (axis.PeakCoord == 0)
+ {
+ // Peak of 0 means ignore this axis.
+ axisScalar = 1;
+ }
+ else if (this.normalizedCoords[j] < axis.StartCoord || this.normalizedCoords[j] > axis.EndCoord)
+ {
+ // Ignore this region if coords are out of range
+ axisScalar = 0;
+ }
+ else
+ {
+ // Calculate a proportional factor.
+ if (this.normalizedCoords[j] == axis.PeakCoord)
+ {
+ axisScalar = 1;
+ }
+ else if (this.normalizedCoords[j] < axis.PeakCoord)
+ {
+ axisScalar = (this.normalizedCoords[j] - axis.StartCoord + Epsilon) /
+ (axis.PeakCoord - axis.StartCoord + Epsilon);
+ }
+ else
+ {
+ axisScalar = (axis.EndCoord - this.normalizedCoords[j] + Epsilon) /
+ (axis.EndCoord - axis.PeakCoord + Epsilon);
+ }
+ }
+
+ // Take product of all the axis scalars.
+ scalar *= axisScalar;
+ }
+
+ blendVector[i] = scalar;
+ }
+
+ this.blendVectors[variationData] = blendVector;
+
+ return blendVector;
+ }
+
+ private float[] NormalizeDefaultCoords()
+ {
+ float[] coords = new float[this.fvar.AxisCount];
+ for (int i = 0; i < this.fvar.AxisCount; i++)
+ {
+ coords[i] = this.fvar.Axes[i].DefaultValue;
+ }
+
+ // The default mapping is linear along each axis, in two segments:
+ // from the minValue to defaultValue, and from defaultValue to maxValue.
+ float[] normalized = new float[this.fvar.AxisCount];
+ for (int i = 0; i < this.fvar.AxisCount; i++)
+ {
+ VariationAxisRecord axis = this.fvar.Axes[i];
+ if (coords[i] < axis.DefaultValue)
+ {
+ normalized[i] = (coords[i] - axis.DefaultValue + Epsilon) / (axis.DefaultValue - axis.MinValue + Epsilon);
+ }
+ else
+ {
+ normalized[i] = (coords[i] - axis.DefaultValue + Epsilon) / (axis.MaxValue - axis.DefaultValue + Epsilon);
+ }
+ }
+
+ // If there is an avar table, the normalized value is calculated
+ // by interpolating between the two nearest mapped values.
+ if (this.avar is not null)
+ {
+ for (int i = 0; i < this.avar.SegmentMaps.Length; i++)
+ {
+ SegmentMapRecord segment = this.avar.SegmentMaps[i];
+ for (int j = 0; j < segment.AxisValueMap.Length; j++)
+ {
+ AxisValueMapRecord pair = segment.AxisValueMap[j];
+ if (j >= 1 && normalized[i] < pair.FromCoordinate)
+ {
+ AxisValueMapRecord prev = segment.AxisValueMap[j - 1];
+ normalized[i] = ((((normalized[i] - prev.FromCoordinate) * (pair.ToCoordinate - prev.ToCoordinate)) + Epsilon) /
+ (pair.FromCoordinate - prev.FromCoordinate + Epsilon)) + prev.ToCoordinate;
+ break;
+ }
+ }
+ }
+ }
+
+ return normalized;
+ }
+
+ private float Delta(int outerIndex, int innerIndex)
+ {
+ if (outerIndex >= this.itemStore.ItemVariations.Length)
+ {
+ return 0;
+ }
+
+ ItemVariationData variationData = this.itemStore.ItemVariations[outerIndex];
+ if (innerIndex >= variationData.DeltaSets.Length)
+ {
+ return 0;
+ }
+
+ DeltaSet deltaSet = variationData.DeltaSets[innerIndex];
+ float[] blendVector = this.BlendVector(outerIndex);
+ float netAdjustment = 0;
+ for (int master = 0; master < variationData.RegionIndexes.Length; master++)
+ {
+ netAdjustment += deltaSet.Deltas[master] * blendVector[master];
+ }
+
+ return netAdjustment;
+ }
+}
diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/HVarTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/HVarTable.cs
new file mode 100644
index 00000000..9c795bd3
--- /dev/null
+++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/HVarTable.cs
@@ -0,0 +1,89 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System;
+
+namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
+
+///
+/// Implements reading the font variations table `HVAR`.
+/// The HVAR table is used in variable fonts to provide variations for horizontal glyph metrics values.
+/// This can be used to provide variation data for advance widths in the 'hmtx' table.
+///
+///
+internal class HVarTable : Table
+{
+ internal const string TableName = "HVAR";
+
+ public HVarTable(ItemVariationStore itemVariationStore, DeltaSetIndexMap[] advanceWidthMapping, DeltaSetIndexMap[] lsbMapping, DeltaSetIndexMap[] rsbMapping)
+ {
+ this.ItemVariationStore = itemVariationStore;
+ this.AdvanceWidthMapping = advanceWidthMapping;
+ this.LsbMapping = lsbMapping;
+ this.RsbMapping = rsbMapping;
+ }
+
+ public ItemVariationStore ItemVariationStore { get; }
+
+ public DeltaSetIndexMap[] AdvanceWidthMapping { get; }
+
+ public DeltaSetIndexMap[] LsbMapping { get; }
+
+ public DeltaSetIndexMap[] RsbMapping { get; }
+
+ public static HVarTable? Load(FontReader reader)
+ {
+ if (!reader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader))
+ {
+ return null;
+ }
+
+ using (binaryReader)
+ {
+ return Load(binaryReader);
+ }
+ }
+
+ public static HVarTable Load(BigEndianBinaryReader reader)
+ {
+ // Horizontal metrics variations table
+ // +--------------------------+----------------------------------------+-------------------------------------------------------------------------+
+ // | Type | Name | Description |
+ // +==========================+========================================+=========================================================================+
+ // | uint16 | majorVersion | Major version number of the font variations table — set to 1. |
+ // +--------------------------+----------------------------------------+-------------------------------------------------------------------------+
+ // | uint16 | minorVersion | Minor version number of the font variations table — set to 0. |
+ // +--------------------------+----------------------------------------+-------------------------------------------------------------------------+
+ // | Offset32 | itemVariationStoreOffset | Offset in bytes from the start of this table to the |
+ // | | | item variation store table. |
+ // +--------------------------+----------------------------------------+-------------------------------------------------------------------------+
+ // | Offset32 | advanceWidthMappingOffset | Offset in bytes from the start of this table to the delta-set index |
+ // | | | mapping for advance widths (may be NULL). |
+ // +--------------------------+----------------------------------------+-------------------------------------------------------------------------+
+ // | Offset32 | lsbMappingOffset | Offset in bytes from the start of this table to the delta-set index |
+ // | | | mapping for left side bearings (may be NULL). |
+ // +--------------------------+----------------------------------------+-------------------------------------------------------------------------+
+ // | Offset32 | rsbMappingOffset | Offset in bytes from the start of this table to the delta-set index |
+ // | | | mapping for right side bearings (may be NULL). |
+ // +--------------------------+----------------------------------------+-------------------------------------------------------------------------+
+ ushort major = reader.ReadUInt16();
+ ushort minor = reader.ReadUInt16();
+ uint itemVariationStoreOffset = reader.ReadOffset32();
+ uint advanceWidthMappingOffset = reader.ReadOffset32();
+ uint lsbMappingOffset = reader.ReadOffset32();
+ uint rsbMappingOffset = reader.ReadOffset32();
+
+ if (major != 1)
+ {
+ throw new NotSupportedException("Only version 1 of hvar table is supported");
+ }
+
+ var itemVariationStore = ItemVariationStore.Load(reader, itemVariationStoreOffset);
+
+ DeltaSetIndexMap[] advanceWidthMapping = DeltaSetIndexMap.Load(reader, advanceWidthMappingOffset);
+ DeltaSetIndexMap[] lsbMapping = DeltaSetIndexMap.Load(reader, lsbMappingOffset);
+ DeltaSetIndexMap[] rsbMapping = DeltaSetIndexMap.Load(reader, rsbMappingOffset);
+
+ return new HVarTable(itemVariationStore, advanceWidthMapping, lsbMapping, rsbMapping);
+ }
+}
diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/InstanceRecord.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/InstanceRecord.cs
new file mode 100644
index 00000000..0dd241ca
--- /dev/null
+++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/InstanceRecord.cs
@@ -0,0 +1,58 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.IO;
+
+namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
+
+///
+/// Defines a InstanceRecord.
+///
+///
+internal class InstanceRecord
+{
+ public InstanceRecord(ushort subfamilyNameId, ushort postScriptNameId, float[] coordinates)
+ {
+ this.SubfamilyNameId = subfamilyNameId;
+ this.PostScriptNameId = postScriptNameId;
+ this.Coordinates = coordinates;
+ }
+
+ public ushort SubfamilyNameId { get; }
+
+ public ushort PostScriptNameId { get; }
+
+ public float[] Coordinates { get; }
+
+ public static InstanceRecord Load(BigEndianBinaryReader reader, long offset, ushort axisCount)
+ {
+ // InstanceRecord
+ // +-----------------+----------------------------------------+----------------------------------------------------------------+
+ // | Type | Name | Description |
+ // +=================+========================================+================================================================+
+ // | uint16 | subfamilyNameID | The name ID for entries in the 'name' table that provide |
+ // | | | subfamily names for this instance. |
+ // +-----------------+----------------------------------------+----------------------------------------------------------------+
+ // | uint16 | flags | Reserved for future use — set to 0. |
+ // +-----------------+----------------------------------------+----------------------------------------------------------------+
+ // | UserTuple | coordinates | The coordinates array for this instance. |
+ // +-----------------+----------------------------------------+----------------------------------------------------------------+
+ // | uint16 | postScriptNameID | Optional. The name ID for entries in the 'name' table that |
+ // | | | provide PostScript names for this instance. |
+ // +-----------------+----------------------------------------+----------------------------------------------------------------+
+ reader.Seek(offset, SeekOrigin.Begin);
+
+ ushort subfamilyNameId = reader.ReadUInt16();
+ ushort flags = reader.ReadUInt16();
+
+ float[] coordinates = new float[axisCount];
+ for (int i = 0; i < axisCount; i++)
+ {
+ coordinates[i] = reader.ReadFixed();
+ }
+
+ ushort postScriptNameId = reader.ReadUInt16();
+
+ return new InstanceRecord(subfamilyNameId, postScriptNameId, coordinates);
+ }
+}
diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/ItemVariationData.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/ItemVariationData.cs
new file mode 100644
index 00000000..9e0d85cf
--- /dev/null
+++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/ItemVariationData.cs
@@ -0,0 +1,87 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System;
+using System.Diagnostics;
+using System.IO;
+
+namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
+
+///
+/// Item variation data, docs:
+///
+[DebuggerDisplay("ItemCount: {ItemCount}, WordDeltaCount: {WordDeltaCount}, RegionIndexCount: {RegionIndexes.Length}")]
+internal sealed class ItemVariationData
+{
+ ///
+ /// Count of "word" deltas.
+ ///
+ private const int WordDeltaCountMask = 0x7FFF;
+
+ ///
+ /// Flag indicating that "word" deltas are long (int32).
+ ///
+ private const int LongWordsMask = 0x8000;
+
+ private ItemVariationData(ushort itemCount, ushort wordDeltaCount, ushort[] regionIndices, DeltaSet[] deltaSets)
+ {
+ this.ItemCount = itemCount;
+ this.WordDeltaCount = wordDeltaCount;
+ this.RegionIndexes = regionIndices;
+ this.DeltaSets = deltaSets;
+ }
+
+ public ushort ItemCount { get; }
+
+ public ushort WordDeltaCount { get; }
+
+ public ushort[] RegionIndexes { get; }
+
+ public DeltaSet[] DeltaSets { get; }
+
+ public static ItemVariationData Load(BigEndianBinaryReader reader, long offset)
+ {
+ // ItemVariationData
+ // +-----------------+----------------------------------------+----------------------------------------------------------------+
+ // | Type | Name | Description |
+ // +=================+========================================+================================================================+
+ // | uint16 | itemCount | The number of delta sets for distinct items. |
+ // +-----------------+----------------------------------------+----------------------------------------------------------------+
+ // | uint16 | wordDeltaCount | A packed field: the high bit is a flag. |
+ // +-----------------+----------------------------------------+----------------------------------------------------------------+
+ // + uint16 | regionIndexCount | The number of variation regions referenced. |
+ // +-----------------+----------------------------------------+----------------------------------------------------------------+
+ // + uint16 | regionIndexes[regionIndexCount] | Array of indices into the variation region list for |
+ // + | | the regions referenced by this item variation data table. |
+ // +-----------------+----------------------------------------+----------------------------------------------------------------+
+ // + DeltaSet | deltaSets[itemCount] | Delta-set rows. |
+ // +-----------------+----------------------------------------+----------------------------------------------------------------+
+ reader.Seek(offset, SeekOrigin.Begin);
+ ushort itemCount = reader.ReadUInt16();
+ ushort wordDeltaCount = reader.ReadUInt16();
+ ushort regionIndexCount = reader.ReadUInt16();
+ ushort[] regionIndexes = new ushort[regionIndexCount];
+ for (int i = 0; i < regionIndexCount; i++)
+ {
+ regionIndexes[i] = reader.ReadUInt16();
+ }
+
+ // The deltaSets array represents a logical two-dimensional table of delta values with itemCount rows and regionIndexCount columns.
+ // Logically, each DeltaSet record has regionIndexCount number of elements. The elements are represented using long and short types.
+ // These are either int16 and int8, or int32 and int16, according to whether the LONG_WORDS flag is set.
+ // The delta array has a sequence of deltas using the long type followed by a sequence of deltas using the short type.
+ bool longWords = (wordDeltaCount & LongWordsMask) != 0;
+ int wordDeltas = wordDeltaCount & WordDeltaCountMask;
+ var deltaSets = new DeltaSet[itemCount];
+ for (int i = 0; i < itemCount; i++)
+ {
+ var deltaSet = new DeltaSet(reader, wordDeltas, longWords, regionIndexCount);
+ deltaSets[i] = deltaSet;
+ }
+
+ return new ItemVariationData(itemCount, wordDeltaCount, regionIndexes, deltaSets);
+ }
+
+ ///
+ public override int GetHashCode() => HashCode.Combine(this.ItemCount, this.WordDeltaCount, this.RegionIndexes);
+}
diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/ItemVariationStore.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/ItemVariationStore.cs
new file mode 100644
index 00000000..fd105bbe
--- /dev/null
+++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/ItemVariationStore.cs
@@ -0,0 +1,77 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.IO;
+
+namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
+
+///
+/// Implements reading the item variation store, which is used in most glyph variation data.
+///
+///
+internal class ItemVariationStore
+{
+ public ItemVariationStore(VariationRegionList variationRegionList, ItemVariationData[] itemVariations)
+ {
+ this.VariationRegionList = variationRegionList;
+ this.ItemVariations = itemVariations;
+ }
+
+ public VariationRegionList VariationRegionList { get; }
+
+ public ItemVariationData[] ItemVariations { get; }
+
+ public static ItemVariationStore Load(BigEndianBinaryReader reader, long offset, long? length = null)
+ {
+ // ItemVariationStore
+ // +--------------------------+--------------------------------------------------+-------------------------------------------------------------------------+
+ // | Type | Name | Description |
+ // +==========================+==================================================+=========================================================================+
+ // | uint16 | format | Format — set to 1 |
+ // +--------------------------+--------------------------------------------------+-------------------------------------------------------------------------+
+ // | Offset32 | variationRegionListOffset | Offset in bytes from the start of the item variation store |
+ // | | | to the variation region list. |
+ // +--------------------------+--------------------------------------------------+-------------------------------------------------------------------------+
+ // | uint16 | itemVariationDataCount | The number of item variation data subtables. |
+ // +--------------------------+--------------------------------------------------+-------------------------------------------------------------------------+
+ // | Offset32 | itemVariationDataOffsets[itemVariationDataCount] | Offsets in bytes from the start of the item variation store |
+ // | | | to each item variation data subtable. |
+ // +--------------------------+--------------------------------------------------+-------------------------------------------------------------------------+
+ reader.Seek(offset, SeekOrigin.Begin);
+
+ ushort format = reader.ReadUInt16();
+ if (format != 1)
+ {
+ throw new InvalidFontFileException($"Invalid value for variation Store Format {format}. Should be '1'.");
+ }
+
+ uint variationRegionListOffset = reader.ReadOffset32();
+ ushort itemVariationDataCount = reader.ReadUInt16();
+
+ if (length.HasValue && variationRegionListOffset > length)
+ {
+ throw new InvalidFontFileException("Invalid variation region list offset");
+ }
+
+ var itemVariations = new ItemVariationData[itemVariationDataCount];
+ long itemVariationsOffset = reader.BaseStream.Position;
+ for (int i = 0; i < itemVariationDataCount; i++)
+ {
+ uint variationDataOffset = reader.ReadOffset32();
+ itemVariationsOffset += 4;
+ if (length.HasValue && offset + variationDataOffset >= length)
+ {
+ throw new InvalidFontFileException("Bad offset to variation data subtable");
+ }
+
+ var itemVariationData = ItemVariationData.Load(reader, offset + variationDataOffset);
+ itemVariations[i] = itemVariationData;
+
+ reader.BaseStream.Position = itemVariationsOffset;
+ }
+
+ var variationRegionList = VariationRegionList.Load(reader, offset + variationRegionListOffset);
+
+ return new ItemVariationStore(variationRegionList, itemVariations);
+ }
+}
diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/RegionAxisCoordinates.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/RegionAxisCoordinates.cs
new file mode 100644
index 00000000..6bcd5eaf
--- /dev/null
+++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/RegionAxisCoordinates.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics;
+
+namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
+
+///
+/// Each RegionAxisCoordinates record provides coordinate values for a region along a single axis.
+/// The three values must all be within the range -1.0 to +1.0. startCoord must be less than or equal to peakCoord,
+/// and peakCoord must be less than or equal to endCoord. The three values must be either all non-positive or all non-negative with one possible exception:
+/// if peakCoord is zero, then startCoord can be negative or 0 while endCoord can be positive or zero.
+///
+///
+[DebuggerDisplay("StartCoord: {StartCoord}, PeakCoord: {PeakCoord}, EndCoord: {EndCoord}")]
+public readonly struct RegionAxisCoordinates
+{
+ ///
+ /// Gets the region start coordinate value for the current axis.
+ ///
+ public float StartCoord { get; init; }
+
+ ///
+ /// Gets the region peak coordinate value for the current axis.
+ ///
+ public float PeakCoord { get; init; }
+
+ ///
+ /// Gets the region end coordinate value for the current axis.
+ ///
+ public float EndCoord { get; init; }
+}
diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/SegmentMapRecord.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/SegmentMapRecord.cs
new file mode 100644
index 00000000..a29d1144
--- /dev/null
+++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/SegmentMapRecord.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
+
+internal class SegmentMapRecord
+{
+ public SegmentMapRecord(AxisValueMapRecord[] axisValueMap) => this.AxisValueMap = axisValueMap;
+
+ public AxisValueMapRecord[] AxisValueMap { get; }
+
+ public static SegmentMapRecord Load(BigEndianBinaryReader reader)
+ {
+ // SegmentMapRecord
+ // +-----------------+----------------------------------------+-------------------------------------------------------------------------+
+ // | Type | Name | Description |
+ // +=================+========================================+=========================================================================+
+ // | uint16 | positionMapCount | The number of correspondence pairs for this axis. |
+ // +-----------------+----------------------------------------+-------------------------------------------------------------------------+
+ // | AxisValueMap | axisValueMaps[positionMapCount] | The array of axis value map records for this axis. |
+ // +-----------------+----------------------------------------+-------------------------------------------------------------------------+
+ ushort positionMapCount = reader.ReadUInt16();
+ var axisValueMap = new AxisValueMapRecord[positionMapCount];
+ for (int i = 0; i < positionMapCount; i++)
+ {
+ axisValueMap[i] = AxisValueMapRecord.Load(reader);
+ }
+
+ return new SegmentMapRecord(axisValueMap);
+ }
+}
diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/TupleVariation.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/TupleVariation.cs
new file mode 100644
index 00000000..7581d6af
--- /dev/null
+++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/TupleVariation.cs
@@ -0,0 +1,111 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
+
+internal class TupleVariation
+{
+ ///
+ /// Flag indicating that this tuple variation header includes an embedded peak tuple record, immediately after the tupleIndex field.
+ /// If set, the low 12 bits of the tupleIndex value are ignored.
+ /// Note that this must always be set within the 'cvar' table.
+ ///
+ internal const int EmbeddedPeakTupleMask = 0x8000;
+
+ ///
+ /// Flag indicating that this tuple variation table applies to an intermediate region within the variation space.
+ /// If set, the header includes the two intermediate-region, start and end tuple records, immediately after the peak tuple record (if present).
+ ///
+ internal const int IntermediateRegionMask = 0x4000;
+
+ ///
+ /// Flag indicating that the serialized data for this tuple variation table includes packed “point” number data.
+ /// If set, this tuple variation table uses that number data; if clear, this tuple variation table uses shared number
+ /// data found at the start of the serialized data for this glyph variation data or 'cvar' table.
+ ///
+ internal const int PrivatePointNumbersMask = 0x2000;
+
+ ///
+ /// Mask for the low 12 bits to give the shared tuple records index.
+ ///
+ internal const int TupleIndexMask = 0x0FFF;
+
+ public TupleVariation(int axisCount, float[]? embeddedPeak, float[]? intermediateStartRegion, float[]? intermediateEndRegion)
+ {
+ this.AxisCount = axisCount;
+ this.EmbeddedPeak = embeddedPeak;
+ this.IntermediateStartRegion = intermediateStartRegion;
+ this.IntermediateEndRegion = intermediateEndRegion;
+ }
+
+ public int AxisCount { get; }
+
+ public float[]? EmbeddedPeak { get; }
+
+ public float[]? IntermediateStartRegion { get; }
+
+ public float[]? IntermediateEndRegion { get; }
+
+ public static TupleVariation Load(BigEndianBinaryReader reader, int axisCount)
+ {
+ // TupleVariation
+ // +----------------------+-------------------------------------------+------------------------------------------------------------------------------+
+ // | Type | Name | Description |
+ // +======================+===========================================+==============================================================================+
+ // | uint16 | variationDataSize | The size in bytes of the serialized data for this tuple variation table. |
+ // +----------------------+-------------------------------------------+------------------------------------------------------------------------------+
+ // | uint16 | tupleIndex | A packed field. The high 4 bits are flags. |
+ // | | | The low 12 bits are an index into a shared tuple records array. |
+ // +----------------------+-------------------------------------------+------------------------------------------------------------------------------+
+ // | Tuple | peakTuple | Peak tuple record for this tuple variation table — |
+ // | | | optional, determined by flags in the tupleIndex value. |
+ // +----------------------+-------------------------------------------+------------------------------------------------------------------------------+
+ // | Tuple | intermediateStartTuple | Intermediate start tuple record for this tuple variation table — |
+ // | | | optional, determined by flags in the tupleIndex value. |
+ // +----------------------+-------------------------------------------+------------------------------------------------------------------------------+
+ // | Tuple | intermediateEndTuple | Intermediate end tuple record for this tuple variation table — |
+ // | | | optional, determined by flags in the tupleIndex value. |
+ // +----------------------+-------------------------------------------+------------------------------------------------------------------------------+
+ ushort variationDataSize = reader.ReadUInt16();
+ int bytesRead = 0;
+ ushort tupleIndex = reader.ReadUInt16();
+ bytesRead += 2;
+
+ int sharedTupleRecords = tupleIndex & TupleIndexMask;
+ bool hasPrivatePointNumbers = (tupleIndex & PrivatePointNumbersMask) == PrivatePointNumbersMask;
+ bool hasEmbeddedPeakTuple = (tupleIndex & EmbeddedPeakTupleMask) == EmbeddedPeakTupleMask;
+ bool hasIntermediateRegion = (tupleIndex & IntermediateRegionMask) == IntermediateRegionMask;
+
+ float[]? embeddedPeak = null;
+ if (hasEmbeddedPeakTuple)
+ {
+ embeddedPeak = new float[axisCount];
+ for (int i = 0; i < axisCount; i++)
+ {
+ embeddedPeak[i] = reader.ReadF2dot14();
+ bytesRead += 2;
+ }
+ }
+
+ float[]? intermediateStartRegion = null;
+ float[]? intermediateEndRegion = null;
+ if (hasIntermediateRegion)
+ {
+ intermediateStartRegion = new float[axisCount];
+ for (int i = 0; i < axisCount; i++)
+ {
+ intermediateStartRegion[i] = reader.ReadF2dot14();
+ bytesRead += 2;
+ }
+
+ intermediateEndRegion = new float[axisCount];
+ for (int i = 0; i < axisCount; i++)
+ {
+ intermediateEndRegion[i] = reader.ReadF2dot14();
+ bytesRead += 2;
+ }
+ }
+
+ return new TupleVariation(axisCount, embeddedPeak, intermediateStartRegion, intermediateEndRegion);
+ }
+}
diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/VariationAxis.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/VariationAxis.cs
new file mode 100644
index 00000000..44aa8a57
--- /dev/null
+++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/VariationAxis.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics;
+
+namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
+
+///
+///
+///
+[DebuggerDisplay("Name: {Name}, Tag: {Tag}, Min: {Min}, Max: {Max}, Default: {Default}")]
+public readonly struct VariationAxis
+{
+ ///
+ /// Gets the name of the axes.
+ ///
+ public string Name { get; init; }
+
+ ///
+ /// Gets tag identifying the design variation for the axis.
+ ///
+ public string Tag { get; init; }
+
+ ///
+ /// Gets the minimum coordinate value for the axis.
+ ///
+ public float Min { get; init; }
+
+ ///
+ /// Gets the maximum coordinate value for the axis.
+ ///
+ public float Max { get; init; }
+
+ ///
+ /// Gets the default coordinate value for the axis.
+ ///
+ public float Default { get; init; }
+}
diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/VariationAxisRecord.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/VariationAxisRecord.cs
new file mode 100644
index 00000000..1d9c6295
--- /dev/null
+++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/VariationAxisRecord.cs
@@ -0,0 +1,68 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics;
+using System.IO;
+
+namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
+
+///
+/// Defines a VariationAxisRecord.
+///
+///
+[DebuggerDisplay("Tag: {Tag}, MinValue: {MinValue}, MaxValue: {MaxValue}, DefaultValue: {DefaultValue}, AxisNameId: {AxisNameId}")]
+internal class VariationAxisRecord
+{
+ internal VariationAxisRecord(string tag, float minValue, float defaultValue, float maxValue, ushort flags, ushort axisNameId)
+ {
+ this.Tag = tag;
+ this.MinValue = minValue;
+ this.MaxValue = maxValue;
+ this.DefaultValue = defaultValue;
+ this.Flags = flags;
+ this.AxisNameId = axisNameId;
+ }
+
+ public string Tag { get; }
+
+ public float MinValue { get; }
+
+ public float DefaultValue { get; }
+
+ public float MaxValue { get; }
+
+ public ushort Flags { get; }
+
+ public ushort AxisNameId { get; }
+
+ public static VariationAxisRecord Load(BigEndianBinaryReader reader, long offset)
+ {
+ // VariationAxisRecord
+ // +-----------------+----------------------------------------+----------------------------------------------------------------+
+ // | Type | Name | Description |
+ // +=================+========================================+================================================================+
+ // | Tag | axisTag | Tag identifying the design variation for the axis. |
+ // +-----------------+----------------------------------------+----------------------------------------------------------------+
+ // | Fixed | minValue | The minimum coordinate value for the axis. |
+ // +-----------------+----------------------------------------+----------------------------------------------------------------+
+ // | Fixed | defaultValue | The default coordinate value for the axis. |
+ // +-----------------+----------------------------------------+----------------------------------------------------------------+
+ // | Fixed | maxValue | The maximum coordinate value for the axis. |
+ // +-----------------+----------------------------------------+----------------------------------------------------------------+
+ // | uint16 | flags | Axis qualifiers — see details below. |
+ // +-----------------+----------------------------------------+----------------------------------------------------------------+
+ // | uint16 | axisNameID | The name ID for entries in the 'name' table that provide |
+ // | | | a display name for this axis. |
+ // +-----------------+----------------------------------------+----------------------------------------------------------------+
+ reader.Seek(offset, SeekOrigin.Begin);
+
+ string tag = reader.ReadTag();
+ float minValue = reader.ReadFixed();
+ float defaultValue = reader.ReadFixed();
+ float maxValue = reader.ReadFixed();
+ ushort flags = reader.ReadUInt16();
+ ushort axisNameID = reader.ReadUInt16();
+
+ return new VariationAxisRecord(tag, minValue, defaultValue, maxValue, flags, axisNameID);
+ }
+}
diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/VariationRegionList.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/VariationRegionList.cs
new file mode 100644
index 00000000..4d86f53a
--- /dev/null
+++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/VariationRegionList.cs
@@ -0,0 +1,88 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System;
+using System.Diagnostics;
+using System.IO;
+
+namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
+
+///
+/// Variation data is comprised of delta adjustment values that have effect over particular regions within the font’s variation space.
+/// In a tuple variation store (described earlier in this chapter), the deltas are organized into groupings by region of applicability, with each grouping associated with a given region.
+/// In contrast, the item variation store format organizes deltas into groupings by the target items to which they apply, with each grouping having deltas for several regions.
+/// Accordingly, the item variation store uses different formats for describing the regions in which a set of deltas apply.
+///
+///
+[DebuggerDisplay("AxisCount: {AxisCount}, RegionCount: {RegionCount}")]
+internal class VariationRegionList
+{
+ public static readonly VariationRegionList EmptyVariationRegionList = new(0, 0, new[] { Array.Empty() });
+
+ private VariationRegionList(ushort axisCount, ushort regionCount, RegionAxisCoordinates[][] variationRegions)
+ {
+ this.AxisCount = axisCount;
+ this.RegionCount = regionCount;
+ this.VariationRegions = variationRegions;
+ }
+
+ public ushort AxisCount { get; }
+
+ public ushort RegionCount { get; }
+
+ public RegionAxisCoordinates[][] VariationRegions { get; }
+
+ public static VariationRegionList Load(BigEndianBinaryReader reader, long offset)
+ {
+ // VariationRegionList
+ // +-----------------+----------------------------------------+----------------------------------------------------------------+
+ // | Type | Name | Description |
+ // +=================+========================================+================================================================+
+ // | uint16 | axisCount | The number of variation axes for this font. |
+ // | | | This must be the same number as axisCount in the 'fvar' table. |
+ // +-----------------+----------------------------------------+----------------------------------------------------------------+
+ // | uint16 | regionCount | The number of variation region tables in the variation region |
+ // | | | list. Must be less than 32,768. |
+ // +-----------------+----------------------------------------+----------------------------------------------------------------+
+ // + VariationRegion | variationRegions[regionCount] | Array of variation regions. |
+ // +-----------------+----------------------------------------+----------------------------------------------------------------+
+ reader.Seek(offset, SeekOrigin.Begin);
+ ushort axisCount = reader.ReadUInt16();
+ ushort regionCount = reader.ReadUInt16();
+ var variationRegions = new RegionAxisCoordinates[regionCount][];
+ for (int i = 0; i < regionCount; i++)
+ {
+ variationRegions[i] = new RegionAxisCoordinates[axisCount];
+ for (int j = 0; j < axisCount; j++)
+ {
+ float startCoord = reader.ReadF2dot14();
+ float peakCoord = reader.ReadF2dot14();
+ float endCoord = reader.ReadF2dot14();
+
+ if (startCoord > peakCoord || peakCoord > endCoord)
+ {
+ throw new InvalidFontFileException("Region axis coordinates out of order");
+ }
+
+ if (startCoord < -0x4000 || endCoord > 0x4000)
+ {
+ throw new InvalidFontFileException("Region axis coordinate out of range");
+ }
+
+ if ((peakCoord < 0 && endCoord > 0) || (peakCoord > 0 && startCoord < 0))
+ {
+ throw new InvalidFontFileException("Invalid region axis coordinates");
+ }
+
+ variationRegions[i][j] = new RegionAxisCoordinates()
+ {
+ StartCoord = startCoord,
+ PeakCoord = peakCoord,
+ EndCoord = endCoord
+ };
+ }
+ }
+
+ return new VariationRegionList(axisCount, regionCount, variationRegions);
+ }
+}
diff --git a/src/SixLabors.Fonts/Tables/Cff/CffParser.cs b/src/SixLabors.Fonts/Tables/Cff/Cff1Parser.cs
similarity index 62%
rename from src/SixLabors.Fonts/Tables/Cff/CffParser.cs
rename to src/SixLabors.Fonts/Tables/Cff/Cff1Parser.cs
index 1a802003..eaa1923a 100644
--- a/src/SixLabors.Fonts/Tables/Cff/CffParser.cs
+++ b/src/SixLabors.Fonts/Tables/Cff/Cff1Parser.cs
@@ -1,8 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using System.Diagnostics.CodeAnalysis;
-using System.Globalization;
+using System;
+using System.Collections.Generic;
using System.Text;
namespace SixLabors.Fonts.Tables.Cff;
@@ -11,14 +11,13 @@ namespace SixLabors.Fonts.Tables.Cff;
/// Parses a Compact Font Format (CFF) font program as described in The Compact Font Format specification (Adobe Technical Note #5176).
/// A CFF font may contain multiple fonts and achieves compression by sharing details between fonts in the set.
///
-internal class CffParser
+internal class Cff1Parser : CffParserBase
{
///
/// Latin 1 Encoding: ISO 8859-1 is a single-byte encoding that can represent the first 256 Unicode characters.
///
private static readonly Encoding Iso88591 = Encoding.GetEncoding("ISO-8859-1");
- private readonly StringBuilder pooledStringBuilder = new();
private long offset;
private int charStringsOffset;
private int charsetOffset;
@@ -32,15 +31,15 @@ public CffFont Load(BigEndianBinaryReader reader, long offset)
string fontName = ReadNameIndex(reader);
- List? dataDicEntries = this.ReadTopDICTIndex(reader);
+ List dataDicEntries = this.ReadTopDictIndex(reader);
string[] stringIndex = ReadStringIndex(reader);
CffTopDictionary topDictionary = this.ResolveTopDictInfo(dataDicEntries, stringIndex);
byte[][] globalSubrRawBuffers = ReadGlobalSubrIndex(reader);
- this.ReadFDSelect(reader, topDictionary.CidFontInfo);
- FontDict[] fontDicts = this.ReadFDArray(reader, topDictionary.CidFontInfo);
+ ReadFdSelect(reader, this.offset, topDictionary.CidFontInfo);
+ FontDict[] fontDicts = this.ReadFdArray(reader, this.offset, topDictionary.CidFontInfo.FDArray);
CffPrivateDictionary? privateDictionary = this.ReadPrivateDict(reader);
CffGlyphData[] glyphs = this.ReadCharStringsIndex(reader, topDictionary, globalSubrRawBuffers, fontDicts, privateDictionary);
@@ -64,7 +63,7 @@ private static string ReadNameIndex(BigEndianBinaryReader reader)
return reader.ReadString(offset.Length, Iso88591);
}
- private List ReadTopDICTIndex(BigEndianBinaryReader reader)
+ private List ReadTopDictIndex(BigEndianBinaryReader reader)
{
// 8. Top DICT INDEX
// This contains the top - level DICTs of all the fonts in the FontSet
@@ -88,7 +87,7 @@ private List ReadTopDICTIndex(BigEndianBinaryReader reader)
// been grouped together with the Top DICT operators for
// simplicity.The keys from the FontInfo dict are indicated in the
// Default, notes column of Table 9)
- return this.ReadDICTData(reader, offsets[0].Length);
+ return this.ReadDictData(reader, offsets[0].Length);
}
private static string[] ReadStringIndex(BigEndianBinaryReader reader)
@@ -109,7 +108,7 @@ private static string[] ReadStringIndex(BigEndianBinaryReader reader)
int length = offsets[i].Length;
if (length < bufferSpan.Length)
{
- Span slice = bufferSpan.Slice(0, length);
+ Span slice = bufferSpan[..length];
int actualRead = reader.BaseStream.Read(slice);
if (actualRead != length)
{
@@ -421,119 +420,6 @@ private static void ReadCharsetsFormat2(BigEndianBinaryReader reader, string[] s
}
}
- private void ReadFDSelect(BigEndianBinaryReader reader, CidFontInfo cidFontInfo)
- {
- if (cidFontInfo.FDSelect == 0)
- {
- return;
- }
-
- reader.BaseStream.Position = this.offset + cidFontInfo.FDSelect;
- switch (reader.ReadByte())
- {
- case 0:
- cidFontInfo.FdSelectFormat = 0;
- for (int i = 0; i < cidFontInfo.CIDFountCount; i++)
- {
- cidFontInfo.FdSelectMap[i] = reader.ReadByte();
- }
-
- break;
-
- case 3:
- cidFontInfo.FdSelectFormat = 3;
- ushort nRanges = reader.ReadUInt16();
- FDRange3[] ranges = new FDRange3[nRanges + 1];
-
- cidFontInfo.FdSelectFormat = 3;
- cidFontInfo.FdRanges = ranges;
- for (int i = 0; i < nRanges; ++i)
- {
- ranges[i] = new FDRange3(reader.ReadUInt16(), reader.ReadByte());
- }
-
- ranges[nRanges] = new FDRange3(reader.ReadUInt16(), 0); // sentinel
- break;
-
- default:
- throw new NotSupportedException("Only FD Select format 0 and 3 are supported");
- }
- }
-
- private FontDict[] ReadFDArray(BigEndianBinaryReader reader, CidFontInfo cidFontInfo)
- {
- if (cidFontInfo.FDArray == 0)
- {
- return Array.Empty();
- }
-
- reader.BaseStream.Position = this.offset + cidFontInfo.FDArray;
-
- if (!TryReadIndexDataOffsets(reader, out CffIndexOffset[]? offsets))
- {
- return Array.Empty();
- }
-
- FontDict[] fontDicts = new FontDict[offsets.Length];
- for (int i = 0; i < fontDicts.Length; ++i)
- {
- // read DICT data
- List dic = this.ReadDICTData(reader, offsets[i].Length);
-
- // translate
- int offset = 0;
- int size = 0;
- int name = 0;
-
- foreach (CffDataDicEntry entry in dic)
- {
- switch (entry.Operator.Name)
- {
- default:
- throw new NotSupportedException();
- case "FontName":
- name = (int)entry.Operands[0].RealNumValue;
- break;
- case "Private": // private dic
- size = (int)entry.Operands[0].RealNumValue;
- offset = (int)entry.Operands[1].RealNumValue;
- break;
- }
- }
-
- fontDicts[i] = new FontDict(name, size, offset);
- }
-
- foreach (FontDict fdict in fontDicts)
- {
- reader.BaseStream.Position = this.offset + fdict.PrivateDicOffset;
-
- List dicData = this.ReadDICTData(reader, fdict.PrivateDicSize);
-
- if (dicData.Count > 0)
- {
- // Interpret the values of private dict
- foreach (CffDataDicEntry dicEntry in dicData)
- {
- switch (dicEntry.Operator.Name)
- {
- case "Subrs":
- int localSubrsOffset = (int)dicEntry.Operands[0].RealNumValue;
- reader.BaseStream.Position = this.offset + fdict.PrivateDicOffset + localSubrsOffset;
- fdict.LocalSubr = ReadSubrBuffer(reader);
- break;
-
- case "defaultWidthX":
- case "nominalWidthX":
- break;
- }
- }
- }
- }
-
- return fontDicts;
- }
-
private CffGlyphData[] ReadCharStringsIndex(
BigEndianBinaryReader reader,
CffTopDictionary topDictionary,
@@ -603,7 +489,8 @@ private CffGlyphData[] ReadCharStringsIndex(
globalSubrBuffers,
localSubBuffer ?? Array.Empty(),
privateDictionary?.NominalWidthX ?? 0,
- charstringsBuffer);
+ charstringsBuffer,
+ 1);
}
return glyphs;
@@ -677,7 +564,7 @@ private static void ReadFormat1Encoding(BigEndianBinaryReader reader)
}
reader.BaseStream.Position = this.offset + this.privateDICTOffset;
- List dicData = this.ReadDICTData(reader, this.privateDICTLength);
+ List dicData = this.ReadDictData(reader, this.privateDICTLength);
byte[][] localSubrRawBuffers = Array.Empty();
int defaultWidthX = 0;
int nominalWidthX = 0;
@@ -708,296 +595,4 @@ private static void ReadFormat1Encoding(BigEndianBinaryReader reader)
return new CffPrivateDictionary(localSubrRawBuffers, defaultWidthX, nominalWidthX);
}
-
- private static byte[][] ReadSubrBuffer(BigEndianBinaryReader reader)
- {
- if (!TryReadIndexDataOffsets(reader, out CffIndexOffset[]? offsets))
- {
- return Array.Empty();
- }
-
- byte[][] rawBufferList = new byte[offsets.Length][];
-
- for (int i = 0; i < rawBufferList.Length; ++i)
- {
- CffIndexOffset offset = offsets[i];
- rawBufferList[i] = reader.ReadBytes(offset.Length);
- }
-
- return rawBufferList;
- }
-
- private List ReadDICTData(BigEndianBinaryReader reader, int length)
- {
- // 4. DICT Data
-
- // Font dictionary data comprising key-value pairs is represented
- // in a compact tokenized format that is similar to that used to
- // represent Type 1 charstrings.
-
- // Dictionary keys are encoded as 1- or 2-byte operators and dictionary values are encoded as
- // variable-size numeric operands that represent either integer or
- // real values.
-
- //-----------------------------
- // A DICT is simply a sequence of
- // operand(s)/operator bytes concatenated together.
- int maxIndex = (int)(reader.BaseStream.Position + length);
- List dicData = new();
- while (reader.BaseStream.Position < maxIndex)
- {
- CffDataDicEntry dicEntry = this.ReadEntry(reader);
- dicData.Add(dicEntry);
- }
-
- return dicData;
- }
-
- private CffDataDicEntry ReadEntry(BigEndianBinaryReader reader)
- {
- List operands = new();
-
- //-----------------------------
- // An operator is preceded by the operand(s) that
- // specify its value.
- //--------------------------------
-
- //-----------------------------
- // Operators and operands may be distinguished by inspection of
- // their first byte:
- // 0–21 specify operators and
- // 28, 29, 30, and 32–254 specify operands(numbers).
- // Byte values 22–27, 31, and 255 are reserved.
-
- // An operator may be preceded by up to a maximum of 48 operands
- CFFOperator? @operator;
- while (true)
- {
- byte b0 = reader.ReadByte();
-
- if (b0 is >= 0 and <= 21)
- {
- // operators
- @operator = ReadOperator(reader, b0);
- break; // **break after found operator
- }
- else if (b0 is 28 or 29)
- {
- int num = ReadIntegerNumber(reader, b0);
- operands.Add(new CffOperand(num, OperandKind.IntNumber));
- }
- else if (b0 == 30)
- {
- double num = this.ReadRealNumber(reader);
- operands.Add(new CffOperand(num, OperandKind.RealNumber));
- }
- else if (b0 is >= 32 and <= 254)
- {
- int num = ReadIntegerNumber(reader, b0);
- operands.Add(new CffOperand(num, OperandKind.IntNumber));
- }
- else
- {
- throw new NotSupportedException("invalid DICT data b0 byte: " + b0);
- }
- }
-
- // I'm fairly confident that the operator can never be null.
- return new CffDataDicEntry(@operator!, operands.ToArray());
- }
-
- private static CFFOperator ReadOperator(BigEndianBinaryReader reader, byte b0)
- {
- // read operator key
- byte b1 = 0;
- if (b0 == 12)
- {
- // 2 bytes
- b1 = reader.ReadByte();
- }
-
- // get registered operator by its key
- return CFFOperator.GetOperatorByKey(b0, b1);
- }
-
- private double ReadRealNumber(BigEndianBinaryReader reader)
- {
- // from https://typekit.files.wordpress.com/2013/05/5176.cff.pdf
- // A real number operand is provided in addition to integer
- // operands.This operand begins with a byte value of 30 followed
- // by a variable-length sequence of bytes.Each byte is composed
- // of two 4 - bit nibbles asdefined in Table 5.
-
- // The first nibble of a
- // pair is stored in the most significant 4 bits of a byte and the
- // second nibble of a pair is stored in the least significant 4 bits of a byte
- StringBuilder sb = this.pooledStringBuilder;
- sb.Clear(); // reset
-
- bool done = false;
- bool exponentMissing = false;
- while (!done)
- {
- int b = reader.ReadByte();
-
- int nb_0 = (b >> 4) & 0xf;
- int nb_1 = b & 0xf;
-
- for (int i = 0; !done && i < 2; ++i)
- {
- int nibble = (i == 0) ? nb_0 : nb_1;
-
- switch (nibble)
- {
- case 0x0:
- case 0x1:
- case 0x2:
- case 0x3:
- case 0x4:
- case 0x5:
- case 0x6:
- case 0x7:
- case 0x8:
- case 0x9:
- sb.Append(nibble);
- exponentMissing = false;
- break;
- case 0xa:
- sb.Append('.');
- break;
- case 0xb:
- sb.Append('E');
- exponentMissing = true;
- break;
- case 0xc:
- sb.Append("E-");
- exponentMissing = true;
- break;
- case 0xd:
- break;
- case 0xe:
- sb.Append('-');
- break;
- case 0xf:
- done = true;
- break;
- default:
- throw new FontException("IllegalArgumentException");
- }
- }
- }
-
- if (exponentMissing)
- {
- // the exponent is missing, just append "0" to avoid an exception
- // not sure if 0 is the correct value, but it seems to fit
- // see PDFBOX-1522
- sb.Append('0');
- }
-
- if (sb.Length == 0)
- {
- return 0d;
- }
-
- if (!double.TryParse(
- sb.ToString(),
- NumberStyles.Number | NumberStyles.AllowExponent,
- CultureInfo.InvariantCulture,
- out double value))
- {
- throw new NotSupportedException();
- }
-
- return value;
- }
-
- private static int ReadIntegerNumber(BigEndianBinaryReader reader, byte b0)
- {
- if (b0 == 28)
- {
- return reader.ReadInt16();
- }
- else if (b0 == 29)
- {
- return reader.ReadInt32();
- }
- else if (b0 is >= 32 and <= 246)
- {
- return b0 - 139;
- }
- else if (b0 is >= 247 and <= 250)
- {
- int b1 = reader.ReadByte();
- return ((b0 - 247) * 256) + b1 + 108;
- }
- else if (b0 is >= 251 and <= 254)
- {
- int b1 = reader.ReadByte();
- return (-(b0 - 251) * 256) - b1 - 108;
- }
- else
- {
- throw new InvalidFontFileException("Invalid DICT data b0 byte: " + b0);
- }
- }
-
- private static bool TryReadIndexDataOffsets(BigEndianBinaryReader reader, [NotNullWhen(true)] out CffIndexOffset[]? value)
- {
- // INDEX Data
- // An INDEX is an array of variable-sized objects.It comprises a
- // header, an offset array, and object data.
- // The offset array specifies offsets within the object data.
- // An object is retrieved by
- // indexing the offset array and fetching the object at the
- // specified offset.
- // The object’s length can be determined by subtracting its offset
- // from the next offset in the offset array.
- // An additional offset is added at the end of the offset array so the
- // length of the last object may be determined.
- // The INDEX format is shown in Table 7
-
- // Table 7 INDEX Format
- // Type Name Description
- // Card16 count Number of objects stored in INDEX
- // OffSize offSize Offset array element size
- // Offset offset[count + 1] Offset array(from byte preceding object data)
- // Card8 data[] Object data
-
- // Offsets in the offset array are relative to the byte that precedes
- // the object data. Therefore the first element of the offset array
- // is always 1. (This ensures that every object has a corresponding
- // offset which is always nonzero and permits the efficient
- // implementation of dynamic object loading.)
-
- // An empty INDEX is represented by a count field with a 0 value
- // and no additional fields.Thus, the total size of an empty INDEX
- // is 2 bytes.
-
- // Note 2
- // An INDEX may be skipped by jumping to the offset specified by the last
- // element of the offset array
- ushort count = reader.ReadUInt16();
- if (count == 0)
- {
- value = null;
- return false;
- }
-
- int offSize = reader.ReadByte();
- int[] offsets = new int[count + 1];
- CffIndexOffset[] indexElems = new CffIndexOffset[count];
- for (int i = 0; i <= count; ++i)
- {
- offsets[i] = reader.ReadOffset(offSize);
- }
-
- for (int i = 0; i < count; ++i)
- {
- indexElems[i] = new CffIndexOffset(offsets[i], offsets[i + 1] - offsets[i]);
- }
-
- value = indexElems;
- return true;
- }
}
diff --git a/src/SixLabors.Fonts/Tables/Cff/Cff1Table.cs b/src/SixLabors.Fonts/Tables/Cff/Cff1Table.cs
index 8fd4f8f9..e43f2edb 100644
--- a/src/SixLabors.Fonts/Tables/Cff/Cff1Table.cs
+++ b/src/SixLabors.Fonts/Tables/Cff/Cff1Table.cs
@@ -1,6 +1,9 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using System;
+using SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
+
namespace SixLabors.Fonts.Tables.Cff;
internal sealed class Cff1Table : Table, ICffTable
@@ -13,6 +16,8 @@ internal sealed class Cff1Table : Table, ICffTable
public int GlyphCount => this.glyphs.Length;
+ public ItemVariationStore? ItemVariationStore => null;
+
public CffGlyphData GetGlyph(int index)
=> this.glyphs[index];
@@ -52,7 +57,7 @@ public static Cff1Table Load(BigEndianBinaryReader reader)
switch (major)
{
case 1:
- CffParser parser = new();
+ Cff1Parser parser = new();
return new(parser.Load(reader, position));
default:
diff --git a/src/SixLabors.Fonts/Tables/Cff/Cff2Font.cs b/src/SixLabors.Fonts/Tables/Cff/Cff2Font.cs
new file mode 100644
index 00000000..706c76fa
--- /dev/null
+++ b/src/SixLabors.Fonts/Tables/Cff/Cff2Font.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
+
+namespace SixLabors.Fonts.Tables.Cff;
+
+internal class Cff2Font : CffFont
+{
+ public Cff2Font(string name, CffTopDictionary metrics, CffGlyphData[] glyphs, ItemVariationStore itemVariationStore)
+ : base(name, metrics, glyphs) => this.ItemVariationStore = itemVariationStore;
+
+ public ItemVariationStore ItemVariationStore { get; set; }
+}
diff --git a/src/SixLabors.Fonts/Tables/Cff/Cff2Parser.cs b/src/SixLabors.Fonts/Tables/Cff/Cff2Parser.cs
new file mode 100644
index 00000000..e1288685
--- /dev/null
+++ b/src/SixLabors.Fonts/Tables/Cff/Cff2Parser.cs
@@ -0,0 +1,328 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics.CodeAnalysis;
+using SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
+
+namespace SixLabors.Fonts.Tables.Cff;
+
+///
+/// Parses a Compact Font Format (CFF) version 2 described in https://docs.microsoft.com/de-de/typography/opentype/spec/cff2
+///
+internal class Cff2Parser : CffParserBase
+{
+ private static readonly ItemVariationStore EmptyItemVariationStoreTable = new(VariationRegionList.EmptyVariationRegionList, Array.Empty());
+
+ private long offset;
+
+ private int fontMatrixOffset;
+ private int charStringIndexOffset;
+ private int variationStoreOffset;
+ private int? fdArrayOffset;
+ private int? fdSelectOffset;
+ private ItemVariationStore? itemVariationStore;
+
+ public Cff2Font Load(BigEndianBinaryReader reader, byte hdrSize, ushort topDictLength, string fontName, long offset)
+ {
+ this.offset = offset;
+ reader.Seek(hdrSize, SeekOrigin.Begin);
+
+ this.ReadTopDictData(reader, topDictLength);
+ reader.Seek(hdrSize + topDictLength, SeekOrigin.Begin);
+
+ CidFontInfo cidFontInfo = new()
+ {
+ FDArray = this.fdArrayOffset.GetValueOrDefault(),
+ FDSelect = this.fdSelectOffset.GetValueOrDefault(),
+ };
+
+ byte[][] globalSubrRawBuffers = ReadGlobalSubrIndex(reader);
+
+ // Length in bytes of the Item Variation Store structure that follows.
+ reader.Seek(this.variationStoreOffset, SeekOrigin.Begin);
+ ushort variationStoreLength = reader.ReadUInt16();
+ this.itemVariationStore = variationStoreLength == 0 ? EmptyItemVariationStoreTable : ItemVariationStore.Load(reader, this.variationStoreOffset + 2);
+
+ // Make sure we point to the stream to the end of the variation store data.
+ reader.Seek(offset + variationStoreLength, SeekOrigin.Begin);
+
+ if (this.fdSelectOffset.HasValue)
+ {
+ ReadFdSelect(reader, this.offset, cidFontInfo);
+ }
+
+ CffIndexOffset[] charStringOffsets = this.ReadCharStringIndex(reader);
+ byte[][] charStringBuffers = ReadCharStringBuffers(reader, charStringOffsets);
+
+ int fdArrayOffset = this.fdArrayOffset.GetValueOrDefault();
+ FontDict[] fontDicts = this.ReadFdArray(reader, this.offset, fdArrayOffset);
+ CffTopDictionary topDictionary = new()
+ {
+ CidFontInfo = cidFontInfo
+ };
+
+ CffPrivateDictionary privateDictionary = new(fontDicts[0].LocalSubr, 0, 0);
+ int glyphCount = charStringOffsets.Length;
+ CffGlyphData[] glyphs = this.ReadCharStringsIndex(topDictionary, globalSubrRawBuffers, fontDicts, privateDictionary, charStringBuffers, glyphCount);
+
+ return new(fontName, topDictionary, glyphs, this.itemVariationStore);
+ }
+
+ private void ReadTopDictData(BigEndianBinaryReader reader, ushort topDictLength)
+ {
+ long startPosition = reader.BaseStream.Position;
+ long maxPosition = startPosition + topDictLength;
+ while (reader.BaseStream.Position < maxPosition)
+ {
+ CffDataDicEntry dataDicEntry = this.ReadEntry(reader);
+ switch (dataDicEntry.Operator.Name)
+ {
+ case "FontMatrix":
+ this.fontMatrixOffset = (int)dataDicEntry.Operands[0].RealNumValue;
+ break;
+ case "CharStrings":
+ this.charStringIndexOffset = (int)dataDicEntry.Operands[0].RealNumValue;
+ break;
+ case "FDArray":
+ this.fdArrayOffset = (int)dataDicEntry.Operands[0].RealNumValue;
+ break;
+ case "FDSelect":
+ this.fdSelectOffset = (int)dataDicEntry.Operands[0].RealNumValue;
+ break;
+ case "vstore":
+ this.variationStoreOffset = (int)dataDicEntry.Operands[0].RealNumValue;
+ break;
+ default:
+ throw new InvalidFontFileException("Error parsing TopDictData.");
+ }
+ }
+ }
+
+ private static byte[][] ReadGlobalSubrIndex(BigEndianBinaryReader reader, bool cff2 = true)
+
+ // 16. Local / Global Subrs INDEXes
+ // Both Type 1 and Type 2 charstrings support the notion of
+ // subroutines or subrs.
+
+ // A subr is typically a sequence of charstring
+ // bytes representing a sub - program that occurs in more than one
+ // place in a font’s charstring data.
+
+ // This subr may be stored once
+ // but referenced many times from within one or more charstrings
+ // by the use of the call subr operator whose operand is the
+ // number of the subr to be called.
+
+ // The subrs are local to a particular font and
+ // cannot be shared between fonts.
+
+ // Type 2 charstrings also permit global subrs which function in the same
+ // way but are called by the call gsubr operator and may be shared
+ // across fonts.
+
+ // Local subrs are stored in an INDEX structure which is located via
+ // the offset operand of the Subrs operator in the Private DICT.
+ // A font without local subrs has no Subrs operator in the Private DICT.
+
+ // Global subrs are stored in an INDEX structure which follows the
+ // String INDEX. A FontSet without any global subrs is represented
+ // by an empty Global Subrs INDEX.
+ => ReadSubrBuffer(reader, cff2);
+
+ private static byte[][] ReadSubrBuffer(BigEndianBinaryReader reader, bool cff2 = true)
+ {
+ if (!TryReadIndexDataOffsets(reader, cff2, out CffIndexOffset[]? offsets))
+ {
+ return Array.Empty();
+ }
+
+ byte[][] rawBufferList = new byte[offsets.Length][];
+
+ for (int i = 0; i < rawBufferList.Length; ++i)
+ {
+ CffIndexOffset offset = offsets[i];
+ rawBufferList[i] = reader.ReadBytes(offset.Length);
+ }
+
+ return rawBufferList;
+ }
+
+ private CffIndexOffset[] ReadCharStringIndex(BigEndianBinaryReader reader)
+ {
+ reader.BaseStream.Position = this.offset + this.charStringIndexOffset;
+ if (!TryReadIndexDataOffsets(reader, true, out CffIndexOffset[]? offsets))
+ {
+ throw new InvalidFontFileException("No glyph data found.");
+ }
+
+ return offsets;
+ }
+
+ private static byte[][] ReadCharStringBuffers(BigEndianBinaryReader reader, CffIndexOffset[] offsets)
+ {
+ int glyphCount = offsets.Length;
+ byte[][] charStringBuffers = new byte[offsets.Length][];
+ for (int i = 0; i < glyphCount; ++i)
+ {
+ CffIndexOffset cffIndexOffset = offsets[i];
+ charStringBuffers[i] = reader.ReadBytes(cffIndexOffset.Length);
+ }
+
+ return charStringBuffers;
+ }
+
+ private CffGlyphData[] ReadCharStringsIndex(
+ CffTopDictionary topDictionary,
+ byte[][] globalSubrBuffers,
+ FontDict[] fontDicts,
+ CffPrivateDictionary? privateDictionary,
+ byte[][] charStringBuffers,
+ int glyphCount)
+ {
+ // 14. CharStrings INDEX
+
+ // This contains the charstrings of all the glyphs in a font stored in
+ // an INDEX structure.
+
+ // Charstring objects contained within this
+ // INDEX are accessed by GID.
+
+ // The first charstring(GID 0) must be
+ // the.notdef glyph.
+
+ // The number of glyphs available in a font may
+ // be determined from the count field in the INDEX.
+
+ //
+
+ // The format of the charstring data, and therefore the method of
+ // interpretation, is specified by the
+ // CharstringType operator in the Top DICT.
+
+ // The CharstringType operator has a default value
+ // of 2 indicating the Type 2 charstring format which was designed
+ // in conjunction with CFF.
+
+ // Type 1 charstrings are documented in
+ // the “Adobe Type 1 Font Format” published by Addison - Wesley.
+
+ // Type 2 charstrings are described in Adobe Technical Note #5177:
+ // “Type 2 Charstring Format.” Other charstring types may also be
+ // supported by this method.
+ CffGlyphData[] glyphs = new CffGlyphData[glyphCount];
+ byte[][]? localSubBuffer = privateDictionary?.LocalSubrRawBuffers;
+
+ // Is the font a CID font?
+ FDRangeProvider fdRangeProvider = new(topDictionary.CidFontInfo);
+ bool isCidFont = topDictionary.CidFontInfo.FdRanges.Length > 0;
+ for (int i = 0; i < glyphCount; ++i)
+ {
+ byte[] charstringsBuffer = charStringBuffers[i];
+
+ // Now we can parse the raw glyph instructions
+ // Select proper local private dict.
+ if (isCidFont)
+ {
+ fdRangeProvider.SetCurrentGlyphIndex((ushort)i);
+ localSubBuffer = fontDicts[fdRangeProvider.SelectedFDArray].LocalSubr;
+ }
+
+ glyphs[i] = new CffGlyphData(
+ (ushort)i,
+ globalSubrBuffers,
+ localSubBuffer ?? Array.Empty(),
+ privateDictionary?.NominalWidthX ?? 0,
+ charstringsBuffer,
+ 2,
+ this.itemVariationStore);
+ }
+
+ return glyphs;
+ }
+
+ private static bool TryReadIndexDataOffsets(BigEndianBinaryReader reader, bool cff2, [NotNullWhen(true)] out CffIndexOffset[]? value)
+ {
+ // INDEX Data
+ // An INDEX is an array of variable-sized objects.It comprises a
+ // header, an offset array, and object data.
+ // The offset array specifies offsets within the object data.
+ // An object is retrieved by
+ // indexing the offset array and fetching the object at the
+ // specified offset.
+ // The object’s length can be determined by subtracting its offset
+ // from the next offset in the offset array.
+ // An additional offset is added at the end of the offset array so the
+ // length of the last object may be determined.
+ // The INDEX format is shown in Table 7
+
+ // Table 7 INDEX Format
+ // Type Name Description
+ // Card16 count Number of objects stored in INDEX
+ // OffSize offSize Offset array element size
+ // Offset offset[count + 1] Offset array(from byte preceding object data)
+ // Card8 data[] Object data
+
+ // Offsets in the offset array are relative to the byte that precedes
+ // the object data. Therefore the first element of the offset array
+ // is always 1. (This ensures that every object has a corresponding
+ // offset which is always nonzero and permits the efficient
+ // implementation of dynamic object loading.)
+
+ // An empty INDEX is represented by a count field with a 0 value
+ // and no additional fields.Thus, the total size of an empty INDEX
+ // is 2 bytes.
+
+ // Note 2
+ // An INDEX may be skipped by jumping to the offset specified by the last
+ // element of the offset array
+ uint count = cff2 ? reader.ReadUInt32() : reader.ReadUInt16();
+
+ if (count == 0)
+ {
+ value = null;
+ return false;
+ }
+
+ int offSize = reader.ReadByte();
+ int[] offsets = new int[count + 1];
+ CffIndexOffset[] indexElems = new CffIndexOffset[count];
+ for (int i = 0; i <= count; ++i)
+ {
+ offsets[i] = reader.ReadOffset(offSize);
+ }
+
+ for (int i = 0; i < count; ++i)
+ {
+ indexElems[i] = new CffIndexOffset(offsets[i], offsets[i + 1] - offsets[i]);
+ }
+
+ value = indexElems;
+ return true;
+ }
+
+ private List ReadDICTData(BigEndianBinaryReader reader, int length)
+ {
+ // 4. DICT Data
+
+ // Font dictionary data comprising key-value pairs is represented
+ // in a compact tokenized format that is similar to that used to
+ // represent Type 1 charstrings.
+
+ // Dictionary keys are encoded as 1- or 2-byte operators and dictionary values are encoded as
+ // variable-size numeric operands that represent either integer or
+ // real values.
+
+ //-----------------------------
+ // A DICT is simply a sequence of
+ // operand(s)/operator bytes concatenated together.
+ int maxIndex = (int)(reader.BaseStream.Position + length);
+ List dicData = new();
+ while (reader.BaseStream.Position < maxIndex)
+ {
+ CffDataDicEntry dicEntry = this.ReadEntry(reader);
+ dicData.Add(dicEntry);
+ }
+
+ return dicData;
+ }
+}
diff --git a/src/SixLabors.Fonts/Tables/Cff/Cff2Table.cs b/src/SixLabors.Fonts/Tables/Cff/Cff2Table.cs
index 355d1ffc..505debab 100644
--- a/src/SixLabors.Fonts/Tables/Cff/Cff2Table.cs
+++ b/src/SixLabors.Fonts/Tables/Cff/Cff2Table.cs
@@ -1,6 +1,12 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using System;
+using System.Globalization;
+using SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
+using SixLabors.Fonts.Tables.General.Name;
+using SixLabors.Fonts.WellKnownIds;
+
namespace SixLabors.Fonts.Tables.Cff;
internal sealed class Cff2Table : Table, ICffTable
@@ -9,10 +15,16 @@ internal sealed class Cff2Table : Table, ICffTable
private readonly CffGlyphData[] glyphs;
- public Cff2Table(CffFont cff1Font) => this.glyphs = cff1Font.Glyphs;
+ public Cff2Table(CffFont cffFont, ItemVariationStore itemVariationStore)
+ {
+ this.glyphs = cffFont.Glyphs;
+ this.ItemVariationStore = itemVariationStore;
+ }
public int GlyphCount => this.glyphs.Length;
+ public ItemVariationStore ItemVariationStore { get; }
+
public CffGlyphData GetGlyph(int index)
=> this.glyphs[index];
@@ -23,11 +35,32 @@ public CffGlyphData GetGlyph(int index)
return null;
}
+ NameTable nameTable = fontReader.GetTable();
+ string fontName = nameTable.GetNameById(CultureInfo.InvariantCulture, KnownNameIds.PostscriptName);
+
using (binaryReader)
{
- return Load(binaryReader);
+ return Load(binaryReader, fontName);
}
}
- public static Cff2Table Load(BigEndianBinaryReader reader) => throw new NotSupportedException("CFF2 Fonts are not currently supported.");
+ public static Cff2Table Load(BigEndianBinaryReader reader, string fontName)
+ {
+ long position = reader.BaseStream.Position;
+ byte major = reader.ReadUInt8();
+ byte minor = reader.ReadUInt8();
+ byte hdrSize = reader.ReadUInt8();
+ ushort topDictLength = reader.ReadUInt16();
+
+ switch (major)
+ {
+ case 2:
+ Cff2Parser parser = new();
+ Cff2Font cffFont = parser.Load(reader, hdrSize, topDictLength, fontName, position);
+ return new(cffFont, cffFont.ItemVariationStore);
+
+ default:
+ throw new NotSupportedException("CFF version 2 is expected");
+ }
+ }
}
diff --git a/src/SixLabors.Fonts/Tables/Cff/CffEvaluationEngine.cs b/src/SixLabors.Fonts/Tables/Cff/CffEvaluationEngine.cs
index a3b48bed..da6a97f2 100644
--- a/src/SixLabors.Fonts/Tables/Cff/CffEvaluationEngine.cs
+++ b/src/SixLabors.Fonts/Tables/Cff/CffEvaluationEngine.cs
@@ -1,8 +1,11 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using System;
+using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
+using SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
namespace SixLabors.Fonts.Tables.Cff;
@@ -13,7 +16,7 @@ namespace SixLabors.Fonts.Tables.Cff;
///
///
/// A Type 2 charstring program is a sequence of unsigned 8-bit bytes that encode numbers and operators.
-/// The byte value specifies a operator, a number, or subsequent bytes that are to be interpreted in a specific manner
+/// The byte value specifies a operator, a number, or subsequent bytes that are to be interpreted in a specific manner.
///
internal ref struct CffEvaluationEngine
{
@@ -32,12 +35,19 @@ internal ref struct CffEvaluationEngine
private readonly int localBias;
private readonly Dictionary trans;
private bool isDisposed;
+ private readonly int version;
+ private readonly GlyphVariationProcessor? glyphVariationProcessor;
+ private int vsIndex;
public CffEvaluationEngine(
ReadOnlySpan charStrings,
ReadOnlySpan globalSubrBuffers,
ReadOnlySpan localSubrBuffers,
- int nominalWidthX)
+ int nominalWidthX,
+ int version,
+ ItemVariationStore? itemVariationStore = null,
+ FVarTable? fVar = null,
+ AVarTable? aVar = null)
{
this.transforming = default;
this.charStrings = charStrings;
@@ -55,6 +65,21 @@ public CffEvaluationEngine(
this.nStems = 0;
this.stack = new(50);
this.isDisposed = false;
+ this.version = version;
+ this.glyphVariationProcessor = null;
+
+ if (itemVariationStore != null)
+ {
+ if (fVar is null)
+ {
+ throw new InvalidFontFileException("missing fVar table required for glyph variations processing");
+ }
+
+ this.glyphVariationProcessor = new GlyphVariationProcessor(itemVariationStore, fVar, aVar);
+ }
+
+ // TODO: always 0 for now. Should be privateDict.vsindex
+ this.vsIndex = 0;
}
public Bounds GetBounds()
@@ -196,12 +221,20 @@ private void Parse(ReadOnlySpan buffer)
case Type2Operator1.Return:
- // TODO: CFF2
+ if (this.version >= 2)
+ {
+ break;
+ }
+
return;
case Type2Operator1.Endchar:
- // TODO: CFF2
+ if (this.version >= 2)
+ {
+ break;
+ }
+
if (this.stack.Length > 0)
{
this.CheckWidth();
@@ -215,14 +248,49 @@ private void Parse(ReadOnlySpan buffer)
endCharEncountered = true;
break;
- case Type2Operator1.Reserved15_:
+ case Type2Operator1.VsIndex:
+ if (this.version < 2)
+ {
+ throw new NotSupportedException("blend operator is not supported in CFF v1");
+ }
- // TODO: CFF2
+ this.vsIndex = (int)this.stack.Pop();
break;
- case Type2Operator1.Reserved16_:
+ case Type2Operator1.Blend:
+ if (this.version < 2)
+ {
+ throw new NotSupportedException("blend operator is not supported in CFF v1");
+ }
+
+ if (this.glyphVariationProcessor is null)
+ {
+ throw new NotSupportedException("blend operator in non-variation font");
+ }
+
+ float[] blendVector = this.glyphVariationProcessor.BlendVector(this.vsIndex);
+ float numBlends = this.stack.Pop();
+ float numOperands = numBlends * blendVector.Length;
+ int delta = this.stack.Length - (int)numOperands;
+ int basis = delta - (int)numBlends;
+
+ for (int i = 0; i < numBlends; i++)
+ {
+ float sum = this.stack[basis + i];
+ for (int j = 0; j < blendVector.Length; j++)
+ {
+ sum += blendVector[j] * this.stack[delta++];
+ }
+
+ this.stack[basis + i] = sum;
+ }
+
+ while (numOperands-- > 0)
+ {
+ this.stack.Pop();
+ }
- // TODO: CFF2
break;
+
case Type2Operator1.Hintmask:
case Type2Operator1.Cntrmask:
diff --git a/src/SixLabors.Fonts/Tables/Cff/CffFont.cs b/src/SixLabors.Fonts/Tables/Cff/CffFont.cs
index 9be4c530..163a1b4b 100644
--- a/src/SixLabors.Fonts/Tables/Cff/CffFont.cs
+++ b/src/SixLabors.Fonts/Tables/Cff/CffFont.cs
@@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
+
namespace SixLabors.Fonts.Tables.Cff;
internal class CffFont
diff --git a/src/SixLabors.Fonts/Tables/Cff/CffGlyphData.cs b/src/SixLabors.Fonts/Tables/Cff/CffGlyphData.cs
index a5e051c8..98302044 100644
--- a/src/SixLabors.Fonts/Tables/Cff/CffGlyphData.cs
+++ b/src/SixLabors.Fonts/Tables/Cff/CffGlyphData.cs
@@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Numerics;
+using SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
namespace SixLabors.Fonts.Tables.Cff;
@@ -11,45 +12,70 @@ internal struct CffGlyphData
private readonly byte[][] localSubrBuffers;
private readonly byte[] charStrings;
private readonly int nominalWidthX;
+ private readonly int version;
+ private readonly ItemVariationStore? itemVariationStore;
public CffGlyphData(
ushort glyphIndex,
byte[][] globalSubrBuffers,
byte[][] localSubrBuffers,
int nominalWidthX,
- byte[] charStrings)
+ byte[] charStrings,
+ int version,
+ ItemVariationStore? itemVariationStore = null)
{
this.GlyphIndex = glyphIndex;
this.globalSubrBuffers = globalSubrBuffers;
this.localSubrBuffers = localSubrBuffers;
this.nominalWidthX = nominalWidthX;
this.charStrings = charStrings;
+ this.version = version;
+ this.itemVariationStore = itemVariationStore;
this.GlyphName = null;
+
+ // Variations tables are only present for CFF2 format.
+ this.FVar = null;
+ this.AVar = null;
+ this.GVar = null;
}
- public readonly ushort GlyphIndex { get; }
+ public ushort GlyphIndex { get; }
public string? GlyphName { get; set; }
- public readonly Bounds GetBounds()
+ public FVarTable? FVar { get; set; }
+
+ public AVarTable? AVar { get; set; }
+
+ public GVarTable? GVar { get; set; }
+
+ public Bounds GetBounds()
{
using var engine = new CffEvaluationEngine(
this.charStrings,
this.globalSubrBuffers,
this.localSubrBuffers,
- this.nominalWidthX);
+ this.nominalWidthX,
+ this.version,
+ this.itemVariationStore,
+ this.FVar,
+ this.AVar);
return engine.GetBounds();
}
- public readonly void RenderTo(IGlyphRenderer renderer, Vector2 origin, Vector2 scale, Vector2 offset, Matrix3x2 transform)
+ public void RenderTo(IGlyphRenderer renderer, Vector2 origin, Vector2 scale, Vector2 offset, Matrix3x2 transform)
{
using var engine = new CffEvaluationEngine(
this.charStrings,
this.globalSubrBuffers,
this.localSubrBuffers,
- this.nominalWidthX);
+ this.nominalWidthX,
+ this.version,
+ this.itemVariationStore,
+ this.FVar,
+ this.AVar);
engine.RenderTo(renderer, origin, scale, offset, transform);
}
diff --git a/src/SixLabors.Fonts/Tables/Cff/CffOperator.cs b/src/SixLabors.Fonts/Tables/Cff/CffOperator.cs
index 0f9514d8..332660d5 100644
--- a/src/SixLabors.Fonts/Tables/Cff/CffOperator.cs
+++ b/src/SixLabors.Fonts/Tables/Cff/CffOperator.cs
@@ -1,6 +1,9 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using System;
+using System.Collections.Generic;
+
namespace SixLabors.Fonts.Tables.Cff;
internal sealed class CFFOperator
@@ -94,6 +97,10 @@ private static Dictionary CreateDictionary()
Register(dictionary, 20, "defaultWidthX", OperatorOperandKind.Number);
Register(dictionary, 21, "nominalWidthX", OperatorOperandKind.Number);
+ Register(dictionary, 22, "vsindex", OperatorOperandKind.Number);
+ Register(dictionary, 23, "blend", OperatorOperandKind.Number);
+ Register(dictionary, 24, "vstore", OperatorOperandKind.Number);
+
return dictionary;
}
diff --git a/src/SixLabors.Fonts/Tables/Cff/CffParserBase.cs b/src/SixLabors.Fonts/Tables/Cff/CffParserBase.cs
new file mode 100644
index 00000000..25a0a3ce
--- /dev/null
+++ b/src/SixLabors.Fonts/Tables/Cff/CffParserBase.cs
@@ -0,0 +1,441 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Text;
+
+namespace SixLabors.Fonts.Tables.Cff;
+
+internal abstract class CffParserBase
+{
+ private readonly StringBuilder pooledStringBuilder = new();
+
+ protected static void ReadFdSelect(BigEndianBinaryReader reader, long offset, CidFontInfo cidFontInfo)
+ {
+ if (cidFontInfo.FDSelect is 0)
+ {
+ return;
+ }
+
+ reader.BaseStream.Position = offset + cidFontInfo.FDSelect;
+ switch (reader.ReadByte())
+ {
+ case 0:
+ {
+ cidFontInfo.FdSelectFormat = 0;
+ for (int i = 0; i < cidFontInfo.CIDFountCount; i++)
+ {
+ cidFontInfo.FdSelectMap[i] = reader.ReadByte();
+ }
+
+ break;
+ }
+
+ case 3:
+ {
+ cidFontInfo.FdSelectFormat = 3;
+ ushort nRanges = reader.ReadUInt16();
+ FDRange[] ranges = new FDRange[nRanges + 1];
+
+ cidFontInfo.FdSelectFormat = 3;
+ cidFontInfo.FdRanges = ranges;
+ for (int i = 0; i < nRanges; ++i)
+ {
+ ranges[i] = new FDRange(reader.ReadUInt16(), reader.ReadByte());
+ }
+
+ ranges[nRanges] = new FDRange(reader.ReadUInt16(), 0); // sentinel
+ break;
+ }
+
+ case 4:
+ {
+ cidFontInfo.FdSelectFormat = 4;
+ uint nRanges = reader.ReadUInt32();
+ FDRange[] ranges = new FDRange[nRanges + 1];
+
+ cidFontInfo.FdSelectFormat = 3;
+ cidFontInfo.FdRanges = ranges;
+ for (int i = 0; i < nRanges; ++i)
+ {
+ ranges[i] = new FDRange(reader.ReadUInt32(), reader.ReadUInt16());
+ }
+
+ ranges[nRanges] = new FDRange(reader.ReadUInt32(), 0); // sentinel
+ break;
+ }
+
+ default:
+ throw new NotSupportedException("Only FD Select format 0, 3 and 4 are supported");
+ }
+ }
+
+ protected FontDict[] ReadFdArray(BigEndianBinaryReader reader, long offset, long fdArrayOffset)
+ {
+ if (fdArrayOffset is 0)
+ {
+ return Array.Empty();
+ }
+
+ reader.BaseStream.Position = offset + fdArrayOffset;
+
+ if (!TryReadIndexDataOffsets(reader, out CffIndexOffset[]? offsets))
+ {
+ return Array.Empty();
+ }
+
+ FontDict[] fontDicts = new FontDict[offsets.Length];
+ for (int i = 0; i < fontDicts.Length; ++i)
+ {
+ // Read DICT data.
+ List dic = this.ReadDictData(reader, offsets[i].Length);
+
+ // translate
+ int fontDictsOffset = 0;
+ int size = 0;
+ int name = 0;
+
+ foreach (CffDataDicEntry entry in dic)
+ {
+ switch (entry.Operator.Name)
+ {
+ default:
+ throw new NotSupportedException();
+ case "FontName":
+ name = (int)entry.Operands[0].RealNumValue;
+ break;
+ case "Private": // private dic
+ size = (int)entry.Operands[0].RealNumValue;
+ fontDictsOffset = (int)entry.Operands[1].RealNumValue;
+ break;
+ }
+ }
+
+ fontDicts[i] = new FontDict(name, size, fontDictsOffset);
+ }
+
+ foreach (FontDict fdict in fontDicts)
+ {
+ reader.BaseStream.Position = offset + fdict.PrivateDicOffset;
+
+ List dicData = this.ReadDictData(reader, fdict.PrivateDicSize);
+
+ if (dicData.Count > 0)
+ {
+ // Interpret the values of private dict.
+ foreach (CffDataDicEntry dicEntry in dicData)
+ {
+ switch (dicEntry.Operator.Name)
+ {
+ case "Subrs":
+ int localSubrsOffset = (int)dicEntry.Operands[0].RealNumValue;
+ reader.BaseStream.Position = offset + fdict.PrivateDicOffset + localSubrsOffset;
+ fdict.LocalSubr = ReadSubrBuffer(reader);
+ break;
+
+ case "defaultWidthX":
+ case "nominalWidthX":
+ break;
+ }
+ }
+ }
+ }
+
+ return fontDicts;
+ }
+
+ protected CffDataDicEntry ReadEntry(BigEndianBinaryReader reader)
+ {
+ List operands = new();
+
+ //-----------------------------
+ // An operator is preceded by the operand(s) that
+ // specify its value.
+ //--------------------------------
+
+ //-----------------------------
+ // Operators and operands may be distinguished by inspection of
+ // their first byte:
+ // 0–21 specify operators and
+ // 28, 29, 30, and 32–254 specify operands(numbers).
+ // Byte values 22–27, 31, and 255 are reserved.
+
+ // An operator may be preceded by up to a maximum of 48 operands
+ CFFOperator? @operator;
+ while (true)
+ {
+ byte b0 = reader.ReadUInt8();
+
+ if (b0 is >= 0 and <= 24)
+ {
+ // operators
+ @operator = ReadOperator(reader, b0);
+ break; // **break after found operator
+ }
+ else if (b0 is 28 or 29)
+ {
+ int num = ReadIntegerNumber(reader, b0);
+ operands.Add(new CffOperand(num, OperandKind.IntNumber));
+ }
+ else if (b0 == 30)
+ {
+ double num = this.ReadRealNumber(reader);
+ operands.Add(new CffOperand(num, OperandKind.RealNumber));
+ }
+ else if (b0 is >= 32 and <= 254)
+ {
+ int num = ReadIntegerNumber(reader, b0);
+ operands.Add(new CffOperand(num, OperandKind.IntNumber));
+ }
+ else
+ {
+ throw new NotSupportedException("invalid DICT data b0 byte: " + b0);
+ }
+ }
+
+ // I'm fairly confident that the operator can never be null.
+ return new CffDataDicEntry(@operator!, operands.ToArray());
+ }
+
+ protected static bool TryReadIndexDataOffsets(BigEndianBinaryReader reader, [NotNullWhen(true)] out CffIndexOffset[]? value)
+ {
+ // INDEX Data
+ // An INDEX is an array of variable-sized objects.It comprises a
+ // header, an offset array, and object data.
+ // The offset array specifies offsets within the object data.
+ // An object is retrieved by
+ // indexing the offset array and fetching the object at the
+ // specified offset.
+ // The object’s length can be determined by subtracting its offset
+ // from the next offset in the offset array.
+ // An additional offset is added at the end of the offset array so the
+ // length of the last object may be determined.
+ // The INDEX format is shown in Table 7
+
+ // Table 7 INDEX Format
+ // Type Name Description
+ // Card16 count Number of objects stored in INDEX
+ // OffSize offSize Offset array element size
+ // Offset offset[count + 1] Offset array(from byte preceding object data)
+ // Card8 data[] Object data
+
+ // Offsets in the offset array are relative to the byte that precedes
+ // the object data. Therefore the first element of the offset array
+ // is always 1. (This ensures that every object has a corresponding
+ // offset which is always nonzero and permits the efficient
+ // implementation of dynamic object loading.)
+
+ // An empty INDEX is represented by a count field with a 0 value
+ // and no additional fields.Thus, the total size of an empty INDEX
+ // is 2 bytes.
+
+ // Note 2
+ // An INDEX may be skipped by jumping to the offset specified by the last
+ // element of the offset array
+ ushort count = reader.ReadUInt16();
+ if (count == 0)
+ {
+ value = null;
+ return false;
+ }
+
+ int offSize = reader.ReadByte();
+ int[] offsets = new int[count + 1];
+ CffIndexOffset[] indexElems = new CffIndexOffset[count];
+ for (int i = 0; i <= count; ++i)
+ {
+ offsets[i] = reader.ReadOffset(offSize);
+ }
+
+ for (int i = 0; i < count; ++i)
+ {
+ indexElems[i] = new CffIndexOffset(offsets[i], offsets[i + 1] - offsets[i]);
+ }
+
+ value = indexElems;
+ return true;
+ }
+
+ protected static byte[][] ReadSubrBuffer(BigEndianBinaryReader reader)
+ {
+ if (!TryReadIndexDataOffsets(reader, out CffIndexOffset[]? offsets))
+ {
+ return Array.Empty();
+ }
+
+ byte[][] rawBufferList = new byte[offsets.Length][];
+
+ for (int i = 0; i < rawBufferList.Length; ++i)
+ {
+ CffIndexOffset offset = offsets[i];
+ rawBufferList[i] = reader.ReadBytes(offset.Length);
+ }
+
+ return rawBufferList;
+ }
+
+ protected List ReadDictData(BigEndianBinaryReader reader, int length)
+ {
+ // 4. DICT Data
+
+ // Font dictionary data comprising key-value pairs is represented
+ // in a compact tokenized format that is similar to that used to
+ // represent Type 1 charstrings.
+
+ // Dictionary keys are encoded as 1- or 2-byte operators and dictionary values are encoded as
+ // variable-size numeric operands that represent either integer or
+ // real values.
+
+ //-----------------------------
+ // A DICT is simply a sequence of
+ // operand(s)/operator bytes concatenated together.
+ int maxIndex = (int)(reader.BaseStream.Position + length);
+ List dicData = new();
+ while (reader.BaseStream.Position < maxIndex)
+ {
+ CffDataDicEntry dicEntry = this.ReadEntry(reader);
+ dicData.Add(dicEntry);
+ }
+
+ return dicData;
+ }
+
+ private static CFFOperator ReadOperator(BigEndianBinaryReader reader, byte b0)
+ {
+ // Read operator key.
+ byte b1 = 0;
+ if (b0 == 12)
+ {
+ // 2 bytes
+ b1 = reader.ReadUInt8();
+ }
+
+ // Get registered operator by its key.
+ return CFFOperator.GetOperatorByKey(b0, b1);
+ }
+
+ private double ReadRealNumber(BigEndianBinaryReader reader)
+ {
+ // from https://typekit.files.wordpress.com/2013/05/5176.cff.pdf
+ // A real number operand is provided in addition to integer
+ // operands.This operand begins with a byte value of 30 followed
+ // by a variable-length sequence of bytes.Each byte is composed
+ // of two 4 - bit nibbles asdefined in Table 5.
+
+ // The first nibble of a
+ // pair is stored in the most significant 4 bits of a byte and the
+ // second nibble of a pair is stored in the least significant 4 bits of a byte
+ StringBuilder sb = this.pooledStringBuilder;
+ sb.Clear(); // reset
+
+ bool done = false;
+ bool exponentMissing = false;
+ while (!done)
+ {
+ int b = reader.ReadByte();
+
+ int nb_0 = (b >> 4) & 0xf;
+ int nb_1 = b & 0xf;
+
+ for (int i = 0; !done && i < 2; ++i)
+ {
+ int nibble = (i == 0) ? nb_0 : nb_1;
+
+ switch (nibble)
+ {
+ case 0x0:
+ case 0x1:
+ case 0x2:
+ case 0x3:
+ case 0x4:
+ case 0x5:
+ case 0x6:
+ case 0x7:
+ case 0x8:
+ case 0x9:
+ sb.Append(nibble);
+ exponentMissing = false;
+ break;
+ case 0xa:
+ sb.Append('.');
+ break;
+ case 0xb:
+ sb.Append('E');
+ exponentMissing = true;
+ break;
+ case 0xc:
+ sb.Append("E-");
+ exponentMissing = true;
+ break;
+ case 0xd:
+ break;
+ case 0xe:
+ sb.Append('-');
+ break;
+ case 0xf:
+ done = true;
+ break;
+ default:
+ throw new FontException("Unable to read real number.");
+ }
+ }
+ }
+
+ if (exponentMissing)
+ {
+ // the exponent is missing, just append "0" to avoid an exception
+ // not sure if 0 is the correct value, but it seems to fit
+ // see PDFBOX-1522
+ sb.Append('0');
+ }
+
+ if (sb.Length == 0)
+ {
+ return 0d;
+ }
+
+ if (!double.TryParse(
+ sb.ToString(),
+ NumberStyles.Number | NumberStyles.AllowExponent,
+ CultureInfo.InvariantCulture,
+ out double value))
+ {
+ throw new NotSupportedException();
+ }
+
+ return value;
+ }
+
+ private static int ReadIntegerNumber(BigEndianBinaryReader reader, byte b0)
+ {
+ if (b0 == 28)
+ {
+ return reader.ReadInt16();
+ }
+
+ if (b0 == 29)
+ {
+ return reader.ReadInt32();
+ }
+
+ if (b0 is >= 32 and <= 246)
+ {
+ return b0 - 139;
+ }
+
+ if (b0 is >= 247 and <= 250)
+ {
+ int b1 = reader.ReadByte();
+ return ((b0 - 247) * 256) + b1 + 108;
+ }
+
+ if (b0 is >= 251 and <= 254)
+ {
+ int b1 = reader.ReadByte();
+ return (-(b0 - 251) * 256) - b1 - 108;
+ }
+
+ throw new InvalidFontFileException("Invalid DICT data b0 byte: " + b0);
+ }
+}
diff --git a/src/SixLabors.Fonts/Tables/Cff/CffPrivateDictionary.cs b/src/SixLabors.Fonts/Tables/Cff/CffPrivateDictionary.cs
index b5af0e95..95b4152a 100644
--- a/src/SixLabors.Fonts/Tables/Cff/CffPrivateDictionary.cs
+++ b/src/SixLabors.Fonts/Tables/Cff/CffPrivateDictionary.cs
@@ -5,14 +5,14 @@ namespace SixLabors.Fonts.Tables.Cff;
internal class CffPrivateDictionary
{
- public CffPrivateDictionary(byte[][] localSubrRawBuffers, int defaultWidthX, int nominalWidthX)
+ public CffPrivateDictionary(byte[][]? localSubrRawBuffers, int defaultWidthX, int nominalWidthX)
{
this.LocalSubrRawBuffers = localSubrRawBuffers;
this.DefaultWidthX = defaultWidthX;
this.NominalWidthX = nominalWidthX;
}
- public byte[][] LocalSubrRawBuffers { get; set; }
+ public byte[][]? LocalSubrRawBuffers { get; set; }
public int DefaultWidthX { get; set; }
diff --git a/src/SixLabors.Fonts/Tables/Cff/CidFontInfo.cs b/src/SixLabors.Fonts/Tables/Cff/CidFontInfo.cs
index bb257631..92ef82fc 100644
--- a/src/SixLabors.Fonts/Tables/Cff/CidFontInfo.cs
+++ b/src/SixLabors.Fonts/Tables/Cff/CidFontInfo.cs
@@ -1,6 +1,9 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using System;
+using System.Collections.Generic;
+
namespace SixLabors.Fonts.Tables.Cff;
internal class CidFontInfo
@@ -21,7 +24,7 @@ internal class CidFontInfo
public int FdSelectFormat { get; set; }
- public FDRange3[] FdRanges { get; set; } = Array.Empty();
+ public FDRange[] FdRanges { get; set; } = Array.Empty();
///
/// Gets or sets the fd select map, which maps glyph # to font #.
diff --git a/src/SixLabors.Fonts/Tables/Cff/CompactFontTables.cs b/src/SixLabors.Fonts/Tables/Cff/CompactFontTables.cs
index c6cf1af1..c8485329 100644
--- a/src/SixLabors.Fonts/Tables/Cff/CompactFontTables.cs
+++ b/src/SixLabors.Fonts/Tables/Cff/CompactFontTables.cs
@@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using SixLabors.Fonts.Tables.AdvancedTypographic;
+using SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
using SixLabors.Fonts.Tables.General;
using SixLabors.Fonts.Tables.General.Colr;
using SixLabors.Fonts.Tables.General.Kern;
@@ -66,6 +67,12 @@ public CompactFontTables(
public VerticalMetricsTable? Vmtx { get; set; }
+ public FVarTable? FVar { get; set; }
+
+ public AVarTable? AVar { get; set; }
+
+ public GVarTable? GVar { get; set; }
+
// Tables Related to CFF Outlines
// +------+----------------------------------+
// | Tag | Name |
diff --git a/src/SixLabors.Fonts/Tables/Cff/FDRange3.cs b/src/SixLabors.Fonts/Tables/Cff/FDRange.cs
similarity index 53%
rename from src/SixLabors.Fonts/Tables/Cff/FDRange3.cs
rename to src/SixLabors.Fonts/Tables/Cff/FDRange.cs
index 22d16215..b5fc1f0d 100644
--- a/src/SixLabors.Fonts/Tables/Cff/FDRange3.cs
+++ b/src/SixLabors.Fonts/Tables/Cff/FDRange.cs
@@ -6,23 +6,29 @@ namespace SixLabors.Fonts.Tables.Cff;
///
/// Represents an element in an font dictionary array.
///
-internal readonly struct FDRange3
+internal readonly struct FDRange
{
- public FDRange3(ushort first, byte fontDictionary)
+ public FDRange(ushort first, byte fontDictionary)
+ {
+ this.First = first;
+ this.FontDictionary = fontDictionary;
+ }
+
+ public FDRange(uint first, ushort fontDictionary)
{
this.First = first;
this.FontDictionary = fontDictionary;
}
///
- /// Gets the first glyph index in range
+ /// Gets the first glyph index in range.
///
- public ushort First { get; }
+ public uint First { get; }
///
- /// Gets the font dictionary index for all glyphs in range
+ /// Gets the font dictionary index for all glyphs in range.
///
- public byte FontDictionary { get; }
+ public ushort FontDictionary { get; }
public override string ToString() => $"First {this.First}, Dictionary {this.FontDictionary}.";
}
diff --git a/src/SixLabors.Fonts/Tables/Cff/FDRangeProvider.cs b/src/SixLabors.Fonts/Tables/Cff/FDRangeProvider.cs
index 20037aff..fde0f568 100644
--- a/src/SixLabors.Fonts/Tables/Cff/FDRangeProvider.cs
+++ b/src/SixLabors.Fonts/Tables/Cff/FDRangeProvider.cs
@@ -1,17 +1,20 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using System;
+using System.Collections.Generic;
+
namespace SixLabors.Fonts.Tables.Cff;
internal struct FDRangeProvider
{
// helper class
private readonly int format;
- private readonly FDRange3[] ranges;
+ private readonly FDRange[] ranges;
private readonly Dictionary fdSelectMap;
- private ushort currentGlyphIndex;
- private ushort endGlyphIndexMax;
- private FDRange3 currentRange;
+ private uint currentGlyphIndex;
+ private uint endGlyphIndexMax;
+ private FDRange currentRange;
private int currentSelectedRangeIndex;
public FDRangeProvider(CidFontInfo cidFontInfo)
@@ -37,7 +40,7 @@ public FDRangeProvider(CidFontInfo cidFontInfo)
this.SelectedFDArray = 0;
}
- public byte SelectedFDArray { get; private set; }
+ public ushort SelectedFDArray { get; private set; }
public void SetCurrentGlyphIndex(ushort index)
{
@@ -48,6 +51,7 @@ public void SetCurrentGlyphIndex(ushort index)
break;
case 3:
+ case 4:
// Find proper range for selected index.
if (index >= this.currentRange.First && index < this.endGlyphIndexMax)
{
diff --git a/src/SixLabors.Fonts/Tables/Cff/ICffTable.cs b/src/SixLabors.Fonts/Tables/Cff/ICffTable.cs
index cdce0e4d..9ff1a9b8 100644
--- a/src/SixLabors.Fonts/Tables/Cff/ICffTable.cs
+++ b/src/SixLabors.Fonts/Tables/Cff/ICffTable.cs
@@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
+
namespace SixLabors.Fonts.Tables.Cff;
///
@@ -16,6 +18,15 @@ int GlyphCount
get;
}
+ ///
+ /// Gets the item variation store.
+ ///
+ /// The item variation store. If CFF1, there is no variations and null will be returned instead.
+ ItemVariationStore? ItemVariationStore
+ {
+ get;
+ }
+
///
/// Gets the glyph data at the given index.
///
diff --git a/src/SixLabors.Fonts/Tables/Cff/Type2Operator1.cs b/src/SixLabors.Fonts/Tables/Cff/Type2Operator1.cs
index bd96c03d..3aca0b2e 100644
--- a/src/SixLabors.Fonts/Tables/Cff/Type2Operator1.cs
+++ b/src/SixLabors.Fonts/Tables/Cff/Type2Operator1.cs
@@ -21,8 +21,8 @@ internal enum Type2Operator1 : byte
Escape, // 12
Reserved13_,
Endchar, // 14
- Reserved15_,
- Reserved16_,
+ VsIndex,
+ Blend,
Reserved17_,
Hstemhm, // 18
Hintmask, // 19
diff --git a/src/SixLabors.Fonts/Tables/General/HeadTable.cs b/src/SixLabors.Fonts/Tables/General/HeadTable.cs
index 178643c6..b87237a8 100644
--- a/src/SixLabors.Fonts/Tables/General/HeadTable.cs
+++ b/src/SixLabors.Fonts/Tables/General/HeadTable.cs
@@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using System;
+
namespace SixLabors.Fonts.Tables.General;
internal class HeadTable : Table
@@ -149,7 +151,7 @@ public static HeadTable Load(BigEndianBinaryReader reader)
// Bit 5: Condensed(if set to 1)
// Bit 6: Extended(if set to 1)
// Bits 7–15: Reserved(set to 0).
- // uint16 |lowestRecPPEM | Smallest readable size in pixels.
+ // uint16 | lowestRecPPEM | Smallest readable size in pixels.
// int16 | fontDirectionHint | Deprecated(Set to 2).
// 0: Fully mixed directional glyphs;
// 1: Only strongly left to right;
diff --git a/src/SixLabors.Fonts/Tables/TableLoader.cs b/src/SixLabors.Fonts/Tables/TableLoader.cs
index fda69907..686ad340 100644
--- a/src/SixLabors.Fonts/Tables/TableLoader.cs
+++ b/src/SixLabors.Fonts/Tables/TableLoader.cs
@@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using SixLabors.Fonts.Tables.AdvancedTypographic;
+using SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
using SixLabors.Fonts.Tables.Cff;
using SixLabors.Fonts.Tables.General;
using SixLabors.Fonts.Tables.General.Colr;
@@ -46,6 +47,10 @@ public TableLoader()
this.Register(PostTable.TableName, PostTable.Load);
this.Register(Cff1Table.TableName, Cff1Table.Load);
this.Register(Cff2Table.TableName, Cff2Table.Load);
+ this.Register(AVarTable.TableName, AVarTable.Load);
+ this.Register(GVarTable.TableName, GVarTable.Load);
+ this.Register(FVarTable.TableName, FVarTable.Load);
+ this.Register(HVarTable.TableName, HVarTable.Load);
}
public static TableLoader Default { get; } = new();
diff --git a/src/SixLabors.Fonts/Tables/TrueType/Glyphs/GlyphLoader.cs b/src/SixLabors.Fonts/Tables/TrueType/Glyphs/GlyphLoader.cs
index 3175284c..001b23ad 100644
--- a/src/SixLabors.Fonts/Tables/TrueType/Glyphs/GlyphLoader.cs
+++ b/src/SixLabors.Fonts/Tables/TrueType/Glyphs/GlyphLoader.cs
@@ -16,9 +16,7 @@ public static GlyphLoader Load(BigEndianBinaryReader reader)
{
return SimpleGlyphLoader.LoadSimpleGlyph(reader, contoursCount, bounds);
}
- else
- {
- return CompositeGlyphLoader.LoadCompositeGlyph(reader, bounds);
- }
+
+ return CompositeGlyphLoader.LoadCompositeGlyph(reader, bounds);
}
}
diff --git a/src/SixLabors.Fonts/Tables/TrueType/Glyphs/GlyphTable.cs b/src/SixLabors.Fonts/Tables/TrueType/Glyphs/GlyphTable.cs
index 72a0d1d1..76572d1f 100644
--- a/src/SixLabors.Fonts/Tables/TrueType/Glyphs/GlyphTable.cs
+++ b/src/SixLabors.Fonts/Tables/TrueType/Glyphs/GlyphTable.cs
@@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using System.IO;
+using SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
using SixLabors.Fonts.Tables.Woff;
namespace SixLabors.Fonts.Tables.TrueType.Glyphs;
@@ -23,6 +25,12 @@ public static GlyphTable Load(FontReader reader)
{
uint[] locations = reader.GetTable().GlyphOffsets;
+ FVarTable? fvar = reader.TryGetTable();
+ AVarTable? avar = reader.TryGetTable();
+ GVarTable? gvar = reader.TryGetTable();
+ HVarTable? hvar = reader.TryGetTable();
+ GlyphVariationProcessor? glyphVariationProcessor = fvar is null || hvar is null ? null : new GlyphVariationProcessor(hvar!.ItemVariationStore, fvar, avar, gvar);
+
// Use an empty bounds instance as the fallback.
// We will substitute this with the advance width/height to determine bounds instead when rendering/measuring.
Bounds fallbackEmptyBounds = Bounds.Empty;
diff --git a/src/SixLabors.Fonts/Tables/TrueType/Glyphs/SimpleGlyphLoader.cs b/src/SixLabors.Fonts/Tables/TrueType/Glyphs/SimpleGlyphLoader.cs
index 209391ab..577e6b93 100644
--- a/src/SixLabors.Fonts/Tables/TrueType/Glyphs/SimpleGlyphLoader.cs
+++ b/src/SixLabors.Fonts/Tables/TrueType/Glyphs/SimpleGlyphLoader.cs
@@ -1,10 +1,15 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using System;
using System.Numerics;
namespace SixLabors.Fonts.Tables.TrueType.Glyphs;
+///
+/// Implements loading Simple Glyph Description which is part of the `glyph`table.
+///
+///
internal class SimpleGlyphLoader : GlyphLoader
{
private readonly ControlPoint[] controlPoints;
@@ -31,12 +36,46 @@ public SimpleGlyphLoader(Bounds bounds)
[Flags]
private enum Flags : byte
{
+ ///
+ /// The point is is off the curve.
+ ///
ControlPoint = 0,
+
+ ///
+ /// The point is on the curve.
+ ///
OnCurve = 1,
+
+ ///
+ /// If set, the corresponding x-coordinate is 1 byte long. If not set, 2 bytes.
+ ///
XByte = 2,
+
+ ///
+ /// If set, the corresponding y-coordinate is 1 byte long. If not set, 2 bytes.
+ ///
YByte = 4,
+
+ ///
+ /// f set, the next byte specifies the number of additional times this set of flags is to be repeated.
+ /// In this way, the number of flags listed can be smaller than the number of points in a character.
+ ///
Repeat = 8,
+
+ ///
+ /// This flag has two meanings, depending on how the x-Short Vector flag is set.
+ /// If x-Short Vector is set, this bit describes the sign of the value, with 1 equalling positive and 0 negative.
+ /// If the x-Short Vector bit is not set and this bit is set, then the current x-coordinate is the same as the previous x-coordinate.
+ /// If the x-Short Vector bit is not set and this bit is also not set, the current x-coordinate is a signed 16-bit delta vector.
+ ///
XSignOrSame = 16,
+
+ ///
+ /// This flag has two meanings, depending on how the y-Short Vector flag is set.
+ /// If y-Short Vector is set, this bit describes the sign of the value, with 1 equalling positive and 0 negative.
+ /// If the y-Short Vector bit is not set and this bit is set, then the current y-coordinate is the same as the previous y-coordinate.
+ /// If the y-Short Vector bit is not set and this bit is also not set, the current y-coordinate is a signed 16-bit delta vector.
+ ///
YSignOrSame = 32
}
@@ -50,12 +89,25 @@ public static GlyphLoader LoadSimpleGlyph(BigEndianBinaryReader reader, short co
return new SimpleGlyphLoader(bounds);
}
- // uint16 | endPtsOfContours[n] | Array of last points of each contour; n is the number of contours.
- // uint16 | instructionLength | Total number of bytes for instructions.
- // uint8 | instructions[n] | Array of instructions for each glyph; n is the number of instructions.
- // uint8 | flags[n] | Array of flags for each coordinate in outline; n is the number of flags.
- // uint8 or int16 | xCoordinates[ ] | First coordinates relative to(0, 0); others are relative to previous point.
- // uint8 or int16 | yCoordinates[] | First coordinates relative to (0, 0); others are relative to previous point.
+ // +-----------------+----------------------------------------+--------------------------------------------------------------------+
+ // | Type | Name | Description |
+ // +=================+========================================+====================================================================+
+ // | uint16 | endPtsOfContours[n] | Array of last points of each contour; n is the number of contours. |
+ // +-----------------+----------------------------------------+--------------------------------------------------------------------+
+ // | uint16 | instructionLength | Total number of bytes for instructions. |
+ // +-----------------+----------------------------------------+--------------------------------------------------------------------+
+ // | uint8 | instructions[n] | Array of instructions for each glyph; |
+ // | | | n is the number of instructions. |
+ // +-----------------+----------------------------------------+--------------------------------------------------------------------+
+ // | uint8 | flags[n] | Array of flags for each coordinate in outline; |
+ // | | | n is the number of flags. |
+ // +-----------------+----------------------------------------+--------------------------------------------------------------------+
+ // | uint8 or int16 | xCoordinates[] | First coordinates relative to(0, 0); |
+ // | | | others are relative to previous point. |
+ // +-----------------+----------------------------------------+--------------------------------------------------------------------+
+ // | uint8 or int16 | yCoordinates[] | First coordinates relative to (0, 0); |
+ // | | | others are relative to previous point. |
+ // +-----------------+----------------------------------------+--------------------------------------------------------------------+
ushort[] endPoints = reader.ReadUInt16Array(count);
ushort instructionSize = reader.ReadUInt16();
diff --git a/src/SixLabors.Fonts/Tables/TrueType/TrueTypeFontTables.cs b/src/SixLabors.Fonts/Tables/TrueType/TrueTypeFontTables.cs
index be0c2b17..fa3bca79 100644
--- a/src/SixLabors.Fonts/Tables/TrueType/TrueTypeFontTables.cs
+++ b/src/SixLabors.Fonts/Tables/TrueType/TrueTypeFontTables.cs
@@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using SixLabors.Fonts.Tables.AdvancedTypographic;
+using SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
using SixLabors.Fonts.Tables.General;
using SixLabors.Fonts.Tables.General.Colr;
using SixLabors.Fonts.Tables.General.Kern;
@@ -95,4 +96,12 @@ public TrueTypeFontTables(
public IndexLocationTable Loca { get; set; }
public PrepTable? Prep { get; set; }
+
+ public FVarTable? Fvar { get; set; }
+
+ public AVarTable? Avar { get; set; }
+
+ public GVarTable? Gvar { get; set; }
+
+ public HVarTable? Hvar { get; set; }
}
diff --git a/tests/SixLabors.Fonts.Tests/Fonts/RobotoFlex.ttf b/tests/SixLabors.Fonts.Tests/Fonts/RobotoFlex.ttf
new file mode 100644
index 00000000..f7cda478
Binary files /dev/null and b/tests/SixLabors.Fonts.Tests/Fonts/RobotoFlex.ttf differ
diff --git a/tests/SixLabors.Fonts.Tests/Tables/Variations/VariationsTests.cs b/tests/SixLabors.Fonts.Tests/Tables/Variations/VariationsTests.cs
new file mode 100644
index 00000000..500cee38
--- /dev/null
+++ b/tests/SixLabors.Fonts.Tests/Tables/Variations/VariationsTests.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
+
+namespace SixLabors.Fonts.Tests.Tables.Variations;
+
+public class VariationsTests
+{
+ private static readonly FontCollection TestFontCollection = new();
+ private static readonly Font RobotoFlexTTF = CreateFont(TestFonts.RobotoFlex);
+
+ private static Font CreateFont(string testFont)
+ {
+ FontFamily family = TestFontCollection.Add(testFont);
+ return family.CreateFont(12);
+ }
+
+ [Fact]
+ public void CanLoadVariationTables()
+ => Assert.True(RobotoFlexTTF.FontMetrics.TryGetVariationAxes(out VariationAxis[] axes));
+}
diff --git a/tests/SixLabors.Fonts.Tests/TestFonts.cs b/tests/SixLabors.Fonts.Tests/TestFonts.cs
index 18e4d016..88d7b074 100644
--- a/tests/SixLabors.Fonts.Tests/TestFonts.cs
+++ b/tests/SixLabors.Fonts.Tests/TestFonts.cs
@@ -1,8 +1,13 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using System;
using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
using System.Reflection;
+using Xunit;
namespace SixLabors.Fonts.Tests;
@@ -197,6 +202,8 @@ public static class TestFonts
public static string RobotoRegular => GetFullPath("Roboto-Regular.ttf");
+ public static string RobotoFlex => GetFullPath("RobotoFlex.ttf");
+
public static string SimpleTrueTypeCollection => GetFullPath("Sample.ttc");
public static string WhitneyBookFile => GetFullPath("whitney-book.ttf");
@@ -323,4 +330,4 @@ private static string GetFullPath(string path)
return Path.Combine(rootPath, path);
}
-}
+}
\ No newline at end of file