From ac767a89111c4fb42e06e5caefa1948ba56931b8 Mon Sep 17 00:00:00 2001 From: redth Date: Thu, 3 Mar 2022 21:19:56 -0500 Subject: [PATCH] First pass at POST table Roughly correct and ported from another project I had this in before, but not exactly tested after porting, so may be some rough edges --- .../Tables/General/Post/PostRecord.cs | 18 ++ .../Tables/General/PostTable.cs | 178 ++++++++++++++++++ .../BigEndianBinaryWriter.cs | 18 ++ .../SixLabors.Fonts.Tests/WriterExtensions.cs | 48 +++++ 4 files changed, 262 insertions(+) create mode 100644 src/SixLabors.Fonts/Tables/General/Post/PostRecord.cs create mode 100644 src/SixLabors.Fonts/Tables/General/PostTable.cs diff --git a/src/SixLabors.Fonts/Tables/General/Post/PostRecord.cs b/src/SixLabors.Fonts/Tables/General/Post/PostRecord.cs new file mode 100644 index 000000000..2c0f112c7 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/General/Post/PostRecord.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.Fonts.Tables.General.Post +{ + internal class PostNameRecord + { + internal PostNameRecord(ushort nameIndex, string name) + { + this.Name = name; + this.NameIndex = nameIndex; + } + + public ushort NameIndex { get; } + + public string Name { get; } + } +} diff --git a/src/SixLabors.Fonts/Tables/General/PostTable.cs b/src/SixLabors.Fonts/Tables/General/PostTable.cs new file mode 100644 index 000000000..c6bb6ced3 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/General/PostTable.cs @@ -0,0 +1,178 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.Fonts.Tables.General.Post; + +namespace SixLabors.Fonts.Tables.General +{ + [TableName(TableName)] + internal class PostTable : Table + { + internal const string TableName = "POST"; + + private static readonly string[] AppleGlyphNameMap = new[] { ".notdef", ".null", "nonmarkingreturn", "space", "exclam", "quotedbl", "numbersign", "dollar", "percent", "ampersand", "quotesingle", "parenleft", "parenright", "asterisk", "plus", "comma", "hyphen", "period", "slash", "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "colon", "semicolon", "less", "equal", "greater", "question", "at", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "bracketleft", "backslash", "bracketright", "asciicircum", "underscore", "grave", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "braceleft", "bar", "braceright", "asciitilde", "Adieresis", "Aring", "Ccedilla", "Eacute", "Ntilde", "Odieresis", "Udieresis", "aacute", "agrave", "acircumflex", "adieresis", "atilde", "aring", "ccedilla", "eacute", "egrave", "ecircumflex", "edieresis", "iacute", "igrave", "icircumflex", "idieresis", "ntilde", "oacute", "ograve", "ocircumflex", "odieresis", "otilde", "uacute", "ugrave", "ucircumflex", "udieresis", "dagger", "degree", "cent", "sterling", "section", "bullet", "paragraph", "germandbls", "registered", "copyright", "trademark", "acute", "dieresis", "notequal", "AE", "Oslash", "infinity", "plusminus", "lessequal", "greaterequal", "yen", "mu", "partialdiff", "summation", "product", "pi", "integral", "ordfeminine", "ordmasculine", "Omega", "ae", "oslash", "questiondown", "exclamdown", "logicalnot", "radical", "florin", "approxequal", "Delta", "guillemotleft", "guillemotright", "ellipsis", "nonbreakingspace", "Agrave", "Atilde", "Otilde", "OE", "oe", "endash", "emdash", "quotedblleft", "quotedblright", "quoteleft", "quoteright", "divide", "lozenge", "ydieresis", "Ydieresis", "fraction", "currency", "guilsinglleft", "guilsinglright", "fi", "fl", "daggerdbl", "periodcentered", "quotesinglbase", "quotedblbase", "perthousand", "Acircumflex", "Ecircumflex", "Aacute", "Edieresis", "Egrave", "Iacute", "Icircumflex", "Idieresis", "Igrave", "Oacute", "Ocircumflex", "apple", "Ograve", "Uacute", "Ucircumflex", "Ugrave", "dotlessi", "circumflex", "tilde", "macron", "breve", "dotaccent", "ring", "cedilla", "hungarumlaut", "ogonek", "caron", "Lslash", "lslash", "Scaron", "scaron", "Zcaron", "zcaron", "brokenbar", "Eth", "eth", "Yacute", "yacute", "Thorn", "thorn", "minus", "multiply", "onesuperior", "twosuperior", "threesuperior", "onehalf", "onequarter", "threequarters", "franc", "Gbreve", "gbreve", "Idotaccent", "Scedilla", "scedilla", "Cacute", "cacute", "Ccaron", "ccaron", "dcroat" }; + + public PostNameRecord[] PostRecords { get; } + + public PostTable( + ushort formatMajor, + ushort formatMinor, + short underlinePosition, + short underlineThickness, + float italicAngle, + uint isFixedPitch, + uint minMemType42, + uint maxMemType42, + uint minMemType1, + uint maxMemType1, + PostNameRecord[] postRecords) + { + this.FormatMajor = formatMajor; + this.FormatMinor = formatMinor; + this.UnderlinePosition = underlinePosition; + this.UnderlineThickness = underlineThickness; + this.ItalicAngle = italicAngle; + this.IsFixedPitch = isFixedPitch; + this.MinMemType42 = minMemType42; + this.MaxMemType42 = maxMemType42; + this.MinMemType1 = minMemType1; + this.MaxMemType1 = maxMemType1; + this.PostRecords = postRecords; + } + + public ushort FormatMajor { get; } + + public ushort FormatMinor { get; } + + public short UnderlinePosition { get; } + + public short UnderlineThickness { get; } + + public float ItalicAngle { get; } + + public uint IsFixedPitch { get; } + + public uint MinMemType42 { get; } + + public uint MaxMemType42 { get; } + + public uint MinMemType1 { get; } + + public uint MaxMemType1 { get; } + + public static PostTable? Load(FontReader fontReader) + { + if (!fontReader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader)) + { + return null; + } + + using (binaryReader) + { + return Load(binaryReader); + } + } + + public static PostTable Load(BigEndianBinaryReader reader) + { + // HEADER + // Type | Name | Description + // ----------------|---------------------|--------------------------------------------------------------- + // Version16Dot16 | version | 0x00010000 for version 1.0, 0x00020000 for version 2.0, 0x00025000 for version 2.5 (deprecated), 0x00030000 for version 3.0 + // Fixed | italicAngle | Italic angle in counter-clockwise degrees from the vertical. Zero for upright text, negative for text that leans to the right (forward). + // FWORD | underlinePosition | This is the suggested distance of the top of the underline from the baseline (negative values indicate below baseline). The PostScript definition of this FontInfo dictionary key (the y coordinate of the center of the stroke) is not used for historical reasons. The value of the PostScript key may be calculated by subtracting half the underlineThickness from the value of this field. + // FWORD | underlineThickness | Suggested values for the underline thickness. In general, the underline thickness should match the thickness of the underscore character (U+005F LOW LINE), and should also match the strikeout thickness, which is specified in the OS/2 table. + // uint32 | isFixedPitch | Set to 0 if the font is proportionally spaced, non-zero if the font is not proportionally spaced (i.e. monospaced). + // uint32 | minMemType42 | Minimum memory usage when an OpenType font is downloaded. + // uint32 | maxMemType42 | Maximum memory usage when an OpenType font is downloaded. + // uint32 | minMemType1 | Minimum memory usage when an OpenType font is downloaded as a Type 1 font. + // uint32 | maxMemType1 | Maximum memory usage when an OpenType font is downloaded as a Type 1 font. + + // FORMAT 2.0 + // Type | Name | Description + // --------|-----------------------------|-------------------------------------------------------------- + // uint16 | numGlyphs | Number of glyphs (this should be the same as numGlyphs in 'maxp' table). + // uint16 | glyphNameIndex[numGlyphs] | Array of indices into the string data. See below for details. + // uint8 | stringData[variable] | Storage for the string data. + ushort formatMajor = reader.ReadUInt16(); + ushort formatMinor = reader.ReadUInt16(); + float italicAngle = reader.ReadFixed(); + short underlinePosition = reader.ReadFWORD(); + short underlineThickness = reader.ReadFWORD(); + uint isFixedPitch = reader.ReadUInt32(); + uint minMemType42 = reader.ReadUInt32(); + uint maxMemType42 = reader.ReadUInt32(); + uint minMemType1 = reader.ReadUInt32(); + uint maxMemType1 = reader.ReadUInt32(); + + var records = new PostNameRecord[0]; + + if (formatMajor == 1) + { + // Supported, no extra subtables needed + } + else if (formatMajor == 2 && formatMinor == 0) + { + ushort numGlyphs = reader.ReadUInt16(); + records = new PostNameRecord[numGlyphs]; + + ushort[] glyphIndices = reader.ReadUInt16Array(numGlyphs); + + for (int i = 0; i < numGlyphs; i++) + { + ushort glyphNameIndex = glyphIndices[i]; + string name; + + // < 258 is a standard fixed apple mapping + if (glyphNameIndex <= 257) + { + name = AppleGlyphNameMap[glyphNameIndex]; + } + else + { + byte strLength = reader.ReadByte(); + name = reader.ReadString(strLength, System.Text.Encoding.ASCII); + } + + records[i] = new PostNameRecord(glyphNameIndex, name); + } + } + else + { + throw new NotSupportedException($"{TableName} table format {formatMajor}.{formatMinor} is not supported."); + } + + return new PostTable( + formatMajor, + formatMinor, + underlinePosition, + underlineThickness, + italicAngle, + isFixedPitch, + minMemType42, + maxMemType42, + minMemType1, + maxMemType1, + records); + } + + public string? GetPostScriptName(int nameIndex) + { + if (this.PostRecords is not null) + { + for (int i = 0; i < this.PostRecords.Length; i++) + { + PostNameRecord p = this.PostRecords[i]; + + if (p.NameIndex == nameIndex) + { + return p.Name; + } + } + } + + return null; + } + } +} diff --git a/tests/SixLabors.Fonts.Tests/BigEndianBinaryWriter.cs b/tests/SixLabors.Fonts.Tests/BigEndianBinaryWriter.cs index 04d7d7afe..e74fa84a3 100644 --- a/tests/SixLabors.Fonts.Tests/BigEndianBinaryWriter.cs +++ b/tests/SixLabors.Fonts.Tests/BigEndianBinaryWriter.cs @@ -380,5 +380,23 @@ public void WriteInt8(sbyte value) public void WriteFWORD(short value) => this.WriteInt16(value); public void WriteUFWORD(ushort value) => this.WriteUInt16(value); + + /// + /// Writes a Pascal string to the stream + /// + /// String value. + /// Encoding type. + public void WriteString(string value, Encoding encoding) + { + // Variable length + byte[] data = encoding.GetBytes(value); + + // Write the length of the data + this.buffer[0] = unchecked((byte)data.Length); + this.WriteInternal(this.buffer, 1); + + // Write the actual string data + this.WriteInternal(data, data.Length); + } } } diff --git a/tests/SixLabors.Fonts.Tests/WriterExtensions.cs b/tests/SixLabors.Fonts.Tests/WriterExtensions.cs index e15d2135b..15cb9e60c 100644 --- a/tests/SixLabors.Fonts.Tests/WriterExtensions.cs +++ b/tests/SixLabors.Fonts.Tests/WriterExtensions.cs @@ -9,6 +9,7 @@ using SixLabors.Fonts.Tables; using SixLabors.Fonts.Tables.General; using SixLabors.Fonts.Tables.General.CMap; +using SixLabors.Fonts.Tables.General.Post; using SixLabors.Fonts.WellKnownIds; namespace SixLabors.Fonts.Tests @@ -320,6 +321,53 @@ public static void WriteCMapSubTable(this BigEndianBinaryWriter writer, Format4S } } + public static void WritePostTable(this BigEndianBinaryWriter writer, PostTable postTable) + { + + // HEADER + // Type | Name | Description + // ----------------|---------------------|--------------------------------------------------------------- + // Version16Dot16 | version | 0x00010000 for version 1.0, 0x00020000 for version 2.0, 0x00025000 for version 2.5 (deprecated), 0x00030000 for version 3.0 + // Fixed | italicAngle | Italic angle in counter-clockwise degrees from the vertical. Zero for upright text, negative for text that leans to the right (forward). + // FWORD | underlinePosition | This is the suggested distance of the top of the underline from the baseline (negative values indicate below baseline). The PostScript definition of this FontInfo dictionary key (the y coordinate of the center of the stroke) is not used for historical reasons. The value of the PostScript key may be calculated by subtracting half the underlineThickness from the value of this field. + // FWORD | underlineThickness | Suggested values for the underline thickness. In general, the underline thickness should match the thickness of the underscore character (U+005F LOW LINE), and should also match the strikeout thickness, which is specified in the OS/2 table. + // uint32 | isFixedPitch | Set to 0 if the font is proportionally spaced, non-zero if the font is not proportionally spaced (i.e. monospaced). + // uint32 | minMemType42 | Minimum memory usage when an OpenType font is downloaded. + // uint32 | maxMemType42 | Maximum memory usage when an OpenType font is downloaded. + // uint32 | minMemType1 | Minimum memory usage when an OpenType font is downloaded as a Type 1 font. + // uint32 | maxMemType1 | Maximum memory usage when an OpenType font is downloaded as a Type 1 font. + writer.WriteUInt16(postTable.FormatMajor); + writer.WriteUInt16(postTable.FormatMinor); + + writer.WriteFWORD(postTable.UnderlinePosition); + writer.WriteFWORD(postTable.UnderlineThickness); + writer.WriteUInt32(postTable.IsFixedPitch); + writer.WriteUInt32(postTable.MinMemType42); + writer.WriteUInt32(postTable.MaxMemType42); + writer.WriteUInt32(postTable.MinMemType1); + writer.WriteUInt32(postTable.MaxMemType1); + + // FORMAT 2.0 + // Type | Name | Description + // --------|-----------------------------|-------------------------------------------------------------- + // uint16 | numGlyphs | Number of glyphs (this should be the same as numGlyphs in 'maxp' table). + // uint16 | glyphNameIndex[numGlyphs] | Array of indices into the string data. See below for details. + // uint8 | stringData[variable] | Storage for the string data. + writer.WriteUInt16((ushort)postTable.PostRecords.Length); + + // Write the array of glyph name indices + foreach (PostNameRecord postRecord in postTable.PostRecords) + { + writer.WriteUInt16(postRecord.NameIndex); + } + + // Write the actual name string data + foreach (PostNameRecord postRecord in postTable.PostRecords) + { + writer.WriteString(postRecord.Name, Encoding.ASCII); + } + } + private static int DataLength(this CMapSubTable subtable) { if (subtable is Format0SubTable table)