// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System; using System.Buffers.Binary; using System.Diagnostics; using System.IO; using System.Runtime.CompilerServices; using System.Text; namespace SixLabors.Fonts { /// /// /// A binary reader that reads in big-endian format. /// /// /// This reader captures the stream position at construction time as startOfStream. /// All offset values read from OpenType tables (via , /// , etc.) are raw values relative to wherever the spec says /// they originate (typically the start of the containing table). /// /// /// When seeking with using , the /// startOfStream is automatically added to the supplied offset. This means /// table-relative offsets can be passed directly to without manually /// adding the table's absolute position. Do not add the table start /// yourself — that would double-count and land at the wrong position. /// /// /// In contrast, . always returns the /// absolute position within the underlying stream and is unaffected by /// startOfStream. /// /// [DebuggerDisplay("Start: {StartOfStream}, Position: {BaseStream.Position}")] internal sealed class BigEndianBinaryReader : IDisposable { /// /// Buffer used for temporary storage before conversion into primitives. /// private readonly byte[] buffer = new byte[16]; private readonly bool leaveOpen; /// /// Initializes a new instance of the class. /// The current position of is captured as startOfStream /// and used as the origin for all subsequent calls with /// . /// /// Stream to read data from. /// If , the stream is not disposed when this reader is disposed. public BigEndianBinaryReader(Stream stream, bool leaveOpen) { this.BaseStream = stream; this.StartOfStream = stream.Position; this.leaveOpen = leaveOpen; } /// /// Gets the underlying stream of the EndianBinaryReader. /// Note that on this stream is always the /// absolute position and is not adjusted by /// startOfStream. Avoid using BaseStream.Position to compute /// offsets for — use raw offsets from , /// , etc. instead. /// public Stream BaseStream { get; } /// /// Gets the absolute stream position captured at construction time. /// This is the origin for all seeks. /// public long StartOfStream { get; } /// /// Seeks within the stream. /// When is , startOfStream /// is automatically added to , so callers should pass /// table-relative offsets directly (e.g. values read from /// or ). Do not add the table's absolute /// position — that would double-count. /// /// Offset to seek to, relative to . /// Origin of seek operation. public void Seek(long offset, SeekOrigin origin) { if (origin == SeekOrigin.Begin) { offset += this.StartOfStream; } _ = this.BaseStream.Seek(offset, origin); } /// /// Reads a single byte from the stream. /// /// The byte read public byte ReadByte() { this.ReadInternal(this.buffer, 1); return this.buffer[0]; } /// /// Reads a single byte from the stream and reinterprets it as the specified enum type. /// /// The enum type whose underlying type must be a single byte. /// The enum value. public TEnum ReadByte() where TEnum : struct, Enum { _ = TryConvert(this.ReadByte(), out TEnum value); return value; } /// /// Reads a single signed byte from the stream. /// /// The byte read public sbyte ReadSByte() { this.ReadInternal(this.buffer, 1); return unchecked((sbyte)this.buffer[0]); } /// /// Reads a 2.14 fixed-point number from the stream. /// 2 bytes are read and divided by 16384 to produce a value in the range [-2, +2). /// /// The fixed-point value as a . public float ReadF2Dot14() { const float f2Dot14ToFloat = 16384F; return this.ReadInt16() / f2Dot14ToFloat; } /// /// Reads a 16-bit signed integer from the stream, using the bit converter /// for this reader. 2 bytes are read. /// /// The 16-bit integer read public short ReadInt16() { this.ReadInternal(this.buffer, 2); return BinaryPrimitives.ReadInt16BigEndian(this.buffer); } /// /// Reads a 16-bit integer from the stream and reinterprets it as the specified enum type. /// /// The enum type whose underlying type must be 16 bits. /// The enum value. public TEnum ReadInt16() where TEnum : struct, Enum { _ = TryConvert(this.ReadUInt16(), out TEnum value); return value; } /// /// Reads a signed 16-bit integer in big-endian order, representing an FWORD value from the current stream position. /// /// A 16-bit signed integer read from the stream, interpreted as an FWORD value. public short ReadFWORD() => this.ReadInt16(); /// /// Reads an array of FWORD (signed 16-bit) values from the stream. /// /// The number of values to read. /// An array of 16-bit signed integers. public short[] ReadFWORDArray(int length) => this.ReadInt16Array(length); /// /// Reads an unsigned 16-bit integer (UFWORD) from the current stream and advances the position by two bytes. /// /// An unsigned 16-bit integer read from the current stream. public ushort ReadUFWORD() => this.ReadUInt16(); /// /// Reads a 32-bit fixed-point number from the underlying data source and returns it as a single-precision /// floating-point value. /// /// A representing the fixed-point value read from the data source. public float ReadFixed() { this.ReadInternal(this.buffer, 4); return BinaryPrimitives.ReadInt32BigEndian(this.buffer) / 65536F; } /// /// Reads a 4-byte signed integer from the current stream. /// /// The 32-bit signed integer read from the stream. public int ReadInt32() { this.ReadInternal(this.buffer, 4); return BinaryPrimitives.ReadInt32BigEndian(this.buffer); } /// /// Reads a 64-bit signed integer from the stream. /// 8 bytes are read. /// /// The 64-bit integer read. public long ReadInt64() { this.ReadInternal(this.buffer, 8); return BinaryPrimitives.ReadInt64BigEndian(this.buffer); } /// /// Reads a 16-bit unsigned integer from the stream. /// 2 bytes are read. /// /// The 16-bit unsigned integer read. public ushort ReadUInt16() { this.ReadInternal(this.buffer, 2); return BinaryPrimitives.ReadUInt16BigEndian(this.buffer); } /// /// Reads a 16-bit unsigned integer from the stream representing an offset position. /// 2 bytes are read. The returned value is the raw offset as stored in the font file /// (typically relative to the start of the containing table). Pass it directly to /// with — do not add the table's /// absolute position. /// /// The 16-bit unsigned integer read. public ushort ReadOffset16() => this.ReadUInt16(); /// /// Reads a 16-bit unsigned integer from the stream and reinterprets it as the specified enum type. /// /// The enum type whose underlying type must be 16 bits. /// The enum value. public TEnum ReadUInt16() where TEnum : struct, Enum { _ = TryConvert(this.ReadUInt16(), out TEnum value); return value; } /// /// Reads an array of 16-bit unsigned integers from the stream. /// /// The number of values to read. /// An array of 16-bit unsigned integers. public ushort[] ReadUInt16Array(int length) { ushort[] data = new ushort[length]; for (int i = 0; i < length; i++) { data[i] = this.ReadUInt16(); } return data; } /// /// Reads array of 16-bit unsigned integers from the stream to the buffer. /// /// The buffer to read to. public void ReadUInt16Array(Span buffer) { for (int i = 0; i < buffer.Length; i++) { buffer[i] = this.ReadUInt16(); } } /// /// Reads an array of 32-bit unsigned integers from the stream. /// /// The number of values to read. /// An array of 32-bit unsigned integers. public uint[] ReadUInt32Array(int length) { uint[] data = new uint[length]; for (int i = 0; i < length; i++) { data[i] = this.ReadUInt32(); } return data; } /// /// Reads an array of 8-bit unsigned integers (bytes) from the stream. /// /// The number of bytes to read. /// A byte array of the requested length. public byte[] ReadUInt8Array(int length) { byte[] data = new byte[length]; this.ReadInternal(data, length); return data; } /// /// Reads an array of 16-bit signed integers from the stream. /// /// The number of values to read. /// An array of 16-bit signed integers. public short[] ReadInt16Array(int length) { short[] data = new short[length]; for (int i = 0; i < length; i++) { data[i] = this.ReadInt16(); } return data; } /// /// Reads an array of 16-bit signed integers from the stream to the buffer. /// /// The buffer to read to. public void ReadInt16Array(Span buffer) { for (int i = 0; i < buffer.Length; i++) { buffer[i] = this.ReadInt16(); } } /// /// Reads a 8-bit unsigned integer from the stream, using the bit converter /// for this reader. 1 bytes are read. /// /// The 8-bit unsigned integer read. public byte ReadUInt8() { this.ReadInternal(this.buffer, 1); return this.buffer[0]; } /// /// Reads a 24-bit unsigned integer from the stream, using the bit converter /// for this reader. 3 bytes are read. /// /// The 24-bit unsigned integer read. public uint ReadUInt24() { byte highByte = this.ReadByte(); return (uint)((highByte << 16) | this.ReadUInt16()); } /// /// Reads a 24-bit unsigned integer from the stream representing an offset position. /// 3 bytes are read. The returned value is the raw offset as stored in the font file /// (typically relative to the start of the containing table). Pass it directly to /// with — do not add the table's /// absolute position. /// /// The 24-bit unsigned integer read. public uint ReadOffset24() => this.ReadUInt24(); /// /// Reads a 32-bit unsigned integer from the stream, using the bit converter /// for this reader. 4 bytes are read. /// /// The 32-bit unsigned integer read. public uint ReadUInt32() { this.ReadInternal(this.buffer, 4); return BinaryPrimitives.ReadUInt32BigEndian(this.buffer); } /// /// Reads a 32-bit unsigned integer from the stream representing an offset position. /// 4 bytes are read. The returned value is the raw offset as stored in the font file /// (typically relative to the start of the containing table). Pass it directly to /// with — do not add the table's /// absolute position. /// /// The 32-bit unsigned integer read. public uint ReadOffset32() => this.ReadUInt32(); /// /// Reads the specified number of bytes, returning them in a new byte array. /// If not enough bytes are available before the end of the stream, this /// method will return what is available. /// /// The number of bytes to read. /// The bytes read. public byte[] ReadBytes(int count) { byte[] ret = new byte[count]; int index = 0; while (index < count) { int read = this.BaseStream.Read(ret, index, count - index); // Stream has finished half way through. That's fine, return what we've got. if (read == 0) { byte[] copy = new byte[index]; Buffer.BlockCopy(ret, 0, copy, 0, index); return copy; } index += read; } return ret; } /// /// Reads a string of a specific length, which specifies the number of bytes /// to read from the stream. These bytes are then converted into a string with /// the encoding for this reader. /// /// The bytes to read. /// The encoding. /// /// The string read from the stream. /// public string ReadString(int bytesToRead, Encoding encoding) { byte[] data = new byte[bytesToRead]; this.ReadInternal(data, bytesToRead); return encoding.GetString(data, 0, data.Length); } /// /// Reads a 4-byte OpenType tag from the stream as a UTF-8 string. /// /// A 4-character string representing the tag (e.g. "glyf", "GPOS"). public string ReadTag() { this.ReadInternal(this.buffer, 4); return Encoding.UTF8.GetString(this.buffer, 0, 4); } /// /// Reads an offset consuming the given number of bytes (1–4). /// The returned value is the raw offset as stored in the font file /// (typically relative to the start of the containing table). Pass it directly to /// with — do not add the table's /// absolute position. /// /// The offset size in bytes (1, 2, 3, or 4). /// The 32-bit signed integer representing the offset. /// Thrown when is not 1–4. public int ReadOffset(int size) => size switch { 1 => this.ReadByte(), 2 => (this.ReadByte() << 8) | (this.ReadByte() << 0), 3 => (this.ReadByte() << 16) | (this.ReadByte() << 8) | (this.ReadByte() << 0), 4 => (this.ReadByte() << 24) | (this.ReadByte() << 16) | (this.ReadByte() << 8) | (this.ReadByte() << 0), _ => throw new InvalidOperationException(), }; /// /// Reads the given number of bytes from the stream, throwing an exception /// if they can't all be read. /// /// Buffer to read into. /// Number of bytes to read. /// The end of the stream was reached before reading could complete. private void ReadInternal(byte[] data, int size) { int index = 0; while (index < size) { int read = this.BaseStream.Read(data, index, size - index); if (read == 0) { throw new EndOfStreamException($"End of stream reached with {size - index} byte{(size - index == 1 ? "s" : string.Empty)} left to read."); } index += read; } } /// public void Dispose() { if (!this.leaveOpen) { this.BaseStream?.Dispose(); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool TryConvert(T input, out TEnum value) where T : struct, IConvertible, IFormattable, IComparable where TEnum : struct, Enum { if (Unsafe.SizeOf() == Unsafe.SizeOf()) { value = Unsafe.As(ref input); return true; } value = default; return false; } } }