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