Skip to content

Stroke rendering artifact in rounded-rect corner at small output sizes #179

Description

@nick-uc

Description

When stroking a rounded rectangle path with tiny-skia and rendering it at a small pixel size, a visible streak/artifact appears in the corners. The exact same path renders cleanly at a larger output size.

The bug is in tiny-skia alone — it does not require resvg/usvg. The path is built directly with tiny_skia::PathBuilder and rendered with Pixmap::stroke_path.

Reproduction

Cargo.toml:

[dependencies]
tiny-skia = "0.12.0"

src/main.rs:

// The path corresponds to this SVG, which is what we started from:
//
// <?xml version="1.0" encoding="UTF-8" standalone="no"?>
// <svg xmlns="http://www.w3.org/2000/svg"
//      xmlns:xlink="http://www.w3.org/1999/xlink"
//      height="3.5in" preserveAspectRatio="none"
//      viewBox="-120 -168 240 336" width="2.5in">
//     <rect width="239" height="335" x="-119.5" y="-167.5"
//           rx="12" ry="12" fill="white" stroke="black"></rect>
// </svg>

use tiny_skia::{Color, FillRule, Paint, PathBuilder, Pixmap, Stroke, Transform};

fn render(to_width: u32, to_height: u32) -> Vec<u8> {
    let svg_w = 240.0_f32;
    let svg_h = 336.0_f32;

    let mut target_height = to_height;
    let mut scale = target_height as f32 / svg_h;
    let mut target_width = (svg_w * scale).round() as u32;
    if target_width > to_width {
        scale = to_width as f32 / svg_w;
        target_width = to_width;
        target_height = (svg_h * scale).round() as u32;
    }

    let mut pixmap = Pixmap::new(target_width, target_height).unwrap();
    pixmap.fill(Color::WHITE);

    let transform = Transform::from_scale(scale, scale)
        .post_translate(120.0 * scale, 168.0 * scale);

    let x = -119.5_f32;
    let y = -167.5_f32;
    let w = 239.0_f32;
    let h = 335.0_f32;
    let r = 12.0_f32;

    let mut pb = PathBuilder::new();
    pb.move_to(x + r, y);
    pb.line_to(x + w - r, y);
    pb.quad_to(x + w, y, x + w, y + r);
    pb.line_to(x + w, y + h - r);
    pb.quad_to(x + w, y + h, x + w - r, y + h);
    pb.line_to(x + r, y + h);
    pb.quad_to(x, y + h, x, y + h - r);
    pb.line_to(x, y + r);
    pb.quad_to(x, y, x + r, y);
    pb.close();
    let path = pb.finish().unwrap();

    let mut fill_paint = Paint::default();
    fill_paint.set_color_rgba8(255, 255, 255, 255);
    fill_paint.anti_alias = true;
    pixmap.fill_path(&path, &fill_paint, FillRule::Winding, transform, None);

    let mut stroke_paint = Paint::default();
    stroke_paint.set_color_rgba8(0, 0, 0, 255);
    stroke_paint.anti_alias = true;
    let stroke = Stroke { width: 1.0, ..Stroke::default() };
    pixmap.stroke_path(&path, &stroke_paint, &stroke, transform, None);

    pixmap.encode_png().unwrap()
}

fn main() {
    std::fs::write("ok.png", render(286, 400)).unwrap();
    std::fs::write("glitches.png", render(143, 200)).unwrap();
}

Expected Behavior

Both outputs should show a clean rounded-rectangle stroke. Lower resolution should only reduce anti-aliasing detail, not introduce structural artifacts.

Actual Behavior

• ok.png (286×400) — renders correctly:
Image

• glitches.png (143×200) — a visible streak appears in the corners:
Image

Notes

• The bug was originally encountered through resvg + usvg rendering an SVG card. I narrowed it down by reconstructing the same path with PathBuilder and reproducing the artifact without any SVG pipeline involved — so the root cause is in tiny-skia's path rendering (likely stroke tessellation / AA edge accumulation), not in the SVG layer.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions