mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-27 11:37:59 -06:00
crates/xfa/src/tests.rs: union of fixture imports (tiny_bmp, tiny_tiff). parity/acrobat-features.toml: form.xfa and the XFA image entry: evidence lists unioned, notes take both sides' insertions.
661 lines
38 KiB
Rust
661 lines
38 KiB
Rust
//! Synthetic fixtures for tests in this and other crates (never real forms).
|
||
|
||
#![doc(hidden)]
|
||
|
||
/// A small dynamic form: a letter page master with a page-number draw, a title, a text field
|
||
/// with a caption on top, a check box, a radio group beside a question, a date field, a table
|
||
/// with a repeated header row and `rows` data rows, and a last section that starts a new page.
|
||
pub fn template(rows: usize) -> String {
|
||
let mut t = String::from(
|
||
r##"<?xml version="1.0" encoding="UTF-8"?>
|
||
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
|
||
<config xmlns="http://www.xfa.org/schema/xci/3.0/"><present><pdf><version>1.7</version></pdf></present><template><base/></template></config>
|
||
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
|
||
<subform name="form" layout="tb" locale="en_US">
|
||
<pageSet>
|
||
<pageArea name="front" id="front"><occur min="1" max="1"/>
|
||
<contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/>
|
||
<medium stock="letter" short="8.5in" long="11in"/>
|
||
<draw name="pn" x="7in" y="10.6in" w="1in" h="0.2in"><value><exData contentType="text/html"><body xmlns="http://www.w3.org/1999/xhtml"><p>Page <span xfa:embed="#pageNo" xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/"/> of <span xfa:embed="#pageCount" xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/"/></p></body></exData></value><font typeface="Arial" size="8pt"/></draw>
|
||
<field name="pageNo" id="pageNo" presence="hidden" w="1in" h="0.2in"><ui><textEdit/></ui><event activity="ready" ref="$layout"><script contentType="application/x-javascript">this.rawValue = xfa.layout.page(this);</script></event></field>
|
||
<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>
|
||
</pageArea>
|
||
<pageArea name="rest"><occur max="-1"/>
|
||
<contentArea x="0.5in" y="1in" w="7.5in" h="9.5in"/>
|
||
<medium stock="letter" short="8.5in" long="11in"/>
|
||
</pageArea>
|
||
</pageSet>
|
||
<subform name="head" layout="lr-tb">
|
||
<draw name="title" w="7.5in" h="0.4in"><value><exData contentType="text/html"><body xmlns="http://www.w3.org/1999/xhtml"><p style="font-weight:bold">Sample Form</p><p>Second line</p></body></exData></value><font typeface="Arial" size="12pt"/><para hAlign="center"/></draw>
|
||
<draw name="hidden" presence="hidden" w="7.5in" h="3in"><value><text>never shown</text></value></draw>
|
||
<field name="familyName" w="3in" h="0.5in"><ui><textEdit><border><edge presence="hidden"/><edge presence="hidden"/><edge/><edge presence="hidden"/></border></textEdit></ui><font typeface="Courier New" size="9pt"/><caption placement="top" reserve="0.2in"><value><text>Family name</text></value><font typeface="Arial" size="7pt"/></caption><assist><toolTip>Your family name</toolTip></assist><value><text maxChars="30"/></value></field>
|
||
<field name="agree" w="2in" h="0.25in"><ui><checkButton><border><edge/><fill/></border></checkButton></ui><caption placement="right" reserve="1.5in"><value><text>I agree</text></value></caption><items><integer>1</integer><integer>0</integer></items></field>
|
||
<field name="born" w="2in" h="0.5in"><ui><dateTimeEdit/><picture>date{YYYY-MM-DD}</picture></ui><caption placement="top" reserve="0.2in"><value><text>Date of birth</text></value></caption><value><date/></value></field>
|
||
<draw name="q" w="6.5in" h="0.25in"><value><text>Have you ever?</text></value></draw>
|
||
<exclGroup name="answer" layout="lr-tb">
|
||
<field name="yes" w="0.5in" h="0.25in"><ui><checkButton/></ui><items><text>Y</text></items></field>
|
||
<field name="no" w="0.5in" h="0.25in"><ui><checkButton/></ui><items><text>N</text></items></field>
|
||
</exclGroup>
|
||
<field name="go" w="1in" h="0.3in"><ui><button/></ui><caption><value><text>Reset</text></value></caption><border><edge stroke="raised"/><fill><color value="212,208,200"/></fill></border><event activity="click"><script contentType="application/x-javascript">xfa.host.resetData();</script></event></field>
|
||
</subform>
|
||
<subform name="table" layout="table" columnWidths="2in 3in 2.5in">
|
||
<overflow leader="header"/>
|
||
<subform name="header" layout="row"><occur max="-1"/>
|
||
<draw w="2in" h="0.3in"><value><text>From</text></value><border><edge/><fill><color value="200,200,200"/></fill></border></draw>
|
||
<draw h="0.3in"><value><text>Activity</text></value><border><edge/></border></draw>
|
||
<draw h="0.3in"><value><text>Place</text></value><border><edge/></border></draw>
|
||
</subform>
|
||
"##,
|
||
);
|
||
for i in 0..rows {
|
||
t.push_str(&format!(
|
||
r##" <subform name="row" layout="row"><occur max="-1"/><field name="from{i}" h="0.4in"><ui><textEdit/></ui></field><field name="what{i}" h="0.4in"><ui><textEdit/></ui></field><field name="where{i}" h="0.4in"><ui><textEdit/></ui></field></subform>
|
||
"##
|
||
));
|
||
}
|
||
t.push_str(
|
||
r##" </subform>
|
||
<subform name="last" layout="tb"><breakBefore targetType="pageArea"/>
|
||
<draw name="sig" w="7.5in" h="0.3in"><value><text>Signature</text></value></draw>
|
||
</subform>
|
||
</subform>
|
||
</template>
|
||
</xdp:xdp>
|
||
"##,
|
||
);
|
||
t
|
||
}
|
||
|
||
/// A growable rich-text field followed by a section that starts on a new page.
|
||
pub fn growable_text_template(scripted: bool) -> String {
|
||
let event = if scripted { r#"<event activity="initialize"><script contentType="application/x-javascript">0;</script></event>"# } else { "" };
|
||
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">{event}
|
||
<pageSet><pageArea name="master"><occur max="-1"/>
|
||
<contentArea x="36pt" y="36pt" w="540pt" h="720pt"/><medium short="612pt" long="792pt"/>
|
||
</pageArea></pageSet>
|
||
<subform name="Page3" layout="tb">
|
||
<field name="ACTIVIDADES" w="540pt" minH="207.017mm"><ui><textEdit multiLine="1" allowRichText="1"/></ui>
|
||
<font typeface="Courier" size="8pt"/><value><exData contentType="text/html"/></value>
|
||
</field>
|
||
</subform>
|
||
<subform name="following" layout="tb"><breakBefore targetType="pageArea"/>
|
||
<draw name="end" w="500pt" h="20pt"><value><text>Following section</text></value></draw>
|
||
</subform>
|
||
</subform></template>
|
||
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/"><xfa:data><form><Page3><ACTIVIDADES>Short</ACTIVIDADES></Page3></form></xfa:data></xfa:datasets>
|
||
</xdp:xdp>"#
|
||
)
|
||
}
|
||
|
||
/// Three body pages sharing a page-master print button and its XFA click script.
|
||
pub fn repeated_print_button_template() -> String {
|
||
let mut xml = String::from(
|
||
r#"<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
|
||
<subform name="form" layout="tb">
|
||
<pageSet><pageArea name="master"><occur max="-1"/>
|
||
<contentArea x="36pt" y="72pt" w="540pt" h="648pt"/><medium short="612pt" long="792pt"/>
|
||
<field name="PrintButton1" x="36pt" y="24pt" w="100pt" h="25pt"><ui><button/></ui>
|
||
<caption><value><text>Print form</text></value></caption>
|
||
<event activity="click"><script contentType="application/x-javascript">xfa.host.print(1, "0", (xfa.host.numPages -1).toString(), 0, 0, 0, 0, 0);</script></event>
|
||
</field>
|
||
</pageArea></pageSet>
|
||
"#,
|
||
);
|
||
for page in 1..=3 {
|
||
xml.push_str(&format!(
|
||
r#"<subform name="page{page}" layout="tb"><breakBefore targetType="pageArea"/>
|
||
<field name="note{page}" w="200pt" h="25pt"><ui><textEdit/></ui><value><text>Page {page}</text></value></field>
|
||
</subform>"#
|
||
));
|
||
}
|
||
xml.push_str("</subform></template>");
|
||
xml
|
||
}
|
||
|
||
/// [`template`] with a datasets packet holding `data` (the children of `xfa:data`).
|
||
pub fn template_with_data(rows: usize, data: &str) -> String {
|
||
let t = template(rows);
|
||
let packet =
|
||
format!("<xfa:datasets xmlns:xfa=\"http://www.xfa.org/schema/xfa-data/1.0/\"><xfa:data>{data}</xfa:data></xfa:datasets>\n</xdp:xdp>");
|
||
t.replace("</xdp:xdp>", &packet)
|
||
}
|
||
|
||
/// A static XFA form: an AcroForm with Designer-style field names (`form1[0].page1[0].name[0]`,
|
||
/// a text field, and `…agree[0]`, a check box with on state `1`), plus XFA packets whose
|
||
/// datasets hold `data` (the children of `xfa:data`). No `/NeedsRendering`.
|
||
pub fn static_shell(data: &str) -> Vec<u8> {
|
||
let xdp = format!(
|
||
"<xdp:xdp xmlns:xdp=\"http://ns.adobe.com/xdp/\"><template xmlns=\"http://www.xfa.org/schema/xfa-template/3.3/\"><subform name=\"form1\"/></template><xfa:datasets xmlns:xfa=\"http://www.xfa.org/schema/xfa-data/1.0/\"><xfa:data>{data}</xfa:data></xfa:datasets></xdp:xdp>"
|
||
);
|
||
let objs: Vec<Vec<u8>> = vec![
|
||
b"<< /Type /Catalog /Pages 2 0 R /AcroForm << /XFA 5 0 R /Fields [6 0 R] /DA (/Helv 0 Tf 0 g) /DR << /Font << /Helv << /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >> >> >> >> >>".to_vec(),
|
||
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
|
||
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R /Annots [8 0 R 9 0 R] >>".to_vec(),
|
||
b"<< /Length 0 >>\nstream\n\nendstream".to_vec(),
|
||
format!("<< /Length {} >>\nstream\n{xdp}\nendstream", xdp.len()).into_bytes(),
|
||
b"<< /T (form1[0]) /Kids [7 0 R] >>".to_vec(),
|
||
b"<< /T (page1[0]) /Parent 6 0 R /Kids [8 0 R 9 0 R] >>".to_vec(),
|
||
b"<< /Type /Annot /Subtype /Widget /FT /Tx /T (name[0]) /Parent 7 0 R /Rect [72 700 300 720] /P 3 0 R /DA (/Helv 10 Tf 0 g) /MK << /BC [0 0 0] >> >>".to_vec(),
|
||
b"<< /Type /Annot /Subtype /Widget /FT /Btn /T (agree[0]) /Parent 7 0 R /Rect [72 660 84 672] /P 3 0 R /V /Off /AS /Off /MK << /BC [0 0 0] >> /AP << /N << /1 10 0 R /Off 10 0 R >> >> >>".to_vec(),
|
||
b"<< /Type /XObject /Subtype /Form /BBox [0 0 12 12] /Length 0 >>\nstream\n\nendstream".to_vec(),
|
||
];
|
||
assemble(objs)
|
||
}
|
||
|
||
/// A PDF file holding `objs` as objects 1, 2, … in order, with a cross-reference table and a
|
||
/// trailer whose root is object 1.
|
||
fn assemble(objs: Vec<Vec<u8>>) -> Vec<u8> {
|
||
let mut out = b"%PDF-1.7\n".to_vec();
|
||
let mut offsets = Vec::new();
|
||
for (i, o) in objs.iter().enumerate() {
|
||
offsets.push(out.len());
|
||
out.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes());
|
||
out.extend_from_slice(o);
|
||
out.extend_from_slice(b"\nendobj\n");
|
||
}
|
||
let xref = out.len();
|
||
out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes());
|
||
for o in offsets {
|
||
out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
|
||
}
|
||
out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objs.len() + 1).as_bytes());
|
||
out
|
||
}
|
||
|
||
/// A hidden subform `details` a script may show, holding a named field and three unnamed
|
||
/// objects: a caption, an old caption the form keeps hidden, and a hidden subform. Unnamed
|
||
/// objects have no SOM name of their own, so showing `details` must leave the hidden ones hidden.
|
||
pub fn unnamed_hidden_template() -> String {
|
||
r##"<?xml version="1.0" encoding="UTF-8"?>
|
||
<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="front"><contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/><medium short="8.5in" long="11in"/></pageArea></pageSet>
|
||
<subform name="page1" layout="tb">
|
||
<subform name="details" layout="tb" presence="hidden">
|
||
<draw w="4in" h="0.3in"><value><text>Current caption</text></value></draw>
|
||
<draw w="4in" h="0.3in" presence="hidden"><value><text>Old caption</text></value></draw>
|
||
<subform layout="tb" presence="hidden"><draw w="4in" h="0.3in"><value><text>Old section</text></value></draw></subform>
|
||
<field name="note" w="4in" h="0.3in"><ui><textEdit/></ui></field>
|
||
</subform>
|
||
</subform>
|
||
</subform>
|
||
</template>
|
||
</xdp:xdp>
|
||
"##
|
||
.to_string()
|
||
}
|
||
|
||
/// A scripted form (JavaScript events): `qty` (initialised to 2, validated ≤ 100) × `price`
|
||
/// (5) = `total` (calculated); a check box `more` that shows the hidden `details` subform;
|
||
/// a table whose `row` (item, amount) repeats, with `addRow`, `removeRow` and `hello` buttons;
|
||
/// `grand`, the calculated sum of the amounts.
|
||
pub fn scripted_template() -> String {
|
||
r##"<?xml version="1.0" encoding="UTF-8"?>
|
||
<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="front"><contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/><medium short="8.5in" long="11in"/></pageArea></pageSet>
|
||
<subform name="page1" layout="tb">
|
||
<event activity="initialize"><script contentType="application/x-javascript">if (qty.rawValue === null) qty.rawValue = 2;</script></event>
|
||
<field name="qty" w="2in" h="0.3in"><ui><numericEdit/></ui><caption placement="left" reserve="0.8in"><value><text>Qty</text></value></caption>
|
||
<validate><script contentType="application/x-javascript">this.rawValue === null || this.rawValue <= 100</script><message><text name="scriptTest">Quantity must be 100 or less</text></message></validate></field>
|
||
<field name="price" w="2in" h="0.3in"><ui><numericEdit/></ui><caption placement="left" reserve="0.8in"><value><text>Price</text></value></caption><value><integer>5</integer></value></field>
|
||
<field name="total" w="2in" h="0.3in" access="readOnly"><ui><numericEdit/></ui><caption placement="left" reserve="0.8in"><value><text>Total</text></value></caption>
|
||
<calculate><script contentType="application/x-javascript">qty.rawValue * price.rawValue</script></calculate></field>
|
||
<field name="more" w="2in" h="0.3in"><ui><checkButton/></ui><caption placement="right" reserve="1.5in"><value><text>More details</text></value></caption><items><integer>1</integer><integer>0</integer></items>
|
||
<event activity="change"><script contentType="application/x-javascript">details.presence = (this.rawValue == 1) ? "visible" : "hidden";</script></event></field>
|
||
<subform name="details" layout="tb" presence="hidden">
|
||
<field name="note" w="4in" h="0.3in"><ui><textEdit/></ui><caption placement="left" reserve="0.8in"><value><text>Note</text></value></caption></field>
|
||
</subform>
|
||
<subform name="table" layout="table" columnWidths="3in 2in">
|
||
<subform name="row" layout="row"><occur min="1" max="-1"/>
|
||
<field name="item" h="0.3in"><ui><textEdit/></ui></field>
|
||
<field name="amount" h="0.3in"><ui><numericEdit/></ui></field>
|
||
</subform>
|
||
</subform>
|
||
<field name="grand" w="2in" h="0.3in" access="readOnly"><ui><numericEdit/></ui><caption placement="left" reserve="0.8in"><value><text>Sum</text></value></caption>
|
||
<calculate><script contentType="application/x-javascript">var t = 0; var rows = xfa.resolveNodes("table.row[*]"); for (var i = 0; i < rows.length; i++) { t += (rows.item(i).amount.rawValue || 0); } t</script></calculate></field>
|
||
<field name="addRow" w="1in" h="0.3in"><ui><button/></ui><caption><value><text>Add row</text></value></caption>
|
||
<event activity="click"><script contentType="application/x-javascript">table._row.addInstance(1);</script></event></field>
|
||
<field name="removeRow" w="1in" h="0.3in"><ui><button/></ui><caption><value><text>Remove row</text></value></caption>
|
||
<event activity="click"><script contentType="application/x-javascript">if (table._row.count > 1) { table._row.removeInstance(table._row.count - 1); }</script></event></field>
|
||
<field name="hello" w="1in" h="0.3in"><ui><button/></ui><caption><value><text>Hello</text></value></caption>
|
||
<event activity="click"><script contentType="application/x-javascript">xfa.host.messageBox("Hello " + qty.rawValue);</script></event></field>
|
||
<field name="legacy" w="1in" h="0.3in"><ui><button/></ui><caption><value><text>Legacy</text></value></caption>
|
||
<event activity="click"><script>$host.messageBox("FormCalc")</script></event></field>
|
||
</subform>
|
||
</subform>
|
||
</template>
|
||
</xdp:xdp>
|
||
"##
|
||
.to_string()
|
||
}
|
||
|
||
/// [`scripted_template`] with every script in FormCalc (no `contentType`, as Designer writes
|
||
/// the default language): the same fields, plus `words`, the total in English words.
|
||
pub fn formcalc_template() -> String {
|
||
r##"<?xml version="1.0" encoding="UTF-8"?>
|
||
<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="front"><contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/><medium short="8.5in" long="11in"/></pageArea></pageSet>
|
||
<subform name="page1" layout="tb">
|
||
<event activity="initialize"><script>if (HasValue(qty) == 0) then qty = 2 endif</script></event>
|
||
<field name="qty" w="2in" h="0.3in"><ui><numericEdit/></ui><caption placement="left" reserve="0.8in"><value><text>Qty</text></value></caption>
|
||
<validate><script>HasValue($) == 0 or $ <= 100</script><message><text name="scriptTest">Quantity must be 100 or less</text></message></validate></field>
|
||
<field name="price" w="2in" h="0.3in"><ui><numericEdit/></ui><caption placement="left" reserve="0.8in"><value><text>Price</text></value></caption><value><integer>5</integer></value></field>
|
||
<field name="total" w="2in" h="0.3in" access="readOnly"><ui><numericEdit/></ui><caption placement="left" reserve="0.8in"><value><text>Total</text></value></caption>
|
||
<calculate><script>qty * price</script></calculate></field>
|
||
<field name="words" w="4in" h="0.3in" access="readOnly"><ui><textEdit/></ui><caption placement="left" reserve="0.8in"><value><text>Words</text></value></caption>
|
||
<calculate><script>WordNum(total)</script></calculate></field>
|
||
<field name="more" w="2in" h="0.3in"><ui><checkButton/></ui><caption placement="right" reserve="1.5in"><value><text>More details</text></value></caption><items><integer>1</integer><integer>0</integer></items>
|
||
<event activity="change"><script>if ($ == 1) then
|
||
details.presence = "visible"
|
||
else
|
||
details.presence = "hidden"
|
||
endif</script></event></field>
|
||
<subform name="details" layout="tb" presence="hidden">
|
||
<field name="note" w="4in" h="0.3in"><ui><textEdit/></ui><caption placement="left" reserve="0.8in"><value><text>Note</text></value></caption></field>
|
||
</subform>
|
||
<subform name="table" layout="table" columnWidths="3in 2in">
|
||
<subform name="row" layout="row"><occur min="1" max="-1"/>
|
||
<field name="item" h="0.3in"><ui><textEdit/></ui></field>
|
||
<field name="amount" h="0.3in"><ui><numericEdit/></ui></field>
|
||
</subform>
|
||
</subform>
|
||
<field name="grand" w="2in" h="0.3in" access="readOnly"><ui><numericEdit/></ui><caption placement="left" reserve="0.8in"><value><text>Sum</text></value></caption>
|
||
<calculate><script>Sum(table.row[*].amount)</script></calculate></field>
|
||
<field name="addRow" w="1in" h="0.3in"><ui><button/></ui><caption><value><text>Add row</text></value></caption>
|
||
<event activity="click"><script>table._row.addInstance(1)</script></event></field>
|
||
<field name="removeRow" w="1in" h="0.3in"><ui><button/></ui><caption><value><text>Remove row</text></value></caption>
|
||
<event activity="click"><script>if (table._row.count > 1) then table._row.removeInstance(table._row.count - 1) endif</script></event></field>
|
||
<field name="hello" w="1in" h="0.3in"><ui><button/></ui><caption><value><text>Hello</text></value></caption>
|
||
<event activity="click"><script>$host.messageBox(Concat("Hello ", qty))</script></event></field>
|
||
</subform>
|
||
</subform>
|
||
</template>
|
||
</xdp:xdp>
|
||
"##
|
||
.to_string()
|
||
}
|
||
|
||
/// Shaped like a real Designer leave-request form: a positioned page whose unnamed subform holds
|
||
/// an area (`box`) with a top-to-bottom table of positioned rows (`<occur min="3"/>`) and a
|
||
/// read-only total that a FormCalc calculate works out over a fixed range of rows, as such forms
|
||
/// do. `data` is `xfa:data` content as Adobe's viewers write it: areas are not data scopes, so
|
||
/// there is no `box` element.
|
||
pub fn leave_template(data: &str) -> String {
|
||
let datasets = if data.is_empty() {
|
||
String::new()
|
||
} else {
|
||
format!("<xfa:datasets xmlns:xfa=\"http://www.xfa.org/schema/xfa-data/1.0/\"><xfa:data>{data}</xfa:data></xfa:datasets>")
|
||
};
|
||
format!(
|
||
r##"<?xml version="1.0" encoding="UTF-8"?>
|
||
<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="front"><contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/><medium short="8.5in" long="11in"/></pageArea></pageSet>
|
||
<subform name="page" w="7.5in" h="9in">
|
||
<subform w="7.5in" h="8in">
|
||
<area name="box" x="0.25in" y="0.5in">
|
||
<subform name="table" layout="tb" w="5in">
|
||
<subform name="row" w="5in" h="0.3in"><occur min="3" max="-1"/>
|
||
<field name="reason" x="0in" w="3in" h="0.3in"><ui><textEdit/></ui></field>
|
||
<field name="days" x="3in" w="1in" h="0.3in"><ui><numericEdit/></ui></field>
|
||
</subform>
|
||
</subform>
|
||
<field name="carried" y="1.5in" w="2in" h="0.3in"><ui><numericEdit/></ui></field>
|
||
<field name="allowance" y="2in" w="2in" h="0.3in"><ui><numericEdit/></ui><value><float>30</float></value></field>
|
||
<field name="rest" y="2.5in" w="2in" h="0.3in" access="readOnly"><ui><numericEdit/></ui>
|
||
<calculate><script>
|
||
var left = carried + allowance
|
||
for i=0 upto 2 step 1 do
|
||
if (table.row[i].reason == "Regular") then
|
||
left = left - table.row[i].days
|
||
endif
|
||
endfor
|
||
this = left
|
||
</script></calculate></field>
|
||
</area>
|
||
</subform>
|
||
</subform>
|
||
</subform>
|
||
</template>
|
||
{datasets}
|
||
</xdp:xdp>
|
||
"##
|
||
)
|
||
}
|
||
|
||
/// Two date fields shown as `MM/DD/YYYY`: `due` binds its value to the data as `YYYYMMDD`
|
||
/// (`<bind><picture>`), `when` has no data picture, so its data holds ISO dates.
|
||
pub fn bound_date_template(data: &str) -> String {
|
||
let datasets = if data.is_empty() {
|
||
String::new()
|
||
} else {
|
||
format!("<xfa:datasets xmlns:xfa=\"http://www.xfa.org/schema/xfa-data/1.0/\"><xfa:data>{data}</xfa:data></xfa:datasets>")
|
||
};
|
||
format!(
|
||
r##"<?xml version="1.0" encoding="UTF-8"?>
|
||
<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="front"><contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/><medium short="8.5in" long="11in"/></pageArea></pageSet>
|
||
<subform name="page" layout="tb">
|
||
<field name="due" w="2in" h="0.3in"><ui><dateTimeEdit/><picture>date{{MM/DD/YYYY}}</picture></ui><value><date/></value><bind match="once"><picture>date{{YYYYMMDD}}</picture></bind></field>
|
||
<field name="when" w="2in" h="0.3in"><ui><dateTimeEdit/><picture>date{{MM/DD/YYYY}}</picture></ui><value><date/></value></field>
|
||
</subform>
|
||
</subform>
|
||
</template>
|
||
{datasets}
|
||
</xdp:xdp>
|
||
"##
|
||
)
|
||
}
|
||
|
||
/// Date fields shown as `MM/DD/YYYY` whose data pictures differ: two siblings both named
|
||
/// `due`, the first bound `YYYYMMDD` and the second `DDMMYYYY`, and `month`, bound `YYYYMM`
|
||
/// (a year and month, no day).
|
||
pub fn sibling_date_template(data: &str) -> String {
|
||
let datasets = if data.is_empty() {
|
||
String::new()
|
||
} else {
|
||
format!("<xfa:datasets xmlns:xfa=\"http://www.xfa.org/schema/xfa-data/1.0/\"><xfa:data>{data}</xfa:data></xfa:datasets>")
|
||
};
|
||
let field = |name: &str, bind: &str| {
|
||
format!(
|
||
r#"<field name="{name}" w="2in" h="0.3in"><ui><dateTimeEdit/><picture>date{{MM/DD/YYYY}}</picture></ui><value><date/></value><bind match="once"><picture>date{{{bind}}}</picture></bind></field>"#
|
||
)
|
||
};
|
||
format!(
|
||
r##"<?xml version="1.0" encoding="UTF-8"?>
|
||
<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="front"><contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/><medium short="8.5in" long="11in"/></pageArea></pageSet>
|
||
<subform name="page" layout="tb">
|
||
{}
|
||
{}
|
||
{}
|
||
</subform>
|
||
</subform>
|
||
</template>
|
||
{datasets}
|
||
</xdp:xdp>
|
||
"##,
|
||
field("due", "YYYYMMDD"),
|
||
field("due", "DDMMYYYY"),
|
||
field("month", "YYYYMM")
|
||
)
|
||
}
|
||
|
||
/// A 2 × 2 RGBA PNG: red, green / blue, transparent.
|
||
pub fn tiny_png() -> Vec<u8> {
|
||
TINY_PNG.to_vec()
|
||
}
|
||
|
||
const TINY_PNG: &[u8] = b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\x00\x00\x02\x00\x00\x00\x02\x08\x06\x00\x00\x00\x72\xb6\x0d\x24\x00\x00\x00\x13\x49\x44\x41\x54\x78\xda\x63\xf8\xcf\xc0\xf0\x1f\x0c\x81\x34\x88\x60\x00\x00\x3f\xd2\x05\xfb\x7f\xe6\x6a\x2b\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82";
|
||
|
||
/// A 1 × 1 white GIF.
|
||
pub const TINY_GIF: &[u8] = b"GIF89a\x01\x00\x01\x00\x80\x00\x00\xff\xff\xff\x00\x00\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02D\x01\x00;";
|
||
|
||
/// A 2 × 1 BMP, red then blue: 24-bit, or 8-bit with a two-colour palette.
|
||
pub fn tiny_bmp(palette: bool) -> Vec<u8> {
|
||
let (bpp, colours, pixels): (u16, &[u8], &[u8]) =
|
||
if palette { (8, &[0, 0, 255, 0, 255, 0, 0, 0], &[0, 1, 0, 0]) } else { (24, &[], &[0, 0, 255, 255, 0, 0, 0, 0]) };
|
||
let offset = 54 + colours.len() as u32;
|
||
let mut b = b"BM".to_vec();
|
||
b.extend_from_slice(&(offset + pixels.len() as u32).to_le_bytes());
|
||
b.extend_from_slice(&[0; 4]);
|
||
b.extend_from_slice(&offset.to_le_bytes());
|
||
for v in [40u32, 2, 1] {
|
||
b.extend_from_slice(&v.to_le_bytes());
|
||
}
|
||
b.extend_from_slice(&1u16.to_le_bytes());
|
||
b.extend_from_slice(&bpp.to_le_bytes());
|
||
for v in [0u32, pixels.len() as u32, 0, 0, (colours.len() / 4) as u32, 0] {
|
||
b.extend_from_slice(&v.to_le_bytes());
|
||
}
|
||
b.extend_from_slice(colours);
|
||
b.extend_from_slice(pixels);
|
||
b
|
||
}
|
||
|
||
/// A 1 × 1 little-endian RGB TIFF of colour `#123456`, uncompressed or LZW.
|
||
pub fn tiny_tiff(lzw: bool) -> Vec<u8> {
|
||
// Clear, 0x12, 0x34, 0x56, end of information, as 9-bit codes, most significant bit first.
|
||
let strip: &[u8] = if lzw { &[0x80, 0x04, 0x86, 0x85, 0x68, 0x08] } else { &[0x12, 0x34, 0x56] };
|
||
// Header (8), the IFD (2 + 9 entries of 12 + 4), then BitsPerSample's three values (6).
|
||
let (bps_at, strip_at) = (122u32, 128u32);
|
||
let (short, long) = (3u16, 4u16);
|
||
let mut b = b"II*\0".to_vec();
|
||
b.extend_from_slice(&8u32.to_le_bytes());
|
||
b.extend_from_slice(&9u16.to_le_bytes());
|
||
for (tag, ty, count, value) in [
|
||
(256u16, short, 1u32, 1u32),
|
||
(257, short, 1, 1),
|
||
(258, short, 3, bps_at),
|
||
(259, short, 1, if lzw { 5 } else { 1 }),
|
||
(262, short, 1, 2),
|
||
(273, long, 1, strip_at),
|
||
(277, short, 1, 3),
|
||
(278, short, 1, 1),
|
||
(279, long, 1, strip.len() as u32),
|
||
] {
|
||
b.extend_from_slice(&tag.to_le_bytes());
|
||
b.extend_from_slice(&ty.to_le_bytes());
|
||
b.extend_from_slice(&count.to_le_bytes());
|
||
b.extend_from_slice(&value.to_le_bytes());
|
||
}
|
||
b.extend_from_slice(&0u32.to_le_bytes());
|
||
for _ in 0..3 {
|
||
b.extend_from_slice(&8u16.to_le_bytes());
|
||
}
|
||
b.extend_from_slice(strip);
|
||
b
|
||
}
|
||
|
||
/// A form with the other field kinds: `country`, a drop-down whose shown names save as codes
|
||
/// (`Canada`/`CA`, `France`/`FR`, `Japan`/`JP`; default `FR`); `langs`, a multi-select list
|
||
/// box (`English`, `French`, `Spanish`); `other`, an editable drop-down (`A`, `B`); `pin`, a
|
||
/// password field; `sign`, a signature field; `photo`, an image field whose template picture
|
||
/// is [`tiny_png`]; `code`, a barcode field with value `12345`; `logo`, a draw showing
|
||
/// [`TINY_GIF`]; `summary`, calculated (JavaScript) from `country`. `data` is the children of
|
||
/// `xfa:data` (empty for none).
|
||
pub fn fields_template(data: &str) -> String {
|
||
use base64::Engine;
|
||
let png = base64::engine::general_purpose::STANDARD.encode(tiny_png());
|
||
let gif = base64::engine::general_purpose::STANDARD.encode(TINY_GIF);
|
||
let datasets = if data.is_empty() {
|
||
String::new()
|
||
} else {
|
||
format!("<xfa:datasets xmlns:xfa=\"http://www.xfa.org/schema/xfa-data/1.0/\"><xfa:data>{data}</xfa:data></xfa:datasets>")
|
||
};
|
||
format!(
|
||
r##"<?xml version="1.0" encoding="UTF-8"?>
|
||
<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="front"><contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/><medium short="8.5in" long="11in"/></pageArea></pageSet>
|
||
<subform name="page1" layout="tb">
|
||
<field name="country" w="3in" h="0.3in"><ui><choiceList/></ui><caption placement="left" reserve="1in"><value><text>Country</text></value></caption>
|
||
<items><text>Canada</text><text>France</text><text>Japan</text></items>
|
||
<items save="1" presence="hidden"><text>CA</text><text>FR</text><text>JP</text></items>
|
||
<value><text>FR</text></value></field>
|
||
<field name="langs" w="3in" h="0.8in"><ui><choiceList open="multiSelect"/></ui><caption placement="left" reserve="1in"><value><text>Languages</text></value></caption>
|
||
<items><text>English</text><text>French</text><text>Spanish</text></items></field>
|
||
<field name="other" w="3in" h="0.3in"><ui><choiceList open="onEntry" textEntry="1"/></ui><items><text>A</text><text>B</text></items></field>
|
||
<field name="pin" w="2in" h="0.3in"><ui><passwordEdit/></ui><caption placement="left" reserve="0.5in"><value><text>PIN</text></value></caption></field>
|
||
<field name="sign" w="3in" h="0.6in"><ui><signature/></ui><caption placement="top" reserve="0.2in"><value><text>Sign here</text></value></caption><border><edge/></border></field>
|
||
<field name="photo" w="1in" h="1in"><ui><imageEdit/></ui><value><image contentType="image/png">{png}</image></value></field>
|
||
<field name="code" w="2in" h="0.5in"><ui><barcode type="code128A"/></ui><value><text>12345</text></value><border><edge/></border></field>
|
||
<draw name="logo" w="0.5in" h="0.5in"><value><image contentType="image/gif">{gif}</image></value></draw>
|
||
<field name="summary" w="3in" h="0.3in" access="readOnly"><ui><textEdit/></ui>
|
||
<calculate><script contentType="application/x-javascript">"Country: " + (country.rawValue || "none")</script></calculate></field>
|
||
</subform>
|
||
</subform>
|
||
</template>
|
||
{datasets}
|
||
</xdp:xdp>
|
||
"##
|
||
)
|
||
}
|
||
|
||
/// A form of `sections` sections of `rows` rows, each row with a calculated total (half in
|
||
/// JavaScript, half in FormCalc) and a section sum over its rows: 2 × sections × rows + sections
|
||
/// calculate scripts, every one reading values the earlier ones settled.
|
||
pub fn many_calculations_template(sections: usize, rows: usize) -> String {
|
||
let mut body = String::new();
|
||
for sct in 0..sections {
|
||
body.push_str(&format!("<subform name=\"s{sct}\" layout=\"tb\"><subform name=\"table\" layout=\"table\" columnWidths=\"1in 1in 1in\">"));
|
||
for r in 0..rows {
|
||
let calc = if (sct + r) % 2 == 0 {
|
||
"<script contentType=\"application/x-javascript\">qty.rawValue * price.rawValue</script>"
|
||
} else {
|
||
"<script>qty * price</script>"
|
||
};
|
||
body.push_str(&format!(
|
||
"<subform name=\"r{r}\" layout=\"row\"><field name=\"qty\" h=\"0.2in\"><ui><numericEdit/></ui><value><integer>{}</integer></value></field><field name=\"price\" h=\"0.2in\"><ui><numericEdit/></ui><value><integer>2</integer></value></field><field name=\"total\" h=\"0.2in\"><ui><numericEdit/></ui><calculate>{calc}</calculate></field></subform>",
|
||
r + 1
|
||
));
|
||
}
|
||
body.push_str("</subform>");
|
||
let terms: Vec<String> = (0..rows).map(|r| format!("table.r{r}.total")).collect();
|
||
body.push_str(&format!(
|
||
"<field name=\"sum\" w=\"2in\" h=\"0.2in\"><ui><numericEdit/></ui><calculate><script>Sum({})</script></calculate></field></subform>",
|
||
terms.join(", ")
|
||
));
|
||
}
|
||
format!(
|
||
r##"<?xml version="1.0" encoding="UTF-8"?>
|
||
<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="front"><occur max="-1"/><contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/><medium short="8.5in" long="11in"/></pageArea></pageSet>
|
||
{body}
|
||
</subform>
|
||
</template>
|
||
</xdp:xdp>
|
||
"##
|
||
)
|
||
}
|
||
|
||
/// Shaped like a leave form's row of dates: `from`, a date field typed and shown as `YYYYMMDD`
|
||
/// (its data keeps ISO dates), `days`, an invisible `minus1` one less than `days`, and `to`,
|
||
/// which a calculation works out as `from` plus `minus1` days. As such forms do, it checks that
|
||
/// `from`'s raw value is a whole ISO date, then does its date arithmetic on `formattedValue`.
|
||
pub fn date_row_template() -> String {
|
||
r##"<?xml version="1.0" encoding="UTF-8"?>
|
||
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
|
||
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
|
||
<subform name="form1" layout="tb">
|
||
<pageSet><pageArea name="front"><contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/><medium short="8.5in" long="11in"/></pageArea></pageSet>
|
||
<subform name="page" layout="tb">
|
||
<field name="from" w="1.5in" h="0.3in"><ui><dateTimeEdit/><picture>date{YYYYMMDD}</picture></ui><value><date/></value><format><picture>date{YYYYMMDD}</picture></format></field>
|
||
<field name="days" w="1in" h="0.3in"><ui><numericEdit/></ui></field>
|
||
<field name="minus1" w="1in" h="0.3in" presence="invisible" access="readOnly"><ui><numericEdit/></ui><calculate><script contentType="application/x-javascript">this.rawValue = days.rawValue - 1;</script></calculate></field>
|
||
<field name="to" w="1.5in" h="0.3in" access="readOnly"><ui><textEdit/></ui><calculate><script contentType="application/x-javascript">
|
||
if (form1.page.from.access == "open") {
|
||
if (form1.page.from.rawValue != null && form1.page.from.rawValue != "" && String(form1.page.from.rawValue).length == 10) {
|
||
form1.page.to.rawValue = AddDays(form1.page.from, form1.page.minus1.rawValue);
|
||
} else {
|
||
form1.page.to.rawValue = "";
|
||
}
|
||
}
|
||
function AddDays(dateObj, daysOffset) {
|
||
var shown = dateObj.formattedValue;
|
||
var date = new Date(shown.substr(0, 4), shown.substr(4, 2) - 1, shown.substr(6, 2), 0, 0, 0, 0);
|
||
date.setMilliseconds(daysOffset * 86400000);
|
||
var month = date.getMonth() + 1;
|
||
if (month < 10) { month = "0" + month; }
|
||
var day = date.getDate();
|
||
if (day < 10) { day = "0" + day; }
|
||
return String(date.getFullYear()) + String(month) + String(day);
|
||
}
|
||
</script></calculate></field>
|
||
</subform>
|
||
</subform>
|
||
</template>
|
||
</xdp:xdp>
|
||
"##
|
||
.to_string()
|
||
}
|
||
|
||
/// A PDF shell around `xdp`: one placeholder page, `/NeedsRendering true`, no fields.
|
||
pub fn shell(xdp: &str) -> Vec<u8> {
|
||
let objs: Vec<Vec<u8>> = vec![
|
||
b"<< /Type /Catalog /Pages 2 0 R /NeedsRendering true /AcroForm << /XFA 5 0 R /Fields [] >> >>".to_vec(),
|
||
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
|
||
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R >>".to_vec(),
|
||
b"<< /Length 44 >>\nstream\nBT /F1 12 Tf 72 700 Td (Please wait...) Tj ET\nendstream".to_vec(),
|
||
format!("<< /Length {} >>\nstream\n{xdp}\nendstream", xdp.len()).into_bytes(),
|
||
];
|
||
assemble(objs)
|
||
}
|
||
|
||
/// A dynamic form with two signature boxes of which `sign` has been signed: the file holds the
|
||
/// field a signing application leaves (named by its SOM path, its value a signature dictionary)
|
||
/// and the picture drawn in the box, a solid blue rectangle.
|
||
pub fn signed_shell() -> Vec<u8> {
|
||
let xdp = r##"<?xml version="1.0" encoding="UTF-8"?>
|
||
<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="page"><contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/><medium short="8.5in" long="11in"/></pageArea></pageSet>
|
||
<subform name="body" layout="tb" w="7.5in">
|
||
<field name="sign" w="3in" h="1in"><ui><signature/></ui></field>
|
||
<field name="other" w="3in" h="1in"><ui><signature/></ui></field>
|
||
</subform>
|
||
</subform>
|
||
</template>
|
||
</xdp:xdp>
|
||
"##;
|
||
let picture = "0 0 1 rg 0 0 100 40 re f";
|
||
let objs: Vec<Vec<u8>> = vec![
|
||
b"<< /Type /Catalog /Pages 2 0 R /NeedsRendering true /AcroForm << /XFA 5 0 R /Fields [6 0 R] /SigFlags 3 >> >>".to_vec(),
|
||
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
|
||
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R >>".to_vec(),
|
||
b"<< /Length 44 >>\nstream\nBT /F1 12 Tf 72 700 Td (Please wait...) Tj ET\nendstream".to_vec(),
|
||
format!("<< /Length {} >>\nstream\n{xdp}\nendstream", xdp.len()).into_bytes(),
|
||
b"<< /T (form[0]) /Kids [7 0 R] >>".to_vec(),
|
||
b"<< /T (body[0]) /Parent 6 0 R /Kids [8 0 R] >>".to_vec(),
|
||
b"<< /T (sign[0]) /Parent 7 0 R /FT /Sig /V 9 0 R /Type /Annot /Subtype /Widget /Rect [0 0 0 0] /AP << /N 10 0 R >> >>".to_vec(),
|
||
b"<< /Type /Sig /Name (A Signer) >>".to_vec(),
|
||
format!("<< /Type /XObject /Subtype /Form /BBox [0 0 100 40] /Length {} >>\nstream\n{picture}\nendstream", picture.len()).into_bytes(),
|
||
];
|
||
assemble(objs)
|
||
}
|
||
|
||
/// A check box `kind` whose change script, once it is checked, opens the protected check box
|
||
/// `leave` and shows the invisible field `details`, then fails on an object the form doesn't
|
||
/// have (as scripts written for an earlier version of a form do).
|
||
pub fn partly_failing_script_template() -> String {
|
||
r##"<?xml version="1.0" encoding="UTF-8"?>
|
||
<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="front"><contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/><medium short="8.5in" long="11in"/></pageArea></pageSet>
|
||
<subform name="page" layout="tb">
|
||
<field name="kind" w="0.3in" h="0.3in"><ui><checkButton/></ui><items><integer>1</integer><integer>0</integer></items><value><integer>0</integer></value>
|
||
<event activity="change"><script contentType="application/x-javascript">
|
||
if (this.rawValue == "1") {
|
||
this.resolveNode("leave").access = "open";
|
||
this.resolveNode("details").presence = "visible";
|
||
this.resolveNode("noSuchField").rawValue = "0";
|
||
}
|
||
</script></event>
|
||
</field>
|
||
<field name="leave" w="0.3in" h="0.3in" access="protected"><ui><checkButton/></ui><items><integer>1</integer><integer>0</integer></items><value><integer>0</integer></value></field>
|
||
<field name="details" w="2in" h="0.3in" presence="invisible"><ui><textEdit/></ui></field>
|
||
</subform>
|
||
</subform>
|
||
</template>
|
||
</xdp:xdp>
|
||
"##
|
||
.to_string()
|
||
}
|