Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions crates/xfa/src/layout/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,17 @@ pub(super) fn divide(item: Item, cut: f64) -> (Option<Item>, Option<Item>) {
(Some(Item::Fill { rect: above, color }), Some(Item::Fill { rect: below, color }))
}
}
Item::Line { from, to, width, color, dashed } => {
Item::Line { from, to, width, color, stroke } => {
let (lo, hi) = if from.1 <= to.1 { (from, to) } else { (to, from) };
if lo.1 >= cut {
(None, Some(Item::Line { from, to, width, color, dashed }))
(None, Some(Item::Line { from, to, width, color, stroke }))
} else if hi.1 <= cut {
(Some(Item::Line { from, to, width, color, dashed }), None)
(Some(Item::Line { from, to, width, color, stroke }), None)
} else {
// lo.1 < cut < hi.1, so the divisor is positive.
let t = (cut - lo.1) / (hi.1 - lo.1);
let mid = (lo.0 + (hi.0 - lo.0) * t, cut);
(Some(Item::Line { from: lo, to: mid, width, color, dashed }), Some(Item::Line { from: mid, to: hi, width, color, dashed }))
(Some(Item::Line { from: lo, to: mid, width, color, stroke }), Some(Item::Line { from: mid, to: hi, width, color, stroke }))
}
}
Item::Rounded(r) => {
Expand Down
12 changes: 3 additions & 9 deletions crates/xfa/src/layout/draw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ impl Layouter<'_> {
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))
(e.thickness.clamp(0.1, 20.0), color, e.stroke)
});
let shape = Rounded {
rect,
Expand Down Expand Up @@ -191,13 +191,7 @@ impl Layouter<'_> {
_ => (t / 2.0, 0.0),
};
let color = if e.stroke == Stroke::Relief { [0.55, 0.55, 0.55] } else { e.color.0 };
self.push(Item::Line {
from: (seg.0.0 + dx, seg.0.1 + dy),
to: (seg.1.0 + dx, seg.1.1 + dy),
width: t,
color,
dashed: matches!(e.stroke, Stroke::Dashed | Stroke::Dotted),
})?;
self.push(Item::Line { from: (seg.0.0 + dx, seg.0.1 + dy), to: (seg.1.0 + dx, seg.1.1 + dy), width: t, color, stroke: e.stroke })?;
}
Ok(())
}
Expand Down Expand Up @@ -243,7 +237,7 @@ impl Layouter<'_> {
} else {
((inner.x, inner.y), (inner.right(), inner.bottom()))
};
self.push(Item::Line { from, to, width: edge.thickness.clamp(0.1, 20.0), color: edge.color.0, dashed: edge.stroke == Stroke::Dashed })
self.push(Item::Line { from, to, width: edge.thickness.clamp(0.1, 20.0), color: edge.color.0, stroke: edge.stroke })
}
Value::Rectangle(b) => self.draw_border(b, inner),
Value::Empty => Ok(()),
Expand Down
9 changes: 5 additions & 4 deletions crates/xfa/src/layout/items.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::{Arc, Face, HAlign, Rect, Span};
use super::{Arc, Face, HAlign, Rect, Span, Stroke};

