// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System; using System.IO; using System.IO.Compression; using System.Runtime.CompilerServices; namespace SixLabors.Fonts.Tables.General.Svg { /// /// Represents the SVG table which contains SVG documents for glyph rendering. /// /// internal class SvgTable : Table { /// /// The table name identifier for the SVG table. /// internal const string TableName = "SVG "; /// /// The raw byte data containing the SVG document payloads. /// private readonly byte[] tableData; /// /// The offset from the beginning of the SVG table to the SVG Document Index. /// private readonly uint svgDocIndexOffset; /// /// The absolute offset of the start of the table data buffer within the font stream. /// private readonly uint tableBaseOffset; /// /// The array of SVG Document Index entries, sorted by start glyph ID. /// private readonly SvgDocumentIndexEntry[] entries; /// /// Initializes a new instance of the class. /// /// The raw byte data containing the SVG document payloads. /// The offset from the beginning of the SVG table to the SVG Document Index. /// The absolute offset of the start of the table data buffer within the font stream. /// The array of SVG Document Index entries. private SvgTable(byte[] tableData, uint svgDocIndexOffset, uint tableBaseOffset, SvgDocumentIndexEntry[] entries) { this.tableData = tableData; this.svgDocIndexOffset = svgDocIndexOffset; this.tableBaseOffset = tableBaseOffset; this.entries = entries; } /// /// Loads the SVG table from the specified font reader. /// /// The font reader to read the table from. /// The , or if the table is not present in the font. public static SvgTable? Load(FontReader fontReader) { if (!fontReader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader)) { return null; } using (binaryReader) { return Load(binaryReader); } } /// /// Loads the SVG table from the specified binary reader. /// /// The binary reader positioned at the start of the SVG table. /// The , or if the table is not valid in the font. public static SvgTable? Load(BigEndianBinaryReader reader) { // HEADER // | Type | Name | Description | // | ---------| ------------------| ----------------------------------------------------------| // | uint16 | version | Table version number(starts at 0). | // | Offset32 | svgDocIndexOffset | Offset(from beginning of SVG table) to SVG Document Index.| // | uint32 | reserved | Reserved; set to 0 ushort version = reader.ReadUInt16(); if (version != 0) { throw new NotSupportedException($"Only SVG table version 0 is supported. Found version {version}."); } uint svgDocIndexOffset = reader.ReadUInt32(); _ = reader.ReadUInt32(); // reserved // SVG Document Index // | Type | Name | Description | // | ------------------| -----------| ------------------------------------------------------------| // | uint16 | numEntries | Number of entries in the SVG Document Index. | // | Entry[numEntries] | entries | Array of SVG Document Index Entries(sorted by startGlyphID).| reader.Seek(svgDocIndexOffset, SeekOrigin.Begin); ushort numEntries = reader.ReadUInt16(); if (numEntries == 0) { // The spec says the number of entries must be non-zero. return null; } SvgDocumentIndexEntry[] entries = new SvgDocumentIndexEntry[numEntries]; // SVG Document Index Entry // | Type | Name | Description | // | ---------| -------------| -----------------------------------------------------------------------------------------| // | uint16 | startGlyphID | First glyph ID in this range(inclusive). | // | uint16 | endGlyphID | Last glyph ID in this range(inclusive). | // | Offset32 | svgDocOffset | Offset from the beginning of the SVG Document Index to an SVG document. Must be non-zero.| // Track min relative offset from the Document Index and absolute max end. uint minRelOffset = uint.MaxValue; uint maxEnd = 0; for (int i = 0; i < numEntries; i++) { ushort startGlyphId = reader.ReadUInt16(); ushort endGlyphId = reader.ReadUInt16(); uint svgDocOffset = reader.ReadUInt32(); uint svgDocLength = reader.ReadUInt32(); if (svgDocOffset == 0 || svgDocLength == 0) { throw new InvalidFontFileException("SVG table contains an entry with zero offset or length."); } if (svgDocOffset < minRelOffset) { minRelOffset = svgDocOffset; } // Track the farthest byte we need to cover in the table buffer. uint absEnd = svgDocIndexOffset + svgDocOffset + svgDocLength; if (absEnd > maxEnd) { maxEnd = absEnd; } entries[i] = new SvgDocumentIndexEntry(startGlyphId, endGlyphId, svgDocOffset, svgDocLength); } // Read exactly the covered range. uint tableStart = svgDocIndexOffset + minRelOffset; int byteCount = (int)(maxEnd - tableStart); reader.Seek(tableStart, SeekOrigin.Begin); byte[] tableData = reader.ReadBytes(byteCount); return new SvgTable(tableData, svgDocIndexOffset, tableStart, entries); } /// /// Returns true if the SVG Document Index contains a document for . /// /// The glyph identifier to look up. /// if a document exists for the glyph; otherwise, . [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool ContainsGlyph(ushort glyphId) => this.TryFindEntry(glyphId, out _); /// /// Gets the encoded document slice for a glyph without opening a stream. /// /// The glyph identifier to look up. /// When this method returns, contains the start offset within the table data buffer. /// When this method returns, contains the length of the SVG document in bytes. /// if a document was found for the glyph; otherwise, . public bool TryGetDocumentSpan(ushort glyphId, out int start, out int length) { if (this.TryFindEntry(glyphId, out SvgDocumentIndexEntry e)) { start = (int)((this.svgDocIndexOffset + e.SvgDocOffset) - this.tableBaseOffset); length = (int)e.SvgDocLength; return true; } start = 0; length = 0; return false; } /// /// Opens a decoding stream for the SVG document associated with the specified glyph. /// If the payload is gzip-compressed (RFC 1952), wraps it in a ; /// otherwise returns the raw memory stream. The caller owns the returned stream. /// /// The glyph identifier to look up. /// When this method returns, contains the decoded SVG document stream, or if not found. /// if a document was found and a stream was opened; otherwise, . public bool TryOpenDecodedDocumentStream(ushort glyphId, out Stream stream) { if (!this.TryOpenEncodedDocumentStream(glyphId, out Stream encoded)) { stream = Stream.Null; return false; } if (encoded is MemoryStream ms && ms.Length >= 2) { long pos = ms.Position; int b0 = ms.ReadByte(); int b1 = ms.ReadByte(); ms.Position = pos; // Start of GZIP (RFC1952) if (b0 == 0x1F && b1 == 0x8B) { stream = new GZipStream(ms, CompressionMode.Decompress, leaveOpen: false); return true; } } stream = encoded; // plain UTF-8 XML return true; } /// /// Attempts to open a raw (potentially gzip-compressed) memory stream for the SVG document /// associated with the specified glyph. /// /// The glyph identifier to look up. /// When this method returns, contains the encoded SVG document stream, or if not found. /// if a document was found and a stream was opened; otherwise, . private bool TryOpenEncodedDocumentStream(ushort glyphId, out Stream stream) { if (this.TryFindEntry(glyphId, out SvgDocumentIndexEntry e)) { int start = (int)((this.svgDocIndexOffset + e.SvgDocOffset) - this.tableBaseOffset); int length = (int)e.SvgDocLength; stream = new MemoryStream(this.tableData, start, length, writable: false); return true; } stream = Stream.Null; return false; } /// /// Performs a binary search on the SVG Document Index entries to find the entry /// whose glyph ID range contains the specified glyph. /// /// The glyph identifier to search for. /// When this method returns, contains the matching index entry, or the default value if not found. /// if a matching entry was found; otherwise, . private bool TryFindEntry(ushort glyphId, out SvgDocumentIndexEntry entry) { int lo = 0; int hi = this.entries.Length - 1; int candidate = -1; while (lo <= hi) { int mid = (int)((uint)(lo + hi) >> 1); ushort start = this.entries[mid].StartGlyphId; if (start <= glyphId) { candidate = mid; lo = mid + 1; } else { hi = mid - 1; } } if (candidate >= 0) { SvgDocumentIndexEntry e = this.entries[candidate]; if (glyphId <= e.EndGlyphId) { entry = e; return true; } } entry = default; return false; } } }