Files
pdfcraft/crates/render/tests/it/stroke_zero_end_tangent.rs
T
c02341f6c0 M2: a wide stroke of a path with y curves is drawn, not dropped (vello_common patch 4) (#6526)
A cubic whose second control point is its end point (as the `y` operator builds,
ISO 32000-2 8.5.2.1 Table 58) has a zero derivative at its end. kurbo 0.13.1's
offset curve looks for cusps with a root search on the curvature, which grows
without bound towards that end; at some widths the search stops on the end point
and normalising the zero tangent there gives NaN. vello_common's fill drops any
outline with NaN, so the whole stroke vanished (a two-curve `y` path drew nothing
at 16, 32 and 63 pt; a PDFBox tracker file lost its 32 pt "XX" outline).

The NaN fallback that #4913 added to `flatten::stroke` now also moves such a
control point the stroke tolerance towards the previous control point before
stroking again (the curve moves by at most 4/9 of the tolerance). Outlines that
come back finite take the old path unchanged. The vendor ledger row also gets
back #4913's entry, which a merge had dropped.


Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA

Co-authored-by: mtlcl410 <chuanlin@niar.org.tw>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 08:31:00 +08:00

62 lines
3.1 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: a wide stroke of a path built with `y` (the second
//! control point on the end point, ISO 32000-2 §8.5.2.1 Table 58) was not drawn at all at some widths,
//! because the stroker put NaN into its outline and the rasterizer dropped the whole path. A file
//! in the PDFBox issue tracker lost the 32 pt outline of its "XX" this way.
use pdfcraft_render::{PageRenderer, RenderConfig, RenderRequest};
use std::sync::Arc;
/// A one-page 200 × 200 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 200 200] /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_wide_stroke_of_a_path_with_y_curves_is_drawn() {
// The path's exact coordinates are the trigger; `cm` moves the point where its two curves
// meet, (18.9, 236.3), to about (98.9, 99.7) from the top left, inside every stroke width
// below whatever the caps. The `v` path is the same two curves run backwards, a guard that
// draws with or without the fix.
let y = "21.4166 234.5928 m 19.9009 236.0303 18.9478 236.3428 y 19.2603 238.4209 20.8697 238.4209 y S";
let v = "20.8697 238.4209 m 19.2603 238.4209 18.9478 236.3428 v 19.9009 236.0303 21.4166 234.5928 v S";
let mut dropped = Vec::new();
for path in [y, v] {
for width in [4, 8, 16, 24, 32, 48, 63, 64] {
for (join, cap) in [(0, 0), (1, 1), (2, 2)] {
let content = format!("0 1 0 RG {width} w {join} j {cap} J 1 0 0 1 80 -136 cm {path}");
let red = red_at(&content, 98, 99);
if red > 64 {
dropped.push(content);
}
}
}
}
assert!(dropped.is_empty(), "strokes not drawn: {dropped:#?}");
}