From c31d51487082c6cf243ecd10da71a15a78d41add Mon Sep 17 00:00:00 2001 From: Micha White Date: Mon, 13 Feb 2023 00:18:57 -0500 Subject: Parse TinyVG files --- alligator_tvg/src/path.rs | 294 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 alligator_tvg/src/path.rs (limited to 'alligator_tvg/src/path.rs') 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 { + 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 { + Ok(Self::Line { + position: Point::read(reader, header)?, + }) + } + + fn read_horizontal_line(reader: &mut impl Read, header: &TvgHeader) -> io::Result { + Ok(Self::HorizontalLine { + x: read_unit(reader, header)?, + }) + } + + fn read_vertical_line(reader: &mut impl Read, header: &TvgHeader) -> io::Result { + Ok(Self::VerticalLine { + y: read_unit(reader, header)?, + }) + } + + fn read_cubic_bezier(reader: &mut impl Read, header: &TvgHeader) -> io::Result { + 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 { + 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 { + 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 { + 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, + /// The arguments to the instruction. + data: InstructionData, +} + +impl Instruction { + fn read(reader: &mut impl Read, header: &TvgHeader) -> io::Result { + 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 { + 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 { + 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(), + }) + } +} -- cgit v1.2.3