tiny-skia 0.8.2

A tiny Skia subset ported to Rust.
Documentation
// Copyright 2006 The Android Open Source Project
// Copyright 2020 Yevhenii Reizner
//
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use core::convert::TryInto;

use tiny_skia_path::{f32x2, PathVerb, SaturateCast, Scalar, ScreenIntRect};

use crate::{IntRect, LineCap, Path, PathSegment, Point, Rect};

use crate::blitter::Blitter;
use crate::fixed_point::{fdot16, fdot6};
use crate::line_clipper;
use crate::math::LENGTH_U32_ONE;
use crate::path_geometry;

#[cfg(all(not(feature = "std"), feature = "no-std-float"))]
use tiny_skia_path::NoStdFloat;

const FLOAT_PI: f32 = 3.14159265;

pub type LineProc = fn(&[Point], Option<&ScreenIntRect>, &mut dyn Blitter) -> Option<()>;

const MAX_CUBIC_SUBDIVIDE_LEVEL: u8 = 9;
const MAX_QUAD_SUBDIVIDE_LEVEL: u8 = 5;

pub fn stroke_path(
    path: &Path,
    line_cap: LineCap,
    clip: &ScreenIntRect,
    blitter: &mut dyn Blitter,
) -> Option<()> {
    super::hairline::stroke_path_impl(path, line_cap, clip, hair_line_rgn, blitter)
}

fn hair_line_rgn(
    points: &[Point],
    clip: Option<&ScreenIntRect>,
    blitter: &mut dyn Blitter,
) -> Option<()> {
    let max = 32767.0;
    let fixed_bounds = Rect::from_ltrb(-max, -max, max, max)?;

    let clip_bounds = clip.map(|c| c.to_rect());

    for i in 0..points.len() - 1 {
        let mut pts = [Point::zero(); 2];

        // We have to pre-clip the line to fit in a Fixed, so we just chop the line.
        if !line_clipper::intersect(&[points[i], points[i + 1]], &fixed_bounds, &mut pts) {
            continue;
        }

        if let Some(clip_bounds) = clip_bounds {
            let tmp = pts.clone();
            // Perform a clip in scalar space, so we catch huge values which might
            // be missed after we convert to FDot6 (overflow).
            if !line_clipper::intersect(&tmp, &clip_bounds, &mut pts) {
                continue;
            }
        }

        let mut x0 = fdot6::from_f32(pts[0].x);
        let mut y0 = fdot6::from_f32(pts[0].y);
        let mut x1 = fdot6::from_f32(pts[1].x);
        let mut y1 = fdot6::from_f32(pts[1].y);

        debug_assert!(fdot6::can_convert_to_fdot16(x0));
        debug_assert!(fdot6::can_convert_to_fdot16(y0));
        debug_assert!(fdot6::can_convert_to_fdot16(x1));
        debug_assert!(fdot6::can_convert_to_fdot16(y1));

        let dx = x1 - x0;
        let dy = y1 - y0;

        if dx.abs() > dy.abs() {
            // mostly horizontal

            if x0 > x1 {
                // we want to go left-to-right
                core::mem::swap(&mut x0, &mut x1);
                core::mem::swap(&mut y0, &mut y1);
            }

            let mut ix0 = fdot6::round(x0);
            let ix1 = fdot6::round(x1);
            if ix0 == ix1 {
                // too short to draw
                continue;
            }

            let slope = fdot16::div(dy, dx);
            #[allow(clippy::precedence)]
            let mut start_y = fdot6::to_fdot16(y0) + (slope * ((32 - x0) & 63) >> 6);

            // In some cases, probably due to precision/rounding issues,
            // `start_y` can become equal to the image height,
            // which will lead to panic, because we would be accessing pixels outside
            // the current memory buffer.
            // This is tiny-skia specific issue. Skia handles this part differently.
            let max_y = if let Some(clip_bounds) = clip_bounds {
                fdot16::from_f32(clip_bounds.bottom())
            } else {
                i32::MAX
            };

            debug_assert!(ix0 < ix1);
            loop {
                if ix0 >= 0 && start_y >= 0 && start_y < max_y {
                    blitter.blit_h(ix0 as u32, (start_y >> 16) as u32, LENGTH_U32_ONE);
                }

                start_y += slope;
                ix0 += 1;
                if ix0 >= ix1 {
                    break;
                }
            }
        } else {
            // mostly vertical

            if y0 > y1 {
                // we want to go top-to-bottom
                core::mem::swap(&mut x0, &mut x1);
                core::mem::swap(&mut y0, &mut y1);
            }

            let mut iy0 = fdot6::round(y0);
            let iy1 = fdot6::round(y1);
            if iy0 == iy1 {
                // too short to draw
                continue;
            }

            let slope = fdot16::div(dx, dy);
            #[allow(clippy::precedence)]
            let mut start_x = fdot6::to_fdot16(x0) + (slope * ((32 - y0) & 63) >> 6);

            debug_assert!(iy0 < iy1);
            loop {
                if start_x >= 0 && iy0 >= 0 {
                    blitter.blit_h((start_x >> 16) as u32, iy0 as u32, LENGTH_U32_ONE);
                }

                start_x += slope;
                iy0 += 1;
                if iy0 >= iy1 {
                    break;
                }
            }
        }
    }

    Some(())
}

