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/.

use crate::{
    path::{Command, Curve, PathContent, PathFlags},
    shape::{LineCap, LineJoin, Transformer},
    style::{GradientType, Style},
    Hvif, Point, Transform,
};

trait CairoContextExt {
    fn curve_to_point(&self, point_in: &Point, point_out: &Point, point: &Point);
}

impl CairoContextExt for cairo::Context {
    fn curve_to_point(&self, point_in: &Point, point_out: &Point, point: &Point) {
        self.curve_to(
            point_in.x.to_f64(),
            point_in.y.to_f64(),
            point_out.x.to_f64(),
            point_out.y.to_f64(),
            point.x.to_f64(),
            point.y.to_f64(),
        );
    }
}

fn transform_to_cairo(transform: &Option<Transform>) -> cairo::Matrix {
    if let Some(transform) = transform {
        cairo::Matrix::new(
            transform[0].to_f64(),
            transform[1].to_f64(),
            transform[2].to_f64(),
            transform[3].to_f64(),
            transform[4].to_f64(),
            transform[5].to_f64(),
        )
    } else {
        cairo::Matrix::identity()
    }
}

fn style_colour_to_cairo(ctx: &cairo::Context, style: &Style) {
    match style {
        Style::SolidColour(colour) => {
            if colour.is_opaque() {
                let (r, g, b) = colour.to_rgb();
                let r = (r as f64) / 255.;
                let g = (g as f64) / 255.;
                let b = (b as f64) / 255.;
                ctx.set_source_rgb(r, g, b);
            } else {
                let (r, g, b, a) = colour.to_rgba();
                let r = (r as f64) / 255.;
                let g = (g as f64) / 255.;
                let b = (b as f64) / 255.;
                let a = (a as f64) / 255.;
                ctx.set_source_rgba(r, g, b, a);
            }
        }
        Style::Gradient(ref g) => match g.type_ {
            GradientType::Linear => {
                let gradient = cairo::LinearGradient::new(-64., 0., 64., 0.);
                for stop in g.stops.iter() {
                    let offset = (stop.offset as f64) / 255.;
                    let (r, g, b, a) = stop.colour.to_rgba();
                    let r = (r as f64) / 255.;
                    let g = (g as f64) / 255.;
                    let b = (b as f64) / 255.;
                    let a = (a as f64) / 255.;
                    gradient.add_color_stop_rgba(offset, r, g, b, a);
                }
                let mut matrix = transform_to_cairo(&g.transform);
                matrix.invert();
                gradient.set_matrix(matrix);
                ctx.set_source(&gradient).unwrap();
            }
            GradientType::Circular => {
                let gradient = cairo::RadialGradient::new(0., 0., 0., 0., 0., 64.);
                for stop in g.stops.iter() {
                    let offset = (stop.offset as f64) / 255.;
                    let (r, g, b, a) = stop.colour.to_rgba();
                    let r = (r as f64) / 255.;
                    let g = (g as f64) / 255.;
                    let b = (b as f64) / 255.;
                    let a = (a as f64) / 255.;
                    gradient.add_color_stop_rgba(offset, r, g, b, a);
                }
                let mut matrix = transform_to_cairo(&g.transform);
                matrix.invert();
                gradient.set_matrix(matrix);
                ctx.set_source(&gradient).unwrap();
            }
            type_ => todo!("Unimplemented gradient type: {:?}", type_),
        },
    }
}

