// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using SixLabors.Fonts.Tables.General.CMap; using SixLabors.Fonts.Unicode; using SixLabors.Fonts.WellKnownIds; using System; using System.Collections.Generic; using System.IO; using System.Linq; namespace SixLabors.Fonts.Tables.General { /// /// Represents the character to glyph index mapping table, which maps character codes to glyph indices. /// /// internal sealed class CMapTable : Table { /// /// The table name identifier. /// internal const string TableName = "cmap"; /// /// The format 14 subtables for Unicode variation sequences. /// private readonly Format14SubTable[] format14SubTables = Array.Empty(); /// /// Cached codepoints available in the font. /// private CodePoint[]? codepoints; /// /// Initializes a new instance of the class. /// /// The collection of CMap subtables. public CMapTable(IEnumerable tables) { this.Tables = tables.OrderBy(t => GetPreferredPlatformOrder(t.Platform)).ToArray(); this.format14SubTables = this.Tables.OfType().ToArray(); } /// /// Gets the subtables ordered by preferred platform. /// internal CMapSubTable[] Tables { get; } /// /// Gets the preferred platform ordering for subtable selection. /// Windows is preferred, followed by Unicode, then Macintosh. /// /// The platform identifier. /// The sort order value (lower is more preferred). private static int GetPreferredPlatformOrder(PlatformIDs platform) => platform switch { PlatformIDs.Windows => 0, PlatformIDs.Unicode => 1, PlatformIDs.Macintosh => 2, _ => int.MaxValue }; /// /// Tries to get the glyph ID for the given code point, optionally considering the next code point /// for Unicode Variation Sequence (UVS) matching. /// /// The code point to look up. /// The optional next code point for UVS matching. /// When this method returns, contains the glyph ID if found. /// When this method returns, indicates whether the next code point was consumed as part of a UVS. /// if a glyph was found; otherwise, . public bool TryGetGlyphId(CodePoint codePoint, CodePoint? nextCodePoint, out ushort glyphId, out bool skipNextCodePoint) { skipNextCodePoint = false; if (this.TryGetGlyphId(codePoint, out glyphId)) { // If there is a second codepoint, we are asked whether this is an UVS sequence // - If true, return a glyph Id. // - Otherwise, return 0. if (nextCodePoint != null && this.format14SubTables.Length > 0) { foreach (Format14SubTable? cmap14 in this.format14SubTables) { ushort pairGlyphId = cmap14.CharacterPairToGlyphId(codePoint, glyphId, nextCodePoint.Value); if (pairGlyphId > 0) { glyphId = pairGlyphId; skipNextCodePoint = true; return true; } } } return true; } return false; } /// /// Tries to get the glyph ID for the given code point by searching all subtables. /// /// The code point to look up. /// When this method returns, contains the glyph ID if found. /// if a non-zero glyph ID was found; otherwise, . private bool TryGetGlyphId(CodePoint codePoint, out ushort glyphId) { foreach (CMapSubTable t in this.Tables) { // Keep looking until we have an index that's not the fallback. // Regardless of the encoding scheme, character codes that do // not correspond to any glyph in the font should be mapped to glyph index 0. // The glyph at this location must be a special glyph representing a missing character, commonly known as .notdef. if (t.TryGetGlyphId(codePoint, out glyphId) && glyphId > 0) { return true; } } glyphId = 0; return false; } /// /// Tries to get the code point for the given glyph ID via reverse lookup. /// /// The glyph ID to look up. /// When this method returns, contains the code point if found. /// if a code point was found; otherwise, . public bool TryGetCodePoint(ushort glyphId, out CodePoint codePoint) { foreach (CMapSubTable t in this.Tables) { if (t.TryGetCodePoint(glyphId, out codePoint)) { return true; } } codePoint = default; return false; } /// /// Gets the unicode codepoints for which a glyph exists in the font. /// /// A read-only memory region containing the available codepoints. public ReadOnlyMemory GetAvailableCodePoints() { if (this.codepoints is not null) { return this.codepoints; } HashSet values = new(); foreach (int v in this.Tables.SelectMany(subtable => subtable.GetAvailableCodePoints())) { values.Add(v); } return this.codepoints = values.OrderBy(v => v).Select(v => new CodePoint(v)).ToArray(); } /// /// Loads the from the specified font reader. /// /// The font reader. /// The . public static CMapTable Load(FontReader reader) { using BigEndianBinaryReader binaryReader = reader.GetReaderAtTablePosition(TableName); return Load(binaryReader); } /// /// Loads the from the specified binary reader. /// /// The big-endian binary reader. /// The . public static CMapTable Load(BigEndianBinaryReader reader) { ushort version = reader.ReadUInt16(); ushort numTables = reader.ReadUInt16(); var encodings = new EncodingRecord[numTables]; for (int i = 0; i < numTables; i++) { encodings[i] = EncodingRecord.Read(reader); } // foreach encoding we move forward looking for the subtables var tables = new List(numTables); foreach (IGrouping encoding in encodings.GroupBy(x => x.Offset)) { long offset = encoding.Key; reader.Seek(offset, SeekOrigin.Begin); // Subtable format. switch (reader.ReadUInt16()) { case 0: tables.AddRange(Format0SubTable.Load(encoding, reader)); break; case 4: tables.AddRange(Format4SubTable.Load(encoding, reader)); break; case 12: tables.AddRange(Format12SubTable.Load(encoding, reader)); break; case 14: tables.AddRange(Format14SubTable.Load(encoding, reader, offset)); break; } } return new CMapTable(tables); } } }