imprimatur 0.1.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +31 -0
- data/Cargo.lock +124 -3
- data/Cargo.toml +1 -1
- data/README.md +123 -23
- data/ext/imprimatur_native/cli/Cargo.toml +2 -2
- data/ext/imprimatur_native/cli/src/cli/grep.rs +1 -1
- data/ext/imprimatur_native/cli/src/cli/markdown.rs +2 -2
- data/ext/imprimatur_native/cli/src/cli.rs +5 -5
- data/ext/imprimatur_native/cli/src/main.rs +1 -1
- data/ext/imprimatur_native/core/Cargo.toml +12 -2
- data/ext/imprimatur_native/core/README.md +15 -1
- data/ext/imprimatur_native/core/src/archive.rs +279 -0
- data/ext/imprimatur_native/core/src/doc/paragraph.rs +10 -3
- data/ext/imprimatur_native/core/src/doc/picture.rs +192 -0
- data/ext/imprimatur_native/core/src/doc/table.rs +21 -0
- data/ext/imprimatur_native/core/src/doc.rs +86 -2
- data/ext/imprimatur_native/core/src/docx/picture.rs +72 -0
- data/ext/imprimatur_native/core/src/docx.rs +91 -28
- data/ext/imprimatur_native/core/src/error.rs +13 -1
- data/ext/imprimatur_native/core/src/font.rs +229 -0
- data/ext/imprimatur_native/core/src/lib.rs +5 -0
- data/ext/imprimatur_native/core/src/pdf/layout.rs +38 -15
- data/ext/imprimatur_native/core/src/pdf/objects.rs +29 -5
- data/ext/imprimatur_native/core/src/pdf/picture.rs +197 -0
- data/ext/imprimatur_native/core/src/pdf.rs +31 -3
- data/ext/imprimatur_native/core/src/read/content.rs +91 -3
- data/ext/imprimatur_native/core/src/read/json.rs +96 -7
- data/ext/imprimatur_native/core/src/read/markdown.rs +1 -1
- data/ext/imprimatur_native/core/src/read/{xlsx/numbers.rs → numbers.rs} +69 -45
- data/ext/imprimatur_native/core/src/read/ods.rs +356 -0
- data/ext/imprimatur_native/core/src/read/ole.rs +244 -0
- data/ext/imprimatur_native/core/src/read/package.rs +0 -5
- data/ext/imprimatur_native/core/src/read/pdf/layout/bidi.rs +212 -0
- data/ext/imprimatur_native/core/src/read/pdf/layout.rs +25 -119
- data/ext/imprimatur_native/core/src/read/xls/strings.rs +156 -0
- data/ext/imprimatur_native/core/src/read/xls.rs +342 -0
- data/ext/imprimatur_native/core/src/read/xlsx/formats.rs +155 -0
- data/ext/imprimatur_native/core/src/read/xlsx.rs +104 -20
- data/ext/imprimatur_native/core/src/read.rs +68 -21
- data/ext/imprimatur_native/core/src/script.rs +116 -0
- data/ext/imprimatur_native/core/src/style.rs +21 -0
- data/ext/imprimatur_native/core/src/test_support.rs +90 -2
- data/ext/imprimatur_native/core/src/xlsx/autofit.rs +60 -0
- data/ext/imprimatur_native/core/src/xlsx/date.rs +7 -1
- data/ext/imprimatur_native/core/src/xlsx/format.rs +22 -6
- data/ext/imprimatur_native/core/src/xlsx/package.rs +22 -8
- data/ext/imprimatur_native/core/src/xlsx/styles.rs +9 -8
- data/ext/imprimatur_native/core/src/xlsx/worksheet.rs +35 -7
- data/ext/imprimatur_native/core/src/xlsx.rs +40 -3
- data/ext/imprimatur_native/core/src/xml/reader.rs +10 -0
- data/ext/imprimatur_native/ffi/Cargo.toml +3 -2
- data/ext/imprimatur_native/ffi/src/archive.rs +119 -0
- data/ext/imprimatur_native/ffi/src/blocking.rs +63 -0
- data/ext/imprimatur_native/ffi/src/document.rs +48 -1
- data/ext/imprimatur_native/ffi/src/lib.rs +52 -1
- data/ext/imprimatur_native/ffi/src/read.rs +9 -14
- data/ext/imprimatur_native/ffi/src/sheets.rs +114 -0
- data/lib/imprimatur/document.rb +30 -6
- data/lib/imprimatur/error.rb +4 -3
- data/lib/imprimatur/reader.rb +3 -3
- data/lib/imprimatur/spreadsheet.rb +151 -0
- data/lib/imprimatur/version.rb +1 -1
- data/lib/imprimatur/xlsx.rb +25 -4
- data/lib/imprimatur/zip.rb +127 -0
- data/lib/imprimatur.rb +18 -5
- metadata +23 -4
|
@@ -8,13 +8,11 @@ use std::fmt::{self, Arguments, Write as _};
|
|
|
8
8
|
|
|
9
9
|
use super::font::Face;
|
|
10
10
|
use super::objects::{Num, push_string};
|
|
11
|
-
use crate::doc::{
|
|
11
|
+
use crate::doc::{Document, Paragraph, Picture, Table};
|
|
12
12
|
use crate::style::{Align, Color};
|
|
13
13
|
use crate::{Error, Result};
|
|
14
14
|
use wrap::{Line, Look, wrap};
|
|
15
15
|
|
|
16
|
-
/// One inch on every side.
|
|
17
|
-
const MARGIN: f64 = 72.0;
|
|
18
16
|
const BODY_SIZE: f64 = 11.0;
|
|
19
17
|
const TABLE_SIZE: f64 = 10.0;
|
|
20
18
|
/// Line height as a multiple of the font size.
|
|
@@ -32,6 +30,7 @@ const HEADINGS: [(f64, f64, Option<Color>); 3] =
|
|
|
32
30
|
pub struct Layout {
|
|
33
31
|
width: f64,
|
|
34
32
|
height: f64,
|
|
33
|
+
margin: f64,
|
|
35
34
|
pages: Vec<Vec<u8>>,
|
|
36
35
|
/// The top of the free space on the current page, from the bottom edge.
|
|
37
36
|
y: f64,
|
|
@@ -39,9 +38,15 @@ pub struct Layout {
|
|
|
39
38
|
}
|
|
40
39
|
|
|
41
40
|
impl Layout {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
/// The document's pages, between its margins.
|
|
42
|
+
///
|
|
43
|
+
/// # Errors
|
|
44
|
+
/// [`Error::InvalidMargin`] when the margins leave no room.
|
|
45
|
+
pub fn new(document: &Document) -> Result<Self> {
|
|
46
|
+
document.room()?;
|
|
47
|
+
let (width, height) = document.page_size().points();
|
|
48
|
+
let margin = document.margin();
|
|
49
|
+
Ok(Self { width, height, margin, pages: vec![Vec::new()], y: height - margin, blank: true })
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
pub const fn page_size(&self) -> (f64, f64) {
|
|
@@ -49,7 +54,7 @@ impl Layout {
|
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
const fn content_width(&self) -> f64 {
|
|
52
|
-
(-2.0f64).mul_add(
|
|
57
|
+
(-2.0f64).mul_add(self.margin, self.width)
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
pub fn heading(&mut self, level: u8, text: &str) -> Result<()> {
|
|
@@ -105,7 +110,7 @@ impl Layout {
|
|
|
105
110
|
}
|
|
106
111
|
|
|
107
112
|
let header_height = rows.first().filter(|row| row.2).map_or(0.0, |row| row.1);
|
|
108
|
-
let room = (-2.0f64).mul_add(
|
|
113
|
+
let room = (-2.0f64).mul_add(self.margin, self.height) - header_height;
|
|
109
114
|
for (index, (cells, height, header)) in rows.iter().enumerate() {
|
|
110
115
|
(*height <= room || *header).ok_or(Error::RowTooTall(*height))?;
|
|
111
116
|
// A header needs the first body row beside it.
|
|
@@ -121,6 +126,25 @@ impl Layout {
|
|
|
121
126
|
Ok(())
|
|
122
127
|
}
|
|
123
128
|
|
|
129
|
+
/// Draws picture `index` (`/Im{index}`), on a new page when it does not
|
|
130
|
+
/// fit, as its alignment says.
|
|
131
|
+
pub fn picture(&mut self, picture: &Picture, index: usize) {
|
|
132
|
+
let room = (self.content_width(), (-2.0f64).mul_add(self.margin, self.height));
|
|
133
|
+
let (width, height) = picture.size(room);
|
|
134
|
+
self.ensure_room(height);
|
|
135
|
+
let x = self.margin
|
|
136
|
+
+ match picture.align.unwrap_or(Align::Left) {
|
|
137
|
+
Align::Left => 0.0,
|
|
138
|
+
Align::Center => (room.0 - width) / 2.0,
|
|
139
|
+
Align::Right => room.0 - width,
|
|
140
|
+
};
|
|
141
|
+
self.y -= height;
|
|
142
|
+
let (width, height, x, y) = (Num(width), Num(height), Num(x), Num(self.y));
|
|
143
|
+
emit(self.current(), format_args!("q {width} 0 0 {height} {x} {y} cm /Im{index} Do Q\n"));
|
|
144
|
+
self.y -= PARAGRAPH_GAP;
|
|
145
|
+
self.blank = false;
|
|
146
|
+
}
|
|
147
|
+
|
|
124
148
|
pub fn page_break(&mut self) {
|
|
125
149
|
self.new_page();
|
|
126
150
|
}
|
|
@@ -129,14 +153,14 @@ impl Layout {
|
|
|
129
153
|
pub fn finish(mut self, page_numbers: bool) -> Vec<Vec<u8>> {
|
|
130
154
|
if page_numbers {
|
|
131
155
|
let total = self.pages.len();
|
|
132
|
-
let center = self.width / 2.0;
|
|
156
|
+
let (center, margin) = (self.width / 2.0, self.margin);
|
|
133
157
|
for (index, page) in self.pages.iter_mut().enumerate() {
|
|
134
158
|
let bytes = format!("{} / {total}", index + 1).into_bytes();
|
|
135
159
|
let width = Face::Regular.width(&bytes, FOOTER_SIZE);
|
|
136
160
|
let look =
|
|
137
161
|
Look { face: Face::Regular, color: Some(FOOTER_COLOR), underline: false };
|
|
138
162
|
let label = wrap::Fragment { bytes, look, width };
|
|
139
|
-
draw_fragment(page, &label, FOOTER_SIZE, center - width / 2.0,
|
|
163
|
+
draw_fragment(page, &label, FOOTER_SIZE, center - width / 2.0, margin / 2.0);
|
|
140
164
|
}
|
|
141
165
|
}
|
|
142
166
|
self.pages
|
|
@@ -146,7 +170,7 @@ impl Layout {
|
|
|
146
170
|
/// (or the page is still blank, where nothing would fit better).
|
|
147
171
|
/// Returns whether it did.
|
|
148
172
|
fn ensure_room(&mut self, needed: f64) -> bool {
|
|
149
|
-
let full = !self.blank && self.y - needed <
|
|
173
|
+
let full = !self.blank && self.y - needed < self.margin;
|
|
150
174
|
if full {
|
|
151
175
|
self.new_page();
|
|
152
176
|
}
|
|
@@ -155,7 +179,7 @@ impl Layout {
|
|
|
155
179
|
|
|
156
180
|
fn new_page(&mut self) {
|
|
157
181
|
self.pages.push(Vec::new());
|
|
158
|
-
self.y = self.height -
|
|
182
|
+
self.y = self.height - self.margin;
|
|
159
183
|
self.blank = true;
|
|
160
184
|
}
|
|
161
185
|
|
|
@@ -174,7 +198,7 @@ impl Layout {
|
|
|
174
198
|
self.ensure_room(leading);
|
|
175
199
|
let baseline = self.y - baseline_offset(size);
|
|
176
200
|
let slack = self.content_width() - line.width;
|
|
177
|
-
let x =
|
|
201
|
+
let x = self.margin
|
|
178
202
|
+ match align {
|
|
179
203
|
Align::Left => 0.0,
|
|
180
204
|
Align::Center => slack / 2.0,
|
|
@@ -188,9 +212,8 @@ impl Layout {
|
|
|
188
212
|
|
|
189
213
|
/// One table row from the cursor down: fill, borders, then text.
|
|
190
214
|
fn row(&mut self, cells: &[Vec<Line>], height: f64, column_width: f64, header: bool) {
|
|
191
|
-
let top = self.y;
|
|
215
|
+
let (top, mut x) = (self.y, self.margin);
|
|
192
216
|
let page = self.current();
|
|
193
|
-
let mut x = MARGIN;
|
|
194
217
|
for lines in cells {
|
|
195
218
|
let rect = format_args!(
|
|
196
219
|
"{} {} {} {} re",
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
//! PDF file syntax: numbered objects, compressed streams, the
|
|
2
2
|
//! cross-reference table and the trailer (ISO 32000-1, section 7).
|
|
3
3
|
|
|
4
|
+
#[cfg(test)]
|
|
5
|
+
mod tests;
|
|
6
|
+
|
|
4
7
|
use std::fmt;
|
|
5
8
|
use std::io::Write;
|
|
6
9
|
|
|
@@ -34,13 +37,34 @@ impl<W: Write> PdfWriter<W> {
|
|
|
34
37
|
self.write_fmt(format_args!("{dictionary}\nendobj\n"))
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
/// Writes a Flate-compressed stream object.
|
|
38
|
-
|
|
40
|
+
/// Writes a Flate-compressed stream object with optional dictionary entries.
|
|
41
|
+
/// Names omit the leading `/`; values are already PDF syntax. For example,
|
|
42
|
+
/// embedded TrueType fonts need `Length1` to hold the decoded byte count.
|
|
43
|
+
/// The writer supplies `Length` and `Filter`; entries must not repeat them.
|
|
44
|
+
pub fn stream(
|
|
45
|
+
&mut self,
|
|
46
|
+
id: usize,
|
|
47
|
+
data: &[u8],
|
|
48
|
+
entries: &[(&str, &dyn fmt::Display)],
|
|
49
|
+
) -> Result<()> {
|
|
39
50
|
let compressed = miniz_oxide::deflate::compress_to_vec_zlib(data, LEVEL);
|
|
51
|
+
let dictionary = fmt::from_fn(|f| {
|
|
52
|
+
f.write_str("/Filter /FlateDecode")?;
|
|
53
|
+
for (name, value) in entries {
|
|
54
|
+
write!(f, " /{name} {value}")?;
|
|
55
|
+
}
|
|
56
|
+
Ok(())
|
|
57
|
+
});
|
|
58
|
+
self.stored(id, format_args!("{dictionary}"), &compressed)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Writes a stream object as it is stored, `dictionary` following its
|
|
62
|
+
/// `/Length`.
|
|
63
|
+
pub fn stored(&mut self, id: usize, dictionary: fmt::Arguments<'_>, data: &[u8]) -> Result<()> {
|
|
40
64
|
self.begin(id)?;
|
|
41
|
-
let length =
|
|
42
|
-
self.write_fmt(format_args!("<< /Length {length}
|
|
43
|
-
self.write(
|
|
65
|
+
let length = data.len();
|
|
66
|
+
self.write_fmt(format_args!("<< /Length {length} {dictionary} >>\nstream\n"))?;
|
|
67
|
+
self.write(data)?;
|
|
44
68
|
self.write(b"\nendstream\nendobj\n")
|
|
45
69
|
}
|
|
46
70
|
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
//! Pictures as image `XObject`s: a JPEG as it is; a PNG's compressed pixels
|
|
2
|
+
//! as they are, PDF reading them with the PNG predictor, unless an alpha
|
|
3
|
+
//! channel needs them decoded into a soft mask.
|
|
4
|
+
|
|
5
|
+
use std::borrow::Cow;
|
|
6
|
+
use std::fmt::Write as _;
|
|
7
|
+
|
|
8
|
+
use crate::doc::Picture;
|
|
9
|
+
use crate::doc::picture::{Kind, Png};
|
|
10
|
+
use crate::{Error, Result};
|
|
11
|
+
|
|
12
|
+
/// The most pixels a PNG with alpha may have: it is decoded in memory.
|
|
13
|
+
const MAX_DECODED: u64 = 1 << 26;
|
|
14
|
+
|
|
15
|
+
/// An image stream: its dictionary after `/Length`, and its data as stored.
|
|
16
|
+
pub struct Stream<'a> {
|
|
17
|
+
pub dictionary: String,
|
|
18
|
+
pub data: Cow<'a, [u8]>,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/// Whether the picture has a soft mask, written right after it.
|
|
22
|
+
pub const fn has_mask(picture: &Picture) -> bool {
|
|
23
|
+
match &picture.kind {
|
|
24
|
+
Kind::Png(png) => alpha(png),
|
|
25
|
+
Kind::Jpeg { .. } => false,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// The image, then its soft mask when it has one; `mask` is the mask's
|
|
30
|
+
/// object number.
|
|
31
|
+
pub fn streams(picture: &Picture, mask: usize) -> Result<Vec<Stream<'_>>> {
|
|
32
|
+
let (width, height) = picture.pixels;
|
|
33
|
+
let head = format!("/Type /XObject /Subtype /Image /Width {width} /Height {height}");
|
|
34
|
+
match &picture.kind {
|
|
35
|
+
Kind::Jpeg { components, inverted } => {
|
|
36
|
+
let space = ["DeviceGray", "", "DeviceRGB", "DeviceCMYK"][usize::from(*components) - 1];
|
|
37
|
+
let decode = if *inverted { " /Decode [1 0 1 0 1 0 1 0]" } else { "" };
|
|
38
|
+
let dictionary = format!(
|
|
39
|
+
"{head} /ColorSpace /{space} /BitsPerComponent 8 /Filter /DCTDecode{decode}"
|
|
40
|
+
);
|
|
41
|
+
Ok(vec![Stream { dictionary, data: Cow::Borrowed(&picture.data) }])
|
|
42
|
+
}
|
|
43
|
+
Kind::Png(png) if alpha(png) => decoded(png, (width, height), &head, mask),
|
|
44
|
+
Kind::Png(png) => Ok(vec![predicted(png, width, &head)]),
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// Alpha that only a soft mask carries: an alpha channel, or a palette's.
|
|
49
|
+
const fn alpha(png: &Png) -> bool {
|
|
50
|
+
matches!(png.color, 4 | 6) || (png.color == 3 && !png.transparency.is_empty())
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// The compressed pixels as stored, read through the PNG predictor; a
|
|
54
|
+
/// transparent color becomes a color key mask.
|
|
55
|
+
fn predicted<'a>(png: &'a Png, width: u32, head: &str) -> Stream<'a> {
|
|
56
|
+
let depth = png.depth;
|
|
57
|
+
let (colors, space) = match png.color {
|
|
58
|
+
0 => (1, "/DeviceGray".to_owned()),
|
|
59
|
+
2 => (3, "/DeviceRGB".to_owned()),
|
|
60
|
+
_ => (1, indexed(&png.palette)),
|
|
61
|
+
};
|
|
62
|
+
let mut dictionary = format!(
|
|
63
|
+
"{head} /ColorSpace {space} /BitsPerComponent {depth} /Filter /FlateDecode \
|
|
64
|
+
/DecodeParms << /Predictor 15 /Colors {colors} /BitsPerComponent {depth} \
|
|
65
|
+
/Columns {width} >>"
|
|
66
|
+
);
|
|
67
|
+
if !png.transparency.is_empty() {
|
|
68
|
+
dictionary.push_str(" /Mask [");
|
|
69
|
+
for key in png.transparency.as_chunks::<2>().0 {
|
|
70
|
+
let value = u16::from_be_bytes(*key);
|
|
71
|
+
// Writing to a String cannot fail.
|
|
72
|
+
let _ = write!(dictionary, "{value} {value} ");
|
|
73
|
+
}
|
|
74
|
+
dictionary.push(']');
|
|
75
|
+
}
|
|
76
|
+
Stream { dictionary, data: Cow::Borrowed(&png.compressed) }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// `[/Indexed /DeviceRGB n <palette>]`.
|
|
80
|
+
fn indexed(palette: &[u8]) -> String {
|
|
81
|
+
let mut space = format!("[/Indexed /DeviceRGB {} <", palette.len() / 3 - 1);
|
|
82
|
+
for byte in palette {
|
|
83
|
+
// Writing to a String cannot fail.
|
|
84
|
+
let _ = write!(space, "{byte:02X}");
|
|
85
|
+
}
|
|
86
|
+
space.push_str(">]");
|
|
87
|
+
space
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// The pixels decoded into 8-bit color and alpha, both compressed again.
|
|
91
|
+
fn decoded<'a>(
|
|
92
|
+
png: &Png,
|
|
93
|
+
(width, height): (u32, u32),
|
|
94
|
+
head: &str,
|
|
95
|
+
mask: usize,
|
|
96
|
+
) -> Result<Vec<Stream<'a>>> {
|
|
97
|
+
let damaged = || Error::InvalidImage("a damaged PNG");
|
|
98
|
+
(u64::from(width) * u64::from(height) <= MAX_DECODED)
|
|
99
|
+
.ok_or(Error::InvalidImage("a PNG with alpha of at most 67 million pixels"))?;
|
|
100
|
+
let channels = match png.color {
|
|
101
|
+
4 => 2,
|
|
102
|
+
6 => 4,
|
|
103
|
+
_ => 1,
|
|
104
|
+
};
|
|
105
|
+
let bits = channels * usize::from(png.depth);
|
|
106
|
+
let (width, height) = (width as usize, height as usize);
|
|
107
|
+
let stride = (width * bits).div_ceil(8);
|
|
108
|
+
let filtered = miniz_oxide::inflate::decompress_to_vec_zlib_with_limit(
|
|
109
|
+
&png.compressed,
|
|
110
|
+
height * (stride + 1),
|
|
111
|
+
)
|
|
112
|
+
.map_err(|_| damaged())?;
|
|
113
|
+
let pixels = unfilter(&filtered, stride, bits.div_ceil(8), height).ok_or_else(damaged)?;
|
|
114
|
+
let gray = png.color == 4;
|
|
115
|
+
let (mut color, mut alpha) =
|
|
116
|
+
(Vec::with_capacity(width * height * 3), Vec::with_capacity(width * height));
|
|
117
|
+
for row in pixels.chunks_exact(stride) {
|
|
118
|
+
for x in 0..width {
|
|
119
|
+
if png.color == 3 {
|
|
120
|
+
let index = usize::from(sample(row, x, png.depth));
|
|
121
|
+
let rgb = png.palette.get(index * 3..index * 3 + 3).ok_or_else(damaged)?;
|
|
122
|
+
color.extend_from_slice(rgb);
|
|
123
|
+
alpha.push(png.transparency.get(index).copied().unwrap_or(u8::MAX));
|
|
124
|
+
} else {
|
|
125
|
+
// 16-bit samples keep their high byte.
|
|
126
|
+
let step = usize::from(png.depth / 8);
|
|
127
|
+
let at = |channel: usize| row[(x * channels + channel) * step];
|
|
128
|
+
color.extend((0..channels - 1).map(at));
|
|
129
|
+
alpha.push(at(channels - 1));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
let space = if gray { "DeviceGray" } else { "DeviceRGB" };
|
|
134
|
+
let compress = |data: &[u8]| Cow::Owned(miniz_oxide::deflate::compress_to_vec_zlib(data, 6));
|
|
135
|
+
Ok(vec![
|
|
136
|
+
Stream {
|
|
137
|
+
dictionary: format!(
|
|
138
|
+
"{head} /ColorSpace /{space} /BitsPerComponent 8 /Filter /FlateDecode \
|
|
139
|
+
/SMask {mask} 0 R"
|
|
140
|
+
),
|
|
141
|
+
data: compress(&color),
|
|
142
|
+
},
|
|
143
|
+
Stream {
|
|
144
|
+
dictionary: format!(
|
|
145
|
+
"{head} /ColorSpace /DeviceGray /BitsPerComponent 8 /Filter /FlateDecode"
|
|
146
|
+
),
|
|
147
|
+
data: compress(&alpha),
|
|
148
|
+
},
|
|
149
|
+
])
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// The `x`th sample of a row of `depth`-bit samples.
|
|
153
|
+
fn sample(row: &[u8], x: usize, depth: u8) -> u8 {
|
|
154
|
+
let depth = usize::from(depth);
|
|
155
|
+
let (byte, shift) = (x * depth / 8, 8 - depth - x * depth % 8);
|
|
156
|
+
(row[byte] >> shift) & (u8::MAX >> (8 - depth))
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/// PNG's filters undone (ISO/IEC 15948, 9): rows of `stride` bytes, `bpp`
|
|
160
|
+
/// bytes a pixel.
|
|
161
|
+
fn unfilter(data: &[u8], stride: usize, bpp: usize, rows: usize) -> Option<Vec<u8>> {
|
|
162
|
+
let mut out = vec![0_u8; stride * rows];
|
|
163
|
+
for row in 0..rows {
|
|
164
|
+
let line = data.get(row * (stride + 1)..(row + 1) * (stride + 1))?;
|
|
165
|
+
let (done, rest) = out.split_at_mut(row * stride);
|
|
166
|
+
let previous = row.checked_sub(1).map(|_| &done[done.len() - stride..]);
|
|
167
|
+
let current = &mut rest[..stride];
|
|
168
|
+
for at in 0..stride {
|
|
169
|
+
let left = at.checked_sub(bpp).map_or(0, |at| current[at]);
|
|
170
|
+
let up = previous.map_or(0, |previous| previous[at]);
|
|
171
|
+
let up_left = at.checked_sub(bpp).and_then(|at| previous.map(|previous| previous[at]));
|
|
172
|
+
let predicted = match line[0] {
|
|
173
|
+
0 => 0,
|
|
174
|
+
1 => left,
|
|
175
|
+
2 => up,
|
|
176
|
+
3 => left.midpoint(up),
|
|
177
|
+
4 => paeth(left, up, up_left.unwrap_or(0)),
|
|
178
|
+
_ => return None,
|
|
179
|
+
};
|
|
180
|
+
current[at] = line[at + 1].wrapping_add(predicted);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
Some(out)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/// The neighbor nearest `left + up - up_left`.
|
|
187
|
+
fn paeth(left: u8, up: u8, up_left: u8) -> u8 {
|
|
188
|
+
let estimate = i16::from(left) + i16::from(up) - i16::from(up_left);
|
|
189
|
+
let distance = |byte: u8| (estimate - i16::from(byte)).abs();
|
|
190
|
+
if distance(left) <= distance(up) && distance(left) <= distance(up_left) {
|
|
191
|
+
left
|
|
192
|
+
} else if distance(up) <= distance(up_left) {
|
|
193
|
+
up
|
|
194
|
+
} else {
|
|
195
|
+
up_left
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -7,6 +7,7 @@ mod layout;
|
|
|
7
7
|
#[rustfmt::skip]
|
|
8
8
|
pub mod metrics;
|
|
9
9
|
mod objects;
|
|
10
|
+
mod picture;
|
|
10
11
|
#[cfg(test)]
|
|
11
12
|
mod tests;
|
|
12
13
|
|
|
@@ -26,12 +27,17 @@ const FIRST_FONT: usize = 3;
|
|
|
26
27
|
const INFO: usize = FIRST_FONT + Face::ALL.len();
|
|
27
28
|
|
|
28
29
|
pub fn write<W: Write>(document: &Document, out: W) -> Result<W> {
|
|
29
|
-
let mut layout = Layout::new(document
|
|
30
|
+
let mut layout = Layout::new(document)?;
|
|
31
|
+
let mut pictures = Vec::new();
|
|
30
32
|
for block in document.blocks() {
|
|
31
33
|
match block {
|
|
32
34
|
Block::Heading { level, text } => layout.heading(*level, text)?,
|
|
33
35
|
Block::Paragraph(paragraph) => layout.paragraph(paragraph)?,
|
|
34
36
|
Block::Table(table) => layout.table(table)?,
|
|
37
|
+
Block::Picture(picture) => {
|
|
38
|
+
layout.picture(picture, pictures.len());
|
|
39
|
+
pictures.push(picture);
|
|
40
|
+
}
|
|
35
41
|
Block::PageBreak => layout.page_break(),
|
|
36
42
|
}
|
|
37
43
|
}
|
|
@@ -67,6 +73,23 @@ pub fn write<W: Write>(document: &Document, out: W) -> Result<W> {
|
|
|
67
73
|
}
|
|
68
74
|
Ok(())
|
|
69
75
|
});
|
|
76
|
+
// Pictures follow the pages: an image, then its soft mask if it has one.
|
|
77
|
+
let mut ids = Vec::with_capacity(pictures.len());
|
|
78
|
+
let mut next = page_id(pages.len());
|
|
79
|
+
for picture in &pictures {
|
|
80
|
+
ids.push(next);
|
|
81
|
+
next += 1 + usize::from(picture::has_mask(picture));
|
|
82
|
+
}
|
|
83
|
+
let images = fmt::from_fn(|f| {
|
|
84
|
+
if !ids.is_empty() {
|
|
85
|
+
f.write_str("/XObject << ")?;
|
|
86
|
+
for (index, id) in ids.iter().enumerate() {
|
|
87
|
+
write!(f, "/Im{index} {id} 0 R ")?;
|
|
88
|
+
}
|
|
89
|
+
f.write_str(">> ")?;
|
|
90
|
+
}
|
|
91
|
+
Ok(())
|
|
92
|
+
});
|
|
70
93
|
let (width, height) = (Num(width), Num(height));
|
|
71
94
|
for (index, content) in pages.iter().enumerate() {
|
|
72
95
|
let id = page_id(index);
|
|
@@ -74,11 +97,16 @@ pub fn write<W: Write>(document: &Document, out: W) -> Result<W> {
|
|
|
74
97
|
id,
|
|
75
98
|
format_args!(
|
|
76
99
|
"<< /Type /Page /Parent {PAGES} 0 R /MediaBox [0 0 {width} {height}] \
|
|
77
|
-
/Resources << /Font << {fonts}>> >> /Contents {} 0 R >>",
|
|
100
|
+
/Resources << /Font << {fonts}>> {images}>> /Contents {} 0 R >>",
|
|
78
101
|
id + 1
|
|
79
102
|
),
|
|
80
103
|
)?;
|
|
81
|
-
pdf.stream(id + 1, content)?;
|
|
104
|
+
pdf.stream(id + 1, content, &[])?;
|
|
105
|
+
}
|
|
106
|
+
for (picture, id) in pictures.into_iter().zip(ids) {
|
|
107
|
+
for (offset, stream) in (0..).zip(picture::streams(picture, id + 1)?) {
|
|
108
|
+
pdf.stored(id + offset, format_args!("{}", stream.dictionary), &stream.data)?;
|
|
109
|
+
}
|
|
82
110
|
}
|
|
83
111
|
pdf.finish(CATALOG, INFO)
|
|
84
112
|
}
|
|
@@ -13,13 +13,17 @@ use super::span;
|
|
|
13
13
|
use crate::xlsx::push_cell_reference;
|
|
14
14
|
use crate::xlsx::{DateTime, cell_reference};
|
|
15
15
|
|
|
16
|
-
/// A document's content, by format.
|
|
16
|
+
/// A document's content, by format. Every workbook format holds sheets.
|
|
17
17
|
#[derive(Debug, Clone, PartialEq)]
|
|
18
18
|
#[non_exhaustive]
|
|
19
19
|
pub enum Content {
|
|
20
20
|
Pdf(Vec<Page>),
|
|
21
21
|
Xlsx(Vec<Sheet>),
|
|
22
22
|
Docx(Vec<Block>),
|
|
23
|
+
/// An `.ods` spreadsheet.
|
|
24
|
+
Ods(Vec<Sheet>),
|
|
25
|
+
/// A legacy Excel workbook (Excel 97 to 2003).
|
|
26
|
+
Xls(Vec<Sheet>),
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
impl Content {
|
|
@@ -30,6 +34,17 @@ impl Content {
|
|
|
30
34
|
Self::Pdf(_) => super::Format::Pdf,
|
|
31
35
|
Self::Xlsx(_) => super::Format::Xlsx,
|
|
32
36
|
Self::Docx(_) => super::Format::Docx,
|
|
37
|
+
Self::Ods(_) => super::Format::Ods,
|
|
38
|
+
Self::Xls(_) => super::Format::Xls,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// A workbook's sheets, whatever its format; `None` for documents.
|
|
43
|
+
#[must_use]
|
|
44
|
+
pub fn sheets(&self) -> Option<&[Sheet]> {
|
|
45
|
+
match self {
|
|
46
|
+
Self::Xlsx(sheets) | Self::Ods(sheets) | Self::Xls(sheets) => Some(sheets),
|
|
47
|
+
Self::Pdf(_) | Self::Docx(_) => None,
|
|
33
48
|
}
|
|
34
49
|
}
|
|
35
50
|
}
|
|
@@ -129,13 +144,86 @@ pub struct Mark {
|
|
|
129
144
|
pub link: Option<String>,
|
|
130
145
|
}
|
|
131
146
|
|
|
132
|
-
/// A workbook sheet: its cells,
|
|
133
|
-
|
|
147
|
+
/// A workbook sheet: its cells and comments and, from an XLSX workbook,
|
|
148
|
+
/// its column widths, data validations and cell formats.
|
|
149
|
+
#[derive(Debug, Clone, Default, PartialEq)]
|
|
134
150
|
pub struct Sheet {
|
|
135
151
|
pub name: String,
|
|
136
152
|
pub hidden: bool,
|
|
137
153
|
pub cells: Grid,
|
|
138
154
|
pub comments: Vec<Note>,
|
|
155
|
+
pub columns: Vec<Columns>,
|
|
156
|
+
pub validations: Vec<Validation>,
|
|
157
|
+
/// The cells with a format of their own, empty ones included.
|
|
158
|
+
pub styles: Vec<Styled>,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/// A run of columns, from 0, and their width in characters.
|
|
162
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
163
|
+
pub struct Columns {
|
|
164
|
+
pub first: u32,
|
|
165
|
+
pub last: u32,
|
|
166
|
+
pub width: Option<f64>,
|
|
167
|
+
pub hidden: bool,
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/// A data validation rule, as the workbook stores it.
|
|
171
|
+
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
|
172
|
+
pub struct Validation {
|
|
173
|
+
/// The cells it applies to: `B2:B100 D2`.
|
|
174
|
+
pub range: String,
|
|
175
|
+
/// `list`, `whole`, `decimal`, `date`, `time`, `textLength`, `custom`,
|
|
176
|
+
/// or `none` for an input message alone.
|
|
177
|
+
pub kind: String,
|
|
178
|
+
pub operator: Option<String>,
|
|
179
|
+
pub formula1: Option<String>,
|
|
180
|
+
pub formula2: Option<String>,
|
|
181
|
+
pub allow_blank: bool,
|
|
182
|
+
/// Whether a list shows its in-cell drop-down arrow.
|
|
183
|
+
pub dropdown: bool,
|
|
184
|
+
/// `stop`, `warning` or `information`, when a wrong value is refused.
|
|
185
|
+
pub alert: Option<String>,
|
|
186
|
+
pub error_title: Option<String>,
|
|
187
|
+
pub error: Option<String>,
|
|
188
|
+
pub prompt_title: Option<String>,
|
|
189
|
+
pub prompt: Option<String>,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/// A cell with a format of its own, from 0.
|
|
193
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
194
|
+
pub struct Styled {
|
|
195
|
+
pub row: u32,
|
|
196
|
+
pub column: u32,
|
|
197
|
+
pub look: Arc<Look>,
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
impl Styled {
|
|
201
|
+
/// The cell's A1 reference: `B2`.
|
|
202
|
+
#[must_use]
|
|
203
|
+
pub fn reference(&self) -> String {
|
|
204
|
+
reference(self.row, self.column)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/// How a cell format shows a cell. Colors are ARGB (`FFFF0000`), and only
|
|
209
|
+
/// those given as such: theme and indexed colors are left out.
|
|
210
|
+
#[derive(Debug, Clone, Default, PartialEq)]
|
|
211
|
+
#[expect(clippy::struct_excessive_bools, reason = "a format's switches, as Excel keeps them")]
|
|
212
|
+
pub struct Look {
|
|
213
|
+
pub font: String,
|
|
214
|
+
/// In points.
|
|
215
|
+
pub size: f64,
|
|
216
|
+
pub bold: bool,
|
|
217
|
+
pub italic: bool,
|
|
218
|
+
pub underline: bool,
|
|
219
|
+
pub color: Option<String>,
|
|
220
|
+
pub fill: Option<String>,
|
|
221
|
+
pub align: Option<String>,
|
|
222
|
+
pub valign: Option<String>,
|
|
223
|
+
pub wrap: bool,
|
|
224
|
+
/// The left border's style: `thin`, `medium`, `thick`, `dashed`...
|
|
225
|
+
pub border: Option<String>,
|
|
226
|
+
pub number_format: String,
|
|
139
227
|
}
|
|
140
228
|
|
|
141
229
|
/// A comment on a cell.
|