//! Utilities for color font handling

use std::io::Read;

use ttf_parser::{GlyphId, RgbaColor};
use typst_syntax::Span;
use usvg::tiny_skia_path;
use xmlwriter::XmlWriter;

use crate::foundations::Bytes;
use crate::layout::{Abs, Frame, FrameItem, Point, Rect, Size};
use crate::text::Font;
use crate::visualize::{
    ExchangeFormat, FixedStroke, Geometry, Image, RasterImage, Shape, SvgImage,
};

/// Whether this glyph should be rendered via simple outlining instead of via
/// `glyph_frame`.
pub fn should_outline(font: &Font, glyph_id: GlyphId) -> bool {
    let ttf = font.ttf();
    (ttf.tables().glyf.is_some() || ttf.tables().cff.is_some())
        && !ttf
            .glyph_raster_image(glyph_id, u16::MAX)
            .is_some_and(|img| img.format == ttf_parser::RasterImageFormat::PNG)
        && !ttf.is_color_glyph(glyph_id)
        && ttf.glyph_svg_image(glyph_id).is_none()
}

/// A frame that can draw a glyph.
#[derive(Clone)]
pub struct GlyphFrame {
    pub upem: Abs,
    pub item: GlyphFrameItem,
}

impl GlyphFrame {
    /// The font unit square.
    pub fn size(&self) -> Size {
        Size::splat(self.upem)
    }
}

impl From<GlyphFrame> for Frame {
    fn from(g: GlyphFrame) -> Self {
        let mut frame = Frame::soft(Size::splat(g.upem));
        match g.item {
            GlyphFrameItem::Tofu(pos, shape) => {
                frame.push(pos, FrameItem::Shape(shape, Span::detached()))
            }
            GlyphFrameItem::Image(pos, image, size) => {
                frame.push(pos, FrameItem::Image(image, size, Span::detached()))
            }
        }
        frame
    }
}

/// The glyph item that is drawn.
#[derive(Clone)]
pub enum GlyphFrameItem {
    /// A fallback rectangle.
    Tofu(Point, Shape),
    /// An image glyph.
    Image(Point, Image, Size),
}

impl GlyphFrameItem {
    /// The position of the glyph item inside the parent frame.
    pub fn pos(&self) -> Point {
        match *self {
            GlyphFrameItem::Tofu(pos, _) => pos,
            GlyphFrameItem::Image(pos, _, _) => pos,
        }
    }
}

/// Returns a frame representing a glyph and whether it is a fallback tofu
/// frame.
///
/// Should only be called on glyphs for which [`should_outline`] returns false.
///
/// The glyphs are sized in font units, [`text.item.size`] is not taken into
/// account.
///
/// [`text.item.size`]: crate::text::TextItem::size
#[comemo::memoize]
pub fn glyph_frame(font: &Font, glyph_id: u16) -> Option<GlyphFrame> {
    let upem = Abs::pt(font.units_per_em());
    let glyph_id = GlyphId(glyph_id);

    if let Some(frame) = draw_glyph(font, upem, glyph_id) {
        return Some(frame);
    }

    // Generate a fallback tofu if the glyph couldn't be drawn, unless it is
    // the space glyph. Then, an empty frame does the job. (This happens for
    // some rare CBDT fonts, which don't define a bitmap for the space, but
    // also don't have a glyf or CFF table.)
    let not_space = font.ttf().glyph_index(' ') != Some(glyph_id);
    not_space.then(|| draw_fallback_tofu(font, upem, glyph_id))
}

/// Tries to draw a glyph.
fn draw_glyph(font: &Font, upem: Abs, glyph_id: GlyphId) -> Option<GlyphFrame> {
    let ttf = font.ttf();
    let kind = if let Some(raster_image) = ttf
        .glyph_raster_image(glyph_id, u16::MAX)
        .filter(|img| img.format == ttf_parser::RasterImageFormat::PNG)
    {
        draw_raster_glyph(font, upem, raster_image)
    } else if ttf.is_color_glyph(glyph_id) {
        draw_colr_glyph(font, upem, glyph_id)
    } else if ttf.glyph_svg_image(glyph_id).is_some() {
        draw_svg_glyph(font, upem, glyph_id)
    } else {
        None
    };

    kind.map(|kind| GlyphFrame { upem, item: kind })
}

