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.
Files changed (98) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +8 -0
  3. data/Cargo.lock +835 -0
  4. data/Cargo.toml +44 -0
  5. data/LICENSE.txt +29 -0
  6. data/README.md +314 -0
  7. data/ext/imprimatur_native/cli/Cargo.toml +30 -0
  8. data/ext/imprimatur_native/cli/src/app.rs +183 -0
  9. data/ext/imprimatur_native/cli/src/cli/completion.rs +26 -0
  10. data/ext/imprimatur_native/cli/src/cli/grep.rs +359 -0
  11. data/ext/imprimatur_native/cli/src/cli/images.rs +137 -0
  12. data/ext/imprimatur_native/cli/src/cli/limits.rs +49 -0
  13. data/ext/imprimatur_native/cli/src/cli/markdown.rs +128 -0
  14. data/ext/imprimatur_native/cli/src/cli.rs +71 -0
  15. data/ext/imprimatur_native/cli/src/main.rs +15 -0
  16. data/ext/imprimatur_native/core/Cargo.toml +33 -0
  17. data/ext/imprimatur_native/core/LICENSE +29 -0
  18. data/ext/imprimatur_native/core/README.md +105 -0
  19. data/ext/imprimatur_native/core/src/doc/paragraph.rs +112 -0
  20. data/ext/imprimatur_native/core/src/doc/table.rs +41 -0
  21. data/ext/imprimatur_native/core/src/doc.rs +210 -0
  22. data/ext/imprimatur_native/core/src/docx.rs +343 -0
  23. data/ext/imprimatur_native/core/src/error.rs +113 -0
  24. data/ext/imprimatur_native/core/src/lib.rs +38 -0
  25. data/ext/imprimatur_native/core/src/opc.rs +51 -0
  26. data/ext/imprimatur_native/core/src/pdf/font.rs +96 -0
  27. data/ext/imprimatur_native/core/src/pdf/layout/wrap.rs +161 -0
  28. data/ext/imprimatur_native/core/src/pdf/layout.rs +280 -0
  29. data/ext/imprimatur_native/core/src/pdf/metrics.rs +139 -0
  30. data/ext/imprimatur_native/core/src/pdf/objects.rs +141 -0
  31. data/ext/imprimatur_native/core/src/pdf.rs +84 -0
  32. data/ext/imprimatur_native/core/src/read/budget.rs +140 -0
  33. data/ext/imprimatur_native/core/src/read/content.rs +266 -0
  34. data/ext/imprimatur_native/core/src/read/docx/styles.rs +118 -0
  35. data/ext/imprimatur_native/core/src/read/docx.rs +336 -0
  36. data/ext/imprimatur_native/core/src/read/image/dimensions.rs +215 -0
  37. data/ext/imprimatur_native/core/src/read/image.rs +233 -0
  38. data/ext/imprimatur_native/core/src/read/inflate.rs +44 -0
  39. data/ext/imprimatur_native/core/src/read/json.rs +172 -0
  40. data/ext/imprimatur_native/core/src/read/markdown.rs +254 -0
  41. data/ext/imprimatur_native/core/src/read/package.rs +191 -0
  42. data/ext/imprimatur_native/core/src/read/pdf/cmap.rs +216 -0
  43. data/ext/imprimatur_native/core/src/read/pdf/content/geometry.rs +79 -0
  44. data/ext/imprimatur_native/core/src/read/pdf/content/operands.rs +156 -0
  45. data/ext/imprimatur_native/core/src/read/pdf/content.rs +439 -0
  46. data/ext/imprimatur_native/core/src/read/pdf/crypt.rs +256 -0
  47. data/ext/imprimatur_native/core/src/read/pdf/document.rs +420 -0
  48. data/ext/imprimatur_native/core/src/read/pdf/filter.rs +258 -0
  49. data/ext/imprimatur_native/core/src/read/pdf/font.rs +315 -0
  50. data/ext/imprimatur_native/core/src/read/pdf/glyphs.rs +179 -0
  51. data/ext/imprimatur_native/core/src/read/pdf/glyphs.txt +6298 -0
  52. data/ext/imprimatur_native/core/src/read/pdf/images.rs +365 -0
  53. data/ext/imprimatur_native/core/src/read/pdf/layout.rs +451 -0
  54. data/ext/imprimatur_native/core/src/read/pdf/lexer.rs +255 -0
  55. data/ext/imprimatur_native/core/src/read/pdf/object/name.rs +44 -0
  56. data/ext/imprimatur_native/core/src/read/pdf/object.rs +261 -0
  57. data/ext/imprimatur_native/core/src/read/pdf/xref.rs +346 -0
  58. data/ext/imprimatur_native/core/src/read/pdf.rs +53 -0
  59. data/ext/imprimatur_native/core/src/read/span.rs +22 -0
  60. data/ext/imprimatur_native/core/src/read/xlsx/numbers.rs +155 -0
  61. data/ext/imprimatur_native/core/src/read/xlsx.rs +340 -0
  62. data/ext/imprimatur_native/core/src/read.rs +145 -0
  63. data/ext/imprimatur_native/core/src/style.rs +82 -0
  64. data/ext/imprimatur_native/core/src/test_support.rs +163 -0
  65. data/ext/imprimatur_native/core/src/xlsx/cell.rs +112 -0
  66. data/ext/imprimatur_native/core/src/xlsx/comment.rs +256 -0
  67. data/ext/imprimatur_native/core/src/xlsx/date.rs +173 -0
  68. data/ext/imprimatur_native/core/src/xlsx/format.rs +226 -0
  69. data/ext/imprimatur_native/core/src/xlsx/package.rs +268 -0
  70. data/ext/imprimatur_native/core/src/xlsx/range.rs +124 -0
  71. data/ext/imprimatur_native/core/src/xlsx/styles.rs +240 -0
  72. data/ext/imprimatur_native/core/src/xlsx/text.rs +122 -0
  73. data/ext/imprimatur_native/core/src/xlsx/validation.rs +258 -0
  74. data/ext/imprimatur_native/core/src/xlsx/worksheet.rs +350 -0
  75. data/ext/imprimatur_native/core/src/xlsx.rs +120 -0
  76. data/ext/imprimatur_native/core/src/xml/reader.rs +238 -0
  77. data/ext/imprimatur_native/core/src/xml.rs +95 -0
  78. data/ext/imprimatur_native/core/src/zip/reader.rs +144 -0
  79. data/ext/imprimatur_native/core/src/zip.rs +161 -0
  80. data/ext/imprimatur_native/extconf.rb +45 -0
  81. data/ext/imprimatur_native/ffi/Cargo.toml +21 -0
  82. data/ext/imprimatur_native/ffi/src/convert.rs +132 -0
  83. data/ext/imprimatur_native/ffi/src/document.rs +138 -0
  84. data/ext/imprimatur_native/ffi/src/lib.rs +301 -0
  85. data/ext/imprimatur_native/ffi/src/read.rs +182 -0
  86. data/ext/imprimatur_native/ffi/src/style.rs +85 -0
  87. data/ext/imprimatur_native/ffi/src/validation.rs +56 -0
  88. data/lib/imprimatur/document.rb +127 -0
  89. data/lib/imprimatur/error.rb +22 -0
  90. data/lib/imprimatur/formula.rb +13 -0
  91. data/lib/imprimatur/native.rb +17 -0
  92. data/lib/imprimatur/reader.rb +59 -0
  93. data/lib/imprimatur/style.rb +31 -0
  94. data/lib/imprimatur/validation.rb +35 -0
  95. data/lib/imprimatur/version.rb +5 -0
  96. data/lib/imprimatur/xlsx.rb +183 -0
  97. data/lib/imprimatur.rb +111 -0
  98. metadata +157 -0
