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

196 lines
7.8 KiB
C#

// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using SixLabors.Fonts.Utilities;
using SixLabors.Fonts.WellKnownIds;
namespace SixLabors.Fonts.Tables.General.Name {
/// <summary>
/// Represents the OpenType 'name' table, which contains human-readable string names
/// for features, font metadata, and other descriptive information.
/// <see href="https://learn.microsoft.com/en-us/typography/opentype/spec/name"/>
/// </summary>
internal class NameTable : Table
{
/// <summary>
/// The table tag name identifying the 'name' table.
/// </summary>
internal const string TableName = "name";
/// <summary>
/// The array of name records contained in this table.
/// </summary>
private readonly NameRecord[] names;
/// <summary>
/// Initializes a new instance of the <see cref="NameTable"/> class.
/// </summary>
/// <param name="names">The name records.</param>
/// <param name="languages">The language tag strings (format 1 only).</param>
internal NameTable(NameRecord[] names, string[] languages)
=> this.names = names;
/// <summary>
/// Gets the unique font identifier for the specified culture.
/// </summary>
/// <param name="culture">The culture used to select the appropriate language-specific name.</param>
/// <returns>The unique font identifier string.</returns>
public string Id(CultureInfo culture)
=> this.GetNameById(culture, KnownNameIds.UniqueFontID);
/// <summary>
/// Gets the full font name for the specified culture.
/// </summary>
/// <param name="culture">The culture used to select the appropriate language-specific name.</param>
/// <returns>The full font name string.</returns>
public string FontName(CultureInfo culture)
=> this.GetNameById(culture, KnownNameIds.FullFontName);
/// <summary>
/// Gets the font family name for the specified culture.
/// </summary>
/// <param name="culture">The culture used to select the appropriate language-specific name.</param>
/// <returns>The font family name string.</returns>
public string FontFamilyName(CultureInfo culture)
=> this.GetNameById(culture, KnownNameIds.FontFamilyName);
/// <summary>
/// Gets the font subfamily name (e.g., "Bold", "Italic") for the specified culture.
/// </summary>
/// <param name="culture">The culture used to select the appropriate language-specific name.</param>
/// <returns>The font subfamily name string.</returns>
public string FontSubFamilyName(CultureInfo culture)
=> this.GetNameById(culture, KnownNameIds.FontSubfamilyName);
/// <summary>
/// Gets the name string for the specified culture and name identifier.
/// Falls back to US English (0x0409), then the first Windows platform record, then any record.
/// </summary>
/// <param name="culture">The culture used to select the appropriate language-specific name.</param>
/// <param name="nameId">The name identifier to look up.</param>
/// <returns>The name string, or <see cref="string.Empty"/> if not found.</returns>
public string GetNameById(CultureInfo culture, KnownNameIds nameId)
{
int languageId = culture.LCID;
NameRecord? usaVersion = null;
NameRecord? firstWindows = null;
NameRecord? first = null;
foreach (NameRecord name in this.names)
{
if (name.NameID == nameId)
{
// Get just the first one, just in case.
first ??= name;
if (name.Platform == PlatformIDs.Windows)
{
// If us not found return the first windows one.
firstWindows ??= name;
if (name.LanguageID == 0x0409)
{
// Grab the us version as its on next best match.
usaVersion ??= name;
}
if (name.LanguageID == languageId)
{
// Return the most exact first.
return name.Value;
}
}
}
}
return usaVersion?.Value ??
firstWindows?.Value ??
first?.Value ??
string.Empty;
}
/// <summary>
/// Gets the name string for the specified culture and raw name identifier.
/// </summary>
/// <param name="culture">The culture used to select the appropriate language-specific name.</param>
/// <param name="nameId">The raw name identifier value to look up.</param>
/// <returns>The name string, or <see cref="string.Empty"/> if not found.</returns>
public string GetNameById(CultureInfo culture, ushort nameId)
=> this.GetNameById(culture, (KnownNameIds)nameId);
/// <summary>
/// Loads the <see cref="NameTable"/> from the specified font reader.
/// </summary>
/// <param name="fontReader">The font reader to read the table from.</param>
/// <returns>The loaded <see cref="NameTable"/>.</returns>
/// <exception cref="InvalidFontTableException">Thrown when the table is missing from the font.</exception>
public static NameTable? Load(FontReader fontReader)
{
if (!fontReader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader))
{
throw new InvalidFontTableException($"Table '{TableName}' is missing", TableName);
}
using (binaryReader)
{
// Move to start of table.
return Load(binaryReader);
}
}
/// <summary>
/// Loads the <see cref="NameTable"/> from the specified binary reader.
/// </summary>
/// <param name="reader">The binary reader positioned at the start of the name table data.</param>
/// <returns>The loaded <see cref="NameTable"/>.</returns>
public static NameTable Load(BigEndianBinaryReader reader)
{
var strings = new List<StringLoader>();
ushort format = reader.ReadUInt16();
ushort nameCount = reader.ReadUInt16();
ushort stringOffset = reader.ReadUInt16();
var names = new NameRecord[nameCount];
for (int i = 0; i < nameCount; i++)
{
names[i] = NameRecord.Read(reader);
StringLoader? sr = names[i].StringReader;
if (sr is not null)
{
strings.Add(sr);
}
}
StringLoader[]? langs = Array.Empty<StringLoader>();
if (format == 1)
{
// Format 1 adds language data.
ushort langCount = reader.ReadUInt16();
langs = new StringLoader[langCount];
for (int i = 0; i < langCount; i++)
{
langs[i] = StringLoader.Create(reader);
strings.Add(langs[i]);
}
}
foreach (StringLoader readable in strings)
{
int readableStartOffset = stringOffset + readable.Offset;
reader.Seek(readableStartOffset, SeekOrigin.Begin);
readable.LoadValue(reader);
}
string[] langNames = langs?.Select(x => x.Value).ToArray() ?? Array.Empty<string>();
return new NameTable(names, langNames);
}
}
}