/// Draws a fallback tofu box with the advance width of the glyph.
fn draw_fallback_tofu(font: &Font, upem: Abs, glyph_id: GlyphId) -> GlyphFrame {
    let advance = font
        .ttf()
        .glyph_hor_advance(glyph_id)
        .map(|advance| Abs::pt(advance as f64))
        .unwrap_or(upem / 3.0);
    let inset = 0.15 * advance;
    let height = 0.7 * upem;
    let pos = Point::new(inset, upem - height);
    let size = Size::new(advance - inset * 2.0, height);
    let thickness = upem / 20.0;
    let stroke = FixedStroke { thickness, ..Default::default() };
    let shape = Geometry::Rect(size).stroked(stroke);
    GlyphFrame { upem, item: GlyphFrameItem::Tofu(pos, shape) }
}

/// Draws a raster glyph in a frame.
///
/// Supports only PNG images.
fn draw_raster_glyph(
    font: &Font,
    upem: Abs,
    raster_image: ttf_parser::RasterGlyphImage,
) -> Option<GlyphFrameItem> {
    let data = Bytes::new(raster_image.data.to_vec());
    let image = Image::plain(RasterImage::plain(data, ExchangeFormat::Png).ok()?);

    // Apple Color emoji doesn't provide offset information (or at least
    // not in a way ttf-parser understands), so we artificially shift their
    // baseline to make it look good.
    let y_offset = if font.info().family.to_lowercase() == "apple color emoji" {
        20.0
    } else {
        -(raster_image.y as f64)
    };

    let position = Point::new(
        upem * raster_image.x as f64 / raster_image.pixels_per_em as f64,
        upem * y_offset / raster_image.pixels_per_em as f64,
    );
    let aspect_ratio = image.width() / image.height();
    let size = Size::new(upem, upem * aspect_ratio);
    Some(GlyphFrameItem::Image(position, image, size))
}

/// Draws a glyph from the COLR table into the frame.
fn draw_colr_glyph(font: &Font, upem: Abs, glyph_id: GlyphId) -> Option<GlyphFrameItem> {
    let svg_string = colr_glyph_to_svg(font, glyph_id)?;

    let ttf = font.ttf();
    let width = ttf.global_bounding_box().width() as f64;
    let height = ttf.global_bounding_box().height() as f64;
    let x_min = ttf.global_bounding_box().x_min as f64;
    let y_max = ttf.global_bounding_box().y_max as f64;

    let data = Bytes::from_string(svg_string);
    let image = Image::plain(SvgImage::new(data).ok()?);

    let y_shift = Abs::pt(upem.to_pt() - y_max);
    let position = Point::new(Abs::pt(x_min), y_shift);
    let size = Size::new(Abs::pt(width), Abs::pt(height));
    Some(GlyphFrameItem::Image(position, image, size))
}

/// Convert a COLR glyph into an SVG file.
fn colr_glyph_to_svg(font: &Font, glyph_id: GlyphId) -> Option<String> {
    let mut svg = XmlWriter::new(xmlwriter::Options::default());

    let ttf = font.ttf();
    let width = ttf.global_bounding_box().width() as f64;
    let height = ttf.global_bounding_box().height() as f64;
    let x_min = ttf.global_bounding_box().x_min as f64;
    let y_max = ttf.global_bounding_box().y_max as f64;
    let tx = -x_min;
    let ty = -y_max;

    svg.start_element("svg");
    svg.write_attribute("xmlns", "http://www.w3.org/2000/svg");
    svg.write_attribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
    svg.write_attribute("width", &width);
    svg.write_attribute("height", &height);
    svg.write_attribute_fmt("viewBox", format_args!("0 0 {width} {height}"));

    let mut path_buf = String::with_capacity(256);
    let gradient_index = 1;
    let clip_path_index = 1;

    svg.start_element("g");
    svg.write_attribute_fmt(
        "transform",
        format_args!("matrix(1 0 0 -1 0 0) matrix(1 0 0 1 {tx} {ty})"),
    );

    let mut glyph_painter = GlyphPainter {
        face: ttf,
        svg: &mut svg,
        path_buf: &mut path_buf,
        gradient_index,
        clip_path_index,
        palette_index: 0,
        transform: ttf_parser::Transform::default(),
        outline_transform: ttf_parser::Transform::default(),
        transforms_stack: vec![ttf_parser::Transform::default()],
    };

    ttf.paint_color_glyph(glyph_id, 0, RgbaColor::new(0, 0, 0, 255), &mut glyph_painter)?;
    svg.end_element();

    Some(svg.end_document())
}

