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