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

use byteorder::{LittleEndian, ReadBytesExt};
use colors::{Color, ColorTable};
use commands::Command;
use header::{CoordinateRange, Scale, TvgHeader};
use thiserror::Error;

pub mod colors;
mod commands;
mod header;
mod path;

pub use header::SUPPORTED_VERSION;

#[derive(Debug, Clone)]
pub struct TvgFile<C: Color> {
	header: TvgHeader,
	color_table: ColorTable<C>,
	commands: Box<[Command]>,
}

impl<C: Color> TvgFile<C> {
	/// Read a TinyVG file.
	pub fn read_from(reader: &mut impl Read) -> Result<Self, TvgError> {
		let header = TvgHeader::read(reader)?;
		let color_table =
			ColorTable::read_from_encoding(reader, header.color_count(), header.color_encoding())?;

		let mut commands = Vec::new();
		loop {
			let command = Command::read(reader, &header)?;
			commands.push(command.clone());

			if command.is_end_of_document() {
				break;
			}
		}

		Ok(Self {
			header,
			color_table,
			commands: commands.into_boxed_slice(),
		})
	}

	/// Read a TinyVG file. If a Custom color encoding if found, use the specified encoding.
	pub fn read_with_custom_encoding<Custom: Color>(
		reader: &mut impl Read,
	) -> Result<Self, TvgError> {
		let header = TvgHeader::read(reader)?;
		let color_table = ColorTable::read_from_encoding_with_custom::<Custom>(
			reader,
			header.color_count(),
			header.color_encoding(),
		)?;

		let mut commands = Vec::new();
		loop {
			let command = Command::read(reader, &header)?;
			commands.push(command.clone());

			if command.is_end_of_document() {
				break;
			}
		}

		Ok(Self {
			header,
			color_table,
			commands: commands.into_boxed_slice(),
		})
	}
}

#[derive(Debug, Error)]
pub enum TvgError {
	#[error("Not a valid TVG file. The magic number was invalid.")]
	InvalidFile,
	#[error("Expected version 1, but found version {}.", .0)]
	UnsupportedVersion(u8),
	#[error("Found a coordinate range with an index of 3, which is invalid.")]
	InvalidCoordinateRange,
	#[error("Found a Custom color encoding, which is unsupported.")]
	UnsupportedColorEncoding,
	#[error("Found a command with an index of {}, which is invalid.", .0)]
	InvalidCommand(u8),
	#[error("Found a style kind with an index of 3, which is invalid.")]
	InvalidStyleKind,
	#[error("Polygons must have at least 3 points, found {}.", .0)]
	InvalidPolygon(u32),
	#[error("All padding bits must be zero. Found {}.", .0)]
	NonZeroPadding(u8),
	#[error("The end of the document must have a `prim_style_kind` value of 0. Found {}.", .0)]
	InvalidEndOfDocument(u8),
	#[error("{}", .0)]
	IoError(#[from] io::Error),
}

trait Decode: Sized {
	fn read(reader: &mut impl Read, header: &TvgHeader) -> io::Result<Self>;

	fn read_multiple(
		reader: &mut impl Read,
		header: &TvgHeader,
		count: u32,
	) -> io::Result<Box<[Self]>> {
		let mut vec = Vec::with_capacity(count as usize);
		for _ in 0..count {
			vec.push(Self::read(reader, header)?);
		}

		Ok(vec.into_boxed_slice())
	}
}

/// The unit is the common type for both positions and sizes in the vector
/// graphic. It is encoded as a signed integer with a configurable amount of
/// bits (see [`CoordinateRange`]) and fractional bits.
fn read_unit(reader: &mut impl Read, header: &TvgHeader) -> io::Result<f64> {
	let value = match header.coordinate_range() {
		CoordinateRange::Reduced => reader.read_i8()? as i32,
		CoordinateRange::Default => reader.read_i16::<LittleEndian>()? as i32,
		CoordinateRange::Enhanced => reader.read_i32::<LittleEndian>()?,
	};

	let fractional = match header.scale() {
		Scale::_1 => 1.0,
		Scale::_2 => 2.0,
		Scale::_4 => 4.0,
		Scale::_8 => 8.0,
		Scale::_16 => 16.0,
		Scale::_32 => 32.0,
		Scale::_64 => 64.0,
		Scale::_128 => 128.0,
		Scale::_256 => 256.0,
		Scale::_512 => 512.0,
		Scale::_1024 => 1024.0,
		Scale::_2048 => 2048.0,
		Scale::_4096 => 4096.0,
		Scale::_8192 => 8192.0,
		Scale::_16384 => 16_384.0,
		Scale::_32768 => 32_768.0,
	};

	Ok((value as f64) / fractional)
}

/// A X and Y coordinate pair.
#[derive(Debug, Clone, Copy)]
pub struct Point {
	/// Horizontal distance of the point to the origin.
	x: f64,
	/// Vertical distance of the point to the origin.
	y: f64,
}

impl Decode for Point {
	fn read(reader: &mut impl Read, header: &TvgHeader) -> io::Result<Self> {
		Ok(Self {
			x: read_unit(reader, header)?,
			y: read_unit(reader, header)?,
		})
	}
}

fn read_varuint(reader: &mut impl Read) -> io::Result<u32> {
	let mut count = 0;
	let mut result = 0;

	loop {
		let byte = reader.read_u8()? as u32;
		let value = (byte & 0x7F) << (7 * count);
		result |= value;

		if (byte & 0x80) == 0 {
			break;
		}

		count += 1;
	}

	Ok(result)
}