summaryrefslogtreecommitdiff
path: root/alligator_tvg/src/commands.rs
blob: f316a53d3dca08b5ae1fc1e2067674e01372e4b2 (plain)
use std::io::{self, Read};

use byteorder::ReadBytesExt;
use raise::yeet;

use crate::{header::TvgHeader, path::Path, read_unit, read_varuint, Decode, Point, TvgError};

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Rectangle {
	/// Horizontal distance of the left side to the origin.
	x: f64,
	/// Vertical distance of the upper side to the origin.
	y: f64,
	/// Horizontal extent of the rectangle.
	width: f64,
	/// Vertical extent of the rectangle.
	height: f64,
}

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

#[derive(Debug, Clone, Copy)]
pub struct Line {
	/// Start point of the line.
	start: Point,
	/// End point of the line.
	end: Point,
}

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

/// A command that can be encoded.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
enum CommandName {
	/// Determines end of file.
	EndOfDocument = 0,
	/// Fills an N-gon.
	FillPolygon = 1,
	/// Fills a set of [`Rectangle`]s.
	FillRectangles = 2,
	/// Fills a free-form [`Path`].
	FillPath = 3,
	/// Draws a set of lines.
	DrawLines = 4,
	/// Draws the outline of a polygon.
	DrawLineLoop = 5,
	/// Draws a list of end-to-end lines.
	DrawLineStrip = 6,
	/// Draws a free-form [`Path`].
	DrawLinePath = 7,
	/// Draws a filled polygon with an outline.
	OutlineFillPolygon = 8,
	/// Draws several filled [`Rectangle`]s with an outline.
	OutlineFillRectangles = 9,
	/// This command combines the [`FillPath`] and [`DrawLinePath`] command
	/// into one.
	OutlineFillPath = 10,
}

impl TryFrom<u8> for CommandName {
	type Error = TvgError;

	fn try_from(value: u8) -> Result<Self, Self::Error> {
		match value {
			0 => Ok(Self::EndOfDocument),
			1 => Ok(Self::FillPolygon),
			2 => Ok(Self::FillRectangles),
			3 => Ok(Self::FillPath),
			4 => Ok(Self::DrawLines),
			5 => Ok(Self::DrawLineLoop),
			6 => Ok(Self::DrawLineStrip),
			7 => Ok(Self::DrawLinePath),
			8 => Ok(Self::OutlineFillPolygon),
			9 => Ok(Self::OutlineFillRectangles),
			10 => Ok(Self::OutlineFillPath),
			v => Err(TvgError::InvalidCommand(v)),
		}
	}
}

/// The type of style the command uses as a primary style.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum StyleKind {
	/// The shape is uniformly colored with a single color.
	FlatColored = 0,
	/// The shape is colored with a linear gradient.
	LinearGradient = 1,
	/// The shape is colored with a radial gradient.
	RadialGradient = 2,
}

impl TryFrom<u8> for StyleKind {
	type Error = TvgError;

	fn try_from(value: u8) -> Result<Self, Self::Error> {
		match value {
			0 => Ok(Self::FlatColored),
			1 => Ok(Self::LinearGradient),
			2 => Ok(Self::RadialGradient),
			_ => Err(TvgError::InvalidStyleKind),
		}
	}
}