pub fn stroke_path_impl(
    path: &Path,
    line_cap: LineCap,
    clip: &ScreenIntRect,
    line_proc: LineProc,
    blitter: &mut dyn Blitter,
) -> Option<()> {
    let mut inset_clip = None;
    let mut outset_clip = None;

    {
        let cap_out = if line_cap == LineCap::Butt { 1.0 } else { 2.0 };
        let ibounds = path.bounds().outset(cap_out, cap_out)?.round_out()?;
        clip.to_int_rect().intersect(&ibounds)?;

        if !clip.to_int_rect().contains(&ibounds) {
            // We now cache two scalar rects, to use for culling per-segment (e.g. cubic).
            // Since we're hairlining, the "bounds" of the control points isn't necessairly the
            // limit of where a segment can draw (it might draw up to 1 pixel beyond in aa-hairs).
            //
            // Compute the pt-bounds per segment is easy, so we do that, and then inversely adjust
            // the culling bounds so we can just do a straight compare per segment.
            //
            // insetClip is use for quick-accept (i.e. the segment is not clipped), so we inset
            // it from the clip-bounds (since segment bounds can be off by 1).
            //
            // outsetClip is used for quick-reject (i.e. the segment is entirely outside), so we
            // outset it from the clip-bounds.
            outset_clip = Some(clip.to_int_rect().make_outset(1, 1)?);
            inset_clip = Some(clip.to_int_rect().inset(1, 1)?);
        }
    }

    let clip = Some(clip);
    let mut prev_verb = PathVerb::Move;
    let mut first_pt = Point::zero();
    let mut last_pt = Point::zero();

    let mut iter = path.segments();
    while let Some(segment) = iter.next() {
        let verb = iter.curr_verb();
        let next_verb = iter.next_verb();
        let last_pt2;
        match segment {
            PathSegment::MoveTo(p) => {
                first_pt = p;
                last_pt = p;
                last_pt2 = p;
            }
            PathSegment::LineTo(p) => {
                let mut points = [last_pt, p];
                if line_cap != LineCap::Butt {
                    extend_pts(line_cap, prev_verb, next_verb, &mut points);
                }

                line_proc(&points, clip, blitter);
                last_pt = p;
                last_pt2 = points[0];
            }
            PathSegment::QuadTo(p0, p1) => {
                let mut points = [last_pt, p0, p1];
                if line_cap != LineCap::Butt {
                    extend_pts(line_cap, prev_verb, next_verb, &mut points);
                }

                hair_quad(
                    &points,
                    clip,
                    inset_clip.as_ref(),
                    outset_clip.as_ref(),
                    compute_quad_level(&points),
                    line_proc,
                    blitter,
                );

                last_pt = p1;
                last_pt2 = points[0];
            }
            PathSegment::CubicTo(p0, p1, p2) => {
                let mut points = [last_pt, p0, p1, p2];
                if line_cap != LineCap::Butt {
                    extend_pts(line_cap, prev_verb, next_verb, &mut points);
                }

                hair_cubic(
                    &points,
                    clip,
                    inset_clip.as_ref(),
                    outset_clip.as_ref(),
                    line_proc,
                    blitter,
                );

                last_pt = p2;
                last_pt2 = points[0];
            }
            PathSegment::Close => {
                let mut points = [last_pt, first_pt];
                if line_cap != LineCap::Butt && prev_verb == PathVerb::Move {
                    // cap moveTo/close to match svg expectations for degenerate segments
                    extend_pts(line_cap, prev_verb, next_verb, &mut points);
                }
                line_proc(&points, clip, blitter);
                last_pt2 = points[0];
            }
        }

        if line_cap != LineCap::Butt {
            if prev_verb == PathVerb::Move
                && matches!(verb, PathVerb::Line | PathVerb::Quad | PathVerb::Cubic)
            {
                first_pt = last_pt2; // the curve moved the initial point, so close to it instead
            }

            prev_verb = verb;
        }
    }

    Some(())
}