/// Draws an SVG glyph in a frame.
fn draw_svg_glyph(font: &Font, upem: Abs, glyph_id: GlyphId) -> Option<GlyphFrameItem> {
    // TODO: Our current conversion of the SVG table works for Twitter Color Emoji,
    // but might not work for others. See also: https://github.com/RazrFalcon/resvg/pull/776
    let mut data = font.ttf().glyph_svg_image(glyph_id)?.data;

    // Decompress SVGZ.
    let mut decoded = vec![];
    if data.starts_with(&[0x1f, 0x8b]) {
        let mut decoder = flate2::read::GzDecoder::new(data);
        decoder.read_to_end(&mut decoded).ok()?;
        data = &decoded;
    }

    // Parse and simplify the SVG.
    let xml = std::str::from_utf8(data).ok()?;
    let document = roxmltree::Document::parse(xml).ok()?;
    let opts = usvg::Options::default();
    let tree = usvg::Tree::from_xmltree(&document, &opts).ok()?;
    let mut data = tree.to_string(&usvg::WriteOptions {
        indent: usvg::Indent::None,
        attributes_indent: usvg::Indent::None,
        ..Default::default()
    });

    // The SVG coordinates and the font coordinates are not the same: the Y axis
    // is mirrored. But the origin of the axes are the same (which means that
    // the horizontal axis in the SVG document corresponds to the baseline). See
    // the reference for more details:
    // https://learn.microsoft.com/en-us/typography/opentype/spec/svg#coordinate-systems-and-glyph-metrics
    //
    // Using this SVG directly can result in a cropped glyph. In order to avoid
    // clipping issues, we apply a translate transform so the top-left corner of
    // the bounding box is moved into the origin (0, 0) to make it fully fit
    // into the view port defined by `viewBox="0 0 width height"`, like a
    // conventional SVG.
    let bbox = tree.root().bounding_box();
    let view_box = Rect::new(
        Point::new(Abs::pt(bbox.left() as f64), Abs::pt(bbox.top() as f64)),
        Point::new(Abs::pt(bbox.right() as f64), Abs::pt(bbox.bottom() as f64)),
    );
    fixup_svg(&mut data, view_box);

    let data = Bytes::from_string(data);
    let image = Image::plain(SvgImage::new(data).ok()?);

    let position = Point::new(view_box.min.x, view_box.min.y + upem);
    let size = view_box.size();
    Some(GlyphFrameItem::Image(position, image, size))
}

/// Replace or insert the size attributes (viewBox, width and height), and
/// insert a group with a transform that translates the `viewBox` so that the
/// top-left point is at the origin (0, 0).
fn fixup_svg(svg: &mut String, view_box: Rect) {
    let mut viewbox_range = None;
    let mut width_range = None;
    let mut height_range = None;

    let mut s = unscanny::Scanner::new(svg);
    s.eat_until("<svg");
    s.expect("<svg");

    let svg_attr_start = s.cursor();

    while !s.eat_if('>') && !s.done() {
        s.eat_whitespace();
        let start = s.cursor();

        let attr_name = s.eat_until('=').trim();
        // Eat the equal sign and the quote.
        s.expect('=');
        s.eat_until('"');
        s.expect('"');

        while !s.eat_if('"') && !s.done() {
            s.eat();
        }

        match attr_name {
            "viewBox" => viewbox_range = Some(start..s.cursor()),
            "width" => width_range = Some(start..s.cursor()),
            "height" => height_range = Some(start..s.cursor()),
            _ => {}
        }
    }

    let svg_body_start = s.cursor();
    let Some(svg_body_end) = svg.rfind("</svg>") else {
        return;
    };

    svg.insert_str(svg_body_end, "</g>");
    svg.insert_str(
        svg_body_start,
        &format!(
            r#"<g transform="translate({} {})">"#,
            -view_box.min.x.to_pt(),
            -view_box.min.y.to_pt()
        ),
    );

    let size = view_box.size();
    let mut edits = [
        (
            viewbox_range,
            format!("viewBox=\"0 0 {} {}\"", size.x.to_pt(), size.y.to_pt(),),
        ),
        (width_range, format!("width=\"{}\"", size.x.to_pt())),
        (height_range, format!("height=\"{}\"", size.y.to_pt())),
    ];

    // Sort edits by ranges; missing ranges will be moved to the start.
    edits.sort_by_key(|(range, _)| range.clone().map(|r| r.start));

    // Replace or insert the attribute. Iterate in reverse, so the modifying the
    // string doesn't affect the ranges of the edits that are applied later on.
    for (range, str) in edits.into_iter().rev() {
        if let Some(range) = range {
            svg.replace_range(range, &str);
        } else {
            svg.insert_str(svg_attr_start, &str);
            svg.insert(svg_attr_start, ' ');
        }
    }
}

