mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 04:12:00 +00:00
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:
co-authored by
Claude Opus 5.5
parent
07cab37e7b
commit
ba16b9b5b8
@@ -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.
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+6
-1
@@ -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
@@ -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>,
|
||||
|
||||
Reference in New Issue
Block a user