hvif 0.1.1

Parser and renderer for HVIF (Haiku Vector Icon Format)
Documentation
// Copyright (c) 2021 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

//! Describes how a path is formed.

use crate::{Coord, Point};
use bitflags::bitflags;
use nom::{multi::length_count, number::complete::u8, IResult};

bitflags! {
    /// Flags affecting a Path.
    pub struct PathFlags: u8 {
        /// The last and first points of this Path are connected.
        const CLOSED = 0b00000010;

        /// This path uses commands, this is overriden by PathFlags::NO_CURVES if it is also set.
        const USES_COMMANDS = 0b00000100;

        /// This path contains only straight lines, this overrides PathFlags::USES_COMMANDS if it
        /// is also set.
        const NO_CURVES = 0b00001000;
    }
}

impl PathFlags {
    fn parse(i: &[u8]) -> IResult<&[u8], PathFlags> {
        let (i, flags) = u8(i)?;
        Ok((i, PathFlags::from_bits_truncate(flags)))
    }
}

/// A curve in a Path.
#[derive(Debug, Clone, PartialEq)]
pub struct Curve {
    /// The point at which to end up.
    pub point: Point,

    /// The control point before this point.
    pub point_in: Point,

    /// The control point after this point.
    pub point_out: Point,
}

impl Curve {
    fn parse(i: &[u8]) -> IResult<&[u8], Curve> {
        let (i, point) = Point::parse(i)?;
        let (i, point_in) = Point::parse(i)?;
        let (i, point_out) = Point::parse(i)?;
        Ok((
            i,
            Curve {
                point,
                point_in,
                point_out,
            },
        ))
    }
}

enum PathCommand {
    HLine = 0b00,
    VLine = 0b01,
    Line = 0b10,
    Curve = 0b11,
}

impl From<u8> for PathCommand {
    fn from(bits: u8) -> PathCommand {
        use PathCommand::*;
        match bits {
            0b00 => HLine,
            0b01 => VLine,
            0b10 => Line,
            0b11 => Curve,
            _ => unreachable!("Only two bits must be passed to this function."),
        }
    }
}

impl PathCommand {
    fn parse_list(i: &[u8]) -> IResult<&[u8], Vec<PathCommand>> {
        let (i, mut num_points) = u8(i)?;
        let (mut i, mut buf) = u8(i)?;
        let mut count = 4;
        let mut commands = Vec::new();
        loop {
            if num_points == 0 {
                break;
            }
            let command = PathCommand::from(buf & 0x03);
            commands.push(command);
            buf = buf >> 2;
            count -= 1;
            num_points -= 1;
            if count == 0 && num_points != 0 {
                let next = u8(i)?;
                i = next.0;
                buf = next.1;
                count = 4;
            }
        }
        Ok((i, commands))
    }
}

/// One command in a commands path.
#[derive(Debug, Clone, PartialEq)]
pub enum Command {
    /// A horizontal straight line.
    HLine(Coord),

    /// A vertical straight line.
    VLine(Coord),

    /// A straight line.
    Line(Point),

    /// A curve.
    Curve(Curve),
}

/// The content of a Path.
#[derive(Debug, Clone, PartialEq)]
pub enum PathContent {
    /// This Path contains only straight lines.
    Lines(Vec<Point>),

    /// This Path contains only curves.
    Curves(Vec<Curve>),

    /// This Path contains commands.
    Commands(Vec<Command>),
}

/// The base struct of a path.
#[derive(Debug, Clone, PartialEq)]
pub struct Path {
    /// The flags applied to this Path.
    pub flags: PathFlags,

    /// The contents of this Path.
    pub content: PathContent,
}

impl Path {
    /// Parse a Path from its HVIF serialisation.
    pub fn parse(i: &[u8]) -> IResult<&[u8], Path> {
        let (i, flags) = PathFlags::parse(i)?;
        let (i, content) = if flags.contains(PathFlags::NO_CURVES) {
            let (i, points) = length_count(u8, Point::parse)(i)?;
            (i, PathContent::Lines(points))
        } else if flags.contains(PathFlags::USES_COMMANDS) {
            let (mut i, command_list) = PathCommand::parse_list(i)?;
            let mut commands = Vec::new();
            for command in command_list {
                let (i2, command) = match command {
                    PathCommand::HLine => {
                        let (i, coord) = Coord::parse(i)?;
                        (i, Command::HLine(coord))
                    }
                    PathCommand::VLine => {
                        let (i, coord) = Coord::parse(i)?;
                        (i, Command::VLine(coord))
                    }
                    PathCommand::Line => {
                        let (i, point) = Point::parse(i)?;
                        (i, Command::Line(point))
                    }
                    PathCommand::Curve => {
                        let (i, curve) = Curve::parse(i)?;
                        (i, Command::Curve(curve))
                    }
                };
                commands.push(command);
                i = i2;
            }
            (i, PathContent::Commands(commands))
        } else {
            let (i, points) = length_count(u8, Curve::parse)(i)?;
            (i, PathContent::Curves(points))
        };
        Ok((i, Path { flags, content }))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    macro_rules! assert_size (
        ($t:ty, $sz:expr) => (
            assert_eq!(::std::mem::size_of::<$t>(), $sz);
        );
    );

    #[test]
    fn sizes() {
        assert_size!(Curve, 24);
        assert_size!(PathFlags, 1);
        assert_size!(Command, 28);
        assert_size!(PathContent, 32);
        assert_size!(Path, 40);
    }

    #[test]
    fn empty() {
        let empty = b"\x0a\x00";
        let (i, path) = Path::parse(empty).unwrap();
        assert!(i.is_empty());
        assert_eq!(path.flags, PathFlags::NO_CURVES | PathFlags::CLOSED);
    }

    #[test]
    fn lines() {
        let data = b"\x08\x02\x20\x20\x60\x60";
        let (i, path) = Path::parse(data).unwrap();
        assert!(i.is_empty());
        assert_eq!(path.flags, PathFlags::NO_CURVES);
        if let PathContent::Lines(points) = path.content {
            assert_eq!(points.len(), 2);
            assert_eq!(
                points[0],
                Point {
                    x: Coord(0.),
                    y: Coord(0.)
                }
            );
            assert_eq!(
                points[1],
                Point {
                    x: Coord(64.),
                    y: Coord(64.)
                }
            );
        } else {
            panic!("Parsed path doesn’t contain lines.");
        }
    }

    #[test]
    fn commands() {
        let data = b"\x06\x04\xd2\x20\x20\x60\x60\x40\x40\x20\x40\x60\x40";
        let (i, path) = Path::parse(data).unwrap();
        assert!(i.is_empty());
        assert_eq!(path.flags, PathFlags::USES_COMMANDS | PathFlags::CLOSED);
        if let PathContent::Commands(commands) = path.content {
            assert_eq!(commands.len(), 4);
            assert_eq!(
                commands[0],
                Command::Line(Point {
                    x: Coord(0.),
                    y: Coord(0.)
                })
            );
            assert_eq!(commands[1], Command::HLine(Coord(64.)));
            assert_eq!(commands[2], Command::VLine(Coord(64.)));
            assert_eq!(
                commands[3],
                Command::Curve(Curve {
                    point: Point {
                        x: Coord(32.),
                        y: Coord(32.)
                    },
                    point_in: Point {
                        x: Coord(0.),
                        y: Coord(32.)
                    },
                    point_out: Point {
                        x: Coord(64.),
                        y: Coord(32.)
                    },
                })
            );
        } else {
            panic!("Parsed path doesn’t contain commands.");
        }
    }
}