/// Extend the points in the direction of the starting or ending tangent by 1/2 unit to
/// account for a round or square cap.
///
/// If there's no distance between the end point and
/// the control point, use the next control point to create a tangent. If the curve
/// is degenerate, move the cap out 1/2 unit horizontally.
fn extend_pts(
    line_cap: LineCap,
    prev_verb: PathVerb,
    next_verb: Option<PathVerb>,
    points: &mut [Point],
) {
    debug_assert!(!points.is_empty()); // TODO: use non-zero slice
    debug_assert!(line_cap != LineCap::Butt);

    // The area of a circle is PI*R*R. For a unit circle, R=1/2, and the cap covers half of that.
    let cap_outset = if line_cap == LineCap::Square {
        0.5
    } else {
        FLOAT_PI / 8.0
    };
    if prev_verb == PathVerb::Move {
        let first = points[0];
        let mut offset = 0;
        let mut controls = points.len() - 1;
        let mut tangent;
        loop {
            offset += 1;
            tangent = first - points[offset];

            if !tangent.is_zero() {
                break;
            }

            controls -= 1;
            if controls == 0 {
                break;
            }
        }

        if tangent.is_zero() {
            tangent = Point::from_xy(1.0, 0.0);
            controls = points.len() - 1; // If all points are equal, move all but one.
        } else {
            tangent.normalize();
        }

        offset = 0;
        loop {
            // If the end point and control points are equal, loop to move them in tandem.
            points[offset].x += tangent.x * cap_outset;
            points[offset].y += tangent.y * cap_outset;

            offset += 1;
            controls += 1;
            if controls >= points.len() {
                break;
            }
        }
    }

    if matches!(
        next_verb,
        Some(PathVerb::Move) | Some(PathVerb::Close) | None
    ) {
        let last = points.last().unwrap().clone();
        let mut offset = points.len() - 1;
        let mut controls = points.len() - 1;
        let mut tangent;
        loop {
            offset -= 1;
            tangent = last - points[offset];

            if !tangent.is_zero() {
                break;
            }

            controls -= 1;
            if controls == 0 {
                break;
            }
        }

        if tangent.is_zero() {
            tangent = Point::from_xy(-1.0, 0.0);
            controls = points.len() - 1;
        } else {
            tangent.normalize();
        }

        offset = points.len() - 1;
        loop {
            points[offset].x += tangent.x * cap_outset;
            points[offset].y += tangent.y * cap_outset;

            offset -= 1;
            controls += 1;
            if controls >= points.len() {
                break;
            }
        }
    }
}

fn hair_quad(
    points: &[Point; 3],
    mut clip: Option<&ScreenIntRect>,
    inset_clip: Option<&IntRect>,
    outset_clip: Option<&IntRect>,
    level: u8,
    line_proc: LineProc,
    blitter: &mut dyn Blitter,
) -> Option<()> {
    if let Some(inset_clip) = inset_clip {
        debug_assert!(outset_clip.is_some());
        let inset_clip = inset_clip.to_rect();
        let outset_clip = outset_clip?.to_rect();

        let bounds = compute_nocheck_quad_bounds(points)?;
        if !geometric_overlap(&outset_clip, &bounds) {
            return Some(());
        } else if geometric_contains(&inset_clip, &bounds) {
            clip = None;
        }
    }

    hair_quad2(points, clip, level, line_proc, blitter);
    Some(())
}