struct ColrBuilder<'a>(&'a mut String);

impl ColrBuilder<'_> {
    fn finish(&mut self) {
        if !self.0.is_empty() {
            self.0.pop(); // remove trailing space
        }
    }
}

impl ttf_parser::OutlineBuilder for ColrBuilder<'_> {
    fn move_to(&mut self, x: f32, y: f32) {
        use std::fmt::Write;
        write!(self.0, "M {x} {y} ").unwrap()
    }

    fn line_to(&mut self, x: f32, y: f32) {
        use std::fmt::Write;
        write!(self.0, "L {x} {y} ").unwrap()
    }

    fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
        use std::fmt::Write;
        write!(self.0, "Q {x1} {y1} {x} {y} ").unwrap()
    }

    fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
        use std::fmt::Write;
        write!(self.0, "C {x1} {y1} {x2} {y2} {x} {y} ").unwrap()
    }

    fn close(&mut self) {
        self.0.push_str("Z ")
    }
}

// NOTE: This is only a best-effort translation of COLR into SVG. It's not feature-complete
// and it's also not possible to make it feature-complete using just raw SVG features.
pub(crate) struct GlyphPainter<'a> {
    pub(crate) face: &'a ttf_parser::Face<'a>,
    pub(crate) svg: &'a mut xmlwriter::XmlWriter,
    pub(crate) path_buf: &'a mut String,
    pub(crate) gradient_index: usize,
    pub(crate) clip_path_index: usize,
    pub(crate) palette_index: u16,
    pub(crate) transform: ttf_parser::Transform,
    pub(crate) outline_transform: ttf_parser::Transform,
    pub(crate) transforms_stack: Vec<ttf_parser::Transform>,
}

impl<'a> GlyphPainter<'a> {
    fn write_gradient_stops(&mut self, stops: ttf_parser::colr::GradientStopsIter) {
        for stop in stops {
            self.svg.start_element("stop");
            self.svg.write_attribute("offset", &stop.stop_offset);
            self.write_color_attribute("stop-color", stop.color);
            let opacity = f32::from(stop.color.alpha) / 255.0;
            self.svg.write_attribute("stop-opacity", &opacity);
            self.svg.end_element();
        }
    }

    fn write_color_attribute(&mut self, name: &str, color: ttf_parser::RgbaColor) {
        self.svg.write_attribute_fmt(
            name,
            format_args!("rgb({}, {}, {})", color.red, color.green, color.blue),
        );
    }

    fn write_transform_attribute(&mut self, name: &str, ts: ttf_parser::Transform) {
        if ts.is_default() {
            return;
        }

        self.svg.write_attribute_fmt(
            name,
            format_args!("matrix({} {} {} {} {} {})", ts.a, ts.b, ts.c, ts.d, ts.e, ts.f),
        );
    }

    fn write_spread_method_attribute(
        &mut self,
        extend: ttf_parser::colr::GradientExtend,
    ) {
        self.svg.write_attribute(
            "spreadMethod",
            match extend {
                ttf_parser::colr::GradientExtend::Pad => &"pad",
                ttf_parser::colr::GradientExtend::Repeat => &"repeat",
                ttf_parser::colr::GradientExtend::Reflect => &"reflect",
            },
        );
    }

