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,141 @@
1
+ //! PDF file syntax: numbered objects, compressed streams, the
2
+ //! cross-reference table and the trailer (ISO 32000-1, section 7).
3
+
4
+ use std::fmt;
5
+ use std::io::Write;
6
+
7
+ use crate::{Error, Result};
8
+
9
+ /// The header, with the binary comment that marks the file as binary.
10
+ const HEADER: &[u8] = b"%PDF-1.7\n%\xE2\xE3\xCF\xD3\n";
11
+ /// zlib's default trade-off between size and speed.
12
+ const LEVEL: u8 = 6;
13
+ /// The largest offset a cross-reference entry's ten digits hold.
14
+ const MAX_OFFSET: usize = 9_999_999_999;
15
+
16
+ /// Streams a PDF into `out`; objects are numbered from 1 in the order
17
+ /// written, and only their offsets are kept for the cross-reference table.
18
+ pub struct PdfWriter<W: Write> {
19
+ out: W,
20
+ written: usize,
21
+ offsets: Vec<usize>,
22
+ }
23
+
24
+ impl<W: Write> PdfWriter<W> {
25
+ pub fn new(out: W) -> Result<Self> {
26
+ let mut writer = Self { out, written: 0, offsets: Vec::new() };
27
+ writer.write(HEADER)?;
28
+ Ok(writer)
29
+ }
30
+
31
+ /// Writes `id 0 obj <dictionary> endobj`; ids must come in order.
32
+ pub fn object(&mut self, id: usize, dictionary: fmt::Arguments<'_>) -> Result<()> {
33
+ self.begin(id)?;
34
+ self.write_fmt(format_args!("{dictionary}\nendobj\n"))
35
+ }
36
+
37
+ /// Writes a Flate-compressed stream object.
38
+ pub fn stream(&mut self, id: usize, data: &[u8]) -> Result<()> {
39
+ let compressed = miniz_oxide::deflate::compress_to_vec_zlib(data, LEVEL);
40
+ self.begin(id)?;
41
+ let length = compressed.len();
42
+ self.write_fmt(format_args!("<< /Length {length} /Filter /FlateDecode >>\nstream\n"))?;
43
+ self.write(&compressed)?;
44
+ self.write(b"\nendstream\nendobj\n")
45
+ }
46
+
47
+ /// Writes the cross-reference table and the trailer; returns `out`.
48
+ pub fn finish(mut self, root: usize, info: usize) -> Result<W> {
49
+ let xref = self.written;
50
+ let size = self.offsets.len() + 1;
51
+ self.write_fmt(format_args!("xref\n0 {size}\n0000000000 65535 f \n"))?;
52
+ for index in 0..self.offsets.len() {
53
+ let offset = self.offsets[index];
54
+ (offset <= MAX_OFFSET).ok_or(Error::ArchiveTooLarge("PDF over 10 GB"))?;
55
+ self.write_fmt(format_args!("{offset:010} 00000 n \n"))?;
56
+ }
57
+ self.write_fmt(format_args!(
58
+ "trailer\n<< /Size {size} /Root {root} 0 R /Info {info} 0 R >>\n\
59
+ startxref\n{xref}\n%%EOF\n"
60
+ ))?;
61
+ self.out.flush()?;
62
+ Ok(self.out)
63
+ }
64
+
65
+ fn begin(&mut self, id: usize) -> Result<()> {
66
+ debug_assert_eq!(id, self.offsets.len() + 1, "PDF objects are written in id order");
67
+ self.offsets.push(self.written);
68
+ self.write_fmt(format_args!("{id} 0 obj\n"))
69
+ }
70
+
71
+ fn write(&mut self, bytes: &[u8]) -> Result<()> {
72
+ self.out.write_all(bytes)?;
73
+ self.written += bytes.len();
74
+ Ok(())
75
+ }
76
+
77
+ /// Formats straight into `out`, counting what it writes.
78
+ fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> Result<()> {
79
+ let mut counted = Counted { out: &mut self.out, written: 0 };
80
+ counted.write_fmt(args)?;
81
+ self.written += counted.written;
82
+ Ok(())
83
+ }
84
+ }
85
+
86
+ /// A writer that counts the bytes passing through it.
87
+ struct Counted<'a, W: Write> {
88
+ out: &'a mut W,
89
+ written: usize,
90
+ }
91
+
92
+ impl<W: Write> Write for Counted<'_, W> {
93
+ fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
94
+ let written = self.out.write(bytes)?;
95
+ self.written += written;
96
+ Ok(written)
97
+ }
98
+
99
+ fn flush(&mut self) -> std::io::Result<()> {
100
+ self.out.flush()
101
+ }
102
+ }
103
+
104
+ /// A number for a content stream: two decimals at most, no trailing zeros,
105
+ /// formatted without an intermediate `String`.
106
+ #[derive(Clone, Copy)]
107
+ pub struct Num(pub f64);
108
+
109
+ impl fmt::Display for Num {
110
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111
+ #[expect(
112
+ clippy::cast_possible_truncation,
113
+ reason = "page coordinates are far inside i64 hundredths"
114
+ )]
115
+ let hundredths = (self.0 * 100.0).round() as i64;
116
+ if hundredths < 0 {
117
+ f.write_str("-")?;
118
+ }
119
+ let (whole, fraction) = (hundredths.unsigned_abs() / 100, hundredths.unsigned_abs() % 100);
120
+ match fraction {
121
+ 0 => write!(f, "{whole}"),
122
+ tenths if tenths % 10 == 0 => write!(f, "{whole}.{}", tenths / 10),
123
+ fraction => write!(f, "{whole}.{fraction:02}"),
124
+ }
125
+ }
126
+ }
127
+
128
+ /// Appends a literal string `( ... )`, escaped.
129
+ pub fn push_string(out: &mut Vec<u8>, bytes: &[u8]) {
130
+ out.reserve(bytes.len() + 2);
131
+ out.push(b'(');
132
+ for &byte in bytes {
133
+ match byte {
134
+ b'(' | b')' | b'\\' => out.extend_from_slice(&[b'\\', byte]),
135
+ // A bare CR inside a string reads back as LF.
136
+ b'\r' => out.extend_from_slice(b"\\r"),
137
+ _ => out.push(byte),
138
+ }
139
+ }
140
+ out.push(b')');
141
+ }
@@ -0,0 +1,84 @@
1
+ //! PDF output: the document laid out with the standard Helvetica fonts
2
+ //! (`WinAnsiEncoding`, so Windows-1252 text), written with our own object
3
+ //! writer. Embedded TrueType fonts, and with them every other script, come next.
4
+
5
+ mod font;
6
+ mod layout;
7
+ #[rustfmt::skip]
8
+ pub mod metrics;
9
+ mod objects;
10
+ #[cfg(test)]
11
+ mod tests;
12
+
13
+ use std::fmt;
14
+ use std::io::Write;
15
+
16
+ use crate::Result;
17
+ use crate::doc::{Block, Document};
18
+ use font::Face;
19
+ use layout::Layout;
20
+ use objects::{Num, PdfWriter};
21
+
22
+ const CATALOG: usize = 1;
23
+ const PAGES: usize = 2;
24
+ /// Fonts are objects 3 to 6, in [`Face::ALL`] order; the info dictionary follows.
25
+ const FIRST_FONT: usize = 3;
26
+ const INFO: usize = FIRST_FONT + Face::ALL.len();
27
+
28
+ pub fn write<W: Write>(document: &Document, out: W) -> Result<W> {
29
+ let mut layout = Layout::new(document.page_size());
30
+ for block in document.blocks() {
31
+ match block {
32
+ Block::Heading { level, text } => layout.heading(*level, text)?,
33
+ Block::Paragraph(paragraph) => layout.paragraph(paragraph)?,
34
+ Block::Table(table) => layout.table(table)?,
35
+ Block::PageBreak => layout.page_break(),
36
+ }
37
+ }
38
+ let (width, height) = layout.page_size();
39
+ let pages = layout.finish(document.has_page_numbers());
40
+
41
+ let mut pdf = PdfWriter::new(out)?;
42
+ // Each page is a page object followed by its content stream.
43
+ let page_id = |index: usize| INFO + 1 + 2 * index;
44
+ pdf.object(CATALOG, format_args!("<< /Type /Catalog /Pages {PAGES} 0 R >>"))?;
45
+ let kids = fmt::from_fn(|f| {
46
+ for index in 0..pages.len() {
47
+ write!(f, "{} 0 R ", page_id(index))?;
48
+ }
49
+ Ok(())
50
+ });
51
+ let count = pages.len();
52
+ pdf.object(PAGES, format_args!("<< /Type /Pages /Kids [{kids}] /Count {count} >>"))?;
53
+ for (id, face) in (FIRST_FONT..).zip(Face::ALL) {
54
+ let name = face.metrics().name;
55
+ pdf.object(
56
+ id,
57
+ format_args!(
58
+ "<< /Type /Font /Subtype /Type1 /BaseFont /{name} /Encoding /WinAnsiEncoding >>"
59
+ ),
60
+ )?;
61
+ }
62
+ pdf.object(INFO, format_args!("<< /Producer (imprimatur) >>"))?;
63
+
64
+ let fonts = fmt::from_fn(|f| {
65
+ for (id, face) in (FIRST_FONT..).zip(Face::ALL) {
66
+ write!(f, "/{} {id} 0 R ", face.resource())?;
67
+ }
68
+ Ok(())
69
+ });
70
+ let (width, height) = (Num(width), Num(height));
71
+ for (index, content) in pages.iter().enumerate() {
72
+ let id = page_id(index);
73
+ pdf.object(
74
+ id,
75
+ format_args!(
76
+ "<< /Type /Page /Parent {PAGES} 0 R /MediaBox [0 0 {width} {height}] \
77
+ /Resources << /Font << {fonts}>> >> /Contents {} 0 R >>",
78
+ id + 1
79
+ ),
80
+ )?;
81
+ pdf.stream(id + 1, content)?;
82
+ }
83
+ pdf.finish(CATALOG, INFO)
84
+ }
@@ -0,0 +1,140 @@
1
+ //! The limits a read runs under, and the running tally against them.
2
+
3
+ use std::cell::Cell;
4
+ use std::sync::Arc;
5
+ use std::sync::atomic::{AtomicBool, Ordering};
6
+ use std::time::{Duration, Instant};
7
+
8
+ use crate::{Error, Result};
9
+
10
+ const MIB: u64 = 1024 * 1024;
11
+ /// Time and cancellation are checked once per this many ticks.
12
+ const TICKS_PER_CHECK: u32 = 4096;
13
+
14
+ /// What a read may consume. Documents are untrusted input: a zip bomb, a
15
+ /// PDF whose objects refer to each other forever, or a part nested a
16
+ /// million deep must fail fast, not take the process down.
17
+ ///
18
+ /// ```
19
+ /// use std::time::Duration;
20
+ /// use imprimatur::read::Limits;
21
+ ///
22
+ /// let limits = Limits::default().max_inflated(64 * 1024 * 1024).timeout(Some(Duration::from_secs(5)));
23
+ /// ```
24
+ #[derive(Debug, Clone)]
25
+ pub struct Limits {
26
+ max_input: usize,
27
+ max_inflated: u64,
28
+ max_depth: usize,
29
+ timeout: Option<Duration>,
30
+ cancel: Option<Arc<AtomicBool>>,
31
+ }
32
+
33
+ impl Default for Limits {
34
+ fn default() -> Self {
35
+ Self {
36
+ max_input: 512 * 1024 * 1024,
37
+ max_inflated: 1024 * MIB,
38
+ max_depth: 256,
39
+ timeout: Some(Duration::from_mins(1)),
40
+ cancel: None,
41
+ }
42
+ }
43
+ }
44
+
45
+ impl Limits {
46
+ /// The largest document accepted, in bytes; 512 MiB by default.
47
+ #[must_use]
48
+ pub const fn max_input(mut self, bytes: usize) -> Self {
49
+ self.max_input = bytes;
50
+ self
51
+ }
52
+
53
+ /// The most bytes decompression may produce in total, across zip
54
+ /// entries and PDF streams; 1 GiB by default.
55
+ #[must_use]
56
+ pub const fn max_inflated(mut self, bytes: u64) -> Self {
57
+ self.max_inflated = bytes;
58
+ self
59
+ }
60
+
61
+ /// How deep XML elements, PDF objects and nested PDF forms may go; 256
62
+ /// by default.
63
+ #[must_use]
64
+ pub const fn max_depth(mut self, depth: usize) -> Self {
65
+ self.max_depth = depth;
66
+ self
67
+ }
68
+
69
+ /// Wall-clock time a read may take, one minute by default; `None` for none.
70
+ #[must_use]
71
+ pub const fn timeout(mut self, timeout: Option<Duration>) -> Self {
72
+ self.timeout = timeout;
73
+ self
74
+ }
75
+
76
+ /// A flag another thread sets to stop the read with [`Error::Cancelled`].
77
+ #[must_use]
78
+ pub fn cancel_on(mut self, flag: Arc<AtomicBool>) -> Self {
79
+ self.cancel = Some(flag);
80
+ self
81
+ }
82
+ }
83
+
84
+ /// One read's consumption. Single-threaded: a read runs on one thread.
85
+ pub struct Budget<'a> {
86
+ limits: &'a Limits,
87
+ deadline: Option<Instant>,
88
+ inflated: Cell<u64>,
89
+ ticks: Cell<u32>,
90
+ }
91
+
92
+ impl<'a> Budget<'a> {
93
+ pub fn new(limits: &'a Limits, input: usize) -> Result<Self> {
94
+ (input <= limits.max_input).ok_or(Error::LimitExceeded("input size"))?;
95
+ let deadline = limits.timeout.map(|timeout| Instant::now() + timeout);
96
+ Ok(Self { limits, deadline, inflated: Cell::new(0), ticks: Cell::new(0) })
97
+ }
98
+
99
+ pub const fn max_depth(&self) -> usize {
100
+ self.limits.max_depth
101
+ }
102
+
103
+ /// Decompressed bytes still allowed.
104
+ pub fn inflatable(&self) -> usize {
105
+ let left = self.limits.max_inflated.saturating_sub(self.inflated.get());
106
+ usize::try_from(left).unwrap_or(usize::MAX)
107
+ }
108
+
109
+ /// Records `bytes` more of decompressed output.
110
+ pub fn inflate(&self, bytes: usize) -> Result<()> {
111
+ let total = self.inflated.get().saturating_add(u64::try_from(bytes).unwrap_or(u64::MAX));
112
+ (total <= self.limits.max_inflated).ok_or(Error::LimitExceeded("decompressed size"))?;
113
+ self.inflated.set(total);
114
+ Ok(())
115
+ }
116
+
117
+ /// Counts one unit of work; every few thousand, checks the clock and
118
+ /// the cancel flag.
119
+ pub fn tick(&self) -> Result<()> {
120
+ let ticks = self.ticks.get().wrapping_add(1);
121
+ self.ticks.set(ticks);
122
+ if ticks.is_multiple_of(TICKS_PER_CHECK) { self.check() } else { Ok(()) }
123
+ }
124
+
125
+ /// Checks the clock and the cancel flag now.
126
+ pub fn check(&self) -> Result<()> {
127
+ if self.limits.cancel.as_deref().is_some_and(|flag| flag.load(Ordering::Relaxed)) {
128
+ return Err(Error::Cancelled);
129
+ }
130
+ if self.deadline.is_some_and(|deadline| Instant::now() >= deadline) {
131
+ return Err(Error::LimitExceeded("time"));
132
+ }
133
+ Ok(())
134
+ }
135
+
136
+ /// Fails beyond the depth limit.
137
+ pub fn nest(&self, depth: usize) -> Result<()> {
138
+ (depth <= self.limits.max_depth).ok_or(Error::LimitExceeded("nesting depth"))
139
+ }
140
+ }
@@ -0,0 +1,266 @@
1
+ //! What a read finds, before it is printed: pages of blocks for a PDF,
2
+ //! sheets of cells for a workbook, blocks for a Word document. Markdown is
3
+ //! one rendering of it ([`Content::to_markdown`]); search and JSON output
4
+ //! work on it directly.
5
+
6
+ #[cfg(test)]
7
+ mod tests;
8
+
9
+ use std::ops::Range;
10
+ use std::sync::Arc;
11
+
12
+ use super::span;
13
+ use crate::xlsx::push_cell_reference;
14
+ use crate::xlsx::{DateTime, cell_reference};
15
+
16
+ /// A document's content, by format.
17
+ #[derive(Debug, Clone, PartialEq)]
18
+ #[non_exhaustive]
19
+ pub enum Content {
20
+ Pdf(Vec<Page>),
21
+ Xlsx(Vec<Sheet>),
22
+ Docx(Vec<Block>),
23
+ }
24
+
25
+ impl Content {
26
+ /// The document's format.
27
+ #[must_use]
28
+ pub const fn format(&self) -> super::Format {
29
+ match self {
30
+ Self::Pdf(_) => super::Format::Pdf,
31
+ Self::Xlsx(_) => super::Format::Xlsx,
32
+ Self::Docx(_) => super::Format::Docx,
33
+ }
34
+ }
35
+ }
36
+
37
+ /// A PDF page: its blocks in reading order, and how many images it draws.
38
+ #[derive(Debug, Clone, PartialEq)]
39
+ pub struct Page {
40
+ /// From 1.
41
+ pub number: u32,
42
+ pub blocks: Vec<Placed>,
43
+ pub images: u32,
44
+ }
45
+
46
+ /// A block and where it lies on its page.
47
+ #[derive(Debug, Clone, PartialEq)]
48
+ pub struct Placed {
49
+ pub block: Block,
50
+ /// `None` for text that does not run left to right across the page.
51
+ pub bounds: Option<Bounds>,
52
+ }
53
+
54
+ /// A rectangle in points from the top-left corner of the page as shown
55
+ /// (its rotation applied).
56
+ #[derive(Debug, Clone, Copy, PartialEq)]
57
+ pub struct Bounds {
58
+ pub left: f64,
59
+ pub top: f64,
60
+ pub right: f64,
61
+ pub bottom: f64,
62
+ }
63
+
64
+ /// A unit of a document's flow.
65
+ #[derive(Debug, Clone, PartialEq)]
66
+ #[non_exhaustive]
67
+ pub enum Block {
68
+ /// Level 1 is the largest.
69
+ Heading {
70
+ level: u8,
71
+ text: Text,
72
+ },
73
+ /// Its lines separated by `\n`.
74
+ Paragraph {
75
+ text: Text,
76
+ },
77
+ /// `level` 0 is the outermost list.
78
+ ListItem {
79
+ level: u8,
80
+ ordered: bool,
81
+ text: Text,
82
+ },
83
+ Table(Grid),
84
+ }
85
+
86
+ impl Block {
87
+ /// The block's text: a table's cells, each on a line.
88
+ #[must_use]
89
+ pub fn plain_text(&self) -> std::borrow::Cow<'_, str> {
90
+ match self {
91
+ Self::Heading { text, .. } | Self::Paragraph { text } | Self::ListItem { text, .. } => {
92
+ std::borrow::Cow::Borrowed(&text.text)
93
+ }
94
+ Self::Table(grid) => {
95
+ let mut joined = String::new();
96
+ for cell in grid.cells() {
97
+ if !joined.is_empty() {
98
+ joined.push('\n');
99
+ }
100
+ joined.push_str(grid.text(cell));
101
+ }
102
+ std::borrow::Cow::Owned(joined)
103
+ }
104
+ }
105
+ }
106
+ }
107
+
108
+ /// Text, and the stretches of it that are styled.
109
+ #[derive(Debug, Clone, Default, PartialEq, Eq)]
110
+ pub struct Text {
111
+ pub text: String,
112
+ /// Byte ranges of `text`, in order and apart. Readers leave whitespace
113
+ /// at a stretch's edges out of it.
114
+ pub marks: Vec<Mark>,
115
+ }
116
+
117
+ impl Text {
118
+ pub fn plain(text: impl Into<String>) -> Self {
119
+ Self { text: text.into(), marks: Vec::new() }
120
+ }
121
+ }
122
+
123
+ /// A styled stretch of text.
124
+ #[derive(Debug, Clone, PartialEq, Eq)]
125
+ pub struct Mark {
126
+ pub range: Range<usize>,
127
+ pub bold: bool,
128
+ pub italic: bool,
129
+ pub link: Option<String>,
130
+ }
131
+
132
+ /// A workbook sheet: its cells, then its comments.
133
+ #[derive(Debug, Clone, PartialEq)]
134
+ pub struct Sheet {
135
+ pub name: String,
136
+ pub hidden: bool,
137
+ pub cells: Grid,
138
+ pub comments: Vec<Note>,
139
+ }
140
+
141
+ /// A comment on a cell.
142
+ #[derive(Debug, Clone, PartialEq, Eq)]
143
+ pub struct Note {
144
+ /// The cell's position, from 0.
145
+ pub row: u32,
146
+ pub column: u32,
147
+ /// Shared by the notes of one author.
148
+ pub author: Option<Arc<str>>,
149
+ pub text: String,
150
+ }
151
+
152
+ impl Note {
153
+ /// The cell's A1 reference: `B2`.
154
+ #[must_use]
155
+ pub fn reference(&self) -> String {
156
+ reference(self.row, self.column)
157
+ }
158
+ }
159
+
160
+ /// Appends the A1 reference of a sheet's cell to `out`.
161
+ pub fn push_reference(out: &mut String, row: u32, column: u32) {
162
+ push_cell_reference(out, row, u16::try_from(column).unwrap_or(u16::MAX));
163
+ }
164
+
165
+ fn reference(row: u32, column: u32) -> String {
166
+ cell_reference(row, u16::try_from(column).unwrap_or(u16::MAX))
167
+ }
168
+
169
+ /// What a cell holds. Its text, as shown, is the grid's
170
+ /// ([`Grid::text`]); the variants carry what the text does not.
171
+ #[derive(Debug, Clone, Copy, PartialEq)]
172
+ #[non_exhaustive]
173
+ pub enum Value {
174
+ Text,
175
+ Number(f64),
176
+ Bool(bool),
177
+ /// A number formatted as a date or a date and time.
178
+ Date(DateTime),
179
+ /// An error such as `#N/A`.
180
+ Error,
181
+ /// A formula without a computed value: its text is `=FORMULA`.
182
+ Formula,
183
+ }
184
+
185
+ /// A cell of a grid: its position, from 0, and its value.
186
+ #[derive(Debug, Clone, PartialEq)]
187
+ pub struct Cell {
188
+ pub row: u32,
189
+ pub column: u32,
190
+ pub value: Value,
191
+ text: Range<u32>,
192
+ }
193
+
194
+ impl Cell {
195
+ /// The cell's A1 reference, which names it in a sheet: `B2`.
196
+ #[must_use]
197
+ pub fn reference(&self) -> String {
198
+ reference(self.row, self.column)
199
+ }
200
+ }
201
+
202
+ /// Cells by row and column, sparse and in row-major order. Their text
203
+ /// shares one buffer.
204
+ #[derive(Debug, Clone, Default, PartialEq)]
205
+ pub struct Grid {
206
+ text: String,
207
+ cells: Vec<Cell>,
208
+ open: usize,
209
+ }
210
+
211
+ impl Grid {
212
+ #[must_use]
213
+ pub fn cells(&self) -> &[Cell] {
214
+ &self.cells
215
+ }
216
+
217
+ /// The text of a cell of this grid, as shown.
218
+ #[must_use]
219
+ pub fn text(&self, cell: &Cell) -> &str {
220
+ span::slice(&self.text, &cell.text)
221
+ }
222
+
223
+ /// The cells row by row; rows without cells are left out.
224
+ pub fn rows(&self) -> impl Iterator<Item = &[Cell]> {
225
+ self.cells.chunk_by(|a, b| a.row == b.row)
226
+ }
227
+
228
+ #[must_use]
229
+ pub const fn is_empty(&self) -> bool {
230
+ self.cells.is_empty()
231
+ }
232
+
233
+ /// The buffer to write the next cell's text into, then
234
+ /// [`end_cell`](Self::end_cell) it.
235
+ pub(crate) const fn buffer(&mut self) -> &mut String {
236
+ self.open = self.text.len();
237
+ &mut self.text
238
+ }
239
+
240
+ /// Ends the cell written since [`buffer`](Self::buffer); a cell with no
241
+ /// text is dropped.
242
+ pub(crate) fn end_cell(&mut self, row: u32, column: u32, value: Value) {
243
+ if self.text[self.open..].trim().is_empty() {
244
+ self.text.truncate(self.open);
245
+ } else {
246
+ let text = span::end(&self.text[..self.open])..span::end(&self.text);
247
+ self.cells.push(Cell { row, column, value, text });
248
+ }
249
+ }
250
+
251
+ #[cfg(test)]
252
+ pub(crate) fn push(&mut self, row: u32, column: u32, text: &str) {
253
+ self.buffer().push_str(text);
254
+ self.end_cell(row, column, Value::Text);
255
+ }
256
+
257
+ /// Sorts cells written out of order; of two at one position, the first
258
+ /// written stays.
259
+ pub(crate) fn finish(mut self) -> Self {
260
+ if !self.cells.is_sorted_by_key(|cell| (cell.row, cell.column)) {
261
+ self.cells.sort_by_key(|cell| (cell.row, cell.column));
262
+ }
263
+ self.cells.dedup_by_key(|cell| (cell.row, cell.column));
264
+ self
265
+ }
266
+ }