/// How far below its baseline an underline is drawn, in the text's size: an underline is the
/// level line pushed just before its span, this far below it (the PDF writer tells it apart from
Expand All @@ -16,7 +16,8 @@ pub enum Item {
to: (f64, f64),
width: f64,
color: [f64; 3],
dashed: bool,
/// Its stroke, which sets its dash pattern ([`Stroke::Relief`] draws solid).
stroke: Stroke,
},
/// A box with rounded (or inverted) corners: its fill, and its edge drawn inside it.
Rounded(Box<Rounded>),
Expand All @@ -39,8 +40,8 @@ pub struct Rounded {
/// 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)>,
/// The edge: its width, colour and stroke (which sets its dash pattern).
pub edge: Option<(f64, [f64; 3], Stroke)>,
/// 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,
Expand Down
10 changes: 8 additions & 2 deletions crates/xfa/src/layout/pages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use super::EMBED_COPIED;
use super::items::{Item, Page, UNDERLINE_DROP};
use super::{
Block, Border, Ctx, Cursor, FlowMark, Font, Frame, Layouter, MAX_ITEMS, MAX_MEASURES, MAX_PAGES, PAGE_COUNT_MARK, PageRole, Para, Rc, Rect,
SomIndices, XfaError, text,
SomIndices, Stroke, XfaError, text,
};

impl Layouter<'_> {
Expand Down Expand Up @@ -56,7 +56,13 @@ impl Layouter<'_> {
if s.face.underline {
let w = s.face.width(&s.text);
let y = s.baseline + s.face.size * UNDERLINE_DROP;
self.push(Item::Line { from: (s.x, y), to: (s.x + w, y), width: (s.face.size * 0.06).max(0.3), color: s.face.color, dashed: false })?;
self.push(Item::Line {
from: (s.x, y),
to: (s.x + w, y),
width: (s.face.size * 0.06).max(0.3),
color: s.face.color,
stroke: Stroke::Solid,
})?;
}
if !s.text.trim().is_empty() {
self.push(Item::Text(s))?;
Expand Down
4 changes: 4 additions & 0 deletions crates/xfa/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ pub enum Stroke {
Solid,
Dashed,
Dotted,
/// A dash, then a dot.
DashDot,
/// A dash, then two dots.
DashDotDot,
/// Raised, lowered, etched and embossed: drawn as a solid grey line.
Relief,
}
Expand Down
4 changes: 3 additions & 1 deletion crates/xfa/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,10 @@ fn parse_edge(n: XmlNode) -> Edge {
thickness: attr_measure(n, "thickness").unwrap_or(0.5).max(0.0),
color: parse_color(child(n, "color")).unwrap_or(Color::BLACK),
stroke: match n.attribute("stroke") {
Some("dashed" | "dashDot" | "dashDotDot") => Stroke::Dashed,
Some("dashed") => Stroke::Dashed,
Some("dotted") => Stroke::Dotted,
Some("dashDot") => Stroke::DashDot,
Some("dashDotDot") => Stroke::DashDotDot,
Some("raised" | "lowered" | "etched" | "embossed") => Stroke::Relief,
_ => Stroke::Solid,
},
Expand Down
18 changes: 2 additions & 16 deletions crates/xfa/src/pdf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ mod common;
mod strip;

pub use common::jpeg_size;
pub(crate) use common::rounded;
use common::{all_faces, content_hash, empty_form, hex32, jpeg_info, js_action, n, parse_hex32, rgb, rgb_array, standard_font, state_name};
pub(crate) use common::{line, rounded};
pub(crate) use strip::USAGE_RIGHTS_REMOVED;
use strip::drop_usage_rights;
pub use strip::{Stripped, XfaShape, existing_layout, strip, xfa_shape};
Expand Down Expand Up @@ -586,21 +586,7 @@ impl Emitter<'_> {
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!(
"q {} RG {} w {}{} {} m {} {} l S Q\n",
rgb(*color),
n(*width),
if *dashed { "[2 2] 0 d " } else { "" },
n(from.0),
n(h - from.1),
n(to.0),
n(h - to.1)
)
.bytes(),
);
}
Item::Line { from, to, width, color, stroke } => content.extend(line(*from, *to, *width, *color, *stroke, h).bytes()),
Item::Text(s) => {
// The form's own text, set in the standard fonts: what they can't draw is
// shown as `?` and reported once (the layout goes on), never silently.
Expand Down
52 changes: 50 additions & 2 deletions crates/xfa/src/pdf/common.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::{Dict, Document, Family, Object, PdfString, Rect, Rounded, Stream};
use crate::model::Stroke;

/// Width and height of a JPEG from its first frame header (SOF0–SOF15, not the DNL/JPG/DAC/DHT
/// markers). `None` for anything else.
Expand Down Expand Up @@ -114,7 +115,7 @@ pub(crate) fn rounded(r: &Rounded, page_h: f64) -> String {
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 {
if let Some((w, color, stroke)) = 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));
Expand All @@ -125,11 +126,58 @@ pub(crate) fn rounded(r: &Rounded, page_h: f64) -> String {
(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.push_str(&format!("q {} RG {} w {}{path}S Q\n", rgb(color), n(w), dash(stroke, w)));
}
c
}

/// The content that draws a straight line from `from` to `to` (top-left based, on a page
/// `page_h` tall), `width` thick, in `color`, with `stroke`'s dash pattern.
///
/// The ink stays between `from` and `to` whatever the caps: a dashed line's projecting caps
/// reach half its width past each end of the path, so the path is shortened by as much (a
/// border's edge would otherwise paint past the box's corners). A dashed line no longer than
/// its width can't be shortened, and is drawn with butt caps instead.
pub(crate) fn line(from: (f64, f64), to: (f64, f64), width: f64, color: [f64; 3], stroke: Stroke, page_h: f64) -> String {
let (dx, dy) = (to.0 - from.0, to.1 - from.1);
let len = dx.hypot(dy);
let (from, to, dash) = match dash_array(stroke, width) {
None => (from, to, String::new()),
Some(array) if len.is_finite() && width.is_finite() && len > width => {
let k = width / 2.0 / len;
((from.0 + dx * k, from.1 + dy * k), (to.0 - dx * k, to.1 - dy * k), format!("{array}2 J "))
}
Some(array) => (from, to, array),
};
format!("q {} RG {} w {dash}{} {} m {} {} l S Q\n", rgb(color), n(width), n(from.0), n(page_h - from.1), n(to.0), n(page_h - to.1))
}

/// The operators that set `stroke`'s dash pattern for an edge `width` thick, with projecting
/// caps and a trailing space, or nothing for a solid (or 3-D) stroke.
///
/// Dashes and gaps are multiples of the width, drawn with projecting caps: for a 3pt edge that is
/// `[12 6]` dashed, `[3 6]` dotted, `[9 6 3 6]` dash-dot and `[9 6 3 6 3 6]` dash-dot-dot, the
/// patterns Acrobat flattens those strokes to (#5561). A width under 0.01pt dashes as if it were
/// 0.01pt (so no dash rounds to zero) and a width that isn't finite as if it were 1pt.
pub(crate) fn dash(stroke: Stroke, width: f64) -> String {
dash_array(stroke, width).map(|a| format!("{a}2 J ")).unwrap_or_default()
}

/// The `d` operator of `stroke`'s dash pattern ([`dash`]), with a trailing space, without the
/// caps; `None` for a solid (or 3-D) stroke.
fn dash_array(stroke: Stroke, width: f64) -> Option<String> {
let units: &[f64] = match stroke {
Stroke::Solid | Stroke::Relief => return None,
Stroke::Dashed => &[4.0, 2.0],
Stroke::Dotted => &[1.0, 2.0],
Stroke::DashDot => &[3.0, 2.0, 1.0, 2.0],
Stroke::DashDotDot => &[3.0, 2.0, 1.0, 2.0, 1.0, 2.0],
};
let w = if width.is_finite() { width.max(0.01) } else { 1.0 };
let array: Vec<String> = units.iter().map(|u| n(u * w)).collect();
Some(format!("[{}] 0 d ", array.join(" ")))
}

/// 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):
Expand Down
82 changes: 80 additions & 2 deletions crates/xfa/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2411,7 +2411,7 @@ fn rounded_corners_make_a_border_one_rounded_shape() {
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))));
assert_eq!((round.fill, round.edge), (Some([230.0 / 255.0; 3]), Some((1.0, [1.0, 0.0, 0.0], Stroke::Solid))));
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");
Expand Down Expand Up @@ -2458,7 +2458,7 @@ fn rounded_boxes_are_written_as_curved_paths() {
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)),
edge: edge.then_some((2.0, [1.0, 0.0, 0.0], Stroke::Solid)),
open_top: open.0,
open_bottom: open.1,
};
Expand All @@ -2479,6 +2479,84 @@ fn rounded_boxes_are_written_as_curved_paths() {
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}");
}

