linebender / linebender/tiny-skia

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

Open
#179 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Rust
Stars
1.6k
Forks
99
Avg merge
2h 52m
Merged PRs (30d)
1

Description

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.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the standalone src/main.rs reproduction and run it to compare ok.png at 286×400 with glitches.png at 143×200. Trace PathBuilder, Pixmap::stroke_path, Stroke, and the transformed rounded-rectangle path to locate the rendering or anti-aliasing behavior that creates the corner streak. Done means both output sizes show a clean rounded-rectangle stroke without structural artifacts.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
computer-graphics
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.