imprimatur 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +8 -0
- data/Cargo.lock +835 -0
- data/Cargo.toml +44 -0
- data/LICENSE.txt +29 -0
- data/README.md +314 -0
- data/ext/imprimatur_native/cli/Cargo.toml +30 -0
- data/ext/imprimatur_native/cli/src/app.rs +183 -0
- data/ext/imprimatur_native/cli/src/cli/completion.rs +26 -0
- data/ext/imprimatur_native/cli/src/cli/grep.rs +359 -0
- data/ext/imprimatur_native/cli/src/cli/images.rs +137 -0
- data/ext/imprimatur_native/cli/src/cli/limits.rs +49 -0
- data/ext/imprimatur_native/cli/src/cli/markdown.rs +128 -0
- data/ext/imprimatur_native/cli/src/cli.rs +71 -0
- data/ext/imprimatur_native/cli/src/main.rs +15 -0
- data/ext/imprimatur_native/core/Cargo.toml +33 -0
- data/ext/imprimatur_native/core/LICENSE +29 -0
- data/ext/imprimatur_native/core/README.md +105 -0
- data/ext/imprimatur_native/core/src/doc/paragraph.rs +112 -0
- data/ext/imprimatur_native/core/src/doc/table.rs +41 -0
- data/ext/imprimatur_native/core/src/doc.rs +210 -0
- data/ext/imprimatur_native/core/src/docx.rs +343 -0
- data/ext/imprimatur_native/core/src/error.rs +113 -0
- data/ext/imprimatur_native/core/src/lib.rs +38 -0
- data/ext/imprimatur_native/core/src/opc.rs +51 -0
- data/ext/imprimatur_native/core/src/pdf/font.rs +96 -0
- data/ext/imprimatur_native/core/src/pdf/layout/wrap.rs +161 -0
- data/ext/imprimatur_native/core/src/pdf/layout.rs +280 -0
- data/ext/imprimatur_native/core/src/pdf/metrics.rs +139 -0
- data/ext/imprimatur_native/core/src/pdf/objects.rs +141 -0
- data/ext/imprimatur_native/core/src/pdf.rs +84 -0
- data/ext/imprimatur_native/core/src/read/budget.rs +140 -0
- data/ext/imprimatur_native/core/src/read/content.rs +266 -0
- data/ext/imprimatur_native/core/src/read/docx/styles.rs +118 -0
- data/ext/imprimatur_native/core/src/read/docx.rs +336 -0
- data/ext/imprimatur_native/core/src/read/image/dimensions.rs +215 -0
- data/ext/imprimatur_native/core/src/read/image.rs +233 -0
- data/ext/imprimatur_native/core/src/read/inflate.rs +44 -0
- data/ext/imprimatur_native/core/src/read/json.rs +172 -0
- data/ext/imprimatur_native/core/src/read/markdown.rs +254 -0
- data/ext/imprimatur_native/core/src/read/package.rs +191 -0
- data/ext/imprimatur_native/core/src/read/pdf/cmap.rs +216 -0
- data/ext/imprimatur_native/core/src/read/pdf/content/geometry.rs +79 -0
- data/ext/imprimatur_native/core/src/read/pdf/content/operands.rs +156 -0
- data/ext/imprimatur_native/core/src/read/pdf/content.rs +439 -0
- data/ext/imprimatur_native/core/src/read/pdf/crypt.rs +256 -0
- data/ext/imprimatur_native/core/src/read/pdf/document.rs +420 -0
- data/ext/imprimatur_native/core/src/read/pdf/filter.rs +258 -0
- data/ext/imprimatur_native/core/src/read/pdf/font.rs +315 -0
- data/ext/imprimatur_native/core/src/read/pdf/glyphs.rs +179 -0
- data/ext/imprimatur_native/core/src/read/pdf/glyphs.txt +6298 -0
- data/ext/imprimatur_native/core/src/read/pdf/images.rs +365 -0
- data/ext/imprimatur_native/core/src/read/pdf/layout.rs +451 -0
- data/ext/imprimatur_native/core/src/read/pdf/lexer.rs +255 -0
- data/ext/imprimatur_native/core/src/read/pdf/object/name.rs +44 -0
- data/ext/imprimatur_native/core/src/read/pdf/object.rs +261 -0
- data/ext/imprimatur_native/core/src/read/pdf/xref.rs +346 -0
- data/ext/imprimatur_native/core/src/read/pdf.rs +53 -0
- data/ext/imprimatur_native/core/src/read/span.rs +22 -0
- data/ext/imprimatur_native/core/src/read/xlsx/numbers.rs +155 -0
- data/ext/imprimatur_native/core/src/read/xlsx.rs +340 -0
- data/ext/imprimatur_native/core/src/read.rs +145 -0
- data/ext/imprimatur_native/core/src/style.rs +82 -0
- data/ext/imprimatur_native/core/src/test_support.rs +163 -0
- data/ext/imprimatur_native/core/src/xlsx/cell.rs +112 -0
- data/ext/imprimatur_native/core/src/xlsx/comment.rs +256 -0
- data/ext/imprimatur_native/core/src/xlsx/date.rs +173 -0
- data/ext/imprimatur_native/core/src/xlsx/format.rs +226 -0
- data/ext/imprimatur_native/core/src/xlsx/package.rs +268 -0
- data/ext/imprimatur_native/core/src/xlsx/range.rs +124 -0
- data/ext/imprimatur_native/core/src/xlsx/styles.rs +240 -0
- data/ext/imprimatur_native/core/src/xlsx/text.rs +122 -0
- data/ext/imprimatur_native/core/src/xlsx/validation.rs +258 -0
- data/ext/imprimatur_native/core/src/xlsx/worksheet.rs +350 -0
- data/ext/imprimatur_native/core/src/xlsx.rs +120 -0
- data/ext/imprimatur_native/core/src/xml/reader.rs +238 -0
- data/ext/imprimatur_native/core/src/xml.rs +95 -0
- data/ext/imprimatur_native/core/src/zip/reader.rs +144 -0
- data/ext/imprimatur_native/core/src/zip.rs +161 -0
- data/ext/imprimatur_native/extconf.rb +45 -0
- data/ext/imprimatur_native/ffi/Cargo.toml +21 -0
- data/ext/imprimatur_native/ffi/src/convert.rs +132 -0
- data/ext/imprimatur_native/ffi/src/document.rs +138 -0
- data/ext/imprimatur_native/ffi/src/lib.rs +301 -0
- data/ext/imprimatur_native/ffi/src/read.rs +182 -0
- data/ext/imprimatur_native/ffi/src/style.rs +85 -0
- data/ext/imprimatur_native/ffi/src/validation.rs +56 -0
- data/lib/imprimatur/document.rb +127 -0
- data/lib/imprimatur/error.rb +22 -0
- data/lib/imprimatur/formula.rb +13 -0
- data/lib/imprimatur/native.rb +17 -0
- data/lib/imprimatur/reader.rb +59 -0
- data/lib/imprimatur/style.rb +31 -0
- data/lib/imprimatur/validation.rb +35 -0
- data/lib/imprimatur/version.rb +5 -0
- data/lib/imprimatur/xlsx.rb +183 -0
- data/lib/imprimatur.rb +111 -0
- metadata +157 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
//! `WordprocessingML` (`.docx`), per ECMA-376 Part 1: the document model as
|
|
2
|
+
//! Word paragraphs, runs and tables, with real Heading 1-3 styles so Word's
|
|
3
|
+
//! navigation pane and table of contents see them.
|
|
4
|
+
|
|
5
|
+
#[cfg(test)]
|
|
6
|
+
mod tests;
|
|
7
|
+
|
|
8
|
+
use std::io::Write;
|
|
9
|
+
|
|
10
|
+
use crate::Result;
|
|
11
|
+
use crate::doc::{Block, Document, Paragraph, Run, Table};
|
|
12
|
+
use crate::opc;
|
|
13
|
+
use crate::style::{Align, Color, TextDirection};
|
|
14
|
+
use crate::xml::XmlWriter;
|
|
15
|
+
use crate::zip::ZipWriter;
|
|
16
|
+
|
|
17
|
+
const W_NS: &str = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
18
|
+
const WORD_TYPE: &str = "application/vnd.openxmlformats-officedocument.wordprocessingml";
|
|
19
|
+
/// One inch, Word's default margin, in twips.
|
|
20
|
+
const MARGIN: u32 = 1_440;
|
|
21
|
+
/// A header row's shading.
|
|
22
|
+
const HEADER_FILL: &str = "D9E2F3";
|
|
23
|
+
|
|
24
|
+
pub fn write<W: Write>(document: &Document, out: W) -> Result<W> {
|
|
25
|
+
let footer = document.has_page_numbers();
|
|
26
|
+
let mut parts = vec![("styles", "styles.xml")];
|
|
27
|
+
if footer {
|
|
28
|
+
parts.push(("footer", "footer1.xml"));
|
|
29
|
+
}
|
|
30
|
+
let mut zip = ZipWriter::new(out);
|
|
31
|
+
zip.add("[Content_Types].xml", content_types(footer).as_bytes())?;
|
|
32
|
+
let package = opc::relationships(&[("officeDocument", "word/document.xml")]);
|
|
33
|
+
zip.add("_rels/.rels", package.as_bytes())?;
|
|
34
|
+
zip.add("word/document.xml", body(document).as_bytes())?;
|
|
35
|
+
zip.add("word/_rels/document.xml.rels", opc::relationships(&parts).as_bytes())?;
|
|
36
|
+
zip.add("word/styles.xml", styles().as_bytes())?;
|
|
37
|
+
if footer {
|
|
38
|
+
zip.add("word/footer1.xml", page_number_footer().as_bytes())?;
|
|
39
|
+
}
|
|
40
|
+
zip.finish()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// One override per part: the document, its styles and the footer.
|
|
44
|
+
fn content_types(footer: bool) -> String {
|
|
45
|
+
let mut parts = vec![("/word/document.xml", "document.main"), ("/word/styles.xml", "styles")];
|
|
46
|
+
if footer {
|
|
47
|
+
parts.push(("/word/footer1.xml", "footer"));
|
|
48
|
+
}
|
|
49
|
+
let overrides: Vec<(&str, String)> =
|
|
50
|
+
parts.into_iter().map(|(part, kind)| (part, format!("{WORD_TYPE}.{kind}+xml"))).collect();
|
|
51
|
+
opc::content_types(&[], &overrides)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// `word/footer1.xml`: a centered `PAGE / NUMPAGES`, which Word computes.
|
|
55
|
+
fn page_number_footer() -> String {
|
|
56
|
+
let mut xml = XmlWriter::new();
|
|
57
|
+
xml.open("w:ftr", &[("xmlns:w", W_NS)]);
|
|
58
|
+
xml.open("w:p", &[]);
|
|
59
|
+
xml.open("w:pPr", &[]);
|
|
60
|
+
xml.empty("w:jc", &[("w:val", "center")]);
|
|
61
|
+
xml.close("w:pPr");
|
|
62
|
+
let field = |xml: &mut XmlWriter, instruction: &str| {
|
|
63
|
+
for kind in ["begin", "instruction", "separate", "placeholder", "end"] {
|
|
64
|
+
xml.open("w:r", &[]);
|
|
65
|
+
match kind {
|
|
66
|
+
"instruction" => {
|
|
67
|
+
xml.element("w:instrText", &[("xml:space", "preserve")], instruction);
|
|
68
|
+
}
|
|
69
|
+
"placeholder" => xml.element("w:t", &[], "1"),
|
|
70
|
+
_ => xml.empty("w:fldChar", &[("w:fldCharType", kind)]),
|
|
71
|
+
}
|
|
72
|
+
xml.close("w:r");
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
field(&mut xml, " PAGE ");
|
|
76
|
+
xml.open("w:r", &[]);
|
|
77
|
+
xml.element("w:t", &[("xml:space", "preserve")], " / ");
|
|
78
|
+
xml.close("w:r");
|
|
79
|
+
field(&mut xml, " NUMPAGES ");
|
|
80
|
+
xml.close("w:p");
|
|
81
|
+
xml.close("w:ftr");
|
|
82
|
+
xml.finish()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
fn body(document: &Document) -> String {
|
|
86
|
+
let mut xml = XmlWriter::new();
|
|
87
|
+
xml.open("w:document", &[("xmlns:w", W_NS), ("xmlns:r", opc::RELATIONSHIPS)]);
|
|
88
|
+
xml.open("w:body", &[]);
|
|
89
|
+
for block in document.blocks() {
|
|
90
|
+
match block {
|
|
91
|
+
Block::Heading { level, text } => {
|
|
92
|
+
let style = format!("Heading{level}");
|
|
93
|
+
simple_paragraph(&mut xml, Some(&style), text, Look::default());
|
|
94
|
+
}
|
|
95
|
+
Block::Paragraph(content) => paragraph(&mut xml, content),
|
|
96
|
+
Block::Table(content) => table(&mut xml, content, document),
|
|
97
|
+
Block::PageBreak => {
|
|
98
|
+
xml.open("w:p", &[]);
|
|
99
|
+
xml.open("w:r", &[]);
|
|
100
|
+
xml.empty("w:br", &[("w:type", "page")]);
|
|
101
|
+
xml.close("w:r");
|
|
102
|
+
xml.close("w:p");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Word ends a body that ends with a table with an empty paragraph anyway.
|
|
107
|
+
if matches!(document.blocks().last(), Some(Block::Table(_))) {
|
|
108
|
+
xml.empty("w:p", &[]);
|
|
109
|
+
}
|
|
110
|
+
section(&mut xml, document);
|
|
111
|
+
xml.close("w:body");
|
|
112
|
+
xml.close("w:document");
|
|
113
|
+
xml.finish()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
fn paragraph(xml: &mut XmlWriter, content: &Paragraph) {
|
|
117
|
+
let rtl = content.direction == Some(TextDirection::RightToLeft);
|
|
118
|
+
open_paragraph(xml, None, rtl, content.align);
|
|
119
|
+
for item in &content.runs {
|
|
120
|
+
run(xml, &item.text, Look::of(item), rtl);
|
|
121
|
+
}
|
|
122
|
+
xml.close("w:p");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// A one-run paragraph straight from borrowed text: headings and table cells.
|
|
126
|
+
fn simple_paragraph(xml: &mut XmlWriter, style: Option<&str>, text: &str, look: Look) {
|
|
127
|
+
open_paragraph(xml, style, false, None);
|
|
128
|
+
run(xml, text, look, false);
|
|
129
|
+
xml.close("w:p");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/// `<w:p>` and its properties; the caller writes the runs and closes it.
|
|
133
|
+
fn open_paragraph(xml: &mut XmlWriter, style: Option<&str>, rtl: bool, align: Option<Align>) {
|
|
134
|
+
xml.open("w:p", &[]);
|
|
135
|
+
if style.is_none() && !rtl && align.is_none() {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
xml.open("w:pPr", &[]);
|
|
139
|
+
if let Some(style) = style {
|
|
140
|
+
xml.empty("w:pStyle", &[("w:val", style)]);
|
|
141
|
+
}
|
|
142
|
+
if rtl {
|
|
143
|
+
xml.empty("w:bidi", &[]);
|
|
144
|
+
}
|
|
145
|
+
if let Some(align) = align {
|
|
146
|
+
let value = match align {
|
|
147
|
+
Align::Left => "left",
|
|
148
|
+
Align::Center => "center",
|
|
149
|
+
Align::Right => "right",
|
|
150
|
+
};
|
|
151
|
+
xml.empty("w:jc", &[("w:val", value)]);
|
|
152
|
+
}
|
|
153
|
+
xml.close("w:pPr");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// How a run looks, apart from its text.
|
|
157
|
+
#[derive(Clone, Copy, Default)]
|
|
158
|
+
struct Look {
|
|
159
|
+
bold: bool,
|
|
160
|
+
italic: bool,
|
|
161
|
+
underline: bool,
|
|
162
|
+
color: Option<Color>,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
impl Look {
|
|
166
|
+
const fn of(run: &Run) -> Self {
|
|
167
|
+
Self { bold: run.bold, italic: run.italic, underline: run.underline, color: run.color }
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/// One `<w:r>` per piece of text, with `<w:br/>` for `\n` and `<w:tab/>` for `\t`.
|
|
172
|
+
fn run(xml: &mut XmlWriter, text: &str, look: Look, rtl: bool) {
|
|
173
|
+
xml.open("w:r", &[]);
|
|
174
|
+
if look.bold || look.italic || look.underline || look.color.is_some() || rtl {
|
|
175
|
+
xml.open("w:rPr", &[]);
|
|
176
|
+
if look.bold {
|
|
177
|
+
xml.empty("w:b", &[]);
|
|
178
|
+
}
|
|
179
|
+
if look.italic {
|
|
180
|
+
xml.empty("w:i", &[]);
|
|
181
|
+
}
|
|
182
|
+
if look.underline {
|
|
183
|
+
xml.empty("w:u", &[("w:val", "single")]);
|
|
184
|
+
}
|
|
185
|
+
if let Some(color) = look.color {
|
|
186
|
+
let hex = color.argb();
|
|
187
|
+
xml.empty("w:color", &[("w:val", &hex[2..])]);
|
|
188
|
+
}
|
|
189
|
+
if rtl {
|
|
190
|
+
xml.empty("w:rtl", &[]);
|
|
191
|
+
}
|
|
192
|
+
xml.close("w:rPr");
|
|
193
|
+
}
|
|
194
|
+
for (index, line) in text.split('\n').enumerate() {
|
|
195
|
+
if index > 0 {
|
|
196
|
+
xml.empty("w:br", &[]);
|
|
197
|
+
}
|
|
198
|
+
for (index, piece) in line.split('\t').enumerate() {
|
|
199
|
+
if index > 0 {
|
|
200
|
+
xml.empty("w:tab", &[]);
|
|
201
|
+
}
|
|
202
|
+
if !piece.is_empty() {
|
|
203
|
+
xml.element("w:t", &[("xml:space", "preserve")], piece);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
xml.close("w:r");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
fn table(xml: &mut XmlWriter, content: &Table, document: &Document) {
|
|
211
|
+
let columns = content.columns();
|
|
212
|
+
let (page_width, _) = document.page_size().twips();
|
|
213
|
+
let usable = page_width - 2 * MARGIN;
|
|
214
|
+
// Never 0: a table has at least one column.
|
|
215
|
+
let column_width = (usable / u32::try_from(columns).unwrap_or(1).max(1)).to_string();
|
|
216
|
+
|
|
217
|
+
xml.open("w:tbl", &[]);
|
|
218
|
+
xml.open("w:tblPr", &[]);
|
|
219
|
+
xml.empty("w:tblStyle", &[("w:val", "TableGrid")]);
|
|
220
|
+
xml.empty("w:tblW", &[("w:w", "5000"), ("w:type", "pct")]);
|
|
221
|
+
xml.close("w:tblPr");
|
|
222
|
+
xml.open("w:tblGrid", &[]);
|
|
223
|
+
for _ in 0..columns {
|
|
224
|
+
xml.empty("w:gridCol", &[("w:w", &column_width)]);
|
|
225
|
+
}
|
|
226
|
+
xml.close("w:tblGrid");
|
|
227
|
+
for (index, row) in content.rows().iter().enumerate() {
|
|
228
|
+
let header = index == 0 && content.has_header();
|
|
229
|
+
xml.open("w:tr", &[]);
|
|
230
|
+
if header {
|
|
231
|
+
xml.open("w:trPr", &[]);
|
|
232
|
+
xml.empty("w:tblHeader", &[]);
|
|
233
|
+
xml.close("w:trPr");
|
|
234
|
+
}
|
|
235
|
+
for column in 0..columns {
|
|
236
|
+
xml.open("w:tc", &[]);
|
|
237
|
+
xml.open("w:tcPr", &[]);
|
|
238
|
+
xml.empty("w:tcW", &[("w:w", &column_width), ("w:type", "dxa")]);
|
|
239
|
+
if header {
|
|
240
|
+
xml.empty(
|
|
241
|
+
"w:shd",
|
|
242
|
+
&[("w:val", "clear"), ("w:color", "auto"), ("w:fill", HEADER_FILL)],
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
xml.close("w:tcPr");
|
|
246
|
+
let text = row.get(column).map_or("", String::as_str);
|
|
247
|
+
simple_paragraph(xml, None, text, Look { bold: header, ..Look::default() });
|
|
248
|
+
xml.close("w:tc");
|
|
249
|
+
}
|
|
250
|
+
xml.close("w:tr");
|
|
251
|
+
}
|
|
252
|
+
xml.close("w:tbl");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
fn section(xml: &mut XmlWriter, document: &Document) {
|
|
256
|
+
let (width, height) = document.page_size().twips();
|
|
257
|
+
let (width, height, margin) = (width.to_string(), height.to_string(), MARGIN.to_string());
|
|
258
|
+
xml.open("w:sectPr", &[]);
|
|
259
|
+
if document.has_page_numbers() {
|
|
260
|
+
// rId2: the footer, after the styles (see `write`).
|
|
261
|
+
xml.empty("w:footerReference", &[("w:type", "default"), ("r:id", "rId2")]);
|
|
262
|
+
}
|
|
263
|
+
xml.empty("w:pgSz", &[("w:w", &width), ("w:h", &height)]);
|
|
264
|
+
let margins =
|
|
265
|
+
[("w:top", &margin), ("w:right", &margin), ("w:bottom", &margin), ("w:left", &margin)];
|
|
266
|
+
xml.empty("w:pgMar", &margins.map(|(key, value)| (key, value.as_str())));
|
|
267
|
+
xml.close("w:sectPr");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/// Heading 1-3: space before and after (twips), color, size (half-points).
|
|
271
|
+
const HEADINGS: [(&str, &str, Option<&str>, &str); 3] = [
|
|
272
|
+
("360", "120", Some("1F3864"), "40"),
|
|
273
|
+
("240", "80", Some("1F3864"), "30"),
|
|
274
|
+
("200", "60", None, "26"),
|
|
275
|
+
];
|
|
276
|
+
|
|
277
|
+
/// `word/styles.xml`: Normal text, Heading 1-3 and a bordered table style,
|
|
278
|
+
/// with a Calibri 11pt body.
|
|
279
|
+
fn styles() -> String {
|
|
280
|
+
let mut xml = XmlWriter::new();
|
|
281
|
+
xml.open("w:styles", &[("xmlns:w", W_NS)]);
|
|
282
|
+
xml.open("w:docDefaults", &[]);
|
|
283
|
+
xml.open("w:rPrDefault", &[]);
|
|
284
|
+
xml.open("w:rPr", &[]);
|
|
285
|
+
xml.empty("w:rFonts", &[("w:ascii", "Calibri"), ("w:hAnsi", "Calibri"), ("w:cs", "Arial")]);
|
|
286
|
+
xml.empty("w:sz", &[("w:val", "22")]);
|
|
287
|
+
xml.empty("w:szCs", &[("w:val", "22")]);
|
|
288
|
+
xml.close("w:rPr");
|
|
289
|
+
xml.close("w:rPrDefault");
|
|
290
|
+
xml.open("w:pPrDefault", &[]);
|
|
291
|
+
xml.open("w:pPr", &[]);
|
|
292
|
+
xml.empty("w:spacing", &[("w:after", "160"), ("w:line", "259"), ("w:lineRule", "auto")]);
|
|
293
|
+
xml.close("w:pPr");
|
|
294
|
+
xml.close("w:pPrDefault");
|
|
295
|
+
xml.close("w:docDefaults");
|
|
296
|
+
|
|
297
|
+
xml.open("w:style", &[("w:type", "paragraph"), ("w:default", "1"), ("w:styleId", "Normal")]);
|
|
298
|
+
xml.empty("w:name", &[("w:val", "Normal")]);
|
|
299
|
+
xml.empty("w:qFormat", &[]);
|
|
300
|
+
xml.close("w:style");
|
|
301
|
+
for (level, (before, after, color, size)) in (1..).zip(HEADINGS) {
|
|
302
|
+
let (id, name, outline) =
|
|
303
|
+
(format!("Heading{level}"), format!("heading {level}"), level - 1);
|
|
304
|
+
xml.open("w:style", &[("w:type", "paragraph"), ("w:styleId", &id)]);
|
|
305
|
+
xml.empty("w:name", &[("w:val", &name)]);
|
|
306
|
+
xml.empty("w:basedOn", &[("w:val", "Normal")]);
|
|
307
|
+
xml.empty("w:next", &[("w:val", "Normal")]);
|
|
308
|
+
xml.empty("w:qFormat", &[]);
|
|
309
|
+
xml.open("w:pPr", &[]);
|
|
310
|
+
xml.empty("w:keepNext", &[]);
|
|
311
|
+
xml.empty("w:spacing", &[("w:before", before), ("w:after", after)]);
|
|
312
|
+
xml.empty("w:outlineLvl", &[("w:val", &outline.to_string())]);
|
|
313
|
+
xml.close("w:pPr");
|
|
314
|
+
xml.open("w:rPr", &[]);
|
|
315
|
+
xml.empty("w:b", &[]);
|
|
316
|
+
if let Some(color) = color {
|
|
317
|
+
xml.empty("w:color", &[("w:val", color)]);
|
|
318
|
+
}
|
|
319
|
+
xml.empty("w:sz", &[("w:val", size)]);
|
|
320
|
+
xml.empty("w:szCs", &[("w:val", size)]);
|
|
321
|
+
xml.close("w:rPr");
|
|
322
|
+
xml.close("w:style");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
xml.open("w:style", &[("w:type", "table"), ("w:styleId", "TableGrid")]);
|
|
326
|
+
xml.empty("w:name", &[("w:val", "Table Grid")]);
|
|
327
|
+
xml.open("w:tblPr", &[]);
|
|
328
|
+
xml.open("w:tblBorders", &[]);
|
|
329
|
+
for side in ["top", "left", "bottom", "right", "insideH", "insideV"] {
|
|
330
|
+
let line = [("w:val", "single"), ("w:sz", "4"), ("w:space", "0"), ("w:color", "auto")];
|
|
331
|
+
xml.empty(&format!("w:{side}"), &line);
|
|
332
|
+
}
|
|
333
|
+
xml.close("w:tblBorders");
|
|
334
|
+
xml.open("w:tblCellMar", &[]);
|
|
335
|
+
for side in ["w:left", "w:right"] {
|
|
336
|
+
xml.empty(side, &[("w:w", "108"), ("w:type", "dxa")]);
|
|
337
|
+
}
|
|
338
|
+
xml.close("w:tblCellMar");
|
|
339
|
+
xml.close("w:tblPr");
|
|
340
|
+
xml.close("w:style");
|
|
341
|
+
xml.close("w:styles");
|
|
342
|
+
xml.finish()
|
|
343
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
use std::io;
|
|
2
|
+
|
|
3
|
+
/// Everything that can go wrong while building or writing a document.
|
|
4
|
+
#[derive(Debug, thiserror::Error)]
|
|
5
|
+
#[non_exhaustive]
|
|
6
|
+
pub enum Error {
|
|
7
|
+
#[error("invalid sheet name {name:?}: {reason}")]
|
|
8
|
+
InvalidSheetName { name: String, reason: &'static str },
|
|
9
|
+
|
|
10
|
+
#[error("a sheet named {0:?} already exists (sheet names are case-insensitive)")]
|
|
11
|
+
DuplicateSheetName(String),
|
|
12
|
+
|
|
13
|
+
#[error("row {0} is out of range (rows are 0..1048576)")]
|
|
14
|
+
RowOutOfRange(u64),
|
|
15
|
+
|
|
16
|
+
#[error("column {0} is out of range (columns are 0..16384)")]
|
|
17
|
+
ColumnOutOfRange(u64),
|
|
18
|
+
|
|
19
|
+
#[error("text of {0} characters is too long for a cell (the limit is 32767)")]
|
|
20
|
+
TextTooLong(usize),
|
|
21
|
+
|
|
22
|
+
#[error("a formula of {0} characters is too long (the limit is 8192)")]
|
|
23
|
+
FormulaTooLong(usize),
|
|
24
|
+
|
|
25
|
+
#[error("{0} is not a finite number; cells cannot hold NaN or infinity")]
|
|
26
|
+
NonFiniteNumber(f64),
|
|
27
|
+
|
|
28
|
+
#[error("invalid color {0:?}: expected #RRGGBB")]
|
|
29
|
+
InvalidColor(String),
|
|
30
|
+
|
|
31
|
+
#[error(
|
|
32
|
+
"invalid number format {0:?}: it must be 1 to 255 characters, without control characters"
|
|
33
|
+
)]
|
|
34
|
+
InvalidNumberFormat(String),
|
|
35
|
+
|
|
36
|
+
#[error("{0} is outside Excel's dates (1900-01-01 to 9999-12-31)")]
|
|
37
|
+
DateOutOfRange(String),
|
|
38
|
+
|
|
39
|
+
#[error("column width {0} is out of range (0 to 255 characters)")]
|
|
40
|
+
InvalidColumnWidth(f64),
|
|
41
|
+
|
|
42
|
+
#[error("invalid font name {0:?}: it must be 1 to 31 characters, without control characters")]
|
|
43
|
+
InvalidFontName(String),
|
|
44
|
+
|
|
45
|
+
#[error("font size {0} is out of range (1 to 409 points, in steps of 0.5)")]
|
|
46
|
+
InvalidFontSize(f64),
|
|
47
|
+
|
|
48
|
+
#[error("invalid cell range {0:?}: expected a reference such as B2 or B2:D9")]
|
|
49
|
+
InvalidRange(String),
|
|
50
|
+
|
|
51
|
+
#[error("invalid data validation: {0}")]
|
|
52
|
+
InvalidValidation(String),
|
|
53
|
+
|
|
54
|
+
#[error("the data validation on {0} overlaps one already on the sheet")]
|
|
55
|
+
OverlappingValidation(String),
|
|
56
|
+
|
|
57
|
+
#[error("too many distinct formats (Excel's limit is 64000 per workbook)")]
|
|
58
|
+
TooManyFormats,
|
|
59
|
+
|
|
60
|
+
#[error("heading level {0} is out of range (1 to 3)")]
|
|
61
|
+
InvalidHeadingLevel(u8),
|
|
62
|
+
|
|
63
|
+
#[error("text contains the control character U+{0:04X}; only newlines and tabs are allowed")]
|
|
64
|
+
ControlCharacter(u32),
|
|
65
|
+
|
|
66
|
+
#[error("a table needs at least one row and one column")]
|
|
67
|
+
EmptyTable,
|
|
68
|
+
|
|
69
|
+
#[error(
|
|
70
|
+
"{character:?} (U+{code:04X}) cannot be written with the PDF standard font {font}: \
|
|
71
|
+
only Windows-1252 (Latin) text is supported until embedded fonts land",
|
|
72
|
+
code = u32::from(*character)
|
|
73
|
+
)]
|
|
74
|
+
UnsupportedCharacter { character: char, font: &'static str },
|
|
75
|
+
|
|
76
|
+
#[error("a table row needs {0:.0}pt, more than a whole page holds")]
|
|
77
|
+
RowTooTall(f64),
|
|
78
|
+
|
|
79
|
+
#[error("a workbook needs at least one sheet")]
|
|
80
|
+
EmptyWorkbook,
|
|
81
|
+
|
|
82
|
+
#[error("archive too large: {0} (zip64 is not supported yet)")]
|
|
83
|
+
ArchiveTooLarge(&'static str),
|
|
84
|
+
|
|
85
|
+
#[error("cannot read this document: {0}")]
|
|
86
|
+
UnsupportedDocument(String),
|
|
87
|
+
|
|
88
|
+
#[error("malformed {format}: {reason}")]
|
|
89
|
+
Malformed { format: &'static str, reason: String },
|
|
90
|
+
|
|
91
|
+
#[error("the document exceeds the {0} limit")]
|
|
92
|
+
LimitExceeded(&'static str),
|
|
93
|
+
|
|
94
|
+
#[error("reading the document was cancelled")]
|
|
95
|
+
Cancelled,
|
|
96
|
+
|
|
97
|
+
#[error("the PDF has no text layer: its pages are images, which need OCR")]
|
|
98
|
+
NoTextLayer,
|
|
99
|
+
|
|
100
|
+
#[error("the PDF is encrypted with a password")]
|
|
101
|
+
PasswordRequired,
|
|
102
|
+
|
|
103
|
+
#[error(transparent)]
|
|
104
|
+
Io(#[from] io::Error),
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
impl Error {
|
|
108
|
+
pub(crate) fn malformed(format: &'static str, reason: impl Into<String>) -> Self {
|
|
109
|
+
Self::Malformed { format, reason: reason.into() }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//! Document authoring with no third-party format stack: the XML and zip
|
|
2
|
+
//! writers underneath every format are written here, on `miniz_oxide`
|
|
3
|
+
//! (deflate) and `crc32fast`.
|
|
4
|
+
//!
|
|
5
|
+
//! [`xlsx`] writes workbooks; [`doc`] is one document model that renders to
|
|
6
|
+
//! PDF and DOCX.
|
|
7
|
+
//!
|
|
8
|
+
//! ```
|
|
9
|
+
//! use imprimatur::xlsx::{Cell, Workbook};
|
|
10
|
+
//!
|
|
11
|
+
//! let mut book = Workbook::new();
|
|
12
|
+
//! let sheet = book.add_sheet("Sales")?;
|
|
13
|
+
//! sheet.append_row(["Region", "Q1"])?;
|
|
14
|
+
//! sheet.append_row([Cell::from("North"), Cell::from(12.5)])?;
|
|
15
|
+
//! sheet.append_row([Cell::from("Total"), Cell::formula("SUM(B2:B2)")])?;
|
|
16
|
+
//! let bytes = book.to_bytes()?;
|
|
17
|
+
//! assert!(bytes.starts_with(b"PK\x03\x04"));
|
|
18
|
+
//! # Ok::<(), imprimatur::Error>(())
|
|
19
|
+
//! ```
|
|
20
|
+
|
|
21
|
+
pub mod doc;
|
|
22
|
+
mod docx;
|
|
23
|
+
mod error;
|
|
24
|
+
mod opc;
|
|
25
|
+
mod pdf;
|
|
26
|
+
pub mod read;
|
|
27
|
+
pub mod style;
|
|
28
|
+
#[cfg(test)]
|
|
29
|
+
mod test_support;
|
|
30
|
+
pub mod xlsx;
|
|
31
|
+
pub(crate) mod xml;
|
|
32
|
+
pub(crate) mod zip;
|
|
33
|
+
|
|
34
|
+
pub use error::{Error, Result};
|
|
35
|
+
|
|
36
|
+
#[doc = include_str!("../README.md")]
|
|
37
|
+
#[cfg(doctest)]
|
|
38
|
+
pub struct ReadmeDoctests;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
//! The Open Packaging Conventions parts every OOXML package carries
|
|
2
|
+
//! (ECMA-376 Part 2): the content types and the relationships.
|
|
3
|
+
|
|
4
|
+
use crate::xml::XmlWriter;
|
|
5
|
+
|
|
6
|
+
/// The relationship namespace (`xmlns:r`), and the prefix of every
|
|
7
|
+
/// relationship type: `{RELATIONSHIPS}/officeDocument`, `.../styles`, ...
|
|
8
|
+
pub const RELATIONSHIPS: &str =
|
|
9
|
+
"http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
10
|
+
const PACKAGE_RELATIONSHIPS_NS: &str =
|
|
11
|
+
"http://schemas.openxmlformats.org/package/2006/relationships";
|
|
12
|
+
const CONTENT_TYPES_NS: &str = "http://schemas.openxmlformats.org/package/2006/content-types";
|
|
13
|
+
const RELATIONSHIPS_CONTENT_TYPE: &str = "application/vnd.openxmlformats-package.relationships+xml";
|
|
14
|
+
|
|
15
|
+
/// `[Content_Types].xml`: defaults for `.rels`, `.xml` and each
|
|
16
|
+
/// `(extension, content type)` in `defaults`, then one override per
|
|
17
|
+
/// `(part name, content type)`.
|
|
18
|
+
pub fn content_types<P: AsRef<str>, T: AsRef<str>>(
|
|
19
|
+
defaults: &[(&str, &str)],
|
|
20
|
+
overrides: &[(P, T)],
|
|
21
|
+
) -> String {
|
|
22
|
+
let mut xml = XmlWriter::new();
|
|
23
|
+
xml.open("Types", &[("xmlns", CONTENT_TYPES_NS)]);
|
|
24
|
+
xml.empty("Default", &[("Extension", "rels"), ("ContentType", RELATIONSHIPS_CONTENT_TYPE)]);
|
|
25
|
+
xml.empty("Default", &[("Extension", "xml"), ("ContentType", "application/xml")]);
|
|
26
|
+
for (extension, content_type) in defaults {
|
|
27
|
+
xml.empty("Default", &[("Extension", extension), ("ContentType", content_type)]);
|
|
28
|
+
}
|
|
29
|
+
for (part, content_type) in overrides {
|
|
30
|
+
xml.empty(
|
|
31
|
+
"Override",
|
|
32
|
+
&[("PartName", part.as_ref()), ("ContentType", content_type.as_ref())],
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
xml.close("Types");
|
|
36
|
+
xml.finish()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// A relationships part: `(type, target)` pairs become `rId1`, `rId2`, ...
|
|
40
|
+
/// in order; the type is the last segment, such as `styles`.
|
|
41
|
+
pub fn relationships<T: AsRef<str>>(targets: &[(&str, T)]) -> String {
|
|
42
|
+
let mut xml = XmlWriter::new();
|
|
43
|
+
xml.open("Relationships", &[("xmlns", PACKAGE_RELATIONSHIPS_NS)]);
|
|
44
|
+
for (index, (kind, target)) in targets.iter().enumerate() {
|
|
45
|
+
let id = format!("rId{}", index + 1);
|
|
46
|
+
let kind = format!("{RELATIONSHIPS}/{kind}");
|
|
47
|
+
xml.empty("Relationship", &[("Id", &id), ("Type", &kind), ("Target", target.as_ref())]);
|
|
48
|
+
}
|
|
49
|
+
xml.close("Relationships");
|
|
50
|
+
xml.finish()
|
|
51
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
//! The four Helvetica faces: `WinAnsiEncoding` and advance widths.
|
|
2
|
+
|
|
3
|
+
#[cfg(test)]
|
|
4
|
+
mod tests;
|
|
5
|
+
|
|
6
|
+
use super::metrics::{
|
|
7
|
+
FontMetrics, HELVETICA, HELVETICA_BOLD, HELVETICA_BOLDOBLIQUE, HELVETICA_OBLIQUE, WIN_ANSI_HIGH,
|
|
8
|
+
};
|
|
9
|
+
use crate::{Error, Result};
|
|
10
|
+
|
|
11
|
+
/// A face of the body font; its index is its `/F1`..`/F4` resource number.
|
|
12
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
13
|
+
pub enum Face {
|
|
14
|
+
Regular,
|
|
15
|
+
Bold,
|
|
16
|
+
Italic,
|
|
17
|
+
BoldItalic,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
impl Face {
|
|
21
|
+
pub const ALL: [Self; 4] = [Self::Regular, Self::Bold, Self::Italic, Self::BoldItalic];
|
|
22
|
+
|
|
23
|
+
pub const fn new(bold: bool, italic: bool) -> Self {
|
|
24
|
+
match (bold, italic) {
|
|
25
|
+
(false, false) => Self::Regular,
|
|
26
|
+
(true, false) => Self::Bold,
|
|
27
|
+
(false, true) => Self::Italic,
|
|
28
|
+
(true, true) => Self::BoldItalic,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
pub const fn metrics(self) -> &'static FontMetrics {
|
|
33
|
+
match self {
|
|
34
|
+
Self::Regular => &HELVETICA,
|
|
35
|
+
Self::Bold => &HELVETICA_BOLD,
|
|
36
|
+
Self::Italic => &HELVETICA_OBLIQUE,
|
|
37
|
+
Self::BoldItalic => &HELVETICA_BOLDOBLIQUE,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// The resource name: `F1` to `F4`.
|
|
42
|
+
pub const fn resource(self) -> &'static str {
|
|
43
|
+
match self {
|
|
44
|
+
Self::Regular => "F1",
|
|
45
|
+
Self::Bold => "F2",
|
|
46
|
+
Self::Italic => "F3",
|
|
47
|
+
Self::BoldItalic => "F4",
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// The text in `WinAnsiEncoding`, the encoding the standard fonts use here.
|
|
52
|
+
///
|
|
53
|
+
/// # Errors
|
|
54
|
+
/// [`Error::UnsupportedCharacter`] for anything Windows-1252 has no byte for
|
|
55
|
+
/// or the font has no glyph for.
|
|
56
|
+
pub fn encode(self, text: &str) -> Result<Vec<u8>> {
|
|
57
|
+
let metrics = self.metrics();
|
|
58
|
+
text.chars()
|
|
59
|
+
.map(|ch| {
|
|
60
|
+
win_ansi(ch)
|
|
61
|
+
.filter(|&byte| {
|
|
62
|
+
ch == '\n' || ch == '\t' || metrics.widths[usize::from(byte)] > 0
|
|
63
|
+
})
|
|
64
|
+
.ok_or(Error::UnsupportedCharacter { character: ch, font: metrics.name })
|
|
65
|
+
})
|
|
66
|
+
.collect()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// The advance width of encoded text at `size` points.
|
|
70
|
+
pub fn width(self, bytes: &[u8], size: f64) -> f64 {
|
|
71
|
+
let widths = &self.metrics().widths;
|
|
72
|
+
let units: u32 = bytes.iter().map(|&byte| u32::from(widths[usize::from(byte)])).sum();
|
|
73
|
+
f64::from(units) * size / 1000.0
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// The height above the baseline at `size` points.
|
|
77
|
+
pub fn ascent(self, size: f64) -> f64 {
|
|
78
|
+
f64::from(self.metrics().ascender) * size / 1000.0
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// The depth below the baseline at `size` points, as a positive number.
|
|
82
|
+
pub fn descent(self, size: f64) -> f64 {
|
|
83
|
+
-f64::from(self.metrics().descender) * size / 1000.0
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// The Windows-1252 byte for a character: Latin-1 maps to itself, except
|
|
88
|
+
/// 0x80..=0x9F, which Windows-1252 gives typographic characters.
|
|
89
|
+
fn win_ansi(ch: char) -> Option<u8> {
|
|
90
|
+
let code = u32::from(ch);
|
|
91
|
+
if code < 0x80 || (0xA0..=0xFF).contains(&code) {
|
|
92
|
+
return u8::try_from(code).ok();
|
|
93
|
+
}
|
|
94
|
+
let index = WIN_ANSI_HIGH.iter().position(|&high| high == Some(ch))?;
|
|
95
|
+
u8::try_from(0x80 + index).ok()
|
|
96
|
+
}
|