    fn paint_solid(&mut self, color: ttf_parser::RgbaColor) {
        self.svg.start_element("path");
        self.write_color_attribute("fill", color);
        let opacity = f32::from(color.alpha) / 255.0;
        self.svg.write_attribute("fill-opacity", &opacity);
        self.write_transform_attribute("transform", self.outline_transform);
        self.svg.write_attribute("d", self.path_buf);
        self.svg.end_element();
    }

    fn paint_linear_gradient(&mut self, gradient: ttf_parser::colr::LinearGradient<'a>) {
        let gradient_id = format!("lg{}", self.gradient_index);
        self.gradient_index += 1;

        let gradient_transform = paint_transform(self.outline_transform, self.transform);

        // TODO: We ignore x2, y2. Have to apply them somehow.
        // TODO: The way spreadMode works in ttf and svg is a bit different. In SVG, the spreadMode
        // will always be applied based on x1/y1 and x2/y2. However, in TTF the spreadMode will
        // be applied from the first/last stop. So if we have a gradient with x1=0 x2=1, and
        // a stop at x=0.4 and x=0.6, then in SVG we will always see a padding, while in ttf
        // we will see the actual spreadMode. We need to account for that somehow.
        self.svg.start_element("linearGradient");
        self.svg.write_attribute("id", &gradient_id);
        self.svg.write_attribute("x1", &gradient.x0);
        self.svg.write_attribute("y1", &gradient.y0);
        self.svg.write_attribute("x2", &gradient.x1);
        self.svg.write_attribute("y2", &gradient.y1);
        self.svg.write_attribute("gradientUnits", &"userSpaceOnUse");
        self.write_spread_method_attribute(gradient.extend);
        self.write_transform_attribute("gradientTransform", gradient_transform);
        self.write_gradient_stops(
            gradient.stops(self.palette_index, self.face.variation_coordinates()),
        );
        self.svg.end_element();

        self.svg.start_element("path");
        self.svg
            .write_attribute_fmt("fill", format_args!("url(#{gradient_id})"));
        self.write_transform_attribute("transform", self.outline_transform);
        self.svg.write_attribute("d", self.path_buf);
        self.svg.end_element();
    }

    fn paint_radial_gradient(&mut self, gradient: ttf_parser::colr::RadialGradient<'a>) {
        let gradient_id = format!("rg{}", self.gradient_index);
        self.gradient_index += 1;

        let gradient_transform = paint_transform(self.outline_transform, self.transform);

        self.svg.start_element("radialGradient");
        self.svg.write_attribute("id", &gradient_id);
        self.svg.write_attribute("cx", &gradient.x1);
        self.svg.write_attribute("cy", &gradient.y1);
        self.svg.write_attribute("r", &gradient.r1);
        self.svg.write_attribute("fr", &gradient.r0);
        self.svg.write_attribute("fx", &gradient.x0);
        self.svg.write_attribute("fy", &gradient.y0);
        self.svg.write_attribute("gradientUnits", &"userSpaceOnUse");
        self.write_spread_method_attribute(gradient.extend);
        self.write_transform_attribute("gradientTransform", gradient_transform);
        self.write_gradient_stops(
            gradient.stops(self.palette_index, self.face.variation_coordinates()),
        );
        self.svg.end_element();

        self.svg.start_element("path");
        self.svg
            .write_attribute_fmt("fill", format_args!("url(#{gradient_id})"));
        self.write_transform_attribute("transform", self.outline_transform);
        self.svg.write_attribute("d", self.path_buf);
        self.svg.end_element();
    }