fn compute_nocheck_quad_bounds(points: &[Point; 3]) -> Option<Rect> {
    debug_assert!(points[0].is_finite());
    debug_assert!(points[1].is_finite());
    debug_assert!(points[2].is_finite());

    let mut min = points[0].to_f32x2();
    let mut max = min;
    for i in 1..3 {
        let pair = points[i].to_f32x2();
        min = min.min(pair);
        max = max.max(pair);
    }

    Rect::from_ltrb(min.x(), min.y(), max.x(), max.y())
}

fn geometric_overlap(a: &Rect, b: &Rect) -> bool {
    a.left() < b.right() && b.left() < a.right() && a.top() < b.bottom() && b.top() < a.bottom()
}

fn geometric_contains(outer: &Rect, inner: &Rect) -> bool {
    inner.right() <= outer.right()
        && inner.left() >= outer.left()
        && inner.bottom() <= outer.bottom()
        && inner.top() >= outer.top()
}

fn hair_quad2(
    points: &[Point; 3],
    clip: Option<&ScreenIntRect>,
    level: u8,
    line_proc: LineProc,
    blitter: &mut dyn Blitter,
) {
    debug_assert!(level <= MAX_QUAD_SUBDIVIDE_LEVEL); // TODO: to type

    let coeff = path_geometry::QuadCoeff::from_points(points);

    const MAX_POINTS: usize = (1 << MAX_QUAD_SUBDIVIDE_LEVEL) + 1;
    let lines = 1 << level;
    debug_assert!(lines < MAX_POINTS);

    let mut tmp = [Point::zero(); MAX_POINTS];
    tmp[0] = points[0];

    let mut t = f32x2::default();
    let dt = f32x2::splat(1.0 / lines as f32);
    for i in 1..lines {
        t = t + dt;
        let v = (coeff.a * t + coeff.b) * t + coeff.c;
        tmp[i] = Point::from_xy(v.x(), v.y());
    }

    tmp[lines] = points[2];
    line_proc(&tmp[0..lines + 1], clip, blitter);
}

fn compute_quad_level(points: &[Point; 3]) -> u8 {
    let d = compute_int_quad_dist(points);
    // Quadratics approach the line connecting their start and end points
    // 4x closer with each subdivision, so we compute the number of
    // subdivisions to be the minimum need to get that distance to be less
    // than a pixel.
    let mut level = (33 - d.leading_zeros()) >> 1;
    // sanity check on level (from the previous version)
    if level > MAX_QUAD_SUBDIVIDE_LEVEL as u32 {
        level = MAX_QUAD_SUBDIVIDE_LEVEL as u32;
    }

    level as u8
}

fn compute_int_quad_dist(points: &[Point; 3]) -> u32 {
    // compute the vector between the control point ([1]) and the middle of the
    // line connecting the start and end ([0] and [2])
    let dx = ((points[0].x + points[2].x).half() - points[1].x).abs();
    let dy = ((points[0].y + points[2].y).half() - points[1].y).abs();

    // convert to whole pixel values (use ceiling to be conservative).
    // assign to unsigned so we can safely add 1/2 of the smaller and still fit in
    // u32, since T::saturate_from() returns 31 bits at most.
    let idx = i32::saturate_from(dx.ceil()) as u32;
    let idy = i32::saturate_from(dy.ceil()) as u32;

    // use the cheap approx for distance
    if idx > idy {
        idx + (idy >> 1)
    } else {
        idy + (idx >> 1)
    }
}

