diff options
Diffstat (limited to 'tvg/src')
| -rw-r--r-- | tvg/src/colors.rs | 333 | ||||
| -rw-r--r-- | tvg/src/commands.rs | 613 | ||||
| -rw-r--r-- | tvg/src/header.rs | 149 | ||||
| -rw-r--r-- | tvg/src/lib.rs | 151 | ||||
| -rw-r--r-- | tvg/src/path.rs | 294 |
5 files changed, 1540 insertions, 0 deletions
diff --git a/tvg/src/colors.rs b/tvg/src/colors.rs new file mode 100644 index 0000000..10bc41c --- /dev/null +++ b/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/tvg/src/commands.rs b/tvg/src/commands.rs new file mode 100644 index 0000000..f316a53 --- /dev/null +++ b/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/tvg/src/header.rs b/tvg/src/header.rs new file mode 100644 index 0000000..b3be494 --- /dev/null +++ b/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/tvg/src/lib.rs b/tvg/src/lib.rs new file mode 100644 index 0000000..5cbe33c --- /dev/null +++ b/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/tvg/src/path.rs b/tvg/src/path.rs new file mode 100644 index 0000000..d2bf4fb --- /dev/null +++ b/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(), + }) + } +} |
