mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-07 12:12:47 +00:00
Merge pull request #5624 from Piohrun/feat/xfa-rotate
M12: an XFA draw placed by position turns about its anchor
This commit is contained in:
@@ -32,7 +32,10 @@ that warning and keeps the existing layout and accepted widget values.
|
||||
page masters chosen by occurrence; a table's header row repeats after a break; presence
|
||||
`hidden` takes no space, `invisible` takes space; repeating subforms get their initial
|
||||
instances, in flowed content and in a top-to-bottom container inside positioned content (a
|
||||
table in an area) alike; fields that compute the page number or count on layout show the numbers.
|
||||
table in an area) alike; fields that compute the page number or count on layout show the numbers;
|
||||
a draw placed by position turns about its anchor (`rotate`: degrees counter-clockwise, a multiple
|
||||
of 90), as one item a page break doesn't cut, and its container grows to the box it stands in. A
|
||||
field, a subform or a draw in a flow doesn't turn.
|
||||
- **PDF** (`pdf::write_form`): pages with content streams in the standard 14 fonts (Arial and
|
||||
friends → Helvetica, Courier New → Courier, serif faces → Times; nothing is embedded), JPEG
|
||||
XObjects, and widgets: text (multi-line, max length, alignment), date (Acrobat's `AFDate`
|
||||
|
||||
@@ -32,7 +32,7 @@ use common::collect_values;
|
||||
pub(crate) use common::date_pattern;
|
||||
pub use common::formatted_value;
|
||||
pub(crate) use items::UNDERLINE_DROP;
|
||||
pub use items::{Action, BorderShape, Form, Item, Page, Rounded, Widget, WidgetBorder, WidgetKind};
|
||||
pub use items::{Action, BorderShape, Form, Item, Page, Rounded, Turned, Widget, WidgetBorder, WidgetKind};
|
||||
|
||||
/// Most pages a form may lay out to.
|
||||
pub const MAX_PAGES: usize = 500;
|
||||
@@ -212,17 +212,22 @@ pub fn layout(tpl: &Template, data: Option<&DataNode>) -> Result<Form, XfaError>
|
||||
let total = l.pages.len();
|
||||
let count = total.to_string();
|
||||
for p in &mut l.pages {
|
||||
for it in &mut p.items {
|
||||
if let Item::Text(s) = it
|
||||
&& s.text.contains(PAGE_COUNT_MARK)
|
||||
{
|
||||
s.text = s.text.replace(PAGE_COUNT_MARK, &count);
|
||||
}
|
||||
}
|
||||
fill_page_count(&mut p.items, &count);
|
||||
}
|
||||
Ok(Form { pages: l.pages, fields: l.fields, warnings: l.warnings })
|
||||
}
|
||||
|
||||
/// Puts the page count, known once every page is, where `items` left room for it.
|
||||
fn fill_page_count(items: &mut [Item], count: &str) {
|
||||
for it in items {
|
||||
match it {
|
||||
Item::Text(s) if s.text.contains(PAGE_COUNT_MARK) => s.text = s.text.replace(PAGE_COUNT_MARK, count),
|
||||
Item::Turned(t) => fill_page_count(&mut t.items, count),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bytes of embedded field values copied out on this thread, for tests that bound layout work.
|
||||
#[cfg(test)]
|
||||
thread_local! {
|
||||
|
||||
@@ -51,6 +51,7 @@ pub(super) 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::Turned(t) => Some(t.extent),
|
||||
Item::Fill { .. } | Item::Line { .. } | Item::Rounded(_) => None,
|
||||
}
|
||||
}
|
||||
@@ -130,6 +131,11 @@ pub(super) fn moved(mut item: Item, dx: f64, dy: f64) -> Item {
|
||||
w.rect.x += dx;
|
||||
w.rect.y += dy;
|
||||
}
|
||||
Item::Turned(t) => {
|
||||
t.about = (t.about.0 + dx, t.about.1 + dy);
|
||||
t.extent = (t.extent.0 + dy, t.extent.1 + dy);
|
||||
t.items = std::mem::take(&mut t.items).into_iter().map(|i| moved(i, dx, dy)).collect();
|
||||
}
|
||||
}
|
||||
item
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::common::clean_name;
|
||||
use super::items::{Item, Rounded};
|
||||
use super::items::{Item, Rounded, Turned};
|
||||
use super::{
|
||||
Arc, Border, Ctx, Draw, ExclGroup, Face, Font, Layout, Layouter, MAX_DEPTH, MAX_IMAGE_BYTES, Node, Para, Presence, Rect, SomIndices, Stroke,
|
||||
Subform, Value, XfaError, data_path, text,
|
||||
@@ -35,7 +35,27 @@ impl Layouter<'_> {
|
||||
}
|
||||
let (w, h) = self.size_of(k, (inner.w - c.x.unwrap_or(0.0)).max(1.0), ctx, depth + 1);
|
||||
let (x, y) = c.top_left(w, h);
|
||||
self.draw_node(k, Rect::new(inner.x + x, inner.y + y, w, h), ctx, depth + 1, sib)
|
||||
let rect = Rect::new(inner.x + x, inner.y + y, w, h);
|
||||
let start = self.item_count();
|
||||
self.draw_node(k, rect, ctx, depth + 1, sib)?;
|
||||
if c.rotate != 0 && matches!(k, Node::Draw(_)) {
|
||||
let stands = k.standing_box(w, h);
|
||||
let extent = (inner.y + stands.y, inner.y + stands.bottom());
|
||||
self.turn(start, (inner.x + c.x.unwrap_or(0.0), inner.y + c.y.unwrap_or(0.0)), c.rotate, extent);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Turn the items the last page got since `start`, a draw, about `about`.
|
||||
fn turn(&mut self, start: usize, about: (f64, f64), quarters: u8, extent: (f64, f64)) {
|
||||
let Some(page) = self.pages.last_mut().filter(|p| start < p.items.len()) else { return };
|
||||
let items = page.items.split_off(start);
|
||||
page.items.push(Item::Turned(Box::new(Turned { about, quarters, extent, items })));
|
||||
// One item now, kept whole by what it is: the record of its items, the last one kept,
|
||||
// would fall on the next.
|
||||
if self.leaves.last().is_some_and(|leaf| leaf.0 == start) {
|
||||
self.leaves.pop();
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn draw_node(&mut self, node: &Node, rect: Rect, ctx: &Ctx, depth: usize, sib: usize) -> Result<(), XfaError> {
|
||||
|
||||
@@ -27,6 +27,22 @@ pub enum Item {
|
||||
content_type: String,
|
||||
},
|
||||
Widget(Box<Widget>),
|
||||
/// A draw turned about its anchor.
|
||||
Turned(Box<Turned>),
|
||||
}
|
||||
|
||||
/// A draw turned about its anchor ([`Item::Turned`]). It stays one item, so a page break doesn't
|
||||
/// cut it, and what it holds is written unturned under one matrix.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Turned {
|
||||
/// The point it turns about.
|
||||
pub about: (f64, f64),
|
||||
/// Quarter turns counter-clockwise, 1 to 3.
|
||||
pub quarters: u8,
|
||||
/// Top and bottom of its box once turned.
|
||||
pub extent: (f64, f64),
|
||||
/// The draw as it stands before turning.
|
||||
pub items: Vec<Item>,
|
||||
}
|
||||
|
||||
/// A box with rounded corners ([`Item::Rounded`]).
|
||||
|
||||
@@ -141,7 +141,7 @@ impl Layouter<'_> {
|
||||
Layout::Positioned => visible
|
||||
.map(|k| {
|
||||
let (w, h) = self.size_of(k, avail_w, ctx, depth + 1);
|
||||
k.common().top_left(w, h).0 + w
|
||||
k.standing_box(w, h).right()
|
||||
})
|
||||
.fold(0.0, f64::max)
|
||||
.min(avail_w),
|
||||
@@ -297,7 +297,7 @@ impl Layouter<'_> {
|
||||
.map(|k| {
|
||||
let c = k.common();
|
||||
let (w, h) = self.size_of(k, (inner_w - c.x.unwrap_or(0.0)).max(1.0), ctx, depth + 1);
|
||||
c.top_left(w, h).1 + h
|
||||
k.standing_box(w, h).bottom()
|
||||
})
|
||||
.fold(0.0, f64::max)
|
||||
}
|
||||
|
||||
@@ -334,6 +334,9 @@ pub struct Common {
|
||||
pub x: Option<f64>,
|
||||
pub y: Option<f64>,
|
||||
pub anchor: Anchor,
|
||||
/// Quarter turns counter-clockwise about the anchor (`rotate`), 0 to 3. Only a draw placed by
|
||||
/// position is turned.
|
||||
pub rotate: u8,
|
||||
pub w: Option<f64>,
|
||||
pub h: Option<f64>,
|
||||
pub min_w: Option<f64>,
|
||||
@@ -366,6 +369,26 @@ impl Common {
|
||||
}
|
||||
|
||||
impl Node {
|
||||
/// The box a positioned object of `w` by `h` stands in, in its parent: from `x`, `y` and the
|
||||
/// anchor, and for a draw, the only object that turns, turned about the anchor.
|
||||
pub fn standing_box(&self, w: f64, h: f64) -> Rect {
|
||||
let c = self.common();
|
||||
let (x, y) = c.top_left(w, h);
|
||||
let upright = Rect::new(x, y, w, h);
|
||||
if !matches!(self, Node::Draw(_)) {
|
||||
return upright;
|
||||
}
|
||||
let (ax, ay) = (c.x.unwrap_or(0.0), c.y.unwrap_or(0.0));
|
||||
let (left, top, right, bottom) = (x - ax, y - ay, x + w - ax, y + h - ay);
|
||||
// Counter-clockwise on a page whose y grows down, from the anchor.
|
||||
match c.rotate {
|
||||
1 => Rect::new(ax + top, ay - right, h, w),
|
||||
2 => Rect::new(ax - right, ay - bottom, w, h),
|
||||
3 => Rect::new(ax - bottom, ay + left, h, w),
|
||||
_ => upright,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn common(&self) -> &Common {
|
||||
match self {
|
||||
Node::Subform(s) => &s.common,
|
||||
|
||||
@@ -198,6 +198,16 @@ fn parse_para(n: Option<XmlNode>) -> ParaSpec {
|
||||
}
|
||||
}
|
||||
|
||||
/// `rotate` as quarter turns (XFA 3.3: degrees counter-clockwise, a multiple of 90, `deg` optional):
|
||||
/// anything else leaves the object as it stands.
|
||||
fn quarter_turns(rotate: Option<&str>) -> u8 {
|
||||
let degrees = rotate.map(str::trim).map(|v| v.strip_suffix("deg").unwrap_or(v).trim_end());
|
||||
let turns = degrees.and_then(|v| v.parse::<f64>().ok()).map(|v| v.rem_euclid(360.0) / 90.0);
|
||||
// Whole, which NaN and infinities aren't, and in range, so the cast is exact; a tiny negative
|
||||
// angle rounds to a full turn.
|
||||
turns.filter(|t| t.fract() == 0.0).map_or(0, |t| t as u8 % 4)
|
||||
}
|
||||
|
||||
fn parse_common(n: XmlNode) -> Common {
|
||||
Common {
|
||||
name: n.attribute("name").map(str::to_string).filter(|s| !s.is_empty()),
|
||||
@@ -205,6 +215,7 @@ fn parse_common(n: XmlNode) -> Common {
|
||||
x: attr_measure(n, "x"),
|
||||
y: attr_measure(n, "y"),
|
||||
anchor: Anchor::parse(n.attribute("anchorType")),
|
||||
rotate: quarter_turns(n.attribute("rotate")),
|
||||
w: attr_measure(n, "w"),
|
||||
h: attr_measure(n, "h"),
|
||||
min_w: attr_measure(n, "minW"),
|
||||
|
||||
+78
-6
@@ -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, Rounded, Widget, WidgetKind};
|
||||
use crate::layout::{Action, BorderShape, Form, Item, Page, Rounded, Turned, Widget, WidgetKind};
|
||||
use crate::model::Rect;
|
||||
use crate::text::{Face, Family, Span};
|
||||
|
||||
@@ -567,12 +567,30 @@ impl Emitter<'_> {
|
||||
let h = page.height;
|
||||
// The link the text before ended in, which a next piece of it on the line widens.
|
||||
let mut last_link: Option<LinkPiece> = None;
|
||||
for (i, item) in page.items.iter().enumerate() {
|
||||
let steps = steps(&page.items);
|
||||
let mut turn = None;
|
||||
for (i, step) in steps.iter().enumerate() {
|
||||
let item = match step {
|
||||
Step::Item(item) => *item,
|
||||
Step::Turn(t) => {
|
||||
let matrix = turn_matrix(t, h);
|
||||
turn = Some(matrix);
|
||||
last_link = None;
|
||||
content.extend(format!("q {} cm\n", matrix.map(n).join(" ")).bytes());
|
||||
continue;
|
||||
}
|
||||
Step::EndTurn => {
|
||||
turn = None;
|
||||
last_link = None;
|
||||
content.extend_from_slice(b"Q\n");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
// A link's pieces join only across text and the underline drawn just before it: a fill,
|
||||
// picture, field or any other line (a rule) between them ends the link there.
|
||||
let keeps_link = match item {
|
||||
Item::Text(_) => true,
|
||||
Item::Line { from, to, .. } => matches!(page.items.get(i + 1), Some(Item::Text(s))
|
||||
Item::Line { from, to, .. } => matches!(steps.get(i + 1), Some(Step::Item(Item::Text(s)))
|
||||
if (from.1 - to.1).abs() < 0.01
|
||||
&& (from.0 - s.x).abs() < 0.01
|
||||
&& (from.1 - (s.baseline + s.face.size * crate::layout::UNDERLINE_DROP)).abs() < 0.01),
|
||||
@@ -627,7 +645,7 @@ impl Emitter<'_> {
|
||||
);
|
||||
content.extend(literal(&win_ansi(&s.text)));
|
||||
content.extend_from_slice(format!(" Tj{close}\n").as_bytes());
|
||||
if let Some(r) = self.text_link(s, h, page_ref, &mut last_link)? {
|
||||
if let Some(r) = self.text_link(s, h, page_ref, &mut last_link, turn)? {
|
||||
annots.push(Object::Ref(r));
|
||||
}
|
||||
}
|
||||
@@ -637,6 +655,8 @@ impl Emitter<'_> {
|
||||
xobjects.set(name.clone().into_bytes(), Object::Ref(r));
|
||||
content.extend(format!("q {} 0 0 {} {} {} cm /{name} Do Q\n", n(rect.w), n(rect.h), n(rect.x), n(h - rect.bottom())).bytes());
|
||||
}
|
||||
// Opened out by `steps`.
|
||||
Item::Turned(_) => {}
|
||||
Item::Widget(w) => {
|
||||
if let Some(r) = self.widget(w, page, page_ref)? {
|
||||
annots.push(Object::Ref(r));
|
||||
@@ -675,12 +695,25 @@ impl Emitter<'_> {
|
||||
/// A span that goes on the link `last` ended in, on the same line, widens that annotation
|
||||
/// instead (a link whose text changes style partway is one link). An address PdfCraft
|
||||
/// wouldn't open from a document leaves the text as plain text, with one warning for the form.
|
||||
fn text_link(&mut self, s: &Span, page_height: f64, page_ref: ObjRef, last: &mut Option<LinkPiece>) -> Result<Option<ObjRef>, XfaError> {
|
||||
fn text_link(
|
||||
&mut self,
|
||||
s: &Span,
|
||||
page_height: f64,
|
||||
page_ref: ObjRef,
|
||||
last: &mut Option<LinkPiece>,
|
||||
turn: Option<[f64; 6]>,
|
||||
) -> Result<Option<ObjRef>, XfaError> {
|
||||
let piece = self.link_piece(s, page_height);
|
||||
let Some((target, rect)) = piece else {
|
||||
let Some((target, mut rect)) = piece else {
|
||||
*last = None;
|
||||
return Ok(None);
|
||||
};
|
||||
// Turned text is drawn by a matrix, and clicked where that puts it. Its pieces don't join:
|
||||
// that goes by the line they stand on before turning.
|
||||
if let Some(m) = turn {
|
||||
rect = turned_rect(rect, m);
|
||||
*last = None;
|
||||
}
|
||||
if let Some(prev) = last.as_mut()
|
||||
&& prev.uri == target
|
||||
&& (prev.baseline - s.baseline).abs() < 0.01
|
||||
@@ -765,6 +798,45 @@ fn write_button_action(action: Option<&Action>, d: &mut Dict) {
|
||||
}
|
||||
}
|
||||
|
||||
/// What a page's content is written from: its items, with a turned draw opened out.
|
||||
enum Step<'a> {
|
||||
Item(&'a Item),
|
||||
Turn(&'a Turned),
|
||||
EndTurn,
|
||||
}
|
||||
|
||||
fn steps(items: &[Item]) -> Vec<Step<'_>> {
|
||||
let mut out = Vec::with_capacity(items.len());
|
||||
for item in items {
|
||||
if let Item::Turned(t) = item {
|
||||
out.push(Step::Turn(t));
|
||||
out.extend(t.items.iter().map(Step::Item));
|
||||
out.push(Step::EndTurn);
|
||||
} else {
|
||||
out.push(Step::Item(item));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The matrix that turns `t` about its point, on a page `page_height` tall.
|
||||
fn turn_matrix(t: &Turned, page_height: f64) -> [f64; 6] {
|
||||
let (cos, sin) = match t.quarters {
|
||||
1 => (0.0, 1.0),
|
||||
2 => (-1.0, 0.0),
|
||||
_ => (0.0, -1.0),
|
||||
};
|
||||
let (x, y) = (t.about.0, page_height - t.about.1);
|
||||
[cos, sin, -sin, cos, x - cos * x + sin * y, y - sin * x - cos * y]
|
||||
}
|
||||
|
||||
/// Rectangle `r`, `[left, bottom, right, top]`, under quarter-turn matrix `m`.
|
||||
fn turned_rect(r: [f64; 4], m: [f64; 6]) -> [f64; 4] {
|
||||
let at = |x: f64, y: f64| (m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]);
|
||||
let (a, b) = (at(r[0], r[1]), at(r[2], r[3]));
|
||||
[a.0.min(b.0), a.1.min(b.1), a.0.max(b.0), a.1.max(b.1)]
|
||||
}
|
||||
|
||||
/// The link annotation a page's text last ended in: the next piece of the same link on the same
|
||||
/// line widens it.
|
||||
struct LinkPiece {
|
||||
|
||||
@@ -4145,3 +4145,389 @@ fn clear_password_values_clears_static_xfa_password_fields() {
|
||||
let packets = read_packets(&doc).unwrap().unwrap();
|
||||
assert!(!packets.xdp.contains("hunter2"), "{}", packets.xdp);
|
||||
}
|
||||
|
||||
/// A form with `draws` in a subform placed by position, on a letter page whose content area is a
|
||||
/// quarter inch in, with `master` on the page itself.
|
||||
fn form_of_draws(master: &str, draws: &str) -> String {
|
||||
format!(
|
||||
r#"<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/"><template xmlns="http://www.xfa.org/schema/xfa-template/3.3/"><subform name="form" layout="tb"><pageSet><pageArea name="p">{master}<contentArea x="0.25in" y="0.25in" w="8in" h="10.5in"/><medium stock="letter" short="8.5in" long="11in"/></pageArea></pageSet><subform name="s" w="8in" h="2.5in">{draws}</subform></subform></template></xdp:xdp>"#
|
||||
)
|
||||
}
|
||||
|
||||
/// A line from the top-left to the bottom-right of a box 0.7in × 0.4in, as #5561's form draws.
|
||||
fn line_draw(x: &str, y: &str, rotate: &str) -> String {
|
||||
format!(r#"<draw x="{x}" y="{y}" w="0.7in" h="0.4in" {rotate}><value><line><edge thickness="2.5pt"/></line></value></draw>"#)
|
||||
}
|
||||
|
||||
/// `xdp` laid out into its document, and that document's first page.
|
||||
fn written(xdp: &str) -> (Document, Dict) {
|
||||
let mut doc = Document::open(Arc::new(shell(xdp))).unwrap();
|
||||
render_into(&mut doc).unwrap();
|
||||
let root = doc.get(doc.root().unwrap());
|
||||
let pages = doc.get(root.as_dict().unwrap().reference(b"Pages").unwrap());
|
||||
let kids = pages.as_dict().unwrap().get(b"Kids").map(|k| doc.resolve(k)).unwrap();
|
||||
let page = doc.get(kids.as_array().unwrap()[0].as_ref().unwrap());
|
||||
let page = page.as_dict().unwrap().clone();
|
||||
(doc, page)
|
||||
}
|
||||
|
||||
fn page_content(doc: &Document, page: &Dict) -> String {
|
||||
let contents = doc.get(page.reference(b"Contents").unwrap());
|
||||
let Object::Stream(s) = &*contents else { panic!("not a stream") };
|
||||
String::from_utf8_lossy(&s.decoded().unwrap()).into_owned()
|
||||
}
|
||||
|
||||
/// The ends of each line `content` strokes, on the page: through the matrices around it.
|
||||
fn stroked_lines(content: &str) -> Vec<[(f64, f64); 2]> {
|
||||
let (mut stack, mut matrix) = (Vec::new(), [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
|
||||
let (mut numbers, mut from, mut lines): (Vec<f64>, _, _) = (Vec::new(), (0.0, 0.0), Vec::new());
|
||||
for token in content.split_whitespace() {
|
||||
let at = |m: [f64; 6], x: f64, y: f64| (m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]);
|
||||
let last = |numbers: &[f64]| (numbers[numbers.len() - 2], numbers[numbers.len() - 1]);
|
||||
match token {
|
||||
"q" => stack.push(matrix),
|
||||
"Q" => matrix = stack.pop().unwrap(),
|
||||
"cm" => {
|
||||
let n: [f64; 6] = numbers[numbers.len() - 6..].try_into().unwrap();
|
||||
let m = matrix;
|
||||
matrix = [
|
||||
n[0] * m[0] + n[1] * m[2],
|
||||
n[0] * m[1] + n[1] * m[3],
|
||||
n[2] * m[0] + n[3] * m[2],
|
||||
n[2] * m[1] + n[3] * m[3],
|
||||
n[4] * m[0] + n[5] * m[2] + m[4],
|
||||
n[4] * m[1] + n[5] * m[3] + m[5],
|
||||
];
|
||||
}
|
||||
"m" => {
|
||||
let (x, y) = last(&numbers);
|
||||
from = at(matrix, x, y);
|
||||
}
|
||||
"l" => {
|
||||
let (x, y) = last(&numbers);
|
||||
lines.push([from, at(matrix, x, y)]);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
match token.parse() {
|
||||
Ok(v) => numbers.push(v),
|
||||
Err(_) => numbers.clear(),
|
||||
}
|
||||
}
|
||||
lines
|
||||
}
|
||||
|
||||
fn turned(page: &Page) -> Vec<&crate::layout::Turned> {
|
||||
page.items.iter().filter_map(|i| if let Item::Turned(t) = i { Some(t.as_ref()) } else { None }).collect()
|
||||
}
|
||||
|
||||
/// A draw turns counter-clockwise about its anchor. The figures are where Acrobat put these lines
|
||||
/// on the page it flattened for #5561's form: from the anchor by (+50.4, -28.8) unturned, and by
|
||||
/// (+28.8, +50.4), (-50.4, +28.8) and (-28.8, -50.4) at 90, 180 and 270 degrees.
|
||||
#[test]
|
||||
fn a_draw_turns_counter_clockwise_about_its_anchor() {
|
||||
let draws = [
|
||||
line_draw("1in", "0.95in", ""),
|
||||
line_draw("3in", "0.95in", r#"rotate="90""#),
|
||||
line_draw("5in", "0.95in", r#"rotate="180""#),
|
||||
line_draw("7in", "0.95in", r#"rotate="270""#),
|
||||
]
|
||||
.concat();
|
||||
let (doc, page) = written(&form_of_draws("", &draws));
|
||||
let lines = stroked_lines(&page_content(&doc, &page));
|
||||
assert_eq!(lines.len(), 4, "{lines:?}");
|
||||
let top = 792.0 - 18.0 - 0.95 * 72.0;
|
||||
let acrobat = [((90.0, top), (50.4, -28.8)), ((234.0, top), (28.8, 50.4)), ((378.0, top), (-50.4, 28.8)), ((522.0, top), (-28.8, -50.4))];
|
||||
for ([from, to], (anchor, by)) in lines.iter().zip(acrobat) {
|
||||
assert!((from.0 - anchor.0).abs() < 0.01 && (from.1 - anchor.1).abs() < 0.01, "{from:?} for {anchor:?}");
|
||||
let moved = (to.0 - from.0, to.1 - from.1);
|
||||
assert!((moved.0 - by.0).abs() < 0.01 && (moved.1 - by.1).abs() < 0.01, "{moved:?} for {by:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The anchor is the point it turns about, whichever corner that is: a box anchored at its bottom
|
||||
/// right stands up and to the left of it, not where a top-left anchor would turn it.
|
||||
#[test]
|
||||
fn a_draw_turns_about_the_anchor_it_names() {
|
||||
let draw = r#"<draw x="3in" y="1in" w="0.7in" h="0.4in" anchorType="bottomRight" rotate="90"><value><line><edge thickness="1pt"/></line></value></draw>"#;
|
||||
let (doc, page) = written(&form_of_draws("", draw));
|
||||
let lines = stroked_lines(&page_content(&doc, &page));
|
||||
let anchor = (18.0 + 216.0, 792.0 - 18.0 - 72.0);
|
||||
// Its bottom right is 0.7in right and 0.4in down of its top left, here turned to 0.7in up and
|
||||
// 0.4in right: so the top left is that far back from the anchor, and the line ends on it.
|
||||
let [from, to] = lines[0];
|
||||
assert!((to.0 - anchor.0).abs() < 0.01 && (to.1 - anchor.1).abs() < 0.01, "{to:?} for {anchor:?}");
|
||||
assert!((from.0 - (anchor.0 - 28.8)).abs() < 0.01 && (from.1 - (anchor.1 - 50.4)).abs() < 0.01, "{from:?}");
|
||||
}
|
||||
|
||||
/// Each of the nine anchors stays where it is, and the box goes round it at each turn. What is
|
||||
/// asked for comes from the box's corners here, not from layout: from the anchor, y up, the line
|
||||
/// runs from the top left to the bottom right, and a quarter turn takes (x, y) to (-y, x).
|
||||
#[test]
|
||||
fn a_draw_turns_about_each_of_the_nine_anchors() {
|
||||
let (w, h) = (50.4, 28.8);
|
||||
let anchor = (18.0 + 216.0, 792.0 - 18.0 - 72.0);
|
||||
for (name, down) in [("top", 0.0), ("middle", 0.5), ("bottom", 1.0)] {
|
||||
for (side, across) in [("Left", 0.0), ("Center", 0.5), ("Right", 1.0)] {
|
||||
for quarters in 0..4 {
|
||||
let rotate = quarters * 90;
|
||||
let draw = format!(
|
||||
r#"<draw x="3in" y="1in" w="0.7in" h="0.4in" anchorType="{name}{side}" rotate="{rotate}"><value><line><edge thickness="1pt"/></line></value></draw>"#
|
||||
);
|
||||
let (doc, page) = written(&form_of_draws("", &draw));
|
||||
let lines = stroked_lines(&page_content(&doc, &page));
|
||||
assert_eq!(lines.len(), 1, "{name}{side} at {rotate}: {lines:?}");
|
||||
let (left, top) = (-across * w, down * h);
|
||||
for (got, corner) in lines[0].iter().zip([(left, top), (left + w, top - h)]) {
|
||||
let turned = (0..quarters).fold(corner, |(x, y), _| (-y, x));
|
||||
let want = (anchor.0 + turned.0, anchor.1 + turned.1);
|
||||
assert!((got.0 - want.0).abs() < 0.01 && (got.1 - want.1).abs() < 0.01, "{name}{side} at {rotate}: {got:?} for {want:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A draw that stands past the edge of the page once turned stays where it turned to: nothing moves
|
||||
/// it back or cuts it, as for an upright one. From the page's top left corner of the content area
|
||||
/// it goes off to the left and above the page, and from its right side, off to the right.
|
||||
#[test]
|
||||
fn a_turned_draw_stands_past_the_edge_of_the_page() {
|
||||
for (x, rotate, anchor, by) in [("0in", "180", 18.0, (-50.4, 28.8)), ("8in", "90", 594.0, (28.8, 50.4))] {
|
||||
let draw = line_draw(x, "0in", &format!(r#"rotate="{rotate}""#));
|
||||
let (doc, page) = written(&form_of_draws("", &draw));
|
||||
let content = page_content(&doc, &page);
|
||||
assert!(!content.split_whitespace().any(|t| t == "W" || t == "W*"), "{rotate}: nothing clips it: {content}");
|
||||
let lines = stroked_lines(&content);
|
||||
assert_eq!(lines.len(), 1, "{rotate}: {lines:?}");
|
||||
let [from, to] = lines[0];
|
||||
assert!((from.0 - anchor).abs() < 0.01 && (from.1 - 774.0).abs() < 0.01, "{rotate}: {from:?}");
|
||||
assert!((to.0 - (anchor + by.0)).abs() < 0.01 && (to.1 - (774.0 + by.1)).abs() < 0.01, "{rotate}: {to:?}");
|
||||
assert!(to.0 < 0.0 || to.0 > 612.0, "{rotate}: it does stand past the edge, at {to:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Everything a draw shows turns with it, under one matrix: its text, its picture and its border,
|
||||
/// rounded or not, each written as if it stood upright.
|
||||
#[test]
|
||||
fn text_pictures_and_borders_turn_with_their_draw() {
|
||||
let picture = format!(
|
||||
r#"<draw x="1in" y="1in" w="1in" h="1in" rotate="90"><value><image contentType="image/png">{}</image></value></draw>"#,
|
||||
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, tiny_png())
|
||||
);
|
||||
for (draw, shows) in [
|
||||
(r#"<draw x="1in" y="1in" w="2in" h="0.5in" rotate="90"><value><text>upright</text></value></draw>"#.to_string(), "(upright) Tj"),
|
||||
(picture, " Do "),
|
||||
(r#"<draw x="1in" y="1in" w="2in" h="0.5in" rotate="90"><value><rectangle><edge thickness="1pt"/></rectangle></value></draw>"#.to_string(), " l S"),
|
||||
(
|
||||
r#"<draw x="1in" y="1in" w="2in" h="0.5in" rotate="90"><value><rectangle><edge thickness="1pt"/><corner radius="5pt"/></rectangle></value></draw>"#.to_string(),
|
||||
" c",
|
||||
),
|
||||
(r#"<draw x="1in" y="1in" w="2in" h="0.5in" rotate="90"><border><fill><color value="255,0,0"/></fill></border><value><text>x</text></value></draw>"#.to_string(), " re f"),
|
||||
] {
|
||||
let (doc, page) = written(&form_of_draws("", &draw));
|
||||
let content = page_content(&doc, &page);
|
||||
let open = content.find("0 1 -1 0 ").unwrap_or_else(|| panic!("no matrix for {draw}: {content}"));
|
||||
let close = open + content[open..].find("\nQ\n").unwrap_or_else(|| panic!("no end for {draw}: {content}"));
|
||||
assert!(content[open..close].contains(shows), "{shows} outside the matrix: {content}");
|
||||
}
|
||||
}
|
||||
|
||||
/// `rotate` as a multiple of 90 degrees, with a unit or not, past a full turn or below none;
|
||||
/// anything else leaves the draw as it stands.
|
||||
#[test]
|
||||
fn rotate_reads_as_quarter_turns() {
|
||||
let quarters = |rotate: &str| {
|
||||
let form = layout_xml(&form_of_draws("", &line_draw("1in", "1in", &format!(r#"rotate="{rotate}""#)))).unwrap();
|
||||
turned(&form.pages[0]).first().map_or(0, |t| t.quarters)
|
||||
};
|
||||
for (rotate, want) in [("90", 1), ("180", 2), ("270", 3), ("0", 0), ("360", 0), ("450", 1), ("-90", 3), ("90deg", 1), (" 90 ", 1), ("90.0", 1)] {
|
||||
assert_eq!(quarters(rotate), want, "{rotate}");
|
||||
}
|
||||
for rotate in ["45", "89.5", "135", "225", "abc", "", "deg", "NaN", "inf", "-inf"] {
|
||||
assert_eq!(quarters(rotate), 0, "{rotate}");
|
||||
}
|
||||
// Far past what a turn can be told apart at, or just under none: in range whatever it reads as.
|
||||
for rotate in ["1e300", "-1e-300"] {
|
||||
assert!(quarters(rotate) < 4, "{rotate}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A turned draw is one item: a page break moves it whole, what it holds moving with the point it
|
||||
/// turns about, and doesn't cut it, and the draws around it stay where they belong.
|
||||
#[test]
|
||||
fn a_page_break_moves_a_turned_draw_whole() {
|
||||
let draws = format!(
|
||||
r#"<draw x="6in" y="10.4in" w="1in" h="0.5in"><border><edge thickness="1pt"/></border><value><text>across</text></value></draw><draw x="1in" y="1in" w="2in" h="0.5in"><value><text>first</text></value></draw>{}{}<draw x="1in" y="13in" w="2in" h="0.5in"><value><text>last</text></value></draw>"#,
|
||||
[
|
||||
line_draw("3in", "12in", r#"rotate="90""#),
|
||||
r#"<draw x="5in" y="12in" w="2in" h="0.5in" rotate="90"><border><edge thickness="1pt"/></border><value><text>sideways</text></value></draw>"#.to_string(),
|
||||
]
|
||||
.concat(),
|
||||
(2..8).map(|y| line_draw("1in", &format!("{y}in"), "")).collect::<String>()
|
||||
);
|
||||
let xdp = form_of_draws("", &draws).replace(r#"h="2.5in""#, r#"h="14in""#);
|
||||
let form = layout_xml(&xdp).unwrap();
|
||||
assert!(form.pages.len() >= 2, "{}", form.pages.len());
|
||||
assert_eq!(texts(&form.pages[0]), ["first"]);
|
||||
// Written after the turned draws, these lines still belong at the top of the block: what the
|
||||
// break knew of the draw's many items mustn't carry over to those that now stand in their places.
|
||||
let lines = form.pages[0].items.iter().filter(|i| matches!(i, Item::Line { .. })).count();
|
||||
assert_eq!(lines, 6, "{:?}", form.pages[0].items);
|
||||
assert_eq!(turned(&form.pages[0]).len(), 0);
|
||||
let moved = turned(&form.pages[1]);
|
||||
assert_eq!(moved.len(), 2, "{:?}", form.pages[1].items);
|
||||
let Item::Line { from, .. } = &moved[0].items[0] else { panic!("not a line") };
|
||||
assert_eq!(*from, moved[0].about, "the line moved with the point it turns about");
|
||||
// Turned 90 degrees, a box 0.7in wide stands 0.7in up from its anchor, on a second page.
|
||||
assert!(moved[0].about.1 < 792.0, "{:?}", moved[0].about);
|
||||
assert!((moved[0].extent.1 - moved[0].about.1).abs() < 0.01, "{:?}", moved[0]);
|
||||
assert!((moved[0].about.1 - moved[0].extent.0 - 50.4).abs() < 0.01, "{:?}", moved[0]);
|
||||
let moved_text = texts(&form.pages[1]);
|
||||
assert!(moved_text.contains(&"last".to_string()) && moved_text.contains(&"across".to_string()), "{moved_text:?}");
|
||||
}
|
||||
|
||||
/// What a page break goes by is the box as it stands once turned, whichever way, about whichever
|
||||
/// corner: from a top-left anchor, it stands up, up or down of it; from a bottom-right one, the
|
||||
/// box starts behind the anchor and the same turns take it the other way.
|
||||
#[test]
|
||||
fn a_turned_draw_knows_where_it_stands() {
|
||||
let box_at = |anchor: &str, rotate: u32| {
|
||||
let draw = format!(
|
||||
r#"<draw x="3in" y="1in" w="0.7in" h="0.4in" {anchor} rotate="{rotate}"><value><line><edge thickness="1pt"/></line></value></draw>"#
|
||||
);
|
||||
let form = layout_xml(&form_of_draws("", &draw)).unwrap();
|
||||
let held = turned(&form.pages[0]);
|
||||
let t = held.first().expect("turned");
|
||||
(t.extent.0 - t.about.1, t.extent.1 - t.about.1)
|
||||
};
|
||||
let close = |got: (f64, f64), want: (f64, f64)| (got.0 - want.0).abs() < 0.01 && (got.1 - want.1).abs() < 0.01;
|
||||
for (rotate, want) in [(90, (-50.4, 0.0)), (180, (-28.8, 0.0)), (270, (0.0, 50.4))] {
|
||||
assert!(close(box_at("", rotate), want), "{rotate}: {:?}", box_at("", rotate));
|
||||
}
|
||||
for (rotate, want) in [(90, (0.0, 50.4)), (180, (0.0, 28.8)), (270, (-50.4, 0.0))] {
|
||||
let got = box_at(r#"anchorType="bottomRight""#, rotate);
|
||||
assert!(close(got, want), "{rotate}: {got:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The page count is filled in inside a turned draw too, as it comes last, when every page is known.
|
||||
#[test]
|
||||
fn the_page_count_reaches_a_turned_draw() {
|
||||
let master = r##"<draw x="7in" y="10in" w="1in" h="0.2in" rotate="90"><value><exData contentType="text/html"><body xmlns="http://www.w3.org/1999/xhtml"><p>of <span xfa:embed="#pageCount" xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/"/></p></body></exData></value></draw><field name="pageCount" id="pageCount" presence="hidden" w="1in" h="0.2in"><ui><textEdit/></ui><event activity="ready" ref="$layout"><script contentType="application/x-javascript">this.rawValue = xfa.layout.pageCount();</script></event></field>"##;
|
||||
let form = layout_xml(&form_of_draws(master, &line_draw("1in", "1in", ""))).unwrap();
|
||||
let held = turned(&form.pages[0]);
|
||||
assert_eq!(held.len(), 1, "{:?}", form.pages[0].items);
|
||||
let text: Vec<&str> = held[0].items.iter().filter_map(|i| if let Item::Text(s) = i { Some(s.text.as_str()) } else { None }).collect();
|
||||
assert_eq!(text.concat().trim(), "of 1", "{text:?}");
|
||||
}
|
||||
|
||||
/// A link in turned text is clicked where the text is drawn, whichever way it turned: the box it
|
||||
/// has unturned, turned about the draw's anchor.
|
||||
#[test]
|
||||
fn a_link_turns_with_its_text() {
|
||||
let rect_at = |rotate: &str| {
|
||||
let draw = format!(
|
||||
r#"<draw x="1in" y="3in" w="2in" h="0.5in" {rotate}><value><exData contentType="text/html"><body xmlns="http://www.w3.org/1999/xhtml"><p><a href="https://example.com/a">a link in turned text</a></p></body></exData></value></draw>"#
|
||||
);
|
||||
let (doc, page) = written(&form_of_draws("", &draw));
|
||||
let annots = page.get(b"Annots").map(|a| doc.resolve(a)).expect("the link");
|
||||
let annot = doc.get(annots.as_array().unwrap()[0].as_ref().unwrap());
|
||||
let rect: Vec<f64> = annot.as_dict().unwrap().get(b"Rect").unwrap().as_array().unwrap().iter().map(|v| v.as_f64().unwrap()).collect();
|
||||
rect
|
||||
};
|
||||
let upright = rect_at("");
|
||||
let (ax, ay) = (18.0 + 72.0, 792.0 - 18.0 - 216.0);
|
||||
let (left, bottom, right, top) = (upright[0] - ax, upright[1] - ay, upright[2] - ax, upright[3] - ay);
|
||||
assert!(left >= -0.01 && top <= 0.01 && right - left > 3.0 * (top - bottom), "it stands right and down of its anchor: {upright:?}");
|
||||
for (rotate, want) in [
|
||||
("90", [ax - top, ay + left, ax - bottom, ay + right]),
|
||||
("180", [ax - right, ay - top, ax - left, ay - bottom]),
|
||||
("270", [ax + bottom, ay - right, ax + top, ay - left]),
|
||||
] {
|
||||
let got = rect_at(&format!(r#"rotate="{rotate}""#));
|
||||
assert!(got.iter().zip(want).all(|(g, w)| (g - w).abs() < 0.01), "{rotate}: {got:?} for {want:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Only a draw placed by position turns: a field, and a draw in a flow, stand as they did.
|
||||
#[test]
|
||||
fn only_positioned_draws_turn() {
|
||||
let field = r#"<field name="f" x="1in" y="1in" w="2in" h="0.5in" rotate="90"><ui><textEdit/></ui></field>"#;
|
||||
let form = layout_xml(&form_of_draws("", field)).unwrap();
|
||||
assert!(turned(&form.pages[0]).is_empty());
|
||||
assert_eq!(widgets(&form.pages[0]).len(), 1);
|
||||
let flowed = form_of_draws("", &line_draw("1in", "1in", r#"rotate="90""#))
|
||||
.replace(r#"<subform name="s" w="8in" h="2.5in">"#, r#"<subform name="s" w="8in" layout="tb">"#);
|
||||
let form = layout_xml(&flowed).unwrap();
|
||||
assert!(turned(&form.pages[0]).is_empty(), "{:?}", form.pages[0].items);
|
||||
assert!(form.pages[0].items.iter().any(|i| matches!(i, Item::Line { .. })), "it is still drawn");
|
||||
// A draw that shows nothing has nothing to turn.
|
||||
let form = layout_xml(&form_of_draws("", &line_draw("1in", "1in", r#"rotate="90" presence="invisible""#))).unwrap();
|
||||
assert!(form.pages[0].items.is_empty(), "{:?}", form.pages[0].items);
|
||||
}
|
||||
|
||||
/// A block carried over to a content area further across takes a turned draw with it, point and
|
||||
/// items both.
|
||||
#[test]
|
||||
fn a_turned_draw_moves_across_with_its_content_area() {
|
||||
let xdp = form_of_draws("", &line_draw("1in", "6in", r#"rotate="90""#))
|
||||
.replace(
|
||||
r#"<contentArea x="0.25in" y="0.25in" w="8in" h="10.5in"/>"#,
|
||||
r#"<contentArea x="0.25in" y="0.25in" w="3in" h="5in"/><contentArea x="4in" y="0.25in" w="3in" h="5in"/>"#,
|
||||
)
|
||||
.replace(r#"h="2.5in""#, r#"h="7in""#);
|
||||
let form = layout_xml(&xdp).unwrap();
|
||||
let moved = form.pages.iter().flat_map(turned).next().expect("turned");
|
||||
let Item::Line { from, .. } = &moved.items[0] else { panic!("not a line") };
|
||||
assert_eq!(*from, moved.about);
|
||||
assert!((moved.about.0 - (4.0 + 1.0) * 72.0).abs() < 0.01, "{:?}", moved.about);
|
||||
}
|
||||
|
||||
/// A positioned subform with no height grows to hold what it holds as it stands, so what comes next
|
||||
/// neither runs through a turned draw nor leaves the room its upright box would have had.
|
||||
#[test]
|
||||
fn a_container_grows_to_a_turned_draw_as_it_stands() {
|
||||
let next = r#"</subform><subform name="next" w="8in" h="1in"><draw x="0in" y="0in" w="2in" h="0.5in"><value><text>next</text></value></draw></subform></subform></template>"#;
|
||||
let baseline = |rotate: &str| {
|
||||
let xdp = form_of_draws("", &line_draw("1in", "1in", rotate)).replace(r#" h="2.5in""#, "").replace("</subform></subform></template>", next);
|
||||
let form = layout_xml(&xdp).unwrap();
|
||||
assert_eq!(form.pages.len(), 1);
|
||||
form.pages[0].items.iter().find_map(|i| if let Item::Text(s) = i { Some(s.baseline) } else { None }).expect("the next block")
|
||||
};
|
||||
// Upright the box ends 0.4in below the anchor, and the next block starts there.
|
||||
let upright = baseline("");
|
||||
for (rotate, bottom) in [("90", 0.0), ("180", 0.0), ("270", 50.4)] {
|
||||
let got = baseline(&format!(r#"rotate="{rotate}""#)) - (upright - 28.8);
|
||||
assert!((got - bottom).abs() < 0.01, "{rotate}: the next block starts {got} below the anchor, not {bottom}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A subform with no width is as wide as what it holds as it stands, whichever way that turned,
|
||||
/// so a neighbour in a row doesn't stand off from it or over it.
|
||||
#[test]
|
||||
fn a_container_is_as_wide_as_a_turned_draw_stands() {
|
||||
let row = |rotate: &str| {
|
||||
let xdp = form_of_draws(
|
||||
"",
|
||||
&format!(
|
||||
r#"<subform name="turned">{}</subform><subform name="beside" w="1in" h="1in"><draw x="0in" y="0in" w="1in" h="0.5in"><value><text>beside</text></value></draw></subform>"#,
|
||||
line_draw("1in", "1in", rotate)
|
||||
),
|
||||
)
|
||||
.replace(r#"<subform name="s" w="8in" h="2.5in">"#, r#"<subform name="s" w="8in" h="2.5in" layout="lr-tb">"#);
|
||||
let form = layout_xml(&xdp).unwrap();
|
||||
form.pages[0].items.iter().find_map(|i| if let Item::Text(s) = i { Some(s.x) } else { None }).expect("the neighbour")
|
||||
};
|
||||
let upright = row("");
|
||||
// Upright it ends 0.7in right of the anchor; turned, 0.4in right, at the anchor, and at it again.
|
||||
for (rotate, right) in [("90", 28.8), ("180", 0.0), ("270", 0.0)] {
|
||||
let got = row(&format!(r#"rotate="{rotate}""#)) - (upright - 50.4);
|
||||
assert!((got - right).abs() < 0.01, "{rotate}: the neighbour stands {got} right of the anchor, not {right}");
|
||||
}
|
||||
// From a bottom-right anchor the box starts behind it, and the same turns take it the other way.
|
||||
for (rotate, right) in [("90", 0.0), ("180", 50.4), ("270", 28.8)] {
|
||||
let got = row(&format!(r#"anchorType="bottomRight" rotate="{rotate}""#)) - (upright - 50.4);
|
||||
assert!((got - right).abs() < 0.01, "{rotate}: the neighbour stands {got} right of the anchor, not {right}");
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user