/// The style kind, along with the colors and points used to render the style.
#[derive(Debug, Clone, Copy)]
pub enum Style {
	/// The shape is uniformly colored with the color at `color_index` in the
	/// [`ColorTable`].
	FlatColored {
		/// The index in the [`ColorTable`].
		color_index: u32,
	},
	/// The gradient is formed by a mental line between `point_0` and
	/// `point_1`.
	///
	/// The color at `point_0` is the color at `color_index_0` in the color
	/// table. The color at `point_1` is the color at `color_index_1` in the
	/// [`ColorTable`]. On the line, the color is interpolated between the two
	/// points. Each point that is not on the line is orthogonally projected to
	/// the line and the color at that point is sampled. Points that are not
	/// projectable onto the line have either the color at `point_0` if they
	/// are closed to `point_0` or vice versa for `point_1`.
	LinearGradient {
		/// The start point of the gradient.
		point_0: Point,
		/// The end point of the gradient.
		point_1: Point,
		/// The color at [`point_0`].
		color_index_0: u32,
		/// The color at [`point_1`].
		color_index_1: u32,
	},
	/// The gradient is formed by a mental circle with the center at `point_0`
	/// and `point_1` being somewhere on the circle outline. Thus, the radius
	/// of said circle is the distance between `point_0` and `point_1`.
	///
	/// The color at `point_0` is the color at `color_index_0` in the color
	/// table. The color on the circle outline is the color at `color_index_1`
	/// in the [`ColorTable`]. If a sampled point is inside the circle, the
	/// color is interpolated based on the distance to the center and the
	/// radius. If the point is not in the circle itself, the color at
	/// `color_index_1` is always taken.
	RadialGradient {
		/// The center point of the mental circle.
		point_0: Point,
		/// The end point of the gradient.
		point_1: Point,
		/// The color at `point_0`.
		color_index_0: u32,
		/// The color at `point_1`.
		color_index_1: u32,
	},
}

impl Style {
	fn read(reader: &mut impl Read, header: &TvgHeader, kind: StyleKind) -> io::Result<Self> {
		match kind {
			StyleKind::FlatColored => Self::read_flat_colored(reader),
			StyleKind::LinearGradient => Self::read_linear_gradient(reader, header),
			StyleKind::RadialGradient => Self::read_radial_gradient(reader, header),
		}
	}

	fn read_flat_colored(reader: &mut impl Read) -> io::Result<Self> {
		Ok(Self::FlatColored {
			color_index: read_varuint(reader)?,
		})
	}

	fn read_linear_gradient(reader: &mut impl Read, header: &TvgHeader) -> io::Result<Self> {
		Ok(Self::LinearGradient {
			point_0: Point::read(reader, header)?,
			point_1: Point::read(reader, header)?,
			color_index_0: read_varuint(reader)?,
			color_index_1: read_varuint(reader)?,
		})
	}

	fn read_radial_gradient(reader: &mut impl Read, header: &TvgHeader) -> io::Result<Self> {
		Ok(Self::RadialGradient {
			point_0: Point::read(reader, header)?,
			point_1: Point::read(reader, header)?,
			color_index_0: read_varuint(reader)?,
			color_index_1: read_varuint(reader)?,
		})
	}
}

