M2: a segment after h begins a new subpath (hayro patch 61)

ISO 32000-2 §8.5.2.1, Table 58: h "terminates the current subpath.
Appending another segment to the current path shall begin a new subpath,
even if the new segment begins at the endpoint reached by the h
operation." kurbo carries a segment after ClosePath on from the subpath
start without a MoveTo, so resolve_degenerate_subpaths saw "h" then an "l"
back to that point as part of the closed subpath, and the round-cap dot
of that degenerate subpath was lost (main drew it, through the nudge this
PR removed). l, c, v and y after h now start with a MoveTo to the point h
reached.

Review on #4410 (CodeRabbit, vendor/hayro-interpret/src/interpret/mod.rs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
This commit is contained in:
mtlcl410
2026-11-02 08:00:22 +08:00
co-authored by Claude Opus 5.5
parent 07cab37e7b
commit ba16b9b5b8
4 changed files with 49 additions and 2 deletions
+27
View File
@@ -121,6 +121,33 @@ fn degenerate_subpaths_leave_the_rest_of_the_path_alone() {
assert_eq!(px(&page, 20, 20), BLUE, "a segment a hundredth of a point long is stroked with its caps");
}
/// `h` "terminates the current subpath. Appending another segment to the current path shall begin
/// a new subpath, even if the new segment begins at the endpoint reached by the h operation"
/// (ISO 32000-2 §8.5.2.1, Table 58). So a segment after `h` back to the point `h` reached is a
/// degenerate subpath of its own: a round dot with round caps, nothing with the others. The closed
/// line from x 10 to x 30 turns back on itself at x 10, where the miter join falls back to a bevel
/// and paints nothing left of x 10; a 6 pt dot at (10, 20) reaches x 7.
#[test]
fn a_segment_after_h_begins_a_new_subpath() {
let page = render("0 0 1 RG 6 w 1 J 10 20 m 30 20 l h S", "<< >>");
assert_eq!(px(&page, 20, 20), BLUE, "the closed line");
assert_eq!(px(&page, 8, 20), WHITE, "a closed line has no cap at its start");
for (what, after) in [("l", "10 20 l"), ("c", "10 20 10 20 10 20 c"), ("v", "10 20 10 20 v"), ("y", "10 20 10 20 y")] {
let page = render(&format!("0 0 1 RG 6 w 1 J 10 20 m 30 20 l h {after} S"), "<< >>");
assert_eq!(px(&page, 20, 20), BLUE, "{what} after h: the closed line");
assert_eq!(px(&page, 8, 20), BLUE, "{what} after h, round caps: a dot at the point h reached");
for cap in ["0 J", "2 J"] {
let page = render(&format!("0 0 1 RG 6 w {cap} 10 20 m 30 20 l h {after} S"), "<< >>");
assert_eq!(px(&page, 20, 20), BLUE, "{what} after h with {cap}: the closed line");
assert_eq!(px(&page, 8, 20), WHITE, "{what} after h with {cap}: no dot");
}
}
// A segment after h that goes somewhere is an open subpath with caps of its own: with square
// caps the line from (10, 20) down to (10, 35) reaches y 38 (device row 2).
let page = render("0 0 1 RG 6 w 2 J 10 20 m 30 20 l h 10 35 l S", "<< >>");
assert_eq!(px(&page, 10, 3), BLUE, "the new subpath's projecting cap");
}
/// §8.4.3.6 asks for dash numbers that are nonnegative and not all zero, but not how to recover
/// from others. They draw a solid line, as an empty dash array does; before, `[0 0]` was cut into
/// hundredth-of-a-point dashes and came out a faint, half-covered line.
+1 -1
View File
File diff suppressed because one or more lines are too long
+6 -1
View File
@@ -5,7 +5,8 @@ use crate::convert::{convert_dash_array, convert_line_cap, convert_line_join, co
use crate::device::Device;
use crate::font::{Font, FontData, FontQuery, StandardFont};
use crate::interpret::path::{
apply_pending_clip, close_path, fill_path, fill_path_impl, fill_stroke_path, stroke_path,
apply_pending_clip, begin_subpath_after_close, close_path, fill_path, fill_path_impl,
fill_stroke_path, stroke_path,
};
use crate::interpret::state::{TextStateFont, handle_gs};
use crate::interpret::text::TextRenderingMode;
@@ -568,6 +569,7 @@ fn interpret_window<'a>(
}
TypedInstruction::LineTo(m) => {
if !context.path().elements().is_empty() {
begin_subpath_after_close(context);
// PdfCraft patch (61): a segment to the point it starts from is kept as it
// is. Upstream nudged its end a ten-thousandth of a point to the right so
// round caps drew a dot, which also gave butt and square caps a direction
@@ -580,6 +582,7 @@ fn interpret_window<'a>(
}
TypedInstruction::CubicTo(c) => {
if !context.path().elements().is_empty() {
begin_subpath_after_close(context);
let p1 = Point::new(c.0.as_f64(), c.1.as_f64());
let p2 = Point::new(c.2.as_f64(), c.3.as_f64());
let p3 = Point::new(c.4.as_f64(), c.5.as_f64());
@@ -591,6 +594,7 @@ fn interpret_window<'a>(
}
TypedInstruction::CubicStartTo(c) => {
if !context.path().elements().is_empty() {
begin_subpath_after_close(context);
let p1 = *context.last_point();
let p2 = Point::new(c.0.as_f64(), c.1.as_f64());
let p3 = Point::new(c.2.as_f64(), c.3.as_f64());
@@ -602,6 +606,7 @@ fn interpret_window<'a>(
}
TypedInstruction::CubicEndTo(c) => {
if !context.path().elements().is_empty() {
begin_subpath_after_close(context);
let p2 = Point::new(c.0.as_f64(), c.1.as_f64());
let p3 = Point::new(c.2.as_f64(), c.3.as_f64());
+15
View File
@@ -67,6 +67,21 @@ pub(crate) fn close_path(context: &mut Context<'_>) {
}
}
/// PdfCraft patch (61): call before appending a segment. ISO 32000-2 §8.5.2.1 (Table 58) says `h`
/// "terminates the current subpath. Appending another segment to the current path shall begin a
/// new subpath, even if the new segment begins at the endpoint reached by the h operation." kurbo
/// takes a segment after a `ClosePath` on from the closed subpath's start without a `MoveTo`, so
/// `resolve_degenerate_subpaths` saw the closed subpath and a degenerate one after it (`h` then an
/// `l` to the same point) as one subpath and drew no round dot. A `MoveTo` to the point `h`
/// reached starts the new subpath.
pub(crate) fn begin_subpath_after_close(context: &mut Context<'_>) {
if context.path().elements().last() == Some(&PathEl::ClosePath) {
let p = *context.last_point();
*(context.sub_path_start_mut()) = p;
context.path_mut().move_to(p);
}
}
pub(crate) fn fill_path_impl<'a>(
context: &mut Context<'a>,
device: &mut impl Device<'a>,