mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 13:35:21 +00:00
address review: pad hairlines by half a unit, rectangles for their corners, square caps by sqrt 2
This commit is contained in:
@@ -72,6 +72,8 @@ struct Gs {
|
||||
line_width: f64,
|
||||
miter_limit: f64,
|
||||
line_join: i64,
|
||||
/// `J` / ExtGState `/LC`: a projecting square cap (2) reaches past the endpoint's corner.
|
||||
line_cap: i64,
|
||||
}
|
||||
|
||||
/// What processing a scope produced.
|
||||
@@ -245,6 +247,7 @@ pub(crate) fn process(doc: &mut Document, scope: &mut Scope<'_>, streams: &[Vec<
|
||||
line_width: 1.0,
|
||||
miter_limit: 10.0,
|
||||
line_join: 0,
|
||||
line_cap: 0,
|
||||
};
|
||||
process_from(doc, scope, streams, resources, gs)
|
||||
}
|
||||
@@ -389,7 +392,7 @@ impl Walk {
|
||||
self.clip = false;
|
||||
}
|
||||
let kept = match o {
|
||||
b"q" | b"Q" | b"cm" | b"Tf" | b"gs" | b"w" | b"M" | b"j" => {
|
||||
b"q" | b"Q" | b"cm" | b"Tf" | b"gs" | b"w" | b"M" | b"j" | b"J" => {
|
||||
self.graphics_state(doc, scope, &op);
|
||||
Some(op)
|
||||
}
|
||||
@@ -427,15 +430,20 @@ impl Walk {
|
||||
|
||||
/// The box a stroke of the current path can paint, grown from its construction box `b` by
|
||||
/// what the pen adds: half the line width (a width of 0 is one device pixel, taken as one
|
||||
/// unit), times the miter limit when the path has joins and they are mitred, scaled by the
|
||||
/// CTM's largest stretch. Caps and joins never reach further, so a hit test on it is
|
||||
/// conservative, and a horizontal or vertical line no longer has a zero-area box.
|
||||
/// unit) scaled by the CTM's largest stretch, and never less than half a unit of page space
|
||||
/// (a renderer paints a hairline, or a line a CTM shrinks, about a device pixel wide); times
|
||||
/// the miter limit when the path has joins and they are mitred (a rectangle has four), or
|
||||
/// √2 for a projecting square cap (its outer corner). Caps and joins never reach further, so
|
||||
/// a hit test on it is conservative, and a horizontal or vertical line no longer has a
|
||||
/// zero-area box.
|
||||
fn stroke_box(&self, b: [f64; 4]) -> [f64; 4] {
|
||||
let m = self.gs.ctm.0;
|
||||
let scale = (m[0] * m[0] + m[1] * m[1] + m[2] * m[2] + m[3] * m[3]).sqrt();
|
||||
let joins = self.path.iter().filter(|op| matches!(op.op.as_slice(), b"l" | b"c" | b"v" | b"y" | b"h" | b"re")).count() >= 2;
|
||||
let joins = self.path.iter().filter(|op| matches!(op.op.as_slice(), b"l" | b"c" | b"v" | b"y" | b"h" | b"re")).count() >= 2
|
||||
|| self.path.iter().any(|op| op.op.as_slice() == b"re");
|
||||
let miter = if joins && self.gs.line_join == 0 { self.gs.miter_limit.max(1.0) } else { 1.0 };
|
||||
let pad = self.gs.line_width.max(1.0) / 2.0 * miter * scale;
|
||||
let reach = if self.gs.line_cap == 2 { miter.max(std::f64::consts::SQRT_2) } else { miter };
|
||||
let pad = (self.gs.line_width.max(1.0) / 2.0 * scale).max(0.5) * reach;
|
||||
[b[0] - pad, b[1] - pad, b[2] + pad, b[3] + pad]
|
||||
}
|
||||
|
||||
@@ -480,6 +488,7 @@ impl Walk {
|
||||
b"w" => self.gs.line_width = op.num(0).unwrap_or(self.gs.line_width),
|
||||
b"M" => self.gs.miter_limit = op.num(0).unwrap_or(self.gs.miter_limit),
|
||||
b"j" => self.gs.line_join = op.num(0).map_or(self.gs.line_join, |j| j as i64),
|
||||
b"J" => self.gs.line_cap = op.num(0).map_or(self.gs.line_cap, |j| j as i64),
|
||||
_ => {
|
||||
if let Some((font, vertical, size)) = op.name(0).and_then(|n| extgstate_font(doc, scope, &self.ext_gstates, n, &self.fallback)) {
|
||||
(self.gs.font, self.gs.vertical, self.gs.size) = (font, vertical, size);
|
||||
@@ -489,6 +498,7 @@ impl Walk {
|
||||
self.gs.line_width = num(b"LW").unwrap_or(self.gs.line_width);
|
||||
self.gs.miter_limit = num(b"ML").unwrap_or(self.gs.miter_limit);
|
||||
self.gs.line_join = num(b"LJ").map_or(self.gs.line_join, |j| j as i64);
|
||||
self.gs.line_cap = num(b"LC").map_or(self.gs.line_cap, |j| j as i64);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3887,3 +3887,31 @@ fn a_reply_on_another_page_to_a_removed_popup_goes_too() {
|
||||
assert!(!bytes.windows(gone.len()).any(|w| w == gone), "{} survived", String::from_utf8_lossy(gone));
|
||||
}
|
||||
}
|
||||
|
||||
/// A hairline under a shrinking CTM still paints about a device pixel: its pad must not shrink
|
||||
/// with the CTM, or a mark on that ink misses it.
|
||||
#[test]
|
||||
fn a_hairline_under_a_shrinking_ctm_is_padded_by_half_a_unit() {
|
||||
// Device line from (10, 10) to (50, 10); the mark lies on its painted pixel, above its axis.
|
||||
let mut doc = one_page(b"0.01 0 0 0.01 0 0 cm 0 w 1000 1000 m 5000 1000 l S", "", vec![]);
|
||||
mark(&mut doc, 0, &[[20.0, 10.2, 30.0, 10.4]], "");
|
||||
assert_eq!(apply(&mut doc, None).unwrap().paths_removed, 1);
|
||||
}
|
||||
|
||||
/// One `re` has four mitred corners: it is padded by the miter limit like any joined path.
|
||||
#[test]
|
||||
fn a_rectangle_stroke_is_padded_for_its_mitred_corners() {
|
||||
let mut doc = one_page(b"2 w 100 100 50 50 re S", "", vec![]);
|
||||
// Past half the line width from the rectangle, inside the miter reach.
|
||||
mark(&mut doc, 0, &[[155.0, 120.0, 160.0, 130.0]], "");
|
||||
assert_eq!(apply(&mut doc, None).unwrap().paths_removed, 1);
|
||||
}
|
||||
|
||||
/// A projecting square cap's outer corner reaches lw/2 x sqrt(2) from the endpoint. Under a shear
|
||||
/// it maps past a pad of lw/2 times the CTM's stretch (the review's example: corner at x = 12).
|
||||
#[test]
|
||||
fn a_square_cap_corner_under_a_shear_is_inside_the_pad() {
|
||||
let mut doc = one_page(b"1 0 1 1 0 0 cm 2 J 2 w 0 0 m 10 0 l S", "", vec![]);
|
||||
mark(&mut doc, 0, &[[11.8, 0.8, 11.9, 0.95]], "");
|
||||
assert_eq!(apply(&mut doc, None).unwrap().paths_removed, 1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user