/// TinyVG files contain a sequence of draw commands that must be executed in
/// the defined order to get the final result. Each draw command adds a new 2D
/// primitive to the graphic.
#[derive(Debug, Clone)]
pub enum Command {
	/// If this command is read, the TinyVG file has ended.
	///
	/// This command must have prim_style_kind to be set to 0, so the last byte
	/// of every TinyVG file is `0x00`. Every byte after this command is
	/// considered not part of the TinyVG data and can be used for other
	/// purposes like metadata or similar.
	EndOfDocument,
	/// Fills a polygon with N [`Point`]s.
	///
	/// The number of points must be at least 3. Files that encode a lower
	/// value must be discarded as ”invalid” by a conforming implementation.
	///
	/// The polygon specified in polygon must be drawn using the even-odd rule.
	/// That means that if for any point to be inside the polygon, a line to
	/// infinity must cross an odd number of polygon segments.
	FillPolygon {
		/// The style that is used to fill the polygon.
		fill_style: Style,
		/// The points of the polygon.
		polygon: Box<[Point]>,
	},
	/// Fills a list of [`Rectangle`]s.
	///
	/// The rectangles must be drawn first to last, which is the order they
	/// appear in the file.
	FillRectangles {
		/// The style that is used to fill all rectangles.
		fill_style: Style,
		/// The list of rectangles to be filled.
		rectangles: Box<[Rectangle]>,
	},
	/// Fills a [`Path`].
	///
	/// For the filling, all path segments are considered a polygon each (drawn
	/// with even-odd rule) that, when overlap, also perform the even odd rule.
	/// This allows the user to carve out parts of the path and create
	/// arbitrarily shaped surfaces.
	FillPath {
		/// The style that is used to fill the path.
		fill_style: Style,
		/// A [`Path`] with `segment_count` segments.
		path: Path,
	},
	/// Draws a set of [`Line`]s.
	///
	/// Each line is `line_width` units wide, and at least a single display
	/// pixel. This means that line_width of 0 is still visible, even though
	/// only marginally. This allows very thin outlines.
	DrawLines {
		/// The style that is used to draw the all lines.
		line_style: Style,
		/// The width of the lines.
		line_width: f64,
		/// The list of lines.
		lines: Box<[Line]>,
	},
	/// Draws a polygon.
	///
	/// Each line is `line_width` units wide. The lines are drawn between
	/// consecutive points as well as the first and the last point.
	DrawLineLoop {
		/// The style that is used to draw the all lines.
		line_style: Style,
		/// The width of the line.
		line_width: f64,
		/// The points of the polygon.
		points: Box<[Point]>,
	},
	/// Draws a list of consecutive lines.
	///
	/// The lines are drawn between consecutive points, but contrary to
	/// [`DrawLineLoop`], the first and the last point are not connected.
	DrawLineStrip {
		/// The style that is used to draw the all rectangles.
		line_style: Style,
		/// The width of the line.
		line_width: f64,
		/// The points of the line strip.
		points: Box<[Point]>,
	},
	/// Draws a [`Path`].
	///
	/// The outline of the path is `line_width` units wide.
	DrawLinePath {
		/// The style that is used to draw the path.
		line_style: Style,
		/// The width of the line.
		line_width: f64,
		/// A path with `segment_count` segments.
		path: Path,
	},
	/// Fills a polygon and draws an outline at the same time.
	///
	/// This command is a combination of [`FillPolygon`] and [`DrawLineLoop`].
	/// It first performs a [`FillPolygon`] with the `fill_style`, then
	/// performs [`DrawLineLoop`] with `line_style` and `line_width`.
	///
	/// The outline commands use a reduced number of elements. The maximum
	/// number of points is 64.
	OutlineFillPolygon {
		/// The style that is used to fill the polygon.
		fill_style: Style,
		/// The style that is used to draw the outline of the polygon.
		line_style: Style,
		/// The width of the line.
		line_width: f64,
		/// The set of points of this polygon.
		points: Box<[Point]>,
	},
	/// Fills and outlines a list of [`Rectangle`]s.
	///
	/// For each rectangle, it is first filled, then its outline is drawn, then
	/// the next rectangle is drawn.
	///
	/// The outline commands use a reduced number of elements, the maximum
	/// number of points is 64.
	OutlineFillRectangles {
		/// The style that is used to fill the rectangles.
		fill_style: Style,
		/// The style that is used to draw the outline of the rectangles.
		line_style: Style,
		/// The width of the line.
		line_width: f64,
		/// The list of rectangles to be drawn.
		rectangles: Box<[Rectangle]>,
	},
	/// Fills a path and draws an outline at the same time.
	///
	/// This command is a combination of [`FillPath`] and [`DrawLinePath`]. It
	/// first performs a [`FillPath`] with the `fill_style`, then performs
	/// [`DrawLinePath`] with `line_style` and `line_width`.
	OutlineFillPath {
		/// The style that is used to fill the path.
		fill_style: Style,
		/// The style that is used to draw the outline of the path.
		line_style: Style,
		/// The width of the line.
		line_width: f64,
		/// The path that should be drawn.
		path: Path,
	},
}

/// The header is different for outline commands, so we use this as a helper
struct OutlineHeader {
	count: u32,
	fill_style: Style,
	line_style: Style,
	line_width: f64,
}

