summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMicha White <botahamec@outlook.com>2023-02-13 00:18:57 -0500
committerMicha White <botahamec@outlook.com>2023-02-13 00:18:57 -0500
commitc31d51487082c6cf243ecd10da71a15a78d41add (patch)
tree6d76b939ffca2e949208197da9b4ef7f31350515
parentb22ddd8da7075954b691903ef85f7c8f1e6b0811 (diff)
Parse TinyVG files
-rw-r--r--Cargo.toml4
-rw-r--r--alligator_tvg/Cargo.toml12
-rw-r--r--alligator_tvg/src/colors.rs333
-rw-r--r--alligator_tvg/src/commands.rs613
-rw-r--r--alligator_tvg/src/header.rs149
-rw-r--r--alligator_tvg/src/lib.rs151
-rw-r--r--alligator_tvg/src/path.rs294
-rw-r--r--alligator_tvg/tests/examples/tvg/everything-32.tvgbin0 -> 2637 bytes
-rw-r--r--alligator_tvg/tests/examples/tvg/everything.tvgbin0 -> 1447 bytes
-rw-r--r--alligator_tvg/tests/examples/tvg/shield-16.tvgbin0 -> 203 bytes
-rw-r--r--alligator_tvg/tests/examples/tvg/shield-32.tvgbin0 -> 371 bytes
-rw-r--r--alligator_tvg/tests/examples/tvg/shield-8.tvgbin0 -> 119 bytes
-rw-r--r--alligator_tvg/tests/parse.rs14
13 files changed, 1568 insertions, 2 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 3ffddd5..6ce8ec1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,5 +1,5 @@
[workspace]
-members = ["alligator_render", "alligator_resources"]
+members = ["alligator_render", "alligator_resources", "alligator_tvg"]
resolver = "2"
[package]
@@ -9,7 +9,7 @@ edition = "2021"
rust-version = "1.65"
[dependencies]
-alligator_render = {path = "alligator_render"}
+alligator_render = { path = "alligator_render" }
[lib]
crate-type = ["cdylib", "lib"]
diff --git a/alligator_tvg/Cargo.toml b/alligator_tvg/Cargo.toml
new file mode 100644
index 0000000..d7e7a6b
--- /dev/null
+++ b/alligator_tvg/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "alligator_tvg"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+byteorder = "1"
+thiserror = "1"
+raise = "2"
+num_enum = "0.5"
diff --git a/alligator_tvg/src/colors.rs b/alligator_tvg/src/colors.rs
new file mode 100644
index 0000000..10bc41c
--- /dev/null
+++ b/alligator_tvg/src/colors.rs
@@ -0,0 +1,333 @@
+use std::io::{self, Read};
+
+use byteorder::{BigEndian, LittleEndian, ReadBytesExt};
+use num_enum::TryFromPrimitive;
+
+/// The color table encodes the palette for this file.
+///
+/// It’s binary content is defined by the `color_encoding` field in the header.
+/// For the three defined color encodings, each will yield a list of
+/// `color_count` RGBA tuples.
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct ColorTable<C: Color> {
+ colors: Box<[C]>,
+}
+
+impl<C: Color> ColorTable<C> {
+ /// Read in one encoding, and convert it to another
+ pub fn read_from_encoding(
+ reader: &mut impl Read,
+ color_count: u32,
+ encoding: ColorEncoding,
+ ) -> io::Result<Self> {
+ Ok(match encoding {
+ ColorEncoding::Rgba8888 => (&ColorTable::<Rgba8888>::read(reader, color_count)?).into(),
+ ColorEncoding::Rgb565 => (&ColorTable::<Rgb565>::read(reader, color_count)?).into(),
+ ColorEncoding::RgbaF32 => (&ColorTable::<RgbaF32>::read(reader, color_count)?).into(),
+ ColorEncoding::Custom => (&ColorTable::<Rgba16>::read(reader, color_count)?).into(),
+ })
+ }
+
+ /// Parse a color table.
+ fn read(reader: &mut impl Read, color_count: u32) -> io::Result<Self> {
+ let mut colors = Vec::with_capacity(color_count as usize);
+ for _ in 0..color_count {
+ colors.push(C::parse_bytes(reader)?);
+ }
+
+ let colors = colors.into_boxed_slice();
+ Ok(Self { colors })
+ }
+
+ /// Returns the number of colors in the table.
+ fn len(&self) -> usize {
+ self.colors.len()
+ }
+
+ /// Returns a reference to a color, or `None` if out-of-bounds.
+ fn get(&self, index: usize) -> Option<&C> {
+ self.colors.get(index)
+ }
+
+ fn iter(&self) -> impl Iterator<Item = &C> {
+ self.colors.iter()
+ }
+}
+
+impl ColorTable<Rgba16> {}
+
+impl<Old: Color, New: Color> From<&ColorTable<Old>> for ColorTable<New> {
+ fn from(value: &ColorTable<Old>) -> Self {
+ let mut colors = Vec::with_capacity(value.len());
+
+ for color in value.iter() {
+ let r = color.red_u16();
+ let g = color.green_u16();
+ let b = color.blue_u16();
+ let a = color.alpha_u16();
+ colors.push(New::from_rgba16_lossy(r, g, b, a));
+ }
+
+ let colors = colors.into_boxed_slice();
+ Self { colors }
+ }
+}
+
+/// The color encoding defines which format the colors in the color table will
+/// have.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive)]
+#[repr(u8)]
+pub enum ColorEncoding {
+ /// Each color is a 4-tuple (red, green, blue, alpha) of bytes with the
+ /// color channels encoded in sRGB and the alpha as linear alpha.
+ Rgba8888 = 0,
+ /// Each color is encoded as a 3-tuple (red, green, blue) with 16 bits per
+ /// color.
+ ///
+ /// While red and blue both use 5 bits, the green channel uses 6 bits. Red
+ /// uses bit range 0...4, green bits 5...10 and blue bits 11...15. This
+ /// color also uses the sRGB color space.
+ Rgb565 = 1,
+ /// Each color is a 4-tuple (red, green, blue, alpha) of binary32 IEEE 754
+ /// floating point value with the color channels encoded in scRGB and the
+ /// alpha as linear alpha. A color value of 1.0 is full intensity, while a
+ /// value of 0.0 is zero intensity.
+ RgbaF32 = 2,
+ /// The custom color encoding is defined *undefined*. The information how
+ /// these colors are encoded must be implemented via external means.
+ Custom = 3,
+}
+
+pub trait Color: Sized {
+ /// The size of the color's representation in bits
+ const SIZE: usize;
+
+ /// Attempt to read the color. Returns `Err` if an error occurred while
+ /// attempting to read [`SIZE`] bytes from `bytes`.
+ fn parse_bytes(reader: &mut impl Read) -> io::Result<Self>;
+
+ /// Convert from the RGBA16 format to this format. This may be lossy.
+ fn from_rgba16_lossy(red: u16, green: u16, blue: u16, alpha: u16) -> Self;
+
+ fn red_u16(&self) -> u16;
+ fn blue_u16(&self) -> u16;
+ fn green_u16(&self) -> u16;
+ fn alpha_u16(&self) -> u16;
+}
+
+/// Each color value is encoded as a sequence of four bytes.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+struct Rgba8888 {
+ /// Red color channel between 0 and 100% intensity, mapped to byte values 0
+ /// to 255.
+ red: u8,
+ /// Green color channel between 0 and 100% intensity, mapped to byte values
+ /// 0 to 255.
+ green: u8,
+ /// Blue color channel between 0 and 100% intensity, mapped to byte values
+ /// 0 to 255.
+ blue: u8,
+ /// Transparency channel between 0 and 100% transparency, mapped to byte
+ /// values 0 to 255.
+ alpha: u8,
+}
+
+impl Color for Rgba8888 {
+ const SIZE: usize = 4;
+
+ fn parse_bytes(reader: &mut impl Read) -> io::Result<Self> {
+ Ok(Self {
+ red: reader.read_u8()?,
+ green: reader.read_u8()?,
+ blue: reader.read_u8()?,
+ alpha: reader.read_u8()?,
+ })
+ }
+
+ fn from_rgba16_lossy(red: u16, green: u16, blue: u16, alpha: u16) -> Self {
+ Self {
+ red: (red >> 8) as u8,
+ green: (green >> 8) as u8,
+ blue: (blue >> 8) as u8,
+ alpha: (alpha >> 8) as u8,
+ }
+ }
+
+ fn red_u16(&self) -> u16 {
+ (self.red as u16) << 8
+ }
+
+ fn green_u16(&self) -> u16 {
+ (self.green as u16) << 8
+ }
+
+ fn blue_u16(&self) -> u16 {
+ (self.blue as u16) << 8
+ }
+
+ fn alpha_u16(&self) -> u16 {
+ (self.alpha as u16) << 8
+ }
+}
+
+/// Each color value is encoded as a sequence of 2 bytes.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+struct Rgb565 {
+ /// Red color channel between 0 and 100% intensity, mapped to integer
+ /// values 0 to 31.
+ red: u8,
+ /// Green color channel between 0 and 100% intensity, mapped to integer
+ /// values 0 to 63.
+ green: u8,
+ /// Blue color channel between 0 and 100% intensity, mapped to integer
+ /// values 0 to 31.
+ blue: u8,
+}
+
+impl Color for Rgb565 {
+ const SIZE: usize = 2;
+
+ fn parse_bytes(reader: &mut impl Read) -> io::Result<Self> {
+ let color = reader.read_u16::<LittleEndian>()?;
+
+ let red = ((color & 0x001F) << 3) as u8;
+ let green = ((color & 0x07E0) >> 3) as u8;
+ let blue = ((color & 0xF800) >> 8) as u8;
+
+ Ok(Self { red, blue, green })
+ }
+
+ fn from_rgba16_lossy(red: u16, green: u16, blue: u16, _a: u16) -> Self {
+ Self {
+ red: (red >> 11) as u8,
+ green: (green >> 10) as u8,
+ blue: (blue >> 11) as u8,
+ }
+ }
+
+ fn red_u16(&self) -> u16 {
+ (self.red as u16) << 11
+ }
+
+ fn green_u16(&self) -> u16 {
+ (self.green as u16) << 11
+ }
+
+ fn blue_u16(&self) -> u16 {
+ (self.blue as u16) << 10
+ }
+
+ fn alpha_u16(&self) -> u16 {
+ 0
+ }
+}
+
+/// Each color value is encoded as a sequence of 16 bytes.
+#[derive(Debug, Clone, Copy, PartialEq)]
+struct RgbaF32 {
+ /// Red color channel, using 0.0 for 0% intensity and 1.0 for 100%
+ /// intensity.
+ red: f32,
+ /// Green color channel, using 0.0 for 0% intensity and 1.0 for 100%
+ /// intensity.
+ green: f32,
+ /// Blue color channel, using 0.0 for 0% intensity and 1.0 for 100%
+ /// intensity.
+ blue: f32,
+ /// Transparency channel between 0 and 100% transparency, mapped to byte
+ /// values 0.0 to 1.0.
+ alpha: f32,
+}
+
+impl Color for RgbaF32 {
+ const SIZE: usize = 16;
+
+ fn parse_bytes(reader: &mut impl Read) -> io::Result<Self> {
+ Ok(Self {
+ red: reader.read_f32::<LittleEndian>()?,
+ green: reader.read_f32::<LittleEndian>()?,
+ blue: reader.read_f32::<LittleEndian>()?,
+ alpha: reader.read_f32::<LittleEndian>()?,
+ })
+ }
+
+ fn from_rgba16_lossy(red: u16, green: u16, blue: u16, alpha: u16) -> Self {
+ Self {
+ red: (red as f32) / (u16::MAX as f32),
+ green: (green as f32) / (u16::MAX as f32),
+ blue: (blue as f32) / (u16::MAX as f32),
+ alpha: (alpha as f32) / (u16::MAX as f32),
+ }
+ }
+
+ fn red_u16(&self) -> u16 {
+ (self.red * (u16::MAX as f32)) as u16
+ }
+
+ fn green_u16(&self) -> u16 {
+ (self.green * (u16::MAX as f32)) as u16
+ }
+
+ fn blue_u16(&self) -> u16 {
+ (self.blue * (u16::MAX as f32)) as u16
+ }
+
+ fn alpha_u16(&self) -> u16 {
+ (self.alpha * (u16::MAX as f32)) as u16
+ }
+}
+
+/// Each color value is encoded as a sequence of 8 bytes.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct Rgba16 {
+ /// Red color channel between 0 and 100% intensity, mapped to a big-endian
+ /// 16-bit integer.
+ red: u16,
+ /// Green color channel between 0 and 100% intensity, mapped to a
+ /// big-endian 16-bit integer.
+ green: u16,
+ /// Blue color channel between 0 and 100% intensity, mapped to a big-endian
+ /// 16-bit integer.
+ blue: u16,
+ /// Transparency channel between 0 and 100% intensity, mapped to a
+ /// big-endian 16-bit integer.
+ alpha: u16,
+}
+
+impl Color for Rgba16 {
+ const SIZE: usize = 8;
+
+ fn parse_bytes(reader: &mut impl Read) -> io::Result<Self> {
+ Ok(Self {
+ red: reader.read_u16::<BigEndian>()?,
+ green: reader.read_u16::<BigEndian>()?,
+ blue: reader.read_u16::<BigEndian>()?,
+ alpha: reader.read_u16::<BigEndian>()?,
+ })
+ }
+
+ fn from_rgba16_lossy(red: u16, green: u16, blue: u16, alpha: u16) -> Self {
+ Self {
+ red,
+ green,
+ blue,
+ alpha,
+ }
+ }
+
+ fn red_u16(&self) -> u16 {
+ self.red
+ }
+
+ fn green_u16(&self) -> u16 {
+ self.green
+ }
+
+ fn blue_u16(&self) -> u16 {
+ self.blue
+ }
+
+ fn alpha_u16(&self) -> u16 {
+ self.alpha
+ }
+}
diff --git a/alligator_tvg/src/commands.rs b/alligator_tvg/src/commands.rs
new file mode 100644
index 0000000..f316a53
--- /dev/null
+++ b/alligator_tvg/src/commands.rs
@@ -0,0 +1,613 @@
+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,
+ })
+ }
+}
diff --git a/alligator_tvg/src/header.rs b/alligator_tvg/src/header.rs
new file mode 100644
index 0000000..b3be494
--- /dev/null
+++ b/alligator_tvg/src/header.rs
@@ -0,0 +1,149 @@
+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
+ }
+}
+
+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>()?,
+ })
+}
diff --git a/alligator_tvg/src/lib.rs b/alligator_tvg/src/lib.rs
new file mode 100644
index 0000000..5cbe33c
--- /dev/null
+++ b/alligator_tvg/src/lib.rs
@@ -0,0 +1,151 @@
+use std::io::{self, Read};
+
+use byteorder::{LittleEndian, ReadBytesExt};
+use colors::{Color, ColorTable};
+use commands::Command;
+use header::{CoordinateRange, Scale, TvgHeader};
+use thiserror::Error;
+
+mod colors;
+mod commands;
+mod header;
+mod path;
+
+pub use colors::Rgba16;
+pub use header::SUPPORTED_VERSION;
+
+pub struct TvgFile<C: Color> {
+ header: TvgHeader,
+ color_table: ColorTable<C>,
+ commands: Box<[Command]>,
+}
+
+impl<C: Color + std::fmt::Debug> TvgFile<C> {
+ 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(),
+ })
+ }
+}
+
+#[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 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("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)
+}
diff --git a/alligator_tvg/src/path.rs b/alligator_tvg/src/path.rs
new file mode 100644
index 0000000..d2bf4fb
--- /dev/null
+++ b/alligator_tvg/src/path.rs
@@ -0,0 +1,294 @@
+use std::io::{self, Read};
+
+use byteorder::ReadBytesExt;
+use num_enum::TryFromPrimitive;
+
+use crate::{header::TvgHeader, read_unit, read_varuint, Decode, Point};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive)]
+#[repr(u8)]
+enum Sweep {
+ Right = 0,
+ Left = 1,
+}
+
+/// An instruction to move a hypothetical "pen".
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive)]
+#[repr(u8)]
+enum InstructionKind {
+ /// A straight line is drawn from the current point to a new point.
+ Line = 0,
+ /// A straight horizontal line is drawn from the current point to a new x
+ /// coordinate.
+ HorizontalLine = 1,
+ /// A straight vertical line is drawn from the current point to a new y
+ /// coordiante.
+ VerticalLine = 2,
+ /// A cubic Bézier curve is drawn from the current point to a new point.
+ CubicBezier = 3,
+ /// A circle segment is drawn from current point to a new point.
+ ArcCircle = 4,
+ /// An ellipse segment is drawn from current point to a new point.
+ ArcEllipse = 5,
+ /// The path is closed, and a straight line is drawn to the starting point.
+ ClosePath = 6,
+ /// A quadratic Bézier curve is drawn from the current point to a new point.
+ QuadraticBezier = 7,
+}
+
+#[derive(Debug, Clone, Copy)]
+enum InstructionData {
+ /// The line instruction draws a straight line to the position.
+ Line {
+ /// The end point of the line.
+ position: Point,
+ },
+ /// The horizontal line instruction draws a straight horizontal line to a
+ /// given x coordinate.
+ HorizontalLine {
+ /// The new x coordinate.
+ x: f64,
+ },
+ /// The vertical line instruction draws a straight vertical line to a given
+ /// y coordinate.
+ VerticalLine {
+ /// The new y coordinate.
+ y: f64,
+ },
+ /// The cubic bezier instruction draws a Bézier curve with two control
+ /// points.
+ ///
+ /// The curve is drawn between the current location and `point_1` with
+ /// `control_0` being the first control point and `control_1` being the
+ /// second one.
+ CubicBezier {
+ /// The first control point.
+ control_0: Point,
+ /// The second control point.
+ control_1: Point,
+ /// The end point of the Bézier curve.
+ point_1: Point,
+ },
+ /// Draws a circle segment between the current and the target point.
+ ///
+ /// The `radius` field determines the radius of the circle. If the distance
+ /// between the current point and `target` is larger than `radius`, the
+ /// distance is used as the radius.
+ ArcCircle {
+ /// If `true`, the large portion of the circle segment is drawn.
+ large_arc: bool,
+ /// Determines if the circle segment is left- or right bending.
+ sweep: Sweep,
+ /// The radius of the circle.
+ radius: f64,
+ /// The end point of the circle segment.
+ target: Point,
+ },
+ /// Draws an ellipse segment between the current and the target point.
+ ///
+ /// The `radius_x` and `radius_y` fields determine the both radii of the
+ /// ellipse. If the distance between the current point and target is not
+ /// enough to fit any ellipse segment between the two points, `radius_x`
+ /// and `radius_y` are scaled uniformly so that it fits exactly.
+ ArcEllipse {
+ /// If `true`, the large portion of the ellipse segment is drawn.
+ large_arc: bool,
+ /// Determines if the ellipse segment is left- or right bending.
+ sweep: Sweep,
+ /// The radius of the ellipse segment in the horizontal direction.
+ radius_x: f64,
+ /// The radius of the ellipse segment in the vertical direction.
+ radius_y: f64,
+ /// The rotation of the ellipse in mathematical negative direction, in
+ /// degrees.
+ rotation: f64,
+ /// The end point of the ellipse segment.
+ target: Point,
+ },
+ /// A straight line is drawn to the start location of the current segment.
+ /// This instruction doesn’t have additional data encoded.
+ ClosePath,
+ /// The quadratic bezier instruction draws a Bézier curve with a single
+ /// control point.
+ ///
+ /// The curve is drawn between the current location and `point_1` with
+ /// control being the control point.
+ QuadraticBezier {
+ /// The control point.
+ control: Point,
+ /// The end point of the Bézier curve.
+ target: Point,
+ },
+}
+
+impl InstructionData {
+ fn read(reader: &mut impl Read, header: &TvgHeader, kind: InstructionKind) -> io::Result<Self> {
+ match kind {
+ InstructionKind::Line => Self::read_line(reader, header),
+ InstructionKind::HorizontalLine => Self::read_horizontal_line(reader, header),
+ InstructionKind::VerticalLine => Self::read_vertical_line(reader, header),
+ InstructionKind::CubicBezier => Self::read_cubic_bezier(reader, header),
+ InstructionKind::ArcCircle => Self::read_arc_circle(reader, header),
+ InstructionKind::ArcEllipse => Self::read_arc_ellipse(reader, header),
+ InstructionKind::ClosePath => Ok(Self::ClosePath),
+ InstructionKind::QuadraticBezier => Self::read_quadratic_bezier(reader, header),
+ }
+ }
+
+ fn read_line(reader: &mut impl Read, header: &TvgHeader) -> io::Result<Self> {
+ Ok(Self::Line {
+ position: Point::read(reader, header)?,
+ })
+ }
+
+ fn read_horizontal_line(reader: &mut impl Read, header: &TvgHeader) -> io::Result<Self> {
+ Ok(Self::HorizontalLine {
+ x: read_unit(reader, header)?,
+ })
+ }
+
+ fn read_vertical_line(reader: &mut impl Read, header: &TvgHeader) -> io::Result<Self> {
+ Ok(Self::VerticalLine {
+ y: read_unit(reader, header)?,
+ })
+ }
+
+ fn read_cubic_bezier(reader: &mut impl Read, header: &TvgHeader) -> io::Result<Self> {
+ Ok(Self::CubicBezier {
+ control_0: Point::read(reader, header)?,
+ control_1: Point::read(reader, header)?,
+ point_1: Point::read(reader, header)?,
+ })
+ }
+
+ fn read_arc_header(reader: &mut impl Read, header: &TvgHeader) -> io::Result<(bool, Sweep)> {
+ // large_arc and sweep are stored in the same byte
+ let byte = reader.read_u8()?;
+ let large_arc = (byte & 1) != 0;
+ let sweep = match byte & 2 {
+ 0 => Sweep::Left,
+ _ => Sweep::Right,
+ };
+
+ Ok((large_arc, sweep))
+ }
+
+ fn read_arc_circle(reader: &mut impl Read, header: &TvgHeader) -> io::Result<Self> {
+ let (large_arc, sweep) = Self::read_arc_header(reader, header)?;
+ let radius = read_unit(reader, header)?;
+ let target = Point::read(reader, header)?;
+
+ Ok(Self::ArcCircle {
+ large_arc,
+ sweep,
+ radius,
+ target,
+ })
+ }
+
+ fn read_arc_ellipse(reader: &mut impl Read, header: &TvgHeader) -> io::Result<Self> {
+ let (large_arc, sweep) = Self::read_arc_header(reader, header)?;
+ let radius_x = read_unit(reader, header)?;
+ let radius_y = read_unit(reader, header)?;
+ let rotation = read_unit(reader, header)?;
+ let target = Point::read(reader, header)?;
+
+ Ok(Self::ArcEllipse {
+ large_arc,
+ sweep,
+ radius_x,
+ radius_y,
+ rotation,
+ target,
+ })
+ }
+
+ fn read_quadratic_bezier(reader: &mut impl Read, header: &TvgHeader) -> io::Result<Self> {
+ Ok(Self::QuadraticBezier {
+ control: Point::read(reader, header)?,
+ target: Point::read(reader, header)?,
+ })
+ }
+}
+
+#[derive(Debug, Clone)]
+struct Instruction {
+ /// The width of the line the "pen" makes, if it makes one at all.
+ line_width: Option<f64>,
+ /// The arguments to the instruction.
+ data: InstructionData,
+}
+
+impl Instruction {
+ fn read(reader: &mut impl Read, header: &TvgHeader) -> io::Result<Self> {
+ let byte = reader.read_u8()?;
+ let instruction_kind =
+ InstructionKind::try_from_primitive(byte & 0b0000_0111).expect("invalid instruction");
+ let has_line_width = (byte & 0b0001_0000) != 0;
+
+ let line_width = has_line_width
+ .then(|| read_unit(reader, header))
+ .transpose()?;
+ let data = InstructionData::read(reader, header, instruction_kind)?;
+
+ Ok(Self { line_width, data })
+ }
+}
+
+#[derive(Debug, Clone)]
+struct Segment {
+ /// The starting point of the segment.
+ start: Point,
+ /// The list of instructions for tha segment.
+ instructions: Box<[Instruction]>,
+}
+
+impl Segment {
+ fn read(reader: &mut impl Read, header: &TvgHeader, segment_length: u32) -> io::Result<Self> {
+ let start = Point::read(reader, header)?;
+
+ let mut instructions = Vec::with_capacity(segment_length as usize);
+ for _ in 0..segment_length {
+ instructions.push(Instruction::read(reader, header)?)
+ }
+
+ Ok(Segment {
+ start,
+ instructions: instructions.into_boxed_slice(),
+ })
+ }
+}
+
+/// Paths describe instructions to create complex 2D graphics.
+///
+/// Each path segment generates a shape by moving a ”pen” around. The path this
+/// ”pen” takes is the outline of our segment. Each segment, the ”pen” starts
+/// at a defined position and is moved by instructions. Each instruction will
+/// leave the ”pen” at a new position. The line drawn by our ”pen” is the
+/// outline of the shape.
+#[derive(Debug, Clone)]
+pub struct Path {
+ segments: Box<[Segment]>,
+}
+
+impl Path {
+ pub(crate) fn read(
+ reader: &mut impl Read,
+ header: &TvgHeader,
+ segment_count: u32,
+ ) -> io::Result<Self> {
+ let mut segment_lengths = Vec::with_capacity(segment_count as usize);
+ for _ in 0..segment_count {
+ segment_lengths.push(read_varuint(reader)? + 1);
+ }
+
+ let mut segments = Vec::with_capacity(segment_count as usize);
+ for segment_length in segment_lengths {
+ segments.push(Segment::read(reader, header, segment_length)?);
+ }
+
+ Ok(Self {
+ segments: segments.into_boxed_slice(),
+ })
+ }
+}
diff --git a/alligator_tvg/tests/examples/tvg/everything-32.tvg b/alligator_tvg/tests/examples/tvg/everything-32.tvg
new file mode 100644
index 0000000..7ea4bdd
--- /dev/null
+++ b/alligator_tvg/tests/examples/tvg/everything-32.tvg
Binary files differ
diff --git a/alligator_tvg/tests/examples/tvg/everything.tvg b/alligator_tvg/tests/examples/tvg/everything.tvg
new file mode 100644
index 0000000..b211c53
--- /dev/null
+++ b/alligator_tvg/tests/examples/tvg/everything.tvg
Binary files differ
diff --git a/alligator_tvg/tests/examples/tvg/shield-16.tvg b/alligator_tvg/tests/examples/tvg/shield-16.tvg
new file mode 100644
index 0000000..aacd3ea
--- /dev/null
+++ b/alligator_tvg/tests/examples/tvg/shield-16.tvg
Binary files differ
diff --git a/alligator_tvg/tests/examples/tvg/shield-32.tvg b/alligator_tvg/tests/examples/tvg/shield-32.tvg
new file mode 100644
index 0000000..a2abc92
--- /dev/null
+++ b/alligator_tvg/tests/examples/tvg/shield-32.tvg
Binary files differ
diff --git a/alligator_tvg/tests/examples/tvg/shield-8.tvg b/alligator_tvg/tests/examples/tvg/shield-8.tvg
new file mode 100644
index 0000000..57033be
--- /dev/null
+++ b/alligator_tvg/tests/examples/tvg/shield-8.tvg
Binary files differ
diff --git a/alligator_tvg/tests/parse.rs b/alligator_tvg/tests/parse.rs
new file mode 100644
index 0000000..eb6e801
--- /dev/null
+++ b/alligator_tvg/tests/parse.rs
@@ -0,0 +1,14 @@
+use std::fs::File;
+
+use alligator_tvg::{Rgba16, TvgFile};
+
+#[test]
+fn main() {
+ for entry in std::fs::read_dir("tests/examples/tvg").unwrap() {
+ let entry = entry.unwrap().file_name();
+ let entry = entry.to_string_lossy();
+ let mut file = File::open(format!("./tests/examples/tvg/{}", &entry)).unwrap();
+ let _: TvgFile<Rgba16> = TvgFile::read_from(&mut file).unwrap();
+ println!("{:?} succeeded!", entry);
+ }
+}