/// Render a parsed HVIF file into a Cairo surface.
pub fn render(hvif: Hvif, surface: &mut cairo::ImageSurface) -> Result<(), cairo::Error> {
    surface.set_device_scale((surface.width() as f64) / 64., (surface.height() as f64) / 64.);
    let ctx = cairo::Context::new(&surface)?;
    for shape in hvif.shapes {
        let style = &hvif.styles[shape.style as usize];
        style_colour_to_cairo(&ctx, style);
        let mut use_fill = true;
        for transformer in shape.transformers {
            match transformer {
                Transformer::Stroke {
                    width,
                    line_join,
                    line_cap,
                    miter_limit,
                } => {
                    ctx.set_line_width(width as f64);
                    ctx.set_line_join(match line_join {
                        LineJoin::MiterJoin => cairo::LineJoin::Miter,
                        LineJoin::MiterJoinRevert => cairo::LineJoin::Miter,
                        LineJoin::RoundJoin => cairo::LineJoin::Round,
                        LineJoin::BevelJoin => cairo::LineJoin::Bevel,
                        LineJoin::MiterJoinRound => cairo::LineJoin::Round,
                    });
                    ctx.set_line_cap(match line_cap {
                        LineCap::Butt => cairo::LineCap::Butt,
                        LineCap::Square => cairo::LineCap::Square,
                        LineCap::Round => cairo::LineCap::Round,
                    });
                    ctx.set_miter_limit(miter_limit as f64);
                    use_fill = false;
                }
                _ => {
                    ctx.set_line_width(0.);
                    ctx.set_miter_limit(0.);
                }
            }
        }
        let matrix = transform_to_cairo(&shape.transform);
        ctx.set_matrix(matrix);
        for &path in shape.paths.iter() {
            let path = &hvif.paths[path as usize];
            if let PathContent::Lines(ref points) = path.content {
                for (i, p) in points.iter().enumerate() {
                    if i == 0 {
                        ctx.move_to(p.x.to_f64(), p.y.to_f64());
                    } else {
                        ctx.line_to(p.x.to_f64(), p.y.to_f64());
                    }
                }
            } else if let PathContent::Curves(ref curves) = path.content {
                let mut iter = curves.iter();
                let first;
                let first_in;
                let mut prev_out;
                if let Some(curve) = iter.next() {
                    ctx.move_to(curve.point.x.to_f64(), curve.point.y.to_f64());
                    first = curve.point.clone();
                    first_in = curve.point_in.clone();
                    prev_out = curve.point_out.clone();
                } else {
                    unreachable!();
                }
                for curve in iter {
                    ctx.curve_to_point(&prev_out, &curve.point_in, &curve.point);
                    prev_out = curve.point_out.clone();
                }
                ctx.curve_to_point(&prev_out, &first_in, &first);
            } else if let PathContent::Commands(ref commands) = path.content {
                let first;
                let first_in;
                let mut prev;
                let mut last_point;
                let mut iter = commands.iter();
                if let Some(command) = iter.next() {
                    if let Command::Line(point) = command {
                        ctx.move_to(point.x.to_f64(), point.y.to_f64());
                        first = point;
                        first_in = point;
                        prev = first.clone();
                        last_point = point.clone();
                    } else if let Command::Curve(Curve {
                        point,
                        point_in,
                        // TODO: is this really unused?
                        point_out: _,
                    }) = command
                    {
                        ctx.move_to(point.x.to_f64(), point.y.to_f64());
                        first = point;
                        prev = first.clone();
                        last_point = point.clone();
                        first_in = point_in;
                    } else {
                        unreachable!();
                    }
                } else {
                    unreachable!();
                }
                for command in iter {
                    match command {
                        Command::HLine(x) => {
                            ctx.line_to(x.to_f64(), last_point.y.to_f64());
                            last_point.x = x.clone();
                            prev = last_point.clone();
                        }
                        Command::VLine(y) => {
                            ctx.line_to(last_point.x.to_f64(), y.to_f64());
                            last_point.y = y.clone();
                            prev = last_point.clone();
                        }
                        Command::Line(point) => {
                            ctx.line_to(point.x.to_f64(), point.y.to_f64());
                            last_point = point.clone();
                            prev = last_point.clone();
                        }
                        Command::Curve(Curve {
                            ref point,
                            ref point_in,
                            ref point_out,
                        }) => {
                            ctx.curve_to_point(&prev, point_in, point);
                            last_point = point.clone();
                            prev = point_out.clone();
                        }
                    }
                }
                ctx.curve_to_point(&prev, &first_in, &first);
            } else {
                unreachable!();
            }
            if path.flags.contains(PathFlags::CLOSED) {
                ctx.close_path();
            }
        }
        if use_fill {
            ctx.fill()?;
        } else {
            ctx.stroke()?;
        }
    }
    Ok(())
}