impl OutlineHeader {
	fn read(
		reader: &mut impl Read,
		header: &TvgHeader,
		prim_style_kind: StyleKind,
	) -> Result<Self, TvgError> {
		// the count and secondary style kind are stores in the same byte
		let byte = reader.read_u8()?;
		let count = (byte & 0b0011_1111) as u32 + 1;
		let sec_style_kind = StyleKind::try_from((byte & 0b1100_0000) >> 6)?;

		let fill_style = Style::read(reader, header, prim_style_kind)?;
		let line_style = Style::read(reader, header, sec_style_kind)?;

		let line_width = read_unit(reader, header)?;

		Ok(Self {
			count,
			fill_style,
			line_style,
			line_width,
		})
	}
}

impl Command {
	pub fn read(reader: &mut impl Read, header: &TvgHeader) -> Result<Self, TvgError> {
		// the command name and primary style kind are stores in the same byte
		let byte = reader.read_u8()?;
		let command = CommandName::try_from(byte & 0b0011_1111)?;
		let style_kind = StyleKind::try_from((byte & 0b1100_0000) >> 6)?;

		match command {
			CommandName::EndOfDocument => Self::end_of_document(style_kind),
			CommandName::FillPolygon => Self::read_fill_polygon(reader, header, style_kind),
			CommandName::FillRectangles => Self::read_fill_rectangles(reader, header, style_kind),
			CommandName::FillPath => Self::read_fill_path(reader, header, style_kind),
			CommandName::DrawLines => Self::read_draw_lines(reader, header, style_kind),
			CommandName::DrawLineLoop => Self::read_draw_line_loop(reader, header, style_kind),
			CommandName::DrawLineStrip => Self::read_draw_line_strip(reader, header, style_kind),
			CommandName::DrawLinePath => Self::read_draw_line_path(reader, header, style_kind),
			CommandName::OutlineFillPolygon => {
				Self::read_outline_fill_polygon(reader, header, style_kind)
			}
			CommandName::OutlineFillRectangles => {
				Self::read_outline_fill_rectangles(reader, header, style_kind)
			}
			CommandName::OutlineFillPath => {
				Self::read_outline_fill_path(reader, header, style_kind)
			}
		}
	}

	pub fn is_end_of_document(&self) -> bool {
		matches!(self, Self::EndOfDocument)
	}

	fn read_command_header(
		reader: &mut impl Read,
		header: &TvgHeader,
		style_kind: StyleKind,
	) -> io::Result<(u32, Style)> {
		// every command adds one to the count
		let count = read_varuint(reader)? + 1;
		let style = Style::read(reader, header, style_kind)?;

		Ok((count, style))
	}

	fn end_of_document(style_kind: StyleKind) -> Result<Self, TvgError> {
		if style_kind != StyleKind::FlatColored {
			Err(TvgError::InvalidEndOfDocument(style_kind as u8))
		} else {
			Ok(Self::EndOfDocument)
		}
	}

	fn read_fill_polygon(
		reader: &mut impl Read,
		header: &TvgHeader,
		style_kind: StyleKind,
	) -> Result<Self, TvgError> {
		let (point_count, fill_style) = Self::read_command_header(reader, header, style_kind)?;
		if point_count < 3 {
			yeet!(TvgError::InvalidPolygon(point_count));
		}

		let polygon = Point::read_multiple(reader, header, point_count)?;

		Ok(Self::FillPolygon {
			fill_style,
			polygon,
		})
	}

	fn read_fill_rectangles(
		reader: &mut impl Read,
		header: &TvgHeader,
		style_kind: StyleKind,
	) -> Result<Self, TvgError> {
		let (rectangle_count, fill_style) = Self::read_command_header(reader, header, style_kind)?;
		let rectangles = Rectangle::read_multiple(reader, header, rectangle_count)?;

		Ok(Self::FillRectangles {
			fill_style,
			rectangles,
		})
	}

