ImageSharp/SixLabors.Fonts/Unicode/UnicodeTrie.cs
2026-08-03 22:31:27 +02:00

175 lines
6.6 KiB
C#

// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Buffers.Binary;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using static SixLabors.Fonts.Unicode.UnicodeTrieBuilder;
namespace SixLabors.Fonts.Unicode {
/// <summary>
/// A read-only Trie, holding 32 bit data values.
/// A UnicodeTrie is a highly optimized data structure for mapping from Unicode
/// code points(values ranging from 0 to 0x10ffff) to a 32 bit value.
/// </summary>
internal sealed class UnicodeTrie
{
private readonly uint[] data;
private readonly int highStart;
private readonly uint errorValue;
public UnicodeTrie(ReadOnlySpan<byte> rawData)
{
UnicodeTrieHeader header = MemoryMarshal.Read<UnicodeTrieHeader>(rawData);
if (!BitConverter.IsLittleEndian)
{
header.HighStart = BinaryPrimitives.ReverseEndianness(header.HighStart);
header.ErrorValue = BinaryPrimitives.ReverseEndianness(header.ErrorValue);
header.DataLength = BinaryPrimitives.ReverseEndianness(header.DataLength);
}
int length = header.DataLength;
uint[] data = new uint[length / sizeof(uint)];
rawData[^length..].CopyTo(MemoryMarshal.AsBytes(data.AsSpan()));
if (!BitConverter.IsLittleEndian)
{
for (int i = 0; i < data.Length; i++)
{
data[i] = BinaryPrimitives.ReverseEndianness(data[i]);
}
}
this.highStart = header.HighStart;
this.errorValue = header.ErrorValue;
this.data = data;
}
/// <summary>
/// Initializes a new instance of the <see cref="UnicodeTrie"/> class.
/// </summary>
/// <param name="stream">The stream containing the compressed data.</param>
public UnicodeTrie(Stream stream)
{
// Read the header info
using (BinaryReader br = new(stream, Encoding.UTF8, true))
{
this.highStart = br.ReadInt32();
this.errorValue = br.ReadUInt32();
this.data = new uint[br.ReadInt32() / sizeof(uint)];
}
// Read the data in compressed format.
using (BinaryReader br = new(stream, Encoding.UTF8, true))
{
for (int i = 0; i < this.data.Length; i++)
{
this.data[i] = br.ReadUInt32();
}
}
}
/// <summary>
/// Initializes a new instance of the <see cref="UnicodeTrie"/> class.
/// </summary>
/// <param name="data">The uncompressed trie data.</param>
/// <param name="highStart">The start of the last range which ends at U+10ffff.</param>
/// <param name="errorValue">The value for out-of-range code points and illegal UTF-8.</param>
public UnicodeTrie(uint[] data, int highStart, uint errorValue)
{
this.data = data;
this.highStart = highStart;
this.errorValue = errorValue;
}
/// <summary>
/// Get the value for a code point as stored in the trie.
/// </summary>
/// <param name="codePoint">The code point.</param>
/// <returns>The <see cref="uint"/> value.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public uint Get(uint codePoint)
{
uint index;
ref uint dataBase = ref MemoryMarshal.GetReference(this.data.AsSpan());
if (codePoint is < 0x0d800 or (> 0x0dbff and <= 0x0ffff))
{
// Ordinary BMP code point, excluding leading surrogates.
// BMP uses a single level lookup. BMP index starts at offset 0 in the Trie2 index.
// 16 bit data is stored in the index array itself.
index = this.data[codePoint >> UTRIE2_SHIFT_2];
index = (index << UTRIE2_INDEX_SHIFT) + (codePoint & UTRIE2_DATA_MASK);
return Unsafe.Add(ref dataBase, (nint)index);
}
if (codePoint <= 0xffff)
{
// Lead Surrogate Code Point. A Separate index section is stored for
// lead surrogate code units and code points.
// The main index has the code unit data.
// For this function, we need the code point data.
// Note: this expression could be refactored for slightly improved efficiency, but
// surrogate code points will be so rare in practice that it's not worth it.
index = this.data[UTRIE2_LSCP_INDEX_2_OFFSET + ((codePoint - 0xd800) >> UTRIE2_SHIFT_2)];
index = (index << UTRIE2_INDEX_SHIFT) + (codePoint & UTRIE2_DATA_MASK);
return Unsafe.Add(ref dataBase, (nint)index);
}
if (codePoint < this.highStart)
{
// Supplemental code point, use two-level lookup.
index = UTRIE2_INDEX_1_OFFSET - UTRIE2_OMITTED_BMP_INDEX_1_LENGTH + (codePoint >> UTRIE2_SHIFT_1);
index = this.data[index];
index += (codePoint >> UTRIE2_SHIFT_2) & UTRIE2_INDEX_2_MASK;
index = this.data[index];
index = (index << UTRIE2_INDEX_SHIFT) + (codePoint & UTRIE2_DATA_MASK);
return Unsafe.Add(ref dataBase, (nint)index);
}
if (codePoint <= 0x10ffff)
{
return Unsafe.Add(ref dataBase, (nint)(this.data.Length - UTRIE2_DATA_GRANULARITY));
}
// Fall through. The code point is outside of the legal range of 0..0x10ffff.
return this.errorValue;
}
/// <summary>
/// Saves the <see cref="UnicodeTrie"/> to the stream in a compressed format.
/// </summary>
/// <param name="stream">The output stream.</param>
public void Save(Stream stream)
{
// Write the header info
using (BinaryWriter bw = new(stream, Encoding.UTF8, true))
{
bw.Write(this.highStart);
bw.Write(this.errorValue);
bw.Write(this.data.Length * sizeof(uint));
}
// Write the data.
using (BinaryWriter bw = new(stream, Encoding.UTF8, true))
{
for (int i = 0; i < this.data.Length; i++)
{
bw.Write(this.data[i]);
}
}
}
private struct UnicodeTrieHeader
{
public int HighStart;
public uint ErrorValue;
public int DataLength;
}
}
}