summaryrefslogtreecommitdiff
path: root/tvg/src/header.rs
blob: d444f66af09bd55796d5abcfbacc4be6375f5b61 (plain)
use std::io::{self, Read};

use byteorder::{LittleEndian, ReadBytesExt};
use num_enum::TryFromPrimitive;
use raise::yeet;

use crate::colors::ColorEncoding;
use crate::read_varuint;
use crate::TvgError;

const MAGIC: [u8; 2] = [0x72, 0x56];
pub const SUPPORTED_VERSION: u8 = 1;

/// The coordinate range defines how many bits a Unit value uses.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive)]
#[repr(u8)]
pub enum CoordinateRange {
	/// Each [`Unit`] takes up 16 bits.
	#[default]
	Default = 0,
	/// Each [`Unit`] takes up 8 bits.
	Reduced = 1,
	/// Each [`Unit`] takes up 32 bits.
	Enhanced = 2,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive)]
#[repr(u8)]
pub enum Scale {
	_1 = 0,
	_2 = 1,
	_4 = 2,
	_8 = 3,
	_16 = 4,
	_32 = 5,
	_64 = 6,
	_128 = 7,
	_256 = 8,
	_512 = 9,
	_1024 = 10,
	_2048 = 11,
	_4096 = 12,
	_8192 = 13,
	_16384 = 14,
	_32768 = 15,
}

/// Each TVG file starts with a header defining some global values for the file
/// like scale and image size. This is a representation of the header, but not
/// necessarily an exact representation of the bits of a TVG header.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TvgHeader {
	/// Must always be [0x72, 0x56]
	magic: [u8; 2],
	/// Must be 1. For future versions, this field might decide how the rest of
	/// the format looks like.
	version: u8,
	/// Defines the number of fraction bits in a Unit value.
	scale: Scale,
	/// Defines the type of color information that is used in the
	/// [`ColorTable`].
	color_encoding: ColorEncoding,
	/// Defines the number of total bits in a Unit value and thus the overall
	/// precision of the file.
	coordinate_range: CoordinateRange,
	/// Encodes the maximum width of the output file in *display units*.
	///
	/// A value of 0 indicates that the image has the maximum possible width.
	width: u32,
	/// Encodes the maximum height of the output file in *display units*.
	///
	/// A value of 0 indicates that the image has the maximum possible height.
	height: u32,
	/// The number of colors in the color table.
	color_count: u32,
}

impl TvgHeader {
	pub fn read<R: Read>(reader: &mut R) -> Result<Self, TvgError> {
		// magic number is used as a first line defense against invalid data
		let magic = [reader.read_u8()?, reader.read_u8()?];
		if magic != MAGIC {
			yeet!(TvgError::InvalidFile);
		}

		// the version of tvg being used
		let version = reader.read_u8()?;
		if version != SUPPORTED_VERSION {
			yeet!(TvgError::UnsupportedVersion(version))
		}

		// scale, color_encoding, and coordinate_range are stored in one byte
		let byte = reader.read_u8()?;
		let scale = Scale::try_from_primitive(byte & 0b0000_1111).expect("invalid scale");
		let color_encoding = ColorEncoding::try_from_primitive((byte & 0b0011_0000) >> 4)
			.expect("invalid color encoding");
		let coordinate_range = CoordinateRange::try_from_primitive((byte & 0b1100_0000) >> 6)
			.expect("invalid coordinate range");

		// width and height depend on the coordinate range
		let width = read_unsigned_unit(coordinate_range, reader)?;
		let height = read_unsigned_unit(coordinate_range, reader)?;

		let color_count = read_varuint(reader)?;

		Ok(Self {
			magic,
			version,
			scale,
			color_encoding,
			coordinate_range,
			width,
			height,
			color_count,
		})
	}

	/// Defines the number of total bits in a Unit value and thus the overall
	/// precision of the file.
	pub fn coordinate_range(&self) -> CoordinateRange {
		self.coordinate_range
	}

	/// Defines the type of color information that is used in the [`ColorTable`].
	pub fn color_encoding(&self) -> ColorEncoding {
		self.color_encoding
	}

	/// Defines the number of fraction bits in a Unit value.
	pub fn scale(&self) -> Scale {
		self.scale
	}

	/// The number of colors in the [`ColorTable`].
	pub fn color_count(&self) -> u32 {
		self.color_count
	}

	pub fn width(&self) -> u32 {
		self.width
	}

	pub fn height(&self) -> u32 {
		self.height
	}
}

fn read_unsigned_unit<R: Read>(
	coordinate_range: CoordinateRange,
	bytes: &mut R,
) -> io::Result<u32> {
	Ok(match coordinate_range {
		CoordinateRange::Reduced => bytes.read_u8()? as u32,
		CoordinateRange::Default => bytes.read_u16::<LittleEndian>()? as u32,
		CoordinateRange::Enhanced => bytes.read_u32::<LittleEndian>()?,
	})
}