Files
pdfcraft/crates/render/tests/it/stroke_degenerate_cubic.rs
T
KenM76andClaude Opus 5.5 a7b07bfc56 fix(render): a stroke whose outline comes back with NaN is drawn, not dropped (#4907)
kurbo 0.13.1's stroker can give a cubic that is nearly a line (all control points within a
rounding error of its chord) a NaN join, and vello_common's `flatten::fill` drops any path
containing NaN, so the whole outline vanished. An Illustrator outline ending in
`73.763 -0.536 l 73.764 -0.536 73.764 -0.536 73.764 -0.536 c` did (#4907). kurbo tracks the
bug class in linebender/kurbo#617 and #519; #610 is unreleased.

Vendored vello_common patch (2): when the expanded stroke is not finite, `flatten::stroke`
strokes the path again with every quadratic or cubic whose control points lie within the
tolerance of its chord replaced by a line, one element out per element in. A finite outline
is unchanged, and a path with non-finite input is still dropped. `stroke` and
`StripGenerator::generate_stroked_path` take a `Clone` path; vello_cpu's callers pass
`&BezPath` and a copied slice iterator.

Tests: `flatten::tests::a_tiny_degenerate_cubic_does_not_drop_the_stroke` and the render
crate's `stroke_degenerate_cubic` pixel test fail with the fallback disabled;
`a_finite_outline_is_stroked_as_before` and `a_path_with_nan_is_still_dropped` are guards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-10-27 00:34:47 -04:00

52 lines
2.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! A contributor-original synthetic page reproducing #4907: an outline that ends in a cubic whose
//! control points all sit 0.001 pt from its start was not drawn at all, because the stroker put
//! NaN into its outline and the rasterizer dropped the whole path.
use pdfcraft_render::{PageRenderer, RenderConfig, RenderRequest};
use std::sync::Arc;
/// A one-page 100 × 20 pt PDF drawing `content`, with a cross-reference table.
fn pdf(content: &str) -> Vec<u8> {
let objects = [
"<< /Type /Catalog /Pages 2 0 R >>".to_string(),
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_string(),
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 20] /Contents 4 0 R >>".to_string(),
format!("<< /Length {} >>\nstream\n{content}\nendstream", content.len()),
];
let mut bytes = b"%PDF-1.7\n".to_vec();
let mut offsets = Vec::new();
for (i, object) in objects.iter().enumerate() {
offsets.push(bytes.len());
bytes.extend_from_slice(format!("{} 0 obj\n{object}\nendobj\n", i + 1).as_bytes());
}
let xref = bytes.len();
bytes.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objects.len() + 1).as_bytes());
for offset in offsets {
bytes.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes());
}
bytes.extend_from_slice(format!("trailer << /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objects.len() + 1).as_bytes());
bytes
}
/// The red channel of the pixel at (`x`, `y`) pt from the top left of `content` rendered at 1×.
fn red_at(content: &str, x: usize, y: usize) -> u8 {
let mut renderer = PageRenderer::new(Arc::new(pdf(content)), RenderConfig::default());
let out = renderer.render(RenderRequest { page: 0, scale: 1.0, ..Default::default() });
assert!(out.error.is_none(), "{:?}", out.error);
let at = (y * out.width as usize + x) * 4;
*out.rgba.get(at).expect("the pixel lies on the page")
}
#[test]
fn a_stroke_ending_in_a_tiny_degenerate_cubic_is_drawn() {
// The line runs from (10, 10) to about (84, 9.5) in page space; (50, 10) from the top left is
// inside its 2 pt width. The cubic's exact coordinates are what trigger the NaN. The control,
// without the cubic, is the baseline: it draws with or without the fix.
let path = "0 0 m 73.763 -0.536 l 73.764 -0.536 73.764 -0.536 73.764 -0.536 c S";
let control = "0 0 m 73.763 -0.536 l S";
for content in [control, path] {
let content = format!("0 G 2 w 1 0 0 1 10 10 cm {content}");
let red = red_at(&content, 50, 10);
assert!(red < 64, "`{content}` left the line's pixel at red {red}, want black");
}
}