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

222 lines
8.7 KiB
C#

// 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 {
/// <summary>
/// Represents the character to glyph index mapping table, which maps character codes to glyph indices.
/// <see href="https://learn.microsoft.com/en-us/typography/opentype/spec/cmap"/>
/// </summary>
internal sealed class CMapTable : Table
{
/// <summary>
/// The table name identifier.
/// </summary>
internal const string TableName = "cmap";
/// <summary>
/// The format 14 subtables for Unicode variation sequences.
/// </summary>
private readonly Format14SubTable[] format14SubTables = Array.Empty<Format14SubTable>();
/// <summary>
/// Cached codepoints available in the font.
/// </summary>
private CodePoint[]? codepoints;
/// <summary>
/// Initializes a new instance of the <see cref="CMapTable"/> class.
/// </summary>
/// <param name="tables">The collection of CMap subtables.</param>
public CMapTable(IEnumerable<CMapSubTable> tables)
{
this.Tables = tables.OrderBy(t => GetPreferredPlatformOrder(t.Platform)).ToArray();
this.format14SubTables = this.Tables.OfType<Format14SubTable>().ToArray();
}
/// <summary>
/// Gets the subtables ordered by preferred platform.
/// </summary>
internal CMapSubTable[] Tables { get; }
/// <summary>
/// Gets the preferred platform ordering for subtable selection.
/// Windows is preferred, followed by Unicode, then Macintosh.
/// </summary>
/// <param name="platform">The platform identifier.</param>
/// <returns>The sort order value (lower is more preferred).</returns>
private static int GetPreferredPlatformOrder(PlatformIDs platform)
=> platform switch
{
PlatformIDs.Windows => 0,
PlatformIDs.Unicode => 1,
PlatformIDs.Macintosh => 2,
_ => int.MaxValue
};
/// <summary>
/// Tries to get the glyph ID for the given code point, optionally considering the next code point
/// for Unicode Variation Sequence (UVS) matching.
/// </summary>
/// <param name="codePoint">The code point to look up.</param>
/// <param name="nextCodePoint">The optional next code point for UVS matching.</param>
/// <param name="glyphId">When this method returns, contains the glyph ID if found.</param>
/// <param name="skipNextCodePoint">When this method returns, indicates whether the next code point was consumed as part of a UVS.</param>
/// <returns><see langword="true"/> if a glyph was found; otherwise, <see langword="false"/>.</returns>
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;
}
/// <summary>
/// Tries to get the glyph ID for the given code point by searching all subtables.
/// </summary>
/// <param name="codePoint">The code point to look up.</param>
/// <param name="glyphId">When this method returns, contains the glyph ID if found.</param>
/// <returns><see langword="true"/> if a non-zero glyph ID was found; otherwise, <see langword="false"/>.</returns>
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;
}
/// <summary>
/// Tries to get the code point for the given glyph ID via reverse lookup.
/// </summary>
/// <param name="glyphId">The glyph ID to look up.</param>
/// <param name="codePoint">When this method returns, contains the code point if found.</param>
/// <returns><see langword="true"/> if a code point was found; otherwise, <see langword="false"/>.</returns>
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;
}
/// <summary>
/// Gets the unicode codepoints for which a glyph exists in the font.
/// </summary>
/// <returns>A read-only memory region containing the available codepoints.</returns>
public ReadOnlyMemory<CodePoint> GetAvailableCodePoints()
{
if (this.codepoints is not null)
{
return this.codepoints;
}
HashSet<int> 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();
}
/// <summary>
/// Loads the <see cref="CMapTable"/> from the specified font reader.
/// </summary>
/// <param name="reader">The font reader.</param>
/// <returns>The <see cref="CMapTable"/>.</returns>
public static CMapTable Load(FontReader reader)
{
using BigEndianBinaryReader binaryReader = reader.GetReaderAtTablePosition(TableName);
return Load(binaryReader);
}
/// <summary>
/// Loads the <see cref="CMapTable"/> from the specified binary reader.
/// </summary>
/// <param name="reader">The big-endian binary reader.</param>
/// <returns>The <see cref="CMapTable"/>.</returns>
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<CMapSubTable>(numTables);
foreach (IGrouping<uint, EncodingRecord> 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);
}
}
}