// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.Fonts.Unicode;
using SixLabors.Fonts.WellKnownIds;
using System;
using System.Collections.Generic;
using System.Linq;
namespace SixLabors.Fonts.Tables.General.CMap {
///
/// Format 4 is a segment mapping to delta values subtable used for character codes in the BMP (U+0000 to U+FFFF).
///
///
internal sealed class Format4SubTable : CMapSubTable
{
///
/// Initializes a new instance of the class.
///
/// The language code for Macintosh platform subtables.
/// The platform identifier.
/// The platform-specific encoding identifier.
/// The array of character code segments.
/// The glyph index array used for offset-based lookups.
public Format4SubTable(ushort language, PlatformIDs platform, ushort encoding, Segment[] segments, ushort[] glyphIds)
: base(platform, encoding, 4)
{
this.Language = language;
this.Segments = segments;
this.GlyphIds = glyphIds;
}
///
/// Gets the array of character code segments.
///
public Segment[] Segments { get; }
///
/// Gets the glyph index array used for offset-based lookups.
///
public ushort[] GlyphIds { get; }
///
/// Gets the language code for Macintosh platform subtables.
///
public ushort Language { get; }
///
public override bool TryGetGlyphId(CodePoint codePoint, out ushort glyphId)
{
int charAsInt = codePoint.Value;
for (int i = 0; i < this.Segments.Length; i++)
{
ref Segment seg = ref this.Segments[i];
if (seg.End >= charAsInt && seg.Start <= charAsInt)
{
if (seg.Offset == 0)
{
glyphId = (ushort)((charAsInt + seg.Delta) & ushort.MaxValue);
return true;
}
long offset = (seg.Offset / 2) + (charAsInt - seg.Start);
long idx = offset - this.Segments.Length + seg.Index;
if (idx < 0 || idx >= this.GlyphIds.Length)
{
glyphId = 0;
return false;
}
glyphId = this.GlyphIds[idx];
return true;
}
}
glyphId = 0;
return false;
}
///
public override bool TryGetCodePoint(ushort glyphId, out CodePoint codePoint)
{
for (int i = 0; i < this.Segments.Length; i++)
{
ref Segment seg = ref this.Segments[i];
if (seg.Offset == 0)
{
// Reverse the delta-based calculation
// Forward was: glyphId = (charAsInt + seg.Delta) & 0xFFFF
// Reverse should apply the inverse logic with the same wrap:
int candidate = (glyphId - seg.Delta) & ushort.MaxValue;
if (candidate >= seg.Start && candidate <= seg.End)
{
codePoint = new CodePoint(candidate);
return true;
}
}
else
{
// Reverse the offset-based calculation:
// Forward logic:
// offset = (seg.Offset / 2) + (charAsInt - seg.Start)
// glyphId = GlyphIds[offset - Segments.Length + seg.Index]
// To reverse, iterate over possible codepoints in the segment and find the matching glyphId.
for (long j = 0; j <= (seg.End - seg.Start); j++)
{
long offset = (seg.Offset / 2) + j;
long idx = offset - this.Segments.Length + seg.Index;
if (idx < 0 || idx >= this.GlyphIds.Length)
{
codePoint = default;
return false;
}
if (this.GlyphIds[idx] == glyphId)
{
codePoint = new CodePoint((int)(seg.Start + j));
return true;
}
}
}
}
codePoint = default;
return false;
}
///
public override IEnumerable GetAvailableCodePoints()
=> this.Segments.SelectMany(segment => Enumerable.Range(segment.Start, segment.End - segment.Start + 1));
///
/// Loads one or more instances from the specified encoding records and reader.
///
/// The encoding records that share this subtable.
/// The binary reader positioned after the format field.
/// An enumerable of instances, one per encoding record.
public static IEnumerable Load(IEnumerable encodings, BigEndianBinaryReader reader)
{
// 'cmap' Subtable Format 4:
// Type | Name | Description
// -------|----------------------------|------------------------------------------------------------------------
// uint16 | format | Format number is set to 4.
// uint16 | length | This is the length in bytes of the subtable.
// uint16 | language | Please see "Note on the language field in 'cmap' subtables" in this document.
// uint16 | segCountX2 | 2 x segCount.
// uint16 | searchRange | 2 x (2**floor(log2(segCount)))
// uint16 | entrySelector | log2(searchRange/2)
// uint16 | rangeShift | 2 x segCount - searchRange
// uint16 | endCount[segCount] | End characterCode for each segment, last=0xFFFF.
// uint16 | reservedPad | Set to 0.
// uint16 | startCount[segCount] | Start character code for each segment.
// int16 | idDelta[segCount] | Delta for all character codes in segment.
// uint16 | idRangeOffset[segCount] | Offsets into glyphIdArray or 0
// uint16 | glyphIdArray[ ] | Glyph index array (arbitrary length)
// format has already been read by this point skip it
ushort length = reader.ReadUInt16();
ushort language = reader.ReadUInt16();
ushort segCountX2 = reader.ReadUInt16();
ushort searchRange = reader.ReadUInt16();
ushort entrySelector = reader.ReadUInt16();
ushort rangeShift = reader.ReadUInt16();
int segCount = segCountX2 / 2;
using Buffer endCountBuffer = new(segCount);
Span endCounts = endCountBuffer.GetSpan();
reader.ReadUInt16Array(endCounts);
ushort reserved = reader.ReadUInt16();
using Buffer startCountsBuffer = new(segCount);
Span startCounts = startCountsBuffer.GetSpan();
reader.ReadUInt16Array(startCounts);
using Buffer idDeltaBuffer = new(segCount);
Span idDelta = idDeltaBuffer.GetSpan();
reader.ReadInt16Array(idDelta);
using Buffer idRangeOffsetBuffer = new(segCount);
Span idRangeOffset = idRangeOffsetBuffer.GetSpan();
reader.ReadUInt16Array(idRangeOffset);
// table length thus far
int headerLength = 16 + (segCount * 8);
int glyphIdCount = (length - headerLength) / 2;
ushort[] glyphIds = reader.ReadUInt16Array(glyphIdCount);
Segment[] segments = Segment.Create(endCounts, startCounts, idDelta, idRangeOffset);
List table = [];
foreach (EncodingRecord encoding in encodings)
{
table.Add(new Format4SubTable(language, encoding.PlatformID, encoding.EncodingID, segments, glyphIds));
}
return table;
}
///
/// Represents a single segment in a Format 4 subtable, defining a contiguous range of character codes
/// and their mapping to glyph indices via delta or offset.
///
internal readonly struct Segment
{
///
/// Initializes a new instance of the struct.
///
/// The zero-based index of this segment in the segment array.
/// The end character code for this segment.
/// The start character code for this segment.
/// The delta value to apply to character codes in this segment.
/// The offset into the glyph index array, or 0 if delta-based mapping is used.
public Segment(ushort index, ushort end, ushort start, short delta, ushort offset)
{
this.Index = index;
this.End = end;
this.Start = start;
this.Delta = delta;
this.Offset = offset;
}
///
/// Gets the zero-based index of this segment in the segment array.
///
public ushort Index { get; }
///
/// Gets the delta value added to character codes to produce glyph indices.
///
public short Delta { get; }
///
/// Gets the end character code for this segment (inclusive).
///
public ushort End { get; }
///
/// Gets the offset into the glyph index array, or 0 if delta-based mapping is used.
///
public ushort Offset { get; }
///
/// Gets the start character code for this segment.
///
public ushort Start { get; }
///
/// Creates an array of instances from the parallel arrays read from the subtable.
///
/// The end character codes for each segment.
/// The start character codes for each segment.
/// The delta values for each segment.
/// The range offset values for each segment.
/// An array of instances.
public static Segment[] Create(ReadOnlySpan endCounts, ReadOnlySpan startCode, ReadOnlySpan idDelta, ReadOnlySpan idRangeOffset)
{
int count = endCounts.Length;
Segment[] segments = new Segment[count];
for (ushort i = 0; i < count; i++)
{
ushort start = startCode[i];
ushort end = endCounts[i];
short delta = idDelta[i];
ushort offset = idRangeOffset[i];
segments[i] = new Segment(i, end, start, delta, offset);
}
return segments;
}
}
}
}