    fn paint_sweep_gradient(&mut self, _: ttf_parser::colr::SweepGradient<'a>) {}
}

fn paint_transform(
    outline_transform: ttf_parser::Transform,
    transform: ttf_parser::Transform,
) -> ttf_parser::Transform {
    let outline_transform = tiny_skia_path::Transform::from_row(
        outline_transform.a,
        outline_transform.b,
        outline_transform.c,
        outline_transform.d,
        outline_transform.e,
        outline_transform.f,
    );

    let gradient_transform = tiny_skia_path::Transform::from_row(
        transform.a,
        transform.b,
        transform.c,
        transform.d,
        transform.e,
        transform.f,
    );

    let gradient_transform = outline_transform
        .invert()
        // In theory, we should error out. But the transform shouldn't ever be uninvertible, so let's ignore it.
        .unwrap_or_default()
        .pre_concat(gradient_transform);

    ttf_parser::Transform {
        a: gradient_transform.sx,
        b: gradient_transform.ky,
        c: gradient_transform.kx,
        d: gradient_transform.sy,
        e: gradient_transform.tx,
        f: gradient_transform.ty,
    }
}

impl GlyphPainter<'_> {
    fn clip_with_path(&mut self, path: &str) {
        let clip_id = format!("cp{}", self.clip_path_index);
        self.clip_path_index += 1;

        self.svg.start_element("clipPath");
        self.svg.write_attribute("id", &clip_id);
        self.svg.start_element("path");
        self.write_transform_attribute("transform", self.outline_transform);
        self.svg.write_attribute("d", &path);
        self.svg.end_element();
        self.svg.end_element();

        self.svg.start_element("g");
        self.svg
            .write_attribute_fmt("clip-path", format_args!("url(#{clip_id})"));
    }
}

impl<'a> ttf_parser::colr::Painter<'a> for GlyphPainter<'a> {
    fn outline_glyph(&mut self, glyph_id: ttf_parser::GlyphId) {
        self.path_buf.clear();
        let mut builder = ColrBuilder(self.path_buf);
        match self.face.outline_glyph(glyph_id, &mut builder) {
            Some(v) => v,
            None => return,
        };
        builder.finish();

        // We have to write outline using the current transform.
        self.outline_transform = self.transform;
    }

    fn push_layer(&mut self, mode: ttf_parser::colr::CompositeMode) {
        self.svg.start_element("g");

        use ttf_parser::colr::CompositeMode;
        // TODO: Need to figure out how to represent the other blend modes
        // in SVG.
        let mode = match mode {
            CompositeMode::SourceOver => "normal",
            CompositeMode::Screen => "screen",
            CompositeMode::Overlay => "overlay",
            CompositeMode::Darken => "darken",
            CompositeMode::Lighten => "lighten",
            CompositeMode::ColorDodge => "color-dodge",
            CompositeMode::ColorBurn => "color-burn",
            CompositeMode::HardLight => "hard-light",
            CompositeMode::SoftLight => "soft-light",
            CompositeMode::Difference => "difference",
            CompositeMode::Exclusion => "exclusion",
            CompositeMode::Multiply => "multiply",
            CompositeMode::Hue => "hue",
            CompositeMode::Saturation => "saturation",
            CompositeMode::Color => "color",
            CompositeMode::Luminosity => "luminosity",
            _ => "normal",
        };
        self.svg.write_attribute_fmt(
            "style",
            format_args!("mix-blend-mode: {mode}; isolation: isolate"),
        );
    }

    fn pop_layer(&mut self) {
        self.svg.end_element(); // g
    }

    fn push_transform(&mut self, transform: ttf_parser::Transform) {
        self.transforms_stack.push(self.transform);
        self.transform = ttf_parser::Transform::combine(self.transform, transform);
    }

    fn paint(&mut self, paint: ttf_parser::colr::Paint<'a>) {
        match paint {
            ttf_parser::colr::Paint::Solid(color) => self.paint_solid(color),
            ttf_parser::colr::Paint::LinearGradient(lg) => self.paint_linear_gradient(lg),
            ttf_parser::colr::Paint::RadialGradient(rg) => self.paint_radial_gradient(rg),
            ttf_parser::colr::Paint::SweepGradient(sg) => self.paint_sweep_gradient(sg),
        }
    }

    fn pop_transform(&mut self) {
        if let Some(ts) = self.transforms_stack.pop() {
            self.transform = ts
        }
    }

    fn push_clip(&mut self) {
        self.clip_with_path(&self.path_buf.clone());
    }

    fn pop_clip(&mut self) {
        self.svg.end_element();
    }

    fn push_clip_box(&mut self, clipbox: ttf_parser::colr::ClipBox) {
        let x_min = clipbox.x_min;
        let x_max = clipbox.x_max;
        let y_min = clipbox.y_min;
        let y_max = clipbox.y_max;

        let clip_path = format!(
            "M {x_min} {y_min} L {x_max} {y_min} L {x_max} {y_max} L {x_min} {y_max} Z"
        );

        self.clip_with_path(&clip_path);
    }
}
