imprimatur 0.2.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 +22 -0
- data/Cargo.lock +123 -3
- data/Cargo.toml +1 -1
- data/README.md +98 -20
- data/ext/imprimatur_native/cli/Cargo.toml +1 -1
- data/ext/imprimatur_native/core/Cargo.toml +6 -1
- data/ext/imprimatur_native/core/README.md +12 -3
- data/ext/imprimatur_native/core/src/archive.rs +3 -16
- 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 +12 -0
- data/ext/imprimatur_native/core/src/font.rs +229 -0
- data/ext/imprimatur_native/core/src/lib.rs +3 -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 +75 -2
- data/ext/imprimatur_native/core/src/read/json.rs +94 -7
- data/ext/imprimatur_native/core/src/read/numbers.rs +65 -42
- data/ext/imprimatur_native/core/src/read/ods.rs +1 -2
- 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.rs +1 -1
- data/ext/imprimatur_native/core/src/read/xlsx/formats.rs +155 -0
- data/ext/imprimatur_native/core/src/read/xlsx.rs +104 -19
- data/ext/imprimatur_native/core/src/read.rs +4 -1
- 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 +3 -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 +1 -3
- data/ext/imprimatur_native/ffi/src/document.rs +48 -1
- data/ext/imprimatur_native/ffi/src/lib.rs +38 -1
- data/ext/imprimatur_native/ffi/src/read.rs +8 -11
- data/ext/imprimatur_native/ffi/src/sheets.rs +114 -0
- data/lib/imprimatur/document.rb +30 -6
- data/lib/imprimatur/reader.rb +2 -2
- data/lib/imprimatur/spreadsheet.rb +151 -0
- data/lib/imprimatur/version.rb +1 -1
- data/lib/imprimatur/xlsx.rb +25 -4
- data/lib/imprimatur.rb +15 -3
- metadata +11 -1
|
@@ -45,6 +45,9 @@ pub enum Error {
|
|
|
45
45
|
#[error("font size {0} is out of range (1 to 409 points, in steps of 0.5)")]
|
|
46
46
|
InvalidFontSize(f64),
|
|
47
47
|
|
|
48
|
+
#[error("unsupported font: {0}")]
|
|
49
|
+
UnsupportedFont(&'static str),
|
|
50
|
+
|
|
48
51
|
#[error("invalid cell range {0:?}: expected a reference such as B2 or B2:D9")]
|
|
49
52
|
InvalidRange(String),
|
|
50
53
|
|
|
@@ -66,6 +69,15 @@ pub enum Error {
|
|
|
66
69
|
#[error("a table needs at least one row and one column")]
|
|
67
70
|
EmptyTable,
|
|
68
71
|
|
|
72
|
+
#[error("invalid picture: {0}")]
|
|
73
|
+
InvalidImage(&'static str),
|
|
74
|
+
|
|
75
|
+
#[error("page size {0} x {1} points is out of range (36 to 1584 points a side)")]
|
|
76
|
+
InvalidPageSize(f64, f64),
|
|
77
|
+
|
|
78
|
+
#[error("margin {0} points is out of range (0 to 144), or leaves no room on the page")]
|
|
79
|
+
InvalidMargin(f64),
|
|
80
|
+
|
|
69
81
|
#[error(
|
|
70
82
|
"{character:?} (U+{code:04X}) cannot be written with the PDF standard font {font}: \
|
|
71
83
|
only Windows-1252 (Latin) text is supported until embedded fonts land",
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
//! Static TrueType font data, Unicode glyph lookup and unscaled metrics.
|
|
2
|
+
//!
|
|
3
|
+
//! This does not select or embed fonts in documents, shape text, or perform
|
|
4
|
+
//! bidirectional layout. A character's nominal glyph is not necessarily the
|
|
5
|
+
//! glyph used when shaping a script such as Arabic.
|
|
6
|
+
|
|
7
|
+
#[cfg(test)]
|
|
8
|
+
mod tests;
|
|
9
|
+
|
|
10
|
+
use read_fonts::{
|
|
11
|
+
FontRead, FontRef, MinByteRange, TableProvider,
|
|
12
|
+
tables::{
|
|
13
|
+
cmap::{Cmap, CmapSubtable, PlatformId},
|
|
14
|
+
hmtx::Hmtx,
|
|
15
|
+
},
|
|
16
|
+
types::{GlyphId, Tag, Version16Dot16},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
use crate::{Error, Result};
|
|
20
|
+
|
|
21
|
+
/// Horizontal line metrics in font design units, taken from `head` and `hhea`.
|
|
22
|
+
///
|
|
23
|
+
/// Multiply a metric by the point size and divide by `units_per_em` to convert
|
|
24
|
+
/// it to points. These are the raw `hhea` metrics, not an OS/2 line-height policy.
|
|
25
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
26
|
+
pub struct FontMetrics {
|
|
27
|
+
pub units_per_em: u16,
|
|
28
|
+
pub ascender: i16,
|
|
29
|
+
/// Usually negative: the position below the baseline.
|
|
30
|
+
pub descender: i16,
|
|
31
|
+
pub line_gap: i16,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// A borrowed, standalone static TrueType font with a Unicode character map.
|
|
35
|
+
///
|
|
36
|
+
/// Loading checks table bounds and the data needed for glyph lookup and metrics,
|
|
37
|
+
/// including the `loca` offsets into `glyf`. It is not a full font sanitizer:
|
|
38
|
+
/// checksums, outline programs and shaping tables are not validated.
|
|
39
|
+
#[derive(Clone)]
|
|
40
|
+
pub struct TrueTypeFont<'a> {
|
|
41
|
+
data: &'a [u8],
|
|
42
|
+
cmap: CmapSubtable<'a>,
|
|
43
|
+
hmtx: Hmtx<'a>,
|
|
44
|
+
metrics: FontMetrics,
|
|
45
|
+
glyph_count: u16,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
impl<'a> TrueTypeFont<'a> {
|
|
49
|
+
/// Load font data without copying it or accessing system fonts.
|
|
50
|
+
///
|
|
51
|
+
/// # Errors
|
|
52
|
+
/// [`Error::UnsupportedFont`] for collections, WOFF, CFF, variable fonts or
|
|
53
|
+
/// fonts without a supported Unicode cmap. [`Error::Malformed`] for invalid
|
|
54
|
+
/// headers, missing required tables or inconsistent metrics/outline offsets.
|
|
55
|
+
pub fn from_bytes(data: &'a [u8]) -> Result<Self> {
|
|
56
|
+
match data.get(..4) {
|
|
57
|
+
Some(b"\0\x01\0\0" | b"true") => {}
|
|
58
|
+
Some(b"ttcf") => return Err(Error::UnsupportedFont("font collections")),
|
|
59
|
+
Some(b"wOFF" | b"wOF2") => return Err(Error::UnsupportedFont("WOFF/WOFF2")),
|
|
60
|
+
Some(b"OTTO") => return Err(Error::UnsupportedFont("CFF outlines")),
|
|
61
|
+
_ => return Err(invalid("invalid or truncated sfnt header")),
|
|
62
|
+
}
|
|
63
|
+
let font = FontRef::new(data).map_err(invalid)?;
|
|
64
|
+
for record in font.table_directory().table_records() {
|
|
65
|
+
if u64::from(record.offset()) + u64::from(record.length()) > data.len() as u64 {
|
|
66
|
+
return Err(invalid(format!(
|
|
67
|
+
"{} table extends beyond the font data",
|
|
68
|
+
record.tag()
|
|
69
|
+
)));
|
|
70
|
+
}
|
|
71
|
+
match record.tag() {
|
|
72
|
+
tag if tag == Tag::new(b"fvar") || tag == Tag::new(b"gvar") => {
|
|
73
|
+
return Err(Error::UnsupportedFont("variable fonts"));
|
|
74
|
+
}
|
|
75
|
+
tag if tag == Tag::new(b"CFF ") || tag == Tag::new(b"CFF2") => {
|
|
76
|
+
return Err(Error::UnsupportedFont("CFF outlines"));
|
|
77
|
+
}
|
|
78
|
+
_ => {}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let head = font.head().map_err(invalid)?;
|
|
83
|
+
if head.magic_number() != 0x5F0F_3CF5 || !(16..=16384).contains(&head.units_per_em()) {
|
|
84
|
+
return Err(invalid("invalid head magic number or units per em"));
|
|
85
|
+
}
|
|
86
|
+
let maxp = font.maxp().map_err(invalid)?;
|
|
87
|
+
let glyph_count = maxp.num_glyphs();
|
|
88
|
+
if maxp.version() != Version16Dot16::VERSION_1_0
|
|
89
|
+
|| maxp.max_component_depth().is_none()
|
|
90
|
+
|| glyph_count == 0
|
|
91
|
+
{
|
|
92
|
+
return Err(invalid("expected a complete TrueType maxp table with at least one glyph"));
|
|
93
|
+
}
|
|
94
|
+
let hhea = font.hhea().map_err(invalid)?;
|
|
95
|
+
let count = hhea.number_of_h_metrics();
|
|
96
|
+
if count == 0 || count > glyph_count {
|
|
97
|
+
return Err(invalid("hhea metric count is outside the glyph range"));
|
|
98
|
+
}
|
|
99
|
+
let hmtx = font.hmtx().map_err(invalid)?;
|
|
100
|
+
if hmtx.h_metrics().len() != usize::from(count)
|
|
101
|
+
|| hmtx.left_side_bearings().len() < usize::from(glyph_count - count)
|
|
102
|
+
{
|
|
103
|
+
return Err(invalid("truncated hmtx table"));
|
|
104
|
+
}
|
|
105
|
+
validate_outlines(&font, head.index_to_loc_format(), glyph_count)?;
|
|
106
|
+
let cmap = font.cmap().map_err(invalid)?;
|
|
107
|
+
if cmap.version() != 0 || cmap.encoding_records().len() != usize::from(cmap.num_tables()) {
|
|
108
|
+
return Err(invalid("invalid or truncated cmap header"));
|
|
109
|
+
}
|
|
110
|
+
Ok(Self {
|
|
111
|
+
data,
|
|
112
|
+
cmap: unicode_cmap(&cmap)?,
|
|
113
|
+
hmtx,
|
|
114
|
+
metrics: FontMetrics {
|
|
115
|
+
units_per_em: head.units_per_em(),
|
|
116
|
+
ascender: hhea.ascender().into(),
|
|
117
|
+
descender: hhea.descender().into(),
|
|
118
|
+
line_gap: hhea.line_gap().into(),
|
|
119
|
+
},
|
|
120
|
+
glyph_count,
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// The original font bytes.
|
|
125
|
+
#[must_use]
|
|
126
|
+
pub const fn as_bytes(&self) -> &'a [u8] {
|
|
127
|
+
self.data
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// Unscaled horizontal line metrics.
|
|
131
|
+
#[must_use]
|
|
132
|
+
pub const fn metrics(&self) -> FontMetrics {
|
|
133
|
+
self.metrics
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/// The number of glyphs, including glyph zero (`.notdef`).
|
|
137
|
+
#[must_use]
|
|
138
|
+
pub const fn glyph_count(&self) -> u16 {
|
|
139
|
+
self.glyph_count
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// The nominal glyph for a Unicode character, without shaping or fallback.
|
|
143
|
+
///
|
|
144
|
+
/// Returns `None` for missing characters, mappings to `.notdef`, or mappings
|
|
145
|
+
/// outside the font's glyph range. Symbol and legacy cmaps are never used as
|
|
146
|
+
/// Unicode mappings; full-repertoire Unicode cmaps take priority over BMP ones.
|
|
147
|
+
#[must_use]
|
|
148
|
+
pub fn glyph_id(&self, character: char) -> Option<u16> {
|
|
149
|
+
let glyph = self.cmap.map_codepoint(character)?.to_u32();
|
|
150
|
+
u16::try_from(glyph).ok().filter(|&id| id != 0 && id < self.glyph_count)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/// The horizontal advance in font design units, without kerning or shaping.
|
|
154
|
+
///
|
|
155
|
+
/// Glyph zero is allowed; an out-of-range glyph returns `None`. A valid
|
|
156
|
+
/// zero-width glyph returns `Some(0)`.
|
|
157
|
+
#[must_use]
|
|
158
|
+
pub fn advance_width(&self, glyph_id: u16) -> Option<u16> {
|
|
159
|
+
if glyph_id >= self.glyph_count {
|
|
160
|
+
return None;
|
|
161
|
+
}
|
|
162
|
+
self.hmtx.advance(GlyphId::new(u32::from(glyph_id)))
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
fn validate_outlines(font: &FontRef<'_>, loca_format: i16, glyph_count: u16) -> Result<()> {
|
|
167
|
+
if !matches!(loca_format, 0 | 1) {
|
|
168
|
+
return Err(invalid("invalid head indexToLocFormat"));
|
|
169
|
+
}
|
|
170
|
+
let loca = font.loca(loca_format == 1).map_err(invalid)?;
|
|
171
|
+
let glyf = font.glyf().map_err(invalid)?;
|
|
172
|
+
if loca.len() != usize::from(glyph_count)
|
|
173
|
+
|| !loca.all_offsets_are_ascending()
|
|
174
|
+
|| loca
|
|
175
|
+
.get_raw(usize::from(glyph_count))
|
|
176
|
+
.is_none_or(|end| u64::from(end) > glyf.offset_data().len() as u64)
|
|
177
|
+
{
|
|
178
|
+
return Err(invalid("invalid loca offsets or truncated glyf table"));
|
|
179
|
+
}
|
|
180
|
+
Ok(())
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
fn unicode_cmap<'a>(cmap: &Cmap<'a>) -> Result<CmapSubtable<'a>> {
|
|
184
|
+
// Fontations' best_subtable also accepts symbol and MacRoman maps. Only
|
|
185
|
+
// Unicode encodings belong here; prefer full repertoire over BMP mappings.
|
|
186
|
+
let encodings = [
|
|
187
|
+
(PlatformId::Windows, 10),
|
|
188
|
+
(PlatformId::Unicode, 6),
|
|
189
|
+
(PlatformId::Unicode, 4),
|
|
190
|
+
(PlatformId::Windows, 1),
|
|
191
|
+
(PlatformId::Unicode, 3),
|
|
192
|
+
(PlatformId::Unicode, 2),
|
|
193
|
+
(PlatformId::Unicode, 1),
|
|
194
|
+
(PlatformId::Unicode, 0),
|
|
195
|
+
];
|
|
196
|
+
for (platform, encoding) in encodings {
|
|
197
|
+
for record in cmap.encoding_records() {
|
|
198
|
+
if record.platform_id() != platform || record.encoding_id() != encoding {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
let subtable = record.subtable(cmap.offset_data()).map_err(invalid)?;
|
|
202
|
+
let length = match &subtable {
|
|
203
|
+
CmapSubtable::Format0(table) => u32::from(table.length()),
|
|
204
|
+
CmapSubtable::Format4(table) => u32::from(table.length()),
|
|
205
|
+
CmapSubtable::Format6(table) => u32::from(table.length()),
|
|
206
|
+
CmapSubtable::Format10(table) => table.length(),
|
|
207
|
+
CmapSubtable::Format12(table) => table.length(),
|
|
208
|
+
CmapSubtable::Format13(table) => table.length(),
|
|
209
|
+
_ => continue,
|
|
210
|
+
};
|
|
211
|
+
let length = usize::try_from(length).map_err(|_| invalid("cmap subtable too large"))?;
|
|
212
|
+
let data = subtable
|
|
213
|
+
.offset_data()
|
|
214
|
+
.slice(0..length)
|
|
215
|
+
.ok_or_else(|| invalid("truncated cmap subtable"))?;
|
|
216
|
+
// Constrain lazy array reads to this subtable's declared extent.
|
|
217
|
+
let subtable = CmapSubtable::read(data).map_err(invalid)?;
|
|
218
|
+
if subtable.min_byte_range().end > length {
|
|
219
|
+
return Err(invalid("truncated cmap arrays"));
|
|
220
|
+
}
|
|
221
|
+
return Ok(subtable);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
Err(Error::UnsupportedFont("no supported Unicode cmap"))
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
fn invalid(reason: impl std::fmt::Display) -> Error {
|
|
228
|
+
Error::malformed("TrueType font", reason.to_string())
|
|
229
|
+
}
|
|
@@ -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
|
+
}
|