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