mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 14:41:51 +00:00
M12: XFA border corners not listed are drawn the way edges are
XFA 3.3 p. 38 gives a border's corners in the order top-left, top-right, bottom-right, bottom-left, as its edges go top, right, bottom, left, and says that when fewer than four are listed the last one listed is reused for the rest. Readers don't do that: every reader tested draws a side not listed from the edge or corner on the same side, one listed serving all four sides, two alternating (0, 1, 0, 1), three reusing the second for the fourth (0, 1, 2, 1). The layout already drew edges the readers' way, but drew corners as p. 38's text says, so with corners [round, square] only the top-left was round where readers round the top-left and the bottom-right. Corners now follow the readers too, through one helper, drawn_side, that edges and corners share: with [round, square] the top-left and bottom-right are round and the top-right and bottom-left square. Edges are drawn as before. The scripting host's reads of a corner past those listed (from the previous commit) mirror the same pattern, so a script reads the corner the page draws. Real forms (358 public forms with XFA, templates read through the engine): 337 borders in 51 forms list two or three corners that differ, all only in a square corner's presence, which the layout doesn't draw differently; none has a rounded or inverted corner that would move, so no rendered page changes. Tests: one, two and three corners, two in both orders, and the rounded-corners test's two-corner box (now [r1, r2, r1, r2]) fail with the change reverted; a script test reads corners past those listed by the same pattern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CBVyQEwtmEw3mS1MoAiwrK
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
34f3716f5e
commit
8d40450d22
@@ -837,8 +837,10 @@ pub(crate) fn node_color(h: &XHost, node: usize, path: &str) -> String {
|
||||
/// than four. XFA 3.3 p. 38 says the last one listed is reused for the rest; readers draw a
|
||||
/// side not listed from the edge or corner on the same side instead, and reads follow them:
|
||||
/// one serves every side, two alternate (0, 1, 0, 1), three reuse the second for the fourth
|
||||
/// (0, 1, 2, 1). `None` past the four sides, and when none is listed (the side is a default
|
||||
/// one).
|
||||
/// (0, 1, 2, 1). `None` past the four sides, and when none is
|
||||
/// listed (the side is a default one). The layout picks edges and corners with the same
|
||||
/// pattern (`drawn_side` in `pdfcraft_xfa`, which this crate doesn't depend on); tests on both
|
||||
/// sides pin it.
|
||||
fn drawn_index(listed: usize, index: usize) -> Option<usize> {
|
||||
match listed {
|
||||
// A border has four sides: edges and corners occur at most four times (p. 629).
|
||||
|
||||
@@ -688,6 +688,24 @@ fn border_color_colours_all_four_edges() {
|
||||
assert_eq!(eval("this.borderColor = '7,7,7'; this.border.getElement('edge', 3).thickness", "styled"), "2pt");
|
||||
}
|
||||
|
||||
/// A corner past those a border lists reads as the corner drawn there: one serves all four,
|
||||
/// two alternate, three reuse the second for the fourth, as for edges.
|
||||
#[test]
|
||||
fn corners_not_listed_read_as_the_corner_drawn_there() {
|
||||
let corners = |listed: &[&str]| {
|
||||
let kids = listed.iter().map(|r| p("corner", &[("radius", r)], vec![])).collect();
|
||||
let f = node("c", XfaKind::Field, &[], vec![p("border", &[], kids)]);
|
||||
let mut root = form();
|
||||
root.children.push(f);
|
||||
let o = go_on(&root, "[0, 1, 2, 3].map(function (i) { return this.border.getElement('corner', i).radius; }, this).join('|')", "c");
|
||||
assert_eq!(o.error, None, "{o:?}");
|
||||
o.result.unwrap_or_default()
|
||||
};
|
||||
assert_eq!(corners(&["1pt"]), "1pt|1pt|1pt|1pt");
|
||||
assert_eq!(corners(&["1pt", "2pt"]), "1pt|2pt|1pt|2pt");
|
||||
assert_eq!(corners(&["1pt", "2pt", "3pt"]), "1pt|2pt|3pt|2pt");
|
||||
}
|
||||
|
||||
/// A border's presence reads `hidden` when the object has no `<border>` element, as readers
|
||||
/// read it, and `visible` when it has one that doesn't say (XFA 3.3 p. 629).
|
||||
#[test]
|
||||
|
||||
+25
-16
@@ -136,26 +136,36 @@ fn parse_fill(n: Option<XmlNode>) -> Option<Color> {
|
||||
Some(parse_color(child(n, "color")).unwrap_or(Color::WHITE))
|
||||
}
|
||||
|
||||
/// `<border>`: edges in XFA order. One edge applies to all four; two: top/bottom then
|
||||
/// left/right; three: top, left/right, bottom; four: top, right, bottom, left.
|
||||
/// `<border>`: edges top, right, bottom, left (XFA 3.3 p. 38), the sides not listed drawn as
|
||||
/// readers draw them ([`drawn_side`]), not as p. 38's text says.
|
||||
fn parse_border(n: Option<XmlNode>) -> Option<Border> {
|
||||
let n = n?;
|
||||
let edges: Vec<Edge> = children(n, "edge").map(parse_edge).collect();
|
||||
let d = Edge::default();
|
||||
let e = |i: usize| edges.get(i).copied().unwrap_or(d);
|
||||
let four = match edges.len() {
|
||||
0 => [d; 4],
|
||||
1 => [e(0); 4],
|
||||
2 => [e(0), e(1), e(0), e(1)],
|
||||
3 => [e(0), e(1), e(2), e(1)],
|
||||
_ => [e(0), e(1), e(2), e(3)],
|
||||
};
|
||||
let edges: Vec<Edge> = children(n, "edge").take(4).map(parse_edge).collect();
|
||||
let four = std::array::from_fn(|i| drawn_side(edges.len(), i).and_then(|k| edges.get(k).copied()).unwrap_or_default());
|
||||
Some(Border { presence: Presence::parse(n.attribute("presence")), edges: four, corners: parse_corners(n), fill: parse_fill(child(n, "fill")) })
|
||||
}
|
||||
|
||||
/// Which of the `listed` edges (or corners) of a border is drawn on side `index`: edges go
|
||||
/// top, right, bottom, left and corners top-left, top-right, bottom-right, bottom-left (XFA 3.3
|
||||
/// p. 38). For a border that lists fewer than four, p. 38 says the last one listed is reused
|
||||
/// for the rest; readers draw a side not listed from the edge or corner on the same side
|
||||
/// instead, and so does this: one serves every side, two alternate (0, 1, 0, 1), three reuse
|
||||
/// the second for the fourth (0, 1, 2, 1). Edges already followed the readers; corners now do
|
||||
/// too. The scripting host's reads mirror the same pattern so a script reads what is drawn.
|
||||
/// `None` when none is listed.
|
||||
pub(crate) fn drawn_side(listed: usize, index: usize) -> Option<usize> {
|
||||
match listed {
|
||||
0 => None,
|
||||
_ if index < listed => Some(index),
|
||||
1 => Some(0),
|
||||
2 => Some(index % 2),
|
||||
_ => Some(1),
|
||||
}
|
||||
}
|
||||
|
||||
/// The `<corner>` children of a border or rectangle, top-left, top-right, bottom-right then
|
||||
/// bottom-left: one applies to all four, and a corner not given is the last one given. Without
|
||||
/// any, the corners are square.
|
||||
/// bottom-left, the ones not listed drawn as [`drawn_side`] picks, as edges are. Without any,
|
||||
/// the corners are square.
|
||||
fn parse_corners(n: XmlNode) -> [Corner; 4] {
|
||||
let given: Vec<Corner> = children(n, "corner")
|
||||
.take(4)
|
||||
@@ -165,8 +175,7 @@ fn parse_corners(n: XmlNode) -> [Corner; 4] {
|
||||
inverted: c.attribute("inverted") == Some("1"),
|
||||
})
|
||||
.collect();
|
||||
let Some(last) = given.last().copied() else { return [Corner::default(); 4] };
|
||||
std::array::from_fn(|i| given.get(i).copied().unwrap_or(last))
|
||||
std::array::from_fn(|i| drawn_side(given.len(), i).and_then(|k| given.get(k).copied()).unwrap_or_default())
|
||||
}
|
||||
|
||||
/// `<font>`: typeface, size, weight, posture, underline, fill color and horizontal scale.
|
||||
|
||||
+30
-1
@@ -2412,8 +2412,9 @@ fn rounded_corners_make_a_border_one_rounded_shape() {
|
||||
let round = at(0.0).expect("the rounded border is one shape");
|
||||
assert_eq!((round.rect.w, round.rect.h, round.radii), (144.0, 72.0, [18.0; 4]));
|
||||
assert_eq!((round.fill, round.edge), (Some([230.0 / 255.0; 3]), Some((1.0, [1.0, 0.0, 0.0], false))));
|
||||
// Two corners alternate: the first is drawn top-left and bottom-right.
|
||||
let mixed = at(1.2).expect("the box with two corners given");
|
||||
assert_eq!((mixed.radii, mixed.inverted), ([7.2, 14.4, 14.4, 14.4], [false, true, true, true]));
|
||||
assert_eq!((mixed.radii, mixed.inverted), ([7.2, 14.4, 7.2, 14.4], [false, true, false, true]));
|
||||
assert_eq!(at(2.4).expect("the huge radius").radii, [36.0; 4], "at most half the shorter side");
|
||||
assert!(at(3.6).is_none() && at(4.8).is_none(), "hidden corners and unlike edges keep a square box");
|
||||
let fill_only = at(6.0).expect("a fill with hidden edges is rounded too");
|
||||
@@ -2431,6 +2432,34 @@ fn rounded_corners_make_a_border_one_rounded_shape() {
|
||||
assert_eq!((lines_at(3.6), lines_at(4.8)), (4, 2));
|
||||
}
|
||||
|
||||
/// Corners not listed are drawn the way edges are, from the corner on the same side, as
|
||||
/// readers draw them (XFA 3.3 p. 38 gives the order top-left, top-right, bottom-right,
|
||||
/// bottom-left, and says the last corner listed is reused): one serves all four, two
|
||||
/// alternate, three reuse the second for the bottom-left.
|
||||
#[test]
|
||||
fn corners_not_listed_follow_the_edge_pattern() {
|
||||
let radii = |corners: &str| {
|
||||
let body = format!(
|
||||
r#"<subform name="body" layout="position" w="7.5in" h="9in"><draw name="d" x="0in" y="0in" w="2in" h="1in"><value><text>d</text></value><border><edge/>{corners}</border></draw></subform>"#
|
||||
);
|
||||
let form = layout_xml(&one_area_form(r#"layout="tb""#, &body)).unwrap();
|
||||
rounded(&form.pages[0]).first().map(|r| r.radii)
|
||||
};
|
||||
let (round, square) = (r#"<corner radius="0.25in"/>"#, r#"<corner/>"#);
|
||||
assert_eq!(radii(&format!("{round}{square}")), Some([18.0, 0.0, 18.0, 0.0]), "round top-left and bottom-right");
|
||||
assert_eq!(radii(&format!("{square}{round}")), Some([0.0, 18.0, 0.0, 18.0]));
|
||||
assert_eq!(
|
||||
radii(r#"<corner radius="5pt"/><corner radius="10pt"/><corner radius="15pt"/>"#),
|
||||
Some([5.0, 10.0, 15.0, 10.0]),
|
||||
"the second serves the bottom-left"
|
||||
);
|
||||
assert_eq!(radii(round), Some([18.0; 4]));
|
||||
for (listed, sides) in [(1, [0, 0, 0, 0]), (2, [0, 1, 0, 1]), (3, [0, 1, 2, 1]), (4, [0, 1, 2, 3])] {
|
||||
assert_eq!(std::array::from_fn::<_, 4, _>(|i| crate::parse::drawn_side(listed, i)), sides.map(Some), "{listed}");
|
||||
}
|
||||
assert_eq!(crate::parse::drawn_side(0, 2), None);
|
||||
}
|
||||
|
||||
/// A rounded box a page break cuts keeps its rounded corners on each page, square where it was
|
||||
/// cut, and its edge stops there instead of closing across the cut.
|
||||
#[test]
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user