fn hair_cubic(
    points: &[Point; 4],
    mut clip: Option<&ScreenIntRect>,
    inset_clip: Option<&IntRect>,
    outset_clip: Option<&IntRect>,
    line_proc: LineProc,
    blitter: &mut dyn Blitter,
) -> Option<()> {
    if let Some(inset_clip) = inset_clip {
        debug_assert!(outset_clip.is_some());
        let inset_clip = inset_clip.to_rect();
        let outset_clip = outset_clip?.to_rect();

        let bounds = compute_nocheck_cubic_bounds(points)?;
        if !geometric_overlap(&outset_clip, &bounds) {
            return Some(());
        } else if geometric_contains(&inset_clip, &bounds) {
            clip = None;
        }
    }

    if quick_cubic_niceness_check(points) {
        hair_cubic2(points, clip, line_proc, blitter);
    } else {
        let mut tmp = [Point::zero(); 13];
        let mut t_values = path_geometry::new_t_values();

        let count = path_geometry::chop_cubic_at_max_curvature(points, &mut t_values, &mut tmp);
        for i in 0..count {
            let offset = i * 3;
            let new_points: [Point; 4] = tmp[offset..offset + 4].try_into().unwrap();
            hair_cubic2(&new_points, clip, line_proc, blitter);
        }
    }

    Some(())
}

fn compute_nocheck_cubic_bounds(points: &[Point; 4]) -> Option<Rect> {
    debug_assert!(points[0].is_finite());
    debug_assert!(points[1].is_finite());
    debug_assert!(points[2].is_finite());
    debug_assert!(points[3].is_finite());

    let mut min = points[0].to_f32x2();
    let mut max = min;
    for i in 1..4 {
        let pair = points[i].to_f32x2();
        min = min.min(pair);
        max = max.max(pair);
    }

    Rect::from_ltrb(min.x(), min.y(), max.x(), max.y())
}

// The off-curve points are "inside" the limits of the on-curve points.
fn quick_cubic_niceness_check(points: &[Point; 4]) -> bool {
    lt_90(points[1], points[0], points[3])
        && lt_90(points[2], points[0], points[3])
        && lt_90(points[1], points[3], points[0])
        && lt_90(points[2], points[3], points[0])
}

fn lt_90(p0: Point, pivot: Point, p2: Point) -> bool {
    (p0 - pivot).dot(p2 - pivot) >= 0.0
}

fn hair_cubic2(
    points: &[Point; 4],
    clip: Option<&ScreenIntRect>,
    line_proc: LineProc,
    blitter: &mut dyn Blitter,
) {
    let lines = compute_cubic_segments(points);
    debug_assert!(lines > 0);
    if lines == 1 {
        line_proc(&[points[0], points[3]], clip, blitter);
        return;
    }

    let coeff = path_geometry::CubicCoeff::from_points(points);

    const MAX_POINTS: usize = (1 << MAX_CUBIC_SUBDIVIDE_LEVEL) + 1;
    debug_assert!(lines < MAX_POINTS);
    let mut tmp = [Point::zero(); MAX_POINTS];

    let dt = f32x2::splat(1.0 / lines as f32);
    let mut t = f32x2::default();

    tmp[0] = points[0];
    for i in 1..lines {
        t = t + dt;
        tmp[i] = Point::from_f32x2(((coeff.a * t + coeff.b) * t + coeff.c) * t + coeff.d);
    }

    if tmp.iter().all(|p| p.is_finite()) {
        tmp[lines] = points[3];
        line_proc(&tmp[0..lines + 1], clip, blitter);
    } else {
        // else some point(s) are non-finite, so don't draw
    }
}

fn compute_cubic_segments(points: &[Point; 4]) -> usize {
    let p0 = points[0].to_f32x2();
    let p1 = points[1].to_f32x2();
    let p2 = points[2].to_f32x2();
    let p3 = points[3].to_f32x2();

    let one_third = f32x2::splat(1.0 / 3.0);
    let two_third = f32x2::splat(2.0 / 3.0);

    let p13 = one_third * p3 + two_third * p0;
    let p23 = one_third * p0 + two_third * p3;

    let diff = (p1 - p13).abs().max((p2 - p23).abs()).max_component();
    let mut tol = 1.0 / 8.0;

    for i in 0..MAX_CUBIC_SUBDIVIDE_LEVEL {
        if diff < tol {
            return 1 << i;
        }

        tol *= 4.0;
    }

    1 << MAX_CUBIC_SUBDIVIDE_LEVEL
}