	fn read_fill_path(
		reader: &mut impl Read,
		header: &TvgHeader,
		style_kind: StyleKind,
	) -> Result<Self, TvgError> {
		let (segment_count, fill_style) = Self::read_command_header(reader, header, style_kind)?;
		let path = Path::read(reader, header, segment_count)?;

		Ok(Self::FillPath { fill_style, path })
	}

	fn read_draw_lines(
		reader: &mut impl Read,
		header: &TvgHeader,
		style_kind: StyleKind,
	) -> Result<Self, TvgError> {
		let (line_count, line_style) = Self::read_command_header(reader, header, style_kind)?;
		let line_width = read_unit(reader, header)?;
		let lines = Line::read_multiple(reader, header, line_count)?;

		Ok(Self::DrawLines {
			line_style,
			line_width,
			lines,
		})
	}

	fn read_draw_line_loop(
		reader: &mut impl Read,
		header: &TvgHeader,
		style_kind: StyleKind,
	) -> Result<Self, TvgError> {
		let (point_count, line_style) = Self::read_command_header(reader, header, style_kind)?;
		let line_width = read_unit(reader, header)?;
		let points = Point::read_multiple(reader, header, point_count)?;

		Ok(Self::DrawLineLoop {
			line_style,
			line_width,
			points,
		})
	}

	fn read_draw_line_strip(
		reader: &mut impl Read,
		header: &TvgHeader,
		style_kind: StyleKind,
	) -> Result<Self, TvgError> {
		let (point_count, line_style) = Self::read_command_header(reader, header, style_kind)?;
		let line_width = read_unit(reader, header)?;
		let points = Point::read_multiple(reader, header, point_count)?;

		Ok(Self::DrawLineStrip {
			line_style,
			line_width,
			points,
		})
	}

	fn read_draw_line_path(
		reader: &mut impl Read,
		header: &TvgHeader,
		style_kind: StyleKind,
	) -> Result<Self, TvgError> {
		let (segment_count, line_style) = Self::read_command_header(reader, header, style_kind)?;
		let line_width = read_unit(reader, header)?;
		let path = Path::read(reader, header, segment_count)?;

		Ok(Self::DrawLinePath {
			line_style,
			line_width,
			path,
		})
	}

	fn read_outline_fill_polygon(
		reader: &mut impl Read,
		header: &TvgHeader,
		style_kind: StyleKind,
	) -> Result<Self, TvgError> {
		let OutlineHeader {
			count: segment_count,
			fill_style,
			line_style,
			line_width,
		} = OutlineHeader::read(reader, header, style_kind)?;

		let points = Point::read_multiple(reader, header, segment_count)?;

		Ok(Self::OutlineFillPolygon {
			fill_style,
			line_style,
			line_width,
			points,
		})
	}

	fn read_outline_fill_rectangles(
		reader: &mut impl Read,
		header: &TvgHeader,
		style_kind: StyleKind,
	) -> Result<Self, TvgError> {
		let OutlineHeader {
			count: rect_count,
			fill_style,
			line_style,
			line_width,
		} = OutlineHeader::read(reader, header, style_kind)?;

		let rectangles = Rectangle::read_multiple(reader, header, rect_count)?;

		Ok(Self::OutlineFillRectangles {
			fill_style,
			line_style,
			line_width,
			rectangles,
		})
	}

	fn read_outline_fill_path(
		reader: &mut impl Read,
		header: &TvgHeader,
		style_kind: StyleKind,
	) -> Result<Self, TvgError> {
		let OutlineHeader {
			count: segment_count,
			fill_style,
			line_style,
			line_width,
		} = OutlineHeader::read(reader, header, style_kind)?;

		let path = Path::read(reader, header, segment_count)?;

		Ok(Self::OutlineFillPath {
			fill_style,
			line_style,
			line_width,
			path,
		})
	}
}