mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
Merge PR #4319: M12: XFA borders and rectangles with rounded corners are drawn rounded
parity/acrobat-features.toml: form.xfa-dynamic evidence: both sides' entries; notes: both sides' insertions.
This commit is contained in:
@@ -44,13 +44,45 @@ pub(crate) const UNDERLINE_DROP: f64 = 0.1;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Item {
|
||||
Fill { rect: Rect, color: [f64; 3] },
|
||||
Line { from: (f64, f64), to: (f64, f64), width: f64, color: [f64; 3], dashed: bool },
|
||||
Fill {
|
||||
rect: Rect,
|
||||
color: [f64; 3],
|
||||
},
|
||||
Line {
|
||||
from: (f64, f64),
|
||||
to: (f64, f64),
|
||||
width: f64,
|
||||
color: [f64; 3],
|
||||
dashed: bool,
|
||||
},
|
||||
/// A box with rounded (or inverted) corners: its fill, and its edge drawn inside it.
|
||||
Rounded(Box<Rounded>),
|
||||
Text(Span),
|
||||
Image { rect: Rect, data: Arc<Vec<u8>>, content_type: String },
|
||||
Image {
|
||||
rect: Rect,
|
||||
data: Arc<Vec<u8>>,
|
||||
content_type: String,
|
||||
},
|
||||
Widget(Box<Widget>),
|
||||
}
|
||||
|
||||
/// A box with rounded corners ([`Item::Rounded`]).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Rounded {
|
||||
pub rect: Rect,
|
||||
/// Top-left, top-right, bottom-right and bottom-left radii, each at most half the box's
|
||||
/// shorter side.
|
||||
pub radii: [f64; 4],
|
||||
/// Corners whose curve goes into the box rather than around it, in the same order.
|
||||
pub inverted: [bool; 4],
|
||||
pub fill: Option<[f64; 3]>,
|
||||
/// The edge: its width, colour and whether it is dashed.
|
||||
pub edge: Option<(f64, [f64; 3], bool)>,
|
||||
/// A page break cut the box here: that side has no edge, and its corners are square.
|
||||
pub open_top: bool,
|
||||
pub open_bottom: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum WidgetKind {
|
||||
Text,
|
||||
@@ -339,7 +371,7 @@ fn solid_extent(item: &Item) -> Option<(f64, f64)> {
|
||||
Item::Text(s) => Some((s.baseline - s.face.size * 0.8, s.baseline + s.face.size * 0.2)),
|
||||
Item::Image { rect, .. } => Some((rect.y, rect.bottom())),
|
||||
Item::Widget(w) => Some((w.rect.y, w.rect.bottom())),
|
||||
Item::Fill { .. } | Item::Line { .. } => None,
|
||||
Item::Fill { .. } | Item::Line { .. } | Item::Rounded(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,6 +406,23 @@ fn divide(item: Item, cut: f64) -> (Option<Item>, Option<Item>) {
|
||||
(Some(Item::Line { from: lo, to: mid, width, color, dashed }), Some(Item::Line { from: mid, to: hi, width, color, dashed }))
|
||||
}
|
||||
}
|
||||
Item::Rounded(r) => {
|
||||
if r.rect.bottom() <= cut + 0.01 {
|
||||
(Some(Item::Rounded(r)), None)
|
||||
} else if r.rect.y >= cut - 0.01 {
|
||||
(None, Some(Item::Rounded(r)))
|
||||
} else {
|
||||
// Each part keeps the corners on its side, rounded no more than its own height
|
||||
// allows, and is open where it was cut.
|
||||
let [tl, tr, br, bl] = r.radii;
|
||||
let above = Rect::new(r.rect.x, r.rect.y, r.rect.w, cut - r.rect.y);
|
||||
let below = Rect::new(r.rect.x, cut, r.rect.w, r.rect.bottom() - cut);
|
||||
let fit = |v: f64, h: f64| v.min(h.max(0.0));
|
||||
let upper = Rounded { rect: above, radii: [fit(tl, above.h), fit(tr, above.h), 0.0, 0.0], open_bottom: true, ..(*r).clone() };
|
||||
let lower = Rounded { rect: below, radii: [0.0, 0.0, fit(br, below.h), fit(bl, below.h)], open_top: true, ..*r };
|
||||
(Some(Item::Rounded(Box::new(upper))), Some(Item::Rounded(Box::new(lower))))
|
||||
}
|
||||
}
|
||||
other => (Some(other), None),
|
||||
}
|
||||
}
|
||||
@@ -385,6 +434,10 @@ fn moved(mut item: Item, dx: f64, dy: f64) -> Item {
|
||||
rect.x += dx;
|
||||
rect.y += dy;
|
||||
}
|
||||
Item::Rounded(r) => {
|
||||
r.rect.x += dx;
|
||||
r.rect.y += dy;
|
||||
}
|
||||
Item::Line { from, to, .. } => {
|
||||
*from = (from.0 + dx, from.1 + dy);
|
||||
*to = (to.0 + dx, to.1 + dy);
|
||||
@@ -1408,11 +1461,35 @@ impl Layouter<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw border `b` around `rect`: its fill, then each shown edge as a line inside the box, or
|
||||
/// the whole box as one rounded shape when its corners are rounded.
|
||||
fn draw_border(&mut self, b: &Border, rect: Rect) -> Result<(), XfaError> {
|
||||
let vis = b.visible_edges();
|
||||
// Rounded corners: the box is one shape, filled and edged along its curves, when its
|
||||
// edges are all alike (all shown and the same, or all hidden). Edges that differ keep
|
||||
// square corners.
|
||||
let radii = b.radii(rect.w, rect.h);
|
||||
let alike = b.edges.iter().all(|e| *e == b.edges[0]);
|
||||
if radii.iter().any(|r| *r > 0.0) && (vis == [true; 4] && alike || vis == [false; 4]) {
|
||||
let e = b.edges[0];
|
||||
let edge = vis[0].then(|| {
|
||||
let color = if e.stroke == Stroke::Relief { [0.55, 0.55, 0.55] } else { e.color.0 };
|
||||
(e.thickness.clamp(0.1, 20.0), color, matches!(e.stroke, Stroke::Dashed | Stroke::Dotted))
|
||||
});
|
||||
let shape = Rounded {
|
||||
rect,
|
||||
radii,
|
||||
inverted: b.corners.map(|c| c.inverted),
|
||||
fill: b.fill().map(|f| f.0),
|
||||
edge,
|
||||
open_top: false,
|
||||
open_bottom: false,
|
||||
};
|
||||
return self.push(Item::Rounded(Box::new(shape)));
|
||||
}
|
||||
if let Some(fill) = b.fill() {
|
||||
self.push(Item::Fill { rect, color: fill.0 })?;
|
||||
}
|
||||
let vis = b.visible_edges();
|
||||
let (x0, y0, x1, y1) = (rect.x, rect.y, rect.right(), rect.bottom());
|
||||
// Top, right, bottom, left; lines sit inside the box.
|
||||
let segs = [((x0, y0), (x1, y0)), ((x1, y0), (x1, y1)), ((x0, y1), (x1, y1)), ((x0, y0), (x0, y1))];
|
||||
|
||||
+22
-2
@@ -125,11 +125,22 @@ impl Edge {
|
||||
}
|
||||
}
|
||||
|
||||
/// A box border: four edges (top, right, bottom, left) and a fill.
|
||||
/// One corner of a border: how far it rounds the box (`radius`), and whether the curve goes
|
||||
/// into the box (`inverted`) rather than around it.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Corner {
|
||||
pub presence: Presence,
|
||||
pub radius: f64,
|
||||
pub inverted: bool,
|
||||
}
|
||||
|
||||
/// A box border: four edges (top, right, bottom, left), four corners (top-left, top-right,
|
||||
/// bottom-right, bottom-left) and a fill.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Border {
|
||||
pub presence: Presence,
|
||||
pub edges: [Edge; 4],
|
||||
pub corners: [Corner; 4],
|
||||
pub fill: Option<Color>,
|
||||
}
|
||||
|
||||
@@ -144,6 +155,15 @@ impl Border {
|
||||
pub fn fill(&self) -> Option<Color> {
|
||||
if self.presence == Presence::Visible { self.fill } else { None }
|
||||
}
|
||||
|
||||
/// How far each corner (top-left, top-right, bottom-right, bottom-left) rounds a box `w` by
|
||||
/// `h`: its radius when the corner is shown, no more than half the box's shorter side, and 0
|
||||
/// for a hidden corner or a hidden border.
|
||||
pub fn radii(&self, w: f64, h: f64) -> [f64; 4] {
|
||||
let most = (w.min(h) / 2.0).max(0.0);
|
||||
let shown = self.presence == Presence::Visible;
|
||||
self.corners.map(|c| if shown && c.presence == Presence::Visible && c.radius.is_finite() { c.radius.clamp(0.0, most) } else { 0.0 })
|
||||
}
|
||||
}
|
||||
|
||||
/// Font properties as written on an element; `None` inherits from the ancestors.
|
||||
@@ -581,7 +601,7 @@ pub enum Value {
|
||||
slope_up: bool,
|
||||
edge: Edge,
|
||||
},
|
||||
Rectangle(Border),
|
||||
Rectangle(Box<Border>),
|
||||
}
|
||||
|
||||
impl Value {
|
||||
|
||||
+18
-2
@@ -146,7 +146,23 @@ fn parse_border(n: Option<XmlNode>) -> Option<Border> {
|
||||
3 => [e(0), e(1), e(2), e(1)],
|
||||
_ => [e(0), e(1), e(2), e(3)],
|
||||
};
|
||||
Some(Border { presence: Presence::parse(n.attribute("presence")), edges: four, fill: parse_fill(child(n, "fill")) })
|
||||
Some(Border { presence: Presence::parse(n.attribute("presence")), edges: four, corners: parse_corners(n), fill: parse_fill(child(n, "fill")) })
|
||||
}
|
||||
|
||||
/// 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.
|
||||
fn parse_corners(n: XmlNode) -> [Corner; 4] {
|
||||
let given: Vec<Corner> = children(n, "corner")
|
||||
.take(4)
|
||||
.map(|c| Corner {
|
||||
presence: Presence::parse(c.attribute("presence")),
|
||||
radius: attr_measure(c, "radius").filter(|r| r.is_finite()).unwrap_or(0.0).max(0.0),
|
||||
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))
|
||||
}
|
||||
|
||||
/// `<font>`: typeface, size, weight, posture, underline, fill color and horizontal scale.
|
||||
@@ -389,7 +405,7 @@ fn parse_value(n: Option<XmlNode>, warnings: &mut Vec<String>) -> Value {
|
||||
"rectangle" => {
|
||||
let mut b = parse_border(Some(c)).unwrap_or_default();
|
||||
b.fill = parse_fill(child(c, "fill"));
|
||||
return Value::Rectangle(b);
|
||||
return Value::Rectangle(Box::new(b));
|
||||
}
|
||||
"date" | "time" | "dateTime" | "integer" | "decimal" | "float" | "boolean" => return Value::Text(text_of(c)),
|
||||
_ => {}
|
||||
|
||||
+73
-1
@@ -8,7 +8,7 @@ use pdfcraft_cos::{Dict, Document, ObjRef, Object, PdfString, Stream};
|
||||
use pdfcraft_fonts::{literal, win_ansi};
|
||||
|
||||
use crate::XfaError;
|
||||
use crate::layout::{Action, BorderShape, Form, Item, Page, Widget, WidgetKind};
|
||||
use crate::layout::{Action, BorderShape, Form, Item, Page, Rounded, Widget, WidgetKind};
|
||||
use crate::model::Rect;
|
||||
use crate::text::{Face, Family, Span};
|
||||
|
||||
@@ -129,6 +129,77 @@ fn all_faces() -> Vec<(&'static str, &'static str)> {
|
||||
out
|
||||
}
|
||||
|
||||
/// The content that draws rounded box `r` on a page `page_h` tall: its fill along its curves,
|
||||
/// then its edge inside it. A side a page break cut has no edge.
|
||||
pub(crate) fn rounded(r: &Rounded, page_h: f64) -> String {
|
||||
let mut c = String::new();
|
||||
if let Some(color) = r.fill {
|
||||
c.push_str(&format!("{} rg {}f\n", rgb(color), outline(r.rect, r.radii, r.inverted, (false, false), page_h)));
|
||||
}
|
||||
if let Some((w, color, dashed)) = r.edge {
|
||||
// The edge's centre line, half its width inside the box, so the edge stays in: a corner
|
||||
// curving around the box tightens by as much, and one curving into it widens.
|
||||
let inset = Rect::new(r.rect.x + w / 2.0, r.rect.y + w / 2.0, (r.rect.w - w).max(0.0), (r.rect.h - w).max(0.0));
|
||||
let most = inset.w.min(inset.h) / 2.0;
|
||||
let radii: [f64; 4] = std::array::from_fn(|i| match (r.radii[i], r.inverted[i]) {
|
||||
(v, _) if v <= 0.0 => 0.0,
|
||||
(v, true) => (v + w / 2.0).min(most),
|
||||
(v, false) => (v - w / 2.0).clamp(0.0, most),
|
||||
});
|
||||
let path = outline(inset, radii, r.inverted, (r.open_top, r.open_bottom), page_h);
|
||||
c.push_str(&format!("q {} RG {} w {}{path}S Q\n", rgb(color), n(w), if dashed { "[2 2] 0 d " } else { "" }));
|
||||
}
|
||||
c
|
||||
}
|
||||
|
||||
/// The path around `rect` (top-left based, on a page `page_h` tall) with the corners `radii`
|
||||
/// (top-left, top-right, bottom-right, bottom-left), each curving around the box or, when
|
||||
/// `inverted`, into it. `open` leaves out the top or bottom side (and doesn't close the path):
|
||||
/// those are where a page break cut the box. Ends with a space, for the paint operator.
|
||||
fn outline(rect: Rect, radii: [f64; 4], inverted: [bool; 4], open: (bool, bool), page_h: f64) -> String {
|
||||
let (x0, y0, x1, y1) = (rect.x, rect.y, rect.right(), rect.bottom());
|
||||
let pt = |q: (f64, f64)| format!("{} {}", n(q.0), n(page_h - q.1));
|
||||
// Corner `i`: its point, then where its curve starts and ends, going clockwise.
|
||||
let corner = |i: usize| {
|
||||
let r = radii.get(i).copied().unwrap_or(0.0);
|
||||
match i {
|
||||
0 => ((x0, y0), (x0, y0 + r), (x0 + r, y0)),
|
||||
1 => ((x1, y0), (x1 - r, y0), (x1, y0 + r)),
|
||||
2 => ((x1, y1), (x1, y1 - r), (x1 - r, y1)),
|
||||
_ => ((x0, y1), (x0 + r, y1), (x0, y1 - r)),
|
||||
}
|
||||
};
|
||||
// Corner `i`'s curve, from its start: a quarter circle as one Bézier curve.
|
||||
let curve = |i: usize| {
|
||||
if radii.get(i).copied().unwrap_or(0.0) <= 0.0 {
|
||||
return String::new();
|
||||
}
|
||||
let (p, a, b) = corner(i);
|
||||
const K: f64 = 0.5523;
|
||||
let (c1, c2) = if inverted.get(i).copied().unwrap_or(false) {
|
||||
// Centred on the corner itself.
|
||||
((a.0 + (b.0 - p.0) * K, a.1 + (b.1 - p.1) * K), (b.0 + (a.0 - p.0) * K, b.1 + (a.1 - p.1) * K))
|
||||
} else {
|
||||
((a.0 + (p.0 - a.0) * K, a.1 + (p.1 - a.1) * K), (b.0 + (p.0 - b.0) * K, b.1 + (p.1 - b.1) * K))
|
||||
};
|
||||
format!("{} {} {} c ", pt(c1), pt(c2), pt(b))
|
||||
};
|
||||
// Along corners `from`, joining them with straight sides.
|
||||
let along = |from: &[usize]| {
|
||||
let mut s = String::new();
|
||||
for &i in from {
|
||||
s.push_str(&format!("{} l {}", pt(corner(i).1), curve(i)));
|
||||
}
|
||||
s
|
||||
};
|
||||
match open {
|
||||
(false, false) => format!("{} m {}{}{}{}h ", pt(corner(0).1), curve(0), along(&[1]), along(&[2]), along(&[3])),
|
||||
(true, false) => format!("{} m {}{} l ", pt((x1, y0)), along(&[2, 3]), pt((x0, y0))),
|
||||
(false, true) => format!("{} m {}{} l ", pt((x0, y1)), along(&[0, 1]), pt((x1, y1))),
|
||||
(true, true) => format!("{} m {} l {} m {} l ", pt((x0, y0)), pt((x0, y1)), pt((x1, y0)), pt((x1, y1))),
|
||||
}
|
||||
}
|
||||
|
||||
/// A PDF appearance-state name for a check button's on value.
|
||||
fn state_name(on: &str) -> String {
|
||||
let s: String = on.trim().chars().map(|c| if c.is_whitespace() || "()<>[]{}/%#".contains(c) || c.is_control() { '_' } else { c }).collect();
|
||||
@@ -619,6 +690,7 @@ impl Emitter<'_> {
|
||||
Item::Fill { rect, color } => {
|
||||
content.extend(format!("{} rg {} {} {} {} re f\n", rgb(*color), n(rect.x), n(h - rect.bottom()), n(rect.w), n(rect.h)).bytes());
|
||||
}
|
||||
Item::Rounded(r) => content.extend(rounded(r, h).bytes()),
|
||||
Item::Line { from, to, width, color, dashed } => {
|
||||
content.extend(
|
||||
format!(
|
||||
|
||||
@@ -2265,6 +2265,104 @@ fn a_multi_line_field_without_a_height_grows_to_hold_its_text() {
|
||||
assert!((notes.rect.h - 108.0).abs() < 0.01, "{:?}", notes.rect);
|
||||
}
|
||||
|
||||
/// The rounded boxes laid out on `page`, in order.
|
||||
fn rounded(page: &Page) -> Vec<&crate::layout::Rounded> {
|
||||
page.items.iter().filter_map(|i| if let Item::Rounded(r) = i { Some(r.as_ref()) } else { None }).collect()
|
||||
}
|
||||
|
||||
/// A border or rectangle with rounded corners is one shape, filled and edged along its curves:
|
||||
/// one corner applies to all four, a corner not given repeats the last one given, a radius is at
|
||||
/// most half the box's shorter side, and inverted corners are kept. Hidden corners, and edges
|
||||
/// that differ from each other, keep the square box.
|
||||
#[test]
|
||||
fn rounded_corners_make_a_border_one_rounded_shape() {
|
||||
let draw = |name: &str, y: f64, border: &str| {
|
||||
format!(r#"<draw name="{name}" x="0in" y="{y}in" w="2in" h="1in"><value><text>{name}</text></value>{border}</draw>"#)
|
||||
};
|
||||
let body = [
|
||||
draw("round", 0.0, r#"<border><edge thickness="1pt"><color value="255,0,0"/></edge><corner radius="0.25in"/><fill><color value="230,230,230"/></fill></border>"#),
|
||||
draw("mixed", 1.2, r#"<border><edge/><corner radius="0.1in"/><corner radius="0.2in" inverted="1"/></border>"#),
|
||||
draw("huge", 2.4, r#"<border><edge/><corner radius="3in"/></border>"#),
|
||||
draw("hiddenCorner", 3.6, r#"<border><edge/><corner radius="0.25in" presence="hidden"/></border>"#),
|
||||
draw("unlikeEdges", 4.8, r#"<border><edge/><edge presence="hidden"/><corner radius="0.25in"/></border>"#),
|
||||
draw("fillOnly", 6.0, r#"<border><edge presence="hidden"/><corner radius="0.25in"/><fill><color value="200,200,200"/></fill></border>"#),
|
||||
r#"<draw name="shape" x="0in" y="7.2in" w="2in" h="1in"><value><rectangle><edge thickness="0.5pt"/><corner radius="5.08mm"/></rectangle></value></draw>"#.to_string(),
|
||||
]
|
||||
.concat();
|
||||
let form = layout_xml(&one_area_form(r#"layout="tb""#, &format!(r#"<subform name="body" layout="position" w="7.5in" h="9in">{body}</subform>"#)))
|
||||
.unwrap();
|
||||
let shapes = rounded(&form.pages[0]);
|
||||
let at = |y: f64| shapes.iter().copied().find(|r| (r.rect.y - 36.0 - y * 72.0).abs() < 0.01);
|
||||
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))));
|
||||
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!(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");
|
||||
assert_eq!((fill_only.fill.is_some(), fill_only.edge), (true, None));
|
||||
let shape = at(7.2).expect("a rectangle drawn as a value");
|
||||
assert!((shape.radii[0] - 5.08 * 72.0 / 25.4).abs() < 1e-9 && shape.edge.is_some());
|
||||
// The square boxes are drawn as before: edges as lines.
|
||||
let lines_at = |y: f64| {
|
||||
form.pages[0]
|
||||
.items
|
||||
.iter()
|
||||
.filter(|i| matches!(i, Item::Line { from, .. } if from.1 >= 36.0 + y * 72.0 - 1.0 && from.1 <= 36.0 + (y + 1.0) * 72.0 + 1.0))
|
||||
.count()
|
||||
};
|
||||
assert_eq!((lines_at(3.6), lines_at(4.8)), (4, 2));
|
||||
}
|
||||
|
||||
/// 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]
|
||||
fn a_rounded_box_cut_by_a_page_break_keeps_its_corners_on_each_page() {
|
||||
// A 12-inch box in a 10-inch content area: it is cut at the area's bottom.
|
||||
let body = r#"<subform name="body" layout="tb" w="7.5in"><subform name="tall" layout="position" w="7.5in" h="12in"><border><edge/><corner radius="0.25in"/></border><draw name="note" x="0in" y="0in" w="2in" h="0.3in"><value><text>Note</text></value></draw></subform></subform>"#;
|
||||
let form = layout_xml(&one_area_form(r#"layout="tb""#, body)).unwrap();
|
||||
assert_eq!(form.pages.len(), 2);
|
||||
let (first, last) = (rounded(&form.pages[0]), rounded(&form.pages[1]));
|
||||
assert_eq!((first.len(), last.len()), (1, 1), "{first:?} {last:?}");
|
||||
assert_eq!((first[0].radii, first[0].open_top, first[0].open_bottom), ([18.0, 18.0, 0.0, 0.0], false, true));
|
||||
assert_eq!((last[0].radii, last[0].open_top, last[0].open_bottom), ([0.0, 0.0, 18.0, 18.0], true, false));
|
||||
assert!((first[0].rect.h + last[0].rect.h - 12.0 * 72.0).abs() < 0.01, "the two parts make the whole box");
|
||||
}
|
||||
|
||||
/// A rounded box is written as one path: filled along its curves, then edged half its width
|
||||
/// inside, the curves as quarter circles. An inverted corner curves into the box, and the edge
|
||||
/// of a box a page break cut doesn't close across the cut.
|
||||
#[test]
|
||||
fn rounded_boxes_are_written_as_curved_paths() {
|
||||
use crate::layout::Rounded;
|
||||
// A 2-by-1-inch box at the page's top left, on a page 792pt tall.
|
||||
let shape = |inverted: [bool; 4], edge: bool, open: (bool, bool)| Rounded {
|
||||
rect: Rect::new(36.0, 36.0, 144.0, 72.0),
|
||||
radii: [18.0; 4],
|
||||
inverted,
|
||||
fill: Some([0.9, 0.9, 0.9]),
|
||||
edge: edge.then_some((2.0, [1.0, 0.0, 0.0], false)),
|
||||
open_top: open.0,
|
||||
open_bottom: open.1,
|
||||
};
|
||||
let content = |r: Rounded| crate::pdf::rounded(&r, 792.0);
|
||||
let ops = content(shape([false; 4], true, (false, false)));
|
||||
// The fill: from the top-left corner's start, a quarter circle to (54, 36), closed.
|
||||
assert!(ops.contains("0.9 0.9 0.9 rg 36 738 m 36 747.941 44.059 756 54 756 c "), "{ops}");
|
||||
assert!(ops.contains(" h f\n"), "{ops}");
|
||||
// The edge: 1pt inside, the corners 1pt tighter.
|
||||
assert!(ops.contains("q 1 0 0 RG 2 w 37 738 m 37 747.389 44.611 755 54 755 c "), "{ops}");
|
||||
assert!(ops.contains(" h S Q\n"), "{ops}");
|
||||
// Inverted: the curve bends towards the box's inside.
|
||||
let inv = content(shape([true; 4], false, (false, false)));
|
||||
assert!(inv.contains("36 738 m 45.941 738 54 746.059 54 756 c "), "{inv}");
|
||||
// Cut at the bottom: the edge runs from the bottom left round the top and back down, open.
|
||||
let cut = content(Rounded { radii: [18.0, 18.0, 0.0, 0.0], ..shape([false; 4], true, (false, true)) });
|
||||
let edge = cut.lines().find(|l| l.contains(" RG ")).unwrap_or_default();
|
||||
assert!(edge.starts_with("q 1 0 0 RG 2 w 37 685 m 37 738 l ") && edge.ends_with(" 179 685 l S Q") && !edge.contains(" h "), "{edge}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_page_break_keeps_a_fields_border_with_the_field() {
|
||||
// 6in of heading, then a bordered 5in field: it doesn't fit the 10in area and moves to
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user