@@ -0,0 +1,118 @@
1
+ //! What a DOCX's parts besides the body say about its paragraphs: heading
2
+ //! levels and lists from `styles.xml`, list kinds from `numbering.xml`.
3
+
4
+ use crate::Result;
5
+ use crate::read::Budget;
6
+ use crate::xml::reader::{Event, Tag, XmlReader};
7
+
8
+ /// What a paragraph style contributes.
9
+ #[derive(Debug, Clone, Default)]
10
+ pub struct Style {
11
+ pub id: String,
12
+ pub based_on: Option<String>,
13
+ pub heading: Option<u8>,
14
+ pub list: Option<(String, u8)>,
15
+ }
16
+
17
+ /// A `w:val` attribute, owned.
18
+ pub fn value(tag: &Tag<'_>) -> Option<String> {
19
+ tag.attr("val").map(std::borrow::Cow::into_owned)
20
+ }
21
+
22
+ /// A `w:val` list level (`w:ilvl`), 0 to 8.
23
+ pub fn level(tag: &Tag<'_>) -> Option<u8> {
24
+ tag.attr("val").and_then(|level| level.parse::<u8>().ok())
25
+ }
26
+
27
+ /// `w:outlineLvl`: 0 is a level-1 heading; 9 is body text.
28
+ pub fn outline(tag: &Tag<'_>) -> Option<u8> {
29
+ level(tag).filter(|&level| level < 9).map(|level| level + 1)
30
+ }
31
+
32
+ pub fn read_styles(text: &str, budget: &Budget<'_>) -> Result<Vec<Style>> {
33
+ let mut xml = XmlReader::new(text, budget.max_depth());
34
+ let (mut styles, mut style): (Vec<Style>, Option<Style>) = (Vec::new(), None);
35
+ let (mut list_level, mut numbering) = (None, None);
36
+ while let Some(event) = xml.next()? {
37
+ match event {
38
+ Event::Start(tag) => match (tag.name(), style.as_mut()) {
39
+ ("style", _) => {
40
+ let paragraph = tag.attr("type").is_none_or(|kind| kind == "paragraph");
41
+ style = paragraph.then(|| Style {
42
+ id: tag.attr("styleId").unwrap_or_default().into_owned(),
43
+ ..Style::default()
44
+ });
45
+ (list_level, numbering) = (None, None);
46
+ }
47
+ ("name", Some(style)) => {
48
+ let name = tag.attr("val").unwrap_or_default().to_ascii_lowercase();
49
+ style.heading = style.heading.or_else(|| match name.as_str() {
50
+ "title" => Some(1),
51
+ name => name
52
+ .strip_prefix("heading ")
53
+ .and_then(|level| level.trim().parse().ok()),
54
+ });
55
+ }
56
+ ("basedOn", Some(style)) => {
57
+ style.based_on = value(&tag);
58
+ }
59
+ ("outlineLvl", Some(style)) => {
60
+ style.heading = style.heading.or_else(|| outline(&tag));
61
+ }
62
+ ("ilvl", Some(_)) => {
63
+ list_level = self::level(&tag);
64
+ }
65
+ ("numId", Some(_)) => numbering = value(&tag),
66
+ _ => {}
67
+ },
68
+ Event::End("style") => {
69
+ if let Some(mut done) = style.take() {
70
+ done.list = numbering.take().map(|id| (id, list_level.unwrap_or(0)));
71
+ styles.push(done);
72
+ }
73
+ }
74
+ _ => {}
75
+ }
76
+ }
77
+ Ok(styles)
78
+ }
79
+
80
+ /// Which list levels are numbered rather than bulleted, by numbering id.
81
+ pub fn read_numbering(text: &str, budget: &Budget<'_>) -> Result<Vec<(String, Vec<bool>)>> {
82
+ let mut xml = XmlReader::new(text, budget.max_depth());
83
+ let mut abstracts: Vec<(String, Vec<bool>)> = Vec::new();
84
+ let mut numbers: Vec<(String, String)> = Vec::new();
85
+ let mut current_number: Option<String> = None;
86
+ while let Some(event) = xml.next()? {
87
+ match event {
88
+ Event::Start(tag) => match tag.name() {
89
+ "abstractNum" => abstracts
90
+ .push((tag.attr("abstractNumId").unwrap_or_default().into_owned(), Vec::new())),
91
+ "numFmt" if current_number.is_none() => {
92
+ if let Some((_, levels)) = abstracts.last_mut() {
93
+ let format = tag.attr("val").unwrap_or_default();
94
+ levels.push(!matches!(&*format, "bullet" | "none"));
95
+ }
96
+ }
97
+ "num" => current_number = tag.attr("numId").map(std::borrow::Cow::into_owned),
98
+ "abstractNumId" => {
99
+ if let (Some(number), Some(id)) = (current_number.clone(), tag.attr("val")) {
100
+ numbers.push((number, id.into_owned()));
101
+ }
102
+ }
103
+ _ => {}
104
+ },
105
+ Event::End("num") => current_number = None,
106
+ _ => {}
107
+ }
108
+ }
109
+ Ok(numbers
110
+ .into_iter()
111
+ .filter_map(|(number, id)| {
112
+ abstracts
113
+ .iter()
114
+ .find(|(abstract_id, _)| *abstract_id == id)
115
+ .map(|(_, levels)| (number, levels.clone()))
116
+ })
117
+ .collect())
118
+ }
@@ -0,0 +1,336 @@
1
+ //! DOCX: the body as blocks. Headings from paragraph styles (and outline
2
+ //! levels), lists from numbering, bold, italic and links from runs, tables,
3
+ //! text boxes after their paragraph. Deleted text, field codes and the
4
+ //! fallback copy of alternate content are left out.
5
+
6
+ mod styles;
7
+ #[cfg(test)]
8
+ mod tests;
9
+
10
+ use super::Budget;
11
+ use super::content::{Block, Grid, Mark, Text};
12
+ use super::markdown::cell_text;
13
+ use super::package::Package;
14
+ use crate::Result;
15
+ use crate::xml::reader::{Event, XmlReader};
16
+ use styles::{Style, outline, read_numbering, read_styles, value};
17
+
18
+ /// Run formatting, and the link a run belongs to.
19
+ #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
20
+ struct Look {
21
+ bold: bool,
22
+ italic: bool,
23
+ link: Option<usize>,
24
+ }
25
+
26
+ impl Look {
27
+ const fn is_plain(self) -> bool {
28
+ !self.bold && !self.italic && self.link.is_none()
29
+ }
30
+ }
31
+
32
+ /// A paragraph's text, with the look of each stretch of it.
33
+ #[derive(Default)]
34
+ struct Inline {
35
+ text: String,
36
+ looks: Vec<(std::ops::Range<usize>, Look)>,
37
+ }
38
+
39
+ impl Inline {
40
+ fn push(&mut self, text: &str, look: Look) {
41
+ let start = self.text.len();
42
+ self.text.push_str(text);
43
+ match self.looks.last_mut() {
44
+ Some((range, last)) if *last == look && range.end == start => {
45
+ range.end = self.text.len();
46
+ }
47
+ _ => self.looks.push((start..self.text.len(), look)),
48
+ }
49
+ }
50
+
51
+ fn clear(&mut self) {
52
+ self.text.clear();
53
+ self.looks.clear();
54
+ }
55
+
56
+ /// The text and its styled stretches, taken: the paragraph is done. A
57
+ /// run's spaces at the edges of a stretch are left out of it.
58
+ fn take(&mut self, links: &[String], styled: bool) -> Text {
59
+ let marks = if styled {
60
+ self.looks
61
+ .iter()
62
+ .filter(|(_, look)| !look.is_plain())
63
+ .filter_map(|(range, look)| {
64
+ let stretch = &self.text[range.clone()];
65
+ let start = range.start + (stretch.len() - stretch.trim_start().len());
66
+ let end = range.end - (stretch.len() - stretch.trim_end().len());
67
+ (start < end).then(|| Mark {
68
+ range: start..end,
69
+ bold: look.bold,
70
+ italic: look.italic,
71
+ link: look.link.and_then(|index| links.get(index)).cloned(),
72
+ })
73
+ })
74
+ .collect()
75
+ } else {
76
+ Vec::new()
77
+ };
78
+ self.looks.clear();
79
+ Text { text: std::mem::take(&mut self.text), marks }
80
+ }
81
+ }
82
+
83
+ /// What the document's parts say about its paragraphs.
84
+ struct Context<'a> {
85
+ budget: &'a Budget<'a>,
86
+ styles: Vec<Style>,
87
+ /// Ordered or not, by numbering id and level.
88
+ numbering: Vec<(String, Vec<bool>)>,
89
+ links: Vec<String>,
90
+ link_ids: Vec<String>,
91
+ }
92
+
93
+ impl Context<'_> {
94
+ /// A style's heading level and list, following `basedOn` a few steps.
95
+ fn style(&self, id: &str) -> (Option<u8>, Option<(String, u8)>) {
96
+ let (mut heading, mut list, mut next) = (None, None, Some(id));
97
+ for _ in 0..8 {
98
+ let Some(style) = next.and_then(|id| self.styles.iter().find(|style| style.id == id))
99
+ else {
100
+ break;
101
+ };
102
+ heading = heading.or(style.heading);
103
+ list = list.or_else(|| style.list.clone());
104
+ next = style.based_on.as_deref();
105
+ }
106
+ (heading, list)
107
+ }
108
+
109
+ fn ordered(&self, id: &str, level: u8) -> bool {
110
+ self.numbering
111
+ .iter()
112
+ .find(|(numbering, _)| numbering == id)
113
+ .and_then(|(_, levels)| levels.get(usize::from(level)).copied())
114
+ .unwrap_or(false)
115
+ }
116
+ }
117
+
118
+ /// The paragraph being read.
119
+ #[derive(Default)]
120
+ struct Paragraph {
121
+ style: Option<String>,
122
+ list: Option<(String, u8)>,
123
+ outline: Option<u8>,
124
+ inline: Inline,
125
+ /// Text boxes anchored in it, which follow it.
126
+ boxes: Vec<Block>,
127
+ }
128
+
129
+ pub fn content(package: &Package<'_>, budget: &Budget<'_>) -> Result<Vec<Block>> {
130
+ let main = package.main();
131
+ let text = package.main_text()?;
132
+ let relationships = package.relationships(main)?;
133
+ let (mut links, mut link_ids) = (Vec::new(), Vec::new());
134
+ for relationship in relationships.iter().filter(|r| r.kind == "hyperlink" && r.external) {
135
+ link_ids.push(relationship.id.clone());
136
+ links.push(relationship.target.clone());
137
+ }
138
+ let styles =
139
+ package.parse_related(&relationships, "styles", |text| read_styles(text, budget))?;
140
+ let numbering =
141
+ package.parse_related(&relationships, "numbering", |text| read_numbering(text, budget))?;
142
+ let context = Context { budget, styles, numbering, links, link_ids };
143
+ let mut xml = XmlReader::new(&text, budget.max_depth());
144
+ let mut out = Vec::new();
145
+ while let Some(event) = xml.next()? {
146
+ if let Event::Start(tag) = event
147
+ && tag.name() == "body"
148
+ {
149
+ blocks(&mut xml, &context, &mut out, "body")?;
150
+ break;
151
+ }
152
+ }
153
+ Ok(out)
154
+ }
155
+
156
+ /// Block content up to the end of `until`: paragraphs, tables, and the
157
+ /// containers around them. Returns the grid columns a table cell spans.
158
+ fn blocks(
159
+ xml: &mut XmlReader<'_>,
160
+ context: &Context<'_>,
161
+ out: &mut Vec<Block>,
162
+ until: &str,
163
+ ) -> Result<u32> {
164
+ let mut span = 1;
165
+ let mut paragraph = Paragraph::default();
166
+ while let Some(event) = xml.next()? {
167
+ context.budget.tick()?;
168
+ match event {
169
+ Event::Start(tag) => match tag.name() {
170
+ "p" => {
171
+ paragraph.inline.clear();
172
+ (paragraph.style, paragraph.list, paragraph.outline) = (None, None, None);
173
+ read_paragraph(xml, context, &mut paragraph)?;
174
+ push_paragraph(out, &mut paragraph, context);
175
+ }
176
+ "tbl" => {
177
+ let grid = read_table(xml, context)?;
178
+ if !grid.is_empty() {
179
+ out.push(Block::Table(grid));
180
+ }
181
+ }
182
+ "gridSpan" => {
183
+ span = tag.attr("val").and_then(|span| span.parse().ok()).unwrap_or(1).max(1);
184
+ }
185
+ "sdtPr" | "sectPr" | "Fallback" | "del" | "moveFrom" => xml.skip()?,
186
+ _ => {}
187
+ },
188
+ Event::End(name) if name == until => return Ok(span),
189
+ _ => {}
190
+ }
191
+ }
192
+ Ok(span)
193
+ }
194
+
195
+ fn read_paragraph(
196
+ xml: &mut XmlReader<'_>,
197
+ context: &Context<'_>,
198
+ paragraph: &mut Paragraph,
199
+ ) -> Result<()> {
200
+ let mut link = None;
201
+ while let Some(event) = xml.next()? {
202
+ context.budget.tick()?;
203
+ match event {
204
+ Event::Start(tag) => match tag.name() {
205
+ "pPr" => read_properties(xml, paragraph)?,
206
+ "r" => read_run(xml, context, paragraph, link)?,
207
+ "hyperlink" => {
208
+ link = tag
209
+ .attr("id")
210
+ .and_then(|id| context.link_ids.iter().position(|known| *known == id));
211
+ }
212
+ "del" | "moveFrom" | "Fallback" | "rPr" | "sdtPr" => xml.skip()?,
213
+ _ => {}
214
+ },
215
+ Event::End("hyperlink") => link = None,
216
+ Event::End("p") => return Ok(()),
217
+ _ => {}
218
+ }
219
+ }
220
+ Ok(())
221
+ }
222
+
223
+ fn read_properties(xml: &mut XmlReader<'_>, paragraph: &mut Paragraph) -> Result<()> {
224
+ let (mut level, mut numbering) = (None, None);
225
+ while let Some(event) = xml.next()? {
226
+ match event {
227
+ Event::Start(tag) => match tag.name() {
228
+ "pStyle" => paragraph.style = value(&tag),
229
+ "ilvl" => level = styles::level(&tag),
230
+ "numId" => numbering = value(&tag),
231
+ "outlineLvl" => paragraph.outline = outline(&tag),
232
+ "rPr" | "pPrChange" | "sectPr" => xml.skip()?,
233
+ _ => {}
234
+ },
235
+ Event::End("pPr") => break,
236
+ _ => {}
237
+ }
238
+ }
239
+ paragraph.list = numbering.map(|id| (id, level.unwrap_or(0)));
240
+ Ok(())
241
+ }
242
+
243
+ fn read_run(
244
+ xml: &mut XmlReader<'_>,
245
+ context: &Context<'_>,
246
+ paragraph: &mut Paragraph,
247
+ link: Option<usize>,
248
+ ) -> Result<()> {
249
+ let mut look = Look { link, ..Look::default() };
250
+ let mut in_text = false;
251
+ while let Some(event) = xml.next()? {
252
+ context.budget.tick()?;
253
+ match event {
254
+ Event::Start(tag) => match tag.name() {
255
+ "rPr" => read_run_properties(xml, &mut look)?,
256
+ "t" => in_text = true,
257
+ "tab" | "ptab" => paragraph.inline.push(" ", look),
258
+ "br" | "cr" => paragraph.inline.push("\n", look),
259
+ "noBreakHyphen" => paragraph.inline.push("-", look),
260
+ "txbxContent" => {
261
+ blocks(xml, context, &mut paragraph.boxes, "txbxContent")?;
262
+ }
263
+ "delText" | "instrText" | "delInstrText" | "Fallback" => xml.skip()?,
264
+ _ => {}
265
+ },
266
+ Event::Text(text) if in_text => paragraph.inline.push(&text, look),
267
+ Event::End("t") => in_text = false,
268
+ Event::End("r") => return Ok(()),
269
+ _ => {}
270
+ }
271
+ }
272
+ Ok(())
273
+ }
274
+
275
+ fn read_run_properties(xml: &mut XmlReader<'_>, look: &mut Look) -> Result<()> {
276
+ while let Some(event) = xml.next()? {
277
+ match event {
278
+ Event::Start(tag) => match tag.name() {
279
+ "b" => look.bold = tag.flag("val"),
280
+ "i" => look.italic = tag.flag("val"),
281
+ "rPrChange" => xml.skip()?,
282
+ _ => {}
283
+ },
284
+ Event::End("rPr") => return Ok(()),
285
+ _ => {}
286
+ }
287
+ }
288
+ Ok(())
289
+ }
290
+
291
+ fn read_table(xml: &mut XmlReader<'_>, context: &Context<'_>) -> Result<Grid> {
292
+ let mut grid = Grid::default();
293
+ let (mut row, mut column) = (0_u32, 0_u32);
294
+ let mut cell = Vec::new();
295
+ while let Some(event) = xml.next()? {
296
+ context.budget.tick()?;
297
+ match event {
298
+ Event::Start(tag) => match tag.name() {
299
+ "tr" => column = 0,
300
+ "tc" => {
301
+ cell.clear();
302
+ let span = blocks(xml, context, &mut cell, "tc")?;
303
+ cell_text(&cell, grid.buffer());
304
+ grid.end_cell(row, column, super::content::Value::Text);
305
+ column += span;
306
+ }
307
+ "tblPr" | "tblGrid" | "trPr" => xml.skip()?,
308
+ _ => {}
309
+ },
310
+ Event::End("tr") => row += 1,
311
+ Event::End("tbl") => return Ok(grid.finish()),
312
+ _ => {}
313
+ }
314
+ }
315
+ Ok(grid.finish())
316
+ }
317
+
318
+ /// The paragraph as a block, unless it has no text; then the blocks of its
319
+ /// text boxes.
320
+ fn push_paragraph(out: &mut Vec<Block>, paragraph: &mut Paragraph, context: &Context<'_>) {
321
+ let (styled_heading, styled_list) =
322
+ paragraph.style.as_deref().map_or((None, None), |style| context.style(style));
323
+ let heading = paragraph.outline.or(styled_heading);
324
+ let list = paragraph.list.clone().or(styled_list).filter(|(id, _)| id != "0");
325
+ if !paragraph.inline.text.trim().is_empty() {
326
+ out.push(if let Some(level) = heading {
327
+ Block::Heading { level, text: paragraph.inline.take(&context.links, false) }
328
+ } else if let Some((id, level)) = list {
329
+ let ordered = context.ordered(&id, level);
330
+ Block::ListItem { level, ordered, text: paragraph.inline.take(&context.links, true) }
331
+ } else {
332
+ Block::Paragraph { text: paragraph.inline.take(&context.links, true) }
333
+ });
334
+ }
335
+ out.append(&mut paragraph.boxes);
336
+ }
@@ -0,0 +1,215 @@
1
+ //! Image sizes from file headers, for the formats that put them up front.
2
+ //! Vector formats give the size they are drawn at, in pixels at 96 dpi.
3
+
4
+ use super::ImageFormat;
5
+ use crate::xml::reader::Tag;
6
+
7
+ /// Pixels per inch, for vector formats sized in physical units.
8
+ const DPI: f64 = 96.0;
9
+
10
+ /// Width and height, when the header says them.
11
+ pub fn dimensions(format: ImageFormat, data: &[u8]) -> Option<(u32, u32)> {
12
+ let bytes = Bytes(data);
13
+ match format {
14
+ ImageFormat::Png => Some((bytes.u32_be(16)?, bytes.u32_be(20)?)),
15
+ ImageFormat::Gif => Some((bytes.u16_le(6)?.into(), bytes.u16_le(8)?.into())),
16
+ ImageFormat::Bmp => {
17
+ Some((bytes.i32_le(18)?.unsigned_abs(), bytes.i32_le(22)?.unsigned_abs()))
18
+ }
19
+ ImageFormat::Jpeg => jpeg(&bytes),
20
+ ImageFormat::Tiff => tiff(&bytes),
21
+ ImageFormat::Webp => webp(&bytes),
22
+ ImageFormat::Jpeg2000 => jpeg2000(&bytes),
23
+ ImageFormat::Emf => emf(&bytes),
24
+ ImageFormat::Wmf => wmf(&bytes),
25
+ ImageFormat::Svg => svg(data),
26
+ _ => None,
27
+ }
28
+ .filter(|&(width, height)| width > 0 && height > 0)
29
+ }
30
+
31
+ /// Bytes read at offsets; `None` past the end.
32
+ struct Bytes<'a>(&'a [u8]);
33
+
34
+ impl Bytes<'_> {
35
+ fn array<const N: usize>(&self, at: usize) -> Option<[u8; N]> {
36
+ self.0.get(at..at.checked_add(N)?)?.try_into().ok()
37
+ }
38
+
39
+ fn u16_le(&self, at: usize) -> Option<u16> {
40
+ self.array(at).map(u16::from_le_bytes)
41
+ }
42
+
43
+ fn u16_be(&self, at: usize) -> Option<u16> {
44
+ self.array(at).map(u16::from_be_bytes)
45
+ }
46
+
47
+ fn u32_le(&self, at: usize) -> Option<u32> {
48
+ self.array(at).map(u32::from_le_bytes)
49
+ }
50
+
51
+ fn u32_be(&self, at: usize) -> Option<u32> {
52
+ self.array(at).map(u32::from_be_bytes)
53
+ }
54
+
55
+ fn i32_le(&self, at: usize) -> Option<i32> {
56
+ self.array(at).map(i32::from_le_bytes)
57
+ }
58
+
59
+ fn i16_le(&self, at: usize) -> Option<i16> {
60
+ self.array(at).map(i16::from_le_bytes)
61
+ }
62
+
63
+ /// A 24-bit little-endian number.
64
+ fn u24_le(&self, at: usize) -> Option<u32> {
65
+ let [low, middle, high] = self.array(at)?;
66
+ Some(u32::from_le_bytes([low, middle, high, 0]))
67
+ }
68
+ }
69
+
70
+ /// From the start-of-frame marker.
71
+ fn jpeg(bytes: &Bytes<'_>) -> Option<(u32, u32)> {
72
+ let mut at = 2;
73
+ loop {
74
+ let [mark, marker] = bytes.array(at)?;
75
+ if mark != 0xFF {
76
+ return None;
77
+ }
78
+ // SOF0 to SOF15, but DHT, JPG and DAC share the range.
79
+ if (0xC0..=0xCF).contains(&marker) && !matches!(marker, 0xC4 | 0xC8 | 0xCC) {
80
+ return Some((bytes.u16_be(at + 7)?.into(), bytes.u16_be(at + 5)?.into()));
81
+ }
82
+ at += 2 + usize::from(bytes.u16_be(at + 2)?);
83
+ }
84
+ }
85
+
86
+ /// From the first image directory's `ImageWidth` and `ImageLength`.
87
+ fn tiff(bytes: &Bytes<'_>) -> Option<(u32, u32)> {
88
+ let little = bytes.0.starts_with(b"II");
89
+ let short = |at| if little { bytes.u16_le(at) } else { bytes.u16_be(at) };
90
+ let long = |at| if little { bytes.u32_le(at) } else { bytes.u32_be(at) };
91
+ let directory = usize::try_from(long(4)?).ok()?;
92
+ let (mut width, mut height) = (None, None);
93
+ for index in 0..usize::from(short(directory)?) {
94
+ let entry = directory + 2 + index * 12;
95
+ let value = match short(entry + 2)? {
96
+ 3 => u32::from(short(entry + 8)?),
97
+ 4 => long(entry + 8)?,
98
+ _ => continue,
99
+ };
100
+ match short(entry)? {
101
+ 256 => width = Some(value),
102
+ 257 => height = Some(value),
103
+ _ => {}
104
+ }
105
+ if let (Some(width), Some(height)) = (width, height) {
106
+ return Some((width, height));
107
+ }
108
+ }
109
+ None
110
+ }
111
+
112
+ /// From the first chunk: lossy, lossless or extended.
113
+ fn webp(bytes: &Bytes<'_>) -> Option<(u32, u32)> {
114
+ match &bytes.array(12)? {
115
+ b"VP8 " => {
116
+ let size = |at| bytes.u16_le(at).map(|value| u32::from(value & 0x3FFF));
117
+ Some((size(26)?, size(28)?))
118
+ }
119
+ b"VP8L" => {
120
+ let bits = bytes.u32_le(21)?;
121
+ Some(((bits & 0x3FFF) + 1, ((bits >> 14) & 0x3FFF) + 1))
122
+ }
123
+ b"VP8X" => Some((bytes.u24_le(24)? + 1, bytes.u24_le(27)? + 1)),
124
+ _ => None,
125
+ }
126
+ }
127
+
128
+ /// From a codestream's SIZ marker, or a JP2 file's image header box.
129
+ fn jpeg2000(bytes: &Bytes<'_>) -> Option<(u32, u32)> {
130
+ if bytes.0.starts_with(&[0xFF, 0x4F, 0xFF, 0x51]) {
131
+ let width = bytes.u32_be(8)?.checked_sub(bytes.u32_be(16)?)?;
132
+ let height = bytes.u32_be(12)?.checked_sub(bytes.u32_be(20)?)?;
133
+ return Some((width, height));
134
+ }
135
+ let header = Bytes(find_box(find_box(bytes.0, *b"jp2h")?, *b"ihdr")?);
136
+ Some((header.u32_be(4)?, header.u32_be(0)?))
137
+ }
138
+
139
+ /// The contents of the first box of `kind` among `data`'s boxes.
140
+ fn find_box(mut data: &[u8], kind: [u8; 4]) -> Option<&[u8]> {
141
+ while data.len() >= 8 {
142
+ let bytes = Bytes(data);
143
+ let (start, length) = match bytes.u32_be(0)? {
144
+ 0 => (8, data.len()),
145
+ 1 => (16, usize::try_from(u64::from_be_bytes(bytes.array(8)?)).ok()?),
146
+ length => (8, usize::try_from(length).ok()?),
147
+ };
148
+ if length < start || length > data.len() {
149
+ return None;
150
+ }
151
+ if bytes.array(4)? == kind {
152
+ return data.get(start..length);
153
+ }
154
+ data = &data[length..];
155
+ }
156
+ None
157
+ }
158
+
159
+ /// From the header record's bounds, in device units, inclusive.
160
+ fn emf(bytes: &Bytes<'_>) -> Option<(u32, u32)> {
161
+ let span = |low: i32, high: i32| u32::try_from(i64::from(high) - i64::from(low) + 1).ok();
162
+ Some((span(bytes.i32_le(8)?, bytes.i32_le(16)?)?, span(bytes.i32_le(12)?, bytes.i32_le(20)?)?))
163
+ }
164
+
165
+ /// From a placeable metafile's bounding box and its units per inch.
166
+ fn wmf(bytes: &Bytes<'_>) -> Option<(u32, u32)> {
167
+ let inch = f64::from(bytes.u16_le(14)?);
168
+ let span =
169
+ |low: i16, high: i16| pixels(f64::from(i32::from(high) - i32::from(low)) / inch * DPI);
170
+ if inch == 0.0 {
171
+ return None;
172
+ }
173
+ Some((span(bytes.i16_le(6)?, bytes.i16_le(10)?)?, span(bytes.i16_le(8)?, bytes.i16_le(12)?)?))
174
+ }
175
+
176
+ /// From the root element's `width` and `height`, or its `viewBox`.
177
+ fn svg(data: &[u8]) -> Option<(u32, u32)> {
178
+ let text = std::str::from_utf8(data.get(..data.len().min(64 * 1024))?).ok()?;
179
+ let start = text.match_indices("<svg").map(|(at, _)| at + 4).find(|&at| {
180
+ text[at..].starts_with(|ch: char| ch.is_ascii_whitespace() || ch == '>' || ch == '/')
181
+ })?;
182
+ let tag = Tag::new("svg", &text[start..start + text[start..].find('>')?]);
183
+ let length = |name| tag.attr(name).and_then(|value| svg_length(&value));
184
+ if let (Some(width), Some(height)) = (length("width"), length("height")) {
185
+ return Some((width, height));
186
+ }
187
+ let view = tag.attr("viewBox")?;
188
+ let mut numbers = view.split([' ', ',']).filter(|part| !part.is_empty()).skip(2);
189
+ let mut next = || numbers.next()?.parse::<f64>().ok().and_then(pixels);
190
+ Some((next()?, next()?))
191
+ }
192
+
193
+ /// An SVG length in pixels; `None` for relative units (`%`, `em`).
194
+ fn svg_length(value: &str) -> Option<u32> {
195
+ let value = value.trim();
196
+ let split = value.find(|ch: char| ch.is_ascii_alphabetic() || ch == '%').unwrap_or(value.len());
197
+ let (number, unit) = value.split_at(split);
198
+ let number: f64 = number.trim().parse().ok()?;
199
+ let scale = match unit {
200
+ "" | "px" => 1.0,
201
+ "pt" => DPI / 72.0,
202
+ "pc" => DPI / 6.0,
203
+ "in" => DPI,
204
+ "cm" => DPI / 2.54,
205
+ "mm" => DPI / 25.4,
206
+ _ => return None,
207
+ };
208
+ pixels(number * scale)
209
+ }
210
+
211
+ #[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss, reason = "checked to fit")]
212
+ fn pixels(value: f64) -> Option<u32> {
213
+ let value = value.round();
214
+ (value.is_finite() && (1.0..=f64::from(u32::MAX)).contains(&value)).then_some(value as u32)
215
+ }