ImageSharp/SixLabors.Fonts/Tables/Cff/Cff1Parser.cs
2026-08-03 22:31:27 +02:00

693 lines
31 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Collections.Generic;
using System.Text;
namespace SixLabors.Fonts.Tables.Cff {
/// <summary>
/// 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.
/// </summary>
internal class Cff1Parser : CffParserBase
{
/// <summary>
/// Latin 1 Encoding: ISO 8859-1 is a single-byte encoding that can represent the first 256 Unicode characters.
/// </summary>
private static readonly Encoding Iso88591 = Encoding.GetEncoding("ISO-8859-1");
private long offset;
private int charStringsOffset;
private int charsetOffset;
private int encodingOffset = -1;
private int privateDICTOffset;
private int privateDICTLength;
/// <summary>
/// Loads and parses a CFF1 font from the given reader at the specified offset.
/// </summary>
/// <param name="reader">The binary reader positioned at the CFF1 data.</param>
/// <param name="offset">The absolute offset of the CFF1 table in the font stream.</param>
/// <returns>The parsed <see cref="CffFont"/>.</returns>
public CffFont Load(BigEndianBinaryReader reader, long offset)
{
this.offset = offset;
string fontName = ReadNameIndex(reader);
List<CffDataDicEntry> dataDicEntries = this.ReadTopDictIndex(reader);
string[] stringIndex = ReadStringIndex(reader);
CffTopDictionary topDictionary = this.ResolveTopDictInfo(dataDicEntries, stringIndex);
byte[][] globalSubrRawBuffers = ReadGlobalSubrIndex(reader);
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);
this.ReadCharsets(reader, stringIndex, glyphs);
this.ReadEncodings(reader);
return new(fontName, topDictionary, glyphs);
}
/// <summary>
/// Reads the Name INDEX, which contains the PostScript name of the font.
/// </summary>
/// <param name="reader">The binary reader.</param>
/// <returns>The font name string.</returns>
private static string ReadNameIndex(BigEndianBinaryReader reader)
{
if (!TryReadIndexDataOffsets(reader, out CffIndexOffset[]? offsets))
{
throw new InvalidFontFileException("No name index found.");
}
// For Open Type the Name INDEX in the CFF data must contain only one entry;
// that is, there must be only one font in the CFF FontSet.
CffIndexOffset offset = offsets[0];
return reader.ReadString(offset.Length, Iso88591);
}
/// <summary>
/// Reads the Top DICT INDEX, which contains top-level dictionary entries for the font.
/// </summary>
/// <param name="reader">The binary reader.</param>
/// <returns>A list of parsed DICT entries.</returns>
private List<CffDataDicEntry> ReadTopDictIndex(BigEndianBinaryReader reader)
{
// 8. Top DICT INDEX
// This contains the top - level DICTs of all the fonts in the FontSet
// stored in an INDEX structure.Objects contained within this
// INDEX correspond to those in the Name INDEX in both order
// and number. Each object is a DICT structure that corresponds to
// the top-level dictionary of a PostScript font.
// A font is identified by an entry in the Name INDEX and its data
// is accessed via the corresponding Top DICT
if (!TryReadIndexDataOffsets(reader, out CffIndexOffset[]? offsets))
{
throw new InvalidFontFileException("No Top DICT index found.");
}
// 9. Top DICT Data
// The names of the Top DICT operators shown in
// Table 9 are, where possible, the same as the corresponding Type 1 dict key.
// Operators that have no corresponding Type1 dict key are noted
// in the table below along with a default value, if any. (Several
// operators have been derived from FontInfo dict keys but have
// 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);
}
/// <summary>
/// Reads the String INDEX containing font-specific strings referenced by SID.
/// </summary>
/// <param name="reader">The binary reader.</param>
/// <returns>An array of strings from the String INDEX.</returns>
private static string[] ReadStringIndex(BigEndianBinaryReader reader)
{
if (!TryReadIndexDataOffsets(reader, out CffIndexOffset[]? offsets))
{
return [];
}
string[] stringIndex = new string[offsets.Length];
// Allow reusing the same buffer for shorter reads.
using Buffer<byte> buffer = new(512);
Span<byte> bufferSpan = buffer.GetSpan();
for (int i = 0; i < offsets.Length; ++i)
{
int length = offsets[i].Length;
if (length < bufferSpan.Length)
{
Span<byte> slice = bufferSpan[..length];
int actualRead = reader.BaseStream.Read(slice);
if (actualRead != length)
{
throw new InvalidFontFileException("Invalid string length.");
}
stringIndex[i] = Iso88591.GetString(slice);
}
else
{
stringIndex[i] = reader.ReadString(length, Iso88591);
}
}
return stringIndex;
}
/// <summary>
/// Resolves a string identifier (SID) to its string value using the standard strings
/// table and the font-specific String INDEX.
/// </summary>
/// <param name="index">The SID to resolve.</param>
/// <param name="stringIndex">The font-specific String INDEX.</param>
/// <returns>The resolved string name.</returns>
private static string GetSid(int index, string[] stringIndex)
{
if (index >= 0 && index <= CffStandardStrings.Count - 1)
{
// Use standard name
return CffStandardStrings.GetName(index);
}
if (index - CffStandardStrings.Count < stringIndex.Length)
{
return stringIndex[index - CffStandardStrings.Count];
}
// Technically this maps to .notdef, but PDFBox uses this
return "SID" + index;
}
/// <summary>
/// Resolves the Top DICT entries into a <see cref="CffTopDictionary"/> by interpreting operator-operand pairs.
/// </summary>
/// <param name="entries">The parsed DICT entries.</param>
/// <param name="stringIndex">The font-specific String INDEX for SID resolution.</param>
/// <returns>The populated <see cref="CffTopDictionary"/>.</returns>
private CffTopDictionary ResolveTopDictInfo(List<CffDataDicEntry> entries, string[] stringIndex)
{
// TODO: Is CID mandatory?
CffTopDictionary metrics = new();
foreach (CffDataDicEntry entry in entries)
{
switch (entry.Operator.Name)
{
default:
#if DEBUG
System.Diagnostics.Debug.WriteLine("topdic:" + entry.Operator.Name);
#endif
break;
case "XUID":
break; // nothing
case "version":
metrics.Version = GetSid((int)entry.Operands[0].RealNumValue, stringIndex);
break;
case "Notice":
metrics.Notice = GetSid((int)entry.Operands[0].RealNumValue, stringIndex);
break;
case "Copyright":
metrics.CopyRight = GetSid((int)entry.Operands[0].RealNumValue, stringIndex);
break;
case "FullName":
metrics.FullName = GetSid((int)entry.Operands[0].RealNumValue, stringIndex);
break;
case "FamilyName":
metrics.FamilyName = GetSid((int)entry.Operands[0].RealNumValue, stringIndex);
break;
case "Weight":
metrics.Weight = GetSid((int)entry.Operands[0].RealNumValue, stringIndex);
break;
case "UnderlinePosition":
metrics.UnderlinePosition = entry.Operands[0].RealNumValue;
break;
case "UnderlineThickness":
metrics.UnderlineThickness = entry.Operands[0].RealNumValue;
break;
case "FontBBox":
metrics.FontBBox =
[
entry.Operands[0].RealNumValue,
entry.Operands[1].RealNumValue,
entry.Operands[2].RealNumValue,
entry.Operands[3].RealNumValue
];
break;
case "CharStrings":
this.charStringsOffset = (int)entry.Operands[0].RealNumValue;
break;
case "charset":
this.charsetOffset = (int)entry.Operands[0].RealNumValue;
break;
case "Encoding":
this.encodingOffset = (int)entry.Operands[0].RealNumValue;
break;
case "Private":
// private DICT size and offset
this.privateDICTLength = (int)entry.Operands[0].RealNumValue;
this.privateDICTOffset = (int)entry.Operands[1].RealNumValue;
break;
case "ROS":
// http://wwwimages.adobe.com/www.adobe.com/content/dam/acom/en/devnet/font/pdfs/5176.CFF.pdf
// A CFF CIDFont has the CIDFontName in the Name INDEX and a corresponding Top DICT.
// The Top DICT begins with ROS operator which specifies the Registry-Ordering - Supplement for the font.
// This will indicate to a CFF parser that special CID processing should be applied to this font. Specifically:
// ROS operator combines the Registry, Ordering, and Supplement keys together.
// see Adobe Cmap resource , https://github.com/adobe-type-tools/cmap-resources
metrics.CidFontInfo.ROS_Register = GetSid((int)entry.Operands[0].RealNumValue, stringIndex);
metrics.CidFontInfo.ROS_Ordering = GetSid((int)entry.Operands[1].RealNumValue, stringIndex);
metrics.CidFontInfo.ROS_Supplement = GetSid((int)entry.Operands[2].RealNumValue, stringIndex);
break;
case "CIDFontVersion":
metrics.CidFontInfo.CIDFontVersion = entry.Operands[0].RealNumValue;
break;
case "CIDCount":
metrics.CidFontInfo.CIDFountCount = (int)entry.Operands[0].RealNumValue;
break;
case "FDSelect":
metrics.CidFontInfo.FDSelect = (int)entry.Operands[0].RealNumValue;
break;
case "FDArray":
metrics.CidFontInfo.FDArray = (int)entry.Operands[0].RealNumValue;
break;
}
}
return metrics;
}
/// <summary>
/// Reads the Global Subrs INDEX, which contains shared subroutine charstring programs.
/// </summary>
/// <param name="reader">The binary reader.</param>
/// <returns>An array of byte arrays, each containing a global subroutine charstring.</returns>
private static byte[][] ReadGlobalSubrIndex(BigEndianBinaryReader reader)
// 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 fonts 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);
/// <summary>
/// Reads the Local Subrs INDEX, which contains font-private subroutine charstring programs.
/// </summary>
/// <param name="reader">The binary reader.</param>
/// <returns>An array of byte arrays, each containing a local subroutine charstring.</returns>
private static byte[][] ReadLocalSubrs(BigEndianBinaryReader reader) => ReadSubrBuffer(reader);
/// <summary>
/// Reads the encoding data for the font if an encoding offset is specified.
/// </summary>
/// <param name="reader">The binary reader.</param>
// TODO: We don't actually need this right now. Will be important though if we ever introduce subsetting.
private void ReadEncodings(BigEndianBinaryReader reader)
{
// Encoding data is located via the offset operand to the
// Encoding operator in the Top DICT.
// Only one Encoding operator can be
// specified per font except for CIDFonts which specify no
// encoding.
// A glyphs encoding is specified by a 1 - byte code that
// permits values in the range 0 - 255.
// Each encoding is described by a format-type identifier byte
// followed by format-specific data.Two formats are currently
// defined as specified in Tables 11(Format 0) and 12(Format 1).
if (this.encodingOffset != -1)
{
byte encoding = reader.ReadByte();
switch (encoding)
{
case 0:
ReadFormat0Encoding(reader);
break;
case 1:
ReadFormat1Encoding(reader);
break;
default:
// TODO: Seek.
break;
}
}
}
/// <summary>
/// Reads the charset data, which maps glyph indices to glyph names.
/// </summary>
/// <param name="reader">The binary reader.</param>
/// <param name="stringIndex">The font-specific String INDEX for SID resolution.</param>
/// <param name="glyphs">The glyph data array to populate with glyph names.</param>
private void ReadCharsets(BigEndianBinaryReader reader, string[] stringIndex, CffGlyphData[] glyphs)
{
// Charset data is located via the offset operand to the
// charset operator in the Top DICT.
// Each charset is described by a format-
// type identifier byte followed by format-specific data.
// Three formats are currently defined as shown in Tables
// 17, 18, and 20.
reader.BaseStream.Position = this.offset + this.charsetOffset;
switch (reader.ReadByte())
{
default:
throw new NotSupportedException();
case 0:
ReadCharsetsFormat0(reader, stringIndex, glyphs);
break;
case 1:
ReadCharsetsFormat1(reader, stringIndex, glyphs);
break;
case 2:
ReadCharsetsFormat2(reader, stringIndex, glyphs);
break;
}
}
/// <summary>
/// Reads charset data in format 0, where each glyph has an individual SID entry.
/// </summary>
/// <param name="reader">The binary reader.</param>
/// <param name="stringIndex">The font-specific String INDEX for SID resolution.</param>
/// <param name="glyphs">The glyph data array to populate with glyph names.</param>
private static void ReadCharsetsFormat0(BigEndianBinaryReader reader, string[] stringIndex, CffGlyphData[] glyphs)
{
// Table 17: Format 0
// Type Name Description
// Card8 format =0
// SID glyph[nGlyphs-1] Glyph name array
// Each element of the glyph array represents the name of the
// corresponding glyph. This format should be used when the SIDs
// are in a fairly random order. The number of glyphs (nGlyphs) is
// the value of the count field in the
// CharStrings INDEX. (There is
// one less element in the glyph name array than nGlyphs because
// the .notdef glyph name is omitted.)
for (int i = 1; i < glyphs.Length; ++i)
{
ref CffGlyphData data = ref glyphs[i];
data.GlyphName = GetSid(reader.ReadUInt16(), stringIndex);
}
}
/// <summary>
/// Reads charset data in format 1, using Range1 structures with 1-byte nLeft counts.
/// </summary>
/// <param name="reader">The binary reader.</param>
/// <param name="stringIndex">The font-specific String INDEX for SID resolution.</param>
/// <param name="glyphs">The glyph data array to populate with glyph names.</param>
private static void ReadCharsetsFormat1(BigEndianBinaryReader reader, string[] stringIndex, CffGlyphData[] glyphs)
{
// Table 18 Format 1
// Type Name Description
// Card8 format =1
// struct Range1[<varies>] Range1 array (see Table 19)
// Table 19 Range1 Format (Charset)
// Type Name Description
// SID first First glyph in range
// Card8 nLeft Glyphs left in range(excluding first)
// Each Range1 describes a group of sequential SIDs. The number
// of ranges is not explicitly specified in the font. Instead, software
// utilizing this data simply processes ranges until all glyphs in the
// font are covered. This format is particularly suited to charsets
// that are well ordered
for (int i = 1; i < glyphs.Length;)
{
int sid = reader.ReadUInt16(); // First glyph in range
int count = reader.ReadByte() + 1; // since it does not include first element.
do
{
ref CffGlyphData data = ref glyphs[i];
data.GlyphName = GetSid(sid, stringIndex);
count--;
i++;
sid++;
}
while (count > 0);
}
}
/// <summary>
/// Reads charset data in format 2, using Range2 structures with 2-byte nLeft counts for large charsets.
/// </summary>
/// <param name="reader">The binary reader.</param>
/// <param name="stringIndex">The font-specific String INDEX for SID resolution.</param>
/// <param name="glyphs">The glyph data array to populate with glyph names.</param>
private static void ReadCharsetsFormat2(BigEndianBinaryReader reader, string[] stringIndex, CffGlyphData[] glyphs)
{
// note:eg, Adobe's source-code-pro font
// Table 20 Format 2
// Type Name Description
// Card8 format 2
// struct Range2[<varies>] Range2 array (see Table 21)
//
//-----------------------------------------------
// Table 21 Range2 Format
// Type Name Description
// SID first First glyph in range
// Card16 nLeft Glyphs left in range (excluding first)
//-----------------------------------------------
// Format 2 differs from format 1 only in the size of the nLeft field in each range.
// This format is most suitable for fonts with a large well - ordered charset — for example, for Asian CIDFonts.
for (int i = 1; i < glyphs.Length;)
{
int sid = reader.ReadUInt16(); // First glyph in range
int count = reader.ReadUInt16() + 1; // since it does not include first element.
do
{
ref CffGlyphData data = ref glyphs[i];
data.GlyphName = GetSid(sid, stringIndex);
count--;
i++;
sid++;
}
while (count > 0);
}
}
/// <summary>
/// Reads the CharStrings INDEX and creates glyph data objects for all glyphs in the font.
/// </summary>
/// <param name="reader">The binary reader.</param>
/// <param name="topDictionary">The top-level dictionary containing font metadata.</param>
/// <param name="globalSubrBuffers">The global subroutine buffers.</param>
/// <param name="fontDicts">The Font DICT array for CID fonts.</param>
/// <param name="privateDictionary">The private dictionary containing local subroutine references.</param>
/// <returns>An array of <see cref="CffGlyphData"/> for each glyph.</returns>
private CffGlyphData[] ReadCharStringsIndex(
BigEndianBinaryReader reader,
CffTopDictionary topDictionary,
byte[][] globalSubrBuffers,
FontDict[] fontDicts,
CffPrivateDictionary? privateDictionary)
{
// 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.
reader.BaseStream.Position = this.offset + this.charStringsOffset;
if (!TryReadIndexDataOffsets(reader, out CffIndexOffset[]? offsets))
{
throw new InvalidFontFileException("No glyph data found.");
}
int glyphCount = offsets.Length;
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)
{
CffIndexOffset offset = offsets[i];
byte[] charstringsBuffer = reader.ReadBytes(offset.Length);
// Now we can parse the raw glyph instructions
if (isCidFont)
{
// Select proper local private dict
fdRangeProvider.SetCurrentGlyphIndex((ushort)i);
localSubBuffer = fontDicts[fdRangeProvider.SelectedFDArray].LocalSubr;
}
glyphs[i] = new CffGlyphData(
(ushort)i,
globalSubrBuffers,
localSubBuffer ?? [],
privateDictionary?.NominalWidthX ?? 0,
charstringsBuffer,
1);
}
return glyphs;
}
/// <summary>
/// Reads format 0 encoding data where each glyph has an individual code assignment.
/// </summary>
/// <param name="reader">The binary reader.</param>
private static void ReadFormat0Encoding(BigEndianBinaryReader reader)
{
// Table 11: Format 0
// Type Name Description
// Card8 format = 0
// Card8 nCodes Number of encoded glyphs
// Card8 code[nCodes] Code array
//-------
// Each element of the code array represents the encoding for the
// corresponding glyph. This format should be used when the
// codes are in a fairly random order
// we have read format field( 1st field) ..
// so start with 2nd field
int nCodes = reader.ReadByte();
byte[] codes = reader.ReadBytes(nCodes);
// TODO: Implement based on PDFPig
}
/// <summary>
/// Reads format 1 encoding data using Range1 structures for sequential code groups.
/// </summary>
/// <param name="reader">The binary reader.</param>
private static void ReadFormat1Encoding(BigEndianBinaryReader reader)
{
// Table 12 Format 1
// Type Name Description
// Card8 format = 1
// Card8 nRanges Number of code ranges
// struct Range1[nRanges] Range1 array(see Table 13)
//--------------
int nRanges = reader.ReadByte();
// Table 13 Range1 Format(Encoding)
// Type Name Description
// Card8 first First code in range
// Card8 nLeft Codes left in range(excluding first)
//--------------
// Each Range1 describes a group of sequential codes. For
// example, the codes 51 52 53 54 55 could be represented by the
// Range1: 51 4, and a perfectly ordered encoding of 256 codes can
// be described with the Range1: 0 255.
// This format is particularly suited to encodings that are well ordered.
// A few fonts have multiply - encoded glyphs which are not
// supported directly by any of the above formats. This situation is
// indicated by setting the high - order bit in the format byte and
// supplementing the encoding, regardless of format type, as
// shown in Table 14.
// Table 14 Supplemental Encoding Data
// Type Name Description
// Card8 nSups Number of supplementary mappings
// struct Supplement[nSups] Supplementary encoding array(see Table 15 below)
// Table 15 Supplement Format
// Type Name Description
// Card8 code Encoding
// SID glyph Name
}
/// <summary>
/// Reads the Private DICT data containing font-level hinting values and local subroutine references.
/// </summary>
/// <param name="reader">The binary reader.</param>
/// <returns>The parsed <see cref="CffPrivateDictionary"/>, or <see langword="null"/> if no Private DICT is present.</returns>
private CffPrivateDictionary? ReadPrivateDict(BigEndianBinaryReader reader)
{
// per-font
if (this.privateDICTLength == 0)
{
return null;
}
reader.BaseStream.Position = this.offset + this.privateDICTOffset;
List<CffDataDicEntry> dicData = this.ReadDictData(reader, this.privateDICTLength);
byte[][] localSubrRawBuffers = [];
int defaultWidthX = 0;
int nominalWidthX = 0;
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 + this.privateDICTOffset + localSubrsOffset;
localSubrRawBuffers = ReadLocalSubrs(reader);
break;
case "defaultWidthX":
defaultWidthX = (int)dicEntry.Operands[0].RealNumValue;
break;
case "nominalWidthX":
nominalWidthX = (int)dicEntry.Operands[0].RealNumValue;
break;
}
}
}
return new CffPrivateDictionary(localSubrRawBuffers, defaultWidthX, nominalWidthX);
}
}
}