/// Each edge stroke keeps its own dash pattern, in multiples of the edge's width and with
/// projecting caps, on a `<line>` and on a border alike: a 3pt edge draws dashed `[12 6]`,
/// dotted `[3 6]` (not solid), dash-dot `[9 6 3 6]` and dash-dot-dot `[9 6 3 6 3 6]` (not
/// dashed), and a solid or 3-D edge draws solid (#5561).
#[test]
fn edge_strokes_keep_their_own_dash_patterns() {
let strokes = [
("solid", Stroke::Solid, ""),
("dashed", Stroke::Dashed, "[12 6] 0 d 2 J "),
("dotted", Stroke::Dotted, "[3 6] 0 d 2 J "),
("dashDot", Stroke::DashDot, "[9 6 3 6] 0 d 2 J "),
("dashDotDot", Stroke::DashDotDot, "[9 6 3 6 3 6] 0 d 2 J "),
("lowered", Stroke::Relief, ""),
];
let body: String = strokes
.iter()
.enumerate()
.map(|(i, (name, ..))| {
format!(
r#"<draw name="line{i}" x="0in" y="{i}in" w="3in" h="0.1in"><value><line><edge thickness="3pt" stroke="{name}"/></line></value></draw><draw name="box{i}" x="4in" y="{i}in" w="2in" h="0.5in"><value><text>box</text></value><border><edge thickness="3pt" stroke="{name}"/></border></draw>"#
)
})
.collect();
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();
for (i, (name, stroke, dash)) in strokes.iter().enumerate() {
// The draws in row `i` start an inch apart, 0.5in below the page's top.
let top = 36.0 + i as f64 * 72.0;
let lines: Vec<String> = form.pages[0]
.items
.iter()
.filter_map(|it| match it {
Item::Line { from, to, width, color, stroke: s } if from.1 >= top - 2.0 && from.1 <= top + 40.0 => {
assert_eq!(s, stroke, "{name}: the item keeps the edge's stroke");
Some(crate::pdf::line(*from, *to, *width, *color, *s, 792.0))
}
_ => None,
})
.collect();
assert_eq!(lines.len(), 5, "{name}: the line and the box's four edges: {lines:?}");
for ops in &lines {
assert!(ops.starts_with("q ") && ops.contains(&format!(" 3 w {dash}")), "{name}: {ops}");
assert_eq!(ops.contains(" d "), !dash.is_empty(), "{name}: {ops}");
}
}
}

/// A dash pattern scales with the edge's width, a dashed line's ink stays between its ends (its
/// path shortened for the projecting caps), a rounded box's edge is dashed like a square one, and
/// a width that would round every dash to zero, or isn't a number, still gives a valid pattern.
#[test]
fn dash_patterns_scale_with_width_and_stay_valid() {
use crate::layout::Rounded;
use crate::pdf::line;
// The path is shortened by half the width at each end, so the projecting caps end at the
// line's own ends; a solid line keeps its ends and butt caps.
assert_eq!(line((0.0, 0.0), (10.0, 0.0), 0.5, [0.0; 3], Stroke::Dashed, 100.0), "q 0 0 0 RG 0.5 w [2 1] 0 d 2 J 0.25 100 m 9.75 100 l S Q\n");
assert_eq!(line((0.0, 0.0), (10.0, 0.0), 0.5, [0.0; 3], Stroke::Solid, 100.0), "q 0 0 0 RG 0.5 w 0 100 m 10 100 l S Q\n");
// A vertical (border) edge is shortened along its own direction.
assert!(line((5.0, 0.0), (5.0, 20.0), 2.0, [0.0; 3], Stroke::Dotted, 100.0).contains(" 5 99 m 5 81 l "));
// A dashed line no longer than its width keeps its ends, with butt caps.
let short = line((0.0, 0.0), (2.0, 0.0), 3.0, [0.0; 3], Stroke::Dashed, 100.0);
assert!(short.contains("[12 6] 0 d 0 100 m 2 100 l ") && !short.contains(" J "), "{short}");
assert!(line((0.0, 0.0), (10.0, 0.0), 0.0001, [0.0; 3], Stroke::Dotted, 100.0).contains("[0.01 0.02] 0 d "));
let nan = line((0.0, 0.0), (10.0, 0.0), f64::NAN, [0.0; 3], Stroke::DashDot, 100.0);
assert!(nan.contains("[3 2 1 2] 0 d 0 100 m 10 100 l "), "{nan}");
let rounded = Rounded {
rect: Rect::new(36.0, 36.0, 144.0, 72.0),
radii: [18.0; 4],
inverted: [false; 4],
fill: None,
edge: Some((2.0, [0.0; 3], Stroke::DashDotDot)),
open_top: false,
open_bottom: false,
};
assert!(crate::pdf::rounded(&rounded, 792.0).starts_with("q 0 0 0 RG 2 w [6 4 2 4 2 4] 0 d 2 J "));
}

/// A flowed subform without a fixed height gets its border and fill round all it takes, margins
/// included, drawn under its content; a flowed table too. One a page break cuts between its
/// children gets a closed border round its part on each page, its bottom margin ending the first
Expand Down
Loading