// 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);
}
}
}