use std::io::Read; use byteorder::ReadBytesExt; use num_enum::TryFromPrimitive; use raise::yeet; use crate::{header::TvgHeader, read_unit, read_varuint, Decode, Point, TvgError}; /// Returns an error if the padding isn't zero fn check_padding(padding: u8) -> Result<(), TvgError> { if padding != 0 { yeet!(TvgError::NonZeroPadding(padding)) } Ok(()) } #[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, ) -> 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) -> Result { Ok(Self::Line { position: Point::read(reader, header)?, }) } fn read_horizontal_line(reader: &mut impl Read, header: &TvgHeader) -> Result { Ok(Self::HorizontalLine { x: read_unit(reader, header)?, }) } fn read_vertical_line(reader: &mut impl Read, header: &TvgHeader) -> Result { Ok(Self::VerticalLine { y: read_unit(reader, header)?, }) } fn read_cubic_bezier(reader: &mut impl Read, header: &TvgHeader) -> 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, ) -> Result<(bool, Sweep), TvgError> { // 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, }; check_padding((byte & 0b1111_1100) >> 2)?; Ok((large_arc, sweep)) } fn read_arc_circle(reader: &mut impl Read, header: &TvgHeader) -> 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) -> 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) -> 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) -> 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; check_padding((byte & 0b0000_1000) >> 3)?; check_padding((byte & 0b1110_0000) >> 5)?; 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, ) -> 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